feat(research): Implement research APIs.

This commit is contained in:
2025-10-03 12:50:42 +00:00
parent e34bf19698
commit fe8540c8ba
11 changed files with 367 additions and 114 deletions

View File

@@ -68,7 +68,7 @@ export class AnthropicProvider extends MultiModalModel {
// If we have a complete message, send it to Anthropic
if (currentMessage) {
const stream = await this.anthropicApiClient.messages.create({
model: 'claude-3-opus-20240229',
model: 'claude-sonnet-4-5-20250929',
messages: [{ role: currentMessage.role, content: currentMessage.content }],
system: '',
stream: true,
@@ -112,7 +112,7 @@ export class AnthropicProvider extends MultiModalModel {
}));
const result = await this.anthropicApiClient.messages.create({
model: 'claude-3-opus-20240229',
model: 'claude-sonnet-4-5-20250929',
system: optionsArg.systemMessage,
messages: [
...messages,
@@ -159,7 +159,7 @@ export class AnthropicProvider extends MultiModalModel {
];
const result = await this.anthropicApiClient.messages.create({
model: 'claude-3-opus-20240229',
model: 'claude-sonnet-4-5-20250929',
messages: [{
role: 'user',
content
@@ -218,7 +218,7 @@ export class AnthropicProvider extends MultiModalModel {
}
const result = await this.anthropicApiClient.messages.create({
model: 'claude-3-opus-20240229',
model: 'claude-sonnet-4-5-20250929',
system: optionsArg.systemMessage,
messages: [
...messages,
@@ -251,23 +251,27 @@ export class AnthropicProvider extends MultiModalModel {
try {
// Build the tool configuration for web search
const tools = this.options.enableWebSearch ? [
{
type: 'web_search_20250305' as const,
name: 'web_search',
description: 'Search the web for current information',
input_schema: {
type: 'object' as const,
properties: {
query: {
type: 'string',
description: 'The search query'
}
},
required: ['query']
}
const tools: any[] = [];
if (this.options.enableWebSearch) {
const webSearchTool: any = {
type: 'web_search_20250305',
name: 'web_search'
};
// Add optional parameters
if (optionsArg.maxSources) {
webSearchTool.max_uses = optionsArg.maxSources;
}
] : [];
if (this.options.searchDomainAllowList?.length) {
webSearchTool.allowed_domains = this.options.searchDomainAllowList;
} else if (this.options.searchDomainBlockList?.length) {
webSearchTool.blocked_domains = this.options.searchDomainBlockList;
}
tools.push(webSearchTool);
}
// Configure the request based on search depth
const maxTokens = optionsArg.searchDepth === 'deep' ? 8192 :
@@ -275,7 +279,7 @@ export class AnthropicProvider extends MultiModalModel {
// Create the research request
const requestParams: any = {
model: 'claude-3-opus-20240229',
model: 'claude-sonnet-4-5-20250929',
system: systemMessage,
messages: [
{
@@ -290,7 +294,6 @@ export class AnthropicProvider extends MultiModalModel {
// Add tools if web search is enabled
if (tools.length > 0) {
requestParams.tools = tools;
requestParams.tool_choice = { type: 'auto' };
}
// Execute the research request
@@ -304,54 +307,71 @@ export class AnthropicProvider extends MultiModalModel {
// Process content blocks
for (const block of result.content) {
if ('text' in block) {
// Accumulate text content
answer += block.text;
// Extract citations if present
if ('citations' in block && Array.isArray(block.citations)) {
for (const citation of block.citations) {
if (citation.type === 'web_search_result_location') {
sources.push({
title: citation.title || '',
url: citation.url || '',
snippet: citation.cited_text || ''
});
}
}
}
} else if ('type' in block && block.type === 'server_tool_use') {
// Extract search queries from server tool use
if (block.name === 'web_search' && block.input && typeof block.input === 'object' && 'query' in block.input) {
searchQueries.push((block.input as any).query);
}
} else if ('type' in block && block.type === 'web_search_tool_result') {
// Extract sources from web search results
if (Array.isArray(block.content)) {
for (const result of block.content) {
if (result.type === 'web_search_result') {
// Only add if not already in sources (avoid duplicates from citations)
if (!sources.some(s => s.url === result.url)) {
sources.push({
title: result.title || '',
url: result.url || '',
snippet: '' // Search results don't include snippets, only citations do
});
}
}
}
}
}
}
// Parse sources from the answer (Claude includes citations in various formats)
const urlRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
let match: RegExpExecArray | null;
// Fallback: Parse markdown-style 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: ''
});
}
// Also look for plain URLs
const plainUrlRegex = /https?:\/\/[^\s\)]+/g;
const plainUrls = answer.match(plainUrlRegex) || [];
for (const url of plainUrls) {
// Check if this URL is already in sources
if (!sources.some(s => s.url === url)) {
while ((match = urlRegex.exec(answer)) !== null) {
sources.push({
title: new URL(url).hostname,
url: url,
title: match[1],
url: match[2],
snippet: ''
});
}
}
// Extract tool use information if available
if ('tool_use' in result && Array.isArray(result.tool_use)) {
for (const toolUse of result.tool_use) {
if (toolUse.name === 'web_search' && toolUse.input?.query) {
searchQueries.push(toolUse.input.query);
}
}
}
// Check if web search was used based on usage info
const webSearchCount = result.usage?.server_tool_use?.web_search_requests || 0;
return {
answer,
sources,
searchQueries: searchQueries.length > 0 ? searchQueries : undefined,
metadata: {
model: 'claude-3-opus-20240229',
model: 'claude-sonnet-4-5-20250929',
searchDepth: optionsArg.searchDepth || 'basic',
tokensUsed: result.usage?.output_tokens
tokensUsed: result.usage?.output_tokens,
webSearchesPerformed: webSearchCount
}
};
} catch (error) {

View File

@@ -233,52 +233,37 @@ export class OpenAiProvider extends MultiModalModel {
}
public async research(optionsArg: ResearchOptions): Promise<ResearchResponse> {
// Determine which model to use based on search depth
// Determine which model to use - Deep Research API requires specific models
let model: string;
if (optionsArg.searchDepth === 'deep') {
model = this.options.researchModel || 'o4-mini-deep-research-2025-06-26';
} else {
model = this.options.chatModel || 'gpt-5-mini';
// For basic/advanced, still use deep research models if web search is needed
if (optionsArg.includeWebSearch) {
model = this.options.researchModel || 'o4-mini-deep-research-2025-06-26';
} else {
model = this.options.chatModel || 'gpt-5-mini';
}
}
// Prepare the request parameters
const systemMessage = 'You are a research assistant. Provide comprehensive answers with citations and sources when available.';
// Prepare request parameters using Deep Research API format
const requestParams: any = {
model,
messages: [
{
role: 'system',
content: 'You are a research assistant. Provide comprehensive answers with citations and sources when available.'
},
{
role: 'user',
content: optionsArg.query
}
],
temperature: 0.7
instructions: systemMessage,
input: optionsArg.query
};
// Add web search tools if requested
// Add web search tool if requested
if (optionsArg.includeWebSearch || optionsArg.searchDepth === 'deep') {
requestParams.tools = [
{
type: 'function',
function: {
name: 'web_search',
description: 'Search the web for information',
parameters: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'The search query'
}
},
required: ['query']
}
}
type: 'web_search_preview',
search_context_size: optionsArg.searchDepth === 'deep' ? 'high' :
optionsArg.searchDepth === 'advanced' ? 'medium' : 'low'
}
];
requestParams.tool_choice = 'auto';
}
// Add background flag for deep research
@@ -287,14 +272,36 @@ export class OpenAiProvider extends MultiModalModel {
}
try {
// Execute the research request
const result = await this.openAiApiClient.chat.completions.create(requestParams);
// Execute the research request using Deep Research API
const result = await this.openAiApiClient.responses.create(requestParams);
// Extract the answer
const answer = result.choices[0].message.content || '';
// Parse sources from the response (OpenAI often includes URLs in markdown format)
// Extract the answer from output items
let answer = '';
const sources: Array<{ url: string; title: string; snippet: string }> = [];
const searchQueries: string[] = [];
// Process output items
for (const item of result.output || []) {
// Extract message content
if (item.type === 'message' && 'content' in item) {
const messageItem = item as any;
for (const contentItem of messageItem.content || []) {
if (contentItem.type === 'output_text' && 'text' in contentItem) {
answer += contentItem.text;
}
}
}
// Extract web search queries
if (item.type === 'web_search_call' && 'action' in item) {
const searchItem = item as any;
if (searchItem.action && searchItem.action.type === 'search' && 'query' in searchItem.action) {
searchQueries.push(searchItem.action.query);
}
}
}
// Parse sources from markdown links in the answer
const urlRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
let match: RegExpExecArray | null;
@@ -302,27 +309,10 @@ export class OpenAiProvider extends MultiModalModel {
sources.push({
title: match[1],
url: match[2],
snippet: '' // OpenAI doesn't provide snippets in standard responses
snippet: ''
});
}
// Extract search queries if tools were used
const searchQueries: string[] = [];
if (result.choices[0].message.tool_calls) {
for (const toolCall of result.choices[0].message.tool_calls) {
if ('function' in toolCall && toolCall.function.name === 'web_search') {
try {
const args = JSON.parse(toolCall.function.arguments);
if (args.query) {
searchQueries.push(args.query);
}
} catch (e) {
// Ignore parsing errors
}
}
}
}
return {
answer,
sources,