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 { $temp = "{0}/{1}" -f $this.urifragment, $_.id $result = New-Object -TypeName $this.objTypeName -ArgumentList $_ $result | Add-Member -MemberType NoteProperty -Name client -Value $this.client $result | Add-Member -MemberType NoteProperty -Name urifragment -Value $temp $result } } return $this.client.web($this.urifragment).data } [psobject]get([string]$id) { if ($this.objTypeName) { $temp = "{0}/{1}" -f $this.urifragment, $id $result = New-Object -TypeName $this.objTypeName -ArgumentList $this.client.web($temp) $result | Add-Member -MemberType NoteProperty -Name client -Value $this.client $result | Add-Member -MemberType NoteProperty -Name urifragment -Value $temp 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 $result | Add-Member -MemberType NoteProperty -Name urifragment -Value "$($this.urifragment)/$($result.id)" 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) Write-Host "remove the instance: $($_.id)" } } } 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 } } [AssistantResourceObject]update([hashtable]$data) { $result = $this.client.web($this.urifragment, "POST", $data) return New-Object -TypeName $this.GetType().Name -ArgumentList $result } } class FileObject:AssistantResourceObject { FileObject([psobject]$data):base($data) {} [AssistantResourceObject]update([hashtable]$data) { Write-Host "You can't update the file object." return $this } } class File:AssistantResource { File([OpenAIClient]$client): base($client, "files", "FileObject") {} [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()] [FileObject[]]upload([string[]]$fullname) { $PSVersion = Get-Variable -Name PSVersionTable -ValueOnly if ($PSVersion.PSVersion.Major -lt 6) { throw "The upload file feature is only supported in PowerShell 6 or later." } # 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 } | ForEach-Object { [FileObject]::new($_) } ) $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" $name = "{0}-{1}" -f (Get-FileHash $file).Hash, (Split-Path $file -Leaf) # rename the file to the new name Rename-Item -Path $file -NewName $name $temppath = Join-Path -Path (Split-Path $file) -ChildPath $name try{ $form = @{ file = Get-Item -Path $temppath purpose = "assistants" } $response = Invoke-RestMethod -Uri $url -Method Post -Headers $this.client.headers -Form $form $result += [FileObject]::new($response) } finally{ # rename the file back to the original name Rename-Item -Path $temppath -NewName (Split-Path $file -Leaf) } } } 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) { #if config is not hashtable, then convert it to hashtable if ($config -isnot [hashtable]) { $config = ConvertTo-Hashtable $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 $result | Add-Member -MemberType NoteProperty -Name urifragment -Value "$($this.urifragment)/$($result.id)" return $result } throw "The body must contain 'name' and 'model', 'instructions' keys." } } 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/wYJKoZIhvcNAQcCoIIc8DCCHOwCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAaQabEpB04K8ZQ # slRxR16FlfEuvJV/xIqqKHkm2HOVy6CCAyowggMmMIICDqADAgECAhBcsg5m3zM9 # 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 # +EkUqkWS4h8zsRiyvR9Pnwklg6UxghkrMIIZJwIBATA+MCoxKDAmBgNVBAMMH0NI # RU5YSVpIQU5HIC0gQ29kZSBTaWduaW5nIENlcnQCEFyyDmbfMz2RRnGZ43MhA2Mw # DQYJYIZIAWUDBAIBBQCgfDAQBgorBgEEAYI3AgEMMQIwADAZBgkqhkiG9w0BCQMx # DAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkq # hkiG9w0BCQQxIgQgUzEYAeURw8nPzBIIklAzPVCgrB/HF87fHvLwsNuWObswDQYJ # KoZIhvcNAQEBBQAEggEAQ8jH/Mrkae4gKX370ZH8Bd0dzStOfcgDxpxQYX1N5Trs # uxyty8ecwT89zFh3FNV+UVdCTsledz/HebIF1hDVjWgyvE6A3BNCCJgbVskTZxwy # HzaKdesBB29tZbMhCKbQ9StyON4gPQkLUTqhLhjDka6HN3HB6VnuJf6qpiEftQdG # uryHvfjrKIOVKb8WwEYp57GSr24nfMglzTPzJqfx6JuBfgmWgl6pVZd0fvhg4zUP # qMPEaz+hRAQD6LcfIJmmj2uHrlqp0Yu5NIHcUzFw10yvbfixpPf4+2h8E4xdWzWc # gI6IKTP4R1ZhjOKZ+FXOdaV/Z4WlDpTa1ypjAeufOKGCF0Awghc8BgorBgEEAYI3 # AwMBMYIXLDCCFygGCSqGSIb3DQEHAqCCFxkwghcVAgEDMQ8wDQYJYIZIAWUDBAIB # BQAweAYLKoZIhvcNAQkQAQSgaQRnMGUCAQEGCWCGSAGG/WwHATAxMA0GCWCGSAFl # AwQCAQUABCDZebEY+x1oCECyHXY68HedFaKfbVWIAqdxY42ENQ6v5QIRAO/ZyC50 # PIwKkxZ7naqFKcAYDzIwMjQwOTAxMTQyMjIxWqCCEwkwggbCMIIEqqADAgECAhAF # RK/zlJ0IOaa/2z9f5WEWMA0GCSqGSIb3DQEBCwUAMGMxCzAJBgNVBAYTAlVTMRcw # FQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1c3Rl # ZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0EwHhcNMjMwNzE0MDAw # MDAwWhcNMzQxMDEzMjM1OTU5WjBIMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln # aUNlcnQsIEluYy4xIDAeBgNVBAMTF0RpZ2lDZXJ0IFRpbWVzdGFtcCAyMDIzMIIC # IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAo1NFhx2DjlusPlSzI+DPn9fl # 0uddoQ4J3C9Io5d6OyqcZ9xiFVjBqZMRp82qsmrdECmKHmJjadNYnDVxvzqX65RQ # jxwg6seaOy+WZuNp52n+W8PWKyAcwZeUtKVQgfLPywemMGjKg0La/H8JJJSkghra # arrYO8pd3hkYhftF6g1hbJ3+cV7EBpo88MUueQ8bZlLjyNY+X9pD04T10Mf2SC1e # RXWWdf7dEKEbg8G45lKVtUfXeCk5a+B4WZfjRCtK1ZXO7wgX6oJkTf8j48qG7rSk # IWRw69XloNpjsy7pBe6q9iT1HbybHLK3X9/w7nZ9MZllR1WdSiQvrCuXvp/k/Xtz # PjLuUjT71Lvr1KAsNJvj3m5kGQc3AZEPHLVRzapMZoOIaGK7vEEbeBlt5NkP4FhB # +9ixLOFRr7StFQYU6mIIE9NpHnxkTZ0P387RXoyqq1AVybPKvNfEO2hEo6U7Qv1z # fe7dCv95NBB+plwKWEwAPoVpdceDZNZ1zY8SdlalJPrXxGshuugfNJgvOuprAbD3 # +yqG7HtSOKmYCaFxsmxxrz64b5bV4RAT/mFHCoz+8LbH1cfebCTwv0KCyqBxPZyS # kwS0aXAnDU+3tTbRyV8IpHCj7ArxES5k4MsiK8rxKBMhSVF+BmbTO77665E42FEH # ypS34lCh8zrTioPLQHsCAwEAAaOCAYswggGHMA4GA1UdDwEB/wQEAwIHgDAMBgNV # HRMBAf8EAjAAMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMCAGA1UdIAQZMBcwCAYG # Z4EMAQQCMAsGCWCGSAGG/WwHATAfBgNVHSMEGDAWgBS6FtltTYUvcyl2mi91jGog # j57IbzAdBgNVHQ4EFgQUpbbvE+fvzdBkodVWqWUxo97V40kwWgYDVR0fBFMwUTBP # oE2gS4ZJaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0 # UlNBNDA5NlNIQTI1NlRpbWVTdGFtcGluZ0NBLmNybDCBkAYIKwYBBQUHAQEEgYMw # gYAwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBYBggrBgEF # BQcwAoZMaHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3Rl # ZEc0UlNBNDA5NlNIQTI1NlRpbWVTdGFtcGluZ0NBLmNydDANBgkqhkiG9w0BAQsF # AAOCAgEAgRrW3qCptZgXvHCNT4o8aJzYJf/LLOTN6l0ikuyMIgKpuM+AqNnn48Xt # JoKKcS8Y3U623mzX4WCcK+3tPUiOuGu6fF29wmE3aEl3o+uQqhLXJ4Xzjh6S2sJA # OJ9dyKAuJXglnSoFeoQpmLZXeY/bJlYrsPOnvTcM2Jh2T1a5UsK2nTipgedtQVyM # adG5K8TGe8+c+njikxp2oml101DkRBK+IA2eqUTQ+OVJdwhaIcW0z5iVGlS6ubzB # aRm6zxbygzc0brBBJt3eWpdPM43UjXd9dUWhpVgmagNF3tlQtVCMr1a9TMXhRsUo # 063nQwBw3syYnhmJA+rUkTfvTVLzyWAhxFZH7doRS4wyw4jmWOK22z75X7BC1o/j # F5HRqsBV44a/rCcsQdCaM0qoNtS5cpZ+l3k4SF/Kwtw9Mt911jZnWon49qfH5U81 # PAC9vpwqbHkB3NpE5jreODsHXjlY9HxzMVWggBHLFAx+rrz+pOt5Zapo1iLKO+ua # gjVXKBbLafIymrLS2Dq4sUaGa7oX/cR3bBVsrquvczroSUa31X/MtjjA2Owc9bah # uEMs305MfR5ocMB3CtQC4Fxguyj/OOVSWtasFyIjTvTs0xf7UGv/B3cfcZdEQcm4 # RtNsMnxYL2dHZeUbc7aZ+WssBkbvQR7w8F/g29mtkIBEr4AQQYowggauMIIElqAD # AgECAhAHNje3JFR82Ees/ShmKl5bMA0GCSqGSIb3DQEBCwUAMGIxCzAJBgNVBAYT # AlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2Vy # dC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBHNDAeFw0yMjAz # MjMwMDAwMDBaFw0zNzAzMjIyMzU5NTlaMGMxCzAJBgNVBAYTAlVTMRcwFQYDVQQK # Ew5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1c3RlZCBHNCBS # U0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0EwggIiMA0GCSqGSIb3DQEBAQUA # A4ICDwAwggIKAoICAQDGhjUGSbPBPXJJUVXHJQPE8pE3qZdRodbSg9GeTKJtoLDM # g/la9hGhRBVCX6SI82j6ffOciQt/nR+eDzMfUBMLJnOWbfhXqAJ9/UO0hNoR8XOx # s+4rgISKIhjf69o9xBd/qxkrPkLcZ47qUT3w1lbU5ygt69OxtXXnHwZljZQp09ns # ad/ZkIdGAHvbREGJ3HxqV3rwN3mfXazL6IRktFLydkf3YYMZ3V+0VAshaG43IbtA # rF+y3kp9zvU5EmfvDqVjbOSmxR3NNg1c1eYbqMFkdECnwHLFuk4fsbVYTXn+149z # k6wsOeKlSNbwsDETqVcplicu9Yemj052FVUmcJgmf6AaRyBD40NjgHt1biclkJg6 # OBGz9vae5jtb7IHeIhTZgirHkr+g3uM+onP65x9abJTyUpURK1h0QCirc0PO30qh # HGs4xSnzyqqWc0Jon7ZGs506o9UD4L/wojzKQtwYSH8UNM/STKvvmz3+DrhkKvp1 # KCRB7UK/BZxmSVJQ9FHzNklNiyDSLFc1eSuo80VgvCONWPfcYd6T/jnA+bIwpUzX # 6ZhKWD7TA4j+s4/TXkt2ElGTyYwMO1uKIqjBJgj5FBASA31fI7tk42PgpuE+9sJ0 # sj8eCXbsq11GdeJgo1gJASgADoRU7s7pXcheMBK9Rp6103a50g5rmQzSM7TNsQID # AQABo4IBXTCCAVkwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUuhbZbU2F # L3MpdpovdYxqII+eyG8wHwYDVR0jBBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08w # DgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMIMHcGCCsGAQUFBwEB # BGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsG # AQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVz # dGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdp # Y2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNybDAgBgNVHSAEGTAXMAgG # BmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZIhvcNAQELBQADggIBAH1ZjsCTtm+Y # qUQiAX5m1tghQuGwGC4QTRPPMFPOvxj7x1Bd4ksp+3CKDaopafxpwc8dB+k+YMjY # C+VcW9dth/qEICU0MWfNthKWb8RQTGIdDAiCqBa9qVbPFXONASIlzpVpP0d3+3J0 # FNf/q0+KLHqrhc1DX+1gtqpPkWaeLJ7giqzl/Yy8ZCaHbJK9nXzQcAp876i8dU+6 # WvepELJd6f8oVInw1YpxdmXazPByoyP6wCeCRK6ZJxurJB4mwbfeKuv2nrF5mYGj # VoarCkXJ38SNoOeY+/umnXKvxMfBwWpx2cYTgAnEtp/Nh4cku0+jSbl3ZpHxcpzp # SwJSpzd+k1OsOx0ISQ+UzTl63f8lY5knLD0/a6fxZsNBzU+2QJshIUDQtxMkzdwd # eDrknq3lNHGS1yZr5Dhzq6YBT70/O3itTK37xJV77QpfMzmHQXh6OOmc4d0j/R0o # 08f56PGYX/sr2H7yRp11LB4nLCbbbxV7HhmLNriT1ObyF5lZynDwN7+YAN8gFk8n # +2BnFqFmut1VwDophrCYoCvtlUG3OtUVmDG0YgkPCr2B2RP+v6TR81fZvAT6gt4y # 3wSJ8ADNXcL50CN/AAvkdgIm2fBldkKmKYcJRyvmfxqkhQ/8mJb2VVQrH4D6wPIO # K+XW+6kvRBVK5xMOHds3OBqhK/bt1nz8MIIFjTCCBHWgAwIBAgIQDpsYjvnQLefv # 21DiCEAYWjANBgkqhkiG9w0BAQwFADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMM # RGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQD # ExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwHhcNMjIwODAxMDAwMDAwWhcN # MzExMTA5MjM1OTU5WjBiMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQg # SW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2Vy # dCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC # AQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3yithZwuEppz1Yq3aaza57G4QNxDAf # 8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1Ifxp4VpX6+n6lXFllVcq9ok3DCsrp1 # mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDVySAdYyktzuxeTsiT+CFhmzTrBcZe # 7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiODCu3T6cw2Vbuyntd463JT17lNecx # y9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQjdjUN6QuBX2I9YI+EJFwq1WCQTLX # 2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/CNdaSaTC5qmgZ92kJ7yhTzm1EVgX # 9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCiEhtmmnTK3kse5w5jrubU75KSOp49 # 3ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADMfRyVw4/3IbKyEbe7f/LVjHAsQWCq # sWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QYuKZ3AeEPlAwhHbJUKSWJbOUOUlFH # dL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXKchYiCd98THU/Y+whX8QgUWtvsauG # i0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t9dmpsh3lGwIDAQABo4IBOjCCATYw # DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08w # HwYDVR0jBBgwFoAUReuir/SSy4IxLVGLp6chnfNtyA8wDgYDVR0PAQH/BAQDAgGG # MHkGCCsGAQUFBwEBBG0wazAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNl # cnQuY29tMEMGCCsGAQUFBzAChjdodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20v # RGlnaUNlcnRBc3N1cmVkSURSb290Q0EuY3J0MEUGA1UdHwQ+MDwwOqA4oDaGNGh0 # dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5j # cmwwEQYDVR0gBAowCDAGBgRVHSAAMA0GCSqGSIb3DQEBDAUAA4IBAQBwoL9DXFXn # OF+go3QbPbYW1/e/Vwe9mqyhhyzshV6pGrsi+IcaaVQi7aSId229GhT0E0p6Ly23 # OO/0/4C5+KH38nLeJLxSA8hO0Cre+i1Wz/n096wwepqLsl7Uz9FDRJtDIeuWcqFI # tJnLnU+nBgMTdydE1Od/6Fmo8L8vC6bp8jQ87PcDx4eo0kxAGTVGamlUsLihVo7s # pNU96LHc/RzY9HdaXFSMb++hUD38dglohJ9vytsgjTVgHAIDyyCwrFigDkBjxZgi # wbJZ9VVrzyerbHbObyMt9H5xaiNrIv8SuFQtJ37YOtnwtoeW/VvRXKwYw02fc7cB # qZ9Xql4o4rmUMYIDdjCCA3ICAQEwdzBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMO # RGlnaUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0ZWQgRzQgUlNB # NDA5NiBTSEEyNTYgVGltZVN0YW1waW5nIENBAhAFRK/zlJ0IOaa/2z9f5WEWMA0G # CWCGSAFlAwQCAQUAoIHRMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkq # hkiG9w0BCQUxDxcNMjQwOTAxMTQyMjIxWjArBgsqhkiG9w0BCRACDDEcMBowGDAW # BBRm8CsywsLJD4JdzqqKycZPGZzPQDAvBgkqhkiG9w0BCQQxIgQg7mYPA56EBnij # 1gyyn+s8woXQA8WbIcth5TlTToZLbogwNwYLKoZIhvcNAQkQAi8xKDAmMCQwIgQg # 0vbkbe10IszR1EBXaEE2b4KK2lWarjMWr00amtQMeCgwDQYJKoZIhvcNAQEBBQAE # ggIAS0L9gfLFyRtQgmDvxlSRwusCWzIrVgrDApforBGzbuNBB0bXhRBRtbbHdEFu # X5o8++f75MxboRH8mTIyvagtlPr0V5v9ibbTzboOMp3SzXWEBHe4oQvmf9zyEFqr # NaoRuH8WQU++w8jcHiK2D6OW931Fb7mLxaAs1CPYg+Lz/MygdxuVrWKXzmf50eKZ # sZToWVycYhzGFKDLkPg5jyqu0xofLsNZOduTZp07EmqV/IDhODJA9WJF7Cu4W8am # mLgm3iXElgnN+ddhDhPIB/0OYC/ZFndALbVcvTArvscztysFWvgJlziVLCZ/w3E3 # Z953sNB1zRH5TwhhqHi4CRTiHz3BkmIGGI6WQ6Yc9r8JKto9izWqfXQhn1acB3GK # 9AQwvuW988Kj2Q6NshkEsLUjLufDI5tr/KvPJ7Liw24t3CO1qz+UZ5UT51tqgUTV # hzS7bZfILAnKZ23wP848IJyzQ2Gea9EPwcse1T6AiN0sCij4VOX1eER4WfvCvvkh # lMIWi9upB1XWsCJ7hw4ASrhne2/sHA3lbKju3ZuH08LPAh6vBLnmeUYb9f7oc3Rl # lFpms+gRvUGKq4EAnHVxoqmSMH/SUhWIK0UhsUxKkGfzNbRhl0TA81mfRQObuHT7 # aWIQjiy6QVc4yDVvJOgP/9ymuqUbZTxrtLZJobxoZn5MTgo= # SIG # End signature block |