mirror of https://github.com/kortix-ai/suna.git
thread types, utils
This commit is contained in:
parent
a8f0da8ae8
commit
d6461c5961
|
@ -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: <tagname ...> or <tagname> 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 <tool_result><tagname>...</tagname></tool_result>
|
||||
// Also handle self-closing tags in the response
|
||||
const toolResultRegex = new RegExp(`^<tool_result>\\s*<(${expectedTag})(?:\\s+[^>]*)?(?:/>|>[\\s\\S]*?</\\1>)\\s*</tool_result>`);
|
||||
|
||||
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(`^<tool_result>\\s*<(${nextExpectedTag})(?:\\s+[^>]*)?(?:/>|>[\\s\\S]*?</\\1>)\\s*</tool_result>`);
|
||||
|
||||
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<ThreadParams> }) {
|
||||
const unwrappedParams = React.use(params);
|
||||
|
|
|
@ -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 };
|
|
@ -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: <tagname ...> or <tagname> 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 <tool_result><tagname>...</tagname></tool_result>
|
||||
// Also handle self-closing tags in the response
|
||||
const toolResultRegex = new RegExp(`^<tool_result>\\s*<(${expectedTag})(?:\\s+[^>]*)?(?:/>|>[\\s\\S]*?</\\1>)\\s*</tool_result>`);
|
||||
|
||||
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(`^<tool_result>\\s*<(${nextExpectedTag})(?:\\s+[^>]*)?(?:/>|>[\\s\\S]*?</\\1>)\\s*</tool_result>`);
|
||||
|
||||
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;
|
||||
}
|
Loading…
Reference in New Issue