diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 65acac0..8c4c599 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -149,3 +149,4 @@ When working on this codebase, you might need to: - You can use the context7 tool to get help using the following identifiers for libraries used in this project - `/langchain-ai/langchainjs` for LangChain - `/langchain-ai/langgraph` for LangGraph + - `/quantizor/markdown-to-jsx` for Markdown to JSX conversion diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index 604438e..d65a7a7 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -108,7 +108,7 @@ const handleEmitterEvents = async ( writer.write( encoder.encode( JSON.stringify({ - type: 'message', + type: 'response', data: parsedData.data, messageId: aiMessageId, }) + '\n', @@ -138,20 +138,23 @@ const handleEmitterEvents = async ( ); sources = parsedData.data; + } else if (parsedData.type === 'tool_call') { + // Handle tool call events - stream them directly to the client AND accumulate for database + writer.write( + encoder.encode( + JSON.stringify({ + type: 'tool_call', + data: parsedData.data, + messageId: aiMessageId, + }) + '\n', + ), + ); + + // Add tool call content to the received message for database storage + recievedMessage += parsedData.data.content; } }); - stream.on('agent_action', (data) => { - writer.write( - encoder.encode( - JSON.stringify({ - type: 'agent_action', - data: data.data, - messageId: userMessageId, - }) + '\n', - ), - ); - }); let modelStats: ModelStats = { modelName: '', }; diff --git a/src/components/AgentActionDisplay.tsx b/src/components/AgentActionDisplay.tsx deleted file mode 100644 index ccd0d6e..0000000 --- a/src/components/AgentActionDisplay.tsx +++ /dev/null @@ -1,263 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import { cn } from '@/lib/utils'; -import { - ChevronDown, - ChevronUp, - Bot, - Search, - Zap, - Microscope, - Ban, - CircleCheck, - ListPlus, -} from 'lucide-react'; -import { AgentActionEvent } from './ChatWindow'; - -interface AgentActionDisplayProps { - events: AgentActionEvent[]; - messageId: string; - isLoading: boolean; -} - -const AgentActionDisplay = ({ - events, - messageId, - isLoading, -}: AgentActionDisplayProps) => { - const [isExpanded, setIsExpanded] = useState(false); - - // Get the most recent event for collapsed view - const latestEvent = events[events.length - 1]; - - // Common function to format action names - const formatActionName = (action: string) => { - return action.replace(/_/g, ' ').toLocaleLowerCase(); - }; - - // Function to get appropriate icon based on action type - const getActionIcon = (action: string, size: number = 20) => { - switch (action) { - case 'ANALYZING_PREVIEW_CONTENT': - return ; - case 'PROCESSING_PREVIEW_CONTENT': - return ; - case 'PROCEEDING_WITH_FULL_ANALYSIS': - return ; - case 'SKIPPING_IRRELEVANT_SOURCE': - return ; - case 'CONTEXT_UPDATED': - return ( - - ); - case 'INFORMATION_GATHERING_COMPLETE': - return ( - - ); - default: - return ; - } - }; - - if (!latestEvent) { - return null; - } - - return ( -
- - - {isExpanded && ( -
-
- {events.map((event, index) => ( -
-
- {getActionIcon(event.action, 16)} - - {formatActionName(event.action)} - -
- - {event.message && event.message.length > 0 && ( -

{event.message}

- )} - - {/* Display relevant details based on event type */} - {event.details && Object.keys(event.details).length > 0 && ( -
- {event.details.sourceUrl && ( -
- - Source: - - - - {event.details.sourceUrl} - - -
- )} - {event.details.skipReason && ( -
- - Reason: - - {event.details.skipReason} -
- )} - {event.details.searchQuery && - event.details.searchQuery !== event.details.query && ( -
- - Search Query: - - - "{event.details.searchQuery}" - -
- )} - {event.details.sourcesFound !== undefined && ( -
- - Sources Found: - - {event.details.sourcesFound} -
- )} - {/* {(event.details.documentCount !== undefined && event.details.documentCount > 0) && ( -
- Documents: - {event.details.documentCount} -
- )} */} - {event.details.contentLength !== undefined && ( -
- - Content Length: - - {event.details.contentLength} characters -
- )} - {event.details.searchInstructions !== undefined && ( -
- - Search Instructions: - - {event.details.searchInstructions} -
- )} - {/* {event.details.previewCount !== undefined && ( -
- Preview Sources: - {event.details.previewCount} -
- )} */} - {event.details.processingType && ( -
- - Processing Type: - - - {event.details.processingType.replace('-', ' ')} - -
- )} - {event.details.insufficiencyReason && ( -
- - Reason: - - {event.details.insufficiencyReason} -
- )} - {event.details.reason && ( -
- - Reason: - - {event.details.reason} -
- )} - {/* {event.details.taskCount !== undefined && ( -
- Tasks: - {event.details.taskCount} -
- )} */} - {event.details.currentTask && ( -
- - Current Task: - - - "{event.details.currentTask}" - -
- )} - {event.details.nextTask && ( -
- - Next: - - - "{event.details.nextTask}" - -
- )} - {event.details.currentSearchFocus && ( -
- - Search Focus: - - - "{event.details.currentSearchFocus}" - -
- )} -
- )} -
- ))} -
-
- )} -
- ); -}; - -export default AgentActionDisplay; diff --git a/src/components/Chat.tsx b/src/components/Chat.tsx index b4d3156..d14c013 100644 --- a/src/components/Chat.tsx +++ b/src/components/Chat.tsx @@ -5,7 +5,6 @@ import { File, Message } from './ChatWindow'; import MessageBox from './MessageBox'; import MessageBoxLoading from './MessageBoxLoading'; import MessageInput from './MessageInput'; -import AgentActionDisplay from './AgentActionDisplay'; const Chat = ({ loading, @@ -25,6 +24,7 @@ const Chat = ({ analysisProgress, systemPromptIds, setSystemPromptIds, + onThinkBoxToggle, }: { messages: Message[]; sendMessage: ( @@ -54,6 +54,11 @@ const Chat = ({ } | null; systemPromptIds: string[]; setSystemPromptIds: (ids: string[]) => void; + onThinkBoxToggle: ( + messageId: string, + thinkBoxId: string, + expanded: boolean, + ) => void; }) => { const [isAtBottom, setIsAtBottom] = useState(true); const [manuallyScrolledUp, setManuallyScrolledUp] = useState(false); @@ -224,30 +229,8 @@ const Chat = ({ rewrite={rewrite} sendMessage={sendMessage} handleEditMessage={handleEditMessage} + onThinkBoxToggle={onThinkBoxToggle} /> - {/* Show agent actions after user messages - either completed or in progress */} - {msg.role === 'user' && ( - <> - {/* Show agent actions if they exist */} - {msg.agentActions && msg.agentActions.length > 0 && ( - - )} - {/* Show empty agent action display if this is the last user message and we're loading */} - {loading && - isLast && - (!msg.agentActions || msg.agentActions.length === 0) && ( - - )} - - )} {!isLast && msg.role === 'assistant' && (
)} diff --git a/src/components/ChatWindow.tsx b/src/components/ChatWindow.tsx index 295a409..6184811 100644 --- a/src/components/ChatWindow.tsx +++ b/src/components/ChatWindow.tsx @@ -36,13 +36,13 @@ export type Message = { modelStats?: ModelStats; searchQuery?: string; searchUrl?: string; - agentActions?: AgentActionEvent[]; progress?: { message: string; current: number; total: number; subMessage?: string; }; + expandedThinkBoxes?: Set; }; export interface File { @@ -467,33 +467,6 @@ const ChatWindow = ({ id }: { id?: string }) => { return; } - if (data.type === 'agent_action') { - const agentActionEvent: AgentActionEvent = { - action: data.data.action, - message: data.data.message, - details: data.data.details || {}, - timestamp: new Date(), - }; - - // Update the user message with agent actions - setMessages((prev) => - prev.map((message) => { - if ( - message.messageId === data.messageId && - message.role === 'user' - ) { - const updatedActions = [ - ...(message.agentActions || []), - agentActionEvent, - ]; - return { ...message, agentActions: updatedActions }; - } - return message; - }), - ); - return; - } - if (data.type === 'sources') { sources = data.data; if (!added) { @@ -512,23 +485,68 @@ const ChatWindow = ({ id }: { id?: string }) => { ]); added = true; setScrollTrigger((prev) => prev + 1); + } else { + // set the sources + setMessages((prev) => + prev.map((message) => { + if (message.messageId === data.messageId) { + return { ...message, sources: sources }; + } + return message; + }), + ); } } - if (data.type === 'message') { + if (data.type === 'tool_call') { + // Add the tool content to the current assistant message (already formatted with newlines) + const toolContent = data.data.content; + + if (!added) { + // Create initial message with tool content + setMessages((prevMessages) => [ + ...prevMessages, + { + content: toolContent, + messageId: data.messageId, // Use the AI message ID from the backend + chatId: chatId!, + role: 'assistant', + sources: sources, + createdAt: new Date(), + }, + ]); + added = true; + } else { + // Append tool content to existing message + setMessages((prev) => + prev.map((message) => { + if (message.messageId === data.messageId) { + return { + ...message, + content: message.content + toolContent, + }; + } + return message; + }), + ); + } + + recievedMessage += toolContent; + setScrollTrigger((prev) => prev + 1); + return; + } + + if (data.type === 'response') { if (!added) { setMessages((prevMessages) => [ ...prevMessages, { content: data.data, - messageId: data.messageId, + messageId: data.messageId, // Use the AI message ID from the backend chatId: chatId!, role: 'assistant', sources: sources, createdAt: new Date(), - modelStats: { - modelName: data.modelName, - }, }, ]); added = true; @@ -703,6 +721,27 @@ const ChatWindow = ({ id }: { id?: string }) => { } }; + const handleThinkBoxToggle = ( + messageId: string, + thinkBoxId: string, + expanded: boolean, + ) => { + setMessages((prev) => + prev.map((message) => { + if (message.messageId === messageId) { + const expandedThinkBoxes = new Set(message.expandedThinkBoxes || []); + if (expanded) { + expandedThinkBoxes.add(thinkBoxId); + } else { + expandedThinkBoxes.delete(thinkBoxId); + } + return { ...message, expandedThinkBoxes }; + } + return message; + }), + ); + }; + useEffect(() => { if (isReady && initialMessage && isConfigReady) { // Check if we have an initial query and apply saved search settings @@ -788,6 +827,7 @@ const ChatWindow = ({ id }: { id?: string }) => { analysisProgress={analysisProgress} systemPromptIds={systemPromptIds} setSystemPromptIds={setSystemPromptIds} + onThinkBoxToggle={handleThinkBoxToggle} /> ) : ( diff --git a/src/components/MarkdownRenderer.tsx b/src/components/MarkdownRenderer.tsx index f7fbae0..1d0601e 100644 --- a/src/components/MarkdownRenderer.tsx +++ b/src/components/MarkdownRenderer.tsx @@ -2,7 +2,7 @@ 'use client'; import { cn } from '@/lib/utils'; -import { CheckCheck, Copy as CopyIcon, Brain } from 'lucide-react'; +import { CheckCheck, Copy as CopyIcon, Search, FileText, Globe, Settings } from 'lucide-react'; import Markdown, { MarkdownToJSX } from 'markdown-to-jsx'; import { useState } from 'react'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; @@ -15,13 +15,13 @@ import ThinkBox from './ThinkBox'; // Helper functions for think overlay const extractThinkContent = (content: string): string | null => { - const thinkRegex = /([\s\S]*?)<\/think>/g; + const thinkRegex = /]*>([\s\S]*?)<\/think>/g; const matches = content.match(thinkRegex); if (!matches) return null; // Extract content between think tags and join if multiple const extractedContent = matches - .map((match) => match.replace(/<\/?think>/g, '')) + .map((match) => match.replace(/<\/?think[^>]*>/g, '')) .join('\n\n'); // Return null if content is empty or only whitespace @@ -29,24 +29,150 @@ const extractThinkContent = (content: string): string | null => { }; const removeThinkTags = (content: string): string => { - return content.replace(/[\s\S]*?<\/think>/g, '').trim(); + return content.replace(/]*>[\s\S]*?<\/think>/g, '').trim(); +}; + +// Add stable IDs to think tags if they don't already have them +const addThinkBoxIds = (content: string): string => { + let thinkCounter = 0; + return content.replace(/]*\sid=)/g, () => { + return `; + onThinkBoxToggle?: ( + messageId: string, + thinkBoxId: string, + expanded: boolean, + ) => void; +} + +// Custom ToolCall component for markdown +const ToolCall = ({ + type, + query, + urls, + count, + children, +}: { + type?: string; + query?: string; + urls?: string; + count?: string; + children?: React.ReactNode; +}) => { + const getIcon = (toolType: string) => { + switch (toolType) { + case 'search': + case 'web_search': + return ( + + ); + case 'file': + case 'file_search': + return ( + + ); + case 'url': + case 'url_summarization': + return ( + + ); + default: + return ( + + ); + } + }; + + const formatToolMessage = () => { + if (type === 'search' || type === 'web_search') { + return ( + <> + {getIcon(type)} + Web search: + + {query || children} + + + ); + } + + if (type === 'file' || type === 'file_search') { + return ( + <> + {getIcon(type)} + File search: + + {query || children} + + + ); + } + + if (type === 'url' || type === 'url_summarization') { + const urlCount = count ? parseInt(count) : 1; + return ( + <> + {getIcon(type)} + + Analyzing {urlCount} web page{urlCount === 1 ? '' : 's'} for + additional details + + + ); + } + + // Fallback for unknown tool types + return ( + <> + {getIcon(type || 'default')} + Using tool: + + {type || 'unknown'} + + + ); + }; + + return ( +
+
+ {formatToolMessage()} +
+
+ ); }; const ThinkTagProcessor = ({ children, - isOverlayMode = false, + id, + isExpanded, + onToggle, }: { children: React.ReactNode; - isOverlayMode?: boolean; + id?: string; + isExpanded?: boolean; + onToggle?: (thinkBoxId: string, expanded: boolean) => void; }) => { - // In overlay mode, don't render anything (content will be handled by overlay) - if (isOverlayMode) { - return null; - } - return ; -}; - -const CodeBlock = ({ + return ( + { + if (id && onToggle) { + onToggle(id, !isExpanded); + } + }} + /> + ); +};const CodeBlock = ({ className, children, }: { @@ -115,31 +241,55 @@ const CodeBlock = ({ ); }; -interface MarkdownRendererProps { - content: string; - className?: string; - thinkOverlay?: boolean; -} - const MarkdownRenderer = ({ content, className, - thinkOverlay = false, + showThinking = true, + messageId, + expandedThinkBoxes, + onThinkBoxToggle, }: MarkdownRendererProps) => { - const [showThinkBox, setShowThinkBox] = useState(false); + // Preprocess content to add stable IDs to think tags + const processedContent = addThinkBoxIds(content); - // Extract think content from the markdown - const thinkContent = thinkOverlay ? extractThinkContent(content) : null; - const contentWithoutThink = thinkOverlay ? removeThinkTags(content) : content; + // Check if a think box is expanded + const isThinkBoxExpanded = (thinkBoxId: string) => { + return expandedThinkBoxes?.has(thinkBoxId) || false; + }; + + // Handle think box toggle + const handleThinkBoxToggle = (thinkBoxId: string, expanded: boolean) => { + if (messageId && onThinkBoxToggle) { + onThinkBoxToggle(messageId, thinkBoxId, expanded); + } + }; + + // Determine what content to render based on showThinking parameter + const contentToRender = showThinking + ? processedContent + : removeThinkTags(processedContent); // Markdown formatting options const markdownOverrides: MarkdownToJSX.Options = { overrides: { + ToolCall: { + component: ToolCall, + }, think: { - component: ({ children }) => ( - - {children} - - ), + component: ({ children, id, ...props }) => { + // Use the id from the HTML attribute + const thinkBoxId = id || 'think-unknown'; + const isExpanded = isThinkBoxExpanded(thinkBoxId); + + return ( + + {children} + + ); + }, }, code: { component: ({ className, children }) => { @@ -181,17 +331,6 @@ const MarkdownRenderer = ({ return (
- {/* Think box when expanded - shows above markdown */} - {thinkOverlay && thinkContent && showThinkBox && ( -
- setShowThinkBox(false)} - /> -
- )} - - {thinkOverlay ? contentWithoutThink : content} + {contentToRender} - - {/* Overlay icon when think box is collapsed */} - {thinkOverlay && thinkContent && !showThinkBox && ( - - )}
); }; diff --git a/src/components/MessageBox.tsx b/src/components/MessageBox.tsx index 77b708c..2900d2b 100644 --- a/src/components/MessageBox.tsx +++ b/src/components/MessageBox.tsx @@ -13,6 +13,7 @@ const MessageBox = ({ rewrite, sendMessage, handleEditMessage, + onThinkBoxToggle, }: { message: Message; messageIndex: number; @@ -29,6 +30,11 @@ const MessageBox = ({ }, ) => void; handleEditMessage: (messageId: string, content: string) => void; + onThinkBoxToggle: ( + messageId: string, + thinkBoxId: string, + expanded: boolean, + ) => void; }) => { // Local state for editing functionality const [isEditing, setIsEditing] = useState(false); @@ -123,6 +129,7 @@ const MessageBox = ({ loading={loading} rewrite={rewrite} sendMessage={sendMessage} + onThinkBoxToggle={onThinkBoxToggle} /> )}
diff --git a/src/components/MessageSources.tsx b/src/components/MessageSources.tsx index ce77aff..764cf35 100644 --- a/src/components/MessageSources.tsx +++ b/src/components/MessageSources.tsx @@ -4,74 +4,90 @@ import { File, Zap, Microscope, FileText, Sparkles } from 'lucide-react'; const MessageSources = ({ sources }: { sources: Document[] }) => { return ( -
+
{sources.map((source, i) => ( -

- {source.metadata.title} -

-
-
- {source.metadata.url === 'File' ? ( -
- -
- ) : ( - favicon - )} -

- {source.metadata.url.replace(/.+\/\/|www.|\..+/g, '')} -

-
+ {/* Left side: Favicon/Icon and source number */} +
+ {source.metadata.url === 'File' ? ( +
+ +
+ ) : ( + favicon + )}
-
- {i + 1} + {i + 1} {/* Processing type indicator */} {source.metadata.processingType === 'preview-only' && ( )} {source.metadata.processingType === 'full-content' && ( )} {source.metadata.processingType === 'url-direct-content' && ( )} {source.metadata.processingType === 'url-content-extraction' && ( )}
+ + {/* Right side: Content */} +
+ {/* Title */} +

+ {source.metadata.title} +

+ + {/* URL */} +

+ {source.metadata.url.replace(/.+\/\/|www.|\..+/g, '')} +

+ + {/* Preview content */} +

+ {/* Use snippet for preview-only content, otherwise use pageContent */} + {source.metadata.processingType === 'preview-only' && source.metadata.snippet + ? source.metadata.snippet + : source.pageContent?.length > 250 + ? source.pageContent.slice(0, 250) + '...' + : source.pageContent || 'No preview available' + } +

+
))}
diff --git a/src/components/MessageTabs.tsx b/src/components/MessageTabs.tsx index 56e70f7..63c5abb 100644 --- a/src/components/MessageTabs.tsx +++ b/src/components/MessageTabs.tsx @@ -43,6 +43,11 @@ interface SearchTabsProps { suggestions?: string[]; }, ) => void; + onThinkBoxToggle: ( + messageId: string, + thinkBoxId: string, + expanded: boolean, + ) => void; } const MessageTabs = ({ @@ -54,6 +59,7 @@ const MessageTabs = ({ loading, rewrite, sendMessage, + onThinkBoxToggle, }: SearchTabsProps) => { const [activeTab, setActiveTab] = useState('text'); const [imageCount, setImageCount] = useState(0); @@ -273,9 +279,14 @@ const MessageTabs = ({ {/* Answer Tab */} {activeTab === 'text' && (
- - - {loading && isLast ? null : ( + {loading && isLast ? null : (
diff --git a/src/components/ThinkBox.tsx b/src/components/ThinkBox.tsx index b694459..449bffb 100644 --- a/src/components/ThinkBox.tsx +++ b/src/components/ThinkBox.tsx @@ -27,14 +27,14 @@ const ThinkBox = ({ content, expanded, onToggle }: ThinkBoxProps) => {
+
+
+ + Thinking + + Show thinking tags + + +
+ +
{previewContent ? (
diff --git a/src/components/dashboard/WidgetDisplay.tsx b/src/components/dashboard/WidgetDisplay.tsx index 4410970..feade67 100644 --- a/src/components/dashboard/WidgetDisplay.tsx +++ b/src/components/dashboard/WidgetDisplay.tsx @@ -119,7 +119,7 @@ const WidgetDisplay = ({
) : widget.content ? (
- +
) : (
diff --git a/src/lib/search/agentSearch.ts b/src/lib/search/agentSearch.ts index 9849925..75e1ccf 100644 --- a/src/lib/search/agentSearch.ts +++ b/src/lib/search/agentSearch.ts @@ -47,19 +47,6 @@ export class AgentSearch { ): Promise { console.log('AgentSearch: Using simplified agent implementation'); - // Emit agent action to indicate simplified agent usage - this.emitter.emit( - 'data', - JSON.stringify({ - type: 'agent_action', - data: { - action: 'agent_implementation_selection', - message: 'Using simplified agent implementation (experimental)', - details: `Focus mode: ${this.focusMode}, Files: ${fileIds.length}`, - }, - }), - ); - // Delegate to simplified agent with focus mode await this.simplifiedAgent.searchAndAnswer( query, diff --git a/src/lib/search/simplifiedAgent.ts b/src/lib/search/simplifiedAgent.ts index 433944c..fda1054 100644 --- a/src/lib/search/simplifiedAgent.ts +++ b/src/lib/search/simplifiedAgent.ts @@ -4,6 +4,7 @@ import { BaseMessage, HumanMessage, SystemMessage, + AIMessage, } from '@langchain/core/messages'; import { Embeddings } from '@langchain/core/embeddings'; import { EventEmitter } from 'events'; @@ -17,6 +18,7 @@ import { } from '@/lib/tools/agents'; import { formatDateForLLM } from '../utils'; import { getModelName } from '../utils/modelUtils'; +import { removeThinkingBlocks } from '../utils/contentUtils'; /** * Simplified Agent using createReactAgent @@ -451,19 +453,6 @@ Use all available tools strategically to provide comprehensive, well-researched, // Initialize agent with the provided focus mode and file context const agent = this.initializeAgent(focusMode, fileIds); - // Emit initial agent action - this.emitter.emit( - 'data', - JSON.stringify({ - type: 'agent_action', - data: { - action: 'simplified_agent_start', - message: `Starting simplified agent search in ${focusMode} mode`, - details: `Processing query with ${fileIds.length} files available`, - }, - }), - ); - // Prepare initial state const initialState = { messages: [...history, new HumanMessage(query)], @@ -489,25 +478,165 @@ Use all available tools strategically to provide comprehensive, well-researched, signal: this.signal, }; - // Execute the agent - const result = await agent.invoke(initialState, config); + // Use streamEvents to capture both tool calls and token-level streaming + const eventStream = agent.streamEvents(initialState, { + ...config, + version: 'v2', + }); - // Collect relevant documents from tool execution history + let finalResult: any = null; let collectedDocuments: any[] = []; + let currentResponseBuffer = ''; - // Get the relevant docs from the current agent state - if (result && result.relevantDocuments) { - collectedDocuments.push(...result.relevantDocuments); + // Process the event stream + for await (const event of eventStream) { + // Handle different event types + if ( + event.event === 'on_chain_end' && + event.name === 'RunnableSequence' + ) { + finalResult = event.data.output; + // Collect relevant documents from the final result + if (finalResult && finalResult.relevantDocuments) { + collectedDocuments.push(...finalResult.relevantDocuments); + } + } + + // Collect sources from tool results + if ( + event.event === 'on_chain_end' && + (event.name.includes('search') || + event.name.includes('Search') || + event.name.includes('tool') || + event.name.includes('Tool')) + ) { + // Handle LangGraph state updates with relevantDocuments + if (event.data?.output && Array.isArray(event.data.output)) { + for (const item of event.data.output) { + if ( + item.update && + item.update.relevantDocuments && + Array.isArray(item.update.relevantDocuments) + ) { + collectedDocuments.push(...item.update.relevantDocuments); + } + } + } + } + + // Emit sources as we collect them + if (collectedDocuments.length > 0) { + this.emitter.emit( + 'data', + JSON.stringify({ + type: 'sources', + data: collectedDocuments, + searchQuery: '', + searchUrl: '', + }), + ); + } + + // Handle streaming tool calls (for thought messages) + if (event.event === 'on_chat_model_end' && event.data.output) { + const output = event.data.output; + if ( + output._getType() === 'ai' && + output.tool_calls && + output.tool_calls.length > 0 + ) { + const aiMessage = output as AIMessage; + + // Process each tool call and emit thought messages + for (const toolCall of aiMessage.tool_calls || []) { + if (toolCall && toolCall.name) { + const toolName = toolCall.name; + const toolArgs = toolCall.args || {}; + + // Create user-friendly messages for different tools using markdown components + let toolMarkdown = ''; + switch (toolName) { + case 'web_search': + toolMarkdown = ``; + break; + case 'file_search': + toolMarkdown = ``; + break; + case 'url_summarization': + if (Array.isArray(toolArgs.urls)) { + toolMarkdown = ``; + } else { + toolMarkdown = ``; + } + break; + default: + toolMarkdown = ``; + } + + // Emit the thought message + this.emitter.emit( + 'data', + JSON.stringify({ + type: 'tool_call', + data: { + // messageId: crypto.randomBytes(7).toString('hex'), + content: toolMarkdown, + }, + }), + ); + } + } + } + } + + // Handle token-level streaming for the final response + if (event.event === 'on_chat_model_stream' && event.data.chunk) { + const chunk = event.data.chunk; + if (chunk.content && typeof chunk.content === 'string') { + // If this is the first token, emit sources if we have them + if (currentResponseBuffer === '' && collectedDocuments.length > 0) { + this.emitter.emit( + 'data', + JSON.stringify({ + type: 'sources', + data: collectedDocuments, + searchQuery: '', + searchUrl: '', + }), + ); + } + + // Add the token to our buffer + currentResponseBuffer += chunk.content; + + // Emit the individual token + this.emitter.emit( + 'data', + JSON.stringify({ + type: 'response', + data: chunk.content, + }), + ); + } + } } - // Add collected documents to result for source tracking - const finalResult = { - ...result, - relevantDocuments: collectedDocuments, - }; + // Emit the final sources used for the response + if (collectedDocuments.length > 0) { + this.emitter.emit( + 'data', + JSON.stringify({ + type: 'sources', + data: collectedDocuments, + searchQuery: '', + searchUrl: '', + }), + ); + } - // Extract final message and emit as response + // If we didn't get any streamed tokens but have a final result, emit it if ( + currentResponseBuffer === '' && finalResult && finalResult.messages && finalResult.messages.length > 0 @@ -516,23 +645,7 @@ Use all available tools strategically to provide comprehensive, well-researched, finalResult.messages[finalResult.messages.length - 1]; if (finalMessage && finalMessage.content) { - console.log('SimplifiedAgent: Emitting final response'); - - // Emit the sources used for the response - if ( - finalResult.relevantDocuments && - finalResult.relevantDocuments.length > 0 - ) { - this.emitter.emit( - 'data', - JSON.stringify({ - type: 'sources', - data: finalResult.relevantDocuments, - searchQuery: '', - searchUrl: '', - }), - ); - } + console.log('SimplifiedAgent: Emitting complete response (fallback)'); this.emitter.emit( 'data', @@ -541,23 +654,22 @@ Use all available tools strategically to provide comprehensive, well-researched, data: finalMessage.content, }), ); - } else { - console.warn('SimplifiedAgent: No valid final message found'); - this.emitter.emit( - 'data', - JSON.stringify({ - type: 'response', - data: 'I apologize, but I was unable to generate a complete response to your query. Please try rephrasing your question or providing more specific details.', - }), - ); } - } else { - console.warn('SimplifiedAgent: No result messages found'); + } + + // If we still have no response, emit a fallback message + if ( + currentResponseBuffer === '' && + (!finalResult || + !finalResult.messages || + finalResult.messages.length === 0) + ) { + console.warn('SimplifiedAgent: No valid response found'); this.emitter.emit( 'data', JSON.stringify({ type: 'response', - data: 'I encountered an issue while processing your request. Please try again with a different query.', + data: 'I apologize, but I was unable to generate a complete response to your query. Please try rephrasing your question or providing more specific details.', }), ); } @@ -577,7 +689,7 @@ Use all available tools strategically to provide comprehensive, well-researched, console.error('SimplifiedAgent: Error during search and answer:', error); // Handle specific error types - if (error.name === 'AbortError') { + if (error.name === 'AbortError' || this.signal.aborted) { console.warn('SimplifiedAgent: Operation was aborted'); this.emitter.emit( 'data',