feat(agent): Improve relevance checking and preview content handling in web search agent.
Better groq support.
This commit is contained in:
parent
2eb0d60918
commit
b392aa2c21
4 changed files with 144 additions and 134 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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 },
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue