pi-web-search 1.0.2 → 1.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/README.md +78 -6
- package/package.json +23 -9
- package/src/api.ts +848 -108
- package/src/index.ts +69 -7
- package/src/url_context.ts +49 -8
- package/src/utils.ts +49 -49
- package/src/web_search.ts +42 -12
- package/dist/api.d.ts +0 -26
- package/dist/api.js +0 -182
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -18
- package/dist/url_context.d.ts +0 -8
- package/dist/url_context.js +0 -102
- package/dist/utils.d.ts +0 -6
- package/dist/utils.js +0 -63
- package/dist/web_search.d.ts +0 -8
- package/dist/web_search.js +0 -75
- package/tsconfig.json +0 -14
package/src/index.ts
CHANGED
|
@@ -1,21 +1,83 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type Model } from "@earendil-works/pi-ai";
|
|
3
|
+
import { getProviderKind } from "./api.ts";
|
|
4
|
+
import { webSearch, WebSearchSchema } from "./web_search.ts";
|
|
5
|
+
import { urlContext, UrlContextSchema } from "./url_context.ts";
|
|
6
|
+
|
|
7
|
+
const WEB_SEARCH_TOOL = "web_search";
|
|
8
|
+
const URL_CONTEXT_TOOL = "url_context";
|
|
9
|
+
|
|
10
|
+
function supportsUrlContext(model: Model<any> | undefined) {
|
|
11
|
+
return !!model && getProviderKind(model) === "google";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function setEquals<T>(a: Set<T>, b: Set<T>) {
|
|
15
|
+
if (a.size !== b.size) return false;
|
|
16
|
+
for (const value of a) {
|
|
17
|
+
if (!b.has(value)) return false;
|
|
18
|
+
}
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createModelScopedToolManager(pi: Pick<ExtensionAPI, "getActiveTools" | "setActiveTools">) {
|
|
23
|
+
let preferredActiveTools: Set<string> | undefined;
|
|
24
|
+
let lastAppliedActiveTools: Set<string> | undefined;
|
|
25
|
+
let suppressedTools = new Set<string>();
|
|
26
|
+
|
|
27
|
+
const sync = (model: Model<any> | undefined) => {
|
|
28
|
+
const currentActiveTools = new Set(pi.getActiveTools());
|
|
29
|
+
|
|
30
|
+
if (!preferredActiveTools) {
|
|
31
|
+
preferredActiveTools = new Set(currentActiveTools);
|
|
32
|
+
} else if (lastAppliedActiveTools) {
|
|
33
|
+
for (const tool of currentActiveTools) {
|
|
34
|
+
if (!lastAppliedActiveTools.has(tool)) preferredActiveTools.add(tool);
|
|
35
|
+
}
|
|
36
|
+
for (const tool of lastAppliedActiveTools) {
|
|
37
|
+
if (!currentActiveTools.has(tool) && !suppressedTools.has(tool)) {
|
|
38
|
+
preferredActiveTools.delete(tool);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const desiredActiveTools = new Set(preferredActiveTools);
|
|
44
|
+
suppressedTools = new Set<string>();
|
|
45
|
+
if (!supportsUrlContext(model)) {
|
|
46
|
+
desiredActiveTools.delete(URL_CONTEXT_TOOL);
|
|
47
|
+
if (preferredActiveTools.has(URL_CONTEXT_TOOL)) {
|
|
48
|
+
suppressedTools.add(URL_CONTEXT_TOOL);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!setEquals(currentActiveTools, desiredActiveTools)) {
|
|
53
|
+
pi.setActiveTools(Array.from(desiredActiveTools));
|
|
54
|
+
}
|
|
55
|
+
lastAppliedActiveTools = new Set(desiredActiveTools);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return { sync };
|
|
59
|
+
}
|
|
4
60
|
|
|
5
61
|
export default function (pi: ExtensionAPI) {
|
|
6
62
|
pi.registerTool({
|
|
7
|
-
name:
|
|
63
|
+
name: WEB_SEARCH_TOOL,
|
|
8
64
|
label: "Web Search",
|
|
9
|
-
description: "Search the web using Google Gemini
|
|
65
|
+
description: "Search the web using the current supported provider (Google Gemini, OpenAI, or Anthropic). Optionally include URLs to analyze alongside search results.",
|
|
10
66
|
parameters: WebSearchSchema,
|
|
11
67
|
execute: webSearch
|
|
12
68
|
});
|
|
13
69
|
|
|
14
70
|
pi.registerTool({
|
|
15
|
-
name:
|
|
71
|
+
name: URL_CONTEXT_TOOL,
|
|
16
72
|
label: "URL Context",
|
|
17
|
-
description: "Analyze the content of up to 20 public URLs. Supports web pages, documents, images, and YouTube videos.",
|
|
73
|
+
description: "Analyze the content of up to 20 public URLs using Gemini URL Context. Supports web pages, documents, images, and YouTube videos.",
|
|
18
74
|
parameters: UrlContextSchema,
|
|
19
75
|
execute: urlContext
|
|
20
76
|
});
|
|
77
|
+
|
|
78
|
+
const toolManager = createModelScopedToolManager(pi);
|
|
79
|
+
pi.on("session_start", (_event, ctx) => toolManager.sync(ctx.model));
|
|
80
|
+
pi.on("session_switch", (_event, ctx) => toolManager.sync(ctx.model));
|
|
81
|
+
pi.on("session_tree", (_event, ctx) => toolManager.sync(ctx.model));
|
|
82
|
+
pi.on("model_select", (event) => toolManager.sync(event.model));
|
|
21
83
|
}
|
package/src/url_context.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { ExtensionContext, AgentToolUpdateCallback } from "@
|
|
1
|
+
import type { ExtensionContext, AgentToolUpdateCallback } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Type, type Static } from "@sinclair/typebox";
|
|
3
|
-
import { callApiStream, getConfig, applyCitations } from "./api.
|
|
4
|
-
import { getModel, missingConfigResult, errorResult, formatResult } from "./utils.
|
|
3
|
+
import { callApiStream, getConfig, applyCitations } from "./api.ts";
|
|
4
|
+
import { getModel, missingConfigResult, errorResult, formatResult } from "./utils.ts";
|
|
5
5
|
|
|
6
6
|
export const UrlContextSchema = Type.Object({
|
|
7
7
|
query: Type.String({ description: "Question or task to perform on the URLs" }),
|
|
@@ -30,9 +30,21 @@ export async function urlContext(
|
|
|
30
30
|
|
|
31
31
|
try {
|
|
32
32
|
const config = getConfig(model);
|
|
33
|
+
if (config.kind !== "google") {
|
|
34
|
+
return formatResult(
|
|
35
|
+
`url_context currently requires a Google Gemini-compatible model. Current model: ${model.id} (${model.provider}/${model.api}).\n\nUse web_search for cross-provider web search, or switch to Gemini for provider-native URL context retrieval.`,
|
|
36
|
+
{
|
|
37
|
+
error: "unsupported_provider",
|
|
38
|
+
providerKind: config.kind,
|
|
39
|
+
model: model.id,
|
|
40
|
+
supportedProviders: ["google", "google-generative-ai"],
|
|
41
|
+
grounded: false,
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
}
|
|
33
45
|
|
|
34
46
|
let contents: any[] = [];
|
|
35
|
-
let tools: any[] | undefined = [{ [config.urlContextTool]: {} }];
|
|
47
|
+
let tools: any[] | undefined = [{ [config.urlContextTool!]: {} }];
|
|
36
48
|
|
|
37
49
|
// Special handling for YouTube videos on Gemini
|
|
38
50
|
if (model.api === "google-generative-ai") {
|
|
@@ -83,10 +95,13 @@ export async function urlContext(
|
|
|
83
95
|
|
|
84
96
|
const result = await callApiStream(ctx, model, {
|
|
85
97
|
contents,
|
|
86
|
-
tools
|
|
87
|
-
}, onUpdate);
|
|
98
|
+
...(tools ? { tools } : {})
|
|
99
|
+
}, onUpdate, signal);
|
|
88
100
|
|
|
89
|
-
const
|
|
101
|
+
const cited = applyCitations(result.text, result.groundingMetadata);
|
|
102
|
+
const text = cited.text;
|
|
103
|
+
const sources = result.sources?.length ? result.sources : cited.sources;
|
|
104
|
+
const extraSearchResults = (result.searchResults || []).filter((item) => item.url && !sources.some((source) => source.url === item.url));
|
|
90
105
|
|
|
91
106
|
// Handle both camelCase and snake_case metadata
|
|
92
107
|
const urlMeta = result.urlContextMetadata?.urlMetadata
|
|
@@ -108,14 +123,40 @@ export async function urlContext(
|
|
|
108
123
|
summary += `\n\n## URL Status\n✅ Retrieved: ${retrieved.length}\n❌ Failed: ${failed.length}`;
|
|
109
124
|
failed.forEach((f: any) => { summary += `\n- ${f.url}: ${f.status}`; });
|
|
110
125
|
}
|
|
126
|
+
const hasUrlContextMetadata = urlMeta.length > 0 || retrieved.length > 0 || sources.length > 0 || extraSearchResults.length > 0;
|
|
127
|
+
if (!hasUrlContextMetadata) {
|
|
128
|
+
summary += `\n\n## URL Context Verification\n⚠️ No verified URL context metadata was returned by provider ${result.providerKind || "unknown"}. Treat the answer as ungrounded unless sources, retrieved URLs, or searchResults are present in tool details.`;
|
|
129
|
+
}
|
|
111
130
|
if (sources.length > 0 && !summary.includes("## Sources")) {
|
|
112
131
|
summary += `\n\n## Sources\n${sources.map((s, i) => `${i + 1}. [${s.title}](${s.url})`).join("\n")}`;
|
|
113
132
|
}
|
|
133
|
+
if (extraSearchResults.length) {
|
|
134
|
+
const visibleResults = extraSearchResults.slice(0, 8);
|
|
135
|
+
summary += `\n\n## Additional Search Results\n${visibleResults.map((r, i) => {
|
|
136
|
+
const label = r.title || r.url || `Result ${i + 1}`;
|
|
137
|
+
const url = r.url ? ` - ${r.url}` : "";
|
|
138
|
+
const meta = [r.source, r.type, r.status, r.query ? `query=${r.query}` : undefined].filter(Boolean).join(", ");
|
|
139
|
+
return `${i + 1}. ${label}${url}${meta ? ` (${meta})` : ""}`;
|
|
140
|
+
}).join("\n")}`;
|
|
141
|
+
if (extraSearchResults.length > visibleResults.length) {
|
|
142
|
+
summary += `\n... and ${extraSearchResults.length - visibleResults.length} more results in tool details.`;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
114
145
|
|
|
115
146
|
return formatResult(summary, {
|
|
147
|
+
sources,
|
|
148
|
+
providerKind: result.providerKind,
|
|
149
|
+
nativeSearchUsed: result.nativeSearchUsed,
|
|
150
|
+
nativeSearchEvents: result.nativeSearchEvents,
|
|
151
|
+
nativeSearchCalls: result.nativeSearchCalls,
|
|
152
|
+
searchQueries: result.searchQueries,
|
|
153
|
+
searchResults: result.searchResults,
|
|
154
|
+
citations: result.citations,
|
|
116
155
|
retrieved,
|
|
117
156
|
failed: failed.length > 0 ? failed : undefined,
|
|
118
|
-
model: model.id
|
|
157
|
+
model: model.id,
|
|
158
|
+
grounded: sources.length > 0 || (result.searchResults?.length || 0) > 0,
|
|
159
|
+
resultCount: result.searchResults?.length || sources.length
|
|
119
160
|
});
|
|
120
161
|
} catch (e: any) {
|
|
121
162
|
return errorResult(e);
|
package/src/utils.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { ExtensionContext, AgentToolResult } from "@
|
|
2
|
-
import { type Model } from "@
|
|
3
|
-
import { truncateHead, DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES } from "@
|
|
1
|
+
import type { ExtensionContext, AgentToolResult } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type Model } from "@earendil-works/pi-ai";
|
|
3
|
+
import { truncateHead, DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { getProviderKind } from "./api.ts";
|
|
4
5
|
|
|
5
6
|
// --- Formatting ---
|
|
6
7
|
|
|
@@ -14,63 +15,62 @@ export function formatResult(text: string, details: any): AgentToolResult<any> {
|
|
|
14
15
|
|
|
15
16
|
// --- Model Selection ---
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
function isSupportedSearchModel(model: Model<any> | undefined): model is Model<any> {
|
|
19
|
+
if (!model) return false;
|
|
20
|
+
return getProviderKind(model) !== "unsupported";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function providerPriority(model: Model<any>): number {
|
|
24
|
+
const priorities = [
|
|
25
|
+
"google",
|
|
26
|
+
"google-generative-ai",
|
|
27
|
+
"openai",
|
|
28
|
+
"anthropic",
|
|
29
|
+
];
|
|
30
|
+
const providerIndex = priorities.indexOf(model.provider);
|
|
31
|
+
if (providerIndex >= 0) return providerIndex;
|
|
32
|
+
return priorities.length;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function modelPriority(model: Model<any>): number {
|
|
36
|
+
const id = model.id;
|
|
37
|
+
const patterns = [
|
|
23
38
|
/gemini-3.*flash/i,
|
|
24
39
|
/gemini-2\.5.*flash/i,
|
|
25
40
|
/gemini-2\.0.*flash/i,
|
|
26
41
|
/gemini.*flash/i,
|
|
42
|
+
/gpt-5\..*mini/i,
|
|
43
|
+
/gpt-4\.1.*mini/i,
|
|
44
|
+
/gpt-4o-mini/i,
|
|
45
|
+
/claude.*haiku/i,
|
|
46
|
+
/claude.*sonnet/i,
|
|
27
47
|
];
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const googleModels = models.filter(m =>
|
|
38
|
-
providers.includes(m.provider) ||
|
|
39
|
-
m.api === "google-generative-ai" ||
|
|
40
|
-
m.api === "google-gemini-cli"
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
// Try each flash pattern in priority order
|
|
44
|
-
for (const pattern of flashModels) {
|
|
45
|
-
const matching = googleModels.filter(m => pattern.test(m.id));
|
|
46
|
-
if (matching.length === 0) continue;
|
|
47
|
-
|
|
48
|
-
// Among matches, pick by provider priority
|
|
49
|
-
for (const provider of providers) {
|
|
50
|
-
const model = matching.find(m => m.provider === provider);
|
|
51
|
-
if (model) return model;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Fall back to first match if no priority provider found
|
|
55
|
-
return matching[0];
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// No flash model found, try any Google model by provider priority
|
|
59
|
-
for (const provider of providers) {
|
|
60
|
-
const model = googleModels.find(m => m.provider === provider);
|
|
61
|
-
if (model) return model;
|
|
48
|
+
const patternIndex = patterns.findIndex((pattern) => pattern.test(id));
|
|
49
|
+
return patternIndex >= 0 ? patternIndex : patterns.length;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function getModel(ctx: ExtensionContext): Promise<Model<any> | undefined> {
|
|
53
|
+
// Use the current model first so the tool follows the user's selected provider.
|
|
54
|
+
// This lets pi --provider openai/anthropic/google automatically pick the matching API.
|
|
55
|
+
if (isSupportedSearchModel(ctx.model)) {
|
|
56
|
+
return ctx.model;
|
|
62
57
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return
|
|
58
|
+
|
|
59
|
+
const models = ctx.modelRegistry.getAvailable().filter(isSupportedSearchModel);
|
|
60
|
+
if (models.length === 0) return undefined;
|
|
61
|
+
|
|
62
|
+
return models.sort((a, b) => {
|
|
63
|
+
const byProvider = providerPriority(a) - providerPriority(b);
|
|
64
|
+
if (byProvider !== 0) return byProvider;
|
|
65
|
+
return modelPriority(a) - modelPriority(b);
|
|
66
|
+
})[0];
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
// --- Error Results ---
|
|
69
70
|
|
|
70
71
|
export function missingConfigResult(ctx: ExtensionContext): AgentToolResult<any> {
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
: "No Google Gemini configuration found. Please configure GEMINI_API_KEY.";
|
|
72
|
+
const current = ctx.model ? `${ctx.model.provider} (${ctx.model.api})` : "none";
|
|
73
|
+
const msg = `No supported web-search model configuration found. Current model: ${current}. Configure or select a supported provider: google-generative-ai, openai, or anthropic.`;
|
|
74
74
|
return { content: [{ type: "text", text: `Failed: ${msg}` }], details: { error: "missing_config" } };
|
|
75
75
|
}
|
|
76
76
|
|
package/src/web_search.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { ExtensionContext, AgentToolUpdateCallback } from "@
|
|
1
|
+
import type { ExtensionContext, AgentToolUpdateCallback } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Type, type Static } from "@sinclair/typebox";
|
|
3
|
-
import { callApiStream, getConfig, applyCitations } from "./api.
|
|
4
|
-
import { getModel, missingConfigResult, errorResult, formatResult } from "./utils.
|
|
3
|
+
import { callApiStream, getConfig, applyCitations } from "./api.ts";
|
|
4
|
+
import { getModel, missingConfigResult, errorResult, formatResult } from "./utils.ts";
|
|
5
5
|
|
|
6
6
|
export const WebSearchSchema = Type.Object({
|
|
7
7
|
query: Type.String({ description: "The search query or question to answer" }),
|
|
@@ -43,17 +43,23 @@ export async function webSearch(
|
|
|
43
43
|
? `${params.query}\n\nAlso analyze these URLs:\n${params.urls!.join("\n")}`
|
|
44
44
|
: params.query;
|
|
45
45
|
|
|
46
|
-
// Enable
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
// Enable provider-native search tools. Google needs explicit Gemini tool names;
|
|
47
|
+
// OpenAI/Anthropic are handled inside callApiStream based on the current model.
|
|
48
|
+
const tools = config.kind === "google"
|
|
49
|
+
? (hasUrls
|
|
50
|
+
? [{ [config.searchTool!]: {} }, { [config.urlContextTool!]: {} }]
|
|
51
|
+
: [{ [config.searchTool!]: {} }])
|
|
52
|
+
: undefined;
|
|
50
53
|
|
|
51
54
|
const result = await callApiStream(ctx, model, {
|
|
52
55
|
contents: [{ role: "user", parts: [{ text: prompt }] }],
|
|
53
|
-
tools
|
|
54
|
-
}, onUpdate);
|
|
56
|
+
...(tools ? { tools } : {})
|
|
57
|
+
}, onUpdate, signal);
|
|
55
58
|
|
|
56
|
-
const
|
|
59
|
+
const cited = applyCitations(result.text, result.groundingMetadata);
|
|
60
|
+
const text = cited.text;
|
|
61
|
+
const sources = result.sources?.length ? result.sources : cited.sources;
|
|
62
|
+
const extraSearchResults = (result.searchResults || []).filter((item) => item.url && !sources.some((source) => source.url === item.url));
|
|
57
63
|
|
|
58
64
|
// Handle URL context metadata
|
|
59
65
|
const urlMeta = result.urlContextMetadata?.urlMetadata
|
|
@@ -78,18 +84,42 @@ export async function webSearch(
|
|
|
78
84
|
failed.forEach((f: any) => { summary += `\n- ${f.url}: ${f.status}`; });
|
|
79
85
|
}
|
|
80
86
|
|
|
87
|
+
if (result.nativeSearchUsed === false) {
|
|
88
|
+
summary += `\n\n## Search Verification\n⚠️ No verified native search metadata was returned by provider ${result.providerKind || "unknown"}. Treat the answer as ungrounded unless sources/searchResults are present in tool details.`;
|
|
89
|
+
}
|
|
90
|
+
|
|
81
91
|
// Add sources
|
|
82
92
|
if (sources.length > 0) {
|
|
83
93
|
summary += `\n\n## Sources\n${sources.map((s, i) => `${i + 1}. [${s.title}](${s.url})`).join("\n")}`;
|
|
84
94
|
}
|
|
85
95
|
|
|
96
|
+
if (extraSearchResults.length) {
|
|
97
|
+
const visibleResults = extraSearchResults.slice(0, 8);
|
|
98
|
+
summary += `\n\n## Additional Search Results\n${visibleResults.map((r, i) => {
|
|
99
|
+
const label = r.title || r.url || `Result ${i + 1}`;
|
|
100
|
+
const url = r.url ? ` - ${r.url}` : "";
|
|
101
|
+
const meta = [r.source, r.type, r.status, r.query ? `query=${r.query}` : undefined].filter(Boolean).join(", ");
|
|
102
|
+
return `${i + 1}. ${label}${url}${meta ? ` (${meta})` : ""}`;
|
|
103
|
+
}).join("\n")}`;
|
|
104
|
+
if (extraSearchResults.length > visibleResults.length) {
|
|
105
|
+
summary += `\n... and ${extraSearchResults.length - visibleResults.length} more results in tool details.`;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
86
109
|
return formatResult(summary, {
|
|
87
110
|
sources,
|
|
88
|
-
|
|
111
|
+
providerKind: result.providerKind,
|
|
112
|
+
nativeSearchUsed: result.nativeSearchUsed,
|
|
113
|
+
nativeSearchEvents: result.nativeSearchEvents,
|
|
114
|
+
nativeSearchCalls: result.nativeSearchCalls,
|
|
115
|
+
searchQueries: result.searchQueries || result.groundingMetadata?.webSearchQueries,
|
|
116
|
+
searchResults: result.searchResults,
|
|
117
|
+
citations: result.citations,
|
|
89
118
|
retrieved: retrieved.length > 0 ? retrieved : undefined,
|
|
90
119
|
failed: failed.length > 0 ? failed : undefined,
|
|
91
120
|
model: model.id,
|
|
92
|
-
grounded: sources.length > 0
|
|
121
|
+
grounded: sources.length > 0 || (result.searchResults?.length || 0) > 0,
|
|
122
|
+
resultCount: result.searchResults?.length || sources.length
|
|
93
123
|
});
|
|
94
124
|
} catch (e: any) {
|
|
95
125
|
return errorResult(e);
|
package/dist/api.d.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import type { ExtensionContext, AgentToolUpdateCallback } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import { type Model } from "@mariozechner/pi-ai";
|
|
3
|
-
type ProviderConfig = {
|
|
4
|
-
searchTool: string;
|
|
5
|
-
urlContextTool: string;
|
|
6
|
-
buildRequest: (model: Model<any>, body: any, projectId?: string) => {
|
|
7
|
-
url: string;
|
|
8
|
-
headers: Record<string, string>;
|
|
9
|
-
body: any;
|
|
10
|
-
};
|
|
11
|
-
};
|
|
12
|
-
export declare function getConfig(model: Model<any>): ProviderConfig;
|
|
13
|
-
export interface StreamResult {
|
|
14
|
-
text: string;
|
|
15
|
-
groundingMetadata?: any;
|
|
16
|
-
urlContextMetadata?: any;
|
|
17
|
-
}
|
|
18
|
-
export declare function callApiStream(ctx: ExtensionContext, model: Model<any>, body: any, onUpdate?: AgentToolUpdateCallback): Promise<StreamResult>;
|
|
19
|
-
export declare function applyCitations(text: string, groundingMetadata: any): {
|
|
20
|
-
text: string;
|
|
21
|
-
sources: {
|
|
22
|
-
title: string;
|
|
23
|
-
url: string;
|
|
24
|
-
}[];
|
|
25
|
-
};
|
|
26
|
-
export {};
|
package/dist/api.js
DELETED
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
import { TextEncoder, TextDecoder } from "util";
|
|
2
|
-
const PROVIDERS = {
|
|
3
|
-
"google-generative-ai": {
|
|
4
|
-
searchTool: "google_search",
|
|
5
|
-
urlContextTool: "url_context",
|
|
6
|
-
buildRequest: (model, body) => ({
|
|
7
|
-
url: `${model.baseUrl}/models/${model.id}:streamGenerateContent?alt=sse`,
|
|
8
|
-
headers: {
|
|
9
|
-
"Content-Type": "application/json",
|
|
10
|
-
"Accept": "text/event-stream",
|
|
11
|
-
},
|
|
12
|
-
body
|
|
13
|
-
})
|
|
14
|
-
},
|
|
15
|
-
"google-gemini-cli": {
|
|
16
|
-
searchTool: "googleSearch",
|
|
17
|
-
urlContextTool: "urlContext",
|
|
18
|
-
buildRequest: (model, body, projectId) => ({
|
|
19
|
-
url: `${model.baseUrl}/v1internal:streamGenerateContent?alt=sse`,
|
|
20
|
-
headers: {
|
|
21
|
-
"Content-Type": "application/json",
|
|
22
|
-
"Accept": "text/event-stream",
|
|
23
|
-
"User-Agent": "google-cloud-sdk vscode_cloudshelleditor/0.1",
|
|
24
|
-
"X-Goog-Api-Client": "gl-node/22.17.0",
|
|
25
|
-
"Client-Metadata": JSON.stringify({ ideType: "IDE_UNSPECIFIED", platform: "PLATFORM_UNSPECIFIED", pluginType: "GEMINI" }),
|
|
26
|
-
},
|
|
27
|
-
body: { project: projectId, model: model.id, request: body }
|
|
28
|
-
})
|
|
29
|
-
},
|
|
30
|
-
"google-antigravity": {
|
|
31
|
-
searchTool: "googleSearch",
|
|
32
|
-
urlContextTool: "urlContext",
|
|
33
|
-
buildRequest: (model, body, projectId) => ({
|
|
34
|
-
url: `${model.baseUrl}/v1internal:streamGenerateContent?alt=sse`,
|
|
35
|
-
headers: {
|
|
36
|
-
"Content-Type": "application/json",
|
|
37
|
-
"Accept": "text/event-stream",
|
|
38
|
-
"User-Agent": "antigravity/1.15.8 darwin/arm64",
|
|
39
|
-
"X-Goog-Api-Client": "gl-node/22.17.0",
|
|
40
|
-
"Client-Metadata": JSON.stringify({ ideType: "IDE_UNSPECIFIED", platform: "PLATFORM_UNSPECIFIED", pluginType: "GEMINI" }),
|
|
41
|
-
},
|
|
42
|
-
body: {
|
|
43
|
-
project: projectId,
|
|
44
|
-
model: model.id,
|
|
45
|
-
request: body,
|
|
46
|
-
requestType: "agent",
|
|
47
|
-
userAgent: "antigravity",
|
|
48
|
-
requestId: `agent-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,
|
|
49
|
-
}
|
|
50
|
-
})
|
|
51
|
-
}
|
|
52
|
-
};
|
|
53
|
-
export function getConfig(model) {
|
|
54
|
-
return PROVIDERS[model.provider] || PROVIDERS[model.api] || PROVIDERS["google-generative-ai"];
|
|
55
|
-
}
|
|
56
|
-
export async function callApiStream(ctx, model, body, onUpdate) {
|
|
57
|
-
const config = getConfig(model);
|
|
58
|
-
const apiKey = await ctx.modelRegistry.getApiKey(model) || "";
|
|
59
|
-
let projectId;
|
|
60
|
-
if (model.api !== "google-generative-ai") {
|
|
61
|
-
const parsed = JSON.parse(apiKey);
|
|
62
|
-
projectId = parsed.projectId;
|
|
63
|
-
}
|
|
64
|
-
const req = config.buildRequest(model, body, projectId);
|
|
65
|
-
// Handle auth
|
|
66
|
-
if (model.api === "google-generative-ai") {
|
|
67
|
-
req.headers["x-goog-api-key"] = apiKey;
|
|
68
|
-
}
|
|
69
|
-
else {
|
|
70
|
-
const parsed = JSON.parse(apiKey);
|
|
71
|
-
req.headers["Authorization"] = `Bearer ${parsed.token}`;
|
|
72
|
-
}
|
|
73
|
-
const response = await fetch(req.url, {
|
|
74
|
-
method: "POST",
|
|
75
|
-
headers: req.headers,
|
|
76
|
-
body: JSON.stringify(req.body)
|
|
77
|
-
});
|
|
78
|
-
if (!response.ok) {
|
|
79
|
-
throw new Error(`API error (${response.status}): ${await response.text()}`);
|
|
80
|
-
}
|
|
81
|
-
if (!response.body) {
|
|
82
|
-
throw new Error("No response body");
|
|
83
|
-
}
|
|
84
|
-
// Parse SSE stream
|
|
85
|
-
const reader = response.body.getReader();
|
|
86
|
-
const decoder = new TextDecoder();
|
|
87
|
-
let buffer = "";
|
|
88
|
-
let accumulatedText = "";
|
|
89
|
-
let groundingMetadata;
|
|
90
|
-
let urlContextMetadata;
|
|
91
|
-
while (true) {
|
|
92
|
-
const { done, value } = await reader.read();
|
|
93
|
-
if (done)
|
|
94
|
-
break;
|
|
95
|
-
buffer += decoder.decode(value, { stream: true });
|
|
96
|
-
const lines = buffer.split("\n");
|
|
97
|
-
buffer = lines.pop() || "";
|
|
98
|
-
for (const line of lines) {
|
|
99
|
-
if (!line.startsWith("data:"))
|
|
100
|
-
continue;
|
|
101
|
-
const jsonStr = line.slice(5).trim();
|
|
102
|
-
if (!jsonStr)
|
|
103
|
-
continue;
|
|
104
|
-
let chunk;
|
|
105
|
-
try {
|
|
106
|
-
chunk = JSON.parse(jsonStr);
|
|
107
|
-
}
|
|
108
|
-
catch {
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
// Unwrap response for internal APIs
|
|
112
|
-
const data = chunk.response || chunk;
|
|
113
|
-
const candidate = data.candidates?.[0];
|
|
114
|
-
if (candidate?.content?.parts) {
|
|
115
|
-
for (const part of candidate.content.parts) {
|
|
116
|
-
if (part.text) {
|
|
117
|
-
accumulatedText += part.text;
|
|
118
|
-
// Stream update
|
|
119
|
-
onUpdate?.({
|
|
120
|
-
content: [{ type: "text", text: accumulatedText }],
|
|
121
|
-
details: { streaming: true }
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
// Capture metadata from final chunk
|
|
127
|
-
if (candidate?.groundingMetadata) {
|
|
128
|
-
groundingMetadata = candidate.groundingMetadata;
|
|
129
|
-
}
|
|
130
|
-
// Handle both camelCase and snake_case
|
|
131
|
-
if (candidate?.urlContextMetadata || candidate?.url_context_metadata) {
|
|
132
|
-
urlContextMetadata = candidate.urlContextMetadata || candidate.url_context_metadata;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
return {
|
|
137
|
-
text: accumulatedText || "No answer available.",
|
|
138
|
-
groundingMetadata,
|
|
139
|
-
urlContextMetadata
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
// --- Citation Processing (byte-safe) ---
|
|
143
|
-
export function applyCitations(text, groundingMetadata) {
|
|
144
|
-
const chunks = groundingMetadata?.groundingChunks || [];
|
|
145
|
-
const supports = groundingMetadata?.groundingSupports || [];
|
|
146
|
-
const sources = chunks
|
|
147
|
-
.filter((c) => c.web)
|
|
148
|
-
.map((c) => ({ title: c.web.title || "Unknown", url: c.web.uri || "" }));
|
|
149
|
-
if (!supports.length || !sources.length)
|
|
150
|
-
return { text, sources };
|
|
151
|
-
// Collect insertions, sort descending
|
|
152
|
-
const insertions = supports
|
|
153
|
-
.filter((s) => s.segment?.endIndex !== undefined && s.groundingChunkIndices?.length)
|
|
154
|
-
.map((s) => ({
|
|
155
|
-
index: s.segment.endIndex,
|
|
156
|
-
marker: s.groundingChunkIndices.map((i) => `[${i + 1}]`).join("")
|
|
157
|
-
}))
|
|
158
|
-
.sort((a, b) => b.index - a.index);
|
|
159
|
-
// Byte-safe insertion
|
|
160
|
-
const encoder = new TextEncoder();
|
|
161
|
-
const decoder = new TextDecoder();
|
|
162
|
-
const bytes = encoder.encode(text);
|
|
163
|
-
const parts = [];
|
|
164
|
-
let lastIndex = bytes.length;
|
|
165
|
-
for (const ins of insertions) {
|
|
166
|
-
const pos = Math.min(ins.index, lastIndex);
|
|
167
|
-
if (pos < lastIndex)
|
|
168
|
-
parts.unshift(bytes.subarray(pos, lastIndex));
|
|
169
|
-
parts.unshift(encoder.encode(ins.marker));
|
|
170
|
-
lastIndex = pos;
|
|
171
|
-
}
|
|
172
|
-
if (lastIndex > 0)
|
|
173
|
-
parts.unshift(bytes.subarray(0, lastIndex));
|
|
174
|
-
const total = parts.reduce((acc, p) => acc + p.length, 0);
|
|
175
|
-
const final = new Uint8Array(total);
|
|
176
|
-
let offset = 0;
|
|
177
|
-
for (const part of parts) {
|
|
178
|
-
final.set(part, offset);
|
|
179
|
-
offset += part.length;
|
|
180
|
-
}
|
|
181
|
-
return { text: decoder.decode(final), sources };
|
|
182
|
-
}
|
package/dist/index.d.ts
DELETED
package/dist/index.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { webSearch, WebSearchSchema } from "./web_search.js";
|
|
2
|
-
import { urlContext, UrlContextSchema } from "./url_context.js";
|
|
3
|
-
export default function (pi) {
|
|
4
|
-
pi.registerTool({
|
|
5
|
-
name: "web_search",
|
|
6
|
-
label: "Web Search",
|
|
7
|
-
description: "Search the web using Google Gemini's grounding. Optionally include URLs to analyze alongside search results.",
|
|
8
|
-
parameters: WebSearchSchema,
|
|
9
|
-
execute: webSearch
|
|
10
|
-
});
|
|
11
|
-
pi.registerTool({
|
|
12
|
-
name: "url_context",
|
|
13
|
-
label: "URL Context",
|
|
14
|
-
description: "Analyze the content of up to 20 public URLs. Supports web pages, documents, images, and YouTube videos.",
|
|
15
|
-
parameters: UrlContextSchema,
|
|
16
|
-
execute: urlContext
|
|
17
|
-
});
|
|
18
|
-
}
|
package/dist/url_context.d.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { ExtensionContext, AgentToolUpdateCallback } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import { type Static } from "@sinclair/typebox";
|
|
3
|
-
export declare const UrlContextSchema: import("@sinclair/typebox").TObject<{
|
|
4
|
-
query: import("@sinclair/typebox").TString;
|
|
5
|
-
urls: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>;
|
|
6
|
-
}>;
|
|
7
|
-
export type UrlContextInput = Static<typeof UrlContextSchema>;
|
|
8
|
-
export declare function urlContext(id: string, params: UrlContextInput, signal: AbortSignal, onUpdate: AgentToolUpdateCallback | undefined, ctx: ExtensionContext): Promise<import("@mariozechner/pi-coding-agent").AgentToolResult<any>>;
|