From d6461c5961ca3c8295bfbafa068ca54e8d6ef14c Mon Sep 17 00:00:00 2001 From: marko-kraemer Date: Thu, 17 Apr 2025 15:26:06 +0100 Subject: [PATCH] thread types, utils --- .../app/dashboard/agents/[threadId]/page.tsx | 243 +----------------- frontend/src/components/thread/types.ts | 41 +++ frontend/src/components/thread/utils.ts | 214 +++++++++++++++ 3 files changed, 259 insertions(+), 239 deletions(-) create mode 100644 frontend/src/components/thread/types.ts create mode 100644 frontend/src/components/thread/utils.ts diff --git a/frontend/src/app/dashboard/agents/[threadId]/page.tsx b/frontend/src/app/dashboard/agents/[threadId]/page.tsx index f2fcfba4..b9c8510a 100644 --- a/frontend/src/app/dashboard/agents/[threadId]/page.tsx +++ b/frontend/src/app/dashboard/agents/[threadId]/page.tsx @@ -5,10 +5,8 @@ import Image from 'next/image'; import { useRouter } from 'next/navigation'; import { Button } from '@/components/ui/button'; import { - ArrowDown, FileText, Terminal, ExternalLink, User, CheckCircle, CircleDashed, - FileEdit, Search, Globe, Code, MessageSquare, Folder, FileX, CloudUpload, Wrench, Cog + ArrowDown, CheckCircle, CircleDashed, } from 'lucide-react'; -import type { ElementType } from 'react'; import { addUserMessage, getMessages, startAgent, stopAgent, getAgentStatus, streamAgent, getAgentRuns, getProject, getThread, updateProject, Project } from '@/lib/api'; import { toast } from 'sonner'; import { Skeleton } from "@/components/ui/skeleton"; @@ -19,243 +17,10 @@ import { ToolCallSidePanel, SidePanelContent, ToolCallData } from "@/components/ import { useSidebar } from "@/components/ui/sidebar"; import { TodoPanel } from '@/components/thread/todo-panel'; -// Define a type for the params to make React.use() work properly -type ThreadParams = { - threadId: string; -}; +// Import types and utils +import { ApiMessage, ThreadParams, isToolSequence } from '@/components/thread/types'; +import { getToolIcon, extractPrimaryParam, groupMessages, SHOULD_RENDER_TOOL_RESULTS } from '@/components/thread/utils'; -interface ApiMessage { - role: string; - content: string; - type?: string; - name?: string; - arguments?: string; - tool_call?: { - id: string; - function: { - name: string; - arguments: string; - }; - type: string; - index: number; - }; -} - -// Define structure for grouped tool call/result sequences -type ToolSequence = { - type: 'tool_sequence'; - items: ApiMessage[]; -}; - -// Type for items that will be rendered -type RenderItem = ApiMessage | ToolSequence; - -// Type guard to check if an item is a ToolSequence -function isToolSequence(item: RenderItem): item is ToolSequence { - return (item as ToolSequence).type === 'tool_sequence'; -} - -// Helper function to get an icon based on tool name -const getToolIcon = (toolName: string): ElementType => { - // Ensure we handle null/undefined toolName gracefully - if (!toolName) return Cog; - - // Convert to lowercase for case-insensitive matching - const normalizedName = toolName.toLowerCase(); - - // Check for browser-related tools with a prefix check - if (normalizedName.startsWith('browser-')) { - return Globe; - } - - switch (normalizedName) { - // File operations - case 'create-file': - case 'str-replace': - case 'full-file-rewrite': - case 'read-file': - return FileEdit; - - // Shell commands - case 'execute-command': - return Terminal; - - // Web operations - case 'web-search': - return Search; - - // API and data operations - case 'call-data-provider': - case 'get-data-provider-endpoints': - return ExternalLink; // Using ExternalLink instead of Database which isn't imported - - // Code operations - case 'delete-file': - return FileX; - - // Deployment - case 'deploy-site': - return CloudUpload; - - // Tools and utilities - case 'execute-code': - return Code; - - // Default case - default: - // Add logging for debugging unhandled tool types - console.log(`[PAGE] Using default icon for unknown tool type: ${toolName}`); - return Wrench; // Default icon for tools - } -}; - -// Helper function to extract a primary parameter from XML/arguments -const extractPrimaryParam = (toolName: string, content: string | undefined): string | null => { - if (!content) return null; - - try { - // Handle browser tools with a prefix check - if (toolName?.toLowerCase().startsWith('browser-')) { - // Try to extract URL for navigation - const urlMatch = content.match(/url=(?:"|')([^"|']+)(?:"|')/); - if (urlMatch) return urlMatch[1]; - - // For other browser operations, extract the goal or action - const goalMatch = content.match(/goal=(?:"|')([^"|']+)(?:"|')/); - if (goalMatch) { - const goal = goalMatch[1]; - return goal.length > 30 ? goal.substring(0, 27) + '...' : goal; - } - - return null; - } - - // Simple regex for common parameters - adjust as needed - let match: RegExpMatchArray | null = null; - - switch (toolName?.toLowerCase()) { - // File operations - case 'create-file': - case 'full-file-rewrite': - case 'read-file': - case 'delete-file': - case 'str-replace': - // Try to match file_path attribute - match = content.match(/file_path=(?:"|')([^"|']+)(?:"|')/); - // Return just the filename part - return match ? match[1].split('/').pop() || match[1] : null; - - // Shell commands - case 'execute-command': - // Extract command content - match = content.match(/command=(?:"|')([^"|']+)(?:"|')/); - if (match) { - const cmd = match[1]; - return cmd.length > 30 ? cmd.substring(0, 27) + '...' : cmd; - } - return null; - - // Web search - case 'web-search': - match = content.match(/query=(?:"|')([^"|']+)(?:"|')/); - return match ? (match[1].length > 30 ? match[1].substring(0, 27) + '...' : match[1]) : null; - - // Data provider operations - case 'call-data-provider': - match = content.match(/service_name=(?:"|')([^"|']+)(?:"|')/); - const route = content.match(/route=(?:"|')([^"|']+)(?:"|')/); - return match && route ? `${match[1]}/${route[1]}` : (match ? match[1] : null); - - // Deployment - case 'deploy-site': - match = content.match(/site_name=(?:"|')([^"|']+)(?:"|')/); - return match ? match[1] : null; - } - - return null; - } catch (e) { - console.warn("Error parsing tool parameters:", e); - return null; - } -}; - -// Flag to control whether tool result messages are rendered -const SHOULD_RENDER_TOOL_RESULTS = false; - -// Function to group consecutive assistant tool call / user tool result pairs -function groupMessages(messages: ApiMessage[]): RenderItem[] { - const grouped: RenderItem[] = []; - let i = 0; - - while (i < messages.length) { - const currentMsg = messages[i]; - const nextMsg = i + 1 < messages.length ? messages[i + 1] : null; - - let currentSequence: ApiMessage[] = []; - - // Check if current message is the start of a potential sequence - if (currentMsg.role === 'assistant') { - // Regex to find the first XML-like tag: or or self-closing tags - const toolTagMatch = currentMsg.content?.match(/<(?!inform\b)([a-zA-Z\-_]+)(?:\s+[^>]*)?(?:\/)?>/); - if (toolTagMatch && nextMsg && nextMsg.role === 'user') { - const expectedTag = toolTagMatch[1]; - - // Regex to check for ... - // Also handle self-closing tags in the response - const toolResultRegex = new RegExp(`^\\s*<(${expectedTag})(?:\\s+[^>]*)?(?:/>|>[\\s\\S]*?)\\s*`); - - if (nextMsg.content?.match(toolResultRegex)) { - // Found a pair, start a sequence - currentSequence.push(currentMsg); - currentSequence.push(nextMsg); - i += 2; // Move past this pair - - // Check for continuation - while (i < messages.length) { - const potentialAssistant = messages[i]; - const potentialUser = i + 1 < messages.length ? messages[i + 1] : null; - - if (potentialAssistant.role === 'assistant') { - const nextToolTagMatch = potentialAssistant.content?.match(/<(?!inform\b)([a-zA-Z\-_]+)(?:\s+[^>]*)?(?:\/)?>/); - if (nextToolTagMatch && potentialUser && potentialUser.role === 'user') { - const nextExpectedTag = nextToolTagMatch[1]; - - // Also handle self-closing tags in the response - const nextToolResultRegex = new RegExp(`^\\s*<(${nextExpectedTag})(?:\\s+[^>]*)?(?:/>|>[\\s\\S]*?)\\s*`); - - if (potentialUser.content?.match(nextToolResultRegex)) { - // Sequence continues - currentSequence.push(potentialAssistant); - currentSequence.push(potentialUser); - i += 2; // Move past the added pair - } else { - // Assistant/User message, but not a matching tool result pair - break sequence - break; - } - } else { - // Assistant message without tool tag, or no following user message - break sequence - break; - } - } else { - // Not an assistant message - break sequence - break; - } - } - // Add the completed sequence to grouped results - grouped.push({ type: 'tool_sequence', items: currentSequence }); - continue; // Continue the outer loop from the new 'i' - } - } - } - - // If no sequence was started or continued, add the current message normally - if (currentSequence.length === 0) { - grouped.push(currentMsg); - i++; // Move to the next message - } - } - return grouped; -} export default function ThreadPage({ params }: { params: Promise }) { const unwrappedParams = React.use(params); diff --git a/frontend/src/components/thread/types.ts b/frontend/src/components/thread/types.ts new file mode 100644 index 00000000..ee70366c --- /dev/null +++ b/frontend/src/components/thread/types.ts @@ -0,0 +1,41 @@ +import type { ElementType } from 'react'; +import type { Project } from '@/lib/api'; + +// Define a type for the params to make React.use() work properly +export type ThreadParams = { + threadId: string; +}; + +export interface ApiMessage { + role: string; + content: string; + type?: string; + name?: string; + arguments?: string; + tool_call?: { + id: string; + function: { + name: string; + arguments: string; + }; + type: string; + index: number; + }; +} + +// Define structure for grouped tool call/result sequences +export type ToolSequence = { + type: 'tool_sequence'; + items: ApiMessage[]; +}; + +// Type for items that will be rendered +export type RenderItem = ApiMessage | ToolSequence; + +// Type guard to check if an item is a ToolSequence +export function isToolSequence(item: RenderItem): item is ToolSequence { + return (item as ToolSequence).type === 'tool_sequence'; +} + +// Re-export existing types +export type { Project }; \ No newline at end of file diff --git a/frontend/src/components/thread/utils.ts b/frontend/src/components/thread/utils.ts new file mode 100644 index 00000000..b00a17ae --- /dev/null +++ b/frontend/src/components/thread/utils.ts @@ -0,0 +1,214 @@ +import type { ElementType } from 'react'; +import { + ArrowDown, FileText, Terminal, ExternalLink, User, CheckCircle, CircleDashed, + FileEdit, Search, Globe, Code, MessageSquare, Folder, FileX, CloudUpload, Wrench, Cog, + Network, FileSearch, FilePlus +} from 'lucide-react'; +import { ApiMessage, RenderItem, ToolSequence, isToolSequence } from './types'; + +// Flag to control whether tool result messages are rendered +export const SHOULD_RENDER_TOOL_RESULTS = false; + +// Helper function to get an icon based on tool name +export const getToolIcon = (toolName: string): ElementType => { + // Ensure we handle null/undefined toolName gracefully + if (!toolName) return Cog; + + // Convert to lowercase for case-insensitive matching + const normalizedName = toolName.toLowerCase(); + + // Check for browser-related tools with a prefix check + if (normalizedName.startsWith('browser-')) { + return Globe; + } + switch (normalizedName) { + // File operations + case 'create-file': + return FileEdit; + case 'str-replace': + return FileSearch; + case 'full-file-rewrite': + return FilePlus; + case 'read-file': + return FileText; + + // Shell commands + case 'execute-command': + return Terminal; + + // Web operations + case 'web-search': + return Search; + case 'crawl-webpage': + return Globe; + + // API and data operations + case 'call-data-provider': + return ExternalLink; + case 'get-data-provider-endpoints': + return Network; + + // Code operations + case 'delete-file': + return FileX; + + // Deployment + case 'deploy-site': + return CloudUpload; + + // Tools and utilities + case 'execute-code': + return Code; + + // Default case + default: + // Add logging for debugging unhandled tool types + console.log(`[PAGE] Using default icon for unknown tool type: ${toolName}`); + return Wrench; // Default icon for tools + } +}; + +// Helper function to extract a primary parameter from XML/arguments +export const extractPrimaryParam = (toolName: string, content: string | undefined): string | null => { + if (!content) return null; + + try { + // Handle browser tools with a prefix check + if (toolName?.toLowerCase().startsWith('browser-')) { + // Try to extract URL for navigation + const urlMatch = content.match(/url=(?:"|')([^"|']+)(?:"|')/); + if (urlMatch) return urlMatch[1]; + + // For other browser operations, extract the goal or action + const goalMatch = content.match(/goal=(?:"|')([^"|']+)(?:"|')/); + if (goalMatch) { + const goal = goalMatch[1]; + return goal.length > 30 ? goal.substring(0, 27) + '...' : goal; + } + + return null; + } + + // Simple regex for common parameters - adjust as needed + let match: RegExpMatchArray | null = null; + + switch (toolName?.toLowerCase()) { + // File operations + case 'create-file': + case 'full-file-rewrite': + case 'read-file': + case 'delete-file': + case 'str-replace': + // Try to match file_path attribute + match = content.match(/file_path=(?:"|')([^"|']+)(?:"|')/); + // Return just the filename part + return match ? match[1].split('/').pop() || match[1] : null; + + // Shell commands + case 'execute-command': + // Extract command content + match = content.match(/command=(?:"|')([^"|']+)(?:"|')/); + if (match) { + const cmd = match[1]; + return cmd.length > 30 ? cmd.substring(0, 27) + '...' : cmd; + } + return null; + + // Web search + case 'web-search': + match = content.match(/query=(?:"|')([^"|']+)(?:"|')/); + return match ? (match[1].length > 30 ? match[1].substring(0, 27) + '...' : match[1]) : null; + + // Data provider operations + case 'call-data-provider': + match = content.match(/service_name=(?:"|')([^"|']+)(?:"|')/); + const route = content.match(/route=(?:"|')([^"|']+)(?:"|')/); + return match && route ? `${match[1]}/${route[1]}` : (match ? match[1] : null); + + // Deployment + case 'deploy-site': + match = content.match(/site_name=(?:"|')([^"|']+)(?:"|')/); + return match ? match[1] : null; + } + + return null; + } catch (e) { + console.warn("Error parsing tool parameters:", e); + return null; + } +}; + +// Function to group consecutive assistant tool call / user tool result pairs +export function groupMessages(messages: ApiMessage[]): RenderItem[] { + const grouped: RenderItem[] = []; + let i = 0; + + while (i < messages.length) { + const currentMsg = messages[i]; + const nextMsg = i + 1 < messages.length ? messages[i + 1] : null; + + let currentSequence: ApiMessage[] = []; + + // Check if current message is the start of a potential sequence + if (currentMsg.role === 'assistant') { + // Regex to find the first XML-like tag: or or self-closing tags + const toolTagMatch = currentMsg.content?.match(/<(?!inform\b)([a-zA-Z\-_]+)(?:\s+[^>]*)?(?:\/)?>/); + if (toolTagMatch && nextMsg && nextMsg.role === 'user') { + const expectedTag = toolTagMatch[1]; + + // Regex to check for ... + // Also handle self-closing tags in the response + const toolResultRegex = new RegExp(`^\\s*<(${expectedTag})(?:\\s+[^>]*)?(?:/>|>[\\s\\S]*?)\\s*`); + + if (nextMsg.content?.match(toolResultRegex)) { + // Found a pair, start a sequence + currentSequence.push(currentMsg); + currentSequence.push(nextMsg); + i += 2; // Move past this pair + + // Check for continuation + while (i < messages.length) { + const potentialAssistant = messages[i]; + const potentialUser = i + 1 < messages.length ? messages[i + 1] : null; + + if (potentialAssistant.role === 'assistant') { + const nextToolTagMatch = potentialAssistant.content?.match(/<(?!inform\b)([a-zA-Z\-_]+)(?:\s+[^>]*)?(?:\/)?>/); + if (nextToolTagMatch && potentialUser && potentialUser.role === 'user') { + const nextExpectedTag = nextToolTagMatch[1]; + + // Also handle self-closing tags in the response + const nextToolResultRegex = new RegExp(`^\\s*<(${nextExpectedTag})(?:\\s+[^>]*)?(?:/>|>[\\s\\S]*?)\\s*`); + + if (potentialUser.content?.match(nextToolResultRegex)) { + // Sequence continues + currentSequence.push(potentialAssistant); + currentSequence.push(potentialUser); + i += 2; // Move past the added pair + } else { + // Assistant/User message, but not a matching tool result pair - break sequence + break; + } + } else { + // Assistant message without tool tag, or no following user message - break sequence + break; + } + } else { + // Not an assistant message - break sequence + break; + } + } + // Add the completed sequence to grouped results + grouped.push({ type: 'tool_sequence', items: currentSequence }); + continue; // Continue the outer loop from the new 'i' + } + } + } + + // If no sequence was started or continued, add the current message normally + if (currentSequence.length === 0) { + grouped.push(currentMsg); + i++; // Move to the next message + } + } + return grouped; +} \ No newline at end of file