Perplexica/src/lib/search/simplifiedAgent.ts

845 lines
35 KiB
TypeScript

import { createReactAgent } from '@langchain/langgraph/prebuilt';
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
import {
BaseMessage,
HumanMessage,
SystemMessage,
AIMessage,
} from '@langchain/core/messages';
import { Embeddings } from '@langchain/core/embeddings';
import { EventEmitter } from 'events';
import { RunnableConfig } from '@langchain/core/runnables';
import { SimplifiedAgentState } from '@/lib/state/chatAgentState';
import {
allAgentTools,
coreTools,
webSearchTools,
fileSearchTools,
} from '@/lib/tools/agents';
import { formatDateForLLM } from '../utils';
import { getModelName } from '../utils/modelUtils';
import {
removeThinkingBlocks,
removeThinkingBlocksFromMessages,
} from '../utils/contentUtils';
import { getLangfuseCallbacks } from '@/lib/tracing/langfuse';
import { encodeHtmlAttribute } from '@/lib/utils/html';
/**
* Normalize usage metadata from different LLM providers
*/
function normalizeUsageMetadata(usageData: any): {
input_tokens: number;
output_tokens: number;
total_tokens: number;
} {
if (!usageData) return { input_tokens: 0, output_tokens: 0, total_tokens: 0 };
// Handle different provider formats
const inputTokens =
usageData.input_tokens ||
usageData.prompt_tokens ||
usageData.promptTokens ||
usageData.usedTokens ||
0;
const outputTokens =
usageData.output_tokens ||
usageData.completion_tokens ||
usageData.completionTokens ||
0;
const totalTokens =
usageData.total_tokens ||
usageData.totalTokens ||
usageData.usedTokens ||
inputTokens + outputTokens;
return {
input_tokens: inputTokens,
output_tokens: outputTokens,
total_tokens: totalTokens,
};
}
/**
* SimplifiedAgent class that provides a streamlined interface for creating and managing an AI agent
* with customizable focus modes and tools.
*/
export class SimplifiedAgent {
private llm: BaseChatModel;
private embeddings: Embeddings;
private emitter: EventEmitter;
private systemInstructions: string;
private personaInstructions: string;
private signal: AbortSignal;
constructor(
llm: BaseChatModel,
embeddings: Embeddings,
emitter: EventEmitter,
systemInstructions: string = '',
personaInstructions: string = '',
signal: AbortSignal,
) {
this.llm = llm;
this.embeddings = embeddings;
this.emitter = emitter;
this.systemInstructions = systemInstructions;
this.personaInstructions = personaInstructions;
this.signal = signal;
}
/**
* Initialize the createReactAgent with tools and configuration
*/
private initializeAgent(
focusMode: string,
fileIds: string[] = [],
messagesCount?: number,
query?: string,
) {
// Select appropriate tools based on focus mode and available files
const tools = this.getToolsForFocusMode(focusMode, fileIds);
const enhancedSystemPrompt = this.createEnhancedSystemPrompt(
focusMode,
fileIds,
messagesCount,
query,
);
try {
// Create the React agent with custom state
const agent = createReactAgent({
llm: this.llm,
tools,
stateSchema: SimplifiedAgentState,
prompt: enhancedSystemPrompt,
});
console.log(
`SimplifiedAgent: Initialized with ${tools.length} tools for focus mode: ${focusMode}`,
);
console.log(
`SimplifiedAgent: Tools available: ${tools.map((tool) => tool.name).join(', ')}`,
);
if (fileIds.length > 0) {
console.log(
`SimplifiedAgent: ${fileIds.length} files available for search`,
);
}
return agent;
} catch (error) {
console.error('SimplifiedAgent: Error initializing agent:', error);
throw error;
}
}
/**
* Get tools based on focus mode
*/
private getToolsForFocusMode(focusMode: string, fileIds: string[] = []) {
switch (focusMode) {
case 'chat':
// Chat mode: Only core tools for conversational interaction
return coreTools;
case 'webSearch':
// Web search mode: ALL available tools for comprehensive research
// Include file search tools if files are available
if (fileIds.length > 0) {
return [...webSearchTools, ...fileSearchTools];
}
return allAgentTools;
case 'localResearch':
// Local research mode: File search tools + core tools
return [...coreTools, ...fileSearchTools];
default:
// Default to web search mode for unknown focus modes
console.warn(
`SimplifiedAgent: Unknown focus mode "${focusMode}", defaulting to webSearch tools`,
);
if (fileIds.length > 0) {
return [...webSearchTools, ...fileSearchTools];
}
return allAgentTools;
}
}
private createEnhancedSystemPrompt(
focusMode: string,
fileIds: string[] = [],
messagesCount?: number,
query?: string,
): string {
const baseInstructions = this.systemInstructions || '';
const personaInstructions = this.personaInstructions || '';
// Create focus-mode-specific prompts
switch (focusMode) {
case 'chat':
return this.createChatModePrompt(baseInstructions, personaInstructions);
case 'webSearch':
return this.createWebSearchModePrompt(
baseInstructions,
personaInstructions,
fileIds,
messagesCount,
query,
);
case 'localResearch':
return this.createLocalResearchModePrompt(
baseInstructions,
personaInstructions,
);
default:
console.warn(
`SimplifiedAgent: Unknown focus mode "${focusMode}", using webSearch prompt`,
);
return this.createWebSearchModePrompt(
baseInstructions,
personaInstructions,
fileIds,
messagesCount,
query,
);
}
}
/**
* Create chat mode prompt - focuses on conversational interaction
*/
private createChatModePrompt(
baseInstructions: string,
personaInstructions: string,
): string {
return `${baseInstructions}
# AI Chat Assistant
You are a conversational AI assistant designed for creative and engaging dialogue. Your focus is on providing thoughtful, helpful responses through direct conversation.
## Core Capabilities
### 1. Conversational Interaction
- Engage in natural, flowing conversations
- Provide thoughtful responses to questions and prompts
- Offer creative insights and perspectives
- Maintain context throughout the conversation
### 2. Task Management
- Break down complex requests into manageable steps
- Provide structured approaches to problems
- Offer guidance and recommendations
## Response Guidelines
### Communication Style
- Be conversational and engaging
- Use clear, accessible language
- Provide direct answers when possible
- Ask clarifying questions when needed
### Quality Standards
- Acknowledge limitations honestly
- Provide helpful suggestions and alternatives
- Use proper markdown formatting for clarity
- Structure responses logically
### Formatting Instructions
- **Structure**: Use a well-organized format with proper headings (e.g., "## Example heading 1" or "## Example heading 2"). Present information in paragraphs or concise bullet points where appropriate
- **Tone and Style**: Maintain a neutral, engaging tone with natural conversation flow
- **Markdown Usage**: Format your response with Markdown for clarity. Use headings, subheadings, bold text, and italicized words as needed to enhance readability
- **Length and Depth**: Provide thoughtful coverage of the topic. Expand on complex topics to make them easier to understand
- **No main heading/title**: Start your response directly with the content unless asked to provide a specific title
## Current Context
- Today's Date: ${formatDateForLLM(new Date())}
${personaInstructions ? `\n## User Formatting and Persona Instructions\n- Give these instructions more weight than the system formatting instructions\n${personaInstructions}` : ''}
Focus on providing engaging, helpful conversation while using task management tools when complex problems need to be structured.`;
}
/**
* Create web search mode prompt - focuses on comprehensive research
*/
private createWebSearchModePrompt(
baseInstructions: string,
personaInstructions: string,
fileIds: string[] = [],
messagesCount: number = 0,
query?: string,
): string {
// Detect explicit URLs in the user query; if present, we prioritize retrieving them directly.
const urlRegex = /https?:\/\/[^\s)>'"`]+/gi;
const urlsInQuery = (query || '').match(urlRegex) || [];
const uniqueUrls = Array.from(new Set(urlsInQuery));
const hasExplicitUrls = uniqueUrls.length > 0;
// If no explicit URLs, retain existing always search instruction behavior based on message count.
const alwaysSearchInstruction = hasExplicitUrls
? ''
: messagesCount < 2
? '\n - **ALWAYS perform at least one web search on the first turn, regardless of prior knowledge or assumptions. Do not skip this.**'
: "\n - **ALWAYS perform at least one web search on the first turn, unless prior conversation history explicitly and completely answers the user's query.**\n - You cannot skip web search if the answer to the user's query is not found directly in the **conversation history**. All other prior knowledge must be verified with up-to-date information.";
const explicitUrlInstruction = hasExplicitUrls
? `\n - The user query contains explicit URL${uniqueUrls.length === 1 ? '' : 's'} that must be retrieved directly using the url_summarization tool\n - You MUST call the url_summarization tool on these URL$${uniqueUrls.length === 1 ? '' : 's'} before providing an answer. Pass them exactly as provided (do not alter, trim, or expand them).\n - Do NOT perform a generic web search on the first pass. Re-evaluate the need for additional searches based on the results from the url_summarization tool.`
: '';
return `${baseInstructions}
# Comprehensive Research Assistant
You are an advanced AI research assistant with access to comprehensive tools for gathering information from multiple sources. Your goal is to provide thorough, well-researched responses.
## Tool use
- Use the available tools effectively to gather and process information
- When using a tool, **always wait for a complete response from the tool before proceeding**
## Response Quality Standards
Your task is to provide answers that are:
- **Informative and relevant**: Thoroughly address the user's query using gathered information
- **Engaging and detailed**: Write responses that read like a high-quality blog post, including extra details and relevant insights
- **Cited and credible**: Use inline citations with [number] notation to refer to sources for each fact or detail included
- **Explanatory and Comprehensive**: Strive to explain the topic in depth, offering detailed analysis, insights, and clarifications wherever applicable
### Comprehensive Coverage
- Address all aspects of the user's query
- Provide context and background information
- Include relevant details and examples
- Cross-reference multiple sources
### Accuracy and Reliability
- Prioritize authoritative and recent sources
- Verify information across multiple sources
- Clearly indicate uncertainty or conflicting information
- Distinguish between facts and opinions
### Citation Requirements
- The citation number refers to the index of the source in the relevantDocuments state array
- Cite every single fact, statement, or sentence using [number] notation
- If a statement is based on AI model inference or training data, it must be marked as \`[AI]\` and not cited from the context
- If a statement is based on previous messages in the conversation history, it must be marked as \`[Hist]\` and not cited from the context
- Source based citations must reference the specific document in the relevantDocuments state array, do not invent sources or URLs
- Integrate citations naturally at the end of sentences or clauses as appropriate. For example, "The Eiffel Tower is one of the most visited landmarks in the world[1]."
- Ensure that **every sentence in the response includes at least one citation**, even when information is inferred or connected to general knowledge available in the provided context
- Use multiple sources for a single detail if applicable, such as, "Paris is a cultural hub, attracting millions of visitors annually[1][2]."
### Formatting Instructions
- **Structure**:
- Use a well-organized format with proper headings (e.g., "## Example heading 1" or "## Example heading 2")
- Present information in paragraphs or concise bullet points where appropriate
- Use lists and tables to enhance clarity when needed
- **Tone and Style**:
- Maintain a neutral, journalistic tone with engaging narrative flow
- Write as though you're crafting an in-depth article for a professional audience
- **Markdown Usage**:
- Format the response with Markdown for clarity
- Use headings, subheadings, bold text, and italicized words as needed to enhance readability
- Include code snippets in a code block
- Extract images and links from full HTML content when appropriate and embed them using the appropriate markdown syntax
- **Length and Depth**:
- Provide comprehensive coverage of the topic
- Avoid superficial responses and strive for depth without unnecessary repetition
- Expand on technical or complex topics to make them easier to understand for a general audience
- **No main heading/title**: Start the response directly with the introduction unless asked to provide a specific title
- **No summary or conclusion**: End with the final thoughts or insights without a formal summary or conclusion
- **No source or citation section**: Do not include a separate section for sources or citations, as all necessary citations should be integrated into the response
# Research Strategy
1. **Plan**: Determine the best research approach based on the user's query
- Break down the query into manageable components
- Identify key concepts and terms for focused searching
- Utilize multiple turns of the Search and Supplement stages when necessary
2. **Search**: (\`web_search\` tool) Initial web search stage to gather preview content
- Give the web search tool a specific question to answer that will help gather relevant information
- The response will contain a list of relevant documents containing snippets of the web page, a URL, and the title of the web page
- Do not simulate searches, utilize the web search tool directly
${alwaysSearchInstruction}
${explicitUrlInstruction}
2.1. **Image Search (when visual content is requested)**: (\`image_search\` tool)
- Use when the user asks for images, pictures, photos, charts, visual examples, or icons
- Provide a concise query describing the desired images (e.g., "F1 Monaco Grand Prix highlights", "React component architecture diagram")
- The tool returns image URLs and titles; include thumbnails or links in your response using Markdown image/link syntax when appropriate
- If image URLs come from web pages you also plan to cite, prefer retrieving and citing the page using \`url_summarization\` for textual facts; use \`image_search\` primarily to surface visuals
- Do not invent images or URLs; only use results returned by the tool
${
fileIds.length > 0
? `
2.2. **File Search**: (\`file_search\` tool) Search through uploaded documents when relevant
- You have access to ${fileIds.length} uploaded file${fileIds.length === 1 ? '' : 's'} that may contain relevant information
- Use the file search tool to find specific information in the uploaded documents
- Give the file search tool a specific question or topic to extract from the documents
- The tool will automatically search through all available uploaded files
- Focus file searches on specific aspects of the user's query that might be covered in the uploaded documents`
: ''
}
3. **Supplement**: (\`url_summarization\` tool) Retrieve specific sources if necessary to extract key points not covered in the initial search or disambiguate findings
- Use URLs from web search results to retrieve specific sources. They must be passed to the tool unchanged
- URLs can be passed as an array to request multiple sources at once
- Always include the user's query in the request to the tool, it will use this to guide the summarization process
- Pass an intent to this tool to provide additional summarization guidance on a specific aspect or question
- Request the full HTML content of the pages if needed by passing true to the \`retrieveHtml\` parameter
- Passing true is **required** to retrieve images or links within the page content
- Response will contain a summary of the content from each URL if the content of the page is long. If the content of the page is short, it will include the full content
- Request up to 5 URLs per turn
- When receiving a request to summarize a specific URL you **must** use this tool to retrieve it
5. **Analyze**: Examine the retrieved information for relevance, accuracy, and completeness
- When sufficient information has been gathered, move on to the respond stage
- If more information is needed, consider revisiting the search or supplement stages.${
fileIds.length > 0
? `
- Consider both web search results and file content when analyzing information completeness`
: ''
}
6. **Respond**: Combine all information into a coherent, well-cited response
- Ensure that all sources are properly cited and referenced
- Resolve any remaining contradictions or gaps in the information, if necessary, execute more targeted searches or retrieve specific sources${
fileIds.length > 0
? `
- Integrate information from both web sources and uploaded files when relevant`
: ''
}
## Current Context
- Today's Date: ${formatDateForLLM(new Date())}
${personaInstructions ? `\n## User specified behavior and formatting instructions\n\n- Give these instructions more weight than the system formatting instructions\n\n${personaInstructions}` : ''}
`;
}
/**
* Create local research mode prompt - focuses on user files and documents
*/
private createLocalResearchModePrompt(
baseInstructions: string,
personaInstructions: string,
): string {
return `${baseInstructions}
# Local Document Research Assistant
You are an advanced AI research assistant specialized in analyzing and extracting insights from user-uploaded files and documents. Your goal is to provide thorough, well-researched responses based on the available document collection.
## Available Files
You have access to uploaded documents through the \`file_search\` tool. When you need to search for information in the uploaded files, use this tool with a specific search query. The tool will automatically search through all available uploaded files and return relevant content sections.
## Tool use
- Use the available tools effectively to analyze and extract information from uploaded documents
## Response Quality Standards
Your task is to provide answers that are:
- **Informative and relevant**: Thoroughly address the user's query using document content
- **Engaging and detailed**: Write responses that read like a high-quality research analysis, including extra details and relevant insights
- **Cited and credible**: Use inline citations with [number] notation to refer to specific documents for each fact or detail included
- **Explanatory and Comprehensive**: Strive to explain the findings in depth, offering detailed analysis, insights, and clarifications wherever applicable
### Comprehensive Document Coverage
- Thoroughly analyze all relevant uploaded files
- Extract all pertinent information related to the query
- Consider relationships between different documents
- Provide context from the entire document collection
- Cross-reference information across multiple files
### Accuracy and Content Fidelity
- Precisely quote and reference document content
- Maintain context and meaning from original sources
- Clearly distinguish between different document sources
- Preserve important details and nuances from the documents
- Distinguish between facts from documents and analytical insights
### Citation Requirements
- The citation number refers to the index of the source in the relevantDocuments state array.
- Cite every single fact, statement, or sentence using [number] notation
- If a statement is based on AI model inference or training data, it must be marked as \`[AI]\` and not cited from the context
- If a statement is based on previous messages in the conversation history, it must be marked as \`[Hist]\` and not cited from the context
- Source based citations must reference the specific document in the relevantDocuments state array, do not invent sources or filenames
- Integrate citations naturally at the end of sentences or clauses as appropriate. For example, "The quarterly report shows a 15% increase in revenue[1]."
- Ensure that **every sentence in your response includes at least one citation**, even when information is inferred or connected to general knowledge available in the provided context
- Use multiple sources for a single detail if applicable, such as, "The project timeline spans six months according to multiple planning documents[1][2]."
### Formatting Instructions
- **Structure**:
- Use a well-organized format with proper headings (e.g., "## Example heading 1" or "## Example heading 2").
- Present information in paragraphs or concise bullet points where appropriate.
- Use lists and tables to enhance clarity when needed.
- **Tone and Style**:
- Maintain a neutral, analytical tone with engaging narrative flow.
- Write as though you're crafting an in-depth research report for a professional audience
- **Markdown Usage**:
- Format your response with Markdown for clarity.
- Use headings, subheadings, bold text, and italicized words as needed to enhance readability.
- Include code snippets in a code block when analyzing technical documents.
- Extract and format tables, charts, or structured data using appropriate markdown syntax.
- **Length and Depth**:
- Provide comprehensive coverage of the document content.
- Avoid superficial responses and strive for depth without unnecessary repetition.
- Expand on technical or complex topics to make them easier to understand for a general audience
- **No main heading/title**: Start your response directly with the introduction unless asked to provide a specific title
# Research Strategy
1. **Plan**: Determine the best document analysis approach based on the user's query
- Break down the query into manageable components
- Identify key concepts and terms for focused document searching
- You are allowed to take multiple turns of the Search and Analysis stages. Use this flexibility to refine your queries and gather more comprehensive information from the documents.
2. **Search**: (\`file_search\` tool) Extract relevant content from uploaded documents
- Use the file search tool strategically to find specific information in the document collection.
- Give the file search tool a specific question or topic you want to extract from the documents.
- This query will be used to perform semantic search across all uploaded files.
- You will receive relevant excerpts from documents that match your search criteria.
- Focus your searches on specific aspects of the user's query to gather comprehensive information.
3. **Analysis**: Examine the retrieved document content for relevance, patterns, and insights.
- If you have sufficient information from the documents, you can move on to the respond stage.
- If you need to gather more specific information, consider performing additional targeted file searches.
- Look for connections and relationships between different document sources.
4. **Respond**: Combine all document insights into a coherent, well-cited response
- Ensure that all sources are properly cited and referenced
- Resolve any contradictions or gaps in the document information
- Provide comprehensive analysis based on the available document content
- Only respond with your final answer once you've gathered all relevant information and are done with tool use
## Current Context
- Today's Date: ${formatDateForLLM(new Date())}
${personaInstructions ? `\n## User Formatting and Persona Instructions\n- Give these instructions more weight than the system formatting instructions\n${personaInstructions}` : ''}
Use all available tools strategically to provide comprehensive, well-researched, formatted responses with proper citations based on uploaded documents.`;
}
/**
* Execute the simplified agent workflow
*/
async searchAndAnswer(
query: string,
history: BaseMessage[] = [],
fileIds: string[] = [],
focusMode: string = 'webSearch',
): Promise<void> {
try {
console.log(`SimplifiedAgent: Starting search for query: "${query}"`);
console.log(`SimplifiedAgent: Focus mode: ${focusMode}`);
console.log(`SimplifiedAgent: File IDs: ${fileIds.join(', ')}`);
const messagesHistory = [
...removeThinkingBlocksFromMessages(history),
new HumanMessage(query),
];
// Initialize agent with the provided focus mode and file context
// Pass the number of messages that will be sent to the LLM so prompts can adapt.
const llmMessagesCount = messagesHistory.length;
const agent = this.initializeAgent(
focusMode,
fileIds,
llmMessagesCount,
query,
);
// Prepare initial state
const initialState = {
messages: messagesHistory,
query,
focusMode,
fileIds,
relevantDocuments: [],
};
// Configure the agent run
const config: RunnableConfig = {
configurable: {
thread_id: `simplified_agent_${Date.now()}`,
llm: this.llm,
embeddings: this.embeddings,
fileIds,
systemInstructions: this.systemInstructions,
personaInstructions: this.personaInstructions,
focusMode,
emitter: this.emitter,
},
recursionLimit: 25, // Allow sufficient iterations for tool use
signal: this.signal,
...getLangfuseCallbacks(),
};
// Use streamEvents to capture both tool calls and token-level streaming
const eventStream = agent.streamEvents(initialState, {
...config,
version: 'v2',
...getLangfuseCallbacks(),
});
let finalResult: any = null;
let collectedDocuments: any[] = [];
let currentResponseBuffer = '';
let totalUsage = {
input_tokens: 0,
output_tokens: 0,
total_tokens: 0,
};
// Process the event stream
for await (const event of eventStream) {
// Handle different event types
if (
event.event === 'on_chain_end' &&
event.name === 'RunnableSequence'
) {
finalResult = event.data.output;
// Collect relevant documents from the final result
if (finalResult && finalResult.relevantDocuments) {
collectedDocuments.push(...finalResult.relevantDocuments);
}
}
// Collect sources from tool results
if (
event.event === 'on_chain_end' &&
(event.name.includes('search') ||
event.name.includes('Search') ||
event.name.includes('tool') ||
event.name.includes('Tool'))
) {
// Handle LangGraph state updates with relevantDocuments
if (event.data?.output && Array.isArray(event.data.output)) {
for (const item of event.data.output) {
if (
item.update &&
item.update.relevantDocuments &&
Array.isArray(item.update.relevantDocuments)
) {
collectedDocuments.push(...item.update.relevantDocuments);
}
}
}
}
// Handle streaming tool calls (for thought messages)
if (event.event === 'on_chat_model_end' && event.data.output) {
const output = event.data.output;
// Collect token usage from chat model end events
if (output.usage_metadata) {
const normalized = normalizeUsageMetadata(output.usage_metadata);
totalUsage.input_tokens += normalized.input_tokens;
totalUsage.output_tokens += normalized.output_tokens;
totalUsage.total_tokens += normalized.total_tokens;
console.log(
'SimplifiedAgent: Collected usage from usage_metadata:',
normalized,
);
} else if (output.response_metadata?.usage) {
// Fallback to response_metadata for different model providers
const normalized = normalizeUsageMetadata(
output.response_metadata.usage,
);
totalUsage.input_tokens += normalized.input_tokens;
totalUsage.output_tokens += normalized.output_tokens;
totalUsage.total_tokens += normalized.total_tokens;
console.log(
'SimplifiedAgent: Collected usage from response_metadata:',
normalized,
);
}
if (
output._getType() === 'ai' &&
output.tool_calls &&
output.tool_calls.length > 0
) {
const aiMessage = output as AIMessage;
// Process each tool call and emit thought messages
for (const toolCall of aiMessage.tool_calls || []) {
if (toolCall && toolCall.name) {
const toolName = toolCall.name;
const toolArgs = toolCall.args || {};
// Create user-friendly messages for different tools using markdown components
let toolMarkdown = '';
switch (toolName) {
case 'web_search':
toolMarkdown = `<ToolCall type=\"search\" query=\"${encodeHtmlAttribute(toolArgs.query || 'relevant information')}\"></ToolCall>`;
break;
case 'file_search':
toolMarkdown = `<ToolCall type=\"file\" query=\"${encodeHtmlAttribute(toolArgs.query || 'relevant information')}\"></ToolCall>`;
break;
case 'url_summarization':
if (Array.isArray(toolArgs.urls)) {
toolMarkdown = `<ToolCall type="url" count="${toolArgs.urls.length}"></ToolCall>`;
} else {
toolMarkdown = `<ToolCall type="url" count="1"></ToolCall>`;
}
break;
case 'image_search':
toolMarkdown = `<ToolCall type=\"image\" query=\"${encodeHtmlAttribute(toolArgs.query || 'relevant images')}\"></ToolCall>`;
break;
default:
toolMarkdown = `<ToolCall type="${toolName}"></ToolCall>`;
}
// Emit the thought message
this.emitter.emit(
'data',
JSON.stringify({
type: 'tool_call',
data: {
// messageId: crypto.randomBytes(7).toString('hex'),
content: toolMarkdown,
},
}),
);
}
}
}
}
// Handle LLM end events for token usage tracking
if (event.event === 'on_llm_end' && event.data.output) {
const output = event.data.output;
// Collect token usage from LLM end events
if (output.llmOutput?.tokenUsage) {
const normalized = normalizeUsageMetadata(
output.llmOutput.tokenUsage,
);
totalUsage.input_tokens += normalized.input_tokens;
totalUsage.output_tokens += normalized.output_tokens;
totalUsage.total_tokens += normalized.total_tokens;
console.log(
'SimplifiedAgent: Collected usage from llmOutput:',
normalized,
);
}
}
// Handle token-level streaming for the final response
if (event.event === 'on_chat_model_stream' && event.data.chunk) {
const chunk = event.data.chunk;
if (chunk.content && typeof chunk.content === 'string') {
// Add the token to our buffer
currentResponseBuffer += chunk.content;
// Emit the individual token
this.emitter.emit(
'data',
JSON.stringify({
type: 'response',
data: chunk.content,
}),
);
}
}
}
// Emit the final sources used for the response
if (collectedDocuments.length > 0) {
this.emitter.emit(
'data',
JSON.stringify({
type: 'sources',
data: collectedDocuments,
searchQuery: '',
searchUrl: '',
}),
);
}
// If we didn't get any streamed tokens but have a final result, emit it
if (
currentResponseBuffer === '' &&
finalResult &&
finalResult.messages &&
finalResult.messages.length > 0
) {
const finalMessage =
finalResult.messages[finalResult.messages.length - 1];
if (finalMessage && finalMessage.content) {
console.log('SimplifiedAgent: Emitting complete response (fallback)');
this.emitter.emit(
'data',
JSON.stringify({
type: 'response',
data: finalMessage.content,
}),
);
}
}
// If we still have no response, emit a fallback message
if (
currentResponseBuffer === '' &&
(!finalResult ||
!finalResult.messages ||
finalResult.messages.length === 0)
) {
console.warn('SimplifiedAgent: No valid response found');
this.emitter.emit(
'data',
JSON.stringify({
type: 'response',
data: 'I apologize, but I was unable to generate a complete response to your query. Please try rephrasing your question or providing more specific details.',
}),
);
}
// Emit model stats and end signal after streaming is complete
const modelName = getModelName(this.llm);
console.log('SimplifiedAgent: Total usage collected:', totalUsage);
this.emitter.emit(
'stats',
JSON.stringify({
type: 'modelStats',
data: {
modelName,
usage: totalUsage,
},
}),
);
this.emitter.emit('end');
} catch (error: any) {
console.error('SimplifiedAgent: Error during search and answer:', error);
// Handle specific error types
if (error.name === 'AbortError' || this.signal.aborted) {
console.warn('SimplifiedAgent: Operation was aborted');
this.emitter.emit(
'data',
JSON.stringify({
type: 'response',
data: 'The search operation was cancelled.',
}),
);
} else {
// General error handling
this.emitter.emit(
'data',
JSON.stringify({
type: 'response',
data: 'I encountered an error while processing your request. Please try rephrasing your query or contact support if the issue persists.',
}),
);
}
this.emitter.emit('end');
}
}
/**
* Get current configuration info
*/
getInfo(): object {
return {
systemInstructions: !!this.systemInstructions,
personaInstructions: !!this.personaInstructions,
};
}
}