powershai.psm1
<#
POWERSHAI Autor: Rodrigo Ribeiro Gomes (https://iatalk.ing) NÃO ESTÁ PRONTO PARA USO EM PRODUÇÃO! Você é livre para usar e modificar, mas deve deixar o crédito ao projeto original! Este script implementa algumas funções simples para facilitar sua interação com a API da OpenAI! O objetivo deste script é mostrar como você pode facilmente invocar a API do OpenAI com PowerShell, ao mesmo tempo que prover uma interface simples para as chamadas mais importantes. Antes de continuar, o que você precisa? Gere um token no site da OpenAI! Coloca na variável de ambiente OPENAI_API_KEY. # AUTENTICAÇÃO > import-module Caminho\powershai.psm1 > Set-OpenaiToken # Define a sua api token > $res = OpenAiTextCompletion "Olá, estou falando com você direto do PowerShell" > $res.choices[0].text Verifique os comentáros em cada funçã abaixo para mais informações! ATENÇÃO: LEMBRE-SE que as chamadas realizadas irão consumir seus créditos da OpenAI! Certifique-se que você compreendeu o modelo de cobrança da OpenAI para evitar surpresas. Além disso, esta é uma versão sem testes e para uso livre por sua própria conta e risco. #> # Função genérica para invocar HTTP e com um minimo de suporte a SSE (Server Sent Events) Function InvokeHttp { [CmdLetBinding()] param( $url = $null ,[object]$data = $null ,$method = "GET" ,$contentType = "application/json; charset=utf-8" ,$headers = @{} ,$SseCallBack = $null ) $ErrorActionPreference = "Stop"; #Converts a hashtable to a URLENCODED format to be send over HTTP requests. Function verbose { $ParentName = (Get-Variable MyInvocation -Scope 1).Value.MyCommand.Name; write-verbose ( $ParentName +':'+ ($Args -Join ' ')) } #Troca caracteres não-unicode por um \u + codigo! #Solucao adapatada da resposta do Douglas em: http://stackoverflow.com/a/25349901/4100116 Function EscapeNonUnicodeJson { param([string]$Json) $Replacer = { param($m) return [string]::format('\u{0:x4}', [int]$m.Value[0] ) } $RegEx = [regex]'[^\x00-\x7F]'; verbose " Original Json: $Json"; $ReplacedJSon = $RegEx.replace( $Json, $Replacer) verbose " NonUnicode Json: $ReplacedJson"; return $ReplacedJSon; } #Converts objets to JSON and vice versa, Function ConvertTojson2($o) { if(Get-Command ConvertTo-Json -EA "SilentlyContinue"){ verbose " Using ConvertTo-Json" return EscapeNonUnicodeJson(ConvertTo-Json $o -Depth 10); } else { verbose " Using javascriptSerializer" Movidesk_LoadJsonEngine $jo=new-object system.web.script.serialization.javascriptSerializer $jo.maxJsonLength=[int32]::maxvalue; return EscapeNonUnicodeJson ($jo.Serialize($o)) } } Function Hash2Qs { param($Data) $FinalString = @(); $Data.GetEnumerator() | %{ write-verbose "$($MyInvocation.InvocationName): Converting $($_.Key)..." $ParamName = MoviDesk_UrlEncode $_.Key; $ParamValue = Movidesk_UrlEncode $_.Value; $FinalString += "$ParamName=$ParamValue"; } $FinalString = $FinalString -Join "&"; return $FinalString; } try { #building the request parameters if($method -eq 'GET' -and $data){ if($data -is [hashtable]){ $QueryString = Hash2Qs $data; } else { $QueryString = $data; } if($url -like '*?*'){ $url += '&' + $QueryString } else { $url += '?' + $QueryString; } } verbose " Creating WebRequest method... Url: $url. Method: $Method ContentType: $ContentType"; $Web = [System.Net.WebRequest]::Create($url); $Web.Method = $method; $Web.ContentType = $contentType @($headers.keys) | %{ $Web.Headers.add($_, $headers[$_]); } #building the body.. if($data -and 'POST','PATCH','PUT' -Contains $method){ if($data -is [hashtable]){ verbose "Converting input object to json string..." $data = $data | ConvertTo-Json; } verbose "Data to be send:`n$data" # Transforma a string json em bytes... [Byte[]]$bytes = [system.Text.Encoding]::UTF8.GetBytes($data); #Escrevendo os dados $Web.ContentLength = $bytes.Length; verbose " Bytes lengths: $($Web.ContentLength)" verbose " Getting request stream...." $RequestStream = $Web.GetRequestStream(); try { verbose " Writing bytes to the request stream..."; $RequestStream.Write($bytes, 0, $bytes.length); } finally { verbose " Disposing the request stream!" $RequestStream.Dispose() #This must be called after writing! } } $UrlUri = [uri]$Url; $Unescaped = $UrlUri.Query.split("&") | %{ [uri]::UnescapeDataString($_) } verbose "Query String:`r`n$($Unescaped | out-string)" verbose " Making http request... Waiting for the response..." try { $HttpResp = $Web.GetResponse(); } catch [System.Net.WebException] { verbose "ResponseError: $_... Processing..." $ErrorResp = $_.Exception.Response; if($ErrorResp.StatusCode -ne "BadRequest"){ throw; } verbose "Processing response error..." $ErrorResponseStream = $ErrorResp.GetResponseStream(); verbose "Creating error response reader..." $ErrorIO = New-Object System.IO.StreamReader($ErrorResponseStream); verbose "Reading error response..." $ErrorText = $ErrorIO.ReadToEnd(); throw $ErrorText; } verbose "Request done..." $Result = @{}; if($HttpResp){ verbose " charset: $($HttpResp.CharacterSet) encoding: $($HttpResp.ContentEncoding). ContentType: $($HttpResp.ContentType)" verbose " Getting response stream..." $ResponseStream = $HttpResp.GetResponseStream(); verbose " Response streamwq1 size: $($ResponseStream.Length) bytes" $IO = New-Object System.IO.StreamReader($ResponseStream); $LineNum = 0; if($SseCallBack){ verbose " Reading SSE..." $LineNum++; $SseResult = $null; $Lines = @(); while($SseResult -ne $false){ verbose " Reading next line..." $line = $IO.ReadLine() verbose " Content: $line"; $Lines += $line; verbose " Invoking callback..." $SseResult = & $SseCallBack @{ line = $line; num = $LineNum; req = $Web; res = $HttpResp; stream = $IO } } $Result.text = $Lines; $Result.stream = $true; } else { verbose " Reading response stream...." $Result.text = $IO.ReadToEnd(); } verbose " response json is: $responseString" } verbose " HttpResult:`n$($Result|out-string)" return $Result } finally { if($IO){ $IO.close() } if($ResponseStream){ $ResponseStream.Close() } if($RequestStream){ write-verbose "Finazling request stream..." $RequestStream.Close() } } } <# Esta função é usada como base para invocar a a API da OpenAI! #> function InvokeOpenai { [CmdletBinding()] param( $endpoint ,$body ,$method = 'POST' ,$token = $Env:OPENAI_API_KEY ,$StreamCallback = $null ) if(!$token){ throw "OPENAI_NO_KEY"; } $headers = @{ "Authorization" = "Bearer $token" } $BaseUrl = $Env:OPENAI_API_BASE; if(!$BaseUrl){ $BaseUrl = "https://api.openai.com/v1" } $url = "$BaseUrl/$endpoint" if($StreamCallback){ $body.stream = $true; $body.stream_options = @{include_usage = $true}; } $JsonParams = @{Depth = 10} $Global:Dbg_LastBody = $body; write-verbose "InvokeOpenai: Converting body to json (depth: $($JsonParams.Depth))..." $ReqBodyPrint = $body | ConvertTo-Json @JsonParams write-verbose "ReqBody:`n$($ReqBodyPrint|out-string)" $ReqBody = $body | ConvertTo-Json @JsonParams -Compress $ReqParams = @{ data = $ReqBody url = $url method = $method Headers = $headers } $StreamData = @{ #Todas as respostas enviadas! answers = @() fullContent = "" FinishMessage = $null #Todas as functions calls calls = @{ all = @() funcs = @{} } CurrentCall = $null } if($StreamCallback){ $ReqParams['SseCallBack'] = { param($data) $line = $data.line; $StreamData.lines += $line; if($line -like 'data: {*'){ $RawJson = $line.replace("data: ",""); $Answer = $RawJson | ConvertFrom-Json; $FinishReason = $Answer.choices[0].finish_reason; $DeltaResp = $Answer.choices[0].delta; $Role = $DeltaResp.role; #Parece vir somente no primeiro chunk... $StreamData.fullContent += $DeltaResp.content; if($FinishReason){ $StreamData.FinishMessage = $Answer } foreach($ToolCall in $DeltaResp.tool_calls){ $CallId = $ToolCall.id; $CallType = $ToolCall.type; verbose "Processing tool`n$($ToolCall|out-string)" if($CallId){ $StreamData.calls.all += $ToolCall; $StreamData.CurrentCall = $ToolCall; continue; } $CurrentCall = $StreamData.CurrentCall; if($CurrentCall.type -eq 'function' -and $ToolCall.function){ $CurrentCall.function.arguments += $ToolCall.function.arguments; } } $StreamData.answers += $Answer & $StreamCallback $Answer } elseif($line -eq 'data: [DONE]'){ #[DONE] documentando aqui: #https://platform.openai.com/docs/api-reference/chat/create#chat-create-stream return $false; } } } write-verbose "ReqParams:`n$($ReqParams|out-string)" $RawResp = InvokeHttp @ReqParams write-verbose "RawResp: `n$($RawResp|out-string)" if($RawResp.stream){ #Isso imita a mensagem de resposta, para ficar igual ao resultado quando está sem Stream! $MessageResponse = @{ role = "assistant" content = $StreamData.fullContent } if($StreamData.calls.all){ $MessageResponse.tool_calls = $StreamData.calls.all; } return @{ stream = @{ RawResp = $RawResp answers = $StreamData.answers tools = $StreamData.calls.all } message = $MessageResponse finish_reason = $StreamData.FinishMessage.choices[0].finish_reason usage = $StreamData.answers[-1].usage model = $StreamData.answers[-1].model } } return $RawResp.text | ConvertFrom-Json } # Define o token a ser usado nas chamadas da OpenAI! # Faz um testes antes para certificar de que é acessível! function Set-OpenaiToken { [CmdletBinding()] param() $ErrorActionPreference = "Stop"; write-host "Forneça o token no campo senha na tela em que se abrir"; $creds = Get-Credential "OPENAI TOKEN"; $TempToken = $creds.GetNetworkCredential().Password; write-host "Checando se o token é válido"; try { $result = InvokeOpenai 'models' -m 'GET' -token $TempToken } catch [System.Net.WebException] { $resp = $_.exception.Response; if($resp.StatusCode -eq 401){ throw "INVALID_TOKEN: Token is not valid!" } throw; } write-host " Tudo certo!"; $Env:OPENAI_API_KEY = $TempToken return; } # Get all models! function Get-OpenaiModels(){ return (InvokeOpenai 'models' -m 'GET').data } <# Esta função chama o endpoint /completions (https://platform.openai.com/docs/api-reference/completions/create) Exemplo: $res = Get-OpenAiTextCompletion "Gere um nome aleatorio" $res.choices[0].text; Ela retorna o mesmo objeto retornado pela API da OpenAI! Por enquanto, apenas os parâmetros temperature, model e MaxTokens foram implementados! #> function Get-OpenAiTextCompletion { [CmdletBinding()] param( $prompt ,$temperature = 0.6 ,$model = "gpt-3.5-turbo-instruct" ,$MaxTokens = 200 ) write-warning "LEGACY! This endpoint is legacy. Use Chat Completion!" $FullPrompt = @($prompt) -Join "`n"; $Body = @{ model = $model prompt = $FullPrompt max_tokens = $MaxTokens temperature = $temperature } InvokeOpenai -endpoint 'completions' -body $Body } <# Esta função chama o endpoint /chat/completions (https://platform.openai.com/docs/api-reference/chat/create) Este endpoint permite você conversar com modelos mais avançados como o GPT-3 e o GPT-4 (veja a disponiblidadena doc) O Chat Completion tem uma forma de conversa um pouco diferente do que o Text Completion. No Chat Completion, você pode especificar um role, que é uma espécie de categorização do autor da mensagem. A API suporta 3 roles: user Representa um prompt genérico do usuário. system Representa uma mensagem de controle, que pode dar instruções que o modelo vai levar em conta para gerar a resposta. assistant Representa mensagens prévias. É útil para que o modelo possa aprender como gerar, entender o contexto, etc. Basicamente, o system e o assistant são úteis para calibrar melhor a resposta. Enquanto que o user, é o que de fato você quer de resposta (você, ou o seu usuário) Nesta função, para tentar facilitar sua vida, eu deixei duas formas pela qual você usar. A primeira forma é a mais simples: $res = OpenAiChat "Oi GPT, tudo bem?" $res.choices[0].message; Nesta forma, você passa apenas uma mensagem padrão, e a função vai cuidar de enviar como o role "user". Você pode passar várias linhas de texto, usando um simples array do PowerShell: $res = OpenAiChat "Oi GPT, tudo bem?","Me de uma dica aleatoria sobre o PowerShell" Isso vai enviar o seguinte prompt ao modelo: Oi,GPT, tudo bem? Me de uma dica aleatoria sobre o PowerShell Caso, você queria especificar um role, basta usar um dos prefixos. (u - user, s - system, a - assitant" $res = OpenAiChat "s: Use muita informalidade e humor!","u: Olá, me explique o que é o PowerShell!" $res.choices[0].message.content; Você pode usar um array no script: $Prompt = @( 'a: function Abc($p){ return $p*100 }' "s: Gere uma explicação bastante dramática com no máximo 100 palavras!" "Me explique o que a função Abc faz!" ) $res = OpenAiChat $Prompt -MaxTokens 1000 DICA: Note que na última mensagem, e não precisei especificar o "u: mensagem", visto que ele ja usa como default se não encontra o prefixo. DICA 2: Note que eu usei o parâmetro MaxTokens para aumentar o limite padrão de 200. #> function Get-OpenaiChat { [CmdletBinding()] param( $prompt ,$temperature = 0.6 ,$model = $null ,$MaxTokens = 200 ,$ResponseFormat = $null ,#Function list, as acceptable by Openai #OpenAuxFunc2Tool can be used to convert from powershell to this format! $Functions = @() ,$PrevContext = $null #Add raw params directly to api! #overwite previous ,$RawParams = @{} ,$StreamCallback = $null ) if(!$model){ $model = $Env:OPENAI_DEFAULT_MODEL } $Messages = @(); $ShortRoles = @{ s = "system" u = "user" a = "assistant" } if(!$model){ $model = "gpt-3.5-turbo" } if($PrevContext){ $Messages += $PrevContext; } [object[]]$InputMessages = @($prompt); foreach($m in $InputMessages){ $ChatMessage = $null; if($m -is [PsCustomObject] -or $m -is [hashtable]){ write-verbose "Adding chat message directly:`n$($m|out-string)" $ChatMessage = $m; } else { if($m -match '(?s)^([s|u|a]): (.+)'){ $ShortName = $matches[1]; $Content = $matches[2]; $RoleName = $ShortRoles[$ShortName]; if(!$RoleName){ $RoleName = "user" $Content = $m; } } else { $RoleName = "user"; $Content = $m; } $ChatMessage = @{role = $RoleName; content = $Content}; } $Messages += $ChatMessage } $Body = @{ model = $model messages = $Messages max_tokens = $MaxTokens temperature = $temperature } if($RawParams){ $RawParams.keys | %{ $Body[$_] = $RawParams[$_] } } if($ResponseFormat){ $Body.response_format = @{type = $ResponseFormat} } if($Functions){ $Body.tools = $Functions $Body.tool_choice = "auto"; } write-verbose "Body:$($body|out-string)" InvokeOpenai -endpoint 'chat/completions' -body $Body -StreamCallback $StreamCallback } Set-Alias -Name OpenAiChat -Value Get-OpenaiChat; <# Função auxiliar para converter um script .ps1 em um formato de schema esperado pela OpenAI. Basicamente, o que essa fução faz é ler um arquivo .ps1 (ou string) juntamente com sua help doc. Então, ele retorna um objeto no formato especifiado pela OpenAI para que o modelo possa invocar! Retorna um hashtable contendo as seguintes keys: functions - A lista de funções, com seu codigo lido do arquivo. Quando o modelo invocar, você pode executar diretamente daqui. tools - Lista de tools, para ser enviando na chamada da OpenAI. Você pode documentar suas funções e parâmetros seguindo o Comment Based Help do PowerShell: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_comment_based_help?view=powershell-7.4 #> function OpenAuxFunc2Tool { [CmdLetBinding()] param( $func ) write-verbose "Processing Func: $($func|out-string)"; if($func.__is_functool_result){ write-verbose "Functions already processed. Ending..."; return $func; } #Defs is : @{ FuncName = @{}, FuncName2 ..., FuncName3... } $FunctionDefs = @{}; # func é um file? if($func -is [string] -and $func){ # existing path! $IsPs1 = $func -match '\.ps1$'; $ResolvedPath = Resolve-Path $func -EA SilentlyContinue; if(!$IsPs1 -or !$ResolvedPath){ throw "POSHAI_FUNC2TOOL_NOTSUPPORTED: $func" } [string]$FilePath = $ResolvedPath write-verbose "Loading function from file $FilePath" <# Aqui é onde fazemos uma mágica interessante... Usamos um scriptblock para carregar o arquivo. Logo, o arquivo será carregado apenas no contexto desse scriptblock. Então, obtemos todas os comandos, usando o Get-Command, e filtramos apenas os que foram definidos no arquivo. Com isso conseguimos trazer uma referencia para todas as funcoes definidas no arquivo, aproveitando o proprio interpretador do PowerShell. Assim, conseguimos acessar tanto a DOC da funcao, quanto manter um objeto que pode ser usado para executá-la. #> $GetFunctionsScript = { write-verbose "Running $FilePath" . $FilePath $AllFunctions = @{} #Obtem todas as funcoes definidas no arquivo. #For each function get a object Get-Command | ? {$_.scriptblock.file -eq $FilePath} | %{ write-verbose "Function: $($_.name)" $help = get-help $_.name; $AllFunctions[$_.name] = @{ func = $_ help = get-help $_.Name } } return $AllFunctions; } $FunctionDefs = & $GetFunctionsScript } [object[]]$AllTools = @(); #for each function! foreach($KeyName in @($FunctionDefs.keys) ){ $Def = $FunctionDefs[$KeyName]; $FuncName = $KeyName $FuncDef = $Def $FuncHelp = $Def.help; $FuncCmd = $Def.func; write-verbose "Creating Tool for Function $FuncName" $OpenAiFunction = @{ name = $null description = $nul parameters = @{} } $OpenAiTool = @{ type = "function" 'function' = $OpenAiFunction } $AllTools += $OpenAiTool; $OpenAiFunction.name = $FuncHelp.name; $description = ( @($FuncHelp.Synopsis) + @($FuncHelp.description|%{$_.text})) -join "`n" $OpenaiFunction.description = $description; # get all parameters! $FuncParams = $FuncHelp.parameters.parameter; $FuncParamSchema = @{} $OpenaiFunction.parameters = @{ type = "object" properties = $FuncParamSchema required = @() } if(!$FuncParams){ continue; } foreach($param in $FuncParams){ $ParamHelp = $param; $ParamName = $ParamHelp.name; $ParamType = $ParamHelp.type; $ParamDesc = @($ParamHelp.description|%{$_.text}) -Join "`n" $ParamSchema = @{ type = "string" description = $ParamDesc #enum? #items:@{type} } $FuncParamSchema[$ParamName] = $ParamSchema #Get the typename! try { $ParamRealType = [type]$ParamType.name if($ParamRealType -eq [int]){ $ParamSchema.type = "number" } } catch{ write-warning "Cannot determined type of param $ParamName! TypeName = $($ParamType.name)" } } } return @{ tools = $AllTools functions = $FunctionDefs #Esta é uma flag indicando que este objeto já foi processado. #Caso envie novamente, ele apenas devolve! __is_functool_result = $true } } #Gets cost of answer! $POWERSHAI_CACHED_MODELS_PRICE = @{}; function Get-OpenAiAnswerCost($answers){ #Pricings at 17/01/2024 $Pricings = @{ 'estimated' = @{ input = 0.1 output = 0.3 match = '.+' } 'gpt-4-turbo' = @{ match = "gpt-4.+-preview" input = 0.01 output = 0.03 } 'gpt-4' = @{ match = "^gpt-4" input = 0.03 output = 0.06 } 'gpt-3' = @{ match = "gpt-3.+" input = 0.0010 output = 0.0020 } 'embedding' = @{ match = '.+embedding.+' input = 0.0001 output = 0 } } $AllAnswers = @(); $costs = @{ answers = @() input = 0 output = 0 total = 0 tokensTotal = 0 tokensInput = 0 tokensOutput = 0 } foreach($answer in @($answers)){ $model = $answer.model; $inputTokens = $answer.usage.prompt_tokens; $outputTokens = $answer.usage.completion_tokens; if(!$outputTokens){ $outputTokens = 0; } #Is in cache? $CachedMatch = $POWERSHAI_CACHED_MODELS_PRICE[$model] if(!$CachedMatch){ # Find high pricing... $SortedPrices = $Pricings.GetEnumerator() | ? {$model -match $_.value.match} | sort {$_.value.match.length} -Desc if($SortedPrices){ $BestMatch = $SortedPrices[0]; } else { $BestMatch = $SortedPrices['estimated']; } $CachedMatch = $BestMatch $POWERSHAI_CACHED_MODELS_PRICE[$model] = $CachedMatch } $AnswerCosts = @{ pricings = $CachedMatch.value table = $CachedMatch.key inputCost = [decimal]($CachedMatch.value.input * $inputTokens/1000) outputCost = [decimal]($CachedMatch.value.output * $outputTokens/1000) totalCost = $null } $AnswerCosts.totalCost = $AnswerCosts.inputCost + $AnswerCosts.outputCost; $costs.answers += $AnswerCosts; $costs.input += $AnswerCosts.inputCost; $costs.output += $AnswerCosts.outputCost; $costs.tokensTotal += $inputTokens + $outputTokens; $costs.tokensInput += $inputTokens; $costs.tokensOutput += $outputTokens; } $costs.total = $costs.output + $costs.input; return $costs; } # Representa cada interação feita com o modelo! function NewAiInteraction { return @{ req = $null # the current req count! sent = $null # Sent data! rawAnswer = $null # the raw answer returned by api! stopReason = $null # The reason stopped! error = $null # If apply, the error ocurred when processing the answer! seqError = $null # current sequence error toolResults = @() # lot of about running functino! } } <# Esta é uma função auxiliar para ajudar a fazer o processamento de tools mais fácil com powershell. Ele lida com o processamento das "Tools", executando quando o modelo solicita! Apenas converse normalmente, defina as funções e deixe que este comando faça o resto! Não se preocupe em processar a resposta do modelo, se tem que executar a função, etc. O comando abaixo já faz tudo isso pra você. Você precisa apenas concentrar em definir as funções que podem ser invocadas e o prompt(veja o comando OpenAuxFunc2Tool). #> function Invoke-OpenAiChatFunctions { [CmdletBinding()] param( $prompt ,# Passe as funcoes aqui # Atualmente, suporta apenas um arquivo .ps1 onde as fucnoes estao definidas. # Crie seu arquivo e documente ele usando a propria sintaxe do powershell. # Essa documentação será enviada ao modelo. Quanto melhor você documentar, melhor o modelo saberá quando invocar. $Functions = $null ,$PrevContext = $null ,# máx output! $MaxTokens = 500 ,# No total, permitir no max 5 iteracoes! $MaxRequests = 5 ,# Quantidade maximo de erros consecutivos que sua funcao pode gerar antes que ele encerre. $MaxSeqErrors = 2 ,$temperature = 0.6 ,$model = $null ,# Event handler # Cada key é um evento que será disparado em algum momento por esse comando! # eventos: # answer: disparado após obter a resposta do modelo (ou quando uma resposta fica disponivel ao usar stream). # func: disparado antes de iniciar a execução de uma tool solicitada pelo modelo. # exec: disparado após o modelo executar a funcao. # error: disparado quando a funcao executada gera um erro # stream: disparado quando uma resposta foi enviada (pelo stream) e -DifferentStreamEvent # beforeAnswer: Disparado após todas as respostas. Util quando usado em stream! # afterAnswer: Disparado antes de iniciar as respostas. Util quando usado em stream! $on = @{} ,# Envia o response_format = "json", forçando o modelo a devolver um json. [switch]$Json ,#Adicionar parâmetros customizados diretamente na chamada (irá sobrescrever os parâmetros definidos automaticamente). $RawParams = $null ,[switch]$Stream ) $ErrorActionPreference = "Stop"; # Converts the function list user passed to format acceptable by openai! $OpenAiTools = OpenAuxFunc2Tool $Functions # initliza message! [object[]]$Message = @($prompt); $TotalPromptTokens = 0; $TotalOutputTokens = 0; $AllInteractions = @(); $emit = { param($evtName, $interaction) $evtScript = $on[$evtName]; if(!$evtScript){ return; } try { $null = & $evtScript $interaction @{event=$evtName} } catch { write-warning "EventCallBackError: $evtName"; write-warning $_ write-warning $_.ScriptStackTrace } } # Vamos iterar em um loop chamando o model toda vez. # Sempre que o model retornar pedindo uma funcao, vamos iterar novamente, executar a funcao, e entregar o resultado pro model! $ReqsCount = 0; $FuncSeqErrors = 0; #Indicates how many consecutive errors happens! :Main while($true){ #Vamos avançr o numero de requests totais que ja fizemos!! $ReqsCount++; #This object is created every request and will store all data bout interaction! $AiInteraction = NewAiInteraction $AiInteraction.req = $ReqsCount; # Parametros que vamos enviar a openai usando a funcao openai! $Params = @{ prompt = $Message temperature = $temperature MaxTokens = $MaxTokens Functions = $OpenAiTools.tools model = $model RawParams = $RawParams } $StreamProcessingData = @{ num = 1 } $ProcessStream = { param($Answer) $AiInteraction.stream = @{ answer = $Answer emiNum = 0 num = $StreamProcessingData.num++ } & $emit "stream" $AiInteraction } if($Stream){ $Params['StreamCallback'] = $ProcessStream } if($Json){ #https://platform.openai.com/docs/guides/text-generation/json-mode $Params.ResponseFormat = "json_object"; $Params.prompt += "s: Response in JSON"; } $AiInteraction.sent = $Params; $AiInteraction.message = $Message; # Se chegamos no limite, então naos vamos enviar mais nada! if($ReqsCount -gt $MaxRequests){ $AiInteraction.stopReason = "MaxReached"; write-warning "MaxRequests reached! ($ReqsCount)"; break; } write-verbose "Sending Request $ReqsCount"; # manda o bla bla pro gpt... & $emit "send" $AiInteraction $Answer = OpenAiChat @Params; $AiInteraction.rawAnswer = $Answer; $AllInteractions += $AiInteraction; & $emit "answer" $AiInteraction if($Answer.stream){ $ModelResponse = $Answer } else { $ModelResponse = $Answer.choices[0]; } write-verbose "FinishReason: $($ModelResponse.finish_reason)" $WarningReasons = 'length','content_filter'; if($ModelResponse.finish_reason -in $WarningReasons){ write-warning "model finished answering due to $($ModelResponse.finish_reason)"; } # Model asked to call a tool! if($ModelResponse.finish_reason -eq "tool_calls"){ # A primeira opcao de resposta... $AnswerMessage = $ModelResponse.message; # Add current message to original message to provided previous context! $Message += $AnswerMessage; $ToolCalls = $AnswerMessage.tool_calls write-verbose "TotalToolsCals: $($ToolCalls.count)" foreach($ToolCall in $ToolCalls){ $CallType = $ToolCall.type; $ToolCallId = $ToolCall.id; write-verbose "ProcessingTooll: $ToolCallId" try { if($CallType -ne 'function'){ throw "Tool type $CallType not supported" } $FuncCall = $ToolCall.function #Get the called function name! $FuncName = $FuncCall.name write-verbose " FuncName: $FuncName" #Build the response message that will sent back! $FuncResp = @{ role = "tool" tool_call_id = $ToolCallId #Set to a default message! content = "ERROR: Some error ocurred processing function!"; } # Lets get func defintion! $FuncDef = $OpenAiTools.functions[$FuncName]; #Functino not found! #This can be erros of model? Or bug of code... #Say back this to model... if(!$FuncDef){ throw "Function $FuncName not found!"; } # Get the function itself! # TODO: Talvez o GPT tenha pedido uma funcao erraa também, precisamos tratar este caso! # Por enquanto, assumimos que ele sempre manda uma funcao existente... $TheFunc = $FuncDef.func; if(!$TheFunc){ write-warning "Model asked function $FuncName but the body was not found. This is a bug of PowershIA"; throw "Function was defined, but code not found. This is a bug of function source." } #Here wil have all that we need1 $FuncArgsRaw = $FuncCall.arguments write-verbose " Arguments: $FuncArgsRaw"; #We assuming model sending something that can be converted... $funcArgs = $FuncArgsRaw | ConvertFrom-Json; #Then, we can call the function! $ArgsHash = @{}; $funcArgs.psobject.properties | %{ $ArgsHash[$_.Name] = $_.Value }; $CurrentToolResult = @{ hash = $ArgsHash obj = $funcArgs name = $FuncName id = $ToolCallId resp = $FuncResp } $AiInteraction.toolResults += $CurrentToolResult & $emit "func" $AiInteraction $CurrentToolResult write-verbose "Calling function $FuncName ($FuncInfo)" $FuncResp.content = & $TheFunc @ArgsHash | ConvertTo-Json -Depth 10 $FuncSeqErrors = 0; } catch { $err = $_; & $emit "error" $AiInteraction #Add curent error to this object to caller debug... $AiInteraction.error = $_; #just log in screen to caller know about these errors! write-warning "Processing answer of model resulted in error. Id = $ToolCallId"; write-warning "FuncName:$FuncName, Request:$ReqsCount" write-warning ("ERROR:") write-warning $err write-warning $err.ScriptStackTrace $FuncResp.content = "ERROR:"+$err; #Resets the seq errorS! $FuncSeqErrors++; $AiInteraction.seqError = $FuncSeqErrors; if($FuncSeqErrors -ge $MaxSeqErrors){ $AiInteraction.stopReason = "MaxSeqErrorsReached"; write-warning "Stopping due to max consecutive errors"; break Main; } } & $emit "exec" $AiInteraction $Message += $FuncResp; } #Start sending again... continue; } # se chegou aqui é pq o gpt processou tudo e nao precisou invocar uma funcao! #entao, podemos encerrar a bagaça! break; } $AllAnswers = $AllInteractions | %{ $_.rawAnswer }; $AnswerCost = Get-OpenAiAnswerCost $AllAnswers return @{ answer = $Answer # last answer interactions = $AllInteractions tools = $OpenAiTools costs = $AnswerCost } } # Obtéms os embegginds de um texto function OpenaiEmbeddings { param($inputText,$model) if(!$model){ $model = 'text-embedding-ada-002' } $body = @{ input = $inputText model = $model } InvokeOpenai -endpoint 'embeddings' -body $Body } <# Gera o embedding de um texto! #> function Invoke-OpenaiEmbedding { [CmdletBinding()] param( $text ,$model ) $ans = OpenaiEmbeddings -input $text -model $model $costs = Get-OpenAiAnswerCost $ans [object[]]$AllEmbeddings = @($null) * $ans.data.length; $ans.data | %{ $AllEmbeddings[$_.index] = $_.embedding } return @{ rawAnswer = $ans costs = $costs embeddings = $AllEmbeddings } } #quebra o texto em tokens... function SplitOpenAiString { write-host "TODO..." } <# Comando ChaTest. Com este comando você pode conversar direto do prompt com o modelo!. Ele também é um belo teste de todos os outros comandos e funcionalidades disponíveis aqui! Note que isso pode consumir bastante dos seus tokens! Syntaxe: Converse com #> function Invoke-PowershaiChat { [CmdletBinding()] param( $Functions = $Null ,$MaxTokens = 200 ,$MaxRequests = 10 ,$SystemMessages = @() ,#Desliga o stream [switch]$NoStream ,#Specify a hash table where you want store all results $ChatMetadata = @{} ) $ErrorActionPreference = "Stop"; function WriteModelAnswer($interaction, $evt){ $WriteParams = @{ NoNewLine = $false ForegroundColor = "Cyan" } $Stream = $interaction.stream; $str = ""; $EventName = $evt.event; if($Stream){ $PartNum = $Stream.num; $text = $Stream.answer.choices[0].delta.content; $WriteParams.NoNewLine = $true; if($PartNum -eq 1){ $str = "🤖 $($model):" } if($text){ $Str += $text } if($EventName -eq "answer"){ $str = "`n`n"; } } else { #So entrará nesse quando o stream estiver desligado! $ans = $interaction.rawAnswer; $text = $ans.choices[0].message.content; $model = $ans.model; $str = "`🤖 $($model): $text`n`n" } if($str){ write-host @WriteParams $Str; } } $ChatHistory = @() $ChatStats = @{ TotalChats = 0 TotalCost = 0 TotalInput = 0 TotalOutput = 0 TotalTokensI = 0 TotalTokensO = 0 } $ChatMetadata.history = $ChatHistory; $ChatMetadata.stats = $ChatStats $VerboseEnabled = $False; $MustStream = $true; if($NoStream){ $MustStream = $False; } write-host "Carregando lista de modelos..." $SupportedModels = Get-OpenaiModels write-host "Montando estrutura de functions..." $Funcs = OpenAuxFunc2Tool $Functions; try { $CurrentUser = Get-LocalUser -SID ([System.Security.Principal.WindowsIdentity]::GetCurrent().User) $FullName = $CurrentUser.FullName; $UserAllNames = $FullName.split(" "); $UserFirstName = $UserAllNames[0]; } catch { write-verbose "Cannot get logged username: $_" } write-host "Iniciando..." # $Ret = Invoke-OpenAiChatFunctions -temperature 1 -prompt @( # "Gere uma saudação inicial para o usuário que está conversando com você a partir do módulo powershell chamado PowerShai" # ) -on @{ # stream = {param($inter,$evt) WriteModelAnswer $inter $evt} # answer = {param($inter,$evt) WriteModelAnswer $inter $evt} # } -Stream:$MustStream $model = $null $UseJson = $false; $ShowFullSend = $false; $ShowTokenStats = $false; $MaxContextSize = 8192 # Vou considerar isso como o número de caracter por uma questão simples... # futuramente, o correto é trabalhar com tokens! $ChatUserParams = @{ model = $model MaxRequest = $MaxRequests MaxTokens = $MaxTokens Json = $false } function ParsePrompt($prompt) { $parts = $prompt -split ' ',2 $cmdName = $parts[0]; $remain = $parts[1]; $result = @{ action = $null prompt = $null; } write-verbose "InputPrompt: $cmdName" switch($cmdName){ "/reload" { write-host "Reloading function list..."; $Funcs = OpenAuxFunc2Tool $Functions; Set-Variable -Scope 1 -Name Funcs -Value $Funcs } "/retool" { write-host "Redefinindo a lista de tools: $remain"; $Funcs = OpenAuxFunc2Tool $remain; Set-Variable -Scope 1 -Name Funcs -Value $Funcs } "/maxcontext" { Set-Variable -Scope 1 -Name MaxContextSize -Value ([int]$remain) } "/models" {write-host $($SupportedModels|out-string)} {'.cls','/clear','/cls','.c' -contains $_} {clear-host} # sets "/von" {Set-Variable -Scope 1 -Name VerboseEnabled -Value $true;} "/voff" {Set-Variable -Scope 1 -Name VerboseEnabled -Value $false;} "/json" {Set-Variable -Scope 1 -Name UseJson -Value $true } "/jsoff" {Set-Variable -Scope 1 -Name UseJson -Value $false } "/fullon" {Set-Variable -Scope 1 -Name ShowFullSend -Value $true } "/fulloff" {Set-Variable -Scope 1 -Name ShowFullSend -Value $false } "/tokon" {Set-Variable -Scope 1 -Name ShowTokenStats -Value $true } "/tokoff" {Set-Variable -Scope 1 -Name ShowTokenStats -Value $false } "/p" { #Split! $paramPair = $remain -split ' ',2 $paramName = $paramPair[0] $paramValStr = $paramPair[1]; if(!$paramValStr){ write-host $($ChatUserParams|out-string); return; } if(!$ChatUserParams.contains($paramName)){ write-warning "No parameter existing with name: $paramName" return; } $ParamVal = $ChatUserParams[$paramName]; if($ParamVal -eq $null){ write-warning "Value is null. Setting as str value!"; $NewVal = $paramValStr return; } else { $NewVal = $ParamVal.getType()::Parse($paramValStr); } $ChatUserParams[$paramName] = $NewVal; write-host "Changed PARAM $paramName from $ParamVal to $NewVal" } "/reqs" { $OldVal = $MaxRequests; Set-Variable -Scope 1 -Name MaxRequests -Value [int]$remain; write-host "Changed Maxtokens from $OldVal to $MaxRequests" } "/ps" { function prompt(){ "PowershAI>> " } write-warning "Entering in nested prompt"; $host.EnterNestedPrompt(); } "/model" { write-host "Changing model..."; $newModel = $remain; $chosenModel = $null; #find all candidates! while($true){ $ElegibleModels = @( $SupportedModels | ? { $_.id -like $newModel } ); if($ElegibleModels.length -eq 1){ $model = $ElegibleModels[0].id; break; } write-warning "Refine your search (search:$newModel)"; write-host $($ElegibleModels|sort id|out-string) $newModel = read-host "Filter model" } $ChatUserParams['model'] = $model write-host " Changed model to: $model" } default { $result.action = "prompt"; $result.prompt = $prompt; } } return $result; } $ChatContext = @{ messages = @() size = 0 } function AddContext($msg) { while($ChatContext.size -ge $MaxContextSize -and $ChatContext.messages){ $removed,$LeftMessages = $ChatContext.messages; $ChatContext.messages = $LeftMessages; $RemovedCount = $removed.length; if($removed.content){ $RemovedCount = $removed.content.length; } write-verbose "Removing Length: $removed $RemovedCount" $ChatContext.size -= [int]$RemovedCount; } $ChatContext.messages += @($msg); if($msg.content){ $ChatContext.size += $msg.content.length; } else { $ChatContext.size += $msg.length; } } $LoopNum = 0; :MainLoop while($true){ try { write-verbose "Verbose habilitado..." -Verbose:$VerboseEnabled; $LoopNum++; if($LoopNum -eq 1){ $Prompt = @( "Olá, estou conversando com você a partir do PowershAI, um módulo powershell para interagir com IA" ) if($UserFirstName){ $Prompt += "Meu nome é: "+$UserFirstName; } $Prompt = $Prompt -Join "`n"; } else { if($UserFirstName){ $PromptLabel = $UserFirstName } else { $PromptLabel = "Você" } write-host -NoNewLine "$PromptLabel>>> " $prompt = read-host } $parsedPrompt = ParsePrompt $Prompt switch($parsedPrompt.action){ "stop" { break MainLoop; } "prompt" { $prompt = $parsedPrompt.prompt; } default { continue MainLoop; } } AddContext "u: $prompt"; $Msg = @( @($SystemMessages|%{"s: $_"}) "u: $prompt" ) if($ShowFullSend){ write-host -ForegroundColor green "YouSending:`n$($Msg|out-string)" } $ChatParams = $ChatUserParams + @{ prompt = $ChatContext.messages Functions = $Funcs Stream = $MustStream on = @{ send = { if($VerboseEnabled){ write-host -ForegroundColor DarkYellow "-- waiting model... --"; } } stream = {param($inter,$evt) WriteModelAnswer $inter $evt} answer = {param($inter,$evt) WriteModelAnswer $inter $evt} func = { param($interaction) $ans = $interaction.rawAnswer; $model = $ans.model; $funcName = $interaction.toolResults[-1].name write-host -ForegroundColor Blue "$funcName{..." -NoNewLine } exec = { param($interaction) write-host -ForegroundColor Blue "}" write-host "" } } } if($VerboseEnabled){ $ChatParams.Verbose = $true; } $Start = (Get-Date); $Ret = Invoke-OpenAiChatFunctions @ChatParams; $End = Get-Date; $Total = $End-$Start; foreach($interaction in $Ret.interactions){ if($interaction.stream){ $Msg = $interaction.rawAnswer.message } else { $Msg = $interaction.rawAnswer.choices[0].message; } AddContext $Msg $toolsResults = $interaction.toolResults; if($toolsResults){ $toolsResults | %{ AddContext $_.resp } } } #Store the historu of chats! $ChatMetadata.history += @{ Ret = $Ret Elapsed = $Total } $ChatHistory = $ChatMetadata.history; $AllAnswers = @($Ret.interactions | %{$_.rawAnswer}); $AnswersCost = $Ret.costs # current chat $AnswerCount = $AllAnswers.count; $TotalCost = $AnswersCost.total $InputCost = $AnswersCost.input $OutputCost = $AnswersCost.output $TotalTokens = $AnswersCost.tokensTotal $InputTokens = $AnswersCost.tokensInput $OutputTokens = $AnswersCost.tokensOutput # all chats $ChatStats.TotalChats += $ChatHistory.count; $TotalChats = $ChatStats.TotalChats ## COSTS (total, input,output) $ChatStats.TotalCost += $TotalCost $ChatStats.TotalInput += $InputCost $ChatStats.TotalOutput += $OutputCost ## TOKENS QTY (total, input, output) $ChatStats.TotalTokens += $TotalTokens $ChatStats.TotalTokensI += $InputTokens $ChatStats.TotalTokensO += $OutputTokens $AnswerLog = @( "Answer: $AnswerCount" "Cost: $TotalCost (i:$InputCost/o:$OutputCost)" "Tokens: $TotalTokens (i:$InputTokens/o:$OutputTokens)" "time: $($Total.totalMilliseconds) ms" ) -Join " " $HistoryLog = @( "AllChats: $($ChatStats.TotalChats)" @( "Cost: $($ChatStats.TotalCost)" "(i:$($ChatStats.TotalInput)" "/" "o:$($ChatStats.TotalOutput))" ) -Join "" @( "Tokens: $($ChatStats.TotalTokens)" "(i:$($ChatStats.TotalTokensI)" "/" "o:$($ChatStats.TotalTokensO))" ) -Join "" ) -Join " " if($ShowTokenStats){ write-host "** $AnswerLog" write-host "** $HistoryLog" } } catch { write-host ($_|out-string) write-host "==== ERROR STACKTRACE ===" write-host "StackTrace: " $_.ScriptStackTrace; } } return $ChatHistory; } Set-Alias -Name Chatest -Value Invoke-PowershaiChat; Set-Alias -Name PowerShait -Value Invoke-PowershaiChat; Set-Alias -Name PowerShai -Value Invoke-PowershaiChat; Set-Alias -Name ia -Value Invoke-PowershaiChat; Export-ModuleMember -Function * -Alias * -Cmdlet * |