diff --git a/backend/agent/tools/sb_files_tool.py b/backend/agent/tools/sb_files_tool.py index 847f54b3..e0b66ac1 100644 --- a/backend/agent/tools/sb_files_tool.py +++ b/backend/agent/tools/sb_files_tool.py @@ -525,10 +525,13 @@ def authenticate_user(username, password): # AI editing successful await self.sandbox.fs.upload_file(new_content.encode(), full_path) - - message = f"File '{target_file}' edited successfully." - - return self.success_response(message) + # Return rich data for frontend diff view + return self.success_response({ + "message": f"File '{target_file}' edited successfully.", + "file_path": target_file, + "original_content": original_content, + "updated_content": new_content + }) except Exception as e: logger.error(f"Unhandled error in edit_file: {str(e)}", exc_info=True) diff --git a/backend/agentpress/response_processor.py b/backend/agentpress/response_processor.py index 84059a77..ff2e7ea1 100644 --- a/backend/agentpress/response_processor.py +++ b/backend/agentpress/response_processor.py @@ -1637,23 +1637,35 @@ class ResponseProcessor: # Determine message role based on strategy result_role = "user" if strategy == "user_message" else "assistant" - # Create the new structured tool result format - structured_result = self._create_structured_tool_result(tool_call, result, parsing_details) - + # Create two versions of the structured result + # 1. Rich version for the frontend + structured_result_for_frontend = self._create_structured_tool_result(tool_call, result, parsing_details, for_llm=False) + # 2. Concise version for the LLM + structured_result_for_llm = self._create_structured_tool_result(tool_call, result, parsing_details, for_llm=True) + # Add the message with the appropriate role to the conversation history # This allows the LLM to see the tool result in subsequent interactions - result_message = { + result_message_for_llm = { "role": result_role, - "content": json.dumps(structured_result) + "content": json.dumps(structured_result_for_llm) } message_obj = await self.add_message( thread_id=thread_id, type="tool", - content=result_message, + content=result_message_for_llm, # Save the LLM-friendly version is_llm_message=True, metadata=metadata ) - return message_obj # Return the full message object + + # If the message was saved, modify it in-memory for the frontend before returning + if message_obj: + result_message_for_frontend = { + "role": result_role, + "content": json.dumps(structured_result_for_frontend) + } + message_obj['content'] = result_message_for_frontend + + return message_obj # Return the modified message object except Exception as e: logger.error(f"Error adding tool result: {str(e)}", exc_info=True) self.trace.event(name="error_adding_tool_result", level="ERROR", status_message=(f"Error adding tool result: {str(e)}"), metadata={"tool_call": tool_call, "result": result, "strategy": strategy, "assistant_message_id": assistant_message_id, "parsing_details": parsing_details}) @@ -1676,13 +1688,14 @@ class ResponseProcessor: self.trace.event(name="failed_even_with_fallback_message", level="ERROR", status_message=(f"Failed even with fallback message: {str(e2)}"), metadata={"tool_call": tool_call, "result": result, "strategy": strategy, "assistant_message_id": assistant_message_id, "parsing_details": parsing_details}) return None # Return None on error - def _create_structured_tool_result(self, tool_call: Dict[str, Any], result: ToolResult, parsing_details: Optional[Dict[str, Any]] = None): + def _create_structured_tool_result(self, tool_call: Dict[str, Any], result: ToolResult, parsing_details: Optional[Dict[str, Any]] = None, for_llm: bool = False): """Create a structured tool result format that's tool-agnostic and provides rich information. Args: tool_call: The original tool call that was executed result: The result from the tool execution parsing_details: Optional parsing details for XML calls + for_llm: If True, creates a concise version for the LLM context. Returns: Structured dictionary containing tool execution information @@ -1692,7 +1705,6 @@ class ResponseProcessor: xml_tag_name = tool_call.get("xml_tag_name") arguments = tool_call.get("arguments", {}) tool_call_id = tool_call.get("id") - logger.info(f"Creating structured tool result for tool_call: {tool_call}") # Process the output - if it's a JSON string, parse it back to an object output = result.output if hasattr(result, 'output') else str(result) @@ -1707,7 +1719,12 @@ class ResponseProcessor: except Exception: # If parsing fails, keep the original string pass - + + # If this is for the LLM and it's an edit_file tool, create a concise output + if for_llm and function_name == 'edit_file' and isinstance(output, dict): + output_for_llm = {"message": output.get("message", "File edited successfully.")} + output = output_for_llm + # Create the structured result structured_result_v1 = { "tool_execution": { @@ -1717,53 +1734,11 @@ class ResponseProcessor: "arguments": arguments, "result": { "success": result.success if hasattr(result, 'success') else True, - "output": output, # Now properly structured for frontend + "output": output, # This will be either rich or concise based on `for_llm` "error": getattr(result, 'error', None) if hasattr(result, 'error') else None }, - # "execution_details": { - # "timestamp": datetime.now(timezone.utc).isoformat(), - # "parsing_details": parsing_details - # } } } - - # STRUCTURED_OUTPUT_TOOLS = { - # "str_replace", - # "get_data_provider_endpoints", - # } - - # summary_output = result.output if hasattr(result, 'output') else str(result) - - # if xml_tag_name: - # status = "completed successfully" if structured_result_v1["tool_execution"]["result"]["success"] else "failed" - # summary = f"Tool '{xml_tag_name}' {status}. Output: {summary_output}" - # else: - # status = "completed successfully" if structured_result_v1["tool_execution"]["result"]["success"] else "failed" - # summary = f"Function '{function_name}' {status}. Output: {summary_output}" - - # if self.is_agent_builder: - # return summary - # if function_name in STRUCTURED_OUTPUT_TOOLS: - # return structured_result_v1 - # else: - # return summary - - summary_output = result.output if hasattr(result, 'output') else str(result) - success_status = structured_result_v1["tool_execution"]["result"]["success"] - - # # Create a more comprehensive summary for the LLM - # if xml_tag_name: - # status = "completed successfully" if structured_result_v1["tool_execution"]["result"]["success"] else "failed" - # summary = f"Tool '{xml_tag_name}' {status}. Output: {summary_output}" - # else: - # status = "completed successfully" if structured_result_v1["tool_execution"]["result"]["success"] else "failed" - # summary = f"Function '{function_name}' {status}. Output: {summary_output}" - - # if self.is_agent_builder: - # return summary - # elif function_name == "get_data_provider_endpoints": - # logger.info(f"Returning sumnary for data provider call: {summary}") - # return summary return structured_result_v1 diff --git a/frontend/src/components/thread/content/ShowToolStream.tsx b/frontend/src/components/thread/content/ShowToolStream.tsx index bd76d7de..a393c293 100644 --- a/frontend/src/components/thread/content/ShowToolStream.tsx +++ b/frontend/src/components/thread/content/ShowToolStream.tsx @@ -9,6 +9,7 @@ const FILE_OPERATION_TOOLS = new Set([ 'Delete File', 'Full File Rewrite', 'Read File', + 'AI File Edit', ]); interface ShowToolStreamProps { @@ -38,6 +39,18 @@ export const ShowToolStream: React.FC = ({ } const toolName = extractToolNameFromStream(content); + const isEditFile = toolName === 'AI File Edit'; + + // Extract code_edit content for streaming + const codeEditContent = React.useMemo(() => { + if (!isEditFile || !content) return ''; + const match = content.match(/([\s\S]*)/); + if (match) { + // Remove closing tag if present + return match[1].replace(/<\/code_edit>[\s\S]*$/, ''); + } + return ''; + }, [content, isEditFile]); // Time-based logic - show streaming content after 1500ms useEffect(() => { @@ -97,7 +110,7 @@ export const ShowToolStream: React.FC = ({ const paramDisplay = extractPrimaryParam(toolName, content); // Always show tool button, conditionally show content below for file operations only - if (showExpanded && isFileOperationTool) { + if (showExpanded && (isFileOperationTool || isEditFile)) { return (
{shouldShowContent ? ( @@ -126,7 +139,7 @@ export const ShowToolStream: React.FC = ({ WebkitMaskImage: 'linear-gradient(to bottom, transparent 0%, black 8%, black 92%, transparent 100%)' }} > - {content} + {isEditFile ? codeEditContent : content}
{/* Top gradient */}
= ({ oldCode, newCode }) => ( + +); + +const SplitDiffView: React.FC<{ oldCode: string; newCode: string }> = ({ oldCode, newCode }) => ( + +); + +const ErrorState: React.FC = () => ( +
+
+ +

+ Invalid File Edit +

+

+ Could not extract the file changes from the tool result. +

+
+
+); + +export function FileEditToolView({ + name = 'edit-file', + assistantContent, + toolContent, + assistantTimestamp, + toolTimestamp, + isSuccess = true, + isStreaming = false, +}: ToolViewProps): JSX.Element { + const [viewMode, setViewMode] = useState<'unified' | 'split'>('unified'); + + const { + filePath, + originalContent, + updatedContent, + actualIsSuccess, + actualToolTimestamp, + } = extractFileEditData( + assistantContent, + toolContent, + isSuccess, + toolTimestamp, + assistantTimestamp + ); + + const toolTitle = getToolTitle(name); + + const lineDiff = originalContent && updatedContent ? generateLineDiff(originalContent, updatedContent) : []; + const stats: DiffStats = calculateDiffStats(lineDiff); + + const shouldShowError = !isStreaming && (!originalContent || !updatedContent); + + return ( + + +
+
+
+ +
+ + {toolTitle} + +
+ + {!isStreaming && ( + + {actualIsSuccess ? ( + + ) : ( + + )} + {actualIsSuccess ? 'Edit applied' : 'Edit failed'} + + )} +
+
+ + + {isStreaming ? ( + + ) : shouldShowError ? ( + + ) : ( +
+
+
+ + + {filePath || 'Unknown file'} + +
+ +
+
+
+ + {stats.additions} +
+
+ + {stats.deletions} +
+
+ setViewMode(v as 'unified' | 'split')} className="w-auto"> + + Unified + Split + + +
+
+ + {viewMode === 'unified' ? ( + + ) : ( + + )} + +
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/thread/tool-views/file-operation/_utils.ts b/frontend/src/components/thread/tool-views/file-operation/_utils.ts index d9519cc4..cb6a735c 100644 --- a/frontend/src/components/thread/tool-views/file-operation/_utils.ts +++ b/frontend/src/components/thread/tool-views/file-operation/_utils.ts @@ -1,6 +1,6 @@ import { LucideIcon, FilePen, Replace, Trash2, FileCode, FileSpreadsheet, File } from 'lucide-react'; -export type FileOperation = 'create' | 'rewrite' | 'delete' | 'edit'; +export type FileOperation = 'create' | 'rewrite' | 'delete' | 'edit' | 'str-replace'; export interface OperationConfig { icon: LucideIcon; @@ -77,12 +77,99 @@ export const getLanguageFromFileName = (fileName: string): string => { return extensionMap[extension] || 'text'; }; +export interface ExtractedEditData { + filePath: string | null; + originalContent: string | null; + updatedContent: string | null; + success?: boolean; + timestamp?: string; +} + +export const extractFileEditData = ( + assistantContent: any, + toolContent: any, + isSuccess: boolean, + toolTimestamp?: string, + assistantTimestamp?: string +): { + filePath: string | null; + originalContent: string | null; + updatedContent: string | null; + actualIsSuccess: boolean; + actualToolTimestamp?: string; + actualAssistantTimestamp?: string; +} => { + let filePath: string | null = null; + let originalContent: string | null = null; + let updatedContent: string | null = null; + let actualIsSuccess = isSuccess; + let actualToolTimestamp = toolTimestamp; + let actualAssistantTimestamp = assistantTimestamp; + + const parseOutput = (output: any) => { + if (typeof output === 'string') { + try { + return JSON.parse(output); + } catch { + return null; + } + } + return output; + }; + + const extractData = (content: any) => { + const parsed = typeof content === 'string' ? parseContent(content) : content; + if (parsed?.tool_execution) { + const args = parsed.tool_execution.arguments || {}; + const output = parseOutput(parsed.tool_execution.result?.output); + return { + filePath: args.target_file || output?.file_path || null, + originalContent: output?.original_content || null, + updatedContent: output?.updated_content || null, + success: parsed.tool_execution.result?.success, + timestamp: parsed.tool_execution.execution_details?.timestamp, + }; + } + return {}; + }; + + const toolData = extractData(toolContent); + const assistantData = extractData(assistantContent); + + filePath = toolData.filePath || assistantData.filePath; + originalContent = toolData.originalContent || assistantData.originalContent; + updatedContent = toolData.updatedContent || assistantData.updatedContent; + + if (toolData.success !== undefined) { + actualIsSuccess = toolData.success; + actualToolTimestamp = toolData.timestamp || toolTimestamp; + } else if (assistantData.success !== undefined) { + actualIsSuccess = assistantData.success; + actualAssistantTimestamp = assistantData.timestamp || assistantTimestamp; + } + + return { filePath, originalContent, updatedContent, actualIsSuccess, actualToolTimestamp, actualAssistantTimestamp }; +}; + +const parseContent = (content: any): any => { + if (typeof content === 'string') { + try { + return JSON.parse(content); + } catch (e) { + return content; + } + } + return content; +}; + + export const getOperationType = (name?: string, assistantContent?: any): FileOperation => { if (name) { if (name.includes('create')) return 'create'; if (name.includes('rewrite')) return 'rewrite'; if (name.includes('delete')) return 'delete'; - if (name.includes('edit')) return 'edit'; + if (name.includes('edit-file')) return 'edit'; // Specific for edit_file + if (name.includes('str-replace')) return 'str-replace'; } if (!assistantContent) return 'create'; diff --git a/frontend/src/components/thread/tool-views/wrapper/ToolViewRegistry.tsx b/frontend/src/components/thread/tool-views/wrapper/ToolViewRegistry.tsx index 5ea4afcc..5484cea8 100644 --- a/frontend/src/components/thread/tool-views/wrapper/ToolViewRegistry.tsx +++ b/frontend/src/components/thread/tool-views/wrapper/ToolViewRegistry.tsx @@ -5,6 +5,7 @@ import { BrowserToolView } from '../BrowserToolView'; import { CommandToolView } from '../command-tool/CommandToolView'; import { ExposePortToolView } from '../expose-port-tool/ExposePortToolView'; import { FileOperationToolView } from '../file-operation/FileOperationToolView'; +import { FileEditToolView } from '../file-operation/FileEditToolView'; import { StrReplaceToolView } from '../str-replace/StrReplaceToolView'; import { WebCrawlToolView } from '../WebCrawlToolView'; import { WebScrapeToolView } from '../web-scrape-tool/WebScrapeToolView'; @@ -56,7 +57,7 @@ const defaultRegistry: ToolViewRegistryType = { 'delete-file': FileOperationToolView, 'full-file-rewrite': FileOperationToolView, 'read-file': FileOperationToolView, - 'edit-file': FileOperationToolView, + 'edit-file': FileEditToolView, 'str-replace': StrReplaceToolView,