feat(agent): Display agent actions on the UI
This commit is contained in:
parent
29146a03dc
commit
09799a880b
9 changed files with 407 additions and 44 deletions
|
|
@ -22,7 +22,7 @@ import { webSearchRetrieverAgentPrompt } from '../prompts/webSearch';
|
|||
import { searchSearxng } from '../searxng';
|
||||
import { formatDateForLLM } from '../utils';
|
||||
import { getModelName } from '../utils/modelUtils';
|
||||
import { summarizeWebContent } from '../utils/summarizeWebContent';
|
||||
import { summarizeWebContent, SummarizeResult } from '../utils/summarizeWebContent';
|
||||
|
||||
/**
|
||||
* State interface for the agent supervisor workflow
|
||||
|
|
@ -97,6 +97,21 @@ export class AgentSearch {
|
|||
private async webSearchAgent(
|
||||
state: typeof AgentState.State,
|
||||
): Promise<Command> {
|
||||
// Emit preparing web search event
|
||||
this.emitter.emit('agent_action', {
|
||||
type: 'agent_action',
|
||||
data: {
|
||||
action: 'PREPARING_SEARCH_QUERY',
|
||||
// message: `Preparing search query`,
|
||||
details: {
|
||||
query: state.query,
|
||||
searchInstructions: state.searchInstructions || state.query,
|
||||
documentCount: state.relevantDocuments.length,
|
||||
searchIterations: state.searchInstructionHistory.length
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const template = PromptTemplate.fromTemplate(webSearchRetrieverAgentPrompt);
|
||||
const prompt = await template.format({
|
||||
systemInstructions: this.systemInstructions,
|
||||
|
|
@ -118,11 +133,43 @@ export class AgentSearch {
|
|||
|
||||
try {
|
||||
console.log(`Performing web search for query: "${searchQuery}"`);
|
||||
|
||||
// Emit executing web search event
|
||||
this.emitter.emit('agent_action', {
|
||||
type: 'agent_action',
|
||||
data: {
|
||||
action: 'EXECUTING_WEB_SEARCH',
|
||||
// message: `Searching the web for: '${searchQuery}'`,
|
||||
details: {
|
||||
query: state.query,
|
||||
searchQuery: searchQuery,
|
||||
documentCount: state.relevantDocuments.length,
|
||||
searchIterations: state.searchInstructionHistory.length
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const searchResults = await searchSearxng(searchQuery, {
|
||||
language: 'en',
|
||||
engines: [],
|
||||
});
|
||||
|
||||
// Emit web sources identified event
|
||||
this.emitter.emit('agent_action', {
|
||||
type: 'agent_action',
|
||||
data: {
|
||||
action: 'WEB_SOURCES_IDENTIFIED',
|
||||
message: `Found ${searchResults.results.length} potential web sources`,
|
||||
details: {
|
||||
query: state.query,
|
||||
searchQuery: searchQuery,
|
||||
sourcesFound: searchResults.results.length,
|
||||
documentCount: state.relevantDocuments.length,
|
||||
searchIterations: state.searchInstructionHistory.length
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let bannedUrls = state.bannedUrls || [];
|
||||
let attemptedUrlCount = 0;
|
||||
// Summarize the top 2 search results
|
||||
|
|
@ -130,6 +177,8 @@ export class AgentSearch {
|
|||
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
|
||||
// optimization that should be transparent to the user
|
||||
continue; // Skip banned URLs
|
||||
}
|
||||
if (attemptedUrlCount >= 5) {
|
||||
|
|
@ -146,20 +195,72 @@ export class AgentSearch {
|
|||
break; // Limit to top 1 document
|
||||
}
|
||||
|
||||
const summary = await summarizeWebContent(
|
||||
// Emit analyzing source event
|
||||
this.emitter.emit('agent_action', {
|
||||
type: 'agent_action',
|
||||
data: {
|
||||
action: 'ANALYZING_SOURCE',
|
||||
message: `Analyzing content from: ${result.title || result.url}`,
|
||||
details: {
|
||||
query: state.query,
|
||||
sourceUrl: result.url,
|
||||
sourceTitle: result.title || 'Untitled',
|
||||
documentCount: state.relevantDocuments.length,
|
||||
searchIterations: state.searchInstructionHistory.length
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const summaryResult = await summarizeWebContent(
|
||||
result.url,
|
||||
state.query,
|
||||
this.llm,
|
||||
this.systemInstructions,
|
||||
this.signal,
|
||||
);
|
||||
if (summary) {
|
||||
documents.push(summary);
|
||||
|
||||
if (summaryResult.document) {
|
||||
documents.push(summaryResult.document);
|
||||
|
||||
// Emit context updated event
|
||||
this.emitter.emit('agent_action', {
|
||||
type: 'agent_action',
|
||||
data: {
|
||||
action: 'CONTEXT_UPDATED',
|
||||
message: `Added information from ${summaryResult.document.metadata.title || result.url} to context`,
|
||||
details: {
|
||||
query: state.query,
|
||||
sourceUrl: result.url,
|
||||
sourceTitle: summaryResult.document.metadata.title || 'Untitled',
|
||||
contentLength: summaryResult.document.pageContent.length,
|
||||
documentCount: state.relevantDocuments.length + documents.length,
|
||||
searchIterations: state.searchInstructionHistory.length
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log(
|
||||
`Summarized content from ${result.url} to ${summary.pageContent.length} characters. Content: ${summary.pageContent}`,
|
||||
`Summarized content from ${result.url} to ${summaryResult.document.pageContent.length} characters. Content: ${summaryResult.document.pageContent}`,
|
||||
);
|
||||
} else {
|
||||
console.warn(`No relevant content found for URL: ${result.url}`);
|
||||
|
||||
// Emit skipping irrelevant source event for non-relevant content
|
||||
this.emitter.emit('agent_action', {
|
||||
type: 'agent_action',
|
||||
data: {
|
||||
action: 'SKIPPING_IRRELEVANT_SOURCE',
|
||||
message: `Source ${result.title || result.url} was not relevant - trying next`,
|
||||
details: {
|
||||
query: state.query,
|
||||
sourceUrl: result.url,
|
||||
sourceTitle: result.title || 'Untitled',
|
||||
skipReason: summaryResult.notRelevantReason || 'Content was not relevant to the query',
|
||||
documentCount: state.relevantDocuments.length + documents.length,
|
||||
searchIterations: state.searchInstructionHistory.length
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -200,6 +301,20 @@ export class AgentSearch {
|
|||
|
||||
private async analyzer(state: typeof AgentState.State): Promise<Command> {
|
||||
try {
|
||||
// Emit initial analysis event
|
||||
this.emitter.emit('agent_action', {
|
||||
type: 'agent_action',
|
||||
data: {
|
||||
action: 'ANALYZING_CONTEXT',
|
||||
message: 'Analyzing the context to see if we have enough information to answer the query',
|
||||
details: {
|
||||
documentCount: state.relevantDocuments.length,
|
||||
query: state.query,
|
||||
searchIterations: state.searchInstructionHistory.length
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log(
|
||||
`Analyzing ${state.relevantDocuments.length} documents for relevance...`,
|
||||
);
|
||||
|
|
@ -282,6 +397,22 @@ Today's date is ${formatDateForLLM(new Date())}
|
|||
console.log('Reason for insufficiency:', reason);
|
||||
|
||||
if (analysisResult.startsWith('need_more_info')) {
|
||||
// Emit reanalyzing event when we need more information
|
||||
this.emitter.emit('agent_action', {
|
||||
type: 'agent_action',
|
||||
data: {
|
||||
action: 'MORE_DATA_NEEDED',
|
||||
message: 'Current context is insufficient - gathering more information',
|
||||
details: {
|
||||
reason: reason,
|
||||
nextSearchQuery: moreInfoQuestion,
|
||||
documentCount: state.relevantDocuments.length,
|
||||
searchIterations: state.searchInstructionHistory.length,
|
||||
query: state.query
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return new Command({
|
||||
goto: 'web_search',
|
||||
update: {
|
||||
|
|
@ -296,6 +427,20 @@ Today's date is ${formatDateForLLM(new Date())}
|
|||
});
|
||||
}
|
||||
|
||||
// Emit information gathering complete event when we have sufficient information
|
||||
this.emitter.emit('agent_action', {
|
||||
type: 'agent_action',
|
||||
data: {
|
||||
action: 'INFORMATION_GATHERING_COMPLETE',
|
||||
message: 'Sufficient information gathered - ready to synthesize response',
|
||||
details: {
|
||||
documentCount: state.relevantDocuments.length,
|
||||
searchIterations: state.searchInstructionHistory.length,
|
||||
query: state.query
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return new Command({
|
||||
goto: 'synthesizer',
|
||||
update: {
|
||||
|
|
@ -328,6 +473,20 @@ Today's date is ${formatDateForLLM(new Date())}
|
|||
state: typeof AgentState.State,
|
||||
): Promise<Command> {
|
||||
try {
|
||||
// Emit synthesizing response event
|
||||
this.emitter.emit('agent_action', {
|
||||
type: 'agent_action',
|
||||
data: {
|
||||
action: 'SYNTHESIZING_RESPONSE',
|
||||
message: 'Synthesizing final answer...',
|
||||
details: {
|
||||
query: state.query,
|
||||
documentCount: state.relevantDocuments.length,
|
||||
searchIterations: state.searchInstructionHistory.length
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const synthesisPrompt = `You are an expert information synthesizer. Based on the search results and analysis provided, create a comprehensive, well-structured answer to the user's query.
|
||||
|
||||
## Response Instructions
|
||||
|
|
|
|||
|
|
@ -4,18 +4,23 @@ import LineOutputParser from '../outputParsers/lineOutputParser';
|
|||
import { formatDateForLLM } from '../utils';
|
||||
import { getWebContent } from './documents';
|
||||
|
||||
export type SummarizeResult = {
|
||||
document: Document | null;
|
||||
notRelevantReason?: string;
|
||||
};
|
||||
|
||||
export const summarizeWebContent = async (
|
||||
url: string,
|
||||
query: string,
|
||||
llm: BaseChatModel,
|
||||
systemInstructions: string,
|
||||
signal: AbortSignal,
|
||||
): Promise<Document | null> => {
|
||||
): Promise<SummarizeResult> => {
|
||||
try {
|
||||
// Helper function to summarize content and check relevance
|
||||
const summarizeContent = async (
|
||||
content: Document,
|
||||
): Promise<Document | null> => {
|
||||
): Promise<SummarizeResult> => {
|
||||
const systemPrompt = systemInstructions
|
||||
? `${systemInstructions}\n\n`
|
||||
: '';
|
||||
|
|
@ -49,7 +54,7 @@ Here is the query you need to answer: ${query}
|
|||
|
||||
Here is the content to summarize:
|
||||
${i === 0 ? content.metadata.html : content.pageContent},
|
||||
`,
|
||||
`,
|
||||
{ signal },
|
||||
);
|
||||
break;
|
||||
|
|
@ -63,7 +68,7 @@ ${i === 0 ? content.metadata.html : content.pageContent},
|
|||
|
||||
if (!summary || !summary.content) {
|
||||
console.error(`No summary content returned for URL: ${url}`);
|
||||
return null;
|
||||
return { document: null, notRelevantReason: 'No summary content returned from LLM' };
|
||||
}
|
||||
|
||||
const summaryParser = new LineOutputParser({ key: 'summary' });
|
||||
|
|
@ -79,16 +84,27 @@ ${i === 0 ? content.metadata.html : content.pageContent},
|
|||
`LLM response for URL "${url}" indicates it's not needed or is empty:`,
|
||||
summarizedContent,
|
||||
);
|
||||
return null;
|
||||
|
||||
// Extract the reason from the "not_needed" response
|
||||
const reason = summarizedContent.startsWith('not_needed')
|
||||
? summarizedContent.substring('not_needed:'.length).trim()
|
||||
: summarizedContent.trim().length === 0
|
||||
? 'Source content was empty or could not be processed'
|
||||
: 'Source content was not relevant to the query';
|
||||
|
||||
return { document: null, notRelevantReason: reason };
|
||||
}
|
||||
|
||||
return new Document({
|
||||
pageContent: summarizedContent,
|
||||
metadata: {
|
||||
...content.metadata,
|
||||
url: url,
|
||||
},
|
||||
});
|
||||
return {
|
||||
document: new Document({
|
||||
pageContent: summarizedContent,
|
||||
metadata: {
|
||||
...content.metadata,
|
||||
url: url,
|
||||
},
|
||||
}),
|
||||
notRelevantReason: undefined
|
||||
};
|
||||
};
|
||||
|
||||
// // First try the lite approach
|
||||
|
|
@ -121,9 +137,10 @@ ${i === 0 ? content.metadata.html : content.pageContent},
|
|||
return await summarizeContent(webContent);
|
||||
} else {
|
||||
console.log(`No valid content found for URL: ${url}`);
|
||||
return { document: null, notRelevantReason: 'No valid content found at the URL' };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error processing URL ${url}:`, error);
|
||||
return { document: null, notRelevantReason: `Error processing URL: ${error instanceof Error ? error.message : 'Unknown error'}` };
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue