Types/OpenAIClient.ps1

class OpenAIClient {
    [System.Management.Automation.HiddenAttribute()]
    [string]$apikey
    [System.Management.Automation.HiddenAttribute()]
    [string]$baseUri
    [System.Management.Automation.HiddenAttribute()]
    [string]$model
    [System.Management.Automation.HiddenAttribute()]
    [hashtable]$headers
    [System.Management.Automation.HiddenAttribute()]
    [string]$apiVersion

    [Assistant]$assistants
    [Vector_store]$vector_stores
    [File]$files
    [Thread]$threads

    OpenAIClient([string]$apiKey, [string]$baseUri, [string]$model, [string]$apiVersion) {
        $this.apikey = $apiKey
        $this.baseUri = $baseUri
        $this.model = $model
        $this.apiVersion = $apiVersion

        $this.init()
    }
    [System.Management.Automation.HiddenAttribute()]
    [void]init() {
        # check the apikey, endpoint and model, if empty, then return error
        $this.headers = @{
            "OpenAI-Beta" = "assistants=v2"
        }

        if ($this.baseUri -match "azure") {
            $this.headers.Add("api-key", $this.apikey)
            $this.baseUri = $this.baseUri + "openai/"
        }
        else {
            $this.headers.Add("Authorization", "Bearer $($this.apikey)")
            $this.apiVersion = ""
        }

        $this.assistants = [Assistant]::new($this)
        $this.vector_stores = [Vector_store]::new($this)
        $this.files = [File]::new($this)
        $this.threads = [Thread]::new($this)
    }

    [psobject]web(
        [string]$urifragment, 
        [string]$method = "GET", 
        [psobject]$body = $null) {
        $url = "{0}{1}" -f $this.baseUri, $urifragment
        if ($this.apiVersion -ne "") {
            if ($url -match "\?") {
                $url = "{0}&api-version={1}" -f $url, $this.apiVersion
            }
            else {
                $url = "{0}?api-version={1}" -f $url, $this.apiVersion
            }
        }

        if ($method -eq "GET" -or $null -eq $body) {
            $params = @{
                Method  = $method
                Uri     = $url
                Headers = $this.headers
            }
            return $this.unicodeiwr($params)
        }
        else {

            $params = @{
                Method  = $method
                Uri     = $url
                Headers = $this.headers
                Body    = ($body | ConvertTo-Json -Depth 10)
            }
            return $this.unicodeiwr($params)
        }
    }

    [System.Management.Automation.HiddenAttribute()]
    [psobject]unicodeiwr([hashtable]$params) {
        $oldProgressPreference = Get-Variable -Name ProgressPreference -ValueOnly
        Set-Variable -Name ProgressPreference -Value "SilentlyContinue" -Scope Script -Force
        $response = Invoke-WebRequest @params -ContentType "application/json;charset=utf-8"
        Set-Variable -Name ProgressPreference -Value $oldProgressPreference -Scope Script -Force

        $contentType = $response.Headers["Content-Type"]
        $version = Get-Variable -Name PSVersionTable -ValueOnly
        if ($version.PSVersion.Major -gt 5 -or $contentType -match 'charset=utf-8') {
            return $response.Content | ConvertFrom-Json
        }
        else {
            $response = $response.Content
            $charset = if ($contentType -match "charset=([^;]+)") { $matches[1] } else { "ISO-8859-1" } 
            $dstEncoding = [System.Text.Encoding]::GetEncoding($charset)
            $srcEncoding = [System.Text.Encoding]::UTF8
            $result = $srcEncoding.GetString([System.Text.Encoding]::Convert($srcEncoding, $dstEncoding, $srcEncoding.GetBytes($response)))
            return $result | ConvertFrom-Json
        }
    }

    [psobject]web($urifragment) {
        return $this.web($urifragment, "GET", @{})
    }
}

class AssistantResource {
    [System.Management.Automation.HiddenAttribute()]
    [OpenAIClient]$client
    [System.Management.Automation.HiddenAttribute()]
    [string]$urifragment
    [System.Management.Automation.HiddenAttribute()]
    [string]$objTypeName

    AssistantResource([OpenAIClient]$client, [string]$urifragment, [string]$objTypeName) {
        $this.client = $client
        $this.urifragment = $urifragment
        $this.objTypeName = $objTypeName
    }
    [psobject[]]list() {

        if ($this.objTypeName) {
            return $this.client.web($this.urifragment).data | ForEach-Object {
                $result = New-Object -TypeName $this.objTypeName -ArgumentList $_
                $result | Add-Member -MemberType NoteProperty -Name client -Value $this.client
                $result
            }
        }

        return $this.client.web($this.urifragment).data
    }
    
    [psobject]get([string]$id) {
        if ($this.objTypeName) {
            $result = New-Object -TypeName $this.objTypeName -ArgumentList $this.client.web("$($this.urifragment)/$id")
            $result | Add-Member -MemberType NoteProperty -Name client -Value $this.client
            return $result
        }

        return $this.client.web("$($this.urifragment)/$id")
    }

    [psobject]delete([string]$id) {
        return $this.client.web("$($this.urifragment)/$id", "DELETE", @{})
    }

    [psobject]create([hashtable]$body) {
        if ($this.objTypeName) {
            $result = New-Object -TypeName $this.objTypeName -ArgumentList $this.client.web("$($this.urifragment)", "POST", $body)
            $result | Add-Member -MemberType NoteProperty -Name client -Value $this.client
            return $result
        }
        return $this.client.web("$($this.urifragment)", "POST", $body)
    }

    [psobject]create() {
        return $this.create(@{})
    }

    
    [void]clear() {
        # warn user this is very dangerous action, it will remove all the instance, and ask for confirmation
        $confirm = Read-Host "Are you sure you want to remove all the instances? (yes/no)"
        if ($confirm -ne "yes" -and $confirm -ne "y") {
            return
        }
        # get all the instances and remove it
        $this.list() | ForEach-Object {
            $this.delete($_.id)
        }
    }
}

class File:AssistantResource {
    File([OpenAIClient]$client): base($client, "files", $null) {}

    [psobject]create([hashtable]$body) {
        if ($body.files) {
            $files = $body.files
            return $this.upload($files)
        }

        throw "The body must contain 'files' key."
    }


    [System.Management.Automation.HiddenAttribute()]
    [psobject]upload([string[]]$fullname) {

        # process the input, if it is a wildcard or a folder, then get all the files based on this pattern
        $fullname = $fullname | Get-ChildItem | Select-Object -ExpandProperty FullName
        # read all the files and check the filename, compute
        $existing_files = $this.list() | Select-Object id, @{l = "hash"; e = { $_.filename.split("-")[0] } }
        $localfiles = $fullname | Select-Object @{l = "fullname"; e = { $_ } }, @{l = "hash"; e = { (Get-FileHash $_).Hash } }
        $result = @(
            $existing_files | Where-Object {
                $_.hash -in $localfiles.hash
            }
        )

        $fullname = $localfiles | Where-Object {
            $_.hash -notin $existing_files.hash
        } | Select-Object -ExpandProperty fullname


        if ($fullname.Count -gt 0) {
            # confirm if user want to upload those files to openai
            $confirm = Read-Host "Are you sure you want to upload the $($fullname.Count) files? (yes/no)"
            if ($confirm -ne "yes" -and $confirm -ne "y") {
                throw "The user canceled the operation."
            }


            $url = "{0}{1}" -f $this.client.baseUri, $this.urifragment
            if ($this.client.baseUri -match "azure") {
                $url = "{0}?api-version=2024-05-01-preview" -f $url
            }

        

            foreach ($file in $fullname) {
                Write-Host "process file: $file"
                # Define the purpose (e.g., "assistants", "vision", "batch", or "fine-tune")
                $purpose = "assistants"
                # Create a new web request
                $request = [System.Net.WebRequest]::Create($url)
                $request.Method = "POST"

                # add the item of headers to request.Headers
                $this.client.headers.GetEnumerator() | Where-Object {
                    $_.Key -ne "Content-Type"
                }  | ForEach-Object {
                    $request.Headers.Add($_.Key, $_.Value)
                }

                # Create a boundary for the multipart/form-data content
                $boundary = [System.Guid]::NewGuid().ToString()

                # Set the content type and boundary
                $request.ContentType = "multipart/form-data; boundary=$boundary"

                $name = "{0}-{1}" -f (Get-FileHash $file).Hash, (Split-Path $file -Leaf)

                # Create the request body
                $body = @"
--$boundary
Content-Disposition: form-data; name="file"; filename="$name"
Content-Type: application/octet-stream
 
$(Get-Content -Path $file)
 
--$boundary
Content-Disposition: form-data; name="purpose"
 
$purpose
--$boundary--
"@



                # Convert the body to bytes
                $bodyBytes = [System.Text.Encoding]::UTF8.GetBytes($body)

                # Set the content length
                $request.ContentLength = $bodyBytes.Length

                # Get the request stream and write the body
                $requestStream = $request.GetRequestStream()
                $requestStream.Write($bodyBytes, 0, $bodyBytes.Length)
                $requestStream.Close()

                # Get the response
                $response = $request.GetResponse()

                # Read the response content
                $responseStream = $response.GetResponseStream()
                $reader = [System.IO.StreamReader]::new($responseStream)
                $responseContent = $reader.ReadToEnd()
                $reader.Close()

                # Print the response content
                $result += ($responseContent | ConvertFrom-Json)
            }
        }



        return $result
    }

  
}

class Assistant:AssistantResource {
    Assistant([OpenAIClient]$client): base($client, "assistants", "AssistantObject") {}


    <#
        .SYNOPSIS
            Create a new assistant
        .DESCRIPTION
            Create a new assistant with the given name, model, and instructions.
        .PARAMETER body
            The body must contain 'name', 'model', and 'instructions' keys. But it can also contain 'config', 'vector_store_ids', 'functions', and 'files' keys.
    #>

    [AssistantObject]create([hashtable]$body) {
        if ($body.name -and $body.model -and $body.instructions) {
            $vector_store_ids = $body.vector_store_ids
            $functions = $body.functions
            $files = $body.files
            $config = $body.config

            if ($files) {
                # upload the files and create new vector store
                $file_ids = $this.client.files.create(@{ "files" = $files }) | Select-Object -ExpandProperty id 
                $body.Add("tools", @(
                        @{
                            "type" = "file_search"
                        }))
                        
                $body.Add("tool_resources", @{
                        "file_search" = @{
                            "vector_stores" = @(
                                @{
                                    file_ids = @($file_ids)
                                })
                        }
                    })
                
            }

            if ($vector_store_ids -and $vector_store_ids.Count -gt 0) {
                $body.Add("tool_resources", @{
                        "file_search" = @{
                            "vector_store_ids" = @($vector_store_ids)
                        }
                    })
                
                $body.Add("tools", @(
                        @{
                            "type" = "file_search"
                        }))
            }

            if ($functions -and $functions.Count -gt 0) {
        
                if ($null -eq $body.tools) {
                    $body.Add("tools", @())
                }

                $functions | ForEach-Object {
                    $func = Get-FunctionJson -functionName $_
                    $body.tools += $func
                }
            }

            if ($config) {
                Merge-Hashtable -table1 $body -table2 $config
            }

            # remove files, vector_store_ids, functions, and config from the body
            $body.Remove("files")
            $body.Remove("vector_store_ids")
            $body.Remove("functions")
            $body.Remove("config")
            
            $result = [AssistantObject]::new($this.client.web("$($this.urifragment)", "POST", $body)) 
            $result | Add-Member -MemberType NoteProperty -Name client -Value $this.client
            return $result
        }
        
        throw "The body must contain 'name' and 'model', 'instructions' keys."
    }
}


class AssistantResourceObject {
    AssistantResourceObject([psobject]$data) {
        # check all the properties and assign it to the object
        $data.PSObject.Properties | ForEach-Object {
            $this | Add-Member -MemberType NoteProperty -Name $_.Name -Value $_.Value
        }
    }
}

class AssistantObject:AssistantResourceObject {
    [ThreadObject]$thread
        
    AssistantObject([psobject]$data):base($data) {}

    [void]chat([bool]$clean = $false) {
        if (-not $this.thread) {
            # create a thread, and associate the assistant id
            $this.thread = $this.client.threads.create($this.id)
        }

        try {
            while ($true) {
                # ask use to input, until the user type 'q' or 'bye'
                $prompt = Read-Host ">"
                if ($prompt -eq "q" -or $prompt -eq "bye") {
                    break
                }

                # send the message to the thread
                $response = $this.thread.send($prompt).run().get_last_message()

                if ($response) {
                    Write-Host $response -ForegroundColor Green
                }
            }
        }
        finally {
            $this.client.threads.delete($this.thread.id)
            if ($clean) {
                Write-Host "clean up the thread, assistant, and vector_store..." -ForegroundColor Yellow
                # clean up the thread, assistant, and vector_store
                $vc_id = $this.tool_resources.file_search.vector_store_ids[0]
                $this.client.vector_stores.delete($vc_id)
                $this.client.assistants.delete($this.id)
            }
        }

    }
}


class ThreadObject:AssistantResourceObject {
    ThreadObject([psobject]$data):base($data) {}

    [ThreadObject]send([string]$message) {
        # send a message
        [AssistantResource]::new($this.client, ("threads/{0}/messages" -f $this.id), $null ).create(@{
                role    = "user"
                content = $message
            }) | Out-Null
            
        return $this
    }

    [ThreadObject]run([string]$assistantId) {
        $obj = [AssistantResource]::new($this.client, ("threads/{0}/runs" -f $this.id), $null ).create(@{assistant_id = $assistantId })
        if ($null -eq $this.last_run_id) {
            $this | Add-Member -MemberType NoteProperty -Name last_run_id -Value $obj.id
        }
        else {
            $this.last_run_id = $obj.id
        }
        return $this
    }

    [ThreadObject]run() {
        return $this.run($this.assistant_id)
    }

    [string]get_last_message() {
        # check if the last_run is set, if not, then return null
        if ($this.last_run_id) {
            $run = [AssistantResource]::new($this.client, ("threads/{0}/runs" -f $this.id), $null ).get($this.last_run_id)
            while ($run.status -ne "completed") {
                Write-Verbose ("Run status: {0}" -f $run.status)

                if ($run.status -eq "failed") {
                    Write-Host ("Run failed: {0}" -f $run.last_error.message) -ForegroundColor Red
                    break
                }

                # The status of the run, which can be either queued, in_progress, requires_action, cancelling, cancelled, failed, completed, incomplete, or expired.

                if ($run.status -eq "requires_action") {
                    $tool_calls = $run.required_action.submit_tool_outputs.tool_calls
                    $tool_output = @()

                    if ($tool_calls -and $tool_calls.Count -gt 0) {

                        foreach ($tool_call in $tool_calls) {
                            $call_id = $tool_call.id
                            $function = $tool_call.function
                            $function_args = $function.arguments | ConvertFrom-Json
                            $exp = "{0} {1}" -f $function.name, (($function_args.PSObject.Properties | ForEach-Object {
                                        "-{0} '{1}'" -f $_.Name, $_.Value
                                    }) -join " ")
                            Write-Verbose "calling function with arguments: $exp"
                            $call_response = Invoke-Expression $exp

                            $tool_output += @{
                                tool_call_id = $call_id
                                output       = $call_response
                            }
                        }
                    }
                    [AssistantResource]::new($this.client, ("threads/{0}/runs/{1}/submit_tool_outputs" -f $this.id, $this.last_run_id), $null ).create(@{tool_outputs = $tool_output })

                }
        
                Start-Sleep -Milliseconds 500
                $run = [AssistantResource]::new($this.client, ("threads/{0}/runs" -f $this.id), $null ).get($this.last_run_id)
            }


            $message = [AssistantResource]::new($this.client, ("threads/{0}/messages?limit=1" -f $this.id), $null).list() | Select-Object id, role, content -First 1

            return $message.content.text.value
        }

        return $null
    }
}
class Thread:AssistantResource {
    Thread([OpenAIClient]$client): base($client, "threads", "ThreadObject") {}

    [psobject[]]list() {
        return @{
            error = "It is not implemented yet, you can't get all the thread information."
        }
    }

    [ThreadObject]create([string]$assistantId) {
        $result = $this.create()
        $result | Add-Member -MemberType NoteProperty -Name assistant_id -Value $assistantId
        return $result
    }
}

class Vector_storeObject:AssistantResourceObject {
    Vector_storeObject([psobject]$data):base($data) {}

    [string[]]file_ids() {
        return $this.client.web("vector_stores/$($this.id)/files").data | Select-Object -ExpandProperty id
    }
}

class Vector_store:AssistantResource {
    Vector_store([OpenAIClient]$client): base($client, "vector_stores", "Vector_storeObject") {}

    [psobject]create([hashtable]$body) {
        <#
            .SYNOPSIS
                Create a new vector store
            .DESCRIPTION
                Create a new vector store with the given name, file_ids, and days_to_expire.
            .PARAMETER body
                The body must contain 'name', 'file_ids', and 'days_to_expire' keys.
        #>


        # check if the body contains name, file_ids, and days_to_expire
        if ($body.name -and $body.file_ids -and $body.days_to_expire) {
            #replace the days_to_expire with expires_after
            $body.expires_after = @{
                "days"   = $body.days_to_expire
                "anchor" = "last_active_at"
            }
            $body.Remove("days_to_expire")
            return $this.client.web("$($this.urifragment)", "POST", $body)
        }
        
        throw "The body must contain 'name', 'file_ids', and 'days_to_expire' keys."
    }
}
# SIG # Begin signature block
# MIIc/gYJKoZIhvcNAQcCoIIc7zCCHOsCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCxIDrizqrTpJSe
# Ge0UsOYei+OGq6lJzHS/29dDqih2n6CCAyowggMmMIICDqADAgECAhBcsg5m3zM9
# kUZxmeNzIQNjMA0GCSqGSIb3DQEBCwUAMCoxKDAmBgNVBAMMH0NIRU5YSVpIQU5H
# IC0gQ29kZSBTaWduaW5nIENlcnQwIBcNMjQwMTA4MTMwMjA0WhgPMjA5OTEyMzEx
# NjAwMDBaMCoxKDAmBgNVBAMMH0NIRU5YSVpIQU5HIC0gQ29kZSBTaWduaW5nIENl
# cnQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDKDY3QG81JOKZG9jTb
# QriDMDhq6gy93Pmoqgav9wErj+CgVvXKk+lGpUu74MWVyLUrJx8/ACb4b287wsXx
# mQj8zQ3SqGn5CCjPKoAPsSbry0LOSl8bsFpwBr3YBJVL6cibhus2KLCbNu/u7sND
# wyivKXYA1Iy1uTQPNVPcBx36krZTZyyE4CmngO75YbTMEzvHEjM3BIXdKtEt673t
# iNOVSP6doh0zRwWEh2Y/eoOpv+FUokORwhKonxMtmIIET+ZPx7Ex+9aqHrliEabx
# FsN4ETnuVT3rST++7Q2fquWFnl5scDnisFhU8JL8k+OGUzpLlo/nOpiRZkbKCEkZ
# FCLhAgMBAAGjRjBEMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcD
# AzAdBgNVHQ4EFgQUwcR3UUOZ6TxpBp9MxnBygyIMhUQwDQYJKoZIhvcNAQELBQAD
# ggEBADwiE9nowKxUNN84BTk9an1ZkdU95ouj+q6MRbafH08u4XV7CxXpkPR8Za/c
# BJWTOqCuz9pMPo0TylqWPm+++Tqy1OJ7Qewvy1+DXPuFGkTqY721uZ+YsHY3CueC
# VSRZRNsWSYE9UxXXFRsjDu/M3+EvyaNDE4xQkwrP8obFJoHq7WaOCCD2wMbKjLb5
# bS/VgtOK7Yn9pU/ghrW+Em+zHOX87wNRh/I5jd+LsnY8bR6REzgdmogIyvD4dsJD
# /IZLxRtbm2BHOn/aGBdu+GpEaYEEb6VkWcJhrQnpiNjjlu43CbRz5Bw14XPWGUDH
# +EkUqkWS4h8zsRiyvR9Pnwklg6UxghkqMIIZJgIBATA+MCoxKDAmBgNVBAMMH0NI
# RU5YSVpIQU5HIC0gQ29kZSBTaWduaW5nIENlcnQCEFyyDmbfMz2RRnGZ43MhA2Mw
# DQYJYIZIAWUDBAIBBQCgfDAQBgorBgEEAYI3AgEMMQIwADAZBgkqhkiG9w0BCQMx
# DAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkq
# hkiG9w0BCQQxIgQgRl8SJUCYuG9ikPHfvBBk1cNvQGzRxhetRD5I8QRqiuAwDQYJ
# KoZIhvcNAQEBBQAEggEATTqJwyjQwdu4IQF5R4ZlqpBvhvGK3yCVIDdl3dAOIGsu
# lTF9UTnBlBn0S9v6iLOs5XjWnRSraXaATuL4W0oZ1QfDbpQUseP8bJMIviYJKieX
# FSGCAtMhmQfY5heLcL0CNhcVxrinEHrxH+0Os821QXDzdoWJg7PasVeQLW/8gCGu
# rkkA/pxqjg5j1RYQMYVID5xu8fJP/6rya/TwxJ+PH+rZLSeADwaedN4H9wsxKNCp
# YHOGRlfIxpniaTxU1cgzONT9d2yh6U4raGNeUqCE2KYuoxOgEPdTnqR06arfeR83
# jGjmHpBRC/9VoZWFhf27wwVynOe1SCXR93emoFJpi6GCFz8wghc7BgorBgEEAYI3
# AwMBMYIXKzCCFycGCSqGSIb3DQEHAqCCFxgwghcUAgEDMQ8wDQYJYIZIAWUDBAIB
# BQAwdwYLKoZIhvcNAQkQAQSgaARmMGQCAQEGCWCGSAGG/WwHATAxMA0GCWCGSAFl
# AwQCAQUABCBehkGod3HSEyg/KtU9m1UfJhtACx3/96jOgs/i5J2VTwIQLXD1Nldg
# kkDkPVmkuoTyaBgPMjAyNDA2MTcxNDE0MDVaoIITCTCCBsIwggSqoAMCAQICEAVE
# r/OUnQg5pr/bP1/lYRYwDQYJKoZIhvcNAQELBQAwYzELMAkGA1UEBhMCVVMxFzAV
# BgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBUcnVzdGVk
# IEc0IFJTQTQwOTYgU0hBMjU2IFRpbWVTdGFtcGluZyBDQTAeFw0yMzA3MTQwMDAw
# MDBaFw0zNDEwMTMyMzU5NTlaMEgxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdp
# Q2VydCwgSW5jLjEgMB4GA1UEAxMXRGlnaUNlcnQgVGltZXN0YW1wIDIwMjMwggIi
# MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCjU0WHHYOOW6w+VLMj4M+f1+XS
# 512hDgncL0ijl3o7Kpxn3GIVWMGpkxGnzaqyat0QKYoeYmNp01icNXG/OpfrlFCP
# HCDqx5o7L5Zm42nnaf5bw9YrIBzBl5S0pVCB8s/LB6YwaMqDQtr8fwkklKSCGtpq
# utg7yl3eGRiF+0XqDWFsnf5xXsQGmjzwxS55DxtmUuPI1j5f2kPThPXQx/ZILV5F
# dZZ1/t0QoRuDwbjmUpW1R9d4KTlr4HhZl+NEK0rVlc7vCBfqgmRN/yPjyobutKQh
# ZHDr1eWg2mOzLukF7qr2JPUdvJscsrdf3/Dudn0xmWVHVZ1KJC+sK5e+n+T9e3M+
# Mu5SNPvUu+vUoCw0m+PebmQZBzcBkQ8ctVHNqkxmg4hoYru8QRt4GW3k2Q/gWEH7
# 2LEs4VGvtK0VBhTqYggT02kefGRNnQ/fztFejKqrUBXJs8q818Q7aESjpTtC/XN9
# 7t0K/3k0EH6mXApYTAA+hWl1x4Nk1nXNjxJ2VqUk+tfEayG66B80mC866msBsPf7
# Kobse1I4qZgJoXGybHGvPrhvltXhEBP+YUcKjP7wtsfVx95sJPC/QoLKoHE9nJKT
# BLRpcCcNT7e1NtHJXwikcKPsCvERLmTgyyIryvEoEyFJUX4GZtM7vvrrkTjYUQfK
# lLfiUKHzOtOKg8tAewIDAQABo4IBizCCAYcwDgYDVR0PAQH/BAQDAgeAMAwGA1Ud
# EwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwIAYDVR0gBBkwFzAIBgZn
# gQwBBAIwCwYJYIZIAYb9bAcBMB8GA1UdIwQYMBaAFLoW2W1NhS9zKXaaL3WMaiCP
# nshvMB0GA1UdDgQWBBSltu8T5+/N0GSh1VapZTGj3tXjSTBaBgNVHR8EUzBRME+g
# TaBLhklodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRS
# U0E0MDk2U0hBMjU2VGltZVN0YW1waW5nQ0EuY3JsMIGQBggrBgEFBQcBAQSBgzCB
# gDAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFgGCCsGAQUF
# BzAChkxodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVk
# RzRSU0E0MDk2U0hBMjU2VGltZVN0YW1waW5nQ0EuY3J0MA0GCSqGSIb3DQEBCwUA
# A4ICAQCBGtbeoKm1mBe8cI1PijxonNgl/8ss5M3qXSKS7IwiAqm4z4Co2efjxe0m
# gopxLxjdTrbebNfhYJwr7e09SI64a7p8Xb3CYTdoSXej65CqEtcnhfOOHpLawkA4
# n13IoC4leCWdKgV6hCmYtld5j9smViuw86e9NwzYmHZPVrlSwradOKmB521BXIxp
# 0bkrxMZ7z5z6eOKTGnaiaXXTUOREEr4gDZ6pRND45Ul3CFohxbTPmJUaVLq5vMFp
# GbrPFvKDNzRusEEm3d5al08zjdSNd311RaGlWCZqA0Xe2VC1UIyvVr1MxeFGxSjT
# redDAHDezJieGYkD6tSRN+9NUvPJYCHEVkft2hFLjDLDiOZY4rbbPvlfsELWj+MX
# kdGqwFXjhr+sJyxB0JozSqg21Llyln6XeThIX8rC3D0y33XWNmdaifj2p8flTzU8
# AL2+nCpseQHc2kTmOt44OwdeOVj0fHMxVaCAEcsUDH6uvP6k63llqmjWIso765qC
# NVcoFstp8jKastLYOrixRoZruhf9xHdsFWyuq69zOuhJRrfVf8y2OMDY7Bz1tqG4
# QyzfTkx9HmhwwHcK1ALgXGC7KP845VJa1qwXIiNO9OzTF/tQa/8Hdx9xl0RBybhG
# 02wyfFgvZ0dl5Rtztpn5aywGRu9BHvDwX+Db2a2QgESvgBBBijCCBq4wggSWoAMC
# AQICEAc2N7ckVHzYR6z9KGYqXlswDQYJKoZIhvcNAQELBQAwYjELMAkGA1UEBhMC
# VVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0
# LmNvbTEhMB8GA1UEAxMYRGlnaUNlcnQgVHJ1c3RlZCBSb290IEc0MB4XDTIyMDMy
# MzAwMDAwMFoXDTM3MDMyMjIzNTk1OVowYzELMAkGA1UEBhMCVVMxFzAVBgNVBAoT
# DkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBUcnVzdGVkIEc0IFJT
# QTQwOTYgU0hBMjU2IFRpbWVTdGFtcGluZyBDQTCCAiIwDQYJKoZIhvcNAQEBBQAD
# ggIPADCCAgoCggIBAMaGNQZJs8E9cklRVcclA8TykTepl1Gh1tKD0Z5Mom2gsMyD
# +Vr2EaFEFUJfpIjzaPp985yJC3+dH54PMx9QEwsmc5Zt+FeoAn39Q7SE2hHxc7Gz
# 7iuAhIoiGN/r2j3EF3+rGSs+QtxnjupRPfDWVtTnKC3r07G1decfBmWNlCnT2exp
# 39mQh0YAe9tEQYncfGpXevA3eZ9drMvohGS0UvJ2R/dhgxndX7RUCyFobjchu0Cs
# X7LeSn3O9TkSZ+8OpWNs5KbFHc02DVzV5huowWR0QKfAcsW6Th+xtVhNef7Xj3OT
# rCw54qVI1vCwMROpVymWJy71h6aPTnYVVSZwmCZ/oBpHIEPjQ2OAe3VuJyWQmDo4
# EbP29p7mO1vsgd4iFNmCKseSv6De4z6ic/rnH1pslPJSlRErWHRAKKtzQ87fSqEc
# azjFKfPKqpZzQmiftkaznTqj1QPgv/CiPMpC3BhIfxQ0z9JMq++bPf4OuGQq+nUo
# JEHtQr8FnGZJUlD0UfM2SU2LINIsVzV5K6jzRWC8I41Y99xh3pP+OcD5sjClTNfp
# mEpYPtMDiP6zj9NeS3YSUZPJjAw7W4oiqMEmCPkUEBIDfV8ju2TjY+Cm4T72wnSy
# Px4JduyrXUZ14mCjWAkBKAAOhFTuzuldyF4wEr1GnrXTdrnSDmuZDNIztM2xAgMB
# AAGjggFdMIIBWTASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBS6FtltTYUv
# cyl2mi91jGogj57IbzAfBgNVHSMEGDAWgBTs1+OC0nFdZEzfLmc/57qYrhwPTzAO
# BgNVHQ8BAf8EBAMCAYYwEwYDVR0lBAwwCgYIKwYBBQUHAwgwdwYIKwYBBQUHAQEE
# azBpMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQQYIKwYB
# BQUHMAKGNWh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0
# ZWRSb290RzQuY3J0MEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwzLmRpZ2lj
# ZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRSb290RzQuY3JsMCAGA1UdIAQZMBcwCAYG
# Z4EMAQQCMAsGCWCGSAGG/WwHATANBgkqhkiG9w0BAQsFAAOCAgEAfVmOwJO2b5ip
# RCIBfmbW2CFC4bAYLhBNE88wU86/GPvHUF3iSyn7cIoNqilp/GnBzx0H6T5gyNgL
# 5Vxb122H+oQgJTQxZ822EpZvxFBMYh0MCIKoFr2pVs8Vc40BIiXOlWk/R3f7cnQU
# 1/+rT4osequFzUNf7WC2qk+RZp4snuCKrOX9jLxkJodskr2dfNBwCnzvqLx1T7pa
# 96kQsl3p/yhUifDVinF2ZdrM8HKjI/rAJ4JErpknG6skHibBt94q6/aesXmZgaNW
# hqsKRcnfxI2g55j7+6adcq/Ex8HBanHZxhOACcS2n82HhyS7T6NJuXdmkfFynOlL
# AlKnN36TU6w7HQhJD5TNOXrd/yVjmScsPT9rp/Fmw0HNT7ZAmyEhQNC3EyTN3B14
# OuSereU0cZLXJmvkOHOrpgFPvT87eK1MrfvElXvtCl8zOYdBeHo46Zzh3SP9HSjT
# x/no8Zhf+yvYfvJGnXUsHicsJttvFXseGYs2uJPU5vIXmVnKcPA3v5gA3yAWTyf7
# YGcWoWa63VXAOimGsJigK+2VQbc61RWYMbRiCQ8KvYHZE/6/pNHzV9m8BPqC3jLf
# BInwAM1dwvnQI38AC+R2AibZ8GV2QqYphwlHK+Z/GqSFD/yYlvZVVCsfgPrA8g4r
# 5db7qS9EFUrnEw4d2zc4GqEr9u3WfPwwggWNMIIEdaADAgECAhAOmxiO+dAt5+/b
# UOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxE
# aWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNVBAMT
# G0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAwMDBaFw0z
# MTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ
# bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0
# IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
# AL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3EMB/z
# G6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKyunWZ
# anMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsFxl7s
# Wxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU15zHL
# 2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJBMtfb
# BHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObURWBf3
# JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6nj3c
# AORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxBYKqx
# YxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5SUUd0
# viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+xq4aL
# T8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6MIIBNjAP
# BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwPTzAf
# BgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMCAYYw
# eQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2Vy
# dC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9E
# aWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0aHR0
# cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNy
# bDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCgv0NcVec4
# X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQTSnovLbc4
# 7/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh65ZyoUi0
# mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSwuKFWjuyk
# 1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPFmCLB
# sln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjDTZ9ztwGp
# n1eqXijiuZQxggN2MIIDcgIBATB3MGMxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5E
# aWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1c3RlZCBHNCBSU0E0
# MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0ECEAVEr/OUnQg5pr/bP1/lYRYwDQYJ
# YIZIAWUDBAIBBQCggdEwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqG
# SIb3DQEJBTEPFw0yNDA2MTcxNDE0MDVaMCsGCyqGSIb3DQEJEAIMMRwwGjAYMBYE
# FGbwKzLCwskPgl3OqorJxk8ZnM9AMC8GCSqGSIb3DQEJBDEiBCCOaroq/Pl+LYdV
# HdP6kXkctddV1jevJf5tDRSt1nu/fzA3BgsqhkiG9w0BCRACLzEoMCYwJDAiBCDS
# 9uRt7XQizNHUQFdoQTZvgoraVZquMxavTRqa1Ax4KDANBgkqhkiG9w0BAQEFAASC
# AgBsHYbeFAhswIaS/LznqjgLsHTErRtdm71Jsnt76I+vDNRJVF2lsrQVZeOiSwlr
# o2ugOpVmDIHBG/fpOXTMLvWQ2tRS1ogVapeoDIMV6fvAhhOtxOiHPT43PqecBQLT
# 8q1Hqt/9fe/6d2en9qzrtj2OlNiWuyUMwSxnyaJ28w3m3AfDgscOfmdt9vqNgDCr
# FhJF+77L14MCYeLaMubN5SS6qb7h05fxxBVZhT/JJseXXVqBsS85tgZojm21dFLz
# cOTlFL3YWQ1C9PM5/Hb/WmeMcOgOvhSWG0aqR/dRqMHBqXk7jovPkAFdXoj69S2r
# eN+a6QFmQP9yL3oA/WHtiWJXVDjZsk9HBt2MA5mAFKxEsL5hYvR/R6sXsMhUiYaI
# JXEMZcovsx4xYwgEoD5FCAj9+eGmLuzEOYPFLsal8Sii2WvM0OXbAdBO1rPtST8j
# HSLV/FE+UixRFDs230uwoy8UoYde5zWdMqkwxgFrBJqHA2C2rNk5evTVbS0YxIjf
# A2OTJkXlpFKa2hsf1R2dnBFvdvcb3oopp1x/g3kVa4oIVLe43QYuLO8OyViKONCE
# 1qy4dCX315oLCWWdaKRWt8cPwswLJn4QAGPfe+73wO0/VRrT/DACWWoaO/VlNYUf
# Oi2/2f4MVepr0f5mef6N0Kv0MzelwECYBW4meihcfRCGHg==
# SIG # End signature block