From cb5f68ae7b9e8e013403ef59e536534412b236f0 Mon Sep 17 00:00:00 2001 From: marko-kraemer Date: Fri, 11 Apr 2025 02:35:29 +0100 Subject: [PATCH] wip --- backend/agent/prompt.py | 54 +++- backend/agent/run.py | 30 +- backend/agent/sandbox_api.py | 402 +++++++++++++++++++++++++ backend/agent/tools/message_tool.py | 29 +- backend/agent/tools/sb_files_tool.py | 146 +-------- backend/agent/tools/web_search_tool.py | 226 +++++++++----- backend/api.py | 4 + 7 files changed, 638 insertions(+), 253 deletions(-) create mode 100644 backend/agent/sandbox_api.py diff --git a/backend/agent/prompt.py b/backend/agent/prompt.py index 351a6836..cc0ac7d9 100644 --- a/backend/agent/prompt.py +++ b/backend/agent/prompt.py @@ -4,11 +4,37 @@ You are Suna.so, an autonomous AI Agent created by the Kortix team. # IDENTITY AND CAPABILITIES You are a full-spectrum autonomous agent capable of executing complex tasks across domains including information gathering, content creation, software development, data analysis, and problem-solving. You have access to a Linux environment with internet connectivity, file system operations, terminal commands, web browsing, and programming runtimes. +# EXECUTION CAPABILITIES +You have the ability to execute a wide range of operations using Python and CLI tools: + +1. FILE OPERATIONS: + - Creating, reading, modifying, and deleting files + - Organizing files into directories/folders + - Converting between file formats + - Searching through file contents + - Batch processing multiple files + +2. DATA PROCESSING: + - Scraping and extracting data from websites + - Parsing structured data (JSON, CSV, XML) + - Cleaning and transforming datasets + - Analyzing data using Python libraries + - Generating reports and visualizations + +3. SYSTEM OPERATIONS: + - Running CLI commands and scripts + - Compressing and extracting archives (zip, tar) + - Installing necessary packages and dependencies + - Monitoring system resources and processes + - Executing scheduled or event-driven tasks + +For any of these operations, you can leverage both Python code execution and CLI commands to achieve the desired outcome efficiently. Choose the most appropriate approach based on the task requirements. + # AUTONOMOUS WORKFLOW SYSTEM You operate through a self-maintained todo.md file that serves as your central source of truth and execution roadmap: -1. Upon receiving a task, you immediately create a comprehensive todo.md with 5-10 major sections covering the entire task lifecycle -2. Each section contains 3-10 specific, actionable subtasks with clear completion criteria +1. Upon receiving a task, you immediately create a lean, focused todo.md with essential sections covering the task lifecycle +2. Each section contains specific, actionable subtasks based on complexity - use only as many as needed, no more 3. Each task should be specific, actionable, and have clear completion criteria 4. You MUST actively work through these tasks one by one, checking them off as you complete them 5. You adapt the plan as needed while maintaining its integrity as your execution compass @@ -30,6 +56,7 @@ The todo.md file is your primary working document and action plan: 12. FINALITY: After marking a section complete, do not reopen it or add new tasks to it unless explicitly directed by the user 13. STOPPING CONDITION: If you've made 3 consecutive updates to todo.md without completing any tasks, you MUST reassess your approach and either simplify your plan or ask for user guidance 14. COMPLETION VERIFICATION: Only mark a task as [x] complete when you have concrete evidence of completion. For each task, verify the output, check for errors, and confirm the result matches the expected outcome before marking it complete. +15. SIMPLICITY: Keep your todo.md lean and direct. Write tasks in simple language with clear actions. Avoid verbose descriptions, unnecessary subtasks, or overly granular breakdowns. Focus on essential steps that drive meaningful progress. # EXECUTION PHILOSOPHY Your approach is deliberately methodical and persistent: @@ -43,9 +70,25 @@ Your approach is deliberately methodical and persistent: 7. CRITICALLY IMPORTANT: You MUST ALWAYS explicitly use one of these two tools when you've completed your task or need user input # TECHNICAL PROTOCOLS -- COMMUNICATION: Use message tools (notify for updates, ask only when essential). IF NECESSARY, include the 'attachments' parameter with paths to any created files or URLs when using message_notify_user or message_ask_user tools - without this parameter, the user cannot properly view file or website contents. -- TOOL RESULTS: After each tool execution, you will receive the results in your messages. You MUST carefully analyze these results to determine your next actions. These results contain critical information from your environment including file contents, execution outputs, search results, and more. Every decision you make should be informed by these tool results. -- FILES: Create organized file structures with clear naming conventions +- COMMUNICATION: Use message tools for updates and essential questions. Include the 'attachments' parameter with file paths or URLs when sharing resources with users. +- TOOL RESULTS: Carefully analyze all tool execution results to inform your next actions. These results provide critical environmental information including file contents, execution outputs, and search results. +- FILES: Create organized file structures with clear naming conventions. Store different types of data in appropriate formats. +- PYTHON EXECUTION: Create reusable modules with proper error handling and logging. Focus on maintainability and readability. +- CLI OPERATIONS: + * Use terminal commands for system operations, file manipulations, and quick tasks + * Avoid commands requiring confirmation; actively use -y or -f flags for automatic confirmation + * Avoid commands with excessive output; save to files when necessary + * Chain multiple commands with && operator to minimize interruptions + * Use pipe operator to pass command outputs, simplifying operations + * Use non-interactive `bc` for simple calculations, Python for complex math; never calculate mentally + * Use `uptime` command when users explicitly request sandbox status check or wake-up +- CODING: + * Must save code to files before execution; direct code input to interpreter commands is forbidden + * Write Python code for complex mathematical calculations and analysis + * Use search tools to find solutions when encountering unfamiliar problems + * For index.html referencing local resources, use deployment tools directly, or package everything into a zip file and provide it as a message attachment +- HYBRID APPROACH: Combine Python and CLI as needed - use Python for logic and data processing, CLI for system operations and utilities. +- WRITING: Use flowing paragraphs rather than lists; provide detailed content with proper citations. # FILES TOOL USAGE - Use file tools for reading, writing, appending, and editing to avoid string escape issues in shell commands @@ -61,7 +104,6 @@ Your approach is deliberately methodical and persistent: - For lengthy documents, first save each section as separate draft files, then append them sequentially to create the final document - During final compilation, no content should be reduced or summarized; the final length must exceed the sum of all individual draft files -- INFORMATION: Prioritize web search > model knowledge; document sources - SHELL: Use efficient command chaining and avoid interactive prompts - CODING: Save code to files before execution; implement error handling - WRITING: Use flowing paragraphs rather than lists; provide detailed content diff --git a/backend/agent/run.py b/backend/agent/run.py index d937abe3..b88e3126 100644 --- a/backend/agent/run.py +++ b/backend/agent/run.py @@ -41,6 +41,7 @@ async def run_agent(thread_id: str, project_id: str, stream: bool = True, thread # thread_manager.add_tool(SandboxBrowseTool, sandbox_id=sandbox_id, password=sandbox_pass) thread_manager.add_tool(SandboxShellTool, sandbox_id=sandbox_id, password=sandbox_pass) thread_manager.add_tool(SandboxFilesTool, sandbox_id=sandbox_id, password=sandbox_pass) + thread_manager.add_tool(MessageTool) files_tool = SandboxFilesTool(sandbox_id=sandbox_id, password=sandbox_pass) system_message = { "role": "system", "content": get_system_prompt() } @@ -69,20 +70,21 @@ async def run_agent(thread_id: str, project_id: str, stream: bool = True, thread print(f"Last message was from assistant, stopping execution") continue_execution = False break - files_state = await files_tool.get_workspace_state() - - # Simple string representation - state_str = str(files_state) - state_message = { - "role": "user", - "content": f""" -Current workspace state: - -{state_str} - - """ - } +# files_state = await files_tool.get_workspace_state() + +# # Simple string representation +# state_str = str(files_state) + +# state_message = { +# "role": "user", +# "content": f""" +# Current workspace state: +# +# {state_str} +# +# """ +# } # print(f"State message: {state_message}") @@ -90,7 +92,7 @@ Current workspace state: thread_id=thread_id, system_prompt=system_message, stream=stream, - temporary_message=state_message, + # temporary_message=state_message, llm_model=model_name, llm_temperature=0.1, llm_max_tokens=64000, diff --git a/backend/agent/sandbox_api.py b/backend/agent/sandbox_api.py new file mode 100644 index 00000000..bf384789 --- /dev/null +++ b/backend/agent/sandbox_api.py @@ -0,0 +1,402 @@ +import os +from typing import List, Optional, Union, BinaryIO + +from fastapi import FastAPI, UploadFile, File, HTTPException, APIRouter +from fastapi.responses import FileResponse, JSONResponse +from pydantic import BaseModel + +from utils.logger import logger +from backend.agent.tools.utils.daytona_sandbox import daytona + +class FileInfo(BaseModel): + """Model for file information""" + name: str + path: str + is_dir: bool + size: int + mod_time: str + permissions: Optional[str] = None + +class CreateFolderRequest(BaseModel): + """Request model for creating folders""" + path: str + permissions: Optional[str] = None + +class FileContentRequest(BaseModel): + """Request model for file content operations""" + path: str + content: str + +class SandboxAPI: + """API wrapper for Daytona file system operations""" + + def __init__(self, sandbox_id: str): + """Initialize the Sandbox API with a sandbox ID + + Args: + sandbox_id: The ID of the Daytona sandbox + """ + self.sandbox_id = sandbox_id + try: + self.sandbox = daytona.get_current_sandbox(sandbox_id) + logger.info(f"SandboxAPI initialized for sandbox: {sandbox_id}") + except Exception as e: + logger.error(f"Error getting sandbox with ID {sandbox_id}: {str(e)}") + raise e + + # CREATE operations + def create_file(self, path: str, content: Union[str, bytes]) -> bool: + """Create a new file in the sandbox + + Args: + path: The path where the file should be created + content: The content to write to the file + + Returns: + bool: True if the file was created successfully + """ + try: + if isinstance(content, str): + content = content.encode('utf-8') + + self.sandbox.fs.upload_file(path, content) + logger.info(f"File created at {path}") + return True + except Exception as e: + logger.error(f"Error creating file at {path}: {str(e)}") + raise e + + # def create_folder(self, path: str, permissions: Optional[str] = None) -> bool: + # """Create a new directory in the sandbox + # + # Args: + # path: The path where the directory should be created + # permissions: Optional permissions for the directory (e.g., "755") + # + # Returns: + # bool: True if the directory was created successfully + # """ + # try: + # if permissions: + # self.sandbox.fs.create_folder(path, permissions) + # else: + # self.sandbox.fs.create_folder(path) + # logger.info(f"Folder created at {path}") + # return True + # except Exception as e: + # logger.error(f"Error creating folder at {path}: {str(e)}") + # raise e + + # READ operations + def list_files(self, path: str) -> List[FileInfo]: + """List files and directories at the specified path + + Args: + path: The path to list files from + + Returns: + List[FileInfo]: List of files and directories + """ + try: + files = self.sandbox.fs.list_files(path) + result = [] + + for file in files: + # Convert file information to our model + file_info = FileInfo( + name=file.name, + path=os.path.join(path, file.name), + is_dir=file.is_dir, + size=file.size, + mod_time=str(file.mod_time), + permissions=getattr(file, 'permissions', None) + ) + result.append(file_info) + + return result + except Exception as e: + logger.error(f"Error listing files at {path}: {str(e)}") + raise e + + def read_file(self, path: str) -> bytes: + """Read a file from the sandbox + + Args: + path: The path of the file to read + + Returns: + bytes: The content of the file + """ + try: + content = self.sandbox.fs.download_file(path) + return content + except Exception as e: + logger.error(f"Error reading file at {path}: {str(e)}") + raise e + + def get_file_info(self, path: str) -> FileInfo: + """Get information about a file or directory + + Args: + path: The path to get information for + + Returns: + FileInfo: File information + """ + try: + file_info = self.sandbox.fs.get_file_info(path) + return FileInfo( + name=os.path.basename(path), + path=path, + is_dir=file_info.is_dir, + size=file_info.size, + mod_time=str(file_info.mod_time), + permissions=getattr(file_info, 'permissions', None) + ) + except Exception as e: + logger.error(f"Error getting file info for {path}: {str(e)}") + raise e + + # # UPDATE operations + # def update_file(self, path: str, content: Union[str, bytes]) -> bool: + # """Update an existing file in the sandbox + # + # Args: + # path: The path of the file to update + # content: The new content for the file + # + # Returns: + # bool: True if the file was updated successfully + # """ + # # For simplicity, we use the same method as create_file, as upload_file will overwrite + # return self.create_file(path, content) + + # def set_file_permissions(self, path: str, permissions: str) -> bool: + # """Set permissions for a file or directory + # + # Args: + # path: The path of the file or directory + # permissions: The permissions to set (e.g., "644", "755") + # + # Returns: + # bool: True if permissions were set successfully + # """ + # try: + # self.sandbox.fs.set_file_permissions(path, permissions) + # logger.info(f"Permissions set to {permissions} for {path}") + # return True + # except Exception as e: + # logger.error(f"Error setting permissions for {path}: {str(e)}") + # raise e + + # # DELETE operations + # def delete_file(self, path: str) -> bool: + # """Delete a file from the sandbox + # + # Args: + # path: The path of the file to delete + # + # Returns: + # bool: True if the file was deleted successfully + # """ + # try: + # self.sandbox.fs.delete_file(path) + # logger.info(f"File deleted at {path}") + # return True + # except Exception as e: + # logger.error(f"Error deleting file at {path}: {str(e)}") + # raise e + + # # SEARCH operations + # def find_files(self, path: str, pattern: str) -> List[dict]: + # """Search for text in files + # + # Args: + # path: The path to search in + # pattern: The text pattern to search for + # + # Returns: + # List[dict]: List of matches with file, line, and content + # """ + # try: + # results = self.sandbox.fs.find_files(path=path, pattern=pattern) + # return [ + # {"file": match.file, "line": match.line, "content": match.content} + # for match in results + # ] + # except Exception as e: + # logger.error(f"Error searching for pattern {pattern} in {path}: {str(e)}") + # raise e + + # def replace_in_files(self, files: List[str], pattern: str, new_value: str) -> bool: + # """Replace text in files + # + # Args: + # files: List of file paths to perform replacement in + # pattern: The text pattern to replace + # new_value: The replacement text + # + # Returns: + # bool: True if replacement was successful + # """ + # try: + # self.sandbox.fs.replace_in_files(files, pattern, new_value) + # logger.info(f"Replaced '{pattern}' with '{new_value}' in {len(files)} files") + # return True + # except Exception as e: + # logger.error(f"Error replacing '{pattern}' with '{new_value}': {str(e)}") + # raise e + +# Create a router for the Sandbox API instead of a standalone FastAPI app +router = APIRouter(tags=["sandbox"]) + +# Store sandbox instances +sandboxes = {} + +@router.post("/sandboxes/{sandbox_id}/connect") +async def connect_sandbox(sandbox_id: str): + """Connect to a sandbox and initialize the API""" + try: + sandboxes[sandbox_id] = SandboxAPI(sandbox_id) + return {"status": "connected", "sandbox_id": sandbox_id} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +# CREATE endpoints +@router.post("/sandboxes/{sandbox_id}/files") +async def create_file_endpoint(sandbox_id: str, file_request: FileContentRequest): + """Create a file in the sandbox""" + if sandbox_id not in sandboxes: + raise HTTPException(status_code=404, detail=f"Sandbox {sandbox_id} not connected") + + try: + result = sandboxes[sandbox_id].create_file(file_request.path, file_request.content) + return {"status": "success", "created": result} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +# READ endpoints +@router.get("/sandboxes/{sandbox_id}/files") +async def list_files_endpoint(sandbox_id: str, path: str): + """List files and directories at the specified path""" + if sandbox_id not in sandboxes: + raise HTTPException(status_code=404, detail=f"Sandbox {sandbox_id} not connected") + + try: + files = sandboxes[sandbox_id].list_files(path) + return {"files": [file.dict() for file in files]} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.get("/sandboxes/{sandbox_id}/files/content") +async def read_file_endpoint(sandbox_id: str, path: str): + """Read a file from the sandbox""" + if sandbox_id not in sandboxes: + raise HTTPException(status_code=404, detail=f"Sandbox {sandbox_id} not connected") + + try: + content = sandboxes[sandbox_id].read_file(path) + return FileResponse( + path=path, + filename=os.path.basename(path), + media_type="application/octet-stream", + content=content + ) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.get("/sandboxes/{sandbox_id}/files/info") +async def get_file_info_endpoint(sandbox_id: str, path: str): + """Get information about a file or directory""" + if sandbox_id not in sandboxes: + raise HTTPException(status_code=404, detail=f"Sandbox {sandbox_id} not connected") + + try: + file_info = sandboxes[sandbox_id].get_file_info(path) + return file_info.dict() + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +# # UPDATE endpoints +# @router.put("/sandboxes/{sandbox_id}/files") +# async def update_file_endpoint(sandbox_id: str, file_request: FileContentRequest): +# """Update a file in the sandbox""" +# if sandbox_id not in sandboxes: +# raise HTTPException(status_code=404, detail=f"Sandbox {sandbox_id} not connected") + +# try: +# result = sandboxes[sandbox_id].update_file(file_request.path, file_request.content) +# return {"status": "success", "updated": result} +# except Exception as e: +# raise HTTPException(status_code=500, detail=str(e)) + +# @router.put("/sandboxes/{sandbox_id}/files/permissions") +# async def set_permissions_endpoint(sandbox_id: str, path: str, permissions: str): +# """Set permissions for a file or directory""" +# if sandbox_id not in sandboxes: +# raise HTTPException(status_code=404, detail=f"Sandbox {sandbox_id} not connected") + +# try: +# result = sandboxes[sandbox_id].set_file_permissions(path, permissions) +# return {"status": "success", "updated": result} +# except Exception as e: +# raise HTTPException(status_code=500, detail=str(e)) + +# # DELETE endpoints +# @router.delete("/sandboxes/{sandbox_id}/files") +# async def delete_file_endpoint(sandbox_id: str, path: str): + """Delete a file from the sandbox""" + if sandbox_id not in sandboxes: + raise HTTPException(status_code=404, detail=f"Sandbox {sandbox_id} not connected") + + try: + result = sandboxes[sandbox_id].delete_file(path) + return {"status": "success", "deleted": result} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +# @router.post("/sandboxes/{sandbox_id}/folders") +# async def create_folder_endpoint(sandbox_id: str, folder_request: CreateFolderRequest): +# """Create a folder in the sandbox""" +# if sandbox_id not in sandboxes: +# raise HTTPException(status_code=404, detail=f"Sandbox {sandbox_id} not connected") + +# try: +# result = sandboxes[sandbox_id].create_folder( +# folder_request.path, +# folder_request.permissions +# ) +# return {"status": "success", "created": result} +# except Exception as e: +# raise HTTPException(status_code=500, detail=str(e)) + +# # SEARCH endpoints +# @router.get("/sandboxes/{sandbox_id}/search") +# async def search_files_endpoint(sandbox_id: str, path: str, pattern: str): +# """Search for text in files""" +# if sandbox_id not in sandboxes: +# raise HTTPException(status_code=404, detail=f"Sandbox {sandbox_id} not connected") + +# try: +# results = sandboxes[sandbox_id].find_files(path, pattern) +# return {"matches": results} +# except Exception as e: +# raise HTTPException(status_code=500, detail=str(e)) + +# @router.post("/sandboxes/{sandbox_id}/replace") +# async def replace_in_files_endpoint( +# sandbox_id: str, +# files: List[str], +# pattern: str, +# new_value: str +# ): +# """Replace text in files""" +# if sandbox_id not in sandboxes: +# raise HTTPException(status_code=404, detail=f"Sandbox {sandbox_id} not connected") + +# try: +# result = sandboxes[sandbox_id].replace_in_files(files, pattern, new_value) +# return {"status": "success", "replaced": result} +# except Exception as e: +# raise HTTPException(status_code=500, detail=str(e)) diff --git a/backend/agent/tools/message_tool.py b/backend/agent/tools/message_tool.py index 69f6208d..4d3baeac 100644 --- a/backend/agent/tools/message_tool.py +++ b/backend/agent/tools/message_tool.py @@ -16,20 +16,20 @@ class MessageTool(Tool): "type": "function", "function": { "name": "message_notify_user", - "description": "Send a message to user without requiring a response. Use for acknowledging receipt of messages, providing progress updates, reporting task completion, or explaining changes in approach.", + "description": "Send a message to user without requiring a response. Use for: 1) Progress updates during long-running tasks, 2) Acknowledging receipt of user instructions, 3) Reporting completion of major milestones, 4) Explaining changes in approach or strategy, 5) Summarizing findings or results without requiring input.", "parameters": { "type": "object", "properties": { "text": { "type": "string", - "description": "Message text to display to user" + "description": "Message text to display to user - should be informative and actionable" }, "attachments": { "anyOf": [ {"type": "string"}, {"items": {"type": "string"}, "type": "array"} ], - "description": "(Optional) List of attachments to show to user, can be file paths or URLs" + "description": "(Optional) List of attachments to show to user, can be file paths or URLs. Include when referencing created files, analysis results, or external resources." } }, "required": ["text"] @@ -43,8 +43,8 @@ class MessageTool(Tool): {"param_name": "attachments", "node_type": "attribute", "path": ".", "required": False} ], example=''' - - Task completed successfully! + + I've completed the data analysis and generated visualizations of the key trends. The analysis shows a 15% increase in engagement metrics over the last quarter, with the most significant growth in mobile users. ''' ) @@ -79,25 +79,25 @@ class MessageTool(Tool): "type": "function", "function": { "name": "message_ask_user", - "description": "Ask user a question and wait for response. Use for requesting clarification, asking for confirmation, or gathering additional information.", + "description": "Ask user a question and wait for response. Use for: 1) Requesting clarification on ambiguous requirements, 2) Seeking confirmation before proceeding with high-impact changes, 3) Gathering additional information needed to complete a task, 4) Offering options and requesting user preference, 5) Validating assumptions when critical to task success.", "parameters": { "type": "object", "properties": { "text": { "type": "string", - "description": "Question text to present to user" + "description": "Question text to present to user - should be specific and clearly indicate what information you need" }, "attachments": { "anyOf": [ {"type": "string"}, {"items": {"type": "string"}, "type": "array"} ], - "description": "(Optional) List of question-related files or reference materials" + "description": "(Optional) List of question-related files or reference materials. Include when the question references specific content the user needs to see." }, "suggest_user_takeover": { "type": "string", "enum": ["none", "browser"], - "description": "(Optional) Suggested operation for user takeover" + "description": "(Optional) Suggested operation for user takeover. Use 'browser' when user might need to access a website for authentication or manual interaction." } }, "required": ["text"] @@ -112,8 +112,8 @@ class MessageTool(Tool): {"param_name": "suggest_user_takeover", "node_type": "attribute", "path": ".", "required": False} ], example=''' - - Would you like to continue with this approach? + + I've prepared two database migration approaches (attached). The first minimizes downtime but requires more storage temporarily, while the second has longer downtime but uses less resources. Which approach would you prefer to implement? ''' ) @@ -154,7 +154,7 @@ class MessageTool(Tool): "type": "function", "function": { "name": "idle", - "description": "A special tool to indicate you have completed all tasks and are about to enter idle state.", + "description": "A special tool to indicate you have completed all tasks and are about to enter idle state. Use ONLY when: 1) All tasks in todo.md are marked complete, 2) The user's original request has been fully addressed, 3) There are no pending actions or follow-ups required, 4) You've delivered all final outputs and results to the user.", "parameters": { "type": "object" } @@ -164,7 +164,10 @@ class MessageTool(Tool): tag_name="idle", mappings=[], example=''' - + + + + ''' ) async def idle(self) -> ToolResult: diff --git a/backend/agent/tools/sb_files_tool.py b/backend/agent/tools/sb_files_tool.py index 32565b7e..d9a44053 100644 --- a/backend/agent/tools/sb_files_tool.py +++ b/backend/agent/tools/sb_files_tool.py @@ -288,125 +288,6 @@ class SandboxFilesTool(SandboxToolsBase): except Exception as e: return self.fail_response(f"Error deleting file: {str(e)}") - @openapi_schema({ - "type": "function", - "function": { - "name": "search_files", - "description": "Search for text in files within a directory. The search is recursive by default.", - "parameters": { - "type": "object", - "properties": { - "path": { - "type": "string", - "description": "Path to search in (directory or file)" - }, - "pattern": { - "type": "string", - "description": "Text pattern to search for" - }, - "recursive": { - "type": "boolean", - "description": "Whether to search recursively in subdirectories", - "default": True - } - }, - "required": ["path", "pattern"] - } - } - }) - @xml_schema( - tag_name="search-files", - mappings=[ - {"param_name": "path", "node_type": "attribute", "path": "."}, - {"param_name": "pattern", "node_type": "attribute", "path": "."}, - {"param_name": "recursive", "node_type": "attribute", "path": "."} - ], - example=''' - - - ''' - ) - async def search_files(self, path: str, pattern: str, recursive: bool = True) -> ToolResult: - try: - path = self.clean_path(path) - full_path = f"{self.workspace_path}/{path}" if not path.startswith(self.workspace_path) else path - results = self.sandbox.fs.find_files( - path=full_path, - pattern=pattern, - recursive=recursive - ) - - formatted_results = [] - for match in results: - formatted_results.append({ - "file": match.file, - "line": match.line, - "content": match.content - }) - - return self.success_response({ - "matches": formatted_results, - "count": len(formatted_results) - }) - except Exception as e: - return self.fail_response(f"Error searching files: {str(e)}") - - @openapi_schema({ - "type": "function", - "function": { - "name": "replace_in_file", - "description": "Replace text in a single file. Use for updating specific content or fixing errors in code.", - "parameters": { - "type": "object", - "properties": { - "file": { - "type": "string", - "description": "Path to the file to perform replacement in" - }, - "pattern": { - "type": "string", - "description": "Text pattern to replace (exact match)" - }, - "new_value": { - "type": "string", - "description": "New text to replace the pattern with" - } - }, - "required": ["file", "pattern", "new_value"] - } - } - }) - @xml_schema( - tag_name="replace-in-file", - mappings=[ - {"param_name": "file", "node_type": "attribute", "path": "."}, - {"param_name": "pattern", "node_type": "element", "path": "pattern"}, - {"param_name": "new_value", "node_type": "element", "path": "new_value"} - ], - example=''' - - old_text - new_text - - ''' - ) - async def replace_in_file(self, file: str, pattern: str, new_value: str) -> ToolResult: - try: - file = self.clean_path(file) - full_path = f"{self.workspace_path}/{file}" if not file.startswith(self.workspace_path) else file - - # Use the same Daytona SDK method but with a single file - self.sandbox.fs.replace_in_files( - files=[full_path], - pattern=pattern, - new_value=new_value - ) - - return self.success_response(f"Text replaced in file '{file}' successfully.") - except Exception as e: - return self.fail_response(f"Error replacing text in file: {str(e)}") - - async def test_files_tool(): files_tool = SandboxFilesTool( @@ -418,27 +299,10 @@ async def test_files_tool(): print(res) print(await files_tool.get_workspace_state()) - print("2)", "*"*10) - res = await files_tool.search_files("/", "Hello") - print(res) - print(await files_tool.get_workspace_state()) - - print("3)", "*"*10) - res = await files_tool.str_replace("test.txt", "Hello", "Hi") print(res) print(await files_tool.get_workspace_state()) - print("4)", "*"*10) - res = await files_tool.search_files("/", "Hello") - print(res) - print(await files_tool.get_workspace_state()) - - print("5)", "*"*10) - res = await files_tool.search_files("/", "Hi") - print(res) - print(await files_tool.get_workspace_state()) - print("6)", "*"*10) res = await files_tool.full_file_rewrite("test.txt", "FOOOOHi, world!") print(res) @@ -452,13 +316,5 @@ async def test_files_tool(): print("8)", "*"*10) - res = await files_tool.search_files("/", "Hello") - print(res) - print(await files_tool.get_workspace_state()) - - print("9)", "*"*10) - - res = await files_tool.replace_in_file("test.txt", "Hello", "Hi") - print(res) - print(await files_tool.get_workspace_state()) + diff --git a/backend/agent/tools/web_search_tool.py b/backend/agent/tools/web_search_tool.py index eeee1e00..6c0b0ce2 100644 --- a/backend/agent/tools/web_search_tool.py +++ b/backend/agent/tools/web_search_tool.py @@ -1,31 +1,76 @@ -import asyncio -import os - -from agentpress.tool import Tool, ToolResult, openapi_schema, xml_schema -from typing import Optional, Dict, Any -from dotenv import load_dotenv from exa_py import Exa +from typing import List, Optional +from datetime import datetime +from agentpress.tool import Tool, ToolResult, openapi_schema, xml_schema -# Load environment variables -load_dotenv() +class ExaWebSearchTool(Tool): + """Tool for performing web searches using the Exa API.""" -class SearchTool(Tool): - """Tool for executing web searches using the Exa API.""" - - def __init__(self): + def __init__(self, api_key: str = "91ada9c3-6d2f-4cd6-90c2-c7d943c5db25"): super().__init__() - self.exa = Exa(api_key="91ada9c3-6d2f-4cd6-90c2-c7d943c5db25") + self.exa = Exa(api_key=api_key) + @openapi_schema({ "type": "function", "function": { - "name": "info_search_web", - "description": "Search web pages using search engine. Use for obtaining latest information or finding references.", + "name": "web_search", + "description": "Search the web for information on a specific topic using Exa API", "parameters": { "type": "object", "properties": { "query": { "type": "string", - "description": "Search query" + "description": "The search query to find relevant web pages" + }, + "summary": { + "type": "boolean", + "description": "Whether to include a summary of the results", + "default": True + }, + "start_published_date": { + "type": "string", + "description": "Optional start date for when results were published (ISO format YYYY-MM-DDTHH:MM:SS.sssZ)" + }, + "end_published_date": { + "type": "string", + "description": "Optional end date for when results were published (ISO format YYYY-MM-DDTHH:MM:SS.sssZ)" + }, + "start_crawl_date": { + "type": "string", + "description": "Optional start date for when results were crawled (ISO format YYYY-MM-DDTHH:MM:SS.sssZ)" + }, + "end_crawl_date": { + "type": "string", + "description": "Optional end date for when results were crawled (ISO format YYYY-MM-DDTHH:MM:SS.sssZ)" + }, + "include_text": { + "type": "array", + "items": { + "type": "string" + }, + "description": "A list of terms that must be included in the results" + }, + "exclude_text": { + "type": "array", + "items": { + "type": "string" + }, + "description": "A list of terms that must be excluded from the results" + }, + # "livecrawl": { + # "type": "string", + # "description": "Whether to perform a live crawl - 'always', 'fallback', or 'never'", + # "default": "always" + # }, + "num_results": { + "type": "integer", + "description": "The number of results to return", + "default": 10 + }, + "type": { + "type": "string", + "description": "The type of search to perform - 'auto', 'keyword', or 'neural'", + "default": "auto" } }, "required": ["query"] @@ -33,73 +78,104 @@ class SearchTool(Tool): } }) @xml_schema( - tag_name="info-search-web", + tag_name="web-search", mappings=[ - {"param_name": "query", "node_type": "content", "path": "."} + {"param_name": "query", "node_type": "attribute", "path": "."}, + {"param_name": "summary", "node_type": "attribute", "path": "."}, + {"param_name": "start_published_date", "node_type": "attribute", "path": "."}, + {"param_name": "end_published_date", "node_type": "attribute", "path": "."}, + {"param_name": "start_crawl_date", "node_type": "attribute", "path": "."}, + {"param_name": "end_crawl_date", "node_type": "attribute", "path": "."}, + {"param_name": "include_text", "node_type": "attribute", "path": "."}, + {"param_name": "exclude_text", "node_type": "attribute", "path": "."}, + # {"param_name": "livecrawl", "node_type": "attribute", "path": "."}, + {"param_name": "num_results", "node_type": "attribute", "path": "."}, + {"param_name": "type", "node_type": "attribute", "path": "."} ], example=''' - - search query here - + + ''' ) - async def info_search_web(self, query: str) -> ToolResult: + async def web_search( + self, + query: str, + summary: bool = True, + start_published_date: Optional[str] = None, + end_published_date: Optional[str] = None, + start_crawl_date: Optional[str] = None, + end_crawl_date: Optional[str] = None, + include_text: Optional[List[str]] = None, + exclude_text: Optional[List[str]] = None, + # livecrawl: str = "always", + num_results: int = 10, + type: str = "auto" + ) -> ToolResult: + """ + Search the web using the Exa API. + + Parameters: + - query: The search query to find relevant web pages + - summary: Whether to include a summary of the results (default: True) + - start_published_date: Optional start date for published results (ISO format) + - end_published_date: Optional end date for published results (ISO format) + - start_crawl_date: Optional start date for crawled results (ISO format) + - end_crawl_date: Optional end date for crawled results (ISO format) + - include_text: List of terms that must be included in the results + - exclude_text: List of terms that must be excluded from the results + - num_results: The number of results to return (default: 10) + - type: The type of search to perform - 'auto', 'keyword', or 'neural' (default: 'auto') + """ try: - # Build search parameters with hardcoded defaults - params: Dict[str, Any] = { - "use_autoprompt": True, - "include_domains": None, - "start_published_date": None, - "end_published_date": None - } - - # Perform search with contents and highlights - search_response = self.exa.search_and_contents( - query, - highlights=True, - **params - ) - - # Format response - formatted_results = [] - for result in search_response.results: - formatted_result = { - "title": result.title if hasattr(result, 'title') else "", - "url": result.url if hasattr(result, 'url') else "", - "published_date": result.published_date if hasattr(result, 'published_date') else "", - "text": result.text if hasattr(result, 'text') else "", - "highlights": result.highlights if hasattr(result, 'highlights') else [] - } - formatted_results.append(formatted_result) - - print("************* ") - print(formatted_results) - print("************* ") - return self.success_response({ - "results": formatted_results, - "total": len(formatted_results) - }) - + # Prepare parameters, only including non-None values + params = {"query": query, "summary": summary, "num_results": num_results} + + if start_published_date: + params["start_published_date"] = start_published_date + if end_published_date: + params["end_published_date"] = end_published_date + if start_crawl_date: + params["start_crawl_date"] = start_crawl_date + if end_crawl_date: + params["end_crawl_date"] = end_crawl_date + if include_text: + params["include_text"] = include_text + if exclude_text: + params["exclude_text"] = exclude_text + # if livecrawl: + # params["livecrawl"] = livecrawl + if type: + params["type"] = type + + # Execute the search + search_response = self.exa.search_and_contents(**params) + + # Convert to string representation + results_data = str(search_response) + + return self.success_response(results_data) + except Exception as e: return self.fail_response(f"Error performing web search: {str(e)}") -async def main(): - """Main execution function with proper async handling.""" - search_tool = SearchTool() - try: - print("\nTesting search functionality...") - result = await search_tool.info_search_web( - "Latest developments in AI", - use_autoprompt=True, - include_domains=["www.techcrunch.com", "www.wired.com"], - start_date="2023-01-01" - ) - print(result.output if result.success else result) - print("\nAll operations complete!") - except Exception as e: - print(f"\nError during execution: {str(e)}") - raise if __name__ == "__main__": - # Run the async main function with a proper event loop - asyncio.run(main()) \ No newline at end of file + import asyncio + + async def test_web_search(): + """Test function for the web search tool""" + search_tool = ExaWebSearchTool() + result = await search_tool.web_search( + query="rubber gym mats best prices comparison", + summary=False, + num_results=10 + ) + print(result) + + asyncio.run(test_web_search()) diff --git a/backend/api.py b/backend/api.py index a683e703..00d8c006 100644 --- a/backend/api.py +++ b/backend/api.py @@ -14,6 +14,7 @@ import uuid # Import the agent API module from agent import api as agent_api +from agent.sandbox_api import router as sandbox_router # Load environment variables load_dotenv() @@ -67,6 +68,9 @@ app.add_middleware( # Include the agent router with a prefix app.include_router(agent_api.router, prefix="/api") +# Include the sandbox router with a prefix +app.include_router(sandbox_router, prefix="/api") + @app.get("/api/health-check") async def health_check(): """Health check endpoint to verify API is working."""