optimized ChatBox component.
This commit is contained in:
@@ -18,6 +18,7 @@ DEPLOYMENT.md
|
|||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
.idea
|
.idea
|
||||||
|
.claude
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.suo
|
*.suo
|
||||||
*.ntvs*
|
*.ntvs*
|
||||||
|
|||||||
+48
-263
@@ -8,23 +8,18 @@ import { GeminiProvider } from '../agent/llm/GeminiProvider';
|
|||||||
import { LLMProvider } from '../agent/llm/LLMProvider';
|
import { LLMProvider } from '../agent/llm/LLMProvider';
|
||||||
import { ConfigManager } from '../core/config/ConfigManager';
|
import { ConfigManager } from '../core/config/ConfigManager';
|
||||||
import { useProjectStore } from '../stores/projectStore';
|
import { useProjectStore } from '../stores/projectStore';
|
||||||
import { XMLToolExecutor } from '../agent/core/XMLToolExecutor';
|
|
||||||
import { extractXMLFromString } from '../util/xmlUtil';
|
|
||||||
import { SystemPrompts } from '../agent/core/SystemPrompts';
|
import { SystemPrompts } from '../agent/core/SystemPrompts';
|
||||||
import { clearChatHistoryAndUI, registerClearChatUICallback } from '../util/chatUtil';
|
import { clearChatHistoryAndUI, registerClearChatUICallback } from '../util/chatUtil';
|
||||||
import { processUserMessage } from '../util/messageFilter/UserMessageFilter';
|
import { processUserMessage } from '../util/messageFilter/UserMessageFilter';
|
||||||
|
import { useStreamProcessor } from '../hooks/useStreamProcessor';
|
||||||
|
import { createMessage, addWelcomeMessage } from '../utils/chatMessageUtils';
|
||||||
|
import { extractActionableTools, executeAllTools } from '../utils/toolExecutionUtils';
|
||||||
|
|
||||||
|
import type { ChatMessage } from '../types/projectTypes';
|
||||||
|
|
||||||
// Module-level guard to avoid duplicate welcome in React StrictMode dev remounts
|
// Module-level guard to avoid duplicate welcome in React StrictMode dev remounts
|
||||||
let hasShownWelcomeOnceInRuntime = false;
|
let hasShownWelcomeOnceInRuntime = false;
|
||||||
|
|
||||||
interface ChatMessage {
|
|
||||||
id: string;
|
|
||||||
role: 'user' | 'assistant';
|
|
||||||
content: string;
|
|
||||||
isStreaming?: boolean;
|
|
||||||
tokenCount?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create the appropriate LLM provider based on configuration
|
* Create the appropriate LLM provider based on configuration
|
||||||
*/
|
*/
|
||||||
@@ -55,7 +50,6 @@ const ChatBox: React.FC<ChatBoxProps> = ({ isVisible }) => {
|
|||||||
// Initialize with empty messages
|
// Initialize with empty messages
|
||||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||||
const [isProcessing, setIsProcessing] = useState(false);
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
const [abortController, setAbortController] = useState<AbortController | null>(null);
|
|
||||||
const [lastUserMessage, setLastUserMessage] = useState<string>('');
|
const [lastUserMessage, setLastUserMessage] = useState<string>('');
|
||||||
|
|
||||||
// Tool execution state
|
// Tool execution state
|
||||||
@@ -66,9 +60,25 @@ const ChatBox: React.FC<ChatBoxProps> = ({ isVisible }) => {
|
|||||||
// Track if this is the first message (for system prompt logging)
|
// Track if this is the first message (for system prompt logging)
|
||||||
const [isFirstMessage, setIsFirstMessage] = useState(true);
|
const [isFirstMessage, setIsFirstMessage] = useState(true);
|
||||||
|
|
||||||
const generateMessageId = (): string => {
|
// Message update callbacks for stream processor
|
||||||
return `msg_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
|
const handleMessageUpdate = useCallback((messageId: string, updater: (msg: ChatMessage) => ChatMessage) => {
|
||||||
};
|
setMessages(prev => prev.map(msg => msg.id === messageId ? updater(msg) : msg));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleMessageAdd = useCallback((message: ChatMessage) => {
|
||||||
|
setMessages(prev => [...prev, message]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleProcessingChange = useCallback((processing: boolean) => {
|
||||||
|
setIsProcessing(processing);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Stream processor hook
|
||||||
|
const streamProcessor = useStreamProcessor({
|
||||||
|
onMessageUpdate: handleMessageUpdate,
|
||||||
|
onMessageAdd: handleMessageAdd,
|
||||||
|
onProcessingChange: handleProcessingChange
|
||||||
|
});
|
||||||
|
|
||||||
const clearChatUI = useCallback(async () => {
|
const clearChatUI = useCallback(async () => {
|
||||||
// Clear UI state
|
// Clear UI state
|
||||||
@@ -78,18 +88,9 @@ const ChatBox: React.FC<ChatBoxProps> = ({ isVisible }) => {
|
|||||||
setIsFirstMessage(true);
|
setIsFirstMessage(true);
|
||||||
|
|
||||||
// Auto-show welcome message after clearing (like on app startup)
|
// Auto-show welcome message after clearing (like on app startup)
|
||||||
try {
|
const welcomeMessage = await addWelcomeMessage();
|
||||||
const result = await processUserMessage('/welcome');
|
if (welcomeMessage) {
|
||||||
if (result.pseudoAssistantResponse) {
|
setMessages([welcomeMessage]);
|
||||||
const pseudoId = generateMessageId();
|
|
||||||
setMessages(prev => [...prev, {
|
|
||||||
id: pseudoId,
|
|
||||||
role: 'assistant',
|
|
||||||
content: result.pseudoAssistantResponse!,
|
|
||||||
}]);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// ignore errors, just don't show welcome if it fails
|
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -117,29 +118,20 @@ const ChatBox: React.FC<ChatBoxProps> = ({ isVisible }) => {
|
|||||||
|
|
||||||
// Auto-trigger welcome on first launch (guard against React StrictMode double-invoke only)
|
// Auto-trigger welcome on first launch (guard against React StrictMode double-invoke only)
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
|
||||||
if (hasShownWelcomeOnceInRuntime) return;
|
if (hasShownWelcomeOnceInRuntime) return;
|
||||||
hasShownWelcomeOnceInRuntime = true;
|
hasShownWelcomeOnceInRuntime = true;
|
||||||
|
|
||||||
const result = await processUserMessage('/welcome');
|
const welcomeMessage = await addWelcomeMessage();
|
||||||
if (result.pseudoAssistantResponse) {
|
if (welcomeMessage) {
|
||||||
const pseudoId = generateMessageId();
|
setMessages([welcomeMessage]);
|
||||||
setMessages(prev => [...prev, {
|
|
||||||
id: pseudoId,
|
|
||||||
role: 'assistant',
|
|
||||||
content: result.pseudoAssistantResponse!,
|
|
||||||
}]);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [clearChatUI]);
|
}, [clearChatUI]);
|
||||||
|
|
||||||
const handleAbort = () => {
|
const handleAbort = () => {
|
||||||
if (abortController) {
|
const controller = streamProcessor.abortController;
|
||||||
abortController.abort();
|
if (controller) {
|
||||||
setAbortController(null);
|
controller.abort();
|
||||||
|
|
||||||
// Use AgentCore to clean up the data model and get the user message content
|
// Use AgentCore to clean up the data model and get the user message content
|
||||||
const agentCore = AgentCore.instance();
|
const agentCore = AgentCore.instance();
|
||||||
@@ -160,28 +152,9 @@ const ChatBox: React.FC<ChatBoxProps> = ({ isVisible }) => {
|
|||||||
clearChatHistoryAndUI(setStatus);
|
clearChatHistoryAndUI(setStatus);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const addToolResultMessage = (toolName: string, success: boolean, result: string) => {
|
|
||||||
const toolMsgId = generateMessageId();
|
|
||||||
const friendlyDisplay = `${success ? '✅' : '❌'} __**${toolName}**__ \n\n └── ${result}`;
|
|
||||||
|
|
||||||
setMessages(prev => [...prev, {
|
|
||||||
id: toolMsgId,
|
|
||||||
role: 'user',
|
|
||||||
content: friendlyDisplay
|
|
||||||
}]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const executeToolsFromResponse = async (response: string): Promise<boolean> => {
|
const executeToolsFromResponse = async (response: string): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
// Check if response contains XML tool invocations
|
const actionableBlocks = extractActionableTools(response);
|
||||||
const xmlBlocks = extractXMLFromString(response);
|
|
||||||
// Consider only actionable tools (exclude think/thinking)
|
|
||||||
const actionableBlocks = xmlBlocks.filter((block) => {
|
|
||||||
const match = block.match(/<([a-zA-Z_][a-zA-Z0-9_-]*)/);
|
|
||||||
const name = match ? match[1].toLowerCase() : '';
|
|
||||||
return name !== 'think' && name !== 'thinking';
|
|
||||||
});
|
|
||||||
|
|
||||||
if (actionableBlocks.length === 0) {
|
if (actionableBlocks.length === 0) {
|
||||||
// No actionable tools to execute, stop the loop
|
// No actionable tools to execute, stop the loop
|
||||||
@@ -194,46 +167,12 @@ const ChatBox: React.FC<ChatBoxProps> = ({ isVisible }) => {
|
|||||||
setCurrentToolIndex(0);
|
setCurrentToolIndex(0);
|
||||||
|
|
||||||
const { setStatus } = useProjectStore.getState();
|
const { setStatus } = useProjectStore.getState();
|
||||||
setStatus(`Executing ${actionableBlocks.length} tool(s)...`);
|
|
||||||
|
|
||||||
const executor = XMLToolExecutor.instance();
|
// Execute all tools and get accumulated results
|
||||||
let accumulatedResults = '';
|
const accumulatedResults = await executeAllTools(actionableBlocks, {
|
||||||
|
onMessageAdd: handleMessageAdd,
|
||||||
// Execute tools sequentially with real-time updates
|
onStatusUpdate: setStatus
|
||||||
for (let i = 0; i < actionableBlocks.length; i++) {
|
});
|
||||||
setCurrentToolIndex(i + 1);
|
|
||||||
setStatus(`Executing tool ${i + 1} of ${actionableBlocks.length}...`);
|
|
||||||
|
|
||||||
// Determine tool name from XML block
|
|
||||||
const toolNameMatch = actionableBlocks[i].match(/<([a-zA-Z_][a-zA-Z0-9_-]*)/);
|
|
||||||
const toolName = toolNameMatch ? toolNameMatch[1] : 'unknown_tool';
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Execute single XML block
|
|
||||||
const results = await executor.executeXMLTools(actionableBlocks[i]);
|
|
||||||
const result = results[0]; // Single block should give single result
|
|
||||||
|
|
||||||
if (result) {
|
|
||||||
// Add friendly display message
|
|
||||||
addToolResultMessage(toolName, result.success, result.result);
|
|
||||||
|
|
||||||
// Accumulate formatted result for LLM (skip thinking tools)
|
|
||||||
if (toolName !== 'thinking' && toolName !== 'think') {
|
|
||||||
const formattedResult = `tool: ${toolName}\nsuccess: ${result.success}\nresult:\n${result.result}\n------------\n`;
|
|
||||||
accumulatedResults += formattedResult;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Handle individual tool error
|
|
||||||
addToolResultMessage(toolName, false, `Tool execution failed: ${error}`);
|
|
||||||
|
|
||||||
// Accumulate error result for LLM (skip thinking tools)
|
|
||||||
if (toolName !== 'thinking' && toolName !== 'think') {
|
|
||||||
const formattedResult = `tool: ${toolName}\nsuccess: false\nresult:\nTool execution failed: ${error}\n------------\n`;
|
|
||||||
accumulatedResults += formattedResult;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store accumulated results
|
// Store accumulated results
|
||||||
setToolResults(accumulatedResults);
|
setToolResults(accumulatedResults);
|
||||||
@@ -264,61 +203,8 @@ const ChatBox: React.FC<ChatBoxProps> = ({ isVisible }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const sendToolResultsToLLM = async (toolResultsString: string): Promise<void> => {
|
const sendToolResultsToLLM = async (toolResultsString: string): Promise<void> => {
|
||||||
// Send tool results as hidden user input to LLM
|
// Process tool results through the stream processor
|
||||||
setIsProcessing(true);
|
const assistantResponse = await streamProcessor.processStream(toolResultsString, 'TOOL_RESULTS');
|
||||||
|
|
||||||
// Create abort controller for this request
|
|
||||||
const controller = new AbortController();
|
|
||||||
setAbortController(controller);
|
|
||||||
|
|
||||||
// Add streaming assistant message for the response
|
|
||||||
const assistantMsgId = generateMessageId();
|
|
||||||
setMessages(prev => [...prev, {
|
|
||||||
id: assistantMsgId,
|
|
||||||
role: 'assistant',
|
|
||||||
content: '<span class="processing-wave">Processing...</span> 0 tokens received. click here to abort.',
|
|
||||||
isStreaming: true,
|
|
||||||
tokenCount: 0
|
|
||||||
}]);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const agentCore = AgentCore.instance();
|
|
||||||
let assistantResponse = '';
|
|
||||||
let tokenCount = 0;
|
|
||||||
|
|
||||||
// Log the tool results being sent to LLM
|
|
||||||
console.log('------------ USER ------------');
|
|
||||||
console.log(toolResultsString);
|
|
||||||
console.log('------------------------------');
|
|
||||||
|
|
||||||
for await (const chunk of agentCore.processUserInput(toolResultsString)) {
|
|
||||||
// Check if request was aborted
|
|
||||||
if (controller.signal.aborted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (chunk.type === 'text') {
|
|
||||||
assistantResponse += chunk.content;
|
|
||||||
tokenCount++;
|
|
||||||
|
|
||||||
// Update streaming message with token count and abort link
|
|
||||||
setMessages(prev => prev.map(msg =>
|
|
||||||
msg.id === assistantMsgId
|
|
||||||
? { ...msg, content: `<span class="processing-wave">Processing...</span> ${tokenCount} tokens received. click here to abort.`, tokenCount }
|
|
||||||
: msg
|
|
||||||
));
|
|
||||||
} else if (chunk.type === 'done') {
|
|
||||||
// Replace with final response
|
|
||||||
setMessages(prev => prev.map(msg =>
|
|
||||||
msg.id === assistantMsgId
|
|
||||||
? { ...msg, content: assistantResponse, isStreaming: false, tokenCount: undefined }
|
|
||||||
: msg
|
|
||||||
));
|
|
||||||
|
|
||||||
// Log the complete assistant response
|
|
||||||
console.log('------------ ASSISTANT ------------');
|
|
||||||
console.log(assistantResponse);
|
|
||||||
console.log('-----------------------------------');
|
|
||||||
|
|
||||||
// Check if the new response contains more tools
|
// Check if the new response contains more tools
|
||||||
const hasMoreTools = await executeToolsFromResponse(assistantResponse);
|
const hasMoreTools = await executeToolsFromResponse(assistantResponse);
|
||||||
@@ -328,27 +214,6 @@ const ChatBox: React.FC<ChatBoxProps> = ({ isVisible }) => {
|
|||||||
const agentCore = AgentCore.instance();
|
const agentCore = AgentCore.instance();
|
||||||
agentCore.getAgentState().setIsWorkingOnTask(false);
|
agentCore.getAgentState().setIsWorkingOnTask(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error && error.name === 'AbortError') {
|
|
||||||
// Request was aborted, don't show error
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error('Error processing tool results:', error);
|
|
||||||
// Update with error message
|
|
||||||
setMessages(prev => prev.map(msg =>
|
|
||||||
msg.id === assistantMsgId
|
|
||||||
? { ...msg, content: 'Error: Failed to process tool results', isStreaming: false, tokenCount: undefined }
|
|
||||||
: msg
|
|
||||||
));
|
|
||||||
} finally {
|
|
||||||
setAbortController(null);
|
|
||||||
setIsProcessing(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSend = async () => {
|
const handleSend = async () => {
|
||||||
@@ -362,22 +227,14 @@ const ChatBox: React.FC<ChatBoxProps> = ({ isVisible }) => {
|
|||||||
|
|
||||||
// Conditionally show the user message bubble
|
// Conditionally show the user message bubble
|
||||||
if (filterResult.displayUserMessage) {
|
if (filterResult.displayUserMessage) {
|
||||||
const userMsgId = generateMessageId();
|
const userMsgObject = createMessage('user', userMessage);
|
||||||
setMessages(prev => [...prev, {
|
handleMessageAdd(userMsgObject);
|
||||||
id: userMsgId,
|
|
||||||
role: 'user',
|
|
||||||
content: userMessage
|
|
||||||
}]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have a pseudo assistant response, show it immediately
|
// If we have a pseudo assistant response, show it immediately
|
||||||
if (filterResult.pseudoAssistantResponse) {
|
if (filterResult.pseudoAssistantResponse) {
|
||||||
const pseudoId = generateMessageId();
|
const pseudoMessage = createMessage('assistant', filterResult.pseudoAssistantResponse);
|
||||||
setMessages(prev => [...prev, {
|
handleMessageAdd(pseudoMessage);
|
||||||
id: pseudoId,
|
|
||||||
role: 'assistant',
|
|
||||||
content: filterResult.pseudoAssistantResponse!,
|
|
||||||
}]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we shouldn't send anything to LLM, stop here
|
// If we shouldn't send anything to LLM, stop here
|
||||||
@@ -385,28 +242,8 @@ const ChatBox: React.FC<ChatBoxProps> = ({ isVisible }) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsProcessing(true);
|
|
||||||
|
|
||||||
// Create abort controller for this request
|
|
||||||
const controller = new AbortController();
|
|
||||||
setAbortController(controller);
|
|
||||||
|
|
||||||
// Add streaming assistant message
|
|
||||||
const assistantMsgId = generateMessageId();
|
|
||||||
setMessages(prev => [...prev, {
|
|
||||||
id: assistantMsgId,
|
|
||||||
role: 'assistant',
|
|
||||||
content: '<span class="processing-wave">Processing...</span> 0 tokens received. click here to abort.',
|
|
||||||
isStreaming: true,
|
|
||||||
tokenCount: 0
|
|
||||||
}]);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const agentCore = AgentCore.instance();
|
|
||||||
let assistantResponse = '';
|
|
||||||
let tokenCount = 0;
|
|
||||||
|
|
||||||
// Set working on task flag when user sends a message
|
// Set working on task flag when user sends a message
|
||||||
|
const agentCore = AgentCore.instance();
|
||||||
agentCore.getAgentState().setIsWorkingOnTask(true);
|
agentCore.getAgentState().setIsWorkingOnTask(true);
|
||||||
|
|
||||||
// Log system prompt only for first message or first message after clear
|
// Log system prompt only for first message or first message after clear
|
||||||
@@ -423,39 +260,8 @@ const ChatBox: React.FC<ChatBoxProps> = ({ isVisible }) => {
|
|||||||
setIsFirstMessage(false);
|
setIsFirstMessage(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log the final user message being sent to LLM
|
// Process user input through the stream processor
|
||||||
console.log('------------ USER ------------');
|
const assistantResponse = await streamProcessor.processStream(filterResult.finalMessageForLLM, 'USER');
|
||||||
console.log(filterResult.finalMessageForLLM);
|
|
||||||
console.log('------------------------------');
|
|
||||||
|
|
||||||
for await (const chunk of agentCore.processUserInput(filterResult.finalMessageForLLM)) {
|
|
||||||
// Check if request was aborted
|
|
||||||
if (controller.signal.aborted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (chunk.type === 'text') {
|
|
||||||
assistantResponse += chunk.content;
|
|
||||||
tokenCount++;
|
|
||||||
|
|
||||||
// Update streaming message with token count and abort link
|
|
||||||
setMessages(prev => prev.map(msg =>
|
|
||||||
msg.id === assistantMsgId
|
|
||||||
? { ...msg, content: `<span class="processing-wave">Processing...</span> ${tokenCount} tokens received. click here to abort.`, tokenCount }
|
|
||||||
: msg
|
|
||||||
));
|
|
||||||
} else if (chunk.type === 'done') {
|
|
||||||
// Replace with final response
|
|
||||||
setMessages(prev => prev.map(msg =>
|
|
||||||
msg.id === assistantMsgId
|
|
||||||
? { ...msg, content: assistantResponse, isStreaming: false, tokenCount: undefined }
|
|
||||||
: msg
|
|
||||||
));
|
|
||||||
|
|
||||||
// Log the complete assistant response
|
|
||||||
console.log('------------ ASSISTANT ------------');
|
|
||||||
console.log(assistantResponse);
|
|
||||||
console.log('-----------------------------------');
|
|
||||||
|
|
||||||
// Check if response contains tools to execute
|
// Check if response contains tools to execute
|
||||||
const hasTools = await executeToolsFromResponse(assistantResponse);
|
const hasTools = await executeToolsFromResponse(assistantResponse);
|
||||||
@@ -464,27 +270,6 @@ const ChatBox: React.FC<ChatBoxProps> = ({ isVisible }) => {
|
|||||||
if (!hasTools) {
|
if (!hasTools) {
|
||||||
agentCore.getAgentState().setIsWorkingOnTask(false);
|
agentCore.getAgentState().setIsWorkingOnTask(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error && error.name === 'AbortError') {
|
|
||||||
// Request was aborted, don't show error
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error('Error processing message:', error);
|
|
||||||
// Update with error message
|
|
||||||
setMessages(prev => prev.map(msg =>
|
|
||||||
msg.id === assistantMsgId
|
|
||||||
? { ...msg, content: 'Error: Failed to process message', isStreaming: false, tokenCount: undefined }
|
|
||||||
: msg
|
|
||||||
));
|
|
||||||
} finally {
|
|
||||||
setAbortController(null);
|
|
||||||
setIsProcessing(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import { AgentCore } from '../agent/core/AgentCore';
|
||||||
|
import { createStreamingMessage } from '../utils/chatMessageUtils';
|
||||||
|
import type { ChatMessage } from '../types/projectTypes';
|
||||||
|
|
||||||
|
interface StreamProcessorOptions {
|
||||||
|
onMessageUpdate: (messageId: string, updater: (msg: ChatMessage) => ChatMessage) => void;
|
||||||
|
onMessageAdd: (message: ChatMessage) => void;
|
||||||
|
onProcessingChange: (isProcessing: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StreamProcessorResult {
|
||||||
|
processStream: (input: string, logPrefix?: string) => Promise<string>;
|
||||||
|
abortController: AbortController | null;
|
||||||
|
isProcessing: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useStreamProcessor = (options: StreamProcessorOptions): StreamProcessorResult => {
|
||||||
|
const { onMessageUpdate, onMessageAdd, onProcessingChange } = options;
|
||||||
|
const [abortController, setAbortController] = useState<AbortController | null>(null);
|
||||||
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
|
|
||||||
|
const processStream = useCallback(async (input: string, logPrefix: string = 'USER'): Promise<string> => {
|
||||||
|
setIsProcessing(true);
|
||||||
|
onProcessingChange(true);
|
||||||
|
|
||||||
|
// Create abort controller for this request
|
||||||
|
const controller = new AbortController();
|
||||||
|
setAbortController(controller);
|
||||||
|
|
||||||
|
// Add streaming assistant message
|
||||||
|
const streamingMessage = createStreamingMessage();
|
||||||
|
onMessageAdd(streamingMessage);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const agentCore = AgentCore.instance();
|
||||||
|
let assistantResponse = '';
|
||||||
|
let tokenCount = 0;
|
||||||
|
let streamCompleted = false;
|
||||||
|
|
||||||
|
// Log the input being sent to LLM
|
||||||
|
console.log(`------------ ${logPrefix} ------------`);
|
||||||
|
console.log(input);
|
||||||
|
console.log('------------------------------');
|
||||||
|
|
||||||
|
for await (const chunk of agentCore.processUserInput(input)) {
|
||||||
|
// Check if request was aborted
|
||||||
|
if (controller.signal.aborted) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chunk.type === 'text') {
|
||||||
|
assistantResponse += chunk.content;
|
||||||
|
tokenCount++;
|
||||||
|
|
||||||
|
// Update streaming message with token count and abort link
|
||||||
|
onMessageUpdate(streamingMessage.id, (msg) => ({
|
||||||
|
...msg,
|
||||||
|
content: `<span class="processing-wave">Processing...</span> ${tokenCount} tokens received. click here to abort.`,
|
||||||
|
tokenCount
|
||||||
|
}));
|
||||||
|
} else if (chunk.type === 'done') {
|
||||||
|
streamCompleted = true;
|
||||||
|
// Replace with final response
|
||||||
|
onMessageUpdate(streamingMessage.id, (msg) => ({
|
||||||
|
...msg,
|
||||||
|
content: assistantResponse,
|
||||||
|
isStreaming: false,
|
||||||
|
tokenCount: undefined
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Log the complete assistant response
|
||||||
|
console.log('------------ ASSISTANT ------------');
|
||||||
|
console.log(assistantResponse);
|
||||||
|
console.log('-----------------------------------');
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If stream didn't complete normally, finalize the message
|
||||||
|
if (!streamCompleted && !controller.signal.aborted) {
|
||||||
|
onMessageUpdate(streamingMessage.id, (msg) => ({
|
||||||
|
...msg,
|
||||||
|
content: assistantResponse || 'Stream was interrupted unexpectedly',
|
||||||
|
isStreaming: false,
|
||||||
|
tokenCount: undefined
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return assistantResponse;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error && error.name === 'AbortError') {
|
||||||
|
// Request was aborted, don't show error
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('Error processing stream:', error);
|
||||||
|
// Update with error message
|
||||||
|
onMessageUpdate(streamingMessage.id, (msg) => ({
|
||||||
|
...msg,
|
||||||
|
content: 'Error: Failed to process message',
|
||||||
|
isStreaming: false,
|
||||||
|
tokenCount: undefined
|
||||||
|
}));
|
||||||
|
return '';
|
||||||
|
} finally {
|
||||||
|
setAbortController(null);
|
||||||
|
setIsProcessing(false);
|
||||||
|
onProcessingChange(false);
|
||||||
|
}
|
||||||
|
}, [onMessageUpdate, onMessageAdd, onProcessingChange]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
processStream,
|
||||||
|
abortController,
|
||||||
|
isProcessing
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -5,6 +5,14 @@ export interface TimeSignature {
|
|||||||
denominator: number;
|
denominator: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ChatMessage {
|
||||||
|
id: string;
|
||||||
|
role: 'user' | 'assistant';
|
||||||
|
content: string;
|
||||||
|
isStreaming?: boolean;
|
||||||
|
tokenCount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A reusable class-transformer decorator to apply a default value during deserialization.
|
* A reusable class-transformer decorator to apply a default value during deserialization.
|
||||||
* @param defaultValue The default value to apply if the field is undefined.
|
* @param defaultValue The default value to apply if the field is undefined.
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { processUserMessage } from '../util/messageFilter/UserMessageFilter';
|
||||||
|
import type { ChatMessage } from '../types/projectTypes';
|
||||||
|
|
||||||
|
export const generateMessageId = (): string => {
|
||||||
|
return `msg_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createMessage = (role: 'user' | 'assistant', content: string): ChatMessage => {
|
||||||
|
return {
|
||||||
|
id: generateMessageId(),
|
||||||
|
role,
|
||||||
|
content,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createStreamingMessage = (content: string = '<span class="processing-wave">Processing...</span> 0 tokens received. click here to abort.'): ChatMessage => {
|
||||||
|
return {
|
||||||
|
id: generateMessageId(),
|
||||||
|
role: 'assistant',
|
||||||
|
content,
|
||||||
|
isStreaming: true,
|
||||||
|
tokenCount: 0,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createToolResultMessage = (toolName: string, success: boolean, result: string): ChatMessage => {
|
||||||
|
const friendlyDisplay = `${success ? '✅' : '❌'} __**${toolName}**__ \n\n └── ${result}`;
|
||||||
|
return createMessage('user', friendlyDisplay);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addWelcomeMessage = async (): Promise<ChatMessage | null> => {
|
||||||
|
try {
|
||||||
|
const result = await processUserMessage('/welcome');
|
||||||
|
if (result.pseudoAssistantResponse) {
|
||||||
|
return createMessage('assistant', result.pseudoAssistantResponse);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch {
|
||||||
|
// ignore errors, just don't show welcome if it fails
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import { XMLToolExecutor } from '../agent/core/XMLToolExecutor';
|
||||||
|
import { extractXMLFromString } from '../util/xmlUtil';
|
||||||
|
import { createToolResultMessage } from './chatMessageUtils';
|
||||||
|
import type { ChatMessage } from '../types/projectTypes';
|
||||||
|
|
||||||
|
interface ToolExecutionResult {
|
||||||
|
success: boolean;
|
||||||
|
result: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ToolExecutionOptions {
|
||||||
|
onMessageAdd: (message: ChatMessage) => void;
|
||||||
|
onStatusUpdate: (status: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const extractActionableTools = (response: string): string[] => {
|
||||||
|
const xmlBlocks = extractXMLFromString(response);
|
||||||
|
// Consider only actionable tools (exclude think/thinking)
|
||||||
|
return xmlBlocks.filter((block) => {
|
||||||
|
const match = block.match(/<([a-zA-Z_][a-zA-Z0-9_-]*)/);
|
||||||
|
const name = match ? match[1].toLowerCase() : '';
|
||||||
|
return name !== 'think' && name !== 'thinking';
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const extractToolName = (xmlBlock: string): string => {
|
||||||
|
const toolNameMatch = xmlBlock.match(/<([a-zA-Z_][a-zA-Z0-9_-]*)/);
|
||||||
|
return toolNameMatch ? toolNameMatch[1] : 'unknown_tool';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const executeSingleTool = async (
|
||||||
|
xmlBlock: string,
|
||||||
|
toolName: string,
|
||||||
|
options: ToolExecutionOptions
|
||||||
|
): Promise<ToolExecutionResult> => {
|
||||||
|
const { onMessageAdd } = options;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const executor = XMLToolExecutor.instance();
|
||||||
|
const results = await executor.executeXMLTools(xmlBlock);
|
||||||
|
const result = results[0]; // Single block should give single result
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
// Add friendly display message
|
||||||
|
const toolMessage = createToolResultMessage(toolName, result.success, result.result);
|
||||||
|
onMessageAdd(toolMessage);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: result.success,
|
||||||
|
result: result.result
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
result: 'No result returned from tool execution'
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
// Handle individual tool error
|
||||||
|
const errorMessage = `Tool execution failed: ${error}`;
|
||||||
|
const toolMessage = createToolResultMessage(toolName, false, errorMessage);
|
||||||
|
onMessageAdd(toolMessage);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
result: errorMessage
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatToolResultForLLM = (toolName: string, result: ToolExecutionResult): string => {
|
||||||
|
// Skip thinking tools
|
||||||
|
if (toolName === 'thinking' || toolName === 'think') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `tool: ${toolName}\nsuccess: ${result.success}\nresult:\n${result.result}\n------------\n`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const executeAllTools = async (
|
||||||
|
actionableBlocks: string[],
|
||||||
|
options: ToolExecutionOptions
|
||||||
|
): Promise<string> => {
|
||||||
|
const { onStatusUpdate } = options;
|
||||||
|
|
||||||
|
onStatusUpdate(`Executing ${actionableBlocks.length} tool(s)...`);
|
||||||
|
|
||||||
|
let accumulatedResults = '';
|
||||||
|
|
||||||
|
// Execute tools sequentially with real-time updates
|
||||||
|
for (let i = 0; i < actionableBlocks.length; i++) {
|
||||||
|
onStatusUpdate(`Executing tool ${i + 1} of ${actionableBlocks.length}...`);
|
||||||
|
|
||||||
|
const toolName = extractToolName(actionableBlocks[i]);
|
||||||
|
const result = await executeSingleTool(actionableBlocks[i], toolName, options);
|
||||||
|
|
||||||
|
// Accumulate formatted result for LLM
|
||||||
|
accumulatedResults += formatToolResultForLLM(toolName, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return accumulatedResults;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user