public/Invoke-HelpChat.ps1
function Invoke-HelpChat { <# .SYNOPSIS Initiates a chat interaction with an AI assistant for PowerShell module help. .DESCRIPTION Invoke-HelpChat sends a message to an AI assistant specialized in a specific PowerShell module and retrieves the response. It supports various options such as embedding hints and returning the response in different formats. This function leverages OpenAI's language models to provide intelligent, context-aware help for PowerShell modules. .PARAMETER Message The message or question to send to the assistant. Accepts input from the pipeline and can be provided without quotes. .PARAMETER Module The name of the PowerShell module to get help for. If provided, the assistant name will be automatically generated as "<Module> Copilot". .PARAMETER AssistantName The specific name of the assistant to interact with. If not provided, the Module parameter must be specified. .PARAMETER As Specifies the format of the response. Valid values are 'String' and 'PSObject'. Defaults to 'String'. - String: Returns the assistant's response as a simple string. - PSObject: Returns a detailed object including the question, answer, and token usage information. .PARAMETER AddHint Adds embedding hints to the interaction, which can enhance the response quality by providing more context to the AI model. .PARAMETER NoYell Suppresses the "USE YOUR RETRIEVAL DOCUMENTS!!!" message that is normally appended to the user message. .EXAMPLE PS C:\> Set-ModuleAssistant -Module dbatools PS C:\> askhelp how do I backup a database? Sets dbatools as the default module and uses the askhelp alias to ask about database backups. .EXAMPLE PS C:\> Invoke-HelpChat "How can I copy files recursively?" -Module Microsoft.PowerShell.Management -As PSObject Asks about copying files recursively in the Microsoft.PowerShell.Management module and returns a detailed PSObject response. .EXAMPLE PS C:\> "Generate an AI summary of this text" | Invoke-HelpChat -AssistantName "PSOpenAI Assistant" Uses pipeline input to send a request to a specific assistant named "PSOpenAI Assistant". .EXAMPLE PS C:\> Invoke-HelpChat List all SQL Server instances -Module dbatools -AddHint Demonstrates using the function without quotes around the message and with the AddHint parameter for enhanced context. #> [CmdletBinding()] param ( [Parameter(Mandatory, ValueFromRemainingArguments, ValueFromPipeline, Position = 0)] [Alias("Text")] [string]$Message, [string]$Module, [Parameter(ValueFromPipelineByPropertyName)] [Alias("Assistant")] [string]$AssistantName, [ValidateSet("String", "PSObject")] [string]$As = "String", [switch]$AddHint, [switch]$NoYell ) begin { if (-not $Module -and -not $AssistantName) { return } $PSDefaultParameterValues['Write-Progress:Activity'] = "Getting answer" if ($Module) { $AssistantName = "$Module Copilot" } $hashkey = (Get-OpenAIProvider -PlainText).ApiKey + "-" + $AssistantName $embeddinghash = @{} if (-not $script:threadcache[$hashkey]) { if ($AddHint) { if (-not (Get-LocalVectorStoreFile -Module $Module)) { Write-Warning "No local vector store found for $Module. Running first time setup..." $null = Initialize-LocalVectorStore -Module $Module } foreach ($embedding in (Get-LocalVectorStoreFile -Module $Module)) { $null = $embeddinghash.Add($embedding.Command, $embedding.Embedding) } } $cacheobject = [PSCustomObject]@{ thread = New-Thread assistant = $null embeddings = $embeddinghash } $script:threadcache[$hashkey] = $cacheobject } else { $thread = $script:threadcache[$hashkey].thread $agent = $script:threadcache[$hashkey].assistant if ($AddHint) { if (-not (Get-LocalVectorStoreFile -Module $Module)) { Write-Warning "No local vector store found for $Module. Running first time setup..." $null = Initialize-LocalVectorStore -Module $Module } foreach ($embedding in (Get-LocalVectorStoreFile -Module $Module)) { $null = $embeddinghash.Add($embedding.Command, $embedding.Embedding) } } $script:threadcache[$hashkey].embeddings = $embeddinghash } $thread = $script:threadcache[$hashkey].thread if ($script:threadcache[$hashkey].assistant.model) { Write-Verbose "Using model $($script:threadcache[$hashkey].assistant.model)" $PSDefaultParameterValues['*:Model'] = $script:threadcache[$hashkey].assistant.model $PSDefaultParameterValues['*:Deployment'] = $script:threadcache[$hashkey].assistant.model } $totalMessages = $Message.Count $processedMessages = 0 $sentence = @() $msgs = @() } process { # has to be here too in addition to begin block if (-not $Module -and -not $AssistantName) { Write-Warning "You must provide either the Module or AssistantName parameter." return } # test for single word or single character messages if ($Message -match '^\w+$' -or $Message -match '^\w{1}$') { $sentence += "$Message" } else { $msgs += $Message } } end { if ($sentence.Length -gt 0) { $msgs += "$sentence" } foreach ($msg in $msgs) { Write-Progress -Status "Processing message $($processedMessages + 1) of $totalMessages" -PercentComplete ((1 / 10) * 100) if (-not $agent) { Write-Progress -Status "Retrieving or creating assistant" -PercentComplete ((2 / 10) * 100) $agent = Get-Assistant -All | Where-Object Name -eq $AssistantName | Select-Object -First 1 if (-not $agent) { throw "No assistant found with the name $AssistantName. You can create one using New-ModuleAssistant." } $script:threadcache[$hashkey].assistant = $agent } Write-Progress -Status "Waiting for response" -PercentComplete ((2 / 10) * 100) if ($AddHint) { Write-Verbose "Checking for embeddings in the vector store for $Module" $queryEmbedding = (Request-Embeddings -Text $msg -Model text-embedding-3-small).data.embedding $compare = Compare-Embedding -QueryEmbedding $queryEmbedding -Embeddings $script:threadcache[$hashkey].embeddings -Top 5 $msg = "## Retrieved Suggestions $($compare.Command -join ', ') ## User Question $msg" } elseif (-not $NoYell) { $msg = $msg + "`r`nOutput in plain-text. Markdown is forbidden." # lol look, i'm desperate $msg = $msg + "`r`nUSE YOUR RETRIEVAL DOCUMENTS!!!" } $null = Add-ThreadMessage -ThreadId $thread.id -Role user -Message $msg if ($agent.tool_resources.file_search.vector_store_ids.count -ne 0) { $vfsid = $agent.tool_resources.file_search.vector_store_ids | Select-Object -First 1 } $params = @{ Stream = $false Message = $msg VectorStoresForFileSearch = ($vfsid | Select-Object -First 1) MaxCompletionTokens = 2048 Assistant = $agent.id } $response = Start-ThreadRun @params -Outvariable run | Receive-ThreadRun -Wait while ($msg -eq $response.Messages[-1].SimpleContent.Content) { # azure FAQ - up quota, pick module Write-Warning "Ran into an issue, retrying..." $response = Get-ThreadRun -ThreadId $thread.id if ($response.last_error.code -eq "rate_limit_exceeded") { $sleep = $response.last_error.message -replace '\D' Write-Warning "Rate limit exceeded, sleeping for $sleep seconds..." Write-Warning "If using Azure, consider increasing your quota" Start-Sleep -Seconds ($sleep + 1) } else { # extract error and throw throw $response.last_error.message } } if ($As -eq "String") { $response.Messages[-1].SimpleContent.Content } elseif ($As -eq "PSObject") { $run = Get-ThreadRun -ThreadId $run.thread_id -RunId $run.id [PSCustomObject]@{ Assistant = $agent.Name Question = $msg Answer = $response.Messages[-1].SimpleContent.Content PromptTokens = $run.usage.prompt_tokens Completion = $run.usage.completion_tokens TotalTokens = $run.usage.total_tokens Response = $response } } } } } |