121 lines
3.5 KiB
TypeScript
121 lines
3.5 KiB
TypeScript
import * as plugins from './plugins.js';
|
|
|
|
export interface IResearchOptions {
|
|
apiKey: string;
|
|
query: string;
|
|
searchDepth?: 'basic' | 'advanced' | 'deep';
|
|
maxSources?: number;
|
|
allowedDomains?: string[];
|
|
blockedDomains?: string[];
|
|
}
|
|
|
|
export interface IResearchResponse {
|
|
answer: string;
|
|
sources: Array<{ url: string; title: string; snippet: string }>;
|
|
searchQueries?: string[];
|
|
metadata?: Record<string, unknown>;
|
|
}
|
|
|
|
export async function research(options: IResearchOptions): Promise<IResearchResponse> {
|
|
const client = new plugins.Anthropic({ apiKey: options.apiKey });
|
|
|
|
const systemMessage = `You are a research assistant with web search capabilities.
|
|
Provide comprehensive, well-researched answers with citations and sources.
|
|
When searching the web, be thorough and cite your sources accurately.`;
|
|
|
|
// Build web search tool config
|
|
const webSearchTool: any = {
|
|
type: 'web_search_20250305',
|
|
name: 'web_search',
|
|
};
|
|
|
|
if (options.maxSources) {
|
|
webSearchTool.max_uses = options.maxSources;
|
|
}
|
|
if (options.allowedDomains?.length) {
|
|
webSearchTool.allowed_domains = options.allowedDomains;
|
|
} else if (options.blockedDomains?.length) {
|
|
webSearchTool.blocked_domains = options.blockedDomains;
|
|
}
|
|
|
|
const result = await client.messages.create({
|
|
model: 'claude-sonnet-4-5-20250929',
|
|
system: systemMessage,
|
|
messages: [
|
|
{ role: 'user' as const, content: options.query },
|
|
],
|
|
max_tokens: 20000,
|
|
temperature: 0.7,
|
|
tools: [webSearchTool],
|
|
});
|
|
|
|
// Extract answer, sources, and search queries
|
|
let answer = '';
|
|
const sources: Array<{ url: string; title: string; snippet: string }> = [];
|
|
const searchQueries: string[] = [];
|
|
|
|
for (const block of result.content) {
|
|
const b: any = block;
|
|
if ('text' in b) {
|
|
answer += b.text;
|
|
|
|
// Extract citations if present
|
|
if (b.citations && Array.isArray(b.citations)) {
|
|
for (const citation of b.citations) {
|
|
if (citation.type === 'web_search_result_location') {
|
|
sources.push({
|
|
title: citation.title || '',
|
|
url: citation.url || '',
|
|
snippet: citation.cited_text || '',
|
|
});
|
|
}
|
|
}
|
|
}
|
|
} else if (b.type === 'server_tool_use') {
|
|
if (b.name === 'web_search' && b.input?.query) {
|
|
searchQueries.push(b.input.query);
|
|
}
|
|
} else if (b.type === 'web_search_tool_result') {
|
|
if (Array.isArray(b.content)) {
|
|
for (const item of b.content) {
|
|
if (item.type === 'web_search_result') {
|
|
if (!sources.some(s => s.url === item.url)) {
|
|
sources.push({
|
|
title: item.title || '',
|
|
url: item.url || '',
|
|
snippet: '',
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback: parse markdown links if no citations found
|
|
if (sources.length === 0) {
|
|
const urlRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
|
|
let match: RegExpExecArray | null;
|
|
while ((match = urlRegex.exec(answer)) !== null) {
|
|
sources.push({
|
|
title: match[1],
|
|
url: match[2],
|
|
snippet: '',
|
|
});
|
|
}
|
|
}
|
|
|
|
const usage: any = result.usage;
|
|
return {
|
|
answer,
|
|
sources,
|
|
searchQueries: searchQueries.length > 0 ? searchQueries : undefined,
|
|
metadata: {
|
|
model: 'claude-sonnet-4-5-20250929',
|
|
searchDepth: options.searchDepth || 'basic',
|
|
tokensUsed: usage?.output_tokens,
|
|
webSearchesPerformed: usage?.server_tool_use?.web_search_requests ?? 0,
|
|
},
|
|
};
|
|
}
|