feat(agent): Improve relevance checking and preview content handling in web search agent.

Better groq support.
This commit is contained in:
Willie Zutz 2025-07-01 00:36:12 -06:00
parent 2eb0d60918
commit b392aa2c21
4 changed files with 144 additions and 134 deletions

View file

@ -181,15 +181,25 @@ export class WebSearchAgent {
}), }),
); );
// Sort by relevance score and take top 12 results let previewContents: PreviewContent[] = [];
const previewContents: PreviewContent[] = resultsWithSimilarity // Always take the top 3 results for preview content
previewContents.push(...filteredResults.slice(0, 3)
.map((result) => ({
title: result.title || 'Untitled',
snippet: result.content || '',
url: result.url,
}))
);
// Sort by relevance score and take top 12 results for a total of 15
previewContents.push(...resultsWithSimilarity.slice(3)
.sort((a, b) => b.similarity - a.similarity) .sort((a, b) => b.similarity - a.similarity)
.slice(0, 12) .slice(0, 12)
.map(({ result }) => ({ .map(({ result }) => ({
title: result.title || 'Untitled', title: result.title || 'Untitled',
snippet: result.content || '', snippet: result.content || '',
url: result.url, url: result.url,
})); })));
console.log( console.log(
`Extracted preview content from ${previewContents.length} search results for analysis`, `Extracted preview content from ${previewContents.length} search results for analysis`,
@ -306,9 +316,7 @@ export class WebSearchAgent {
}); });
// Summarize the top 2 search results // Summarize the top 2 search results
for (const result of resultsWithSimilarity for (const result of previewContents) {
.slice(0, 12)
.map((r) => r.result)) {
if (this.signal.aborted) { if (this.signal.aborted) {
console.warn('Search operation aborted by signal'); console.warn('Search operation aborted by signal');
break; // Exit if the operation is aborted break; // Exit if the operation is aborted

View file

@ -1,91 +1,91 @@
import { ChatOpenAI } from '@langchain/openai'; import { ChatOpenAI } from '@langchain/openai';
import { getGroqApiKey } from '../config'; import { getGroqApiKey } from '../config';
import { ChatModel } from '.'; import { ChatModel } from '.';
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
export const PROVIDER_INFO = { export const PROVIDER_INFO = {
key: 'groq', key: 'groq',
displayName: 'Groq', displayName: 'Groq',
}; };
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
const groqChatModels: Record<string, string>[] = [ interface GroqModel {
{ id: string;
displayName: 'Gemma2 9B IT', object: string;
key: 'gemma2-9b-it', created: number;
}, owned_by: string;
{ active: boolean;
displayName: 'Llama 3.3 70B Versatile', context_window: number;
key: 'llama-3.3-70b-versatile', max_completion_tokens: number;
}, }
{
displayName: 'Llama 3.1 8B Instant', interface GroqModelsResponse {
key: 'llama-3.1-8b-instant', object: string;
}, data: GroqModel[];
{ }
displayName: 'Llama3 70B 8192',
key: 'llama3-70b-8192', const generateDisplayName = (modelId: string, ownedBy: string): string => {
}, // Handle special cases for better display names
{ const modelMap: Record<string, string> = {
displayName: 'Llama3 8B 8192', 'gemma2-9b-it': 'Gemma2 9B IT',
key: 'llama3-8b-8192', 'llama-3.3-70b-versatile': 'Llama 3.3 70B Versatile',
}, 'llama-3.1-8b-instant': 'Llama 3.1 8B Instant',
{ 'llama3-70b-8192': 'Llama3 70B 8192',
displayName: 'Mixtral 8x7B 32768', 'llama3-8b-8192': 'Llama3 8B 8192',
key: 'mixtral-8x7b-32768', 'mixtral-8x7b-32768': 'Mixtral 8x7B 32768',
}, 'qwen-qwq-32b': 'Qwen QWQ 32B',
{ 'mistral-saba-24b': 'Mistral Saba 24B',
displayName: 'Qwen QWQ 32B (Preview)', 'deepseek-r1-distill-llama-70b': 'DeepSeek R1 Distill Llama 70B',
key: 'qwen-qwq-32b', 'deepseek-r1-distill-qwen-32b': 'DeepSeek R1 Distill Qwen 32B',
}, };
{
displayName: 'Mistral Saba 24B (Preview)', // Return mapped name if available
key: 'mistral-saba-24b', if (modelMap[modelId]) {
}, return modelMap[modelId];
{ }
displayName: 'Qwen 2.5 Coder 32B (Preview)',
key: 'qwen-2.5-coder-32b', // Generate display name from model ID
}, let displayName = modelId
{ .replace(/[-_]/g, ' ')
displayName: 'Qwen 2.5 32B (Preview)', .split(' ')
key: 'qwen-2.5-32b', .map(word => word.charAt(0).toUpperCase() + word.slice(1))
}, .join(' ');
{
displayName: 'DeepSeek R1 Distill Qwen 32B (Preview)', // Add owner info for certain models
key: 'deepseek-r1-distill-qwen-32b', if (modelId.includes('meta-llama/')) {
}, displayName = displayName.replace('Meta Llama/', '');
{ }
displayName: 'DeepSeek R1 Distill Llama 70B (Preview)',
key: 'deepseek-r1-distill-llama-70b', return displayName;
}, };
{
displayName: 'Llama 3.3 70B SpecDec (Preview)', const fetchGroqModels = async (apiKey: string): Promise<GroqModel[]> => {
key: 'llama-3.3-70b-specdec', try {
}, const response = await fetch('https://api.groq.com/openai/v1/models', {
{ headers: {
displayName: 'Llama 3.2 1B Preview (Preview)', 'Authorization': `Bearer ${apiKey}`,
key: 'llama-3.2-1b-preview', 'Content-Type': 'application/json',
}, },
{ });
displayName: 'Llama 3.2 3B Preview (Preview)',
key: 'llama-3.2-3b-preview', if (!response.ok) {
}, throw new Error(`Failed to fetch models: ${response.status} ${response.statusText}`);
{ }
displayName: 'Llama 3.2 11B Vision Preview (Preview)',
key: 'llama-3.2-11b-vision-preview', const data: GroqModelsResponse = await response.json();
},
{ // Filter for active chat completion models (exclude audio/whisper models)
displayName: 'Llama 3.2 90B Vision Preview (Preview)', return data.data.filter(model =>
key: 'llama-3.2-90b-vision-preview', model.active &&
}, !model.id.includes('whisper') &&
/* { !model.id.includes('tts') &&
displayName: 'Llama 4 Maverick 17B 128E Instruct (Preview)', !model.id.includes('guard') &&
key: 'meta-llama/llama-4-maverick-17b-128e-instruct', !model.id.includes('prompt-guard')
}, */ );
{ } catch (error) {
displayName: 'Llama 4 Scout 17B 16E Instruct (Preview)', console.error('Error fetching Groq models:', error);
key: 'meta-llama/llama-4-scout-17b-16e-instruct', return [];
}, }
]; };
export const loadGroqChatModels = async () => { export const loadGroqChatModels = async () => {
const groqApiKey = getGroqApiKey(); const groqApiKey = getGroqApiKey();
@ -95,12 +95,15 @@ export const loadGroqChatModels = async () => {
try { try {
const chatModels: Record<string, ChatModel> = {}; const chatModels: Record<string, ChatModel> = {};
groqChatModels.forEach((model) => { // Fetch available models from Groq API
chatModels[model.key] = { const availableModels = await fetchGroqModels(groqApiKey);
displayName: model.displayName,
availableModels.forEach((model) => {
chatModels[model.id] = {
displayName: generateDisplayName(model.id, model.owned_by),
model: new ChatOpenAI({ model: new ChatOpenAI({
openAIApiKey: groqApiKey, openAIApiKey: groqApiKey,
modelName: model.key, modelName: model.id,
// temperature: 0.7, // temperature: 0.7,
configuration: { configuration: {
baseURL: 'https://api.groq.com/openai/v1', baseURL: 'https://api.groq.com/openai/v1',

View file

@ -83,9 +83,8 @@ Snippet: ${content.snippet}
# Instructions # Instructions
- Analyze the provided search result previews (titles + snippets), and chat history context to determine if they collectively contain enough information to provide a complete and accurate answer to the Task Query - Analyze the provided search result previews (titles + snippets), and chat history context to determine if they collectively contain enough information to provide a complete and accurate answer to the Task Query
- 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 Task Query, consider it sufficient
- If the preview content can provide a complete answer to the Task Query, set isSufficient to true - If the preview content lacks important details, requires deeper analysis, or cannot fully answer the Task Query, consider it insufficient
- If the preview content lacks important details, requires deeper analysis, or cannot fully answer the Task Query, set isSufficient to false and provide a specific reason
- Be specific in your reasoning when the content is not sufficient - Be specific in your reasoning when the content is not sufficient
- The original query is provided for additional context, only use it for clarification of overall expectations and intent. You do **not** need to answer the original query directly or completely - The original query is provided for additional context, only use it for clarification of overall expectations and intent. You do **not** need to answer the original query directly or completely
@ -103,11 +102,6 @@ ${taskQuery}
# Search Result Previews to Analyze: # Search Result Previews to Analyze:
${formattedPreviewContent} ${formattedPreviewContent}
# Response Format
You must return a JSON object with:
- isSufficient: boolean indicating whether preview content is sufficient
- reason: string explaining why full content analysis is required (only if isSufficient is false)
`, `,
{ signal }, { signal },
); );

View file

@ -1,5 +1,6 @@
import { Document } from '@langchain/core/documents'; import { Document } from '@langchain/core/documents';
import { BaseChatModel } from '@langchain/core/language_models/chat_models'; import { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { z } from 'zod';
import { formatDateForLLM } from '../utils'; import { formatDateForLLM } from '../utils';
import { getWebContent } from './documents'; import { getWebContent } from './documents';
import { removeThinkingBlocks } from './contentUtils'; import { removeThinkingBlocks } from './contentUtils';
@ -10,6 +11,16 @@ export type SummarizeResult = {
notRelevantReason?: string; notRelevantReason?: string;
}; };
// Zod schema for structured relevance check output
const RelevanceCheckSchema = z.object({
relevant: z
.boolean()
.describe('Whether the content is relevant to the user query'),
reason: z
.string()
.describe('Brief explanation of why content is or isn\'t relevant'),
});
export const summarizeWebContent = async ( export const summarizeWebContent = async (
url: string, url: string,
query: string, query: string,
@ -37,55 +48,49 @@ export const summarizeWebContent = async (
`Short content detected (${contentToAnalyze.length} chars) for URL: ${url}, checking relevance only`, `Short content detected (${contentToAnalyze.length} chars) for URL: ${url}, checking relevance only`,
); );
const relevancePrompt = `${systemPrompt}You are a content relevance checker. Your task is to determine if the given content is relevant to the user's query. try {
// Create structured LLM with Zod schema
const structuredLLM = llm.withStructuredOutput(RelevanceCheckSchema);
const relevanceResult = await structuredLLM.invoke(
`${systemPrompt}You are a content relevance checker. Your task is to determine if the given content is relevant to the user's query.
# Instructions # Instructions
- Analyze the content to determine if it contains information relevant to the user's query - Analyze the content to determine if it contains information relevant to the user's query
- You do not need to provide a full answer to the query in order to be relevant, partial answers are acceptable - You do not need to provide a full answer to the query in order to be relevant, partial answers are acceptable
- Respond with valid JSON in the following format: - Provide a brief explanation of your reasoning
{
"relevant": true/false,
"reason": "brief explanation of why content is or isn't relevant"
}
Today's date is ${formatDateForLLM(new Date())} Today's date is ${formatDateForLLM(new Date())}
Here is the query you need to answer: ${query} Here is the query you need to answer: ${query}
Here is the content to analyze: Here is the content to analyze:
${contentToAnalyze}`; ${contentToAnalyze}`,
{ signal }
);
try { if (!relevanceResult) {
const result = await llm.invoke(relevancePrompt, { signal }); console.error(`No relevance result returned for URL ${url}`);
const responseText = removeThinkingBlocks(result.content as string).trim();
try {
const parsedResponse = JSON.parse(responseText);
if (parsedResponse.relevant === true) {
console.log(`Short content for URL "${url}" is relevant: ${parsedResponse.reason}`);
return {
document: new Document({
pageContent: content.pageContent,
metadata: {
...content.metadata,
url: url,
processingType: 'short-content',
},
}),
notRelevantReason: undefined,
};
} else {
console.log(`Short content for URL "${url}" is not relevant: ${parsedResponse.reason}`);
return {
document: null,
notRelevantReason: parsedResponse.reason || 'Content not relevant to query',
};
}
} catch (parseError) {
console.error(`Error parsing JSON response for URL ${url}:`, parseError);
console.error(`Raw response:`, responseText);
// Fall through to full summarization as fallback // Fall through to full summarization as fallback
} else if (relevanceResult.relevant) {
console.log(`Short content for URL "${url}" is relevant: ${relevanceResult.reason}`);
return {
document: new Document({
pageContent: content.pageContent,
metadata: {
...content.metadata,
url: url,
processingType: 'short-content',
},
}),
notRelevantReason: undefined,
};
} else {
console.log(`Short content for URL "${url}" is not relevant: ${relevanceResult.reason}`);
return {
document: null,
notRelevantReason: relevanceResult.reason || 'Content not relevant to query',
};
} }
} catch (error) { } catch (error) {
console.error(`Error checking relevance for short content from URL ${url}:`, error); console.error(`Error checking relevance for short content from URL ${url}:`, error);