This commit is contained in:
marko-kraemer 2025-05-10 02:44:12 +02:00
parent 405095a463
commit 71b185b659
6 changed files with 253 additions and 85 deletions

View File

@ -133,21 +133,39 @@ interface StreamingToolCall {
function getMetadataField(metadata: any, field: string, defaultValue: any = null): any { function getMetadataField(metadata: any, field: string, defaultValue: any = null): any {
if (!metadata) return defaultValue; if (!metadata) return defaultValue;
// Debug the type of metadata
console.log(`[METADATA DEBUG] Type: ${typeof metadata}, Field: ${field}`);
// If it's already an object // If it's already an object
if (typeof metadata === 'object' && !Array.isArray(metadata)) { if (typeof metadata === 'object' && !Array.isArray(metadata)) {
return metadata[field] !== undefined ? metadata[field] : defaultValue; // Log the metadata object to see its structure
console.log('[METADATA DEBUG] Object metadata:', metadata);
// Check if the field exists directly in the object
if (metadata[field] !== undefined) {
console.log(`[METADATA DEBUG] Found field ${field} in object:`, metadata[field]);
return metadata[field];
}
return defaultValue;
} }
// If it's a string, try to parse it // If it's a string, try to parse it
if (typeof metadata === 'string') { if (typeof metadata === 'string') {
try { try {
console.log('[METADATA DEBUG] Attempting to parse string metadata:', metadata);
const parsed = JSON.parse(metadata); const parsed = JSON.parse(metadata);
return parsed[field] !== undefined ? parsed[field] : defaultValue; if (parsed[field] !== undefined) {
console.log(`[METADATA DEBUG] Found field ${field} in parsed string:`, parsed[field]);
return parsed[field];
}
return defaultValue;
} catch (e) { } catch (e) {
console.log('[METADATA DEBUG] Error parsing metadata string:', e);
return defaultValue; return defaultValue;
} }
} }
console.log('[METADATA DEBUG] Metadata is neither object nor string, returning default value');
return defaultValue; return defaultValue;
} }
@ -324,14 +342,20 @@ function renderAttachments(
// Restore the safeJsonParse function with improved typing // Restore the safeJsonParse function with improved typing
function safeJsonParse<T>(jsonString: any, defaultValue: T): T { function safeJsonParse<T>(jsonString: any, defaultValue: T): T {
// Debug the input
console.log(`[JSON PARSE DEBUG] Type: ${typeof jsonString}`);
if (typeof jsonString !== 'string') { if (typeof jsonString !== 'string') {
// If it's already an object, just return it // If it's already an object, just return it
console.log('[JSON PARSE DEBUG] Already an object, returning as-is');
return jsonString as unknown as T; return jsonString as unknown as T;
} }
try { try {
console.log('[JSON PARSE DEBUG] Attempting to parse string:', jsonString.substring(0, 50) + (jsonString.length > 50 ? '...' : ''));
return JSON.parse(jsonString) as T; return JSON.parse(jsonString) as T;
} catch (e) { } catch (e) {
console.log('[JSON PARSE DEBUG] Error parsing JSON string:', e);
return defaultValue; return defaultValue;
} }
} }
@ -902,13 +926,24 @@ export default function ThreadPage({
// Automatically detect and populate tool calls from messages // Automatically detect and populate tool calls from messages
useEffect(() => { useEffect(() => {
console.log('[TOOL CALLS] Starting tool calls detection');
// Calculate historical tool pairs regardless of panel state // Calculate historical tool pairs regardless of panel state
const historicalToolPairs: ToolCallInput[] = []; const historicalToolPairs: ToolCallInput[] = [];
const assistantMessages = messages.filter( const assistantMessages = messages.filter(
(m) => m.type === 'assistant' && m.message_id, (m) => m.type === 'assistant' && m.message_id,
); );
console.log(`[TOOL CALLS] Found ${assistantMessages.length} assistant messages`);
assistantMessages.forEach((assistantMsg) => { assistantMessages.forEach((assistantMsg) => {
console.log('[TOOL CALLS] Processing assistant message:', assistantMsg.message_id);
console.log('[TOOL CALLS] Assistant message content type:', typeof assistantMsg.content);
console.log('[TOOL CALLS] Assistant message content preview:',
typeof assistantMsg.content === 'string'
? assistantMsg.content.substring(0, 50)
: JSON.stringify(assistantMsg.content).substring(0, 50));
const resultMessage = messages.find((toolMsg) => { const resultMessage = messages.find((toolMsg) => {
if ( if (
toolMsg.type !== 'tool' || toolMsg.type !== 'tool' ||
@ -917,34 +952,82 @@ export default function ThreadPage({
) )
return false; return false;
const metadata = getMetadataField(toolMsg.metadata, 'assistant_message_id'); const metadata = getMetadataField(toolMsg.metadata, 'assistant_message_id');
return metadata === assistantMsg.message_id; const matches = metadata === assistantMsg.message_id;
if (matches) {
console.log('[TOOL CALLS] Found matching tool message:', toolMsg.message_id);
}
return matches;
}); });
if (resultMessage) { if (resultMessage) {
// Determine tool name from assistant message content console.log('[TOOL CALLS] Processing result message for assistant:', assistantMsg.message_id);
// Determine tool name from assistant message content or metadata
let toolName = 'unknown'; let toolName = 'unknown';
try { try {
// First check if we can extract the tool type from parsing_details in metadata
if (resultMessage.metadata) {
// Try to get from metadata parsing_details
const parsingDetails = getMetadataField(resultMessage.metadata, 'parsing_details');
console.log('[TOOL CALLS] Parsing details:', parsingDetails);
if (parsingDetails && parsingDetails.raw_chunk) {
// Extract from the raw XML chunk - this is the most reliable way
const xmlTagMatch = parsingDetails.raw_chunk.match(/<([a-zA-Z\-_]+)(?:\s+[^>]*)?>|<([a-zA-Z\-_]+)(?:\s+[^>]*)?\/>/);
if (xmlTagMatch) {
toolName = xmlTagMatch[1] || xmlTagMatch[2] || 'unknown';
console.log('[TOOL CALLS] Extracted tool name from raw_chunk:', toolName);
}
}
}
// If we couldn't get it from the metadata, try from the assistant message content
if (toolName === 'unknown' && typeof assistantMsg.content === 'string') {
console.log('[TOOL CALLS] Trying to extract tool name from assistant content');
// Try to extract tool name from content // Try to extract tool name from content
const xmlMatch = assistantMsg.content.match( const xmlMatch = assistantMsg.content.match(
/<([a-zA-Z\-_]+)(?:\s+[^>]*)?>|<([a-zA-Z\-_]+)(?:\s+[^>]*)?\/>/, /<([a-zA-Z\-_]+)(?:\s+[^>]*)?>|<([a-zA-Z\-_]+)(?:\s+[^>]*)?\/>/,
); );
if (xmlMatch) { if (xmlMatch) {
toolName = xmlMatch[1] || xmlMatch[2] || 'unknown'; toolName = xmlMatch[1] || xmlMatch[2] || 'unknown';
console.log('[TOOL CALLS] Found XML tool name from assistant content:', toolName);
} else { } else {
console.log('[TOOL CALLS] No XML tags found in assistant content, checking for JSON structure');
// Fallback to checking for tool_calls JSON structure // Fallback to checking for tool_calls JSON structure
try {
const assistantContentParsed = safeJsonParse<{ const assistantContentParsed = safeJsonParse<{
tool_calls?: Array<{ name: string }> tool_calls?: Array<{ name?: string; function?: { name?: string } }>
}>(assistantMsg.content, { tool_calls: [] }); }>(assistantMsg.content, { tool_calls: [] });
console.log('[TOOL CALLS] Parsed assistant content:', assistantContentParsed);
if ( if (
assistantContentParsed && assistantContentParsed &&
assistantContentParsed.tool_calls && assistantContentParsed.tool_calls &&
assistantContentParsed.tool_calls.length > 0 assistantContentParsed.tool_calls.length > 0
) { ) {
toolName = assistantContentParsed.tool_calls[0].name || 'unknown'; // Try to get name directly or from function.name
const toolCall = assistantContentParsed.tool_calls[0];
toolName =
toolCall.name ||
(toolCall.function ? toolCall.function.name : null) ||
'unknown';
console.log('[TOOL CALLS] Found tool name from JSON:', toolName);
}
} catch (parseError) {
console.log('[TOOL CALLS] Error parsing assistant content as JSON:', parseError);
} }
} }
} catch {} }
} catch (error) {
console.log('[TOOL CALLS] Error extracting tool name:', error);
}
console.log('[TOOL CALLS] Final determined tool name:', toolName);
// Skip adding <ask> tags to the tool calls // Skip adding <ask> tags to the tool calls
if (toolName === 'ask' || toolName === 'complete') { if (toolName === 'ask' || toolName === 'complete') {
@ -953,27 +1036,71 @@ export default function ThreadPage({
let isSuccess = true; let isSuccess = true;
try { try {
const toolContent = resultMessage.content?.toLowerCase() || ''; console.log('[TOOL CALLS] Processing tool result message');
isSuccess = !( console.log('[TOOL CALLS] Content type:', typeof resultMessage.content);
toolContent.includes('failed') ||
toolContent.includes('error') || // Prepare content for historicalToolPairs
toolContent.includes('failure') let assistantCallContent = '';
); let toolResultContent = '';
} catch {}
// Process assistant content
if (typeof assistantMsg.content === 'string') {
assistantCallContent = assistantMsg.content;
} else if (assistantMsg.content && typeof assistantMsg.content === 'object') {
try {
assistantCallContent = JSON.stringify(assistantMsg.content);
} catch (e) {
console.log('[TOOL CALLS] Failed to stringify assistant content:', e);
assistantCallContent = 'Error: Content could not be displayed';
}
}
// Process tool result content
if (typeof resultMessage.content === 'string') {
toolResultContent = resultMessage.content;
} else if (resultMessage.content && typeof resultMessage.content === 'object') {
try {
toolResultContent = JSON.stringify(resultMessage.content, null, 2);
} catch (e) {
console.log('[TOOL CALLS] Failed to stringify tool result content:', e);
toolResultContent = 'Error: Content could not be displayed';
}
}
console.log('[TOOL CALLS] Final assistantCallContent type:', typeof assistantCallContent);
console.log('[TOOL CALLS] Final toolResultContent type:', typeof toolResultContent);
// Determine success by checking for error indicators in a lower-cased version of the content
const lowerCaseContent = (typeof toolResultContent === 'string' ? toolResultContent : '').toLowerCase();
const errorIndicators = ['failed', 'error', 'failure', 'exception'];
const hasErrorIndicator = errorIndicators.some(indicator => lowerCaseContent.includes(indicator));
// Set success state
isSuccess = !hasErrorIndicator;
console.log('[TOOL CALLS] Success determination:', isSuccess);
historicalToolPairs.push({ historicalToolPairs.push({
assistantCall: { assistantCall: {
name: toolName, name: toolName,
content: assistantMsg.content, content: assistantCallContent,
timestamp: assistantMsg.created_at, timestamp: assistantMsg.created_at,
}, },
toolResult: { toolResult: {
content: resultMessage.content, content: toolResultContent,
isSuccess: isSuccess, isSuccess: isSuccess,
timestamp: resultMessage.created_at, timestamp: resultMessage.created_at,
}, },
}); });
} catch (error) {
console.log('[TOOL CALLS] Error processing tool result:', error);
} }
}
});
// Log the tool pairs we've found
console.log(`[TOOL CALLS] Found ${historicalToolPairs.length} tool pairs`);
historicalToolPairs.forEach((pair, idx) => {
console.log(`[TOOL CALLS] Tool pair ${idx}:`, pair.assistantCall?.name);
}); });
// Always update the toolCalls state // Always update the toolCalls state
@ -1085,52 +1212,93 @@ export default function ThreadPage({
clickedToolName, clickedToolName,
); );
// Find the index of the tool call associated with the clicked assistant message // Log toolCalls for debugging
const toolIndex = toolCalls.findIndex((tc) => { console.log('[PAGE] Available tool calls:', toolCalls.length);
// Check if the assistant message ID matches the one stored in the tool result's metadata toolCalls.forEach((tc, idx) => {
if (!tc.toolResult?.content || tc.toolResult.content === 'STREAMING') console.log(`[PAGE] Tool call ${idx}: name=${tc.assistantCall?.name}`);
return false; // Skip streaming or incomplete calls });
// Directly compare assistant message IDs if available in the structure // Find all assistant messages by ID (should be just one)
// Find the original assistant message based on the ID
const assistantMessage = messages.find( const assistantMessage = messages.find(
(m) => (m) => m.message_id === clickedAssistantMessageId && m.type === 'assistant'
m.message_id === clickedAssistantMessageId &&
m.type === 'assistant',
); );
if (!assistantMessage) return false;
// Find the corresponding tool message using metadata if (!assistantMessage) {
const toolMessage = messages.find((m) => { console.error(`[PAGE] Could not find assistant message with ID: ${clickedAssistantMessageId}`);
toast.error('Could not find assistant message');
return;
}
console.log('[PAGE] Found assistant message:', assistantMessage.message_id);
// Find tool messages that reference this assistant message
const matchingToolMessages = messages.filter((m) => {
if (m.type !== 'tool' || !m.metadata) return false; if (m.type !== 'tool' || !m.metadata) return false;
const metadata = getMetadataField(m.metadata, 'assistant_message_id'); const metadata = getMetadataField(m.metadata, 'assistant_message_id');
return metadata === assistantMessage.message_id; const isMatch = metadata === assistantMessage.message_id;
if (isMatch) {
console.log('[PAGE] Found matching tool message:', m.message_id);
}
return isMatch;
}); });
// Check if the current toolCall 'tc' corresponds to this assistant/tool message pair if (matchingToolMessages.length === 0) {
return ( console.error('[PAGE] No tool messages found for this assistant message');
tc.assistantCall?.content === assistantMessage.content && toast.error('No tool results found for this command');
tc.toolResult?.content === toolMessage?.content return;
); }
// Look for any tool call with matching tool name
const toolIndex = toolCalls.findIndex((tc) => {
// First try to match by the specific clicked tool name
if (tc.assistantCall?.name?.toLowerCase() === clickedToolName.toLowerCase()) {
console.log('[PAGE] Found tool call with matching name:', clickedToolName);
return true;
}
// Otherwise try to match by looking at the assistant content
if (typeof tc.assistantCall?.content === 'string' &&
tc.assistantCall.content.includes(`<${clickedToolName}`)) {
console.log('[PAGE] Found tool call with matching tag in content:', clickedToolName);
return true;
}
return false;
}); });
if (toolIndex !== -1) { // If no exact match, try to find any tool call with matching assistant message ID
console.log( if (toolIndex === -1) {
`[PAGE] Found tool call at index ${toolIndex} for assistant message ${clickedAssistantMessageId}`, // Try a less strict approach - just find any tool call where assistant content matches
); const lessStrictMatch = toolCalls.findIndex((tc) => {
return tc.assistantCall?.content === assistantMessage.content;
});
if (lessStrictMatch !== -1) {
console.log('[PAGE] Found tool call with matching assistant content at index:', lessStrictMatch);
setCurrentToolIndex(lessStrictMatch);
setIsSidePanelOpen(true);
return;
}
// If still no match, just open the latest tool call
if (toolCalls.length > 0) {
console.log('[PAGE] No exact match found, showing latest tool call');
setCurrentToolIndex(toolCalls.length - 1);
setIsSidePanelOpen(true);
return;
}
console.warn('[PAGE] No tool calls available to show');
toast.info('Could not find details for this tool call.');
return;
}
console.log(`[PAGE] Found matching tool call at index ${toolIndex}`);
setCurrentToolIndex(toolIndex); setCurrentToolIndex(toolIndex);
setIsSidePanelOpen(true); // Explicitly open the panel setIsSidePanelOpen(true); // Explicitly open the panel
} else {
console.warn(
`[PAGE] Could not find matching tool call in toolCalls array for assistant message ID: ${clickedAssistantMessageId}`,
);
toast.info('Could not find details for this tool call.');
// Optionally, still open the panel but maybe at the last index or show a message?
// setIsSidePanelOpen(true);
}
}, },
[messages, toolCalls], [messages, toolCalls],
); // Add toolCalls as a dependency );
// Handle streaming tool calls // Handle streaming tool calls
const handleStreamingToolCall = useCallback( const handleStreamingToolCall = useCallback(