Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,4 @@ jobs:
# Install-Module -Name PwshSpectreConsole -Force

cd ./__tests__
Invoke-Pester -Output Detailed
Invoke-Pester -Path ., ../Tests -Output Detailed -CI
2 changes: 2 additions & 0 deletions PSAI.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ PSAI brings OpenAI ChatGPT to PowerShell, leveraging advanced AI capabilities in
# 'Get-YouTubeTranscript'

# Public
'Search-ParallelWeb'
'Get-ParallelWebContent'
'Out-BoxedText'
'Invoke-QuickPrompt'
'New-Agent'
Expand Down
3 changes: 3 additions & 0 deletions PSAI.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
. $PSScriptRoot/Private/Invoke-OAIBeta.ps1
. $PSScriptRoot/Private/Get-MultipartFormData.ps1
. $PSScriptRoot/Private/Get-ToolProperty.ps1
. $PSScriptRoot/Private/Invoke-ParallelMcp.ps1

# # Agent Tools
# Import-Module $PSScriptRoot/Public/Tools/CalculatorTool.psm1
Expand All @@ -14,6 +15,8 @@
# Import-Module $PSScriptRoot/Public/Tools/YouTubeAssistant.psm1

# Public Functions
. $PSScriptRoot/Public/Search-ParallelWeb.ps1
. $PSScriptRoot/Public/Get-ParallelWebContent.ps1
. $PSScriptRoot/Public/Invoke-QuickPrompt.ps1
. $PSScriptRoot/Public/New-Agent.ps1
. $PSScriptRoot/Public/Get-AgentResponse.ps1
Expand Down
95 changes: 95 additions & 0 deletions Private/Invoke-ParallelMcp.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Reuse one anonymous conversation identifier for this module instance.
$script:ParallelSessionId = [guid]::NewGuid().ToString()

function Invoke-ParallelMcpRequest {
[CmdletBinding()]
param(
[hashtable]$Message,
[hashtable]$Headers,
[int]$TimeoutSec
)

try {
$timeoutOptions = @{ TimeoutSec = $TimeoutSec }
# Newer PowerShell versions split connection and operation timeouts.
if ((Get-Command Invoke-WebRequest).Parameters.ContainsKey('OperationTimeoutSeconds')) {
$timeoutOptions['OperationTimeoutSeconds'] = $TimeoutSec
}
$response = Invoke-WebRequest -Uri 'https://search.parallel.ai/mcp' -Method Post `
-ContentType 'application/json; charset=utf-8' -Headers $Headers `
-UserAgent "PSAI/$($ExecutionContext.SessionState.Module.Version) (https://github.com/dfinke/PSAI)" `
-Body ($Message | ConvertTo-Json -Depth 20 -Compress) @timeoutOptions -ErrorAction Stop

# Notifications have no JSON-RPC response body.
if (-not $Message.ContainsKey('id')) {
return
}

$contentType = $response.Headers['Content-Type'] -join ','
if ($contentType -like 'text/event-stream*') {
$replies = @(
foreach ($sseEvent in ($response.Content -split '\r?\n\r?\n')) {
$data = @($sseEvent -split '\r?\n' | Where-Object { $_ -like 'data:*' } |
ForEach-Object { $_.Substring(5).TrimStart(' ') }) -join "`n"
if ($data) { $data | ConvertFrom-Json -Depth 30 }
}
)
}
else {
$replies = @($response.Content | ConvertFrom-Json -Depth 30)
}

$reply = $replies | Where-Object { $_.id -eq $Message.id } | Select-Object -First 1
if (-not $reply -or $reply.jsonrpc -ne '2.0') {
throw 'Missing matching MCP response.'
}
if ($reply.error) {
throw "MCP error $($reply.error.code): $($reply.error.message)"
}
if ($null -eq $reply.result) {
throw 'Missing MCP result.'
}

[pscustomobject]@{ Result = $reply.result; Headers = $response.Headers }
}
catch {
throw "Parallel MCP $($Message.method) failed: $_"
}
}

function Invoke-ParallelMcp {
[CmdletBinding()]
param(
[string]$ToolName,
[hashtable]$Arguments,
[int]$TimeoutSec
)

$headers = @{ Accept = 'application/json, text/event-stream' }
$initialized = Invoke-ParallelMcpRequest -Headers $headers -TimeoutSec $TimeoutSec -Message @{
jsonrpc = '2.0'; id = 1; method = 'initialize'
params = @{
protocolVersion = '2025-03-26'
capabilities = @{}
clientInfo = @{ name = 'PSAI'; version = "$($ExecutionContext.SessionState.Module.Version)" }
}
}
$headers['MCP-Protocol-Version'] = $initialized.Result.protocolVersion
$sessionId = $initialized.Headers['Mcp-Session-Id'] -join ''
if ($sessionId) { $headers['Mcp-Session-Id'] = $sessionId }

Invoke-ParallelMcpRequest -Headers $headers -TimeoutSec $TimeoutSec -Message @{
jsonrpc = '2.0'; method = 'notifications/initialized'
}
$Arguments['session_id'] = $script:ParallelSessionId
$called = Invoke-ParallelMcpRequest -Headers $headers -TimeoutSec $TimeoutSec -Message @{
jsonrpc = '2.0'; id = 2; method = 'tools/call'
params = @{ name = $ToolName; arguments = $Arguments }
}

$text = @($called.Result.content | Where-Object { $_.type -eq 'text' } |
ForEach-Object { $_.text }) -join "`n"
if ($called.Result.isError) { throw "Parallel MCP $ToolName failed: $text" }
if (-not $text) { throw "Parallel MCP $ToolName returned no text content." }
$text
}
38 changes: 38 additions & 0 deletions Public/Get-ParallelWebContent.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
function Get-ParallelWebContent {
<#
.SYNOPSIS
Fetches relevant excerpts from web pages through the anonymous Parallel Search MCP.

.DESCRIPTION
Returns excerpts as text. Use when a specific page is requested or search
excerpts are insufficient. No Parallel API key is required; free-tier rate
limits apply. Partial fetch failures remain in the returned tool text.

.PARAMETER Urls
One to twenty HTTP or HTTPS URLs sharing the same objective.

.PARAMETER Objective
Information to extract, up to 200 characters. Omit to use the server default.

.PARAMETER TimeoutSec
Timeout in seconds for each MCP HTTP request. Defaults to 60.

.EXAMPLE
Get-ParallelWebContent -Urls 'https://learn.microsoft.com/powershell/' -Objective 'What is PowerShell?'
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[ValidateCount(1, 20)]
[ValidateScript({ [uri]::IsWellFormedUriString($_, [UriKind]::Absolute) -and ([uri]$_).Scheme -in 'http', 'https' })]
[string[]]$Urls,
[ValidateLength(0, 200)]
[string]$Objective,
[ValidateRange(1, 300)]
[int]$TimeoutSec = 60
)

$arguments = @{ urls = @($Urls) }
if ($Objective) { $arguments['objective'] = $Objective }
Invoke-ParallelMcp -ToolName 'web_fetch' -Arguments $arguments -TimeoutSec $TimeoutSec
}
39 changes: 39 additions & 0 deletions Public/Search-ParallelWeb.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
function Search-ParallelWeb {
<#
.SYNOPSIS
Searches the web through the anonymous Parallel Search MCP.

.DESCRIPTION
Returns source URLs and relevant excerpts as text for an agent to use directly.
No Parallel API key is required. The free endpoint is intended for light use
and has rate limits. Model inference is separate.

.PARAMETER Objective
A specific description of the information to find, including freshness or source preferences.

.PARAMETER SearchQueries
One or more related keyword queries for the objective, usually three to six words each.

.PARAMETER TimeoutSec
Timeout in seconds for each MCP HTTP request. Defaults to 60.

.EXAMPLE
Search-ParallelWeb -Objective 'Find the current PowerShell release' -SearchQueries 'PowerShell latest stable release'
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$Objective,
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string[]]$SearchQueries,
[ValidateRange(1, 300)]
[int]$TimeoutSec = 60
)

Invoke-ParallelMcp -ToolName 'web_search' -TimeoutSec $TimeoutSec -Arguments @{
objective = $Objective
search_queries = @($SearchQueries)
}
}
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,40 @@ $agent = New-Agent -Tools $tools -ShowToolCalls
$agent | Get-AgentResponse 'What did Microsoft close at and the latest news for them?'
``` -->

## Parallel web search and fetch

Register the optional Parallel tools with an agent:

```powershell
$agent = New-Agent -Tools 'Search-ParallelWeb', 'Get-ParallelWebContent'
$agent | Get-AgentResponse 'Find the current stable PowerShell release and cite sources.'
```

These functions use [Parallel Search MCP](https://docs.parallel.ai/integrations/mcp/search-mcp)
over Streamable HTTP at `https://search.parallel.ai/mcp`. Search and fetch need
no Parallel API key and do not read saved credentials or environment keys.
The anonymous tier is intended for exploration and light use, with rate limits.
Your configured model still needs its own credentials and may incur inference costs.
Existing agents and provider defaults are unchanged.

You can also run the tools directly without a model:

```powershell
Search-ParallelWeb -Objective 'Find official PowerShell documentation' -SearchQueries 'Microsoft PowerShell documentation'
Get-ParallelWebContent -Urls 'https://learn.microsoft.com/powershell/' -Objective 'What is PowerShell?'
```

Both return source excerpts as text, including any server warnings or partial fetch
errors. HTTP and MCP failures raise errors that PSAI's agent loop adds to the tool
response. Each HTTP request times out after 60 seconds by default; use `-TimeoutSec`
to change it. Search and fetch share an anonymous conversation identifier until
the module is reimported. Reimport with `-Force` to start a separate conversation.

For a checkout, run `Import-Module ./PSAI.psd1 -Force` in PowerShell 7.1 or newer;
no extra modules are needed. The [Parallel quickstart](examples/agent-quickstarts/06-ParallelWebSearch.ps1)
uses the same agent path. Run its tests with Pester 5:
`Invoke-Pester ./Tests/ParallelWeb.tests.ps1`.

## Slash Commands

PSAI Agents now support slash commands in interactive sessions, allowing you to perform quick actions without leaving the conversation flow.
Expand Down
Loading