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;
}
}
@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,
Zap,
Microscope,
Ban,
CircleCheck,
ListPlus,
} from 'lucide-react';
import { AgentActionEvent } from './ChatWindow';
@ -42,6 +45,22 @@ const AgentActionDisplay = ({
return <Zap size={size} className="text-[#9C27B0]" />;
case 'PROCEEDING_WITH_FULL_ANALYSIS':
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:
return <Bot size={size} className="text-[#9C27B0]" />;
}
@ -64,12 +83,12 @@ const AgentActionDisplay = ({
latestEvent.action === 'INFORMATION_GATHERING_COMPLETE'
? 'Agent Log'
: formatActionName(latestEvent.action)}
{isLoading &&
{/* {isLoading &&
latestEvent.action !== 'INFORMATION_GATHERING_COMPLETE' && (
<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>
)}
)} */}
</span>
</div>
{isExpanded ? (
@ -103,7 +122,9 @@ const AgentActionDisplay = ({
<div className="mt-2 text-sm text-black/60 dark:text-white/60">
{event.details.sourceUrl && (
<div className="flex space-x-1">
<span className="font-bold">Source:</span>
<span className="font-bold whitespace-nowrap">
Source:
</span>
<span className="truncate">
<a href={event.details.sourceUrl} target="_blank">
{event.details.sourceUrl}
@ -113,14 +134,18 @@ const AgentActionDisplay = ({
)}
{event.details.skipReason && (
<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>
</div>
)}
{event.details.searchQuery &&
event.details.searchQuery !== event.details.query && (
<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">
&quot;{event.details.searchQuery}&quot;
</span>
@ -128,37 +153,45 @@ const AgentActionDisplay = ({
)}
{event.details.sourcesFound !== undefined && (
<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>
</div>
)}
{/* {(event.details.documentCount !== undefined && event.details.documentCount > 0) && (
<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>
</div>
)} */}
{event.details.contentLength !== undefined && (
<div className="flex space-x-1">
<span className="font-bold">Content Length:</span>
<span>{event.details.contentLength} chars</span>
<span className="font-bold whitespace-nowrap">
Content Length:
</span>
<span>{event.details.contentLength} characters</span>
</div>
)}
{event.details.searchInstructions !== undefined && (
<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>
</div>
)}
{event.details.previewCount !== undefined && (
{/* {event.details.previewCount !== undefined && (
<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>
</div>
)}
)} */}
{event.details.processingType && (
<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">
{event.details.processingType.replace('-', ' ')}
</span>
@ -166,51 +199,41 @@ const AgentActionDisplay = ({
)}
{event.details.insufficiencyReason && (
<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>
</div>
)}
{event.details.reason && (
<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>
</div>
)}
{event.details.taskCount !== undefined && (
{/* {event.details.taskCount !== undefined && (
<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>
</div>
)}
)} */}
{event.details.currentTask && (
<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">
&quot;{event.details.currentTask}&quot;
</span>
</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 && (
<div className="flex space-x-1">
<span className="font-bold">Next:</span>
<span className="font-bold whitespace-nowrap">
Next:
</span>
<span className="italic">
&quot;{event.details.nextTask}&quot;
</span>
@ -218,7 +241,9 @@ const AgentActionDisplay = ({
)}
{event.details.currentSearchFocus && (
<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">
&quot;{event.details.currentSearchFocus}&quot;
</span>

View file

@ -93,7 +93,10 @@ const MessageBox = ({
) : (
<>
<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}
</h2>
<button

View file

@ -37,10 +37,12 @@ const MessageBoxLoading = ({ progress }: MessageBoxLoadingProps) => {
</div>
</div>
) : (
<div className="bg-light-primary dark:bg-dark-primary animate-pulse rounded-lg py-3">
<div className="h-2 rounded-full w-full bg-light-secondary dark:bg-dark-secondary" />
<div className="h-2 mt-2 rounded-full w-9/12 bg-light-secondary dark:bg-dark-secondary" />
<div className="h-2 mt-2 rounded-full w-10/12 bg-light-secondary dark:bg-dark-secondary" />
<div className="pl-3 flex items-center justify-start">
<div className="flex space-x-1">
<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="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>

View file

@ -25,18 +25,32 @@ import next from 'next';
// Define Zod schemas for structured output
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'),
reasoning: z.string().describe('Brief explanation of why this action was chosen')
action: z
.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({
question: z.string().describe('A detailed question to ask the user for additional information'),
reasoning: z.string().describe('Explanation of why this information is needed')
question: z
.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({
question: z.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')
question: z
.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 {
@ -120,9 +134,7 @@ export class AnalyzerAgent {
console.log('Next action response:', nextActionResponse);
if (
nextActionResponse.action !== 'good_content'
) {
if (nextActionResponse.action !== 'good_content') {
// 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) {
@ -135,13 +147,14 @@ export class AnalyzerAgent {
}
}
if (
nextActionResponse.action === 'need_user_info'
) {
if (nextActionResponse.action === 'need_user_info') {
// Use structured output for user info request
const userInfoLlm = this.llm.withStructuredOutput(UserInfoRequestSchema, {
name: 'request_user_info',
});
const userInfoLlm = this.llm.withStructuredOutput(
UserInfoRequestSchema,
{
name: 'request_user_info',
},
);
const moreUserInfoPrompt = await ChatPromptTemplate.fromTemplate(
additionalUserInputPrompt,
@ -193,9 +206,12 @@ export class AnalyzerAgent {
// If we need more information from the LLM, generate a more specific search query
// Use structured output for search refinement
const searchRefinementLlm = this.llm.withStructuredOutput(SearchRefinementSchema, {
name: 'refine_search',
});
const searchRefinementLlm = this.llm.withStructuredOutput(
SearchRefinementSchema,
{
name: 'refine_search',
},
);
const moreInfoPrompt = await ChatPromptTemplate.fromTemplate(
additionalWebSearchPrompt,
@ -268,7 +284,7 @@ export class AnalyzerAgent {
type: 'agent_action',
data: {
action: 'INFORMATION_GATHERING_COMPLETE',
message: 'Sufficient information gathered, ready to respond.',
message: 'Ready to respond.',
details: {
documentCount: state.relevantDocuments.length,
searchIterations: state.searchInstructionHistory.length,

View file

@ -10,8 +10,16 @@ import { setTemperature } from '../utils/modelUtils';
// Define Zod schema for structured task breakdown output
const TaskBreakdownSchema = z.object({
tasks: z.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')
tasks: z
.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>;
@ -136,7 +144,9 @@ export class TaskManagerAgent {
console.log('Task breakdown response:', taskBreakdownResult);
// 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) {
// 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
const SearchQuerySchema = z.object({
searchQuery: z.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')
searchQuery: z
.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>;
@ -333,7 +339,7 @@ export class WebSearchAgent {
type: 'agent_action',
data: {
action: 'ANALYZING_SOURCE',
message: `Analyzing content from: ${result.title || result.url}`,
message: `Analyzing and summarizing content from: ${result.title || result.url}`,
details: {
query: currentTask,
sourceUrl: result.url,

View file

@ -18,8 +18,17 @@ export type PreviewContent = {
// Zod schema for structured preview analysis output
const PreviewAnalysisSchema = z.object({
isSufficient: z.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)')
isSufficient: z
.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 (
@ -123,9 +132,11 @@ You must return a JSON object with:
console.log(
`Preview content determined to be insufficient. Reason: ${analysisResult.reason}`,
);
return {
isSufficient: false,
reason: analysisResult.reason || 'Preview content insufficient for complete answer'
return {
isSufficient: false,
reason:
analysisResult.reason ||
'Preview content insufficient for complete answer',
};
}
} catch (error) {

View file

@ -33,7 +33,7 @@ export const summarizeWebContent = async (
console.log(
`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.
# 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)`,
);
return {
document: null,
notRelevantReason: 'Content not relevant to query'
return {
document: null,
notRelevantReason: 'Content not relevant to query',
};
}