@oh-my-pi/pi-coding-agent 17.1.0 → 17.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +55 -0
- package/dist/cli.js +6325 -4420
- package/dist/types/cli/bench-cli.d.ts +1 -0
- package/dist/types/config/model-resolver.d.ts +6 -3
- package/dist/types/config/settings-schema.d.ts +62 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +3 -0
- package/dist/types/launch/broker-list-order.test.d.ts +1 -0
- package/dist/types/live/attestation.d.ts +2 -0
- package/dist/types/live/controller.d.ts +10 -2
- package/dist/types/live/protocol.d.ts +1 -1
- package/dist/types/live/transport.d.ts +6 -19
- package/dist/types/live/visualizer.d.ts +8 -11
- package/dist/types/modes/components/assistant-message.d.ts +1 -0
- package/dist/types/modes/components/custom-message.d.ts +1 -1
- package/dist/types/modes/components/message-frame.d.ts +8 -4
- package/dist/types/modes/components/session-account-selector.d.ts +11 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +3 -6
- package/dist/types/modes/types.d.ts +9 -5
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -6
- package/dist/types/session/agent-session-types.d.ts +8 -1
- package/dist/types/session/agent-session.d.ts +20 -1
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/session/eval-runner.d.ts +2 -0
- package/dist/types/session/messages.d.ts +2 -0
- package/dist/types/session/session-tools.d.ts +15 -0
- package/dist/types/session/streaming-output.d.ts +8 -0
- package/dist/types/slash-commands/helpers/session-pin.d.ts +9 -0
- package/dist/types/stt/index.d.ts +0 -2
- package/dist/types/stt/stt-controller.d.ts +7 -0
- package/dist/types/task/executor.d.ts +3 -1
- package/dist/types/task/structured-subagent.d.ts +3 -0
- package/dist/types/task/types.d.ts +11 -11
- package/dist/types/thinking.d.ts +13 -0
- package/dist/types/tiny/title-client.d.ts +10 -0
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/computer/protocol.d.ts +43 -0
- package/dist/types/tools/computer/supervisor.d.ts +32 -0
- package/dist/types/tools/computer/worker-entry.d.ts +1 -0
- package/dist/types/tools/computer/worker.d.ts +15 -0
- package/dist/types/tools/computer-renderer.d.ts +22 -0
- package/dist/types/tools/computer.d.ts +71 -0
- package/dist/types/tools/context.d.ts +2 -0
- package/dist/types/tools/default-renderer.d.ts +21 -0
- package/dist/types/tools/eval-format/index.d.ts +7 -0
- package/dist/types/tools/eval-format/javascript.d.ts +2 -0
- package/dist/types/tools/eval-format/julia.d.ts +2 -0
- package/dist/types/tools/eval-format/python.d.ts +2 -0
- package/dist/types/tools/eval-format/ruby.d.ts +2 -0
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/resolve.d.ts +7 -0
- package/dist/types/tools/todo.d.ts +4 -1
- package/dist/types/tts/streaming-player.d.ts +10 -43
- package/dist/types/utils/tools-manager.d.ts +1 -2
- package/dist/types/web/search/providers/base.d.ts +16 -0
- package/dist/types/web/search/providers/brave.d.ts +2 -0
- package/dist/types/web/search/providers/firecrawl.d.ts +2 -0
- package/dist/types/web/search/providers/gemini.d.ts +3 -0
- package/dist/types/web/search/providers/jina.d.ts +2 -0
- package/dist/types/web/search/providers/kagi.d.ts +2 -0
- package/dist/types/web/search/providers/kimi.d.ts +2 -0
- package/dist/types/web/search/providers/parallel.d.ts +2 -0
- package/dist/types/web/search/providers/perplexity.d.ts +3 -0
- package/dist/types/web/search/providers/searxng.d.ts +10 -0
- package/dist/types/web/search/providers/tavily.d.ts +8 -0
- package/dist/types/web/search/query.d.ts +190 -0
- package/package.json +12 -12
- package/src/cli/args.ts +1 -0
- package/src/cli/bench-cli.ts +12 -1
- package/src/cli/setup-cli.ts +2 -14
- package/src/cli/web-search-cli.ts +7 -0
- package/src/cli.ts +8 -0
- package/src/config/model-registry.ts +6 -0
- package/src/config/model-resolver.ts +17 -6
- package/src/config/settings-schema.ts +66 -0
- package/src/debug/raw-sse-buffer.ts +157 -30
- package/src/eval/executor-base.ts +1 -0
- package/src/eval/js/executor.ts +2 -0
- package/src/exec/bash-executor.ts +1 -0
- package/src/export/share.ts +4 -3
- package/src/extensibility/extensions/wrapper.ts +68 -12
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +7 -1
- package/src/launch/broker-list-order.test.ts +89 -0
- package/src/launch/broker.ts +49 -8
- package/src/live/attestation.ts +91 -0
- package/src/live/controller.ts +76 -23
- package/src/live/protocol.test.ts +3 -3
- package/src/live/protocol.ts +1 -1
- package/src/live/transport.ts +72 -140
- package/src/live/visualizer.ts +114 -134
- package/src/modes/components/assistant-message.ts +7 -2
- package/src/modes/components/custom-message.ts +4 -1
- package/src/modes/components/message-frame.ts +14 -8
- package/src/modes/components/session-account-selector.ts +62 -0
- package/src/modes/components/tool-execution.ts +17 -110
- package/src/modes/controllers/input-controller.ts +55 -47
- package/src/modes/controllers/live-command-controller.ts +82 -5
- package/src/modes/controllers/selector-controller.ts +62 -0
- package/src/modes/interactive-mode.ts +79 -5
- package/src/modes/types.ts +10 -4
- package/src/modes/utils/ui-helpers.ts +7 -8
- package/src/prompts/system/computer-safety.md +14 -0
- package/src/prompts/system/resolve-device-reminder.md +1 -1
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/ast-edit.md +1 -0
- package/src/prompts/tools/bash.md +1 -0
- package/src/prompts/tools/computer.md +26 -0
- package/src/prompts/tools/eval.md +2 -2
- package/src/prompts/tools/task.md +5 -2
- package/src/prompts/tools/web-search.md +2 -0
- package/src/sdk.ts +13 -4
- package/src/session/agent-session-types.ts +9 -0
- package/src/session/agent-session.ts +70 -0
- package/src/session/auth-storage.ts +1 -0
- package/src/session/eval-runner.ts +5 -0
- package/src/session/messages.ts +3 -0
- package/src/session/queued-messages.ts +7 -1
- package/src/session/session-tools.ts +37 -0
- package/src/session/streaming-output.ts +52 -5
- package/src/slash-commands/builtin-registry.ts +165 -9
- package/src/slash-commands/helpers/session-pin.ts +44 -0
- package/src/stt/downloader.ts +0 -2
- package/src/stt/index.ts +0 -2
- package/src/stt/stt-controller.ts +57 -146
- package/src/system-prompt.ts +4 -0
- package/src/task/executor.ts +17 -9
- package/src/task/index.ts +14 -34
- package/src/task/structured-subagent.ts +4 -0
- package/src/task/types.ts +15 -13
- package/src/thinking.ts +27 -0
- package/src/tiny/title-client.ts +22 -0
- package/src/tools/ast-edit.ts +4 -1
- package/src/tools/bash-interactive.ts +90 -86
- package/src/tools/bash.ts +13 -0
- package/src/tools/browser/render.ts +2 -1
- package/src/tools/builtin-names.ts +1 -0
- package/src/tools/computer/protocol.ts +28 -0
- package/src/tools/computer/supervisor.ts +258 -0
- package/src/tools/computer/worker-entry.ts +25 -0
- package/src/tools/computer/worker.ts +135 -0
- package/src/tools/computer-renderer.ts +108 -0
- package/src/tools/computer.ts +433 -0
- package/src/tools/context.ts +2 -0
- package/src/tools/default-renderer.ts +139 -0
- package/src/tools/essential-tools.ts +1 -0
- package/src/tools/eval-format/index.ts +24 -0
- package/src/tools/eval-format/javascript.ts +952 -0
- package/src/tools/eval-format/julia.ts +446 -0
- package/src/tools/eval-format/python.ts +544 -0
- package/src/tools/eval-format/ruby.ts +380 -0
- package/src/tools/eval-render.ts +12 -6
- package/src/tools/index.ts +5 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/resolve.ts +10 -1
- package/src/tools/todo.ts +58 -6
- package/src/tools/xdev.ts +54 -26
- package/src/tts/streaming-player.ts +81 -340
- package/src/utils/clipboard.ts +1 -30
- package/src/utils/mac-file-urls.applescript +37 -0
- package/src/utils/tool-choice.ts +14 -0
- package/src/utils/tools-manager.ts +1 -19
- package/src/web/search/index.ts +28 -5
- package/src/web/search/providers/anthropic.ts +62 -4
- package/src/web/search/providers/base.ts +16 -0
- package/src/web/search/providers/brave.ts +30 -3
- package/src/web/search/providers/codex.ts +14 -1
- package/src/web/search/providers/duckduckgo.ts +23 -1
- package/src/web/search/providers/ecosia.ts +5 -1
- package/src/web/search/providers/exa.ts +28 -1
- package/src/web/search/providers/firecrawl.ts +37 -3
- package/src/web/search/providers/gemini.ts +12 -2
- package/src/web/search/providers/google.ts +2 -1
- package/src/web/search/providers/jina.ts +31 -8
- package/src/web/search/providers/kagi.ts +10 -1
- package/src/web/search/providers/kimi.ts +16 -1
- package/src/web/search/providers/mojeek.ts +14 -1
- package/src/web/search/providers/parallel.ts +48 -2
- package/src/web/search/providers/perplexity.ts +94 -6
- package/src/web/search/providers/searxng.ts +145 -9
- package/src/web/search/providers/startpage.ts +8 -2
- package/src/web/search/providers/synthetic.ts +7 -1
- package/src/web/search/providers/tavily.ts +52 -3
- package/src/web/search/providers/tinyfish.ts +6 -1
- package/src/web/search/providers/xai.ts +49 -2
- package/src/web/search/providers/zai.ts +19 -1
- package/src/web/search/query.ts +850 -0
- package/dist/types/stt/recorder.d.ts +0 -30
- package/dist/types/stt/transcriber.d.ts +0 -14
- package/dist/types/stt/wav.d.ts +0 -29
- package/dist/types/tts/player.d.ts +0 -32
- package/src/live/audio-worklet.txt +0 -59
- package/src/live/browser-runtime.txt +0 -221
- package/src/stt/recorder.ts +0 -551
- package/src/stt/transcriber.ts +0 -60
- package/src/stt/wav.ts +0 -173
- package/src/tts/player.ts +0 -137
package/src/utils/tool-choice.ts
CHANGED
|
@@ -12,6 +12,19 @@ export function buildNamedToolChoice(toolName: string, model?: Model<Api>): Tool
|
|
|
12
12
|
return { type: "tool", name: toolName };
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
if (toolName === "computer" && model.supportsComputerUse === true) {
|
|
16
|
+
if (
|
|
17
|
+
model.api === "openai-codex-responses" ||
|
|
18
|
+
model.api === "openai-responses" ||
|
|
19
|
+
model.api === "azure-openai-responses"
|
|
20
|
+
) {
|
|
21
|
+
return { type: "computer" };
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
// Models without native computer support receive the tool as a regular
|
|
26
|
+
// function tool, so the generic named-function forcing below applies.
|
|
27
|
+
|
|
15
28
|
if (
|
|
16
29
|
model.api === "openai-codex-responses" ||
|
|
17
30
|
model.api === "openai-responses" ||
|
|
@@ -39,6 +52,7 @@ export function buildNamedToolChoice(toolName: string, model?: Model<Api>): Tool
|
|
|
39
52
|
*/
|
|
40
53
|
export function isToolChoiceActive(toolChoice: ToolChoice | undefined, tools: readonly { name: string }[]): boolean {
|
|
41
54
|
if (!toolChoice || typeof toolChoice === "string") return true;
|
|
55
|
+
if (toolChoice.type === "computer") return tools.some(tool => tool.name === "computer");
|
|
42
56
|
const name =
|
|
43
57
|
toolChoice.type === "tool"
|
|
44
58
|
? toolChoice.name
|
|
@@ -73,16 +73,6 @@ interface ToolConfig {
|
|
|
73
73
|
getAssetName: (version: string, plat: string, architecture: string) => string | null;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
// ffmpeg static-binary asset names (eugeneware/ffmpeg-static direct binaries).
|
|
77
|
-
// Maps node arch (arm64|x64) only; everything else is unsupported.
|
|
78
|
-
export function ffmpegAssetName(_version: string, plat: string, architecture: string): string | null {
|
|
79
|
-
if (architecture !== "arm64" && architecture !== "x64") return null;
|
|
80
|
-
if (plat === "darwin") return `ffmpeg-darwin-${architecture}`;
|
|
81
|
-
if (plat === "linux") return `ffmpeg-linux-${architecture}`;
|
|
82
|
-
if (plat === "win32") return architecture === "x64" ? "ffmpeg-win32-x64" : null;
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
76
|
const TOOLS: Record<string, ToolConfig> = {
|
|
87
77
|
sd: {
|
|
88
78
|
name: "sd",
|
|
@@ -139,14 +129,6 @@ const TOOLS: Record<string, ToolConfig> = {
|
|
|
139
129
|
return null;
|
|
140
130
|
},
|
|
141
131
|
},
|
|
142
|
-
ffmpeg: {
|
|
143
|
-
name: "ffmpeg",
|
|
144
|
-
repo: "eugeneware/ffmpeg-static",
|
|
145
|
-
binaryName: "ffmpeg",
|
|
146
|
-
tagPrefix: "",
|
|
147
|
-
isDirectBinary: true,
|
|
148
|
-
getAssetName: ffmpegAssetName,
|
|
149
|
-
},
|
|
150
132
|
};
|
|
151
133
|
|
|
152
134
|
// CLI packages installed via uv/pip
|
|
@@ -164,7 +146,7 @@ const PYTHON_TOOLS: Record<string, PythonPackageToolConfig> = {
|
|
|
164
146
|
},
|
|
165
147
|
};
|
|
166
148
|
|
|
167
|
-
export type ToolName = "sd" | "sg" | "yt-dlp" | "trafilatura"
|
|
149
|
+
export type ToolName = "sd" | "sg" | "yt-dlp" | "trafilatura";
|
|
168
150
|
|
|
169
151
|
// Get the path to a tool (system-wide or in our tools dir)
|
|
170
152
|
export function getToolPath(tool: ToolName): string | null {
|
package/src/web/search/index.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
type SearchProvider,
|
|
28
28
|
type SearchProviderCandidate,
|
|
29
29
|
} from "./provider";
|
|
30
|
+
import { applyQueryConstraints, parseSearchQuery } from "./query";
|
|
30
31
|
import { renderSearchCall, renderSearchResult, type SearchRenderDetails } from "./render";
|
|
31
32
|
import type { SearchProviderId, SearchResponse } from "./types";
|
|
32
33
|
import { SearchProviderError } from "./types";
|
|
@@ -57,9 +58,12 @@ function formatCount(label: string, count: number): string {
|
|
|
57
58
|
return `${count} ${label}${count === 1 ? "" : "s"}`;
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
/** Format response for LLM consumption */
|
|
61
|
-
function formatForLLM(response: SearchResponse): string {
|
|
61
|
+
/** Format response for LLM consumption. `notes` lead the output (e.g. relaxed-constraint warnings). */
|
|
62
|
+
function formatForLLM(response: SearchResponse, notes: readonly string[] = []): string {
|
|
62
63
|
const parts: string[] = [];
|
|
64
|
+
for (const note of notes) {
|
|
65
|
+
parts.push(`Note: ${note}`);
|
|
66
|
+
}
|
|
63
67
|
|
|
64
68
|
if (response.answer) {
|
|
65
69
|
parts.push(response.answer);
|
|
@@ -141,6 +145,8 @@ async function executeSearch(
|
|
|
141
145
|
candidates = resolveProviderCandidates();
|
|
142
146
|
}
|
|
143
147
|
|
|
148
|
+
const parsedQuery = parseSearchQuery(params.query);
|
|
149
|
+
|
|
144
150
|
// Invariant across providers; read once and tolerate an uninitialized
|
|
145
151
|
// Settings singleton (e.g. `omp q ...` CLI path, unit tests) so the
|
|
146
152
|
// provider-fallback loop never aborts before any provider runs.
|
|
@@ -182,6 +188,7 @@ async function executeSearch(
|
|
|
182
188
|
|
|
183
189
|
const response = await provider.search({
|
|
184
190
|
query: params.query,
|
|
191
|
+
parsedQuery,
|
|
185
192
|
limit: params.limit,
|
|
186
193
|
recency: params.recency,
|
|
187
194
|
systemPrompt: webSearchSystemPrompt,
|
|
@@ -196,15 +203,31 @@ async function executeSearch(
|
|
|
196
203
|
geminiModel,
|
|
197
204
|
});
|
|
198
205
|
|
|
199
|
-
|
|
206
|
+
// Lenient constraint pass over whatever the provider returned: enforce
|
|
207
|
+
// site:/inurl:/intitle:/filetype:/date directives the provider could
|
|
208
|
+
// not (or only partially) honor natively, relaxing any dimension that
|
|
209
|
+
// would wipe out every result. Citations/answer text stay untouched.
|
|
210
|
+
let finalResponse = response;
|
|
211
|
+
const constraintNotes: string[] = [];
|
|
212
|
+
if (parsedQuery.hasConstraints && response.sources.length > 0) {
|
|
213
|
+
const filtered = applyQueryConstraints(response.sources, parsedQuery);
|
|
214
|
+
if (filtered.sources.length !== response.sources.length) {
|
|
215
|
+
finalResponse = { ...response, sources: filtered.sources };
|
|
216
|
+
}
|
|
217
|
+
for (const label of filtered.dropped) {
|
|
218
|
+
constraintNotes.push(`no results matched \`${label}\`; the constraint was relaxed`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (!hasRenderableSearchContent(finalResponse)) {
|
|
200
223
|
throw new SearchProviderError(provider.id, `${provider.label} returned no renderable search content.`, 204);
|
|
201
224
|
}
|
|
202
225
|
|
|
203
|
-
const text = formatForLLM(
|
|
226
|
+
const text = formatForLLM(finalResponse, constraintNotes);
|
|
204
227
|
|
|
205
228
|
return {
|
|
206
229
|
content: [{ type: "text" as const, text }],
|
|
207
|
-
details: { response },
|
|
230
|
+
details: { response: finalResponse },
|
|
208
231
|
};
|
|
209
232
|
} catch (error) {
|
|
210
233
|
// Surface user-initiated cancellation immediately so the session sees
|
|
@@ -28,6 +28,7 @@ import type {
|
|
|
28
28
|
SearchSource,
|
|
29
29
|
} from "../../../web/search/types";
|
|
30
30
|
import { SearchProviderError } from "../../../web/search/types";
|
|
31
|
+
import { formatQuery, parseSearchQuery, type QuerySyntax, type StructuredQuery } from "../query";
|
|
31
32
|
import type { SearchParams } from "./base";
|
|
32
33
|
import { SearchProvider } from "./base";
|
|
33
34
|
import { classifyProviderHttpError, withHardTimeout } from "./utils";
|
|
@@ -36,6 +37,59 @@ const DEFAULT_MODEL = "claude-haiku-4-5";
|
|
|
36
37
|
const DEFAULT_MAX_TOKENS = 4096;
|
|
37
38
|
const WEB_SEARCH_TOOL_NAME = "web_search";
|
|
38
39
|
const WEB_SEARCH_TOOL_TYPE = "web_search_20250305";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Claude's search backend understands common Google-style operators, so most
|
|
43
|
+
* directives are re-emitted as query text. `site:` is intentionally absent:
|
|
44
|
+
* site includes/excludes map onto the web_search tool's native
|
|
45
|
+
* `allowed_domains`/`blocked_domains` parameters instead.
|
|
46
|
+
*/
|
|
47
|
+
const ANTHROPIC_QUERY_SYNTAX: QuerySyntax = {
|
|
48
|
+
phrases: true,
|
|
49
|
+
negation: true,
|
|
50
|
+
or: true,
|
|
51
|
+
inUrl: true,
|
|
52
|
+
inTitle: true,
|
|
53
|
+
filetype: true,
|
|
54
|
+
dateRange: true,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** Upstream request shape derived from the parsed query. */
|
|
58
|
+
interface AnthropicQueryPlan {
|
|
59
|
+
query: string;
|
|
60
|
+
allowedDomains?: string[];
|
|
61
|
+
blockedDomains?: string[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Map parsed directives onto the request: `site:` includes become
|
|
66
|
+
* `allowed_domains`, `-site:` exclusions become `blocked_domains` (the two are
|
|
67
|
+
* mutually exclusive on the API, so exclusions are only sent when there are no
|
|
68
|
+
* includes), and remaining directives are re-emitted as query syntax.
|
|
69
|
+
* Directive-free queries pass through byte-identical. Anthropic domain
|
|
70
|
+
* filters take bare hosts (subdomains included automatically); any path part
|
|
71
|
+
* of a `site:` value is enforced by the central constraint filter.
|
|
72
|
+
*/
|
|
73
|
+
function planQuery(rawQuery: string, parsed: StructuredQuery): AnthropicQueryPlan {
|
|
74
|
+
if (!parsed.hasDirectives) return { query: rawQuery };
|
|
75
|
+
const hosts = (sites: readonly string[]) => {
|
|
76
|
+
const unique = new Set<string>();
|
|
77
|
+
for (const site of sites) {
|
|
78
|
+
const slash = site.indexOf("/");
|
|
79
|
+
const host = slash === -1 ? site : site.slice(0, slash);
|
|
80
|
+
if (host.length > 0) unique.add(host);
|
|
81
|
+
}
|
|
82
|
+
return [...unique];
|
|
83
|
+
};
|
|
84
|
+
const allowed = hosts(parsed.sites);
|
|
85
|
+
const blocked = allowed.length === 0 ? hosts(parsed.excludedSites) : [];
|
|
86
|
+
return {
|
|
87
|
+
query: formatQuery(parsed, ANTHROPIC_QUERY_SYNTAX),
|
|
88
|
+
allowedDomains: allowed.length > 0 ? allowed : undefined,
|
|
89
|
+
blockedDomains: blocked.length > 0 ? blocked : undefined,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
39
93
|
export interface AnthropicSearchParams {
|
|
40
94
|
query: string;
|
|
41
95
|
system_prompt?: string;
|
|
@@ -82,7 +136,7 @@ function buildSystemBlocks(
|
|
|
82
136
|
* Calls the Anthropic API with web search tool enabled.
|
|
83
137
|
* @param auth - Authentication configuration (API key or OAuth)
|
|
84
138
|
* @param model - Model identifier to use
|
|
85
|
-
* @param
|
|
139
|
+
* @param plan - Query text plus native domain filters derived from parsed directives
|
|
86
140
|
* @param metadataUserId - Optional Anthropic Messages metadata.user_id (already shaped for OAuth)
|
|
87
141
|
* @param systemPrompt - Optional system prompt for guiding response style
|
|
88
142
|
* @returns Raw API response from Anthropic
|
|
@@ -91,7 +145,7 @@ function buildSystemBlocks(
|
|
|
91
145
|
async function callSearch(
|
|
92
146
|
auth: AnthropicAuthConfig,
|
|
93
147
|
model: string,
|
|
94
|
-
|
|
148
|
+
plan: AnthropicQueryPlan,
|
|
95
149
|
metadataUserId?: string,
|
|
96
150
|
systemPrompt?: string,
|
|
97
151
|
maxTokens?: number,
|
|
@@ -107,11 +161,13 @@ async function callSearch(
|
|
|
107
161
|
const body: Record<string, unknown> = {
|
|
108
162
|
model,
|
|
109
163
|
max_tokens: maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
110
|
-
messages: [{ role: "user", content: query }],
|
|
164
|
+
messages: [{ role: "user", content: plan.query }],
|
|
111
165
|
tools: [
|
|
112
166
|
{
|
|
113
167
|
type: WEB_SEARCH_TOOL_TYPE,
|
|
114
168
|
name: WEB_SEARCH_TOOL_NAME,
|
|
169
|
+
...(plan.allowedDomains ? { allowed_domains: plan.allowedDomains } : {}),
|
|
170
|
+
...(plan.blockedDomains ? { blocked_domains: plan.blockedDomains } : {}),
|
|
115
171
|
},
|
|
116
172
|
],
|
|
117
173
|
};
|
|
@@ -283,6 +339,8 @@ export async function searchAnthropic(
|
|
|
283
339
|
const callerSessionId = "authStorage" in params ? params.sessionId : undefined;
|
|
284
340
|
const accountId =
|
|
285
341
|
"authStorage" in params ? params.authStorage.getOAuthAccountId("anthropic", params.sessionId) : undefined;
|
|
342
|
+
const parsed = ("parsedQuery" in params ? params.parsedQuery : undefined) ?? parseSearchQuery(params.query);
|
|
343
|
+
const plan = planQuery(params.query, parsed);
|
|
286
344
|
const response = await withAuth(
|
|
287
345
|
keyOrResolver,
|
|
288
346
|
key => {
|
|
@@ -302,7 +360,7 @@ export async function searchAnthropic(
|
|
|
302
360
|
return callSearch(
|
|
303
361
|
auth,
|
|
304
362
|
model,
|
|
305
|
-
|
|
363
|
+
plan,
|
|
306
364
|
metadataUserId,
|
|
307
365
|
systemPrompt,
|
|
308
366
|
maxTokens,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AuthStorage, FetchImpl } from "@oh-my-pi/pi-ai";
|
|
2
2
|
import type { ModelRegistry } from "../../../config/model-registry";
|
|
3
|
+
import type { StructuredQuery } from "../query";
|
|
3
4
|
import type { SearchProviderId, SearchResponse } from "../types";
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -14,6 +15,21 @@ import type { SearchProviderId, SearchResponse } from "../types";
|
|
|
14
15
|
*/
|
|
15
16
|
export interface SearchParams {
|
|
16
17
|
query: string;
|
|
18
|
+
/**
|
|
19
|
+
* Structured view of `query`, parsed once by the search pipeline:
|
|
20
|
+
* Google-style directives (`site:`, `before:`/`after:`, `inurl:`,
|
|
21
|
+
* `intitle:`, `filetype:`, quoted phrases, `OR` groups, `-exclusions`)
|
|
22
|
+
* extracted into fields.
|
|
23
|
+
*
|
|
24
|
+
* Providers SHOULD map constraints onto native API parameters
|
|
25
|
+
* (domain/date filters) or engine query syntax (`formatQuery`) where the
|
|
26
|
+
* upstream supports them, and lean lenient otherwise: the pipeline
|
|
27
|
+
* post-filters every response with `applyQueryConstraints`, which
|
|
28
|
+
* relaxes any constraint that would eliminate all results — so a
|
|
29
|
+
* best-effort search always beats an empty one. When absent (direct
|
|
30
|
+
* provider calls), parse with `parseSearchQuery(params.query)`.
|
|
31
|
+
*/
|
|
32
|
+
parsedQuery?: StructuredQuery;
|
|
17
33
|
limit?: number;
|
|
18
34
|
/**
|
|
19
35
|
* Temporal filter narrowing results to the specified time window.
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
import { type AuthStorage, type FetchImpl, getEnvApiKey } from "@oh-my-pi/pi-ai";
|
|
8
8
|
import type { SearchResponse, SearchSource } from "../../../web/search/types";
|
|
9
9
|
import { SearchProviderError } from "../../../web/search/types";
|
|
10
|
+
import type { QuerySyntax, StructuredQuery } from "../query";
|
|
11
|
+
import { formatQuery, GOOGLE_QUERY_SYNTAX, parseSearchQuery } from "../query";
|
|
10
12
|
import { clampNumResults, dateToAgeSeconds } from "../utils";
|
|
11
13
|
import type { SearchParams } from "./base";
|
|
12
14
|
import { SearchProvider } from "./base";
|
|
@@ -23,10 +25,32 @@ const RECENCY_MAP: Record<"day" | "week" | "month" | "year", "pd" | "pw" | "pm"
|
|
|
23
25
|
year: "py",
|
|
24
26
|
};
|
|
25
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Brave parses the classic operator set inline (site:, quotes, -, OR…) but
|
|
30
|
+
* date bounds map onto the native `freshness` param, so `before:`/`after:`
|
|
31
|
+
* tokens are stripped from the rebuilt query string.
|
|
32
|
+
*/
|
|
33
|
+
const BRAVE_QUERY_SYNTAX: QuerySyntax = { ...GOOGLE_QUERY_SYNTAX, dateRange: false };
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Freshness param: explicit `after:`/`before:` bounds win over the
|
|
37
|
+
* recency-derived period, rendered as Brave's absolute range
|
|
38
|
+
* `YYYY-MM-DDtoYYYY-MM-DD` with sensible open ends.
|
|
39
|
+
*/
|
|
40
|
+
function braveFreshness(parsed: StructuredQuery, recency?: keyof typeof RECENCY_MAP): string | undefined {
|
|
41
|
+
if (parsed.after || parsed.before) {
|
|
42
|
+
const start = parsed.after ?? "1970-01-01";
|
|
43
|
+
const end = parsed.before ?? new Date().toISOString().slice(0, 10);
|
|
44
|
+
return `${start}to${end}`;
|
|
45
|
+
}
|
|
46
|
+
return recency ? RECENCY_MAP[recency] : undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
26
49
|
export interface BraveSearchParams {
|
|
27
50
|
query: string;
|
|
28
51
|
num_results?: number;
|
|
29
52
|
recency?: "day" | "week" | "month" | "year";
|
|
53
|
+
parsedQuery?: StructuredQuery;
|
|
30
54
|
signal?: AbortSignal;
|
|
31
55
|
fetch?: FetchImpl;
|
|
32
56
|
}
|
|
@@ -73,12 +97,14 @@ async function callBraveSearch(
|
|
|
73
97
|
params: BraveSearchParams,
|
|
74
98
|
): Promise<{ response: BraveSearchResponse; requestId?: string }> {
|
|
75
99
|
const numResults = clampNumResults(params.num_results, DEFAULT_NUM_RESULTS, MAX_NUM_RESULTS);
|
|
100
|
+
const parsed = params.parsedQuery ?? parseSearchQuery(params.query);
|
|
76
101
|
const url = new URL(BRAVE_SEARCH_URL);
|
|
77
|
-
url.searchParams.set("q", params.query);
|
|
102
|
+
url.searchParams.set("q", parsed.hasDirectives ? formatQuery(parsed, BRAVE_QUERY_SYNTAX) : params.query);
|
|
78
103
|
url.searchParams.set("count", String(numResults));
|
|
79
104
|
url.searchParams.set("extra_snippets", "true");
|
|
80
|
-
|
|
81
|
-
|
|
105
|
+
const freshness = braveFreshness(parsed, params.recency);
|
|
106
|
+
if (freshness) {
|
|
107
|
+
url.searchParams.set("freshness", freshness);
|
|
82
108
|
}
|
|
83
109
|
|
|
84
110
|
const fetchImpl = params.fetch ?? fetch;
|
|
@@ -145,6 +171,7 @@ export class BraveProvider extends SearchProvider {
|
|
|
145
171
|
query: params.query,
|
|
146
172
|
num_results: params.numSearchResults ?? params.limit,
|
|
147
173
|
recency: params.recency,
|
|
174
|
+
parsedQuery: params.parsedQuery,
|
|
148
175
|
signal: params.signal,
|
|
149
176
|
fetch: params.fetch,
|
|
150
177
|
});
|
|
@@ -31,6 +31,7 @@ import packageJson from "../../../../package.json" with { type: "json" };
|
|
|
31
31
|
import type { ModelRegistry } from "../../../config/model-registry";
|
|
32
32
|
import type { SearchResponse, SearchSource } from "../../../web/search/types";
|
|
33
33
|
import { SearchProviderError } from "../../../web/search/types";
|
|
34
|
+
import { formatQuery, GOOGLE_QUERY_SYNTAX, parseSearchQuery } from "../query";
|
|
34
35
|
import type { SearchParams } from "./base";
|
|
35
36
|
import { SearchProvider } from "./base";
|
|
36
37
|
import { classifyProviderHttpError, withHardTimeout } from "./utils";
|
|
@@ -572,6 +573,7 @@ async function callCodexSearch(
|
|
|
572
573
|
async function runCodexSearchCandidates(options: {
|
|
573
574
|
auth: { accessToken: string; accountId?: string };
|
|
574
575
|
params: SearchParams;
|
|
576
|
+
query: string;
|
|
575
577
|
modelCandidates: CodexModelCandidate[];
|
|
576
578
|
modelWasConfigured: boolean;
|
|
577
579
|
transport: CodexSearchTransport;
|
|
@@ -582,7 +584,7 @@ async function runCodexSearchCandidates(options: {
|
|
|
582
584
|
if (!candidate) continue;
|
|
583
585
|
|
|
584
586
|
try {
|
|
585
|
-
return await callCodexSearch(options.auth, options.
|
|
587
|
+
return await callCodexSearch(options.auth, options.query, {
|
|
586
588
|
signal: options.params.signal,
|
|
587
589
|
systemPrompt: options.params.systemPrompt,
|
|
588
590
|
searchContextSize: "high",
|
|
@@ -622,6 +624,15 @@ export async function searchCodex(params: SearchParams): Promise<SearchResponse>
|
|
|
622
624
|
throw new SearchProviderError("codex", "No Codex web search model is configured.");
|
|
623
625
|
}
|
|
624
626
|
const transport = resolveCodexSearchTransport(params.modelRegistry, firstCandidate.modelId);
|
|
627
|
+
// The ChatGPT-backend Codex endpoint speaks the undocumented codex-rs
|
|
628
|
+
// request shape (responses-lite moves tools into an `additional_tools`
|
|
629
|
+
// developer item), so the documented `web_search.filters.allowed_domains`
|
|
630
|
+
// parameter cannot be assumed to survive it. Instead, re-emit directive
|
|
631
|
+
// queries with the full Google-style operator syntax — the backing index
|
|
632
|
+
// parses the classic operator set — and leave directive-free queries
|
|
633
|
+
// byte-identical.
|
|
634
|
+
const parsed = params.parsedQuery ?? parseSearchQuery(params.query);
|
|
635
|
+
const query = parsed.hasDirectives ? formatQuery(parsed, GOOGLE_QUERY_SYNTAX) : params.query;
|
|
625
636
|
|
|
626
637
|
let result: CodexSearchResult;
|
|
627
638
|
if (transport.customEndpoint) {
|
|
@@ -652,6 +663,7 @@ export async function searchCodex(params: SearchParams): Promise<SearchResponse>
|
|
|
652
663
|
runCodexSearchCandidates({
|
|
653
664
|
auth: { accessToken },
|
|
654
665
|
params,
|
|
666
|
+
query,
|
|
655
667
|
modelCandidates,
|
|
656
668
|
modelWasConfigured: configuredModel !== undefined,
|
|
657
669
|
transport,
|
|
@@ -682,6 +694,7 @@ export async function searchCodex(params: SearchParams): Promise<SearchResponse>
|
|
|
682
694
|
return runCodexSearchCandidates({
|
|
683
695
|
auth: { accessToken: access.accessToken, accountId },
|
|
684
696
|
params,
|
|
697
|
+
query,
|
|
685
698
|
modelCandidates,
|
|
686
699
|
modelWasConfigured: configuredModel !== undefined,
|
|
687
700
|
transport,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { AuthStorage } from "@oh-my-pi/pi-ai";
|
|
2
2
|
import type { SearchResponse, SearchSource } from "../../../web/search/types";
|
|
3
3
|
import { SearchProviderError } from "../../../web/search/types";
|
|
4
|
+
import type { QuerySyntax } from "../query";
|
|
5
|
+
import { formatScraperQuery } from "../query";
|
|
4
6
|
import { clampNumResults } from "../utils";
|
|
5
7
|
import type { SearchParams } from "./base";
|
|
6
8
|
import { SearchProvider } from "./base";
|
|
@@ -118,8 +120,28 @@ function isAnomalyResponse(html: string): boolean {
|
|
|
118
120
|
return html.includes("anomaly-modal") || html.includes("anomaly.js");
|
|
119
121
|
}
|
|
120
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Query syntax the DDG HTML frontend parses: quotes, `-`, OR, site:,
|
|
125
|
+
* filetype:, intitle:, inurl:, intext:. Date bounds (`before:`/`after:`) are
|
|
126
|
+
* deliberately off — DDG does not parse them, so they are stripped from the
|
|
127
|
+
* query and enforced by the pipeline's lenient post-filter instead.
|
|
128
|
+
*/
|
|
129
|
+
const DDG_QUERY_SYNTAX: QuerySyntax = {
|
|
130
|
+
phrases: true,
|
|
131
|
+
negation: true,
|
|
132
|
+
or: true,
|
|
133
|
+
site: true,
|
|
134
|
+
inUrl: true,
|
|
135
|
+
inTitle: true,
|
|
136
|
+
inText: true,
|
|
137
|
+
filetype: true,
|
|
138
|
+
};
|
|
139
|
+
|
|
121
140
|
async function callDuckDuckGoHtml(params: SearchParams): Promise<string> {
|
|
122
|
-
const form = new URLSearchParams({
|
|
141
|
+
const form = new URLSearchParams({
|
|
142
|
+
q: formatScraperQuery(params.query, params.parsedQuery, DDG_QUERY_SYNTAX),
|
|
143
|
+
kl: "us-en",
|
|
144
|
+
});
|
|
123
145
|
const df = params.recency ? RECENCY_TO_DDG_DF[params.recency] : undefined;
|
|
124
146
|
if (df) form.set("df", df);
|
|
125
147
|
// Add b: "" parameter as specified in the browser fetch template to match real browser form submission
|
|
@@ -2,6 +2,7 @@ import type { AuthStorage } from "@oh-my-pi/pi-ai";
|
|
|
2
2
|
import { parseHTML } from "linkedom";
|
|
3
3
|
import type { SearchResponse, SearchSource } from "../../../web/search/types";
|
|
4
4
|
import { SearchProviderError } from "../../../web/search/types";
|
|
5
|
+
import { formatScraperQuery } from "../query";
|
|
5
6
|
import { clampNumResults } from "../utils";
|
|
6
7
|
import type { SearchParams } from "./base";
|
|
7
8
|
import { SearchProvider } from "./base";
|
|
@@ -100,7 +101,10 @@ function isBlockedPage(page: LoadedHtmlPage): boolean {
|
|
|
100
101
|
async function callEcosiaHtml(params: SearchParams): Promise<string> {
|
|
101
102
|
const signal = withHardTimeout(params.signal);
|
|
102
103
|
const url = new URL(ECOSIA_SEARCH_URL);
|
|
103
|
-
|
|
104
|
+
// Ecosia serves Google-backed results, so classic operators pass through
|
|
105
|
+
// inline; canonicalize aliases (domain: -> site:, since: -> after:) and
|
|
106
|
+
// demote scraper-hostile operators via the shared scraper formatter.
|
|
107
|
+
url.searchParams.set("q", formatScraperQuery(params.query, params.parsedQuery));
|
|
104
108
|
|
|
105
109
|
let page: LoadedHtmlPage;
|
|
106
110
|
try {
|
|
@@ -12,6 +12,7 @@ import { findApiKey, isSearchResponse } from "../../../exa/mcp-client";
|
|
|
12
12
|
import { parseSSE } from "../../../mcp/json-rpc";
|
|
13
13
|
import type { SearchResponse, SearchSource } from "../../../web/search/types";
|
|
14
14
|
import { SearchProviderError } from "../../../web/search/types";
|
|
15
|
+
import { formatQuery, parseSearchQuery, type StructuredQuery } from "../query";
|
|
15
16
|
import { dateToAgeSeconds } from "../utils";
|
|
16
17
|
import type { SearchParams } from "./base";
|
|
17
18
|
import { SearchProvider } from "./base";
|
|
@@ -472,8 +473,9 @@ export class ExaProvider extends SearchProvider {
|
|
|
472
473
|
}
|
|
473
474
|
|
|
474
475
|
search(params: SearchParams): Promise<SearchResponse> {
|
|
476
|
+
const parsed = params.parsedQuery ?? parseSearchQuery(params.query);
|
|
475
477
|
return searchExa({
|
|
476
|
-
|
|
478
|
+
...directiveParams(parsed),
|
|
477
479
|
num_results: params.numSearchResults ?? params.limit,
|
|
478
480
|
signal: params.signal,
|
|
479
481
|
authStorage: params.authStorage,
|
|
@@ -482,3 +484,28 @@ export class ExaProvider extends SearchProvider {
|
|
|
482
484
|
});
|
|
483
485
|
}
|
|
484
486
|
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Map parsed query directives onto Exa's native request parameters:
|
|
490
|
+
* `site:` → includeDomains, `-site:` → excludeDomains (bare hosts; path parts
|
|
491
|
+
* are enforced by the central constraint filter), `after:`/`before:` →
|
|
492
|
+
* start/endPublishedDate (ISO 8601). Exa's neural search prefers natural
|
|
493
|
+
* language, so the query itself is re-emitted with quoted phrases only.
|
|
494
|
+
* Directive-free queries pass through byte-identical.
|
|
495
|
+
*/
|
|
496
|
+
function directiveParams(
|
|
497
|
+
parsed: StructuredQuery,
|
|
498
|
+
): Pick<
|
|
499
|
+
ExaSearchParams,
|
|
500
|
+
"query" | "include_domains" | "exclude_domains" | "start_published_date" | "end_published_date"
|
|
501
|
+
> {
|
|
502
|
+
if (!parsed.hasDirectives) return { query: parsed.raw };
|
|
503
|
+
const hosts = (sites: readonly string[]) => [...new Set(sites.map(site => site.split("/", 1)[0]))];
|
|
504
|
+
return {
|
|
505
|
+
query: formatQuery(parsed, { phrases: true }),
|
|
506
|
+
include_domains: parsed.sites.length ? hosts(parsed.sites) : undefined,
|
|
507
|
+
exclude_domains: parsed.excludedSites.length ? hosts(parsed.excludedSites) : undefined,
|
|
508
|
+
start_published_date: parsed.after,
|
|
509
|
+
end_published_date: parsed.before,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from "@oh-my-pi/pi-ai";
|
|
15
15
|
import type { SearchResponse, SearchSource } from "../../../web/search/types";
|
|
16
16
|
import { SearchProviderError } from "../../../web/search/types";
|
|
17
|
+
import { formatQuery, GOOGLE_QUERY_SYNTAX, parseSearchQuery, type StructuredQuery } from "../query";
|
|
17
18
|
import { clampNumResults } from "../utils";
|
|
18
19
|
import type { SearchParams } from "./base";
|
|
19
20
|
import { SearchProvider } from "./base";
|
|
@@ -34,6 +35,8 @@ export interface FirecrawlSearchParams {
|
|
|
34
35
|
query: string;
|
|
35
36
|
num_results?: number;
|
|
36
37
|
recency?: SearchParams["recency"];
|
|
38
|
+
/** Explicit `tbs` (custom date range); takes precedence over `recency`. */
|
|
39
|
+
tbs?: string;
|
|
37
40
|
signal?: AbortSignal;
|
|
38
41
|
fetch?: FetchImpl;
|
|
39
42
|
}
|
|
@@ -67,8 +70,9 @@ function buildRequestBody(params: FirecrawlSearchParams): Record<string, unknown
|
|
|
67
70
|
limit: clampNumResults(params.num_results, DEFAULT_NUM_RESULTS, MAX_NUM_RESULTS),
|
|
68
71
|
sources: [{ type: "web" }],
|
|
69
72
|
};
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
const tbs = params.tbs ?? (params.recency ? RECENCY_TBS[params.recency] : undefined);
|
|
74
|
+
if (tbs) {
|
|
75
|
+
body.tbs = tbs;
|
|
72
76
|
}
|
|
73
77
|
return body;
|
|
74
78
|
}
|
|
@@ -104,12 +108,42 @@ async function callFirecrawlSearch(
|
|
|
104
108
|
return (await response.json()) as FirecrawlSearchResponse;
|
|
105
109
|
}
|
|
106
110
|
|
|
111
|
+
/** ISO `YYYY-MM-DD` to Google `MM/DD/YYYY` for `tbs=cdr` custom date ranges. */
|
|
112
|
+
function toGoogleDate(iso: string): string {
|
|
113
|
+
const [year, month, day] = iso.split("-");
|
|
114
|
+
return `${month}/${day}/${year}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Map explicit `before:`/`after:` bounds to a Firecrawl `tbs` custom date
|
|
119
|
+
* range (`cdr:1,cd_min:MM/DD/YYYY,cd_max:MM/DD/YYYY`), or undefined when the
|
|
120
|
+
* query carries no absolute date bounds.
|
|
121
|
+
*/
|
|
122
|
+
function buildDateTbs(parsed: StructuredQuery): string | undefined {
|
|
123
|
+
if (!parsed.after && !parsed.before) return undefined;
|
|
124
|
+
const parts = ["cdr:1"];
|
|
125
|
+
if (parsed.after) parts.push(`cd_min:${toGoogleDate(parsed.after)}`);
|
|
126
|
+
if (parsed.before) parts.push(`cd_max:${toGoogleDate(parsed.before)}`);
|
|
127
|
+
return parts.join(",");
|
|
128
|
+
}
|
|
129
|
+
|
|
107
130
|
/** Execute Firecrawl web search. */
|
|
108
131
|
export async function searchFirecrawl(params: SearchParams): Promise<SearchResponse> {
|
|
132
|
+
const parsed = params.parsedQuery ?? parseSearchQuery(params.query);
|
|
133
|
+
let query = params.query;
|
|
134
|
+
let tbs: string | undefined;
|
|
135
|
+
if (parsed.hasDirectives) {
|
|
136
|
+
// Firecrawl search is SERP-backed: the query supports Google operators
|
|
137
|
+
// (site:, inurl:, intitle:, quotes, -, OR). Absolute date bounds move to
|
|
138
|
+
// the native tbs param and are stripped from the query string.
|
|
139
|
+
tbs = buildDateTbs(parsed);
|
|
140
|
+
query = formatQuery(parsed, tbs ? { ...GOOGLE_QUERY_SYNTAX, dateRange: false } : GOOGLE_QUERY_SYNTAX);
|
|
141
|
+
}
|
|
109
142
|
const firecrawlParams: FirecrawlSearchParams = {
|
|
110
|
-
query
|
|
143
|
+
query,
|
|
111
144
|
num_results: params.numSearchResults ?? params.limit,
|
|
112
145
|
recency: params.recency,
|
|
146
|
+
tbs,
|
|
113
147
|
signal: params.signal,
|
|
114
148
|
fetch: params.fetch,
|
|
115
149
|
};
|
|
@@ -18,6 +18,7 @@ import { fetchWithRetry } from "@oh-my-pi/pi-utils";
|
|
|
18
18
|
|
|
19
19
|
import type { SearchCitation, SearchResponse, SearchSource } from "../../../web/search/types";
|
|
20
20
|
import { SearchProviderError } from "../../../web/search/types";
|
|
21
|
+
import { formatQuery, GOOGLE_QUERY_SYNTAX, parseSearchQuery, type StructuredQuery } from "../query";
|
|
21
22
|
import type { SearchParams } from "./base";
|
|
22
23
|
import { SearchProvider } from "./base";
|
|
23
24
|
import { classifyProviderHttpError, withHardTimeout } from "./utils";
|
|
@@ -51,6 +52,8 @@ interface GeminiToolParams {
|
|
|
51
52
|
|
|
52
53
|
export interface GeminiSearchParams extends GeminiToolParams {
|
|
53
54
|
query: string;
|
|
55
|
+
/** Pre-parsed structured query; falls back to parsing `query` when omitted. */
|
|
56
|
+
parsedQuery?: StructuredQuery;
|
|
54
57
|
system_prompt?: string;
|
|
55
58
|
num_results?: number;
|
|
56
59
|
/** Maximum output tokens. */
|
|
@@ -508,6 +511,12 @@ async function callGeminiDeveloperSearch(
|
|
|
508
511
|
*/
|
|
509
512
|
export async function searchGemini(params: GeminiSearchParams): Promise<SearchResponse> {
|
|
510
513
|
const selectedModel = resolveGeminiSearchModel(params.geminiModel);
|
|
514
|
+
// Gemini's googleSearch grounding forwards the query to Google Search, which
|
|
515
|
+
// understands the classic operator set natively. Normalize directive aliases
|
|
516
|
+
// (domain: → site:, since: → after:, …) to canonical Google forms; leave
|
|
517
|
+
// directive-free queries byte-identical.
|
|
518
|
+
const parsed = params.parsedQuery ?? parseSearchQuery(params.query);
|
|
519
|
+
const searchQuery = parsed.hasDirectives ? formatQuery(parsed, GOOGLE_QUERY_SYNTAX) : params.query;
|
|
511
520
|
const seed = await findGeminiAuth(params.authStorage, params.sessionId, params.signal);
|
|
512
521
|
let result: GeminiSearchResult;
|
|
513
522
|
|
|
@@ -528,7 +537,7 @@ export async function searchGemini(params: GeminiSearchParams): Promise<SearchRe
|
|
|
528
537
|
isAntigravity,
|
|
529
538
|
},
|
|
530
539
|
selectedModel,
|
|
531
|
-
|
|
540
|
+
searchQuery,
|
|
532
541
|
params.system_prompt,
|
|
533
542
|
params.max_output_tokens,
|
|
534
543
|
params.temperature,
|
|
@@ -555,7 +564,7 @@ export async function searchGemini(params: GeminiSearchParams): Promise<SearchRe
|
|
|
555
564
|
result = await callGeminiDeveloperSearch(
|
|
556
565
|
apiKey,
|
|
557
566
|
selectedModel,
|
|
558
|
-
|
|
567
|
+
searchQuery,
|
|
559
568
|
params.system_prompt,
|
|
560
569
|
params.max_output_tokens,
|
|
561
570
|
params.temperature,
|
|
@@ -601,6 +610,7 @@ export class GeminiProvider extends SearchProvider {
|
|
|
601
610
|
search(params: SearchParams): Promise<SearchResponse> {
|
|
602
611
|
return searchGemini({
|
|
603
612
|
query: params.query,
|
|
613
|
+
parsedQuery: params.parsedQuery,
|
|
604
614
|
system_prompt: params.systemPrompt,
|
|
605
615
|
num_results: params.numSearchResults ?? params.limit,
|
|
606
616
|
max_output_tokens: params.maxOutputTokens,
|
|
@@ -2,6 +2,7 @@ import type { AuthStorage } from "@oh-my-pi/pi-ai";
|
|
|
2
2
|
import { parseHTML } from "linkedom";
|
|
3
3
|
import type { SearchResponse, SearchSource } from "../../../web/search/types";
|
|
4
4
|
import { SearchProviderError } from "../../../web/search/types";
|
|
5
|
+
import { formatScraperQuery } from "../query";
|
|
5
6
|
import { clampNumResults } from "../utils";
|
|
6
7
|
import type { SearchParams } from "./base";
|
|
7
8
|
import { SearchProvider } from "./base";
|
|
@@ -91,7 +92,7 @@ function parseHtmlResults(html: string): ParsedResult[] {
|
|
|
91
92
|
|
|
92
93
|
function buildSearchUrl(params: SearchParams, numResults: number): string {
|
|
93
94
|
const url = new URL(GOOGLE_SEARCH_URL);
|
|
94
|
-
url.searchParams.set("q", params.query);
|
|
95
|
+
url.searchParams.set("q", formatScraperQuery(params.query, params.parsedQuery));
|
|
95
96
|
url.searchParams.set("num", String(numResults));
|
|
96
97
|
url.searchParams.set("hl", "en");
|
|
97
98
|
url.searchParams.set("gl", "us");
|