This commit is contained in:
marko-kraemer 2025-04-18 14:42:23 +01:00
parent dae4cff57c
commit aaf7efd615
4 changed files with 298 additions and 130 deletions

View File

@ -0,0 +1,58 @@
import sys
import os
from uuid import uuid4
from dotenv import load_dotenv
# Add the backend directory to the Python path
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..'))
backend_dir = os.path.join(project_root, 'backend')
if backend_dir not in sys.path:
sys.path.insert(0, backend_dir)
# Load environment variables
load_dotenv()
# Import ThreadManager and tools
from agentpress.thread_manager import ThreadManager
from agent.tools.sb_shell_tool import SandboxShellTool
from agent.tools.sb_files_tool import SandboxFilesTool
from agent.tools.sb_browser_tool import SandboxBrowserTool
from agent.tools.sb_deploy_tool import SandboxDeployTool
from agent.tools.message_tool import MessageTool
from agent.tools.web_search_tool import WebSearchTool
from agent.tools.data_providers_tool import DataProvidersTool
# Simple mock sandbox with minimal required attributes
class MockSandbox:
def __init__(self):
self.id = "sandbox-6005d12f"
# Get and print all XML tags
if __name__ == "__main__":
# Initialize ThreadManager
thread_manager = ThreadManager()
# Create a simple mock sandbox
mock_sandbox = MockSandbox()
# Register all tools
thread_manager.add_tool(SandboxShellTool, sandbox=mock_sandbox)
thread_manager.add_tool(SandboxFilesTool, sandbox=mock_sandbox)
thread_manager.add_tool(SandboxBrowserTool, sandbox=mock_sandbox, thread_id="mock-thread-id", thread_manager=thread_manager)
thread_manager.add_tool(SandboxDeployTool, sandbox=mock_sandbox)
thread_manager.add_tool(MessageTool)
# Conditionally register API-dependent tools
if os.getenv("EXA_API_KEY"):
thread_manager.add_tool(WebSearchTool)
if os.getenv("RAPID_API_KEY"):
thread_manager.add_tool(DataProvidersTool)
# Get XML examples
xml_examples = thread_manager.tool_registry.get_xml_examples()
# Print all XML tags in requested format
print("\nXML Tags:")
for tag_name in xml_examples.keys():
print(f"<{tag_name}></{tag_name}>")

View File

@ -3,7 +3,7 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"

View File

@ -20,6 +20,34 @@ import { useAgentStream } from '@/hooks/useAgentStream';
import { UnifiedMessage, ParsedContent, ParsedMetadata, ThreadParams } from '@/components/thread/types';
import { getToolIcon, extractPrimaryParam, safeJsonParse } from '@/components/thread/utils';
// Define the set of tags whose raw XML should be hidden during streaming
const HIDE_STREAMING_XML_TAGS = new Set([
'execute-command',
'create-file',
'delete-file',
'full-file-rewrite',
'str-replace',
'browser-click-element',
'browser-close-tab',
'browser-drag-drop',
'browser-get-dropdown-options',
'browser-go-back',
'browser-input-text',
'browser-navigate-to',
'browser-scroll-down',
'browser-scroll-to-text',
'browser-scroll-up',
'browser-select-dropdown-option',
'browser-send-keys',
'browser-switch-tab',
'browser-wait',
'deploy',
'ask',
'complete',
'crawl-webpage',
'web-search'
]);
// Extend the base Message type with the expected database fields
interface ApiMessageType extends BaseApiMessageType {
message_id?: string;
@ -71,6 +99,10 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
const messagesLoadedRef = useRef(false);
const agentRunsCheckedRef = useRef(false);
const handleProjectRenamed = useCallback((newName: string) => {
setProjectName(newName);
}, []);
const { state: leftSidebarState, setOpen: setLeftSidebarOpen } = useSidebar();
const initialLayoutAppliedRef = useRef(false);
@ -82,10 +114,6 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
setCurrentToolIndex(newIndex);
}, []);
const handleProjectRenamed = useCallback((newName: string) => {
setProjectName(newName);
}, []);
useEffect(() => {
if (isSidePanelOpen && leftSidebarState !== 'collapsed') {
setLeftSidebarOpen(false);
@ -428,107 +456,113 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
const handleOpenFileViewer = useCallback(() => setFileViewerOpen(true), []);
const handleToolClick = useCallback((message: UnifiedMessage, toolInfo: { name: string, args: any, xml: string } | null) => {
if (!toolInfo) return;
const assistantMessageId = message.message_id;
console.log("Tool Clicked:", toolInfo.name, "Assistant Message ID:", assistantMessageId);
console.log("Clicked assistant message content:", message.content);
if (!assistantMessageId) {
console.warn("Warning: Clicked assistant message has a null ID. Matching tool result will likely fail.");
const handleToolClick = useCallback((clickedAssistantMessageId: string | null, clickedToolName: string) => {
if (!clickedAssistantMessageId) {
console.warn("Clicked assistant message ID is null. Cannot open side panel.");
toast.warning("Cannot view details: Assistant message ID is missing.");
return;
}
// Find the corresponding tool result message for this assistant message
const resultMessage = messages.find(m => {
if (m.type !== 'tool' || !m.metadata) return false;
console.log("Tool Click Triggered. Assistant Message ID:", clickedAssistantMessageId, "Tool Name:", clickedToolName);
const historicalToolPairs: ToolCallInput[] = [];
const assistantMessages = messages.filter(m => m.type === 'assistant' && m.message_id);
assistantMessages.forEach(assistantMsg => {
// We need to parse the content to see if it actually contains tool calls
// For simplicity, we assume any assistant message *might* have a corresponding tool result
// A more robust solution would parse assistantMsg.content for tool XML
try {
const metadata = JSON.parse(m.metadata);
// Ensure both IDs exist for a valid comparison
return assistantMessageId && metadata.assistant_message_id === assistantMessageId;
} catch (e) {
console.error("Error parsing metadata for tool message:", m.message_id, e);
return false;
const resultMessage = messages.find(toolMsg => {
if (toolMsg.type !== 'tool' || !toolMsg.metadata || !assistantMsg.message_id) return false;
try {
const metadata = JSON.parse(toolMsg.metadata);
return metadata.assistant_message_id === assistantMsg.message_id;
} catch (e) {
return false;
}
});
if (resultMessage) {
// Try to get the specific tool name from the result metadata if possible,
// otherwise fallback to a generic name or the one passed from the click.
let toolNameForResult = clickedToolName; // Fallback
try {
const assistantContentParsed = safeJsonParse<{ tool_calls?: { name: string }[] }>(assistantMsg.content, {});
// A simple heuristic: if the assistant message content has tool_calls structure
if (assistantContentParsed.tool_calls && assistantContentParsed.tool_calls.length > 0) {
toolNameForResult = assistantContentParsed.tool_calls[0].name || clickedToolName;
}
// More advanced: parse the XML in assistant message content to find the tool name associated with this result
} catch {}
let isSuccess = true;
try {
const toolContent = resultMessage.content?.toLowerCase() || '';
isSuccess = !(toolContent.includes('failed') ||
toolContent.includes('error') ||
toolContent.includes('failure'));
} catch {}
historicalToolPairs.push({
assistantCall: {
name: toolNameForResult,
content: assistantMsg.content,
timestamp: assistantMsg.created_at
},
toolResult: {
content: resultMessage.content,
isSuccess: isSuccess,
timestamp: resultMessage.created_at
}
});
} else {
// Optionally handle assistant messages with tool calls but no result yet (or error in result)
// console.log(`No tool result found for assistant message: ${assistantMsg.message_id}`);
}
});
let toolCall: ToolCallInput;
if (historicalToolPairs.length === 0) {
console.warn("No historical tool pairs found to display.");
toast.info("No tool call details available to display.");
return;
}
if (resultMessage) {
console.log("Found matching tool result:", resultMessage.message_id);
console.log("Tool result content:", resultMessage.content);
console.log("Tool result metadata:", resultMessage.metadata);
let isSuccess = true; // Default to success
try {
// Basic check in content for failure keywords
const toolContent = resultMessage.content?.toLowerCase() || '';
isSuccess = !(toolContent.includes('failed') ||
toolContent.includes('error') ||
toolContent.includes('failure'));
} catch (e) {
console.error("Error checking tool success status:", e);
}
// Find the index of the specific pair that was clicked
const clickedIndex = historicalToolPairs.findIndex(pair =>
pair.assistantCall.timestamp === messages.find(m => m.message_id === clickedAssistantMessageId)?.created_at
);
toolCall = {
assistantCall: {
name: toolInfo.name,
content: message.content // Store the original assistant message content
},
toolResult: {
content: resultMessage.content,
isSuccess: isSuccess
}
};
if (clickedIndex === -1) {
console.error("Could not find the clicked tool call pair in the generated list. Displaying the first one.");
setToolCalls(historicalToolPairs);
setCurrentToolIndex(0); // Fallback to the first item
} else {
console.log(`No matching tool result found for assistant message ID: ${assistantMessageId}`);
// Log details of available tool messages for debugging
const availableToolResults = messages
.filter(m => m.type === 'tool')
.map(m => {
let assistantId = 'unknown';
try {
if (m.metadata) {
const meta = JSON.parse(m.metadata);
assistantId = meta.assistant_message_id || 'missing';
}
} catch { }
return { tool_msg_id: m.message_id, links_to_assistant_id: assistantId };
});
console.log("Available tool results in state:", availableToolResults);
toolCall = {
assistantCall: {
name: toolInfo.name,
content: message.content
},
toolResult: {
content: `No matching tool result found for assistant message ID: ${assistantMessageId}. See console logs for details.`,
isSuccess: false
}
};
setToolCalls(historicalToolPairs);
setCurrentToolIndex(clickedIndex);
}
setToolCalls([toolCall]); // Display only the clicked tool call
setCurrentToolIndex(0);
setIsSidePanelOpen(true);
}, [messages]);
// Handle streaming tool calls
// Handle streaming tool calls - Temporarily disable opening side panel
const handleStreamingToolCall = useCallback((toolCall: StreamingToolCall | null) => {
if (!toolCall) return;
const newToolCall: ToolCallInput = {
assistantCall: {
name: toolCall.name || toolCall.xml_tag_name || 'Unknown Tool',
content: toolCall.arguments || ''
}
};
setToolCalls([newToolCall]);
setCurrentToolIndex(0);
setIsSidePanelOpen(true);
console.log("[STREAM] Received tool call:", toolCall.name || toolCall.xml_tag_name);
// --- Temporarily disable opening side panel for streaming calls ---
// const newToolCall: ToolCallInput = {
// assistantCall: {
// name: toolCall.name || toolCall.xml_tag_name || 'Unknown Tool',
// content: toolCall.arguments || ''
// // No timestamp available easily here
// }
// // No toolResult available yet
// };
// setToolCalls([newToolCall]);
// setCurrentToolIndex(0);
// setIsSidePanelOpen(true);
// --- End temporary disable ---
}, []);
// Update useEffect to handle streaming tool calls
@ -538,11 +572,6 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
}
}, [streamingToolCall, handleStreamingToolCall]);
const mainContentStyle = {
marginRight: isSidePanelOpen ? "384px" : "0",
transition: "margin-right 0.2s ease-in-out"
};
if (isLoading && !initialLoadCompleted.current) {
return (
<div className="flex h-screen">
@ -714,11 +743,8 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
contentParts.push(
<button
key={`tool-${match.index}`}
onClick={() => handleToolClick(message, {
name: toolName,
args: toolArgs,
xml: rawXml
})}
// Pass assistant message ID and tool name
onClick={() => handleToolClick(message.message_id, toolName)}
className="inline-flex items-center gap-1.5 py-0.5 px-2 my-0.5 text-xs text-gray-600 bg-gray-100 hover:bg-gray-200 rounded-md transition-colors cursor-pointer border border-gray-200"
>
<IconComponent className="h-3.5 w-3.5 text-gray-500 flex-shrink-0" />
@ -850,38 +876,89 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
<div className="flex items-start gap-3">
<div className="flex-shrink-0 w-5 h-5 rounded-full flex items-center justify-center overflow-hidden bg-gray-200">
<Image src="/kortix-symbol.svg" alt="Suna" width={14} height={14} className="object-contain"/>
</div>
</div>
<div className="flex-1 space-y-2">
<div className="max-w-[85%] rounded-lg bg-muted px-4 py-3 text-sm">
{streamingTextContent && (
<span className="whitespace-pre-wrap break-words">
{streamingTextContent}
</span>
)}
{(streamHookStatus === 'streaming' || streamHookStatus === 'connecting') && <span className="inline-block h-4 w-0.5 bg-gray-400 ml-0.5 -mb-1 animate-pulse" />}
{(() => {
let detectedTag: string | null = null;
let tagStartIndex = -1;
{streamingToolCall && (
<div className="mt-2">
{(() => {
const toolName = streamingToolCall.name || streamingToolCall.xml_tag_name || 'Tool';
const IconComponent = getToolIcon(toolName);
const paramDisplay = extractPrimaryParam(toolName, streamingToolCall.arguments || '');
return (
<button
className="inline-flex items-center gap-1.5 py-0.5 px-2 text-xs text-gray-600 bg-gray-100 hover:bg-gray-200 rounded-md transition-colors cursor-pointer border border-gray-200"
>
<CircleDashed className="h-3.5 w-3.5 text-gray-500 flex-shrink-0 animate-spin animation-duration-2000" />
<span className="font-mono text-xs text-gray-700">{toolName}</span>
{paramDisplay && <span className="ml-1 text-gray-500 truncate" title={paramDisplay}>{paramDisplay}</span>}
</button>
);
if (streamingTextContent) {
for (const tag of HIDE_STREAMING_XML_TAGS) {
const openingTagPattern = `<${tag}`; // Check for <tagname or <tagname>
const index = streamingTextContent.indexOf(openingTagPattern);
if (index !== -1) {
// Found an opening tag from the list
detectedTag = tag;
tagStartIndex = index;
break; // Stop after finding the first one
}
}
}
if (detectedTag && tagStartIndex !== -1) {
// Render text before the tag and the preview button
const textBeforeTag = streamingTextContent.substring(0, tagStartIndex);
const IconComponent = getToolIcon(detectedTag);
// Note: We don't have parsed args here, just the name
return (
<>
{textBeforeTag && (
<span className="whitespace-pre-wrap break-words">
{textBeforeTag}
</span>
)}
<div className="mt-2">
<button
className="inline-flex items-center gap-1.5 py-0.5 px-2 text-xs text-gray-600 bg-gray-100 hover:bg-gray-200 rounded-md transition-colors cursor-pointer border border-gray-200"
>
<CircleDashed className="h-3.5 w-3.5 text-gray-500 flex-shrink-0 animate-spin animation-duration-2000" />
<span className="font-mono text-xs text-gray-700">{detectedTag}</span>
{/* No paramDisplay available here easily */}
</button>
</div>
</>
);
} else {
// Render normally: full text + blinking cursor
// And potentially the fully parsed tool call from the hook
return (
<>
{streamingTextContent && (
<span className="whitespace-pre-wrap break-words">
{streamingTextContent}
</span>
)}
{(streamHookStatus === 'streaming' || streamHookStatus === 'connecting') && !detectedTag && (
<span className="inline-block h-4 w-0.5 bg-gray-400 ml-0.5 -mb-1 animate-pulse" />
)}
{/* Render fully parsed tool call if available AND no hidden tag detected */}
{streamingToolCall && !detectedTag && (
<div className="mt-2">
{(() => {
const toolName = streamingToolCall.name || streamingToolCall.xml_tag_name || 'Tool';
const IconComponent = getToolIcon(toolName);
const paramDisplay = extractPrimaryParam(toolName, streamingToolCall.arguments || '');
return (
<button
className="inline-flex items-center gap-1.5 py-0.5 px-2 text-xs text-gray-600 bg-gray-100 hover:bg-gray-200 rounded-md transition-colors cursor-pointer border border-gray-200"
>
<CircleDashed className="h-3.5 w-3.5 text-gray-500 flex-shrink-0 animate-spin animation-duration-2000" />
<span className="font-mono text-xs text-gray-700">{toolName}</span>
{paramDisplay && <span className="ml-1 text-gray-500 truncate" title={paramDisplay}>{paramDisplay}</span>}
</button>
);
})()}
</div>
)}
</>
);
}
})()}
</div>
)}
</div>
</div>
</div>
</div>
</div>
)}
{agentStatus === 'running' && !streamingTextContent && !streamingToolCall && messages.length > 0 && messages[messages.length-1].type === 'user' && (
<div ref={latestMessageRef}>

View File

@ -9,19 +9,40 @@ export interface ToolCallInput {
assistantCall: {
content?: string;
name?: string;
timestamp?: string;
};
toolResult?: {
content?: string;
isSuccess?: boolean;
timestamp?: string;
};
}
// Helper function to format timestamp
function formatTimestamp(isoString?: string): string {
if (!isoString) return 'No timestamp';
try {
return new Date(isoString).toLocaleString();
} catch (e) {
return 'Invalid date';
}
}
// Simplified generic tool view
function GenericToolView({ name, assistantContent, toolContent, isSuccess = true }: {
function GenericToolView({
name,
assistantContent,
toolContent,
isSuccess = true,
assistantTimestamp,
toolTimestamp
}: {
name?: string;
assistantContent?: string;
toolContent?: string;
isSuccess?: boolean;
assistantTimestamp?: string;
toolTimestamp?: string;
}) {
const toolName = name || 'Unknown Tool';
@ -48,7 +69,12 @@ function GenericToolView({ name, assistantContent, toolContent, isSuccess = true
{/* Assistant Message */}
<div className="space-y-1">
<div className="text-xs font-medium text-muted-foreground">Assistant Message</div>
<div className="flex justify-between items-center">
<div className="text-xs font-medium text-muted-foreground">Assistant Message</div>
{assistantTimestamp && (
<div className="text-xs text-muted-foreground">{formatTimestamp(assistantTimestamp)}</div>
)}
</div>
<div className="rounded-md border bg-muted/50 p-3">
<pre className="text-xs overflow-auto whitespace-pre-wrap break-words">{assistantContent}</pre>
</div>
@ -57,7 +83,12 @@ function GenericToolView({ name, assistantContent, toolContent, isSuccess = true
{/* Tool Result */}
{toolContent && (
<div className="space-y-1">
<div className="text-xs font-medium text-muted-foreground">Tool Result</div>
<div className="flex justify-between items-center">
<div className="text-xs font-medium text-muted-foreground">Tool Result</div>
{toolTimestamp && (
<div className="text-xs text-muted-foreground">{formatTimestamp(toolTimestamp)}</div>
)}
</div>
<div className={`rounded-md border p-3 ${isSuccess ? 'bg-muted/50' : 'bg-red-50'}`}>
<pre className="text-xs overflow-auto whitespace-pre-wrap break-words">{toolContent}</pre>
</div>
@ -102,14 +133,16 @@ export function ToolCallSidePanel({
<GenericToolView
name={currentToolCall.assistantCall.name}
assistantContent={currentToolCall.assistantCall.content}
assistantTimestamp={currentToolCall.assistantCall.timestamp}
toolContent={currentToolCall.toolResult?.content}
isSuccess={currentToolCall.toolResult?.isSuccess}
toolTimestamp={currentToolCall.toolResult?.timestamp}
/>
);
};
return (
<div className="fixed inset-y-0 right-0 w-[90%] sm:w-[450px] md:w-[500px] lg:w-[550px] xl:w-[600px] bg-background border-l shadow-lg flex flex-col z-10">
<div className="fixed inset-y-0 right-0 w-[90%] sm:w-[450px] md:w-[500px] lg:w-[550px] xl:w-[600px] bg-background border-l flex flex-col z-10">
<div className="p-4 flex items-center justify-between">
<h3 className="text-sm font-medium">Tool Details</h3>
<Button variant="ghost" size="icon" onClick={onClose}>