forked from NoeFabris/opencode-antigravity-auth
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsearch.ts
More file actions
313 lines (270 loc) · 7.96 KB
/
Copy pathsearch.ts
File metadata and controls
313 lines (270 loc) · 7.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
/**
* Google Search Tool Implementation
*
* Due to Gemini API limitations, native search tools (googleSearch, urlContext)
* cannot be combined with function declarations. This module implements a
* wrapper that makes separate API calls with only the grounding tools enabled.
*/
import {
ANTIGRAVITY_ENDPOINT,
getAntigravityHeaders,
SEARCH_MODEL,
SEARCH_TIMEOUT_MS,
SEARCH_SYSTEM_INSTRUCTION,
} from "../constants";
import { createLogger } from "./logger";
const log = createLogger("search");
// ============================================================================
// Types
// ============================================================================
interface GroundingChunk {
web?: {
uri?: string;
title?: string;
};
}
interface GroundingSupport {
segment?: {
startIndex?: number;
endIndex?: number;
text?: string;
};
groundingChunkIndices?: number[];
}
interface GroundingMetadata {
webSearchQueries?: string[];
groundingChunks?: GroundingChunk[];
groundingSupports?: GroundingSupport[];
searchEntryPoint?: {
renderedContent?: string;
};
}
interface UrlMetadata {
retrieved_url?: string;
url_retrieval_status?: string;
}
interface UrlContextMetadata {
url_metadata?: UrlMetadata[];
}
interface SearchResponse {
candidates?: Array<{
content?: {
parts?: Array<{ text?: string }>;
role?: string;
};
finishReason?: string;
groundingMetadata?: GroundingMetadata;
urlContextMetadata?: UrlContextMetadata;
}>;
error?: {
code?: number;
message?: string;
status?: string;
};
}
interface AntigravitySearchResponse {
response?: SearchResponse;
error?: {
code?: number;
message?: string;
status?: string;
};
}
export interface SearchArgs {
query: string;
urls?: string[];
thinking?: boolean;
}
export interface SearchResult {
text: string;
sources: Array<{ title: string; url: string }>;
searchQueries: string[];
urlsRetrieved: Array<{ url: string; status: string }>;
}
// ============================================================================
// Helper Functions
// ============================================================================
let sessionCounter = 0;
const sessionPrefix = `search-${Date.now().toString(36)}`;
function generateRequestId(): string {
return `search-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
}
function getSessionId(): string {
sessionCounter++;
return `${sessionPrefix}-${sessionCounter}`;
}
function formatSearchResult(result: SearchResult): string {
const lines: string[] = [];
lines.push("## Search Results\n");
lines.push(result.text);
lines.push("");
if (result.sources.length > 0) {
lines.push("### Sources");
for (const source of result.sources) {
lines.push(`- [${source.title}](${source.url})`);
}
lines.push("");
}
if (result.urlsRetrieved.length > 0) {
lines.push("### URLs Retrieved");
for (const url of result.urlsRetrieved) {
const status = url.status === "URL_RETRIEVAL_STATUS_SUCCESS" ? "✓" : "✗";
lines.push(`- ${status} ${url.url}`);
}
lines.push("");
}
if (result.searchQueries.length > 0) {
lines.push("### Search Queries Used");
for (const q of result.searchQueries) {
lines.push(`- "${q}"`);
}
}
return lines.join("\n");
}
function parseSearchResponse(data: AntigravitySearchResponse): SearchResult {
const result: SearchResult = {
text: "",
sources: [],
searchQueries: [],
urlsRetrieved: [],
};
const response = data.response;
if (!response || !response.candidates || response.candidates.length === 0) {
if (data.error) {
result.text = `Error: ${data.error.message ?? "Unknown error"}`;
} else if (response?.error) {
result.text = `Error: ${response.error.message ?? "Unknown error"}`;
}
return result;
}
const candidate = response.candidates[0];
if (!candidate) {
return result;
}
// Extract text content
if (candidate.content?.parts) {
result.text = candidate.content.parts
.map((p: { text?: string }) => p.text ?? "")
.filter(Boolean)
.join("\n");
}
// Extract grounding metadata
if (candidate.groundingMetadata) {
const gm = candidate.groundingMetadata;
if (gm.webSearchQueries) {
result.searchQueries = gm.webSearchQueries;
}
if (gm.groundingChunks) {
for (const chunk of gm.groundingChunks) {
if (chunk.web?.uri && chunk.web?.title) {
result.sources.push({
title: chunk.web.title,
url: chunk.web.uri,
});
}
}
}
}
// Extract URL context metadata
if (candidate.urlContextMetadata?.url_metadata) {
for (const meta of candidate.urlContextMetadata.url_metadata) {
if (meta.retrieved_url) {
result.urlsRetrieved.push({
url: meta.retrieved_url,
status: meta.url_retrieval_status ?? "UNKNOWN",
});
}
}
}
return result;
}
// ============================================================================
// Main Search Function
// ============================================================================
/**
* Execute a Google Search using the Gemini grounding API.
*
* This makes a SEPARATE API call with only googleSearch/urlContext tools,
* which is required because these tools cannot be combined with function declarations.
*/
export async function executeSearch(
args: SearchArgs,
accessToken: string,
projectId: string,
abortSignal?: AbortSignal,
): Promise<string> {
const { query, urls, thinking = true } = args;
// Build prompt with optional URLs
let prompt = query;
if (urls && urls.length > 0) {
const urlList = urls.join("\n");
prompt = `${query}\n\nURLs to analyze:\n${urlList}`;
}
// Build tools array - only grounding tools, no function declarations
const tools: Array<Record<string, unknown>> = [];
tools.push({ googleSearch: {} });
if (urls && urls.length > 0) {
tools.push({ urlContext: {} });
}
const requestPayload = {
systemInstruction: {
parts: [{ text: SEARCH_SYSTEM_INSTRUCTION }],
},
contents: [
{
role: "user",
parts: [{ text: prompt }],
},
],
tools,
generationConfig: {
temperature: 0,
topP: 1,
},
};
// Wrap in Antigravity format
const wrappedBody = {
project: projectId,
model: SEARCH_MODEL,
userAgent: "antigravity",
requestId: generateRequestId(),
request: {
...requestPayload,
sessionId: getSessionId(),
},
};
// Use non-streaming endpoint for search
const url = `${ANTIGRAVITY_ENDPOINT}/v1internal:generateContent`;
log.debug("Executing search", {
query,
urlCount: urls?.length ?? 0,
thinking,
});
try {
const response = await fetch(url, {
method: "POST",
headers: {
...getAntigravityHeaders(),
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(wrappedBody),
signal: abortSignal ?? AbortSignal.timeout(SEARCH_TIMEOUT_MS),
});
if (!response.ok) {
const errorText = await response.text();
log.debug("Search API error", { status: response.status, error: errorText });
return `## Search Error\n\nFailed to execute search: ${response.status} ${response.statusText}\n\n${errorText}\n\nPlease try again with a different query.`;
}
const data = (await response.json()) as AntigravitySearchResponse;
log.debug("Search response received", { hasResponse: !!data.response });
const result = parseSearchResponse(data);
const formatted = formatSearchResult(result);
log.debug("Search response formatted", { resultLength: formatted.length });
return formatted;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
log.debug("Search execution error", { error: message });
return `## Search Error\n\nFailed to execute search: ${message}. Please try again with a different query.`;
}
}