@oh-my-pi/pi-coding-agent 11.0.3 → 11.2.0
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 +199 -49
- package/README.md +1 -1
- package/docs/config-usage.md +3 -4
- package/docs/sdk.md +6 -5
- package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
- package/examples/sdk/README.md +1 -1
- package/package.json +19 -11
- package/src/cli/args.ts +11 -94
- package/src/cli/config-cli.ts +1 -1
- package/src/cli/file-processor.ts +3 -3
- package/src/cli/oclif-help.ts +26 -0
- package/src/cli/web-search-cli.ts +148 -0
- package/src/cli.ts +8 -2
- package/src/commands/commit.ts +36 -0
- package/src/commands/config.ts +51 -0
- package/src/commands/grep.ts +41 -0
- package/src/commands/index/index.ts +136 -0
- package/src/commands/jupyter.ts +32 -0
- package/src/commands/plugin.ts +70 -0
- package/src/commands/setup.ts +39 -0
- package/src/commands/shell.ts +29 -0
- package/src/commands/stats.ts +29 -0
- package/src/commands/update.ts +21 -0
- package/src/commands/web-search.ts +50 -0
- package/src/commit/agentic/index.ts +3 -2
- package/src/commit/agentic/tools/analyze-file.ts +1 -3
- package/src/commit/git/errors.ts +4 -6
- package/src/commit/pipeline.ts +3 -2
- package/src/config/keybindings.ts +1 -3
- package/src/config/model-registry.ts +89 -162
- package/src/config/settings-schema.ts +10 -0
- package/src/config.ts +202 -132
- package/src/exa/mcp-client.ts +8 -41
- package/src/export/html/index.ts +1 -1
- package/src/extensibility/extensions/loader.ts +7 -10
- package/src/extensibility/extensions/runner.ts +5 -15
- package/src/extensibility/extensions/types.ts +1 -1
- package/src/extensibility/hooks/runner.ts +6 -9
- package/src/index.ts +0 -1
- package/src/ipy/kernel.ts +10 -22
- package/src/lsp/clients/biome-client.ts +4 -7
- package/src/lsp/clients/lsp-linter-client.ts +4 -6
- package/src/lsp/index.ts +5 -4
- package/src/lsp/utils.ts +18 -0
- package/src/main.ts +86 -181
- package/src/mcp/json-rpc.ts +2 -2
- package/src/mcp/transports/http.ts +12 -49
- package/src/modes/components/armin.ts +1 -3
- package/src/modes/components/assistant-message.ts +4 -4
- package/src/modes/components/bash-execution.ts +5 -3
- package/src/modes/components/branch-summary-message.ts +1 -3
- package/src/modes/components/compaction-summary-message.ts +1 -3
- package/src/modes/components/custom-message.ts +4 -5
- package/src/modes/components/extensions/extension-dashboard.ts +10 -16
- package/src/modes/components/extensions/extension-list.ts +5 -5
- package/src/modes/components/footer.ts +1 -4
- package/src/modes/components/hook-editor.ts +7 -32
- package/src/modes/components/hook-message.ts +4 -5
- package/src/modes/components/model-selector.ts +2 -2
- package/src/modes/components/plugin-settings.ts +16 -20
- package/src/modes/components/python-execution.ts +5 -5
- package/src/modes/components/session-selector.ts +6 -7
- package/src/modes/components/settings-defs.ts +49 -40
- package/src/modes/components/settings-selector.ts +8 -17
- package/src/modes/components/skill-message.ts +1 -3
- package/src/modes/components/status-line-segment-editor.ts +1 -3
- package/src/modes/components/status-line.ts +1 -3
- package/src/modes/components/todo-reminder.ts +5 -7
- package/src/modes/components/tree-selector.ts +10 -12
- package/src/modes/components/ttsr-notification.ts +1 -3
- package/src/modes/components/user-message-selector.ts +2 -4
- package/src/modes/components/welcome.ts +6 -18
- package/src/modes/controllers/event-controller.ts +1 -0
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +7 -34
- package/src/modes/controllers/selector-controller.ts +3 -3
- package/src/modes/interactive-mode.ts +27 -1
- package/src/modes/rpc/rpc-client.ts +2 -5
- package/src/modes/rpc/rpc-mode.ts +2 -2
- package/src/modes/theme/theme.ts +2 -6
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +6 -1
- package/src/patch/index.ts +1 -4
- package/src/prompts/agents/explore.md +1 -0
- package/src/prompts/agents/frontmatter.md +2 -1
- package/src/prompts/agents/init.md +1 -0
- package/src/prompts/agents/plan.md +1 -0
- package/src/prompts/agents/reviewer.md +1 -0
- package/src/prompts/system/subagent-submit-reminder.md +2 -0
- package/src/prompts/system/subagent-system-prompt.md +2 -0
- package/src/prompts/system/subagent-user-prompt.md +8 -0
- package/src/prompts/system/system-prompt.md +5 -3
- package/src/prompts/system/web-search.md +6 -4
- package/src/prompts/tools/task.md +216 -163
- package/src/sdk.ts +11 -110
- package/src/session/agent-session.ts +117 -83
- package/src/session/auth-storage.ts +10 -51
- package/src/session/messages.ts +17 -3
- package/src/session/session-manager.ts +30 -30
- package/src/session/streaming-output.ts +1 -1
- package/src/ssh/ssh-executor.ts +6 -3
- package/src/task/agents.ts +2 -0
- package/src/task/discovery.ts +1 -1
- package/src/task/executor.ts +5 -10
- package/src/task/index.ts +43 -23
- package/src/task/render.ts +67 -64
- package/src/task/template.ts +17 -34
- package/src/task/types.ts +49 -22
- package/src/tools/ask.ts +1 -3
- package/src/tools/bash.ts +1 -4
- package/src/tools/browser.ts +5 -7
- package/src/tools/exit-plan-mode.ts +1 -4
- package/src/tools/fetch.ts +1 -3
- package/src/tools/find.ts +4 -3
- package/src/tools/gemini-image.ts +24 -55
- package/src/tools/grep.ts +4 -4
- package/src/tools/index.ts +12 -14
- package/src/tools/notebook.ts +1 -5
- package/src/tools/python.ts +4 -3
- package/src/tools/read.ts +2 -4
- package/src/tools/render-utils.ts +23 -0
- package/src/tools/ssh.ts +8 -12
- package/src/tools/todo-write.ts +1 -4
- package/src/tools/tool-errors.ts +1 -4
- package/src/tools/write.ts +1 -3
- package/src/utils/external-editor.ts +59 -0
- package/src/utils/file-mentions.ts +39 -1
- package/src/utils/image-convert.ts +1 -1
- package/src/utils/image-resize.ts +4 -4
- package/src/web/search/auth.ts +3 -33
- package/src/web/search/index.ts +73 -139
- package/src/web/search/provider.ts +58 -0
- package/src/web/search/providers/anthropic.ts +53 -14
- package/src/web/search/providers/base.ts +22 -0
- package/src/web/search/providers/codex.ts +38 -16
- package/src/web/search/providers/exa.ts +30 -6
- package/src/web/search/providers/gemini.ts +56 -20
- package/src/web/search/providers/jina.ts +28 -5
- package/src/web/search/providers/perplexity.ts +103 -36
- package/src/web/search/render.ts +84 -74
- package/src/web/search/types.ts +285 -59
- package/src/migrations.ts +0 -175
- package/src/session/storage-migration.ts +0 -173
|
@@ -6,12 +6,14 @@
|
|
|
6
6
|
* Returns synthesized answers with web search sources.
|
|
7
7
|
*/
|
|
8
8
|
import * as os from "node:os";
|
|
9
|
-
import {
|
|
9
|
+
import { readSseJson } from "@oh-my-pi/pi-utils";
|
|
10
10
|
import packageJson from "../../../../package.json" with { type: "json" };
|
|
11
11
|
import { getAgentDbPath, getConfigDirPaths } from "../../../config";
|
|
12
12
|
import { AgentStorage } from "../../../session/agent-storage";
|
|
13
|
-
import type {
|
|
14
|
-
import {
|
|
13
|
+
import type { SearchResponse, SearchSource } from "../../../web/search/types";
|
|
14
|
+
import { SearchProviderError } from "../../../web/search/types";
|
|
15
|
+
import type { SearchParams } from "./base";
|
|
16
|
+
import { SearchProvider } from "./base";
|
|
15
17
|
|
|
16
18
|
const CODEX_BASE_URL = "https://chatgpt.com/backend-api";
|
|
17
19
|
const CODEX_RESPONSES_PATH = "/codex/responses";
|
|
@@ -21,6 +23,7 @@ const DEFAULT_INSTRUCTIONS =
|
|
|
21
23
|
"You are a helpful assistant with web search capabilities. Search the web to answer the user's question accurately and cite your sources.";
|
|
22
24
|
|
|
23
25
|
export interface CodexSearchParams {
|
|
26
|
+
signal?: AbortSignal;
|
|
24
27
|
query: string;
|
|
25
28
|
system_prompt?: string;
|
|
26
29
|
num_results?: number;
|
|
@@ -173,15 +176,15 @@ function buildCodexHeaders(accessToken: string, accountId: string): Record<strin
|
|
|
173
176
|
* @param query - Search query from the user
|
|
174
177
|
* @param options - Search options including system prompt and context size
|
|
175
178
|
* @returns Parsed response with answer, sources, and usage
|
|
176
|
-
* @throws {
|
|
179
|
+
* @throws {SearchProviderError} If the API request fails
|
|
177
180
|
*/
|
|
178
|
-
async function
|
|
181
|
+
async function callCodexSearch(
|
|
179
182
|
auth: { accessToken: string; accountId: string },
|
|
180
183
|
query: string,
|
|
181
|
-
options: { systemPrompt?: string; searchContextSize?: "low" | "medium" | "high" },
|
|
184
|
+
options: { signal?: AbortSignal; systemPrompt?: string; searchContextSize?: "low" | "medium" | "high" },
|
|
182
185
|
): Promise<{
|
|
183
186
|
answer: string;
|
|
184
|
-
sources:
|
|
187
|
+
sources: SearchSource[];
|
|
185
188
|
model: string;
|
|
186
189
|
requestId: string;
|
|
187
190
|
usage?: { inputTokens: number; outputTokens: number; totalTokens: number };
|
|
@@ -217,21 +220,21 @@ async function callCodexWebSearch(
|
|
|
217
220
|
|
|
218
221
|
if (!response.ok) {
|
|
219
222
|
const errorText = await response.text();
|
|
220
|
-
throw new
|
|
223
|
+
throw new SearchProviderError("codex", `Codex API error (${response.status}): ${errorText}`, response.status);
|
|
221
224
|
}
|
|
222
225
|
|
|
223
226
|
if (!response.body) {
|
|
224
|
-
throw new
|
|
227
|
+
throw new SearchProviderError("codex", "Codex API returned no response body", 500);
|
|
225
228
|
}
|
|
226
229
|
|
|
227
230
|
// Parse SSE stream
|
|
228
231
|
const answerParts: string[] = [];
|
|
229
|
-
const sources:
|
|
232
|
+
const sources: SearchSource[] = [];
|
|
230
233
|
let model = DEFAULT_MODEL;
|
|
231
234
|
let requestId = "";
|
|
232
235
|
let usage: { inputTokens: number; outputTokens: number; totalTokens: number } | undefined;
|
|
233
236
|
|
|
234
|
-
for await (const rawEvent of
|
|
237
|
+
for await (const rawEvent of readSseJson<Record<string, unknown>>(response.body, options.signal)) {
|
|
235
238
|
const eventType = typeof rawEvent.type === "string" ? rawEvent.type : "";
|
|
236
239
|
if (!eventType) continue;
|
|
237
240
|
|
|
@@ -288,11 +291,11 @@ async function callCodexWebSearch(
|
|
|
288
291
|
} else if (eventType === "error") {
|
|
289
292
|
const code = (rawEvent as { code?: string }).code ?? "";
|
|
290
293
|
const message = (rawEvent as { message?: string }).message ?? "Unknown error";
|
|
291
|
-
throw new
|
|
294
|
+
throw new SearchProviderError("codex", `Codex error (${code}): ${message}`, 500);
|
|
292
295
|
} else if (eventType === "response.failed") {
|
|
293
296
|
const resp = (rawEvent as { response?: { error?: { message?: string } } }).response;
|
|
294
297
|
const errorMessage = resp?.error?.message ?? "Request failed";
|
|
295
|
-
throw new
|
|
298
|
+
throw new SearchProviderError("codex", `Codex request failed: ${errorMessage}`, 500);
|
|
296
299
|
}
|
|
297
300
|
}
|
|
298
301
|
|
|
@@ -312,7 +315,7 @@ async function callCodexWebSearch(
|
|
|
312
315
|
* @returns Search response with synthesized answer, sources, and usage
|
|
313
316
|
* @throws {Error} If no Codex OAuth credentials are configured
|
|
314
317
|
*/
|
|
315
|
-
export async function searchCodex(params: CodexSearchParams): Promise<
|
|
318
|
+
export async function searchCodex(params: CodexSearchParams): Promise<SearchResponse> {
|
|
316
319
|
const auth = await findCodexAuth();
|
|
317
320
|
if (!auth) {
|
|
318
321
|
throw new Error(
|
|
@@ -320,7 +323,7 @@ export async function searchCodex(params: CodexSearchParams): Promise<WebSearchR
|
|
|
320
323
|
);
|
|
321
324
|
}
|
|
322
325
|
|
|
323
|
-
const result = await
|
|
326
|
+
const result = await callCodexSearch(auth, params.query, {
|
|
324
327
|
systemPrompt: params.system_prompt,
|
|
325
328
|
searchContextSize: params.search_context_size ?? "high",
|
|
326
329
|
});
|
|
@@ -352,7 +355,26 @@ export async function searchCodex(params: CodexSearchParams): Promise<WebSearchR
|
|
|
352
355
|
* Checks if Codex web search is available.
|
|
353
356
|
* @returns True if valid OAuth credentials exist for openai-codex
|
|
354
357
|
*/
|
|
355
|
-
export async function
|
|
358
|
+
export async function hasCodexSearch(): Promise<boolean> {
|
|
356
359
|
const auth = await findCodexAuth();
|
|
357
360
|
return auth !== null;
|
|
358
361
|
}
|
|
362
|
+
|
|
363
|
+
/** Search provider for OpenAI Codex web search. */
|
|
364
|
+
export class CodexProvider extends SearchProvider {
|
|
365
|
+
readonly id = "codex";
|
|
366
|
+
readonly label = "Codex";
|
|
367
|
+
|
|
368
|
+
isAvailable(): Promise<boolean> {
|
|
369
|
+
return Promise.resolve(hasCodexSearch());
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
search(params: SearchParams): Promise<SearchResponse> {
|
|
373
|
+
return searchCodex({
|
|
374
|
+
signal: params.signal,
|
|
375
|
+
query: params.query,
|
|
376
|
+
system_prompt: params.systemPrompt,
|
|
377
|
+
num_results: params.numSearchResults ?? params.limit,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
@@ -5,8 +5,11 @@
|
|
|
5
5
|
* Returns structured search results with optional content extraction.
|
|
6
6
|
*/
|
|
7
7
|
import { getEnvApiKey } from "@oh-my-pi/pi-ai";
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
8
|
+
import { findApiKey as findExaKey } from "../../../exa/mcp-client";
|
|
9
|
+
import type { SearchResponse, SearchSource } from "../../../web/search/types";
|
|
10
|
+
import { SearchProviderError } from "../../../web/search/types";
|
|
11
|
+
import type { SearchParams } from "./base";
|
|
12
|
+
import { SearchProvider } from "./base";
|
|
10
13
|
|
|
11
14
|
const EXA_API_URL = "https://api.exa.ai/search";
|
|
12
15
|
|
|
@@ -79,7 +82,7 @@ async function callExaSearch(apiKey: string, params: ExaSearchParams): Promise<E
|
|
|
79
82
|
|
|
80
83
|
if (!response.ok) {
|
|
81
84
|
const errorText = await response.text();
|
|
82
|
-
throw new
|
|
85
|
+
throw new SearchProviderError("exa", `Exa API error (${response.status}): ${errorText}`, response.status);
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
return response.json() as Promise<ExaSearchResponse>;
|
|
@@ -98,7 +101,7 @@ function dateToAgeSeconds(dateStr: string | null | undefined): number | undefine
|
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
/** Execute Exa web search */
|
|
101
|
-
export async function searchExa(params: ExaSearchParams): Promise<
|
|
104
|
+
export async function searchExa(params: ExaSearchParams): Promise<SearchResponse> {
|
|
102
105
|
const apiKey = getEnvApiKey("exa");
|
|
103
106
|
if (!apiKey) {
|
|
104
107
|
throw new Error("EXA_API_KEY not found. Set it in environment or .env file.");
|
|
@@ -106,8 +109,8 @@ export async function searchExa(params: ExaSearchParams): Promise<WebSearchRespo
|
|
|
106
109
|
|
|
107
110
|
const response = await callExaSearch(apiKey, params);
|
|
108
111
|
|
|
109
|
-
// Convert to unified
|
|
110
|
-
const sources:
|
|
112
|
+
// Convert to unified SearchResponse
|
|
113
|
+
const sources: SearchSource[] = [];
|
|
111
114
|
|
|
112
115
|
if (response.results) {
|
|
113
116
|
for (const result of response.results) {
|
|
@@ -132,3 +135,24 @@ export async function searchExa(params: ExaSearchParams): Promise<WebSearchRespo
|
|
|
132
135
|
requestId: response.requestId,
|
|
133
136
|
};
|
|
134
137
|
}
|
|
138
|
+
|
|
139
|
+
/** Search provider for Exa. */
|
|
140
|
+
export class ExaProvider extends SearchProvider {
|
|
141
|
+
readonly id = "exa";
|
|
142
|
+
readonly label = "Exa";
|
|
143
|
+
|
|
144
|
+
isAvailable(): boolean {
|
|
145
|
+
try {
|
|
146
|
+
return !!findExaKey();
|
|
147
|
+
} catch {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
search(params: SearchParams): Promise<SearchResponse> {
|
|
153
|
+
return searchExa({
|
|
154
|
+
query: params.query,
|
|
155
|
+
num_results: params.numSearchResults ?? params.limit,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
import { refreshGoogleCloudToken } from "@oh-my-pi/pi-ai";
|
|
9
9
|
import { getAgentDbPath, getConfigDirPaths } from "../../../config";
|
|
10
10
|
import { AgentStorage } from "../../../session/agent-storage";
|
|
11
|
-
import type {
|
|
12
|
-
import {
|
|
11
|
+
import type { SearchCitation, SearchResponse, SearchSource } from "../../../web/search/types";
|
|
12
|
+
import { SearchProviderError } from "../../../web/search/types";
|
|
13
|
+
import type { SearchParams } from "./base";
|
|
14
|
+
import { SearchProvider } from "./base";
|
|
13
15
|
|
|
14
16
|
const DEFAULT_ENDPOINT = "https://cloudcode-pa.googleapis.com";
|
|
15
17
|
const ANTIGRAVITY_ENDPOINT = "https://daily-cloudcode-pa.sandbox.googleapis.com";
|
|
@@ -41,6 +43,10 @@ export interface GeminiSearchParams {
|
|
|
41
43
|
query: string;
|
|
42
44
|
system_prompt?: string;
|
|
43
45
|
num_results?: number;
|
|
46
|
+
/** Maximum output tokens. */
|
|
47
|
+
max_output_tokens?: number;
|
|
48
|
+
/** Sampling temperature (0–1). Lower = more focused/factual. */
|
|
49
|
+
temperature?: number;
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
/** OAuth credential stored in agent.db */
|
|
@@ -67,7 +73,7 @@ interface GeminiAuth {
|
|
|
67
73
|
* Checks google-antigravity first (daily sandbox, more quota), then google-gemini-cli (prod).
|
|
68
74
|
* @returns OAuth credential with access token and project ID, or null if none found
|
|
69
75
|
*/
|
|
70
|
-
async function findGeminiAuth(): Promise<GeminiAuth | null> {
|
|
76
|
+
export async function findGeminiAuth(): Promise<GeminiAuth | null> {
|
|
71
77
|
const configDirs = getConfigDirPaths("", { project: false });
|
|
72
78
|
const expiryBuffer = 5 * 60 * 1000; // 5 minutes
|
|
73
79
|
const now = Date.now();
|
|
@@ -191,16 +197,18 @@ interface CloudCodeResponseChunk {
|
|
|
191
197
|
* @param query - Search query from the user
|
|
192
198
|
* @param systemPrompt - Optional system prompt
|
|
193
199
|
* @returns Parsed response with answer, sources, and usage
|
|
194
|
-
* @throws {
|
|
200
|
+
* @throws {SearchProviderError} If the API request fails
|
|
195
201
|
*/
|
|
196
202
|
async function callGeminiSearch(
|
|
197
203
|
auth: GeminiAuth,
|
|
198
204
|
query: string,
|
|
199
205
|
systemPrompt?: string,
|
|
206
|
+
maxOutputTokens?: number,
|
|
207
|
+
temperature?: number,
|
|
200
208
|
): Promise<{
|
|
201
209
|
answer: string;
|
|
202
|
-
sources:
|
|
203
|
-
citations:
|
|
210
|
+
sources: SearchSource[];
|
|
211
|
+
citations: SearchCitation[];
|
|
204
212
|
searchQueries: string[];
|
|
205
213
|
model: string;
|
|
206
214
|
usage?: { inputTokens: number; outputTokens: number; totalTokens: number };
|
|
@@ -209,7 +217,7 @@ async function callGeminiSearch(
|
|
|
209
217
|
const url = `${endpoint}/v1internal:streamGenerateContent?alt=sse`;
|
|
210
218
|
const headers = auth.isAntigravity ? ANTIGRAVITY_HEADERS : GEMINI_CLI_HEADERS;
|
|
211
219
|
|
|
212
|
-
const requestBody = {
|
|
220
|
+
const requestBody: Record<string, unknown> = {
|
|
213
221
|
project: auth.projectId,
|
|
214
222
|
model: DEFAULT_MODEL,
|
|
215
223
|
request: {
|
|
@@ -231,6 +239,17 @@ async function callGeminiSearch(
|
|
|
231
239
|
requestId: `search-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,
|
|
232
240
|
};
|
|
233
241
|
|
|
242
|
+
if (maxOutputTokens !== undefined || temperature !== undefined) {
|
|
243
|
+
const generationConfig: Record<string, number> = {};
|
|
244
|
+
if (maxOutputTokens !== undefined) {
|
|
245
|
+
generationConfig.maxOutputTokens = maxOutputTokens;
|
|
246
|
+
}
|
|
247
|
+
if (temperature !== undefined) {
|
|
248
|
+
generationConfig.temperature = temperature;
|
|
249
|
+
}
|
|
250
|
+
(requestBody.request as Record<string, unknown>).generationConfig = generationConfig;
|
|
251
|
+
}
|
|
252
|
+
|
|
234
253
|
const response = await fetch(url, {
|
|
235
254
|
method: "POST",
|
|
236
255
|
headers: {
|
|
@@ -244,7 +263,7 @@ async function callGeminiSearch(
|
|
|
244
263
|
|
|
245
264
|
if (!response.ok) {
|
|
246
265
|
const errorText = await response.text();
|
|
247
|
-
throw new
|
|
266
|
+
throw new SearchProviderError(
|
|
248
267
|
"gemini",
|
|
249
268
|
`Gemini Cloud Code API error (${response.status}): ${errorText}`,
|
|
250
269
|
response.status,
|
|
@@ -252,13 +271,13 @@ async function callGeminiSearch(
|
|
|
252
271
|
}
|
|
253
272
|
|
|
254
273
|
if (!response.body) {
|
|
255
|
-
throw new
|
|
274
|
+
throw new SearchProviderError("gemini", "Gemini API returned no response body", 500);
|
|
256
275
|
}
|
|
257
276
|
|
|
258
277
|
// Parse SSE stream
|
|
259
278
|
const answerParts: string[] = [];
|
|
260
|
-
const sources:
|
|
261
|
-
const citations:
|
|
279
|
+
const sources: SearchSource[] = [];
|
|
280
|
+
const citations: SearchCitation[] = [];
|
|
262
281
|
const searchQueries: string[] = [];
|
|
263
282
|
const seenUrls = new Set<string>();
|
|
264
283
|
let model = DEFAULT_MODEL;
|
|
@@ -388,7 +407,7 @@ async function callGeminiSearch(
|
|
|
388
407
|
* @returns Search response with synthesized answer, sources, and citations
|
|
389
408
|
* @throws {Error} If no Gemini OAuth credentials are configured
|
|
390
409
|
*/
|
|
391
|
-
export async function searchGemini(params: GeminiSearchParams): Promise<
|
|
410
|
+
export async function searchGemini(params: GeminiSearchParams): Promise<SearchResponse> {
|
|
392
411
|
const auth = await findGeminiAuth();
|
|
393
412
|
if (!auth) {
|
|
394
413
|
throw new Error(
|
|
@@ -396,7 +415,13 @@ export async function searchGemini(params: GeminiSearchParams): Promise<WebSearc
|
|
|
396
415
|
);
|
|
397
416
|
}
|
|
398
417
|
|
|
399
|
-
const result = await callGeminiSearch(
|
|
418
|
+
const result = await callGeminiSearch(
|
|
419
|
+
auth,
|
|
420
|
+
params.query,
|
|
421
|
+
params.system_prompt,
|
|
422
|
+
params.max_output_tokens,
|
|
423
|
+
params.temperature,
|
|
424
|
+
);
|
|
400
425
|
|
|
401
426
|
let sources = result.sources;
|
|
402
427
|
|
|
@@ -416,11 +441,22 @@ export async function searchGemini(params: GeminiSearchParams): Promise<WebSearc
|
|
|
416
441
|
};
|
|
417
442
|
}
|
|
418
443
|
|
|
419
|
-
/**
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
444
|
+
/** Search provider for Google Gemini web search. */
|
|
445
|
+
export class GeminiProvider extends SearchProvider {
|
|
446
|
+
readonly id = "gemini";
|
|
447
|
+
readonly label = "Gemini";
|
|
448
|
+
|
|
449
|
+
isAvailable() {
|
|
450
|
+
return findGeminiAuth().then(Boolean);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
search(params: SearchParams): Promise<SearchResponse> {
|
|
454
|
+
return searchGemini({
|
|
455
|
+
query: params.query,
|
|
456
|
+
system_prompt: params.systemPrompt,
|
|
457
|
+
num_results: params.numSearchResults ?? params.limit,
|
|
458
|
+
max_output_tokens: params.maxOutputTokens,
|
|
459
|
+
temperature: params.temperature,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
426
462
|
}
|
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { getEnvApiKey } from "@oh-my-pi/pi-ai";
|
|
9
|
-
import type {
|
|
10
|
-
import {
|
|
9
|
+
import type { SearchResponse, SearchSource } from "../../../web/search/types";
|
|
10
|
+
import { SearchProviderError } from "../../../web/search/types";
|
|
11
|
+
import type { SearchParams } from "./base";
|
|
12
|
+
import { SearchProvider } from "./base";
|
|
11
13
|
|
|
12
14
|
const JINA_SEARCH_URL = "https://s.jina.ai";
|
|
13
15
|
|
|
@@ -41,7 +43,7 @@ async function callJinaSearch(apiKey: string, query: string): Promise<JinaSearch
|
|
|
41
43
|
|
|
42
44
|
if (!response.ok) {
|
|
43
45
|
const errorText = await response.text();
|
|
44
|
-
throw new
|
|
46
|
+
throw new SearchProviderError("jina", `Jina API error (${response.status}): ${errorText}`, response.status);
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
const data = (await response.json()) as unknown;
|
|
@@ -49,14 +51,14 @@ async function callJinaSearch(apiKey: string, query: string): Promise<JinaSearch
|
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
/** Execute Jina web search. */
|
|
52
|
-
export async function searchJina(params: JinaSearchParams): Promise<
|
|
54
|
+
export async function searchJina(params: JinaSearchParams): Promise<SearchResponse> {
|
|
53
55
|
const apiKey = findApiKey();
|
|
54
56
|
if (!apiKey) {
|
|
55
57
|
throw new Error("JINA_API_KEY not found. Set it in environment or .env file.");
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
const response = await callJinaSearch(apiKey, params.query);
|
|
59
|
-
const sources:
|
|
61
|
+
const sources: SearchSource[] = [];
|
|
60
62
|
|
|
61
63
|
for (const result of response) {
|
|
62
64
|
if (!result?.url) continue;
|
|
@@ -74,3 +76,24 @@ export async function searchJina(params: JinaSearchParams): Promise<WebSearchRes
|
|
|
74
76
|
sources: limitedSources,
|
|
75
77
|
};
|
|
76
78
|
}
|
|
79
|
+
|
|
80
|
+
/** Search provider for Jina Reader. */
|
|
81
|
+
export class JinaProvider extends SearchProvider {
|
|
82
|
+
readonly id = "jina";
|
|
83
|
+
readonly label = "Jina";
|
|
84
|
+
|
|
85
|
+
isAvailable() {
|
|
86
|
+
try {
|
|
87
|
+
return !!findApiKey();
|
|
88
|
+
} catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
search(params: SearchParams): Promise<SearchResponse> {
|
|
94
|
+
return searchJina({
|
|
95
|
+
query: params.query,
|
|
96
|
+
num_results: params.numSearchResults ?? params.limit,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -7,21 +7,34 @@
|
|
|
7
7
|
|
|
8
8
|
import { getEnvApiKey } from "@oh-my-pi/pi-ai";
|
|
9
9
|
import type {
|
|
10
|
+
PerplexityMessageOutput,
|
|
10
11
|
PerplexityRequest,
|
|
11
12
|
PerplexityResponse,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
SearchCitation,
|
|
14
|
+
SearchResponse,
|
|
15
|
+
SearchSource,
|
|
15
16
|
} from "../../../web/search/types";
|
|
16
|
-
import {
|
|
17
|
+
import { SearchProviderError } from "../../../web/search/types";
|
|
18
|
+
import type { SearchParams } from "./base";
|
|
19
|
+
import { SearchProvider } from "./base";
|
|
17
20
|
|
|
18
21
|
const PERPLEXITY_API_URL = "https://api.perplexity.ai/chat/completions";
|
|
19
22
|
|
|
23
|
+
const DEFAULT_MAX_TOKENS = 8192;
|
|
24
|
+
const DEFAULT_TEMPERATURE = 0.2;
|
|
25
|
+
const DEFAULT_NUM_SEARCH_RESULTS = 10;
|
|
26
|
+
|
|
20
27
|
export interface PerplexitySearchParams {
|
|
21
28
|
query: string;
|
|
22
29
|
system_prompt?: string;
|
|
23
|
-
search_recency_filter?: "day" | "week" | "month" | "year";
|
|
30
|
+
search_recency_filter?: "hour" | "day" | "week" | "month" | "year";
|
|
24
31
|
num_results?: number;
|
|
32
|
+
/** Maximum output tokens. Defaults to 4096. */
|
|
33
|
+
max_tokens?: number;
|
|
34
|
+
/** Sampling temperature (0–1). Lower = more focused/factual. Defaults to 0.2. */
|
|
35
|
+
temperature?: number;
|
|
36
|
+
/** Number of search results to retrieve. Defaults to 10. */
|
|
37
|
+
num_search_results?: number;
|
|
25
38
|
}
|
|
26
39
|
|
|
27
40
|
/** Find PERPLEXITY_API_KEY from environment or .env files (also checks PPLX_API_KEY) */
|
|
@@ -42,7 +55,7 @@ async function callPerplexity(apiKey: string, request: PerplexityRequest): Promi
|
|
|
42
55
|
|
|
43
56
|
if (!response.ok) {
|
|
44
57
|
const errorText = await response.text();
|
|
45
|
-
throw new
|
|
58
|
+
throw new SearchProviderError(
|
|
46
59
|
"perplexity",
|
|
47
60
|
`Perplexity API error (${response.status}): ${errorText}`,
|
|
48
61
|
response.status,
|
|
@@ -53,7 +66,7 @@ async function callPerplexity(apiKey: string, request: PerplexityRequest): Promi
|
|
|
53
66
|
}
|
|
54
67
|
|
|
55
68
|
/** Calculate age in seconds from ISO date string */
|
|
56
|
-
function dateToAgeSeconds(dateStr: string | undefined): number | undefined {
|
|
69
|
+
function dateToAgeSeconds(dateStr: string | null | undefined): number | undefined {
|
|
57
70
|
if (!dateStr) return undefined;
|
|
58
71
|
try {
|
|
59
72
|
const date = new Date(dateStr);
|
|
@@ -64,30 +77,49 @@ function dateToAgeSeconds(dateStr: string | undefined): number | undefined {
|
|
|
64
77
|
}
|
|
65
78
|
}
|
|
66
79
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
80
|
+
function messageContentToText(content: PerplexityMessageOutput["content"]): string {
|
|
81
|
+
if (!content) return "";
|
|
82
|
+
if (typeof content === "string") return content;
|
|
83
|
+
return content.map(chunk => (chunk.type === "text" ? chunk.text : "")).join("");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Parse API response into unified SearchResponse */
|
|
87
|
+
function parseResponse(response: PerplexityResponse): SearchResponse {
|
|
88
|
+
const messageContent = response.choices[0]?.message?.content ?? null;
|
|
89
|
+
const answer = messageContentToText(messageContent);
|
|
70
90
|
|
|
71
91
|
// Build sources by matching citations to search_results
|
|
72
|
-
const sources:
|
|
73
|
-
const citations:
|
|
92
|
+
const sources: SearchSource[] = [];
|
|
93
|
+
const citations: SearchCitation[] = [];
|
|
74
94
|
|
|
75
95
|
const citationUrls = response.citations ?? [];
|
|
76
96
|
const searchResults = response.search_results ?? [];
|
|
77
97
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
98
|
+
if (citationUrls.length > 0) {
|
|
99
|
+
for (const url of citationUrls) {
|
|
100
|
+
const searchResult = searchResults.find(r => r.url === url);
|
|
101
|
+
sources.push({
|
|
102
|
+
title: searchResult?.title ?? url,
|
|
103
|
+
url,
|
|
104
|
+
snippet: searchResult?.snippet,
|
|
105
|
+
publishedDate: searchResult?.date ?? undefined,
|
|
106
|
+
ageSeconds: dateToAgeSeconds(searchResult?.date),
|
|
107
|
+
});
|
|
108
|
+
citations.push({
|
|
109
|
+
url,
|
|
110
|
+
title: searchResult?.title ?? url,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
for (const searchResult of searchResults) {
|
|
115
|
+
sources.push({
|
|
116
|
+
title: searchResult.title ?? searchResult.url,
|
|
117
|
+
url: searchResult.url,
|
|
118
|
+
snippet: searchResult.snippet,
|
|
119
|
+
publishedDate: searchResult.date ?? undefined,
|
|
120
|
+
ageSeconds: dateToAgeSeconds(searchResult.date),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
91
123
|
}
|
|
92
124
|
|
|
93
125
|
return {
|
|
@@ -95,37 +127,46 @@ function parseResponse(response: PerplexityResponse): WebSearchResponse {
|
|
|
95
127
|
answer: answer || undefined,
|
|
96
128
|
sources,
|
|
97
129
|
citations: citations.length > 0 ? citations : undefined,
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
130
|
+
usage: response.usage
|
|
131
|
+
? {
|
|
132
|
+
inputTokens: response.usage.prompt_tokens,
|
|
133
|
+
outputTokens: response.usage.completion_tokens,
|
|
134
|
+
totalTokens: response.usage.total_tokens,
|
|
135
|
+
}
|
|
136
|
+
: undefined,
|
|
104
137
|
model: response.model,
|
|
105
138
|
requestId: response.id,
|
|
106
139
|
};
|
|
107
140
|
}
|
|
108
141
|
|
|
109
142
|
/** Execute Perplexity web search */
|
|
110
|
-
export async function searchPerplexity(params: PerplexitySearchParams): Promise<
|
|
143
|
+
export async function searchPerplexity(params: PerplexitySearchParams): Promise<SearchResponse> {
|
|
111
144
|
const apiKey = findApiKey();
|
|
112
145
|
if (!apiKey) {
|
|
113
146
|
throw new Error("PERPLEXITY_API_KEY not found. Set it in environment or .env file.");
|
|
114
147
|
}
|
|
115
148
|
|
|
149
|
+
const systemPrompt = params.system_prompt;
|
|
116
150
|
const messages: PerplexityRequest["messages"] = [];
|
|
117
|
-
if (
|
|
118
|
-
messages.push({ role: "system", content:
|
|
151
|
+
if (systemPrompt) {
|
|
152
|
+
messages.push({ role: "system", content: systemPrompt });
|
|
119
153
|
}
|
|
120
154
|
messages.push({ role: "user", content: params.query });
|
|
121
155
|
|
|
122
156
|
const request: PerplexityRequest = {
|
|
123
157
|
model: "sonar-pro",
|
|
124
158
|
messages,
|
|
125
|
-
|
|
159
|
+
max_tokens: params.max_tokens ?? DEFAULT_MAX_TOKENS,
|
|
160
|
+
temperature: params.temperature ?? DEFAULT_TEMPERATURE,
|
|
161
|
+
search_mode: "web",
|
|
162
|
+
num_search_results: params.num_search_results ?? DEFAULT_NUM_SEARCH_RESULTS,
|
|
126
163
|
web_search_options: {
|
|
127
|
-
|
|
164
|
+
search_type: "pro",
|
|
165
|
+
search_context_size: "medium",
|
|
128
166
|
},
|
|
167
|
+
enable_search_classifier: true,
|
|
168
|
+
reasoning_effort: "medium",
|
|
169
|
+
language_preference: "en",
|
|
129
170
|
};
|
|
130
171
|
|
|
131
172
|
if (params.search_recency_filter) {
|
|
@@ -142,3 +183,29 @@ export async function searchPerplexity(params: PerplexitySearchParams): Promise<
|
|
|
142
183
|
|
|
143
184
|
return result;
|
|
144
185
|
}
|
|
186
|
+
|
|
187
|
+
/** Search provider for Perplexity. */
|
|
188
|
+
export class PerplexityProvider extends SearchProvider {
|
|
189
|
+
readonly id = "perplexity";
|
|
190
|
+
readonly label = "Perplexity";
|
|
191
|
+
|
|
192
|
+
isAvailable() {
|
|
193
|
+
try {
|
|
194
|
+
return !!findApiKey();
|
|
195
|
+
} catch {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
search(params: SearchParams): Promise<SearchResponse> {
|
|
201
|
+
return searchPerplexity({
|
|
202
|
+
query: params.query,
|
|
203
|
+
temperature: params.temperature,
|
|
204
|
+
max_tokens: params.maxOutputTokens,
|
|
205
|
+
num_search_results: params.numSearchResults,
|
|
206
|
+
system_prompt: params.systemPrompt,
|
|
207
|
+
search_recency_filter: params.recency,
|
|
208
|
+
num_results: params.limit,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|