feat(agent): Enhance agent components with new actions and improve loading animations

This commit is contained in:
Willie Zutz 2025-06-26 23:53:52 -06:00
parent 2805417307
commit 7b47d3dacb
9 changed files with 166 additions and 79 deletions

View file

@ -11,3 +11,17 @@
display: none; display: none;
} }
} }
@layer utilities {
@keyframes high-bounce {
0%,
100% {
transform: translateY(0);
animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
}
50% {
transform: translateY(-9px);
animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
}
}
}

View file

@ -9,6 +9,9 @@ import {
Search, Search,
Zap, Zap,
Microscope, Microscope,
Ban,
CircleCheck,
ListPlus,
} from 'lucide-react'; } from 'lucide-react';
import { AgentActionEvent } from './ChatWindow'; import { AgentActionEvent } from './ChatWindow';
@ -42,6 +45,22 @@ const AgentActionDisplay = ({
return <Zap size={size} className="text-[#9C27B0]" />; return <Zap size={size} className="text-[#9C27B0]" />;
case 'PROCEEDING_WITH_FULL_ANALYSIS': case 'PROCEEDING_WITH_FULL_ANALYSIS':
return <Microscope size={size} className="text-[#9C27B0]" />; return <Microscope size={size} className="text-[#9C27B0]" />;
case 'SKIPPING_IRRELEVANT_SOURCE':
return <Ban size={size} className="text-red-600 dark:text-red-500" />;
case 'CONTEXT_UPDATED':
return (
<ListPlus
size={size}
className="text-green-600 dark:text-green-500"
/>
);
case 'INFORMATION_GATHERING_COMPLETE':
return (
<CircleCheck
size={size}
className="text-green-600 dark:text-green-500"
/>
);
default: default:
return <Bot size={size} className="text-[#9C27B0]" />; return <Bot size={size} className="text-[#9C27B0]" />;
} }
@ -64,12 +83,12 @@ const AgentActionDisplay = ({
latestEvent.action === 'INFORMATION_GATHERING_COMPLETE' latestEvent.action === 'INFORMATION_GATHERING_COMPLETE'
? 'Agent Log' ? 'Agent Log'
: formatActionName(latestEvent.action)} : formatActionName(latestEvent.action)}
{isLoading && {/* {isLoading &&
latestEvent.action !== 'INFORMATION_GATHERING_COMPLETE' && ( latestEvent.action !== 'INFORMATION_GATHERING_COMPLETE' && (
<span className="ml-2 inline-block align-middle"> <span className="ml-2 inline-block align-middle">
<span className="animate-spin inline-block w-4 h-4 border-2 border-t-transparent border-[#9C27B0] rounded-full align-middle"></span> <span className="animate-spin inline-block w-4 h-4 border-2 border-t-transparent border-[#9C27B0] rounded-full align-middle"></span>
</span> </span>
)} )} */}
</span> </span>
</div> </div>
{isExpanded ? ( {isExpanded ? (
@ -103,7 +122,9 @@ const AgentActionDisplay = ({
<div className="mt-2 text-sm text-black/60 dark:text-white/60"> <div className="mt-2 text-sm text-black/60 dark:text-white/60">
{event.details.sourceUrl && ( {event.details.sourceUrl && (
<div className="flex space-x-1"> <div className="flex space-x-1">
<span className="font-bold">Source:</span> <span className="font-bold whitespace-nowrap">
Source:
</span>
<span className="truncate"> <span className="truncate">
<a href={event.details.sourceUrl} target="_blank"> <a href={event.details.sourceUrl} target="_blank">
{event.details.sourceUrl} {event.details.sourceUrl}
@ -113,14 +134,18 @@ const AgentActionDisplay = ({
)} )}
{event.details.skipReason && ( {event.details.skipReason && (
<div className="flex space-x-1"> <div className="flex space-x-1">
<span className="font-bold">Reason:</span> <span className="font-bold whitespace-nowrap">
Reason:
</span>
<span>{event.details.skipReason}</span> <span>{event.details.skipReason}</span>
</div> </div>
)} )}
{event.details.searchQuery && {event.details.searchQuery &&
event.details.searchQuery !== event.details.query && ( event.details.searchQuery !== event.details.query && (
<div className="flex space-x-1"> <div className="flex space-x-1">
<span className="font-bold">Search Query:</span> <span className="font-bold whitespace-nowrap">
Search Query:
</span>
<span className="italic"> <span className="italic">
&quot;{event.details.searchQuery}&quot; &quot;{event.details.searchQuery}&quot;
</span> </span>
@ -128,37 +153,45 @@ const AgentActionDisplay = ({
)} )}
{event.details.sourcesFound !== undefined && ( {event.details.sourcesFound !== undefined && (
<div className="flex space-x-1"> <div className="flex space-x-1">
<span className="font-bold">Sources Found:</span> <span className="font-bold whitespace-nowrap">
Sources Found:
</span>
<span>{event.details.sourcesFound}</span> <span>{event.details.sourcesFound}</span>
</div> </div>
)} )}
{/* {(event.details.documentCount !== undefined && event.details.documentCount > 0) && ( {/* {(event.details.documentCount !== undefined && event.details.documentCount > 0) && (
<div className="flex space-x-1"> <div className="flex space-x-1">
<span className="font-bold">Documents:</span> <span className="font-bold whitespace-nowrap">Documents:</span>
<span>{event.details.documentCount}</span> <span>{event.details.documentCount}</span>
</div> </div>
)} */} )} */}
{event.details.contentLength !== undefined && ( {event.details.contentLength !== undefined && (
<div className="flex space-x-1"> <div className="flex space-x-1">
<span className="font-bold">Content Length:</span> <span className="font-bold whitespace-nowrap">
<span>{event.details.contentLength} chars</span> Content Length:
</span>
<span>{event.details.contentLength} characters</span>
</div> </div>
)} )}
{event.details.searchInstructions !== undefined && ( {event.details.searchInstructions !== undefined && (
<div className="flex space-x-1"> <div className="flex space-x-1">
<span className="font-bold">Search Instructions:</span> <span className="font-bold whitespace-nowrap">
Search Instructions:
</span>
<span>{event.details.searchInstructions}</span> <span>{event.details.searchInstructions}</span>
</div> </div>
)} )}
{event.details.previewCount !== undefined && ( {/* {event.details.previewCount !== undefined && (
<div className="flex space-x-1"> <div className="flex space-x-1">
<span className="font-bold">Preview Sources:</span> <span className="font-bold whitespace-nowrap">Preview Sources:</span>
<span>{event.details.previewCount}</span> <span>{event.details.previewCount}</span>
</div> </div>
)} )} */}
{event.details.processingType && ( {event.details.processingType && (
<div className="flex space-x-1"> <div className="flex space-x-1">
<span className="font-bold">Processing Type:</span> <span className="font-bold whitespace-nowrap">
Processing Type:
</span>
<span className="capitalize"> <span className="capitalize">
{event.details.processingType.replace('-', ' ')} {event.details.processingType.replace('-', ' ')}
</span> </span>
@ -166,51 +199,41 @@ const AgentActionDisplay = ({
)} )}
{event.details.insufficiencyReason && ( {event.details.insufficiencyReason && (
<div className="flex space-x-1"> <div className="flex space-x-1">
<span className="font-bold">Reason:</span> <span className="font-bold whitespace-nowrap">
Reason:
</span>
<span>{event.details.insufficiencyReason}</span> <span>{event.details.insufficiencyReason}</span>
</div> </div>
)} )}
{event.details.reason && ( {event.details.reason && (
<div className="flex space-x-1"> <div className="flex space-x-1">
<span className="font-bold">Reason:</span> <span className="font-bold whitespace-nowrap">
Reason:
</span>
<span>{event.details.reason}</span> <span>{event.details.reason}</span>
</div> </div>
)} )}
{event.details.taskCount !== undefined && ( {/* {event.details.taskCount !== undefined && (
<div className="flex space-x-1"> <div className="flex space-x-1">
<span className="font-bold">Tasks:</span> <span className="font-bold whitespace-nowrap">Tasks:</span>
<span>{event.details.taskCount}</span> <span>{event.details.taskCount}</span>
</div> </div>
)} )} */}
{event.details.currentTask && ( {event.details.currentTask && (
<div className="flex space-x-1"> <div className="flex space-x-1">
<span className="font-bold">Current Task:</span> <span className="font-bold whitespace-nowrap">
Current Task:
</span>
<span className="italic"> <span className="italic">
&quot;{event.details.currentTask}&quot; &quot;{event.details.currentTask}&quot;
</span> </span>
</div> </div>
)} )}
{event.details.taskIndex !== undefined &&
event.details.totalTasks !== undefined && (
<div className="flex space-x-1">
<span className="font-bold">Progress:</span>
<span>
Task {event.details.taskIndex} of{' '}
{event.details.totalTasks}
</span>
</div>
)}
{event.details.completedTask && (
<div className="flex space-x-1">
<span className="font-bold">Completed:</span>
<span className="italic">
&quot;{event.details.completedTask}&quot;
</span>
</div>
)}
{event.details.nextTask && ( {event.details.nextTask && (
<div className="flex space-x-1"> <div className="flex space-x-1">
<span className="font-bold">Next:</span> <span className="font-bold whitespace-nowrap">
Next:
</span>
<span className="italic"> <span className="italic">
&quot;{event.details.nextTask}&quot; &quot;{event.details.nextTask}&quot;
</span> </span>
@ -218,7 +241,9 @@ const AgentActionDisplay = ({
)} )}
{event.details.currentSearchFocus && ( {event.details.currentSearchFocus && (
<div className="flex space-x-1"> <div className="flex space-x-1">
<span className="font-bold">Search Focus:</span> <span className="font-bold whitespace-nowrap">
Search Focus:
</span>
<span className="italic"> <span className="italic">
&quot;{event.details.currentSearchFocus}&quot; &quot;{event.details.currentSearchFocus}&quot;
</span> </span>

View file

@ -93,7 +93,10 @@ const MessageBox = ({
) : ( ) : (
<> <>
<div className="flex items-center"> <div className="flex items-center">
<h2 className="text-black dark:text-white font-medium text-3xl" onClick={startEditMessage}> <h2
className="text-black dark:text-white font-medium text-3xl"
onClick={startEditMessage}
>
{message.content} {message.content}
</h2> </h2>
<button <button

View file

@ -37,10 +37,12 @@ const MessageBoxLoading = ({ progress }: MessageBoxLoadingProps) => {
</div> </div>
</div> </div>
) : ( ) : (
<div className="bg-light-primary dark:bg-dark-primary animate-pulse rounded-lg py-3"> <div className="pl-3 flex items-center justify-start">
<div className="h-2 rounded-full w-full bg-light-secondary dark:bg-dark-secondary" /> <div className="flex space-x-1">
<div className="h-2 mt-2 rounded-full w-9/12 bg-light-secondary dark:bg-dark-secondary" /> <div className="w-1.5 h-1.5 bg-black/40 dark:bg-white/40 rounded-full animate-[high-bounce_1s_infinite] [animation-delay:-0.3s]"></div>
<div className="h-2 mt-2 rounded-full w-10/12 bg-light-secondary dark:bg-dark-secondary" /> <div className="w-1.5 h-1.5 bg-black/40 dark:bg-white/40 rounded-full animate-[high-bounce_1s_infinite] [animation-delay:-0.15s]"></div>
<div className="w-1.5 h-1.5 bg-black/40 dark:bg-white/40 rounded-full animate-[high-bounce_1s_infinite]"></div>
</div>
</div> </div>
)} )}
</div> </div>

View file

@ -25,18 +25,32 @@ import next from 'next';
// Define Zod schemas for structured output // Define Zod schemas for structured output
const NextActionSchema = z.object({ const NextActionSchema = z.object({
action: z.enum(['good_content', 'need_user_info', 'need_more_info']).describe('The next action to take based on content analysis'), action: z
reasoning: z.string().describe('Brief explanation of why this action was chosen') .enum(['good_content', 'need_user_info', 'need_more_info'])
.describe('The next action to take based on content analysis'),
reasoning: z
.string()
.describe('Brief explanation of why this action was chosen'),
}); });
const UserInfoRequestSchema = z.object({ const UserInfoRequestSchema = z.object({
question: z.string().describe('A detailed question to ask the user for additional information'), question: z
reasoning: z.string().describe('Explanation of why this information is needed') .string()
.describe('A detailed question to ask the user for additional information'),
reasoning: z
.string()
.describe('Explanation of why this information is needed'),
}); });
const SearchRefinementSchema = z.object({ const SearchRefinementSchema = z.object({
question: z.string().describe('A refined search question to gather more specific information'), question: z
reasoning: z.string().describe('Explanation of what information is missing and why this search will help') .string()
.describe('A refined search question to gather more specific information'),
reasoning: z
.string()
.describe(
'Explanation of what information is missing and why this search will help',
),
}); });
export class AnalyzerAgent { export class AnalyzerAgent {
@ -120,9 +134,7 @@ export class AnalyzerAgent {
console.log('Next action response:', nextActionResponse); console.log('Next action response:', nextActionResponse);
if ( if (nextActionResponse.action !== 'good_content') {
nextActionResponse.action !== 'good_content'
) {
// If we don't have enough information, but we still have available tasks, proceed with the next task // If we don't have enough information, but we still have available tasks, proceed with the next task
if (state.tasks && state.tasks.length > 0) { if (state.tasks && state.tasks.length > 0) {
@ -135,13 +147,14 @@ export class AnalyzerAgent {
} }
} }
if ( if (nextActionResponse.action === 'need_user_info') {
nextActionResponse.action === 'need_user_info'
) {
// Use structured output for user info request // Use structured output for user info request
const userInfoLlm = this.llm.withStructuredOutput(UserInfoRequestSchema, { const userInfoLlm = this.llm.withStructuredOutput(
name: 'request_user_info', UserInfoRequestSchema,
}); {
name: 'request_user_info',
},
);
const moreUserInfoPrompt = await ChatPromptTemplate.fromTemplate( const moreUserInfoPrompt = await ChatPromptTemplate.fromTemplate(
additionalUserInputPrompt, additionalUserInputPrompt,
@ -193,9 +206,12 @@ export class AnalyzerAgent {
// If we need more information from the LLM, generate a more specific search query // If we need more information from the LLM, generate a more specific search query
// Use structured output for search refinement // Use structured output for search refinement
const searchRefinementLlm = this.llm.withStructuredOutput(SearchRefinementSchema, { const searchRefinementLlm = this.llm.withStructuredOutput(
name: 'refine_search', SearchRefinementSchema,
}); {
name: 'refine_search',
},
);
const moreInfoPrompt = await ChatPromptTemplate.fromTemplate( const moreInfoPrompt = await ChatPromptTemplate.fromTemplate(
additionalWebSearchPrompt, additionalWebSearchPrompt,
@ -268,7 +284,7 @@ export class AnalyzerAgent {
type: 'agent_action', type: 'agent_action',
data: { data: {
action: 'INFORMATION_GATHERING_COMPLETE', action: 'INFORMATION_GATHERING_COMPLETE',
message: 'Sufficient information gathered, ready to respond.', message: 'Ready to respond.',
details: { details: {
documentCount: state.relevantDocuments.length, documentCount: state.relevantDocuments.length,
searchIterations: state.searchInstructionHistory.length, searchIterations: state.searchInstructionHistory.length,

View file

@ -10,8 +10,16 @@ import { setTemperature } from '../utils/modelUtils';
// Define Zod schema for structured task breakdown output // Define Zod schema for structured task breakdown output
const TaskBreakdownSchema = z.object({ const TaskBreakdownSchema = z.object({
tasks: z.array(z.string()).describe('Array of specific, focused tasks broken down from the original query'), tasks: z
reasoning: z.string().describe('Explanation of how and why the query was broken down into these tasks') .array(z.string())
.describe(
'Array of specific, focused tasks broken down from the original query',
),
reasoning: z
.string()
.describe(
'Explanation of how and why the query was broken down into these tasks',
),
}); });
type TaskBreakdown = z.infer<typeof TaskBreakdownSchema>; type TaskBreakdown = z.infer<typeof TaskBreakdownSchema>;
@ -136,7 +144,9 @@ export class TaskManagerAgent {
console.log('Task breakdown response:', taskBreakdownResult); console.log('Task breakdown response:', taskBreakdownResult);
// Extract tasks from structured response // Extract tasks from structured response
const taskLines = taskBreakdownResult.tasks.filter((task) => task.trim().length > 0); const taskLines = taskBreakdownResult.tasks.filter(
(task) => task.trim().length > 0,
);
if (taskLines.length === 0) { if (taskLines.length === 0) {
// Fallback: if no tasks found, use the original query // Fallback: if no tasks found, use the original query

View file

@ -22,8 +22,14 @@ import computeSimilarity from '../utils/computeSimilarity';
// Define Zod schema for structured search query output // Define Zod schema for structured search query output
const SearchQuerySchema = z.object({ const SearchQuerySchema = z.object({
searchQuery: z.string().describe('The optimized search query to use for web search'), searchQuery: z
reasoning: z.string().describe('Explanation of how the search query was optimized for better results') .string()
.describe('The optimized search query to use for web search'),
reasoning: z
.string()
.describe(
'Explanation of how the search query was optimized for better results',
),
}); });
type SearchQuery = z.infer<typeof SearchQuerySchema>; type SearchQuery = z.infer<typeof SearchQuerySchema>;
@ -333,7 +339,7 @@ export class WebSearchAgent {
type: 'agent_action', type: 'agent_action',
data: { data: {
action: 'ANALYZING_SOURCE', action: 'ANALYZING_SOURCE',
message: `Analyzing content from: ${result.title || result.url}`, message: `Analyzing and summarizing content from: ${result.title || result.url}`,
details: { details: {
query: currentTask, query: currentTask,
sourceUrl: result.url, sourceUrl: result.url,

View file

@ -18,8 +18,17 @@ export type PreviewContent = {
// Zod schema for structured preview analysis output // Zod schema for structured preview analysis output
const PreviewAnalysisSchema = z.object({ const PreviewAnalysisSchema = z.object({
isSufficient: z.boolean().describe('Whether the preview content is sufficient to answer the task query'), isSufficient: z
reason: z.string().nullable().describe('Specific reason why full content analysis is required (only if isSufficient is false)') .boolean()
.describe(
'Whether the preview content is sufficient to answer the task query',
),
reason: z
.string()
.nullable()
.describe(
'Specific reason why full content analysis is required (only if isSufficient is false)',
),
}); });
export const analyzePreviewContent = async ( export const analyzePreviewContent = async (
@ -123,9 +132,11 @@ You must return a JSON object with:
console.log( console.log(
`Preview content determined to be insufficient. Reason: ${analysisResult.reason}`, `Preview content determined to be insufficient. Reason: ${analysisResult.reason}`,
); );
return { return {
isSufficient: false, isSufficient: false,
reason: analysisResult.reason || 'Preview content insufficient for complete answer' reason:
analysisResult.reason ||
'Preview content insufficient for complete answer',
}; };
} }
} catch (error) { } catch (error) {

View file

@ -33,7 +33,7 @@ export const summarizeWebContent = async (
console.log( console.log(
`Summarizing content from URL: ${url} using ${i === 0 ? 'html' : 'text'}`, `Summarizing content from URL: ${url} using ${i === 0 ? 'html' : 'text'}`,
); );
const prompt = `${systemPrompt}You are a web content summarizer, tasked with creating a detailed, accurate summary of content from a webpage. const prompt = `${systemPrompt}You are a web content summarizer, tasked with creating a detailed, accurate summary of content from a webpage.
# Instructions # Instructions
@ -82,9 +82,9 @@ ${i === 0 ? content.metadata.html : content.pageContent}`;
`LLM response for URL "${url}" indicates it's not relevant (empty or very short response)`, `LLM response for URL "${url}" indicates it's not relevant (empty or very short response)`,
); );
return { return {
document: null, document: null,
notRelevantReason: 'Content not relevant to query' notRelevantReason: 'Content not relevant to query',
}; };
} }