feat(research): Introduce research API with provider implementations, docs and tests

This commit is contained in:
2025-09-28 15:06:07 +00:00
parent 5c856ec3ed
commit e2ed429aac
17 changed files with 852 additions and 14 deletions

View File

@@ -9,13 +9,15 @@ export type TChatCompletionRequestMessage = {
};
import { MultiModalModel } from './abstract.classes.multimodal.js';
import type { ResearchOptions, ResearchResponse } from './abstract.classes.multimodal.js';
export interface IOpenaiProviderOptions {
openaiToken: string;
chatModel?: string;
audioModel?: string;
visionModel?: string;
// Optionally add more model options (e.g., documentModel) if needed.
researchModel?: string;
enableWebSearch?: boolean;
}
export class OpenAiProvider extends MultiModalModel {
@@ -229,4 +231,111 @@ export class OpenAiProvider extends MultiModalModel {
const result = await this.openAiApiClient.chat.completions.create(requestParams);
return result.choices[0].message.content || '';
}
public async research(optionsArg: ResearchOptions): Promise<ResearchResponse> {
// Determine which model to use based on search depth
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';
}
// Prepare the request parameters
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
};
// Add web search tools 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']
}
}
}
];
requestParams.tool_choice = 'auto';
}
// Add background flag for deep research
if (optionsArg.background && optionsArg.searchDepth === 'deep') {
requestParams.background = true;
}
try {
// Execute the research request
const result = await this.openAiApiClient.chat.completions.create(requestParams);
// Extract the answer
const answer = result.choices[0].message.content || '';
// Parse sources from the response (OpenAI often includes URLs in markdown format)
const sources: Array<{ url: string; title: string; snippet: string }> = [];
const urlRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
let match: RegExpExecArray | null;
while ((match = urlRegex.exec(answer)) !== null) {
sources.push({
title: match[1],
url: match[2],
snippet: '' // OpenAI doesn't provide snippets in standard responses
});
}
// 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,
searchQueries: searchQueries.length > 0 ? searchQueries : undefined,
metadata: {
model,
searchDepth: optionsArg.searchDepth || 'basic',
tokensUsed: result.usage?.total_tokens
}
};
} catch (error) {
console.error('Research API error:', error);
throw new Error(`Failed to perform research: ${error.message}`);
}
}
}