When you enter a command that does not exist, it is automatically replaced with a query to the LLM.
このように、存在しないコマンドを入力すると、LLMへのクエリに自動置換される。
| <# | |
| This script customizes the PowerShell command line to detect when an entered command is not found. | |
| If a command is not recognized, it automatically rewrites the line to call a LLM assistant for help. | |
| To run this script, execute it in the console as follows: | |
| . .\copilot_shell.ps1 | |
| このスクリプトは、PowerShellのコマンドラインをカスタマイズし、入力されたコマンドが見つからない場合に検出します。 | |
| コマンドが見つからない場合、自動的にLLMアシスタントに問い合わせる形に行を書き換えます。 | |
| コンソールで | |
| . .\copilot_shell.ps1 | |
| のように実行します。 | |
| 【APIのURLとAPIKeyの指定方法】 | |
| - APIのURLは、Get-LLMResponse関数の-Uriパラメータで指定できます。省略時は環境変数OPENAI_API_URI、なければ https://api.openai.com/v1/chat/completions が使われます。 | |
| - APIKeyは-ApiKeyパラメータで指定できます。省略時は環境変数OPENAI_API_KEYが参照されます。指定も環境変数もない場合は認証ヘッダーは付与されません。 | |
| #> | |
| Set-PSReadLineKeyHandler -Chord Enter -ScriptBlock { | |
| $line = $null | |
| $cursor = $null | |
| [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, | |
| [ref]$cursor) | |
| try | |
| { | |
| $sb = [scriptblock]::Create($line) | |
| $command_name = $sb.Ast.EndBlock.Statements[0].PipelineElements[0].CommandElements[0].Value | |
| $command = Get-Command $command_name -ErrorAction SilentlyContinue | |
| if ($command -eq $null) | |
| { | |
| [Microsoft.PowerShell.PSConsoleReadLine]::RevertLine() | |
| [Microsoft.PowerShell.PSConsoleReadLine]::Insert("?? $line") | |
| } | |
| } | |
| catch | |
| { | |
| } | |
| [Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine() | |
| } | |
| $global:messages = @() | |
| # Function to generate text from LLM | |
| function Get-LLMResponse | |
| { | |
| param ( | |
| [Parameter(Mandatory = $true)] | |
| [string]$Prompt, | |
| [switch]$NoStream, | |
| [string]$Uri, | |
| [string]$ApiKey, | |
| [string]$Model = "gpt-4.1-mini" | |
| ) | |
| # Set API URL | |
| if (-not $Uri) | |
| { | |
| $Uri = ${env:OPENAI_API_URI} | |
| if (-not $Uri) | |
| { | |
| $Uri = "https://api.openai.com/v1/chat/completions" | |
| } | |
| } | |
| # Set API Key from environment if not provided | |
| if (-not $ApiKey) | |
| { | |
| $ApiKey = $env:OPENAI_API_KEY | |
| } | |
| # Set system message according to locale | |
| $locale = [System.Globalization.CultureInfo]::CurrentUICulture.Name | |
| if ($locale -like "ja*") | |
| { | |
| $system_message = [PSCustomObject]@{ | |
| "role" = "system" | |
| "content" = "あなたは優秀なアシスタントです。ユーザーの質問に答えてください。" | |
| } | |
| } | |
| else | |
| { | |
| $system_message = [PSCustomObject]@{ | |
| "role" = "system" | |
| "content" = "You are an excellent assistant. Please answer the user's questions." | |
| } | |
| } | |
| # First, add the current user message | |
| $global:messages += [PSCustomObject]@{ | |
| "role" = "user" | |
| "content" = $Prompt | |
| } | |
| # Remove old messages so that the total number of characters does not exceed 5000 | |
| $maxLength = 5000 | |
| $new_messages = @() | |
| $totalLength = $system_message.content.Length | |
| $cursor = 0 | |
| # Remove system message | |
| $global:messages = $global:messages[1..($global:messages.Count - 1)] | |
| while ($true) | |
| { | |
| $cursor-- | |
| $message = $global:messages[$cursor] | |
| $totalLength += $message.content.Length | |
| if (-not $message -or $totalLength -gt $maxLength) | |
| { | |
| $new_messages = @($system_message) + $new_messages | |
| break | |
| } | |
| else | |
| { | |
| $new_messages = @($message) + $new_messages | |
| } | |
| } | |
| # Rebuild messages | |
| $global:messages = $new_messages | |
| # Request body | |
| $body = @{ | |
| "model" = $Model | |
| "messages" = $global:messages | |
| "stream" = -not $NoStream | |
| } | ConvertTo-Json -Depth 10 | |
| if (-not $NoStream) | |
| { | |
| # Streaming response | |
| $client = New-Object System.Net.Http.HttpClient | |
| $request = [System.Net.Http.HttpRequestMessage]::new() | |
| $request.Method = "POST" | |
| $request.RequestUri = $Uri | |
| $request.Headers.Clear() | |
| if ($ApiKey) | |
| { | |
| $request.Headers.Add("Authorization", "Bearer $ApiKey") | |
| } | |
| $request.Content = [System.Net.Http.StringContent]::new(($body), [System.Text.Encoding]::UTF8) | |
| $request.Content.Headers.Clear() | |
| $request.Content.Headers.Add("Content-Type", "application/json;chatset=utf-8") | |
| $task = $client.Send($request) | |
| $response = $task.Content.ReadAsStream() | |
| $reader = [System.IO.StreamReader]::new($response) | |
| $result = "" | |
| while ($true) | |
| { | |
| $line = $reader.ReadLine() | |
| if (($line -eq $null) -or ($line -eq "data: [DONE]")) { break } | |
| $chunk = ($line -replace "data: ", "" | ConvertFrom-Json).choices.delta.content | |
| Write-Host $chunk -NoNewline | |
| $result += $chunk | |
| Start-Sleep -Milliseconds 1 | |
| } | |
| Write-Host "" | |
| $reader.Close() | |
| $reader.Dispose() | |
| # Add AI response to history | |
| if ($result) | |
| { | |
| $global:messages += [PSCustomObject]@{ | |
| "role" = "assistant" | |
| "content" = $result | |
| } | |
| } | |
| } | |
| else | |
| { | |
| # Normal response | |
| $headers = @{ | |
| "Content-Type" = "application/json" | |
| } | |
| if ($ApiKey) | |
| { | |
| $headers["Authorization"] = "Bearer $ApiKey" | |
| } | |
| $response = Invoke-RestMethod -Uri $Uri -Headers $headers -Method POST -Body $body | |
| $result = $response.choices[0].message.content | |
| if ($result) | |
| { | |
| $global:messages += [PSCustomObject]@{ | |
| "role" = "assistant" | |
| "content" = $result | |
| } | |
| } | |
| return $result | |
| } | |
| } | |
| function ?? | |
| { | |
| $prompt = $args -join " " | |
| Get-LLMResponse -Prompt $prompt | |
| } |