From 3437c6522f32c83429f81c8936714b941be7b876 Mon Sep 17 00:00:00 2001 From: Willie Zutz Date: Sun, 15 Jun 2025 15:51:46 -0600 Subject: [PATCH] feat(agent): Start agent speed optimizations. --- src/components/AgentActionDisplay.tsx | 2 +- src/lib/agents/analyzerAgent.ts | 3 +- src/lib/agents/webSearchAgent.ts | 110 ++++++++++++++++++++- src/lib/utils/analyzePreviewContent.ts | 128 +++++++++++++++++++++++++ 4 files changed, 238 insertions(+), 5 deletions(-) create mode 100644 src/lib/utils/analyzePreviewContent.ts diff --git a/src/components/AgentActionDisplay.tsx b/src/components/AgentActionDisplay.tsx index 510fc54..d3d38f1 100644 --- a/src/components/AgentActionDisplay.tsx +++ b/src/components/AgentActionDisplay.tsx @@ -81,7 +81,7 @@ const AgentActionDisplay = ({ events, messageId }: AgentActionDisplayProps) => { {event.details.searchQuery && event.details.searchQuery !== event.details.query && (
Search Query: - "{event.details.searchQuery}" + "{event.details.searchQuery}"
)} {event.details.sourcesFound !== undefined && ( diff --git a/src/lib/agents/analyzerAgent.ts b/src/lib/agents/analyzerAgent.ts index 704fcc1..9d028bf 100644 --- a/src/lib/agents/analyzerAgent.ts +++ b/src/lib/agents/analyzerAgent.ts @@ -103,6 +103,7 @@ Today's date is ${formatDateForLLM(new Date())} { signal: this.signal }, ); + console.log('Analysis response:', response.content); // Parse the response to extract the analysis result const analysisOutputParser = new LineOutputParser({ key: 'answer' }); const moreInfoOutputParser = new LineOutputParser({ key: 'question' }); @@ -122,7 +123,7 @@ Today's date is ${formatDateForLLM(new Date())} console.log('More info question:', moreInfoQuestion); console.log('Reason for insufficiency:', reason); - if (analysisResult.startsWith('need_more_info')) { + if (!analysisResult.startsWith('good_content')) { // Emit reanalyzing event when we need more information this.emitter.emit('agent_action', { type: 'agent_action', diff --git a/src/lib/agents/webSearchAgent.ts b/src/lib/agents/webSearchAgent.ts index bc03f89..585f6ad 100644 --- a/src/lib/agents/webSearchAgent.ts +++ b/src/lib/agents/webSearchAgent.ts @@ -9,6 +9,7 @@ import { webSearchRetrieverAgentPrompt } from '../prompts/webSearch'; import { searchSearxng } from '../searxng'; import { formatDateForLLM } from '../utils'; import { summarizeWebContent } from '../utils/summarizeWebContent'; +import { analyzePreviewContent, PreviewContent } from '../utils/analyzePreviewContent'; import { AgentState } from './agentState'; export class WebSearchAgent { @@ -107,10 +108,112 @@ export class WebSearchAgent { }); let bannedUrls = state.bannedUrls || []; - let attemptedUrlCount = 0; - // Summarize the top 2 search results + + // Extract preview content from top 8 search results for analysis + const previewContents: PreviewContent[] = searchResults.results + .filter(result => !bannedUrls.includes(result.url)) // Filter out banned URLs first + .slice(0, 8) // Then take top 8 results + .map(result => ({ + title: result.title || 'Untitled', + snippet: result.content || '', + url: result.url + })); + + console.log(`Extracted preview content from ${previewContents.length} search results for analysis`); + + // Perform preview analysis to determine if full content retrieval is needed + let previewAnalysisResult = null; + if (previewContents.length > 0) { + console.log('Starting preview content analysis to determine if full processing is needed'); + + // Emit preview analysis event + this.emitter.emit('agent_action', { + type: 'agent_action', + data: { + action: 'ANALYZING_PREVIEW_CONTENT', + message: `Analyzing ${previewContents.length} search result previews to determine processing approach`, + details: { + query: state.query, + previewCount: previewContents.length, + documentCount: state.relevantDocuments.length, + searchIterations: state.searchInstructionHistory.length + } + } + }); + + previewAnalysisResult = await analyzePreviewContent( + previewContents, + state.query, + state.messages, + this.llm, + this.systemInstructions, + this.signal + ); + + console.log(`Preview analysis result: ${previewAnalysisResult.isSufficient ? 'SUFFICIENT' : 'INSUFFICIENT'}${previewAnalysisResult.reason ? ` - ${previewAnalysisResult.reason}` : ''}`); + } + let documents: Document[] = []; - for (const result of searchResults.results) { + let attemptedUrlCount = 0; // Declare outside conditional blocks + + // Conditional workflow based on preview analysis result + if (previewAnalysisResult && previewAnalysisResult.isSufficient) { + // Preview content is sufficient - create documents from preview content + console.log('Preview content determined sufficient - skipping full content retrieval'); + + // Emit preview processing event + this.emitter.emit('agent_action', { + type: 'agent_action', + data: { + action: 'PROCESSING_PREVIEW_CONTENT', + message: `Using preview content from ${previewContents.length} sources - no full content retrieval needed`, + details: { + query: state.query, + previewCount: previewContents.length, + documentCount: state.relevantDocuments.length, + searchIterations: state.searchInstructionHistory.length, + processingType: 'preview-only' + } + } + }); + + // Create documents from preview content + documents = previewContents.map((content, index) => new Document({ + pageContent: `# ${content.title}\n\n${content.snippet}`, + metadata: { + title: content.title, + url: content.url, + source: content.url, + processingType: 'preview-only', + snippet: content.snippet + } + })); + + console.log(`Created ${documents.length} documents from preview content`); + + } else { + // Preview content is insufficient - proceed with full content processing + const insufficiencyReason = previewAnalysisResult?.reason || 'Preview content not available or insufficient'; + console.log(`Preview content insufficient: ${insufficiencyReason} - proceeding with full content retrieval`); + + // Emit full processing event + this.emitter.emit('agent_action', { + type: 'agent_action', + data: { + action: 'PROCEEDING_WITH_FULL_ANALYSIS', + message: `Preview content insufficient - proceeding with detailed content analysis`, + details: { + query: state.query, + insufficiencyReason: insufficiencyReason, + documentCount: state.relevantDocuments.length, + searchIterations: state.searchInstructionHistory.length, + processingType: 'full-content' + } + } + }); + + // Summarize the top 2 search results + for (const result of searchResults.results) { if (bannedUrls.includes(result.url)) { console.log(`Skipping banned URL: ${result.url}`); // Note: We don't emit an agent_action event for banned URLs as this is an internal @@ -199,6 +302,7 @@ export class WebSearchAgent { }); } } + } // Close the else block for full content processing if (documents.length === 0) { return new Command({ diff --git a/src/lib/utils/analyzePreviewContent.ts b/src/lib/utils/analyzePreviewContent.ts new file mode 100644 index 0000000..2f1367a --- /dev/null +++ b/src/lib/utils/analyzePreviewContent.ts @@ -0,0 +1,128 @@ +import { BaseChatModel } from '@langchain/core/language_models/chat_models'; +import { BaseMessage } from '@langchain/core/messages'; +import LineOutputParser from '../outputParsers/lineOutputParser'; +import { formatDateForLLM } from '../utils'; + +export type PreviewAnalysisResult = { + isSufficient: boolean; + reason?: string; +}; + +export type PreviewContent = { + title: string; + snippet: string; + url: string; +}; + +export const analyzePreviewContent = async ( + previewContents: PreviewContent[], + query: string, + chatHistory: BaseMessage[], + llm: BaseChatModel, + systemInstructions: string, + signal: AbortSignal, +): Promise => { + try { + console.log(`Analyzing preview content for query: "${query}"`); + console.log(`Preview content being analyzed:`, previewContents.map(content => ({ + title: content.title, + snippet: content.snippet.substring(0, 100) + '...', + url: content.url + }))); + + // Format preview content for analysis + const formattedPreviewContent = previewContents + .map((content, index) => + `Source ${index + 1}: +Title: ${content.title} +Snippet: ${content.snippet} +URL: ${content.url} +---` + ) + .join('\n\n'); + + // Format chat history for context + const formattedChatHistory = chatHistory + .slice(-10) // Only include last 10 messages for context + .map((message, index) => + `${message._getType()}: ${message.content}` + ) + .join('\n'); + + const systemPrompt = systemInstructions + ? `${systemInstructions}\n\n` + : ''; + + console.log(`Invoking LLM for preview content analysis`); + + const analysisResponse = await llm.invoke( + `${systemPrompt}You are a preview content analyzer, tasked with determining if search result snippets contain sufficient information to answer a user's query. + +# Instructions +- Analyze the provided search result previews (titles + snippets) to determine if they collectively contain enough information to provide a complete and accurate answer to the user's query +- Consider the chat history context when making your decision +- You must make a binary decision: either the preview content is sufficient OR it is not sufficient +- If the preview content can provide a complete answer to the query, respond with "sufficient" +- If the preview content lacks important details, requires deeper analysis, or cannot fully answer the query, respond with "not_needed: [specific reason why full content analysis is required]" +- Be specific in your reasoning when the content is not sufficient +- Consider query complexity: simple factual questions may be answerable from snippets, while complex research questions typically need full content +- Consider information completeness: if key details are missing from the snippets that would be needed for a complete answer, full analysis is required +- Output your decision inside a \`decision\` XML tag + +Today's date is ${formatDateForLLM(new Date())} + +# Chat History Context: +${formattedChatHistory ? formattedChatHistory : 'No previous conversation context'} + +# User Query: +${query} + +# Search Result Previews to Analyze: +${formattedPreviewContent} + `, + { signal }, + ); + + if (!analysisResponse || !analysisResponse.content) { + console.error('No analysis response returned from LLM'); + return { + isSufficient: false, + reason: 'No analysis response returned from LLM - falling back to full content processing' + }; + } + + const decisionParser = new LineOutputParser({ key: 'decision' }); + const decision = await decisionParser.parse( + analysisResponse.content as string, + ); + + console.log(`LLM decision response:`, decision); + + if (decision.toLowerCase().trim() === 'sufficient') { + console.log('Preview content determined to be sufficient for answering the query'); + return { isSufficient: true }; + } else if (decision.toLowerCase().startsWith('not_needed')) { + // Extract the reason from the "not_needed" response + const reason = decision.startsWith('not_needed') + ? decision.substring('not_needed:'.length).trim() + : 'Preview content insufficient for complete answer'; + + console.log(`Preview content determined to be insufficient. Reason: ${reason}`); + return { isSufficient: false, reason }; + } else { + // Default to not sufficient if unclear response + console.log(`Unclear LLM response, defaulting to insufficient: ${decision}`); + return { + isSufficient: false, + reason: 'Unclear analysis response - falling back to full content processing' + }; + } + + } catch (error) { + console.error('Error analyzing preview content:', error); + return { + isSufficient: false, + reason: `Error during preview analysis: ${error instanceof Error ? error.message : 'Unknown error'} - falling back to full content processing` + }; + } +};