tools and skills, etc in the containers
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
const TIMEOUT_MS = 10_000;
|
||||
const DEFAULT_MAX_RESULTS = 10;
|
||||
const HARD_MAX_RESULTS = 20;
|
||||
|
||||
type SearxngResult = {
|
||||
title: string;
|
||||
url: string;
|
||||
content?: string;
|
||||
engine?: string;
|
||||
score?: number;
|
||||
};
|
||||
|
||||
type SearxngResponse = {
|
||||
query: string;
|
||||
number_of_results: number;
|
||||
results: SearxngResult[];
|
||||
};
|
||||
|
||||
type OnUpdate = (partial: { content: Array<{ type: string; text: string }> }) => void;
|
||||
|
||||
function update(onUpdate: OnUpdate | undefined, text: string): void {
|
||||
onUpdate?.({ content: [{ type: 'text', text }] });
|
||||
}
|
||||
|
||||
function formatResults(results: SearxngResult[]): string {
|
||||
if (results.length === 0) return 'No results found.';
|
||||
|
||||
return results
|
||||
.map((r, i) => {
|
||||
const lines = [`${i + 1}. **${r.title}**`, ` ${r.url}`];
|
||||
if (r.content?.trim()) lines.push(` ${r.content.trim()}`);
|
||||
return lines.join('\n');
|
||||
})
|
||||
.join('\n\n');
|
||||
}
|
||||
|
||||
export async function execute(
|
||||
_toolCallId: string,
|
||||
params: { query: string; max_results?: number },
|
||||
_signal: AbortSignal | undefined,
|
||||
onUpdate?: OnUpdate,
|
||||
) {
|
||||
const searxngUrl = process.env.PI_SEARXNG_URL;
|
||||
|
||||
if (!searxngUrl) {
|
||||
return {
|
||||
content: [{ type: 'text', text: 'Web search is not configured. PI_SEARXNG_URL is not set.' }],
|
||||
details: { error: 'not_configured' },
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const { query } = params;
|
||||
const maxResults = Math.min(params.max_results ?? DEFAULT_MAX_RESULTS, HARD_MAX_RESULTS);
|
||||
|
||||
if (!query?.trim()) {
|
||||
return {
|
||||
content: [{ type: 'text', text: 'Query cannot be empty.' }],
|
||||
details: { error: 'empty_query' },
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
update(onUpdate, `Searching for: ${query}`);
|
||||
|
||||
const searchUrl = new URL('/search', searxngUrl);
|
||||
searchUrl.searchParams.set('q', query);
|
||||
searchUrl.searchParams.set('format', 'json');
|
||||
searchUrl.searchParams.set('categories', 'general');
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(searchUrl.toString(), {
|
||||
signal: AbortSignal.timeout(TIMEOUT_MS),
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
} catch (err) {
|
||||
return {
|
||||
content: [{ type: 'text', text: `Failed to reach SearXNG at ${searxngUrl}: ${String(err)}` }],
|
||||
details: { error: 'fetch_failed', url: searxngUrl },
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
content: [{ type: 'text', text: `SearXNG returned HTTP ${response.status}` }],
|
||||
details: { error: 'http_error', status: response.status },
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
let data: SearxngResponse;
|
||||
try {
|
||||
data = (await response.json()) as SearxngResponse;
|
||||
} catch {
|
||||
return {
|
||||
content: [{ type: 'text', text: 'SearXNG returned an invalid response.' }],
|
||||
details: { error: 'invalid_json' },
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const results = (data.results ?? []).slice(0, maxResults);
|
||||
|
||||
update(onUpdate, `Found ${data.number_of_results ?? results.length} results, returning top ${results.length}`);
|
||||
|
||||
const output = [
|
||||
`Search: "${query}"`,
|
||||
`Results: ${results.length}`,
|
||||
``,
|
||||
formatResults(results),
|
||||
].join('\n');
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: output }],
|
||||
details: {
|
||||
query,
|
||||
total: data.number_of_results ?? results.length,
|
||||
returned: results.length,
|
||||
results: results.map((r) => ({ title: r.title, url: r.url })),
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user