From 88f07ebc4915258f27e2c64015ebb74e7372d42f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Oct 2025 21:43:22 +0000 Subject: [PATCH 1/5] Initial plan From b2066c974bc28fa676326e5944b173e37a89b8fa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Oct 2025 21:54:45 +0000 Subject: [PATCH 2/5] Add PSToolboxAI spike implementation with toolbox discovery and sample tools Co-authored-by: dfinke <67258+dfinke@users.noreply.github.com> --- spikes/PSToolboxAI | 1 - .../PSToolboxAI/Examples/01-BasicToolbox.ps1 | 91 ++++++ .../Examples/02-ConvenienceFunction.ps1 | 38 +++ spikes/PSToolboxAI/PSToolboxAI.psd1 | 30 ++ spikes/PSToolboxAI/PSToolboxAI.psm1 | 26 ++ spikes/PSToolboxAI/Public/PSToolboxAI.ps1 | 229 ++++++++++++++ spikes/PSToolboxAI/README.md | 280 ++++++++++++++++++ .../tmp/tools/datetime/DateTimeTools.ps1 | 228 ++++++++++++++ .../tmp/tools/filesystem/FileSystemTools.ps1 | 210 +++++++++++++ .../PSToolboxAI/tmp/tools/text/TextTools.ps1 | 217 ++++++++++++++ 10 files changed, 1349 insertions(+), 1 deletion(-) delete mode 160000 spikes/PSToolboxAI create mode 100644 spikes/PSToolboxAI/Examples/01-BasicToolbox.ps1 create mode 100644 spikes/PSToolboxAI/Examples/02-ConvenienceFunction.ps1 create mode 100644 spikes/PSToolboxAI/PSToolboxAI.psd1 create mode 100644 spikes/PSToolboxAI/PSToolboxAI.psm1 create mode 100644 spikes/PSToolboxAI/Public/PSToolboxAI.ps1 create mode 100644 spikes/PSToolboxAI/README.md create mode 100644 spikes/PSToolboxAI/tmp/tools/datetime/DateTimeTools.ps1 create mode 100644 spikes/PSToolboxAI/tmp/tools/filesystem/FileSystemTools.ps1 create mode 100644 spikes/PSToolboxAI/tmp/tools/text/TextTools.ps1 diff --git a/spikes/PSToolboxAI b/spikes/PSToolboxAI deleted file mode 160000 index c7758a2..0000000 --- a/spikes/PSToolboxAI +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c7758a220926c03a24a558dbc2a7bf2851ee01d2 diff --git a/spikes/PSToolboxAI/Examples/01-BasicToolbox.ps1 b/spikes/PSToolboxAI/Examples/01-BasicToolbox.ps1 new file mode 100644 index 0000000..79cfd2b --- /dev/null +++ b/spikes/PSToolboxAI/Examples/01-BasicToolbox.ps1 @@ -0,0 +1,91 @@ +<# +.SYNOPSIS +Basic example of using PSToolboxAI with PSAI agents. + +.DESCRIPTION +This example demonstrates how to: +1. Load the PSToolboxAI module +2. Discover toolbox tools +3. Create an agent with those tools +4. Use the agent to perform tasks + +.EXAMPLE +.\01-BasicToolbox.ps1 + +.EXAMPLE +.\01-BasicToolbox.ps1 -Interactive +Runs in interactive mode. +#> + +param( + [Switch]$Interactive, + [Switch]$ShowToolCalls +) + +# Import PSAI module +$psaiPath = Join-Path $PSScriptRoot ".." ".." ".." "PSAI.psd1" +Import-Module $psaiPath -Force + +# Import PSToolboxAI module +$toolboxPath = Join-Path $PSScriptRoot ".." "PSToolboxAI.psd1" +Import-Module $toolboxPath -Force + +Write-Host "=== PSToolboxAI Example ===" -ForegroundColor Cyan +Write-Host "" + +# Discover available toolbox tools +Write-Host "Discovering toolbox tools..." -ForegroundColor Yellow +$tools = Get-PSAIToolbox -Verbose +Write-Host "Found $($tools.Count) tools" -ForegroundColor Green +Write-Host "" + +# Create an agent with the toolbox tools +$agent = New-Agent ` + -Tools $tools ` + -Instructions "You are a helpful assistant with access to file system, date/time, and text processing tools." ` + -Name "ToolboxAgent" ` + -Description "An agent with toolbox capabilities" ` + -ShowToolCalls:$ShowToolCalls + +if ($Interactive) { + Write-Host "Starting interactive session..." -ForegroundColor Cyan + Write-Host "Try commands like:" -ForegroundColor Yellow + Write-Host " - What files are in the current directory?" -ForegroundColor Gray + Write-Host " - What is the current date and time?" -ForegroundColor Gray + Write-Host " - Count the words in 'Hello world from PowerShell'" -ForegroundColor Gray + Write-Host " - Press Enter (empty) to copy last response and exit" -ForegroundColor Gray + Write-Host "" + + $agent | Invoke-InteractiveCLI +} +else { + Write-Host "Running example queries..." -ForegroundColor Cyan + Write-Host "" + + # Example 1: File system operations + Write-Host "Example 1: File System Operations" -ForegroundColor Yellow + Write-Host "Query: What files are in the current directory?" -ForegroundColor Gray + $response = $agent | Get-AgentResponse "List the files in the current directory" + Write-Host "Response:" -ForegroundColor Green + Write-Host $response + Write-Host "" + + # Example 2: Date/time operations + Write-Host "Example 2: Date/Time Operations" -ForegroundColor Yellow + Write-Host "Query: What is the current date and time?" -ForegroundColor Gray + $response = $agent | Get-AgentResponse "What is the current date and time in ISO8601 format?" + Write-Host "Response:" -ForegroundColor Green + Write-Host $response + Write-Host "" + + # Example 3: Text operations + Write-Host "Example 3: Text Operations" -ForegroundColor Yellow + Write-Host "Query: Analyze text statistics" -ForegroundColor Gray + $response = $agent | Get-AgentResponse "Count the words in this text: 'The quick brown fox jumps over the lazy dog.'" + Write-Host "Response:" -ForegroundColor Green + Write-Host $response + Write-Host "" + + Write-Host "Examples completed!" -ForegroundColor Cyan + Write-Host "Run with -Interactive flag for interactive mode." -ForegroundColor Gray +} diff --git a/spikes/PSToolboxAI/Examples/02-ConvenienceFunction.ps1 b/spikes/PSToolboxAI/Examples/02-ConvenienceFunction.ps1 new file mode 100644 index 0000000..b39c3eb --- /dev/null +++ b/spikes/PSToolboxAI/Examples/02-ConvenienceFunction.ps1 @@ -0,0 +1,38 @@ +<# +.SYNOPSIS +Example using the New-PSAIToolboxAgent convenience function. + +.DESCRIPTION +This example demonstrates the simplified approach using New-PSAIToolboxAgent +which combines tool discovery and agent creation in one step. + +.EXAMPLE +.\02-ConvenienceFunction.ps1 +#> + +# Import PSAI module +$psaiPath = Join-Path $PSScriptRoot ".." ".." ".." "PSAI.psd1" +Import-Module $psaiPath -Force + +# Import PSToolboxAI module +$toolboxPath = Join-Path $PSScriptRoot ".." "PSToolboxAI.psd1" +Import-Module $toolboxPath -Force + +Write-Host "=== PSToolboxAI Convenience Function Example ===" -ForegroundColor Cyan +Write-Host "" + +# Create an agent with auto-discovered toolbox tools in one step +Write-Host "Creating agent with auto-discovered tools..." -ForegroundColor Yellow +$agent = New-PSAIToolboxAgent ` + -Instructions "You are a helpful assistant." ` + -Name "QuickBot" ` + -ShowToolCalls + +Write-Host "Agent created successfully!" -ForegroundColor Green +Write-Host "" + +# Use the agent +Write-Host "Testing the agent..." -ForegroundColor Yellow +$response = $agent | Get-AgentResponse "What is today's date and the count of files in the current directory?" +Write-Host "Response:" -ForegroundColor Green +Write-Host $response diff --git a/spikes/PSToolboxAI/PSToolboxAI.psd1 b/spikes/PSToolboxAI/PSToolboxAI.psd1 new file mode 100644 index 0000000..5da8b69 --- /dev/null +++ b/spikes/PSToolboxAI/PSToolboxAI.psd1 @@ -0,0 +1,30 @@ +@{ + RootModule = 'PSToolboxAI.psm1' + ModuleVersion = '0.1.0' + GUID = 'a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d' + Author = 'PSAI Contributors' + CompanyName = 'PSAI' + Copyright = '© 2025 All rights reserved.' + Description = 'Sourcegraph Toolbox integration for PSAI - Enables discovery and loading of toolbox-style tools for AI agents' + PowerShellVersion = '7.1' + + RequiredModules = @() + + FunctionsToExport = @( + 'Get-PSAIToolboxPath' + 'Get-PSAIToolbox' + 'New-PSAIToolboxAgent' + ) + + CmdletsToExport = @() + VariablesToExport = @() + AliasesToExport = @() + + PrivateData = @{ + PSData = @{ + Tags = @('PowerShell', 'AI', 'Toolbox', 'Sourcegraph', 'PSAI') + LicenseUri = 'https://github.com/dfinke/PSAI/blob/main/LICENSE' + ProjectUri = 'https://github.com/dfinke/PSAI' + } + } +} diff --git a/spikes/PSToolboxAI/PSToolboxAI.psm1 b/spikes/PSToolboxAI/PSToolboxAI.psm1 new file mode 100644 index 0000000..a8bbff5 --- /dev/null +++ b/spikes/PSToolboxAI/PSToolboxAI.psm1 @@ -0,0 +1,26 @@ +<# +.SYNOPSIS +PSToolboxAI - Sourcegraph Toolbox integration for PSAI + +.DESCRIPTION +This module provides integration with Sourcegraph-style toolboxes for PSAI. +Toolboxes are collections of tools that can be discovered and loaded dynamically +for use with AI agents. + +.NOTES +This is a spike implementation demonstrating the Sourcegraph Toolbox pattern in PowerShell. +#> + +# Import PSAI if not already loaded +if (-not (Get-Module PSAI)) { + $psaiPath = Join-Path $PSScriptRoot ".." ".." "PSAI.psd1" + if (Test-Path $psaiPath) { + Import-Module $psaiPath -Force + } +} + +# Import public functions +. $PSScriptRoot/Public/PSToolboxAI.ps1 + +# Set module-level variables if needed +$script:ToolboxCache = @{} diff --git a/spikes/PSToolboxAI/Public/PSToolboxAI.ps1 b/spikes/PSToolboxAI/Public/PSToolboxAI.ps1 new file mode 100644 index 0000000..4d8fa05 --- /dev/null +++ b/spikes/PSToolboxAI/Public/PSToolboxAI.ps1 @@ -0,0 +1,229 @@ +<# +.SYNOPSIS +PowerShell implementation of Sourcegraph Toolbox integration for PSAI. + +.DESCRIPTION +This module provides functions to discover and load Sourcegraph-style toolboxes +for use with PSAI agents. Toolboxes are discovered via the $env:PSAI_TOOLBOX_PATH +environment variable and registered as tools that can be used with New-Agent. + +.NOTES +Sourcegraph Toolbox pattern: +- Tools are defined as JSON specifications +- Tools are discovered via directory scanning +- Tools are registered using Register-Tool +- Tools are integrated via New-Agent -Tools + +.LINK +https://ampcode.com/manual#toolboxes +https://ampcode.com/manual/appendix#toolboxes-reference +#> + +<# +.SYNOPSIS +Gets the toolbox search paths from environment variable. + +.DESCRIPTION +Retrieves toolbox directories from the PSAI_TOOLBOX_PATH environment variable. +If not set, returns a default path relative to this module. + +.EXAMPLE +Get-PSAIToolboxPath +Returns an array of toolbox directory paths. + +.OUTPUTS +System.String[] +#> +function Get-PSAIToolboxPath { + [CmdletBinding()] + param() + + $toolboxPaths = @() + + if ($env:PSAI_TOOLBOX_PATH) { + # Split on semicolon (Windows) or colon (Unix) + $separator = if ($IsWindows -or $PSVersionTable.PSVersion.Major -le 5) { ';' } else { ':' } + $toolboxPaths = $env:PSAI_TOOLBOX_PATH -split $separator | Where-Object { $_ } + } + + # Add default path if no environment variable is set + if ($toolboxPaths.Count -eq 0) { + $defaultPath = Join-Path $PSScriptRoot ".." "tmp" "tools" + $toolboxPaths += $defaultPath + } + + # Resolve and return only existing directories + $toolboxPaths | ForEach-Object { + $resolved = Resolve-Path $_ -ErrorAction SilentlyContinue + if ($resolved) { + $resolved.Path + } + } +} + +<# +.SYNOPSIS +Discovers toolbox tool definitions from specified directories. + +.DESCRIPTION +Scans toolbox directories for PowerShell scripts (.ps1 files) that define tools. +Each tool script should return a tool specification compatible with Register-Tool. + +.PARAMETER Path +Optional specific path to scan. If not provided, uses Get-PSAIToolboxPath. + +.EXAMPLE +Get-PSAIToolbox +Discovers all toolbox tools from configured paths. + +.EXAMPLE +Get-PSAIToolbox -Path "./my-tools" +Discovers toolbox tools from a specific directory. + +.OUTPUTS +System.Object[] +Array of tool definitions ready for New-Agent -Tools. +#> +function Get-PSAIToolbox { + [CmdletBinding()] + param( + [Parameter()] + [string[]]$Path + ) + + if (-not $Path) { + $Path = Get-PSAIToolboxPath + } + + $tools = @() + + foreach ($toolPath in $Path) { + if (Test-Path $toolPath) { + Write-Verbose "Scanning toolbox path: $toolPath" + + # Find all .ps1 files in subdirectories + $toolScripts = Get-ChildItem -Path $toolPath -Filter "*.ps1" -Recurse -File + + foreach ($script in $toolScripts) { + try { + Write-Verbose "Loading tool from: $($script.FullName)" + + # Dot-source the script to load its functions + . $script.FullName + + # The script should define a function that returns tool specifications + # Convention: function name matches filename without extension + $functionName = $script.BaseName + + if (Get-Command $functionName -ErrorAction SilentlyContinue) { + # Call the function to get tool registrations + $toolSpecs = & $functionName + + if ($toolSpecs) { + $tools += $toolSpecs + Write-Verbose "Registered $(@($toolSpecs).Count) tool(s) from $functionName" + } + } + } + catch { + Write-Warning "Failed to load tool from $($script.FullName): $_" + } + } + } + else { + Write-Warning "Toolbox path does not exist: $toolPath" + } + } + + Write-Verbose "Total tools discovered: $($tools.Count)" + return $tools +} + +<# +.SYNOPSIS +Creates a new PSAI agent with toolbox tools automatically loaded. + +.DESCRIPTION +Convenience function that discovers toolbox tools and creates an agent with them. +This combines Get-PSAIToolbox with New-Agent for a streamlined experience. + +.PARAMETER Instructions +Instructions for the agent. + +.PARAMETER ToolboxPath +Optional specific toolbox path(s). If not provided, uses environment configuration. + +.PARAMETER Name +Name of the agent. + +.PARAMETER Description +Description of the agent. + +.PARAMETER ShowToolCalls +Switch to show tool invocations. + +.EXAMPLE +$agent = New-PSAIToolboxAgent -Instructions "You are a helpful assistant" +Creates an agent with all discovered toolbox tools. + +.EXAMPLE +$agent = New-PSAIToolboxAgent -ToolboxPath "./my-tools" -ShowToolCalls +Creates an agent with tools from a specific path and shows tool calls. + +.OUTPUTS +PSCustomObject +An agent object that can be used with Get-AgentResponse and Invoke-InteractiveCLI. +#> +function New-PSAIToolboxAgent { + [CmdletBinding()] + param( + [Parameter()] + [string]$Instructions, + + [Parameter()] + [string[]]$ToolboxPath, + + [Parameter()] + [string]$Name, + + [Parameter()] + [string]$Description, + + [Switch]$ShowToolCalls + ) + + # Discover toolbox tools + if ($ToolboxPath) { + $tools = Get-PSAIToolbox -Path $ToolboxPath + } + else { + $tools = Get-PSAIToolbox + } + + if ($tools.Count -eq 0) { + Write-Warning "No toolbox tools discovered. Agent will be created without tools." + } + + # Create agent with discovered tools + $agentParams = @{ + Tools = $tools + ShowToolCalls = $ShowToolCalls + } + + if ($Instructions) { + $agentParams['Instructions'] = $Instructions + } + + if ($Name) { + $agentParams['Name'] = $Name + } + + if ($Description) { + $agentParams['Description'] = $Description + } + + New-Agent @agentParams +} + +# Export functions +Export-ModuleMember -Function Get-PSAIToolboxPath, Get-PSAIToolbox, New-PSAIToolboxAgent diff --git a/spikes/PSToolboxAI/README.md b/spikes/PSToolboxAI/README.md new file mode 100644 index 0000000..c215f33 --- /dev/null +++ b/spikes/PSToolboxAI/README.md @@ -0,0 +1,280 @@ +# PSToolboxAI - Sourcegraph Toolbox for PSAI + +This is a spike implementation of Sourcegraph Toolbox pattern integration with PSAI, enabling dynamic discovery and loading of tool collections for AI agents. + +## Overview + +PSToolboxAI brings the Sourcegraph Toolbox concept to PowerShell AI agents. Toolboxes are collections of related tools that can be discovered automatically and loaded into agents, providing them with capabilities like file system operations, date/time calculations, text processing, and more. + +## Key Concepts + +### Sourcegraph Toolbox Pattern + +The Sourcegraph Toolbox pattern involves: +1. **Environment-based discovery**: Tools are discovered via `$env:PSAI_TOOLBOX_PATH` +2. **JSON-based registration**: Tools return JSON specifications compatible with OpenAI function calling +3. **Dynamic loading**: Tools are loaded at runtime and registered via `Register-Tool` +4. **Agent integration**: Tools are integrated via `New-Agent -Tools` + +### How It Works + +1. **Discovery**: PSToolboxAI scans directories specified in `$env:PSAI_TOOLBOX_PATH` (or a default path) +2. **Loading**: Each `.ps1` file in the toolbox directories is loaded +3. **Registration**: Tool functions are registered using PSAI's `Register-Tool` function +4. **Integration**: The registered tools are passed to `New-Agent` for use by the AI agent + +## Installation + +Since this is a spike in the PSAI repository: + +```powershell +# Clone or navigate to PSAI repository +cd /path/to/PSAI + +# Import the module +Import-Module ./spikes/PSToolboxAI/PSToolboxAI.psd1 +``` + +## Usage + +### Basic Usage - Auto-discovery + +Use the default toolbox path (no environment variable needed): + +```powershell +# Import PSAI first +Import-Module PSAI + +# Import the toolbox module +Import-Module ./spikes/PSToolboxAI/PSToolboxAI.psd1 + +# Discover and load all toolbox tools +$tools = Get-PSAIToolbox + +# Create an agent with the tools +$agent = New-Agent -Tools $tools -ShowToolCalls + +# Use the agent +$agent | Get-AgentResponse "List files in the current directory" +$agent | Get-AgentResponse "What is the current date and time?" +$agent | Get-AgentResponse "Count the words in this text: 'Hello world from PowerShell AI'" +``` + +### Using Environment Variable + +Configure custom toolbox paths: + +```powershell +# Set the toolbox path (can be multiple paths separated by ; on Windows or : on Unix) +$env:PSAI_TOOLBOX_PATH = "C:\MyTools;C:\SharedTools" + +# Or on Unix/Linux: +$env:PSAI_TOOLBOX_PATH = "/home/user/mytools:/opt/sharedtools" + +# Now discover tools from these paths +$tools = Get-PSAIToolbox + +$agent = New-Agent -Tools $tools +``` + +### Convenience Function + +Use the convenience function to create an agent with toolbox tools in one step: + +```powershell +# Creates an agent with auto-discovered tools +$agent = New-PSAIToolboxAgent -Instructions "You are a helpful file management assistant" -ShowToolCalls + +# Use it interactively +$agent | Invoke-InteractiveCLI +``` + +### Selective Loading + +Load tools from a specific path: + +```powershell +# Load only filesystem tools +$fsTools = Get-PSAIToolbox -Path "./spikes/PSToolboxAI/tmp/tools/filesystem" + +$agent = New-Agent -Tools $fsTools -Name "FileAgent" +``` + +## Included Toolboxes + +### FileSystem Toolbox +Located in: `tmp/tools/filesystem/` + +Tools: +- **Get-DirectoryListing**: List files and directories +- **Read-FileContent**: Read file contents +- **Test-FileExists**: Check if a file or directory exists + +### DateTime Toolbox +Located in: `tmp/tools/datetime/` + +Tools: +- **Get-CurrentDateTime**: Get current date/time in various formats +- **Format-DateTime**: Format date/time strings +- **Get-DateDifference**: Calculate time differences + +### Text Toolbox +Located in: `tmp/tools/text/` + +Tools: +- **Get-TextStatistics**: Analyze text (word count, character count, etc.) +- **Convert-TextCase**: Convert text case (upper, lower, title, sentence) +- **Search-TextPattern**: Search text using regex patterns + +## Creating Your Own Toolbox + +1. **Create a directory** for your toolbox under `tmp/tools/` (or your custom path) +2. **Create a PowerShell script** (e.g., `MyTools.ps1`) with this structure: + +```powershell +# Main registration function (matches filename) +function MyTools { + [CmdletBinding()] + param() + + Write-Verbose "Registering MyTools" + + # Register your tool functions + $tools = @( + Register-Tool -FunctionName My-ToolFunction + ) + + return $tools +} + +# Your actual tool function +function My-ToolFunction { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$Input + ) + + # Do work here + $result = @{ + input = $Input + output = "Processed: $Input" + } + + # Return JSON + return ($result | ConvertTo-Json -Compress) +} + +# Return the registration function +return MyTools +``` + +3. **Tool functions should**: + - Accept parameters that are clearly typed + - Return JSON strings (use `ConvertTo-Json`) + - Handle errors gracefully + - Provide good descriptions in comment-based help + +## Architecture + +``` +PSToolboxAI/ +├── PSToolboxAI.psd1 # Module manifest +├── PSToolboxAI.psm1 # Module loader +├── Public/ +│ └── PSToolboxAI.ps1 # Main functions (Get-PSAIToolbox, etc.) +└── tmp/ + └── tools/ # Default toolbox location + ├── filesystem/ + │ └── FileSystemTools.ps1 + ├── datetime/ + │ └── DateTimeTools.ps1 + └── text/ + └── TextTools.ps1 +``` + +## Environment Variables + +- **`$env:PSAI_TOOLBOX_PATH`**: Path(s) to toolbox directories (optional) + - Multiple paths can be specified (`;` on Windows, `:` on Unix) + - If not set, uses default path: `spikes/PSToolboxAI/tmp/tools` + +- **`$env:PSAI_ENABLE_TOOLBOX`**: Enable/disable toolbox loading (optional) + - Set to `$false` to disable toolbox loading + - Useful for testing or debugging + +## Examples + +### Example 1: File Management Agent + +```powershell +Import-Module PSAI +Import-Module ./spikes/PSToolboxAI/PSToolboxAI.psd1 + +$agent = New-PSAIToolboxAgent ` + -Instructions "You are a file management assistant. Help users with file operations." ` + -Name "FileBot" ` + -ShowToolCalls + +$agent | Get-AgentResponse "What files are in the current directory?" +$agent | Get-AgentResponse "Read the content of README.md" +``` + +### Example 2: Date Calculator + +```powershell +$agent = New-PSAIToolboxAgent -Name "DateBot" + +$agent | Get-AgentResponse "How many days until December 31, 2025?" +$agent | Get-AgentResponse "What day of the week is Christmas 2025?" +``` + +### Example 3: Text Analyzer + +```powershell +$agent = New-PSAIToolboxAgent -Name "TextBot" + +$text = "The quick brown fox jumps over the lazy dog." +$agent | Get-AgentResponse "Analyze this text: $text" +$agent | Get-AgentResponse "Convert 'hello world' to title case" +``` + +## Integration with PSAI + +PSToolboxAI integrates seamlessly with PSAI's existing infrastructure: + +- Uses `Register-Tool` for tool registration +- Compatible with `New-Agent` function +- Works with `Get-AgentResponse` and `Invoke-InteractiveCLI` +- Follows PSAI's tool specification format + +## Reference Links + +- [Sourcegraph Toolboxes Manual](https://ampcode.com/manual#toolboxes) +- [Sourcegraph Toolboxes Reference](https://ampcode.com/manual/appendix#toolboxes-reference) +- [PSAI Repository](https://github.com/dfinke/PSAI) + +## Future Enhancements + +Potential improvements for this spike: + +1. **Tool metadata**: Add more descriptive metadata to tools +2. **Tool versioning**: Support versioned toolboxes +3. **Tool dependencies**: Handle dependencies between tools +4. **Tool discovery optimization**: Cache discovered tools +5. **Remote toolboxes**: Load toolboxes from remote URLs +6. **Tool validation**: Validate tool specifications before registration +7. **Tool documentation**: Auto-generate documentation from tools + +## Contributing + +This is a spike/proof-of-concept. To contribute: + +1. Add new toolboxes under `tmp/tools/` +2. Follow the existing pattern +3. Test with PSAI agents +4. Document your toolbox in this README + +## License + +This code is part of the PSAI project and follows the same license (MIT). diff --git a/spikes/PSToolboxAI/tmp/tools/datetime/DateTimeTools.ps1 b/spikes/PSToolboxAI/tmp/tools/datetime/DateTimeTools.ps1 new file mode 100644 index 0000000..0827b2c --- /dev/null +++ b/spikes/PSToolboxAI/tmp/tools/datetime/DateTimeTools.ps1 @@ -0,0 +1,228 @@ +<# +.SYNOPSIS +Date and time toolbox for PSAI agents. + +.DESCRIPTION +Provides date and time operations as tools for AI agents. +Tools include: getting current time, formatting dates, calculating date differences. + +.NOTES +This toolbox is automatically discovered when $env:PSAI_TOOLBOX_PATH is set +or when using the default toolbox path. +#> + +function DateTimeTools { + [CmdletBinding()] + param() + + # Check if we should register tools based on environment variable + if ($env:PSAI_ENABLE_TOOLBOX -eq $false) { + Write-Verbose "Toolbox registration skipped (PSAI_ENABLE_TOOLBOX is false)" + return @() + } + + Write-Verbose "Registering DateTime toolbox tools" + + # Register the tools using PSAI's Register-Tool function + $tools = @( + Register-Tool -FunctionName Get-CurrentDateTime + Register-Tool -FunctionName Format-DateTime + Register-Tool -FunctionName Get-DateDifference + ) + + return $tools +} + +<# +.SYNOPSIS +Gets the current date and time. + +.DESCRIPTION +Returns the current date and time in various formats. + +.PARAMETER Format +The format to return: ISO8601, Unix, Readable, or All (default). + +.PARAMETER TimeZone +Optional timezone name (e.g., "UTC", "Eastern Standard Time"). + +.EXAMPLE +Get-CurrentDateTime -Format ISO8601 +Returns current time in ISO8601 format. +#> +function Get-CurrentDateTime { + [CmdletBinding()] + param( + [Parameter()] + [ValidateSet('ISO8601', 'Unix', 'Readable', 'All')] + [string]$Format = 'All', + + [Parameter()] + [string]$TimeZone + ) + + try { + $now = Get-Date + + if ($TimeZone) { + try { + $tz = [System.TimeZoneInfo]::FindSystemTimeZoneById($TimeZone) + $now = [System.TimeZoneInfo]::ConvertTime($now, $tz) + } + catch { + Write-Warning "Invalid timezone: $TimeZone, using local time" + } + } + + $result = @{ + requested_format = $Format + timezone = if ($TimeZone) { $TimeZone } else { 'Local' } + } + + switch ($Format) { + 'ISO8601' { + $result['datetime'] = $now.ToString("o") + } + 'Unix' { + $result['datetime'] = [int][double]::Parse(($now.ToUniversalTime() - (Get-Date "1970-01-01")).TotalSeconds) + } + 'Readable' { + $result['datetime'] = $now.ToString("yyyy-MM-dd HH:mm:ss") + } + 'All' { + $result['iso8601'] = $now.ToString("o") + $result['unix'] = [int][double]::Parse(($now.ToUniversalTime() - (Get-Date "1970-01-01")).TotalSeconds) + $result['readable'] = $now.ToString("yyyy-MM-dd HH:mm:ss") + $result['day_of_week'] = $now.DayOfWeek.ToString() + } + } + + return ($result | ConvertTo-Json -Depth 2 -Compress) + } + catch { + return (@{ + error = $_.Exception.Message + } | ConvertTo-Json -Compress) + } +} + +<# +.SYNOPSIS +Formats a date/time string. + +.DESCRIPTION +Converts a date/time string from one format to another. + +.PARAMETER DateTime +The date/time to format (string or DateTime object). + +.PARAMETER OutputFormat +The desired output format. + +.EXAMPLE +Format-DateTime -DateTime "2025-01-01" -OutputFormat "yyyy-MM-dd HH:mm:ss" +Formats the date in the specified format. +#> +function Format-DateTime { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$DateTime, + + [Parameter()] + [string]$OutputFormat = "yyyy-MM-dd HH:mm:ss" + ) + + try { + $dt = [DateTime]::Parse($DateTime) + + $result = @{ + input = $DateTime + output_format = $OutputFormat + formatted = $dt.ToString($OutputFormat) + day_of_week = $dt.DayOfWeek.ToString() + } + + return ($result | ConvertTo-Json -Depth 2 -Compress) + } + catch { + return (@{ + error = $_.Exception.Message + input = $DateTime + } | ConvertTo-Json -Compress) + } +} + +<# +.SYNOPSIS +Calculates the difference between two dates. + +.DESCRIPTION +Returns the time span between two dates in various units. + +.PARAMETER StartDate +The start date/time. + +.PARAMETER EndDate +The end date/time (defaults to now if not specified). + +.PARAMETER Unit +The unit to return: Days, Hours, Minutes, Seconds, or All (default). + +.EXAMPLE +Get-DateDifference -StartDate "2025-01-01" -Unit Days +Returns the number of days between the start date and now. +#> +function Get-DateDifference { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$StartDate, + + [Parameter()] + [string]$EndDate, + + [Parameter()] + [ValidateSet('Days', 'Hours', 'Minutes', 'Seconds', 'All')] + [string]$Unit = 'All' + ) + + try { + $start = [DateTime]::Parse($StartDate) + $end = if ($EndDate) { [DateTime]::Parse($EndDate) } else { Get-Date } + + $diff = $end - $start + + $result = @{ + start_date = $start.ToString("o") + end_date = $end.ToString("o") + unit = $Unit + } + + switch ($Unit) { + 'Days' { $result['difference'] = $diff.TotalDays } + 'Hours' { $result['difference'] = $diff.TotalHours } + 'Minutes' { $result['difference'] = $diff.TotalMinutes } + 'Seconds' { $result['difference'] = $diff.TotalSeconds } + 'All' { + $result['days'] = $diff.TotalDays + $result['hours'] = $diff.TotalHours + $result['minutes'] = $diff.TotalMinutes + $result['seconds'] = $diff.TotalSeconds + $result['readable'] = "$($diff.Days) days, $($diff.Hours) hours, $($diff.Minutes) minutes" + } + } + + return ($result | ConvertTo-Json -Depth 2 -Compress) + } + catch { + return (@{ + error = $_.Exception.Message + start_date = $StartDate + end_date = $EndDate + } | ConvertTo-Json -Compress) + } +} + +# Return the toolbox registration function +return DateTimeTools diff --git a/spikes/PSToolboxAI/tmp/tools/filesystem/FileSystemTools.ps1 b/spikes/PSToolboxAI/tmp/tools/filesystem/FileSystemTools.ps1 new file mode 100644 index 0000000..1efae50 --- /dev/null +++ b/spikes/PSToolboxAI/tmp/tools/filesystem/FileSystemTools.ps1 @@ -0,0 +1,210 @@ +<# +.SYNOPSIS +File system toolbox for PSAI agents. + +.DESCRIPTION +Provides file system operations as tools for AI agents. +Tools include: listing files, reading file content, checking file existence. + +.NOTES +This toolbox is automatically discovered when $env:PSAI_TOOLBOX_PATH is set +or when using the default toolbox path. +#> + +function FileSystemTools { + [CmdletBinding()] + param() + + # Check if we should register tools based on environment variable + # This follows the Sourcegraph Toolbox pattern + if ($env:PSAI_ENABLE_TOOLBOX -eq $false) { + Write-Verbose "Toolbox registration skipped (PSAI_ENABLE_TOOLBOX is false)" + return @() + } + + Write-Verbose "Registering FileSystem toolbox tools" + + # Register the tools using PSAI's Register-Tool function + $tools = @( + Register-Tool -FunctionName Get-DirectoryListing + Register-Tool -FunctionName Read-FileContent + Register-Tool -FunctionName Test-FileExists + ) + + return $tools +} + +<# +.SYNOPSIS +Gets a listing of files and directories. + +.DESCRIPTION +Lists files and directories in the specified path, similar to Get-ChildItem +but optimized for AI agent use with JSON output. + +.PARAMETER Path +The path to list. Defaults to current directory. + +.PARAMETER Filter +Optional filter pattern (e.g., "*.ps1"). + +.PARAMETER Recurse +If specified, recursively lists subdirectories. + +.EXAMPLE +Get-DirectoryListing -Path "C:\Scripts" -Filter "*.ps1" +Lists all PowerShell scripts in C:\Scripts. +#> +function Get-DirectoryListing { + [CmdletBinding()] + param( + [Parameter()] + [string]$Path = ".", + + [Parameter()] + [string]$Filter = "*", + + [Parameter()] + [switch]$Recurse + ) + + try { + $params = @{ + Path = $Path + Filter = $Filter + } + + if ($Recurse) { + $params['Recurse'] = $true + } + + $items = Get-ChildItem @params | Select-Object -Property Name, FullName, Length, LastWriteTime, @{ + Name = 'Type' + Expression = { if ($_.PSIsContainer) { 'Directory' } else { 'File' } } + } + + $result = @{ + path = $Path + filter = $Filter + count = $items.Count + items = $items + } + + return ($result | ConvertTo-Json -Depth 5 -Compress) + } + catch { + return (@{ + error = $_.Exception.Message + path = $Path + } | ConvertTo-Json -Compress) + } +} + +<# +.SYNOPSIS +Reads the content of a file. + +.DESCRIPTION +Reads and returns the content of the specified file as a string. + +.PARAMETER Path +The path to the file to read. + +.PARAMETER Encoding +The encoding to use (default: UTF8). + +.EXAMPLE +Read-FileContent -Path "C:\Scripts\script.ps1" +Reads the content of script.ps1. +#> +function Read-FileContent { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$Path, + + [Parameter()] + [string]$Encoding = "UTF8" + ) + + try { + if (Test-Path $Path -PathType Leaf) { + $content = Get-Content -Path $Path -Raw -Encoding $Encoding + $fileInfo = Get-Item $Path + + $result = @{ + path = $Path + size = $fileInfo.Length + lastModified = $fileInfo.LastWriteTime.ToString("o") + content = $content + } + + return ($result | ConvertTo-Json -Depth 3 -Compress) + } + else { + return (@{ + error = "File not found or is not a file" + path = $Path + } | ConvertTo-Json -Compress) + } + } + catch { + return (@{ + error = $_.Exception.Message + path = $Path + } | ConvertTo-Json -Compress) + } +} + +<# +.SYNOPSIS +Tests if a file or directory exists. + +.DESCRIPTION +Checks whether the specified path exists and returns information about it. + +.PARAMETER Path +The path to test. + +.EXAMPLE +Test-FileExists -Path "C:\Scripts\script.ps1" +Checks if script.ps1 exists. +#> +function Test-FileExists { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$Path + ) + + try { + $exists = Test-Path $Path + + $result = @{ + path = $Path + exists = $exists + } + + if ($exists) { + $item = Get-Item $Path + $result['type'] = if ($item.PSIsContainer) { 'Directory' } else { 'File' } + $result['lastModified'] = $item.LastWriteTime.ToString("o") + + if (-not $item.PSIsContainer) { + $result['size'] = $item.Length + } + } + + return ($result | ConvertTo-Json -Depth 2 -Compress) + } + catch { + return (@{ + error = $_.Exception.Message + path = $Path + exists = $false + } | ConvertTo-Json -Compress) + } +} + +# Return the toolbox registration function +return FileSystemTools diff --git a/spikes/PSToolboxAI/tmp/tools/text/TextTools.ps1 b/spikes/PSToolboxAI/tmp/tools/text/TextTools.ps1 new file mode 100644 index 0000000..da73b63 --- /dev/null +++ b/spikes/PSToolboxAI/tmp/tools/text/TextTools.ps1 @@ -0,0 +1,217 @@ +<# +.SYNOPSIS +Text processing toolbox for PSAI agents. + +.DESCRIPTION +Provides text processing operations as tools for AI agents. +Tools include: counting words, converting case, searching text. + +.NOTES +This toolbox is automatically discovered when $env:PSAI_TOOLBOX_PATH is set +or when using the default toolbox path. +#> + +function TextTools { + [CmdletBinding()] + param() + + # Check if we should register tools based on environment variable + if ($env:PSAI_ENABLE_TOOLBOX -eq $false) { + Write-Verbose "Toolbox registration skipped (PSAI_ENABLE_TOOLBOX is false)" + return @() + } + + Write-Verbose "Registering Text toolbox tools" + + # Register the tools using PSAI's Register-Tool function + $tools = @( + Register-Tool -FunctionName Get-TextStatistics + Register-Tool -FunctionName Convert-TextCase + Register-Tool -FunctionName Search-TextPattern + ) + + return $tools +} + +<# +.SYNOPSIS +Gets statistics about text content. + +.DESCRIPTION +Analyzes text and returns statistics like word count, character count, line count, etc. + +.PARAMETER Text +The text to analyze. + +.EXAMPLE +Get-TextStatistics -Text "Hello world! This is a test." +Returns statistics about the text. +#> +function Get-TextStatistics { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$Text + ) + + try { + $lines = $Text -split "`n" + $words = $Text -split '\s+' | Where-Object { $_ -ne '' } + $sentences = $Text -split '[.!?]+' | Where-Object { $_ -match '\S' } + + $result = @{ + character_count = $Text.Length + character_count_no_spaces = ($Text -replace '\s', '').Length + word_count = $words.Count + line_count = $lines.Count + sentence_count = $sentences.Count + average_word_length = if ($words.Count -gt 0) { + [math]::Round(($words | ForEach-Object { $_.Length } | Measure-Object -Average).Average, 2) + } else { 0 } + average_words_per_sentence = if ($sentences.Count -gt 0) { + [math]::Round($words.Count / $sentences.Count, 2) + } else { 0 } + } + + return ($result | ConvertTo-Json -Depth 2 -Compress) + } + catch { + return (@{ + error = $_.Exception.Message + } | ConvertTo-Json -Compress) + } +} + +<# +.SYNOPSIS +Converts text case. + +.DESCRIPTION +Converts text to various cases: upper, lower, title, or sentence case. + +.PARAMETER Text +The text to convert. + +.PARAMETER Case +The target case: Upper, Lower, Title, or Sentence. + +.EXAMPLE +Convert-TextCase -Text "hello world" -Case Title +Converts to "Hello World". +#> +function Convert-TextCase { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$Text, + + [Parameter(Mandatory)] + [ValidateSet('Upper', 'Lower', 'Title', 'Sentence')] + [string]$Case + ) + + try { + $converted = switch ($Case) { + 'Upper' { + $Text.ToUpper() + } + 'Lower' { + $Text.ToLower() + } + 'Title' { + (Get-Culture).TextInfo.ToTitleCase($Text.ToLower()) + } + 'Sentence' { + if ($Text.Length -gt 0) { + $Text.Substring(0, 1).ToUpper() + $Text.Substring(1).ToLower() + } else { + $Text + } + } + } + + $result = @{ + original = $Text + case_type = $Case + converted = $converted + } + + return ($result | ConvertTo-Json -Depth 2 -Compress) + } + catch { + return (@{ + error = $_.Exception.Message + text = $Text + } | ConvertTo-Json -Compress) + } +} + +<# +.SYNOPSIS +Searches for a pattern in text. + +.DESCRIPTION +Searches for a regular expression pattern in text and returns matches. + +.PARAMETER Text +The text to search. + +.PARAMETER Pattern +The regex pattern to search for. + +.PARAMETER CaseSensitive +If specified, search is case-sensitive. + +.EXAMPLE +Search-TextPattern -Text "Hello world 123" -Pattern "\d+" +Finds all numeric sequences in the text. +#> +function Search-TextPattern { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$Text, + + [Parameter(Mandatory)] + [string]$Pattern, + + [Parameter()] + [switch]$CaseSensitive + ) + + try { + $options = [System.Text.RegularExpressions.RegexOptions]::None + if (-not $CaseSensitive) { + $options = [System.Text.RegularExpressions.RegexOptions]::IgnoreCase + } + + $matches = [regex]::Matches($Text, $Pattern, $options) + + $matchResults = @() + foreach ($match in $matches) { + $matchResults += @{ + value = $match.Value + index = $match.Index + length = $match.Length + } + } + + $result = @{ + pattern = $Pattern + case_sensitive = $CaseSensitive.IsPresent + match_count = $matches.Count + matches = $matchResults + } + + return ($result | ConvertTo-Json -Depth 3 -Compress) + } + catch { + return (@{ + error = $_.Exception.Message + pattern = $Pattern + } | ConvertTo-Json -Compress) + } +} + +# Return the toolbox registration function +return TextTools From 90677225486ad939c455285dcbb1e6c66886e9d6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Oct 2025 21:59:58 +0000 Subject: [PATCH 3/5] Fix toolbox tool registration to use global scope and remove duplicate registrations Co-authored-by: dfinke <67258+dfinke@users.noreply.github.com> --- .../tmp/tools/datetime/DateTimeTools.ps1 | 52 +++++++++--------- .../tmp/tools/filesystem/FileSystemTools.ps1 | 54 +++++++++---------- .../PSToolboxAI/tmp/tools/text/TextTools.ps1 | 52 +++++++++--------- 3 files changed, 76 insertions(+), 82 deletions(-) diff --git a/spikes/PSToolboxAI/tmp/tools/datetime/DateTimeTools.ps1 b/spikes/PSToolboxAI/tmp/tools/datetime/DateTimeTools.ps1 index 0827b2c..71326a7 100644 --- a/spikes/PSToolboxAI/tmp/tools/datetime/DateTimeTools.ps1 +++ b/spikes/PSToolboxAI/tmp/tools/datetime/DateTimeTools.ps1 @@ -11,28 +11,6 @@ This toolbox is automatically discovered when $env:PSAI_TOOLBOX_PATH is set or when using the default toolbox path. #> -function DateTimeTools { - [CmdletBinding()] - param() - - # Check if we should register tools based on environment variable - if ($env:PSAI_ENABLE_TOOLBOX -eq $false) { - Write-Verbose "Toolbox registration skipped (PSAI_ENABLE_TOOLBOX is false)" - return @() - } - - Write-Verbose "Registering DateTime toolbox tools" - - # Register the tools using PSAI's Register-Tool function - $tools = @( - Register-Tool -FunctionName Get-CurrentDateTime - Register-Tool -FunctionName Format-DateTime - Register-Tool -FunctionName Get-DateDifference - ) - - return $tools -} - <# .SYNOPSIS Gets the current date and time. @@ -50,7 +28,7 @@ Optional timezone name (e.g., "UTC", "Eastern Standard Time"). Get-CurrentDateTime -Format ISO8601 Returns current time in ISO8601 format. #> -function Get-CurrentDateTime { +function Global:Get-CurrentDateTime { [CmdletBinding()] param( [Parameter()] @@ -123,7 +101,7 @@ The desired output format. Format-DateTime -DateTime "2025-01-01" -OutputFormat "yyyy-MM-dd HH:mm:ss" Formats the date in the specified format. #> -function Format-DateTime { +function Global:Format-DateTime { [CmdletBinding()] param( [Parameter(Mandatory)] @@ -173,7 +151,7 @@ The unit to return: Days, Hours, Minutes, Seconds, or All (default). Get-DateDifference -StartDate "2025-01-01" -Unit Days Returns the number of days between the start date and now. #> -function Get-DateDifference { +function Global:Get-DateDifference { [CmdletBinding()] param( [Parameter(Mandatory)] @@ -224,5 +202,25 @@ function Get-DateDifference { } } -# Return the toolbox registration function -return DateTimeTools +# Registration function - called by Get-PSAIToolbox +function DateTimeTools { + [CmdletBinding()] + param() + + # Check if we should register tools based on environment variable + if ($env:PSAI_ENABLE_TOOLBOX -eq $false) { + Write-Verbose "Toolbox registration skipped (PSAI_ENABLE_TOOLBOX is false)" + return @() + } + + Write-Verbose "Registering DateTime toolbox tools" + + # Register the tools using PSAI's Register-Tool function + $tools = @( + Register-Tool -FunctionName Get-CurrentDateTime + Register-Tool -FunctionName Format-DateTime + Register-Tool -FunctionName Get-DateDifference + ) + + return $tools +} diff --git a/spikes/PSToolboxAI/tmp/tools/filesystem/FileSystemTools.ps1 b/spikes/PSToolboxAI/tmp/tools/filesystem/FileSystemTools.ps1 index 1efae50..6861914 100644 --- a/spikes/PSToolboxAI/tmp/tools/filesystem/FileSystemTools.ps1 +++ b/spikes/PSToolboxAI/tmp/tools/filesystem/FileSystemTools.ps1 @@ -11,29 +11,6 @@ This toolbox is automatically discovered when $env:PSAI_TOOLBOX_PATH is set or when using the default toolbox path. #> -function FileSystemTools { - [CmdletBinding()] - param() - - # Check if we should register tools based on environment variable - # This follows the Sourcegraph Toolbox pattern - if ($env:PSAI_ENABLE_TOOLBOX -eq $false) { - Write-Verbose "Toolbox registration skipped (PSAI_ENABLE_TOOLBOX is false)" - return @() - } - - Write-Verbose "Registering FileSystem toolbox tools" - - # Register the tools using PSAI's Register-Tool function - $tools = @( - Register-Tool -FunctionName Get-DirectoryListing - Register-Tool -FunctionName Read-FileContent - Register-Tool -FunctionName Test-FileExists - ) - - return $tools -} - <# .SYNOPSIS Gets a listing of files and directories. @@ -55,7 +32,7 @@ If specified, recursively lists subdirectories. Get-DirectoryListing -Path "C:\Scripts" -Filter "*.ps1" Lists all PowerShell scripts in C:\Scripts. #> -function Get-DirectoryListing { +function Global:Get-DirectoryListing { [CmdletBinding()] param( [Parameter()] @@ -117,7 +94,7 @@ The encoding to use (default: UTF8). Read-FileContent -Path "C:\Scripts\script.ps1" Reads the content of script.ps1. #> -function Read-FileContent { +function Global:Read-FileContent { [CmdletBinding()] param( [Parameter(Mandatory)] @@ -170,7 +147,7 @@ The path to test. Test-FileExists -Path "C:\Scripts\script.ps1" Checks if script.ps1 exists. #> -function Test-FileExists { +function Global:Test-FileExists { [CmdletBinding()] param( [Parameter(Mandatory)] @@ -206,5 +183,26 @@ function Test-FileExists { } } -# Return the toolbox registration function -return FileSystemTools +# Registration function - called by Get-PSAIToolbox +function FileSystemTools { + [CmdletBinding()] + param() + + # Check if we should register tools based on environment variable + # This follows the Sourcegraph Toolbox pattern + if ($env:PSAI_ENABLE_TOOLBOX -eq $false) { + Write-Verbose "Toolbox registration skipped (PSAI_ENABLE_TOOLBOX is false)" + return @() + } + + Write-Verbose "Registering FileSystem toolbox tools" + + # Register the tools using PSAI's Register-Tool function + $tools = @( + Register-Tool -FunctionName Get-DirectoryListing + Register-Tool -FunctionName Read-FileContent + Register-Tool -FunctionName Test-FileExists + ) + + return $tools +} diff --git a/spikes/PSToolboxAI/tmp/tools/text/TextTools.ps1 b/spikes/PSToolboxAI/tmp/tools/text/TextTools.ps1 index da73b63..5a89ea2 100644 --- a/spikes/PSToolboxAI/tmp/tools/text/TextTools.ps1 +++ b/spikes/PSToolboxAI/tmp/tools/text/TextTools.ps1 @@ -11,28 +11,6 @@ This toolbox is automatically discovered when $env:PSAI_TOOLBOX_PATH is set or when using the default toolbox path. #> -function TextTools { - [CmdletBinding()] - param() - - # Check if we should register tools based on environment variable - if ($env:PSAI_ENABLE_TOOLBOX -eq $false) { - Write-Verbose "Toolbox registration skipped (PSAI_ENABLE_TOOLBOX is false)" - return @() - } - - Write-Verbose "Registering Text toolbox tools" - - # Register the tools using PSAI's Register-Tool function - $tools = @( - Register-Tool -FunctionName Get-TextStatistics - Register-Tool -FunctionName Convert-TextCase - Register-Tool -FunctionName Search-TextPattern - ) - - return $tools -} - <# .SYNOPSIS Gets statistics about text content. @@ -47,7 +25,7 @@ The text to analyze. Get-TextStatistics -Text "Hello world! This is a test." Returns statistics about the text. #> -function Get-TextStatistics { +function Global:Get-TextStatistics { [CmdletBinding()] param( [Parameter(Mandatory)] @@ -99,7 +77,7 @@ The target case: Upper, Lower, Title, or Sentence. Convert-TextCase -Text "hello world" -Case Title Converts to "Hello World". #> -function Convert-TextCase { +function Global:Convert-TextCase { [CmdletBinding()] param( [Parameter(Mandatory)] @@ -166,7 +144,7 @@ If specified, search is case-sensitive. Search-TextPattern -Text "Hello world 123" -Pattern "\d+" Finds all numeric sequences in the text. #> -function Search-TextPattern { +function Global:Search-TextPattern { [CmdletBinding()] param( [Parameter(Mandatory)] @@ -213,5 +191,25 @@ function Search-TextPattern { } } -# Return the toolbox registration function -return TextTools +# Registration function - called by Get-PSAIToolbox +function TextTools { + [CmdletBinding()] + param() + + # Check if we should register tools based on environment variable + if ($env:PSAI_ENABLE_TOOLBOX -eq $false) { + Write-Verbose "Toolbox registration skipped (PSAI_ENABLE_TOOLBOX is false)" + return @() + } + + Write-Verbose "Registering Text toolbox tools" + + # Register the tools using PSAI's Register-Tool function + $tools = @( + Register-Tool -FunctionName Get-TextStatistics + Register-Tool -FunctionName Convert-TextCase + Register-Tool -FunctionName Search-TextPattern + ) + + return $tools +} From f6a9a7dfe58ad0cf6125d246240736527cccf49f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Oct 2025 22:01:08 +0000 Subject: [PATCH 4/5] Add comprehensive test script for PSToolboxAI implementation Co-authored-by: dfinke <67258+dfinke@users.noreply.github.com> --- spikes/PSToolboxAI/test-toolbox.ps1 | 196 ++++++++++++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 spikes/PSToolboxAI/test-toolbox.ps1 diff --git a/spikes/PSToolboxAI/test-toolbox.ps1 b/spikes/PSToolboxAI/test-toolbox.ps1 new file mode 100644 index 0000000..97c4305 --- /dev/null +++ b/spikes/PSToolboxAI/test-toolbox.ps1 @@ -0,0 +1,196 @@ +<# +.SYNOPSIS +Test script for PSToolboxAI module. + +.DESCRIPTION +Tests the PSToolboxAI module's functionality including: +- Module loading +- Toolbox discovery +- Tool registration +- Function availability + +.NOTES +This is a basic test script. For actual agent testing, you need OpenAI API key. +#> + +param( + [Switch]$Verbose +) + +Write-Host "=== PSToolboxAI Test Script ===" -ForegroundColor Cyan +Write-Host "" + +# Test 1: Module Loading +Write-Host "[Test 1] Loading PSAI module..." -ForegroundColor Yellow +try { + Import-Module ./PSAI.psd1 -Force -ErrorAction Stop + Write-Host " ✓ PSAI module loaded successfully" -ForegroundColor Green +} +catch { + Write-Host " ✗ Failed to load PSAI module: $_" -ForegroundColor Red + exit 1 +} + +Write-Host "[Test 1] Loading PSToolboxAI module..." -ForegroundColor Yellow +try { + Import-Module ./spikes/PSToolboxAI/PSToolboxAI.psd1 -Force -ErrorAction Stop + Write-Host " ✓ PSToolboxAI module loaded successfully" -ForegroundColor Green +} +catch { + Write-Host " ✗ Failed to load PSToolboxAI module: $_" -ForegroundColor Red + exit 1 +} +Write-Host "" + +# Test 2: Toolbox Path Discovery +Write-Host "[Test 2] Testing Get-PSAIToolboxPath..." -ForegroundColor Yellow +try { + $paths = Get-PSAIToolboxPath + if ($paths) { + Write-Host " ✓ Toolbox paths discovered: $($paths.Count) path(s)" -ForegroundColor Green + $paths | ForEach-Object { Write-Host " - $_" -ForegroundColor Gray } + } + else { + Write-Host " ✗ No toolbox paths found" -ForegroundColor Red + exit 1 + } +} +catch { + Write-Host " ✗ Error getting toolbox paths: $_" -ForegroundColor Red + exit 1 +} +Write-Host "" + +# Test 3: Tool Discovery +Write-Host "[Test 3] Testing Get-PSAIToolbox..." -ForegroundColor Yellow +try { + $tools = Get-PSAIToolbox + if ($tools -and $tools.Count -gt 0) { + Write-Host " ✓ Tools discovered: $($tools.Count) tool(s)" -ForegroundColor Green + $tools | ForEach-Object { + if ($_.function -and $_.function.name) { + Write-Host " - $($_.function.name)" -ForegroundColor Gray + } + } + } + else { + Write-Host " ✗ No tools discovered" -ForegroundColor Red + exit 1 + } +} +catch { + Write-Host " ✗ Error discovering tools: $_" -ForegroundColor Red + exit 1 +} +Write-Host "" + +# Test 4: Function Availability +Write-Host "[Test 4] Verifying tool functions are available..." -ForegroundColor Yellow +$expectedFunctions = @( + 'Get-DirectoryListing' + 'Read-FileContent' + 'Test-FileExists' + 'Get-CurrentDateTime' + 'Format-DateTime' + 'Get-DateDifference' + 'Get-TextStatistics' + 'Convert-TextCase' + 'Search-TextPattern' +) + +$allFound = $true +foreach ($funcName in $expectedFunctions) { + $cmd = Get-Command $funcName -ErrorAction SilentlyContinue + if ($cmd) { + Write-Host " ✓ $funcName" -ForegroundColor Green + } + else { + Write-Host " ✗ $funcName not found" -ForegroundColor Red + $allFound = $false + } +} + +if (-not $allFound) { + Write-Host " Some functions are missing!" -ForegroundColor Red + exit 1 +} +Write-Host "" + +# Test 5: Tool Function Execution (basic smoke test) +Write-Host "[Test 5] Testing tool function execution..." -ForegroundColor Yellow + +# Test Get-CurrentDateTime +try { + $result = Get-CurrentDateTime -Format All + $parsed = $result | ConvertFrom-Json + if ($parsed.iso8601) { + Write-Host " ✓ Get-CurrentDateTime works" -ForegroundColor Green + } + else { + Write-Host " ✗ Get-CurrentDateTime output invalid" -ForegroundColor Red + } +} +catch { + Write-Host " ✗ Get-CurrentDateTime failed: $_" -ForegroundColor Red +} + +# Test Get-TextStatistics +try { + $result = Get-TextStatistics -Text "Hello world" + $parsed = $result | ConvertFrom-Json + if ($parsed.word_count -eq 2) { + Write-Host " ✓ Get-TextStatistics works" -ForegroundColor Green + } + else { + Write-Host " ✗ Get-TextStatistics output invalid" -ForegroundColor Red + } +} +catch { + Write-Host " ✗ Get-TextStatistics failed: $_" -ForegroundColor Red +} + +# Test Test-FileExists +try { + $result = Test-FileExists -Path "." + $parsed = $result | ConvertFrom-Json + if ($parsed.exists -eq $true) { + Write-Host " ✓ Test-FileExists works" -ForegroundColor Green + } + else { + Write-Host " ✗ Test-FileExists output invalid" -ForegroundColor Red + } +} +catch { + Write-Host " ✗ Test-FileExists failed: $_" -ForegroundColor Red +} + +Write-Host "" + +# Test 6: Agent Creation (without API call) +Write-Host "[Test 6] Testing agent creation with tools..." -ForegroundColor Yellow +try { + $agent = New-Agent -Tools $tools -Instructions "Test agent" + if ($agent) { + Write-Host " ✓ Agent created successfully" -ForegroundColor Green + Write-Host " Type: $($agent.psobject.TypeNames[0])" -ForegroundColor Gray + Write-Host " Tools: $($agent.Tools.Count)" -ForegroundColor Gray + } + else { + Write-Host " ✗ Agent creation returned null" -ForegroundColor Red + } +} +catch { + Write-Host " ✗ Agent creation failed: $_" -ForegroundColor Red +} +Write-Host "" + +# Summary +Write-Host "=== Test Summary ===" -ForegroundColor Cyan +Write-Host "All basic tests passed! ✓" -ForegroundColor Green +Write-Host "" +Write-Host "Note: These tests verify module loading, tool discovery, and basic function execution." -ForegroundColor Gray +Write-Host "To test actual agent interactions, you'll need an OpenAI API key set in `$env:OpenAIKey" -ForegroundColor Gray +Write-Host "" +Write-Host "Example usage with API key:" -ForegroundColor Yellow +Write-Host ' $agent = New-PSAIToolboxAgent -ShowToolCalls' -ForegroundColor Gray +Write-Host ' $agent | Get-AgentResponse "What is the current date?"' -ForegroundColor Gray From 570d179105d5a21cf9e87e17e46e7032ca0d2154 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Oct 2025 22:02:47 +0000 Subject: [PATCH 5/5] Add comprehensive implementation notes documenting the PSToolboxAI spike Co-authored-by: dfinke <67258+dfinke@users.noreply.github.com> --- spikes/PSToolboxAI/IMPLEMENTATION_NOTES.md | 260 +++++++++++++++++++++ 1 file changed, 260 insertions(+) create mode 100644 spikes/PSToolboxAI/IMPLEMENTATION_NOTES.md diff --git a/spikes/PSToolboxAI/IMPLEMENTATION_NOTES.md b/spikes/PSToolboxAI/IMPLEMENTATION_NOTES.md new file mode 100644 index 0000000..575fc5e --- /dev/null +++ b/spikes/PSToolboxAI/IMPLEMENTATION_NOTES.md @@ -0,0 +1,260 @@ +# PSToolboxAI Implementation Notes + +## Overview + +This document describes the implementation of the Sourcegraph Toolbox pattern as a PowerShell module integrated with PSAI. + +## Issue Reference + +- **Issue**: #108 - Implement Sourcegraph Toolbox PowerShell port into PSAI +- **PR**: #111 +- **Branch**: `copilot/implement-sourcegraph-toolbox-port` + +## Implementation Summary + +Successfully implemented a Sourcegraph Toolbox-style toolbox system for PSAI that: +1. Discovers tools from configurable directories +2. Dynamically loads and registers tools +3. Integrates seamlessly with PSAI's agent system +4. Follows the established patterns from existing PSAI tools + +## Key Design Decisions + +### 1. Module Structure + +``` +PSToolboxAI/ +├── PSToolboxAI.psd1 # Module manifest +├── PSToolboxAI.psm1 # Module loader +├── Public/ +│ └── PSToolboxAI.ps1 # Main functions +├── Private/ # (Reserved for future use) +├── Examples/ # Usage examples +│ ├── 01-BasicToolbox.ps1 +│ └── 02-ConvenienceFunction.ps1 +├── tmp/tools/ # Default toolbox location +│ ├── filesystem/ +│ ├── datetime/ +│ └── text/ +├── README.md # Documentation +└── test-toolbox.ps1 # Test script +``` + +### 2. Tool Discovery Pattern + +**Environment Variable**: `$env:PSAI_TOOLBOX_PATH` +- Can specify multiple paths (`;` on Windows, `:` on Unix) +- If not set, uses default path: `spikes/PSToolboxAI/tmp/tools` + +**Discovery Process**: +1. Scan toolbox directories for `.ps1` files +2. Dot-source each script to load functions +3. Call registration function (matches script basename) +4. Collect tool specifications from `Register-Tool` + +### 3. Tool Definition Pattern + +Each toolbox script follows this pattern: + +```powershell +# Tool functions defined with Global scope +function Global:My-ToolFunction { + # ... implementation ... + return (ConvertTo-Json -Compress -InputObject @{ result = $result }) +} + +# Registration function (matches script filename) +function MyToolbox { + $tools = @( + Register-Tool -FunctionName My-ToolFunction + ) + return $tools +} +``` + +**Key Points**: +- Tool functions use `Global:` scope to be accessible to `Register-Tool` +- Functions return JSON strings (required by OpenAI function calling) +- Registration function matches script basename +- Uses PSAI's `Register-Tool` for consistency + +### 4. Integration with PSAI + +The implementation integrates with PSAI's existing infrastructure: + +- **Register-Tool**: Uses PSAI's tool registration function +- **New-Agent**: Tools work with standard `New-Agent -Tools` parameter +- **Get-AgentResponse**: Compatible with standard agent interactions +- **Invoke-InteractiveCLI**: Works with interactive agent sessions + +## Implemented Toolboxes + +### FileSystem Toolbox +Located: `tmp/tools/filesystem/FileSystemTools.ps1` + +**Tools**: +1. `Get-DirectoryListing` - List files and directories +2. `Read-FileContent` - Read file contents +3. `Test-FileExists` - Check file/directory existence + +### DateTime Toolbox +Located: `tmp/tools/datetime/DateTimeTools.ps1` + +**Tools**: +1. `Get-CurrentDateTime` - Get current time in various formats +2. `Format-DateTime` - Format date/time strings +3. `Get-DateDifference` - Calculate time differences + +### Text Toolbox +Located: `tmp/tools/text/TextTools.ps1` + +**Tools**: +1. `Get-TextStatistics` - Analyze text (word count, etc.) +2. `Convert-TextCase` - Convert case (upper, lower, title, sentence) +3. `Search-TextPattern` - Search text using regex + +## Public Functions + +### Get-PSAIToolboxPath +Retrieves toolbox directories from environment variable or returns default path. + +```powershell +$paths = Get-PSAIToolboxPath +``` + +### Get-PSAIToolbox +Discovers and loads toolbox tools from specified paths. + +```powershell +# Use default paths +$tools = Get-PSAIToolbox + +# Use specific path +$tools = Get-PSAIToolbox -Path "./my-tools" +``` + +### New-PSAIToolboxAgent +Convenience function that combines tool discovery and agent creation. + +```powershell +$agent = New-PSAIToolboxAgent ` + -Instructions "You are a helpful assistant" ` + -ShowToolCalls +``` + +## Usage Examples + +### Basic Usage +```powershell +Import-Module PSAI +Import-Module ./spikes/PSToolboxAI/PSToolboxAI.psd1 + +# Discover tools +$tools = Get-PSAIToolbox + +# Create agent +$agent = New-Agent -Tools $tools -ShowToolCalls + +# Use agent +$agent | Get-AgentResponse "What is the current date?" +``` + +### With Environment Variable +```powershell +# Set custom toolbox paths +$env:PSAI_TOOLBOX_PATH = "C:\MyTools;C:\SharedTools" + +# Tools will be discovered from these paths +$agent = New-PSAIToolboxAgent -ShowToolCalls +``` + +### Convenience Function +```powershell +# One-step agent creation with auto-discovery +$agent = New-PSAIToolboxAgent ` + -Instructions "You are a file management assistant" ` + -Name "FileBot" ` + -ShowToolCalls + +$agent | Invoke-InteractiveCLI +``` + +## Testing + +A comprehensive test script (`test-toolbox.ps1`) validates: + +1. ✓ Module loading (PSAI and PSToolboxAI) +2. ✓ Toolbox path discovery +3. ✓ Tool discovery (9 tools) +4. ✓ Function availability (all 9 functions) +5. ✓ Function execution (DateTime, Text, FileSystem) +6. ✓ Agent creation with tools + +All tests pass successfully. + +## Technical Challenges & Solutions + +### Challenge 1: Function Scope +**Problem**: Tool functions weren't visible to `Register-Tool` when called. + +**Solution**: Defined tool functions with `Global:` scope, following the pattern used in `TavilyTool.psm1`. + +### Challenge 2: Duplicate Registration +**Problem**: Tools were being registered twice (18 instead of 9). + +**Solution**: Removed `return FunctionName` at end of scripts, which was outputting the function name as a string when dot-sourcing. + +### Challenge 3: Submodule Issue +**Problem**: `spikes/PSToolboxAI` was initially a gitlink (submodule reference). + +**Solution**: Removed the gitlink with `git rm --cached` and added files as regular repository content. + +## Comparison with Sourcegraph Toolbox + +### Similarities +- Environment-based discovery +- Dynamic tool loading +- JSON-based tool specifications +- Integration with agent frameworks + +### PowerShell-Specific Adaptations +- Uses PowerShell module system +- Leverages PSAI's existing `Register-Tool` infrastructure +- Follows PowerShell naming conventions (Verb-Noun) +- Uses PowerShell's comment-based help +- Returns JSON strings for LLM compatibility + +## Future Enhancements + +Potential improvements identified: + +1. **Tool Caching**: Cache discovered tools for performance +2. **Tool Versioning**: Support versioned toolboxes +3. **Remote Toolboxes**: Load toolboxes from URLs +4. **Tool Validation**: Validate tool specifications +5. **Tool Dependencies**: Handle inter-tool dependencies +6. **Auto-documentation**: Generate docs from tool metadata +7. **Tool Marketplace**: Community toolbox sharing + +## References + +- [Sourcegraph Toolboxes Manual](https://ampcode.com/manual#toolboxes) +- [Sourcegraph Toolboxes Reference](https://ampcode.com/manual/appendix#toolboxes-reference) +- [PSAI Repository](https://github.com/dfinke/PSAI) +- Issue #108: Implement Sourcegraph Toolbox PowerShell port into PSAI +- Issue #106: Analysis of Anthropic Claude Skills and Sourcegraph Toolbox approach + +## Conclusion + +The implementation successfully demonstrates the Sourcegraph Toolbox pattern in PowerShell, integrated with PSAI's agent system. The spike provides: + +- A working proof of concept +- Clear patterns for creating new toolboxes +- Comprehensive documentation +- Example toolboxes with 9 useful tools +- A solid foundation for future enhancements + +The implementation is ready for review and can serve as the basis for either: +1. Integration into the main PSAI module (recommended) +2. Publication as a separate companion module +3. Further iteration based on user feedback