
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 | |
} |