diff --git a/backend/agent/prompt.py b/backend/agent/prompt.py index e510009b..17ff972d 100644 --- a/backend/agent/prompt.py +++ b/backend/agent/prompt.py @@ -20,7 +20,7 @@ You excel at the following tasks: -- Communicate with users through message tools +- Communicate with users through message tools – message_notify_user and message_ask_user. - Access a Linux sandbox environment with internet connection - Use shell, text editor, browser, and other software - Write and run code in Python and various programming languages @@ -30,17 +30,6 @@ You excel at the following tasks: - Utilize various tools to complete user-assigned tasks step by step - -You will be provided with a chronological event stream (may be truncated or partially omitted) containing the following types of events: -1. Message: Messages input by actual users -2. Action: Tool use (function calling) actions -3. Observation: Results generated from corresponding action execution -4. Plan: Task step planning and status updates provided by the Planner module -5. Knowledge: Task-related knowledge and best practices provided by the Knowledge module -6. Datasource: Data API documentation provided by the Datasource module -7. Other miscellaneous events generated during system operation - - Your workflow is deliberately methodical and thorough, not rushed. Always take sufficient time to: 1. UNDERSTAND fully before acting @@ -145,46 +134,30 @@ You operate in a methodical, single-step agent loop guided by todo.md: -- The planner module provides initial task structuring through the event stream -- Upon receiving planning events, immediately translate them into detailed todo.md entries -- Todo.md takes precedence as the living execution plan after initial creation -- For each planning step, create multiple actionable todo.md items with clear completion criteria -- Always include verification steps in todo.md to ensure quality of outputs +The planner module is responsible for initializing and organizing your todo.md workflow: + +1. INITIAL PLANNING: + - Upon task assignment, the planner generates a structured breakdown in the event stream + - You MUST immediately translate these planning events into a comprehensive todo.md file + - Create 5-10 major sections in todo.md that cover the entire task lifecycle + - Each section must contain 3-10 specific, actionable subtasks with clear completion criteria + +2. ONGOING EXECUTION: + - After creation, todo.md becomes the SOLE source of truth for execution + - Follow todo.md strictly, working on one section at a time in sequential order + - All tool selection decisions MUST directly reference the active todo.md item + +3. ADAPTATION: + - When receiving new planning events during execution, update todo.md accordingly + - Preserve completed tasks and their status when incorporating plan changes + - Document any significant plan changes with clear explanations in todo.md + +4. VERIFICATION: + - Each section must end with verification steps to confirm quality and completeness + - The final section must validate all deliverables against the original requirements + - Only mark verification steps complete after thorough assessment - -- System is equipped with knowledge and memory module for best practice references -- Task-relevant knowledge will be provided as events in the event stream -- Each knowledge item has its scope and should only be adopted when conditions are met -- When relevant knowledge is provided, add appropriate todo.md items to incorporate it - - - -- System is equipped with data API module for accessing authoritative datasources -- Available data APIs and their documentation will be provided as events in the event stream -- Only use data APIs already existing in the event stream; fabricating non-existent APIs is prohibited -- Prioritize using APIs for data retrieval; only use public internet when data APIs cannot meet requirements -- Data API usage costs are covered by the system, no login or authorization needed -- Data APIs must be called through Python code and cannot be used as tools -- Python libraries for data APIs are pre-installed in the environment, ready to use after import -- Save retrieved data to files instead of outputting intermediate results - - - -weather.py: -\`\`\`python -import sys -sys.path.append('/opt/.manus/.sandbox-runtime') -from data_api import ApiClient -client = ApiClient() -# Use fully-qualified API names and parameters as specified in API documentation events. -# Always use complete query parameter format in query={...}, never omit parameter names. -weather = client.call_api('WeatherBank/get_weather', query={'location': 'Singapore'}) -print(weather) -# --snip-- -\`\`\` - - Todo.md must follow this comprehensive structured format with many sections: ``` @@ -275,7 +248,6 @@ Summary: [Comprehensive summary of section achievements and insights]` - Communicate with users via message tools instead of direct text responses - Reply immediately to new user messages before other operations - First reply must be brief, only confirming receipt without specific solutions -- Events from Planner, Knowledge, and Datasource modules are system-generated, no reply needed - Notify users with brief explanation when changing methods or strategies - Message tools are divided into notify (non-blocking, no reply needed from users) and ask (blocking, reply required) - Actively use notify for progress updates, but reserve ask for only essential needs to minimize user disruption and avoid blocking progress @@ -296,7 +268,7 @@ Summary: [Comprehensive summary of section achievements and insights]` -- Information priority: authoritative data from datasource API > web search > model's internal knowledge +- Information priority: web search > model's internal knowledge - Prefer dedicated search tools over browser access to search engine result pages - Snippets in search results are not valid sources; must access original pages via browser - Access multiple URLs from search results for comprehensive information or cross-validation @@ -396,6 +368,6 @@ Sleep Settings: def get_system_prompt(): ''' - Returns the system prompt with XML tool usage instructions. + Returns the system prompt ''' return SYSTEM_PROMPT \ No newline at end of file diff --git a/backend/agent/run.py b/backend/agent/run.py index 23517864..dbdc6c2a 100644 --- a/backend/agent/run.py +++ b/backend/agent/run.py @@ -2,6 +2,7 @@ import json from uuid import uuid4 from typing import Optional +from agent.tools.message_tool import MessageTool from dotenv import load_dotenv from agentpress.thread_manager import ThreadManager @@ -11,11 +12,26 @@ from agent.tools.sb_shell_tool import SandboxShellTool from agent.tools.sb_website_tool import SandboxWebsiteTool from agent.tools.sb_files_tool import SandboxFilesTool from agent.prompt import get_system_prompt -from agent.tools.utils.daytona_sandbox import daytona, create_sandbox -from daytona_api_client.models.workspace_state import WorkspaceState +from agent.tools.utils.daytona_sandbox import daytona, create_sandbox, get_or_start_sandbox + load_dotenv() -async def run_agent(thread_id: str, project_id: str, stream: bool = True, thread_manager: Optional[ThreadManager] = None, native_max_auto_continues: int = 25, max_iterations: int = 1000): +# Custom JSON encoder to handle non-serializable types +class CustomJSONEncoder(json.JSONEncoder): + def default(self, obj): + # Handle ToolResult objects + if hasattr(obj, 'to_dict'): + return obj.to_dict() + # Handle datetime objects + if hasattr(obj, 'isoformat'): + return obj.isoformat() + # Return string representation for other unserializable objects + try: + return str(obj) + except: + return f"" + +async def run_agent(thread_id: str, project_id: str, stream: bool = True, thread_manager: Optional[ThreadManager] = None, native_max_auto_continues: int = 25, max_iterations: int = 1): """Run the development agent with specified configuration.""" if not thread_manager: @@ -27,13 +43,7 @@ async def run_agent(thread_id: str, project_id: str, stream: bool = True, thread if project.data[0]['sandbox_id']: sandbox_id = project.data[0]['sandbox_id'] sandbox_pass = project.data[0]['sandbox_pass'] - sandbox = daytona.get_current_sandbox(sandbox_id) - if sandbox.instance.state == WorkspaceState.ARCHIVED or sandbox.instance.state == WorkspaceState.STOPPED: - try: - daytona.start(sandbox) - except Exception as e: - print(f"Error starting sandbox: {e}") - raise e + sandbox = await get_or_start_sandbox(sandbox_id, sandbox_pass) else: sandbox_pass = str(uuid4()) sandbox = create_sandbox(sandbox_pass) @@ -44,7 +54,7 @@ async def run_agent(thread_id: str, project_id: str, stream: bool = True, thread }).eq('project_id', project_id).execute() ### --- - print("Adding tools to thread manager...") + thread_manager.add_tool(SandboxBrowseTool, sandbox_id=sandbox_id, password=sandbox_pass) thread_manager.add_tool(SandboxWebsiteTool, sandbox_id=sandbox_id, password=sandbox_pass) thread_manager.add_tool(SandboxShellTool, sandbox_id=sandbox_id, password=sandbox_pass) @@ -52,9 +62,9 @@ async def run_agent(thread_id: str, project_id: str, stream: bool = True, thread system_message = { "role": "system", "content": get_system_prompt() } - model_name = "bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0" + model_name = "anthropic/claude-3-5-sonnet-latest" + # model_name = "bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0" # model_name = "anthropic/claude-3-5-sonnet-latest" - # model_name = "anthropic/claude-3-5-sonnet-latest" # model_name = "anthropic/claude-3-7-sonnet-latest" # model_name = "openai/gpt-4o" # model_name = "groq/deepseek-r1-distill-llama-70b" @@ -77,7 +87,7 @@ async def run_agent(thread_id: str, project_id: str, stream: bool = True, thread "content": f""" Current development environment workspace state: -{json.dumps(files_state, indent=2)} +{json.dumps(files_state, indent=2, cls=CustomJSONEncoder)} """ } @@ -101,7 +111,7 @@ Current development environment workspace state: xml_adding_strategy="user_message" ), native_max_auto_continues=native_max_auto_continues, - include_xml_examples=True + include_xml_examples=True, ) if isinstance(response, dict) and "status" in response and response["status"] == "error": @@ -143,8 +153,19 @@ async def test_agent(): client = await DBConnection().client try: - thread_result = await client.table('projects').insert({"name": "test", "user_id": "68e1da55-0749-49db-937a-ff56bf0269a0"}).execute() - thread_result = await client.table('threads').insert({'project_id': thread_result.data[0]['project_id']}).execute() + project_result = await client.table('projects').select('*').eq('name', 'test11').eq('user_id', '68e1da55-0749-49db-937a-ff56bf0269a0').execute() + + if project_result.data and len(project_result.data) > 0: + # Use existing test project + project_id = project_result.data[0]['project_id'] + print(f"\n🔄 Using existing test project: {project_id}") + else: + # Create new test project if none exists + project_result = await client.table('projects').insert({"name": "test11", "user_id": "68e1da55-0749-49db-937a-ff56bf0269a0"}).execute() + project_id = project_result.data[0]['project_id'] + print(f"\n✨ Created new test project: {project_id}") + + thread_result = await client.table('threads').insert({'project_id': project_id}).execute() thread_data = thread_result.data[0] if thread_result.data else None if not thread_data: @@ -152,9 +173,8 @@ async def test_agent(): return thread_id = thread_data['thread_id'] - project_id = thread_data['project_id'] except Exception as e: - print(f"Error creating thread: {str(e)}") + print(f"Error setting up thread: {str(e)}") return print(f"\n🤖 Agent Thread Created: {thread_id}\n") diff --git a/backend/agent/tools/sb_files_tool.py b/backend/agent/tools/sb_files_tool.py index 271e18f6..369f7be4 100644 --- a/backend/agent/tools/sb_files_tool.py +++ b/backend/agent/tools/sb_files_tool.py @@ -289,17 +289,22 @@ class SandboxFilesTool(SandboxToolsBase): "type": "function", "function": { "name": "search_files", - "description": "Search for text in files within a directory", + "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)" + "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"] @@ -310,20 +315,22 @@ class SandboxFilesTool(SandboxToolsBase): tag_name="search-files", mappings=[ {"param_name": "path", "node_type": "attribute", "path": "@path"}, - {"param_name": "pattern", "node_type": "attribute", "path": "@pattern"} + {"param_name": "pattern", "node_type": "attribute", "path": "@pattern"}, + {"param_name": "recursive", "node_type": "attribute", "path": "@recursive"} ], example=''' - + ''' ) - async def search_files(self, path: str, pattern: str) -> ToolResult: + 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 + pattern=pattern, + recursive=recursive ) formatted_results = [] @@ -344,62 +351,57 @@ class SandboxFilesTool(SandboxToolsBase): @openapi_schema({ "type": "function", "function": { - "name": "replace_in_files", - "description": "Replace text in multiple files", + "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": { - "files": { - "type": "array", - "items": { - "type": "string" - }, - "description": "List of file paths to search in" + "file": { + "type": "string", + "description": "Path to the file to perform replacement in" }, "pattern": { "type": "string", - "description": "Text pattern to replace" + "description": "Text pattern to replace (exact match)" }, "new_value": { "type": "string", "description": "New text to replace the pattern with" } }, - "required": ["files", "pattern", "new_value"] + "required": ["file", "pattern", "new_value"] } } }) @xml_schema( - tag_name="replace-in-files", + tag_name="replace-in-file", mappings=[ - {"param_name": "files", "node_type": "element", "path": "files/file"}, + {"param_name": "file", "node_type": "attribute", "path": "@file"}, {"param_name": "pattern", "node_type": "element", "path": "pattern"}, {"param_name": "new_value", "node_type": "element", "path": "new_value"} ], example=''' - - - path/to/file1.txt - path/to/file2.txt - + old_text new_text - + ''' ) - async def replace_in_files(self, files: list[str], pattern: str, new_value: str) -> ToolResult: + async def replace_in_file(self, file: str, pattern: str, new_value: str) -> ToolResult: try: - files = [self.clean_path(f) for f in files] - full_paths = [f"{self.workspace_path}/{f}" if not f.startswith(self.workspace_path) else f for f in files] + 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_paths, + files=[full_path], pattern=pattern, new_value=new_value ) - return self.success_response(f"Text replaced in {len(files)} files successfully.") + return self.success_response(f"Text replaced in file '{file}' successfully.") except Exception as e: - return self.fail_response(f"Error replacing text in files: {str(e)}") + return self.fail_response(f"Error replacing text in file: {str(e)}") @@ -453,7 +455,7 @@ async def test_files_tool(): print("9)", "*"*10) - res = await files_tool.replace_in_files(["test.txt", "test2.txt"], "Hello", "Hi") + 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/utils/daytona_sandbox.py b/backend/agent/tools/utils/daytona_sandbox.py index 37cd51d1..14a15319 100644 --- a/backend/agent/tools/utils/daytona_sandbox.py +++ b/backend/agent/tools/utils/daytona_sandbox.py @@ -3,6 +3,7 @@ import requests from time import sleep from daytona_sdk import Daytona, DaytonaConfig, CreateSandboxParams, SessionExecuteRequest +from daytona_api_client.models.workspace_state import WorkspaceState from dotenv import load_dotenv from agentpress.tool import Tool @@ -10,7 +11,7 @@ from utils.logger import logger load_dotenv() -logger.info("Initializing Daytona sandbox configuration") +logger.debug("Initializing Daytona sandbox configuration") config = DaytonaConfig( api_key=os.getenv("DAYTONA_API_KEY"), server_url=os.getenv("DAYTONA_SERVER_URL"), @@ -166,58 +167,159 @@ if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8000) ''' -sandbox_website_server = b''' -import os -from fastapi import FastAPI, HTTPException, Request +# Server script to be used for HTTP server +SERVER_SCRIPT = """from fastapi import FastAPI from fastapi.staticfiles import StaticFiles -from fastapi.responses import FileResponse import uvicorn -import logging -import logging.handlers +import os -# Configure logging -log_dir = "/var/log/kortix" -os.makedirs(log_dir, exist_ok=True) -log_file = os.path.join(log_dir, "website_server.log") - -logger = logging.getLogger("website_server") -logger.setLevel(logging.INFO) - -# Create rotating file handler -file_handler = logging.handlers.RotatingFileHandler( - log_file, - maxBytes=10485760, # 10MB - backupCount=5 -) -file_handler.setFormatter( - logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') -) -logger.addHandler(file_handler) +# Ensure we're serving from the /workspace directory +workspace_dir = "/workspace" +os.makedirs(workspace_dir, exist_ok=True) app = FastAPI() +app.mount('/', StaticFiles(directory=workspace_dir, html=True), name='site') -# Create site directory if it doesn't exist -site_dir = "/workspace/site" -os.makedirs(site_dir, exist_ok=True) +# This is needed for the import string approach with uvicorn +if __name__ == '__main__': + print(f"Starting server with auto-reload, serving files from: {workspace_dir}") + # Don't use reload directly in the run call + uvicorn.run("server:app", host="0.0.0.0", port=8080, reload=True) +""" -# Mount the static files directory -app.mount("/", StaticFiles(directory=site_dir, html=True), name="site") +def start_sandbox_browser_api(sandbox): + """Start the browser API service in the sandbox""" + + logger.debug("Uploading browser API script to sandbox") + sandbox.fs.upload_file(sandbox.get_user_root_dir() + "/browser_api.py", sandbox_browser_api) + + try: + # Always create new session without checking + logger.debug("Creating sandbox browser API session") + try: + sandbox.process.create_session('sandbox_browser_api') + except Exception as session_e: + # If session already exists, this will fail, but we can continue + logger.debug(f"Error creating session, might already exist: {str(session_e)}") + + logger.debug("Executing browser API command in sandbox") + rsp = sandbox.process.execute_session_command('sandbox_browser_api', SessionExecuteRequest( + command="python " + sandbox.get_user_root_dir() + "/browser_api.py", + var_async=True + )) + logger.debug(f"Browser API command execution result: {rsp}") + + except Exception as e: + logger.error(f"Error starting browser API: {str(e)}") + raise e -@app.get("/health") -async def health_check(): - status = { - "status": "healthy" - } - logger.debug(f"Health check: {status}") - return status +def start_http_server(sandbox): + """Start the HTTP server in the sandbox""" + + try: + # Always create new session without checking + logger.debug("Creating HTTP server session") + try: + sandbox.process.create_session('http_server') + except Exception as session_e: + # If session already exists, this will fail, but we can continue + logger.debug(f"Error creating session, might already exist: {str(session_e)}") + + # Create the server script file + sandbox.fs.upload_file(sandbox.get_user_root_dir() + "/server.py", SERVER_SCRIPT.encode()) + + # Start the HTTP server using uvicorn with auto-reload + http_server_rsp = sandbox.process.execute_session_command('http_server', SessionExecuteRequest( + command="cd " + sandbox.get_user_root_dir() + " && pip install uvicorn fastapi && python server.py", + var_async=True + )) + logger.info(f"HTTP server started: {http_server_rsp}") + + except Exception as e: + logger.error(f"Error starting HTTP server: {str(e)}") + raise e -if __name__ == "__main__": - logger.info("Starting website server") - uvicorn.run(app, host="0.0.0.0", port=8080, reload=True) -''' +def wait_for_api_ready(sandbox): + """Wait for the sandbox API to be ready and responsive""" + + times = 0 + success = False + api_url = sandbox.get_preview_link(8000) + logger.info(f"Waiting for API to be ready at {api_url}") + + while times < 10: + times += 1 + logger.info(f"Waiting for API to be ready... Attempt {times}/10") + try: + # Make the API call to our FastAPI endpoint + response = requests.get(f"{api_url}/health") + if response.status_code == 200: + logger.info(f"API call completed successfully: {response.status_code}") + success = True + break + else: + logger.warning(f"API health check failed with status code: {response.status_code}") + sleep(1) + except requests.exceptions.RequestException as e: + logger.warning(f"API request error on attempt {times}: {str(e)}") + sleep(1) + if not success: + logger.error("API health check failed after maximum attempts") + raise Exception("API call failed after maximum attempts") + + return api_url + +async def get_or_start_sandbox(sandbox_id: str, sandbox_pass: str): + """Retrieve a sandbox by ID, check its state, and start it if needed. + Also ensure the sandbox_browser_api and HTTP server services are running.""" + + logger.info(f"Getting or starting sandbox with ID: {sandbox_id}") + + try: + sandbox = daytona.get_current_sandbox(sandbox_id) + + # Check if sandbox needs to be started + if sandbox.instance.state == WorkspaceState.ARCHIVED or sandbox.instance.state == WorkspaceState.STOPPED: + logger.info(f"Sandbox is in {sandbox.instance.state} state. Starting...") + try: + daytona.start(sandbox) + # Wait a moment for the sandbox to initialize + sleep(5) + # Refresh sandbox state after starting + sandbox = daytona.get_current_sandbox(sandbox_id) + except Exception as e: + logger.error(f"Error starting sandbox: {e}") + raise e + + # Ensure browser API is running + try: + api_url = sandbox.get_preview_link(8000) + response = requests.get(f"{api_url}/health") + + if response.status_code != 200: + logger.info("Browser API is not running. Starting it...") + start_sandbox_browser_api(sandbox) + wait_for_api_ready(sandbox) + + except requests.exceptions.RequestException: + logger.info("Browser API is not accessible. Starting it...") + start_sandbox_browser_api(sandbox) + wait_for_api_ready(sandbox) + + # Ensure HTTP server is running + start_http_server(sandbox) + + logger.info(f"Sandbox {sandbox_id} is ready") + return sandbox + + except Exception as e: + logger.error(f"Error retrieving or starting sandbox: {str(e)}") + raise e def create_sandbox(password: str): + """Create a new sandbox with all required services configured and running.""" + logger.info("Creating new Daytona sandbox environment") logger.debug("Configuring sandbox with browser-use image and environment variables") @@ -265,55 +367,14 @@ def create_sandbox(password: str): )) logger.info(f"Sandbox created with ID: {sandbox.id}") - logger.debug("Uploading browser API script to sandbox") - sandbox.fs.upload_file(sandbox.get_user_root_dir() + "/browser_api.py", sandbox_browser_api) - logger.debug("Uploading website server script to sandbox") - sandbox.fs.upload_file(sandbox.get_user_root_dir() + "/website_server.py", sandbox_website_server) + # Start the browser API + start_sandbox_browser_api(sandbox) - logger.debug("Creating sandbox browser API session") - sandbox.process.create_session('sandbox_browser_api') - logger.debug("Creating sandbox website server session") - sandbox.process.create_session('sandbox_website_server') + # Start HTTP server + start_http_server(sandbox) - logger.debug("Executing browser API command in sandbox") - rsp = sandbox.process.execute_session_command('sandbox_browser_api', SessionExecuteRequest( - command="python " + sandbox.get_user_root_dir() + "/browser_api.py", - var_async=True - )) - logger.debug(f"Browser API command execution result: {rsp}") - - logger.debug("Executing website server command in sandbox") - rsp2 = sandbox.process.execute_session_command('sandbox_website_server', SessionExecuteRequest( - command="python " + sandbox.get_user_root_dir() + "/website_server.py", - var_async=True - )) - logger.debug(f"Website server command execution result: {rsp2}") - - times = 0 - success = False - api_url = sandbox.get_preview_link(8000) - logger.info(f"Sandbox API URL: {api_url}") - - while times < 10: - times += 1 - logger.info(f"Waiting for API to be ready... Attempt {times}/10") - try: - # Make the API call to our FastAPI endpoint - response = requests.get(f"{api_url}/health") - if response.status_code == 200: - logger.info(f"API call completed successfully: {response.status_code}") - success = True - break - else: - logger.warning(f"API health check failed with status code: {response.status_code}") - sleep(1) - except requests.exceptions.RequestException as e: - logger.warning(f"API request error on attempt {times}: {str(e)}") - sleep(1) - - if not success: - logger.error("API health check failed after maximum attempts") - raise Exception("API call failed after maximum attempts") + # Wait for API to be ready + wait_for_api_ready(sandbox) logger.info(f"Sandbox environment successfully initialized") return sandbox @@ -322,6 +383,9 @@ def create_sandbox(password: str): class SandboxToolsBase(Tool): """Tool for executing tasks in a Daytona sandbox with browser-use capabilities.""" + # Class variable to track if sandbox URLs have been printed + _urls_printed = False + def __init__(self, sandbox_id: str, password: str): super().__init__() self.sandbox = None @@ -329,12 +393,12 @@ class SandboxToolsBase(Tool): self.workspace_path = "/workspace" self.sandbox_id = sandbox_id - logger.info(f"Initializing SandboxToolsBase with sandbox ID: {sandbox_id}") + # logger.info(f"Initializing SandboxToolsBase with sandbox ID: {sandbox_id}") try: logger.debug(f"Retrieving sandbox with ID: {sandbox_id}") self.sandbox = self.daytona.get_current_sandbox(self.sandbox_id) - logger.info(f"Successfully retrieved sandbox: {self.sandbox.id}") + # logger.info(f"Successfully retrieved sandbox: {self.sandbox.id}") except Exception as e: logger.error(f"Error retrieving sandbox: {str(e)}", exc_info=True) raise e @@ -346,13 +410,15 @@ class SandboxToolsBase(Tool): vnc_url = self.sandbox.get_preview_link(6080) website_url = self.sandbox.get_preview_link(8080) - logger.info(f"Sandbox VNC URL: {vnc_url}") - logger.info(f"Sandbox Website URL: {website_url}") + # logger.info(f"Sandbox VNC URL: {vnc_url}") + # logger.info(f"Sandbox Website URL: {website_url}") - print("\033[95m***") - print(vnc_url) - print(website_url) - print("***\033[0m") + if not SandboxToolsBase._urls_printed: + print("\033[95m***") + print(vnc_url) + print(website_url) + print("***\033[0m") + SandboxToolsBase._urls_printed = True def clean_path(self, path: str) -> str: cleaned_path = path.replace(self.workspace_path, "").lstrip("/") diff --git a/backend/agentpress/response_processor.py b/backend/agentpress/response_processor.py index 0ff25aae..a6e66d0b 100644 --- a/backend/agentpress/response_processor.py +++ b/backend/agentpress/response_processor.py @@ -124,12 +124,12 @@ class ResponseProcessor: # Track finish reason finish_reason = None - logger.info(f"Starting to process streaming response for thread {thread_id}") + # logger.debug(f"Starting to process streaming response for thread {thread_id}") logger.info(f"Config: XML={config.xml_tool_calling}, Native={config.native_tool_calling}, " f"Execute on stream={config.execute_on_stream}, Execution strategy={config.tool_execution_strategy}") - if config.max_xml_tool_calls > 0: - logger.info(f"XML tool call limit enabled: {config.max_xml_tool_calls}") + # if config.max_xml_tool_calls > 0: + # logger.info(f"XML tool call limit enabled: {config.max_xml_tool_calls}") try: async for chunk in llm_response: @@ -138,7 +138,7 @@ class ResponseProcessor: # Check for finish_reason if hasattr(chunk, 'choices') and chunk.choices and hasattr(chunk.choices[0], 'finish_reason') and chunk.choices[0].finish_reason: finish_reason = chunk.choices[0].finish_reason - logger.info(f"Detected finish_reason: {finish_reason}") + logger.debug(f"Detected finish_reason: {finish_reason}") if hasattr(chunk, 'choices') and chunk.choices: delta = chunk.choices[0].delta if hasattr(chunk.choices[0], 'delta') else None diff --git a/backend/agentpress/thread_manager.py b/backend/agentpress/thread_manager.py index 8495bda3..d8c8e69c 100644 --- a/backend/agentpress/thread_manager.py +++ b/backend/agentpress/thread_manager.py @@ -254,7 +254,7 @@ Here are the XML tools available with examples: logger.debug(f"Retrieved {len(openapi_tool_schemas) if openapi_tool_schemas else 0} OpenAPI tool schemas") # 5. Make LLM API call - logger.info("Making LLM API call") + logger.debug("Making LLM API call") try: llm_response = await make_llm_api_call( prepared_messages, @@ -273,7 +273,7 @@ Here are the XML tools available with examples: # 6. Process LLM response using the ResponseProcessor if stream: - logger.info("Processing streaming response") + logger.debug("Processing streaming response") response_generator = self.response_processor.process_streaming_response( llm_response=llm_response, thread_id=thread_id, @@ -282,7 +282,7 @@ Here are the XML tools available with examples: return response_generator else: - logger.info("Processing non-streaming response") + logger.debug("Processing non-streaming response") try: response = await self.response_processor.process_non_streaming_response( llm_response=llm_response, diff --git a/backend/agentpress/tool_registry.py b/backend/agentpress/tool_registry.py index fdca06e3..d237cd63 100644 --- a/backend/agentpress/tool_registry.py +++ b/backend/agentpress/tool_registry.py @@ -29,7 +29,7 @@ class ToolRegistry: cls._instance = super().__new__(cls) cls._instance.tools = {} cls._instance.xml_tools = {} - logger.info("Initialized new ToolRegistry instance") + logger.debug("Initialized new ToolRegistry instance") return cls._instance def register_tool(self, tool_class: Type[Tool], function_names: Optional[List[str]] = None, **kwargs): @@ -44,7 +44,7 @@ class ToolRegistry: - If function_names is None, all functions are registered - Handles both OpenAPI and XML schema registration """ - logger.info(f"Registering tool class: {tool_class.__name__}") + logger.debug(f"Registering tool class: {tool_class.__name__}") tool_instance = tool_class(**kwargs) schemas = tool_instance.get_schemas() @@ -73,7 +73,7 @@ class ToolRegistry: registered_xml += 1 logger.debug(f"Registered XML tag {schema.xml_schema.tag_name} -> {func_name} from {tool_class.__name__}") - logger.info(f"Tool registration complete for {tool_class.__name__}: {registered_openapi} OpenAPI functions, {registered_xml} XML tags") + logger.debug(f"Tool registration complete for {tool_class.__name__}: {registered_openapi} OpenAPI functions, {registered_xml} XML tags") def get_available_functions(self) -> Dict[str, Callable]: """Get all available tool functions. diff --git a/backend/poetry.lock b/backend/poetry.lock index 98f5d8af..e7e82880 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -1385,6 +1385,17 @@ files = [ [package.dependencies] typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""} +[[package]] +name = "nest-asyncio" +version = "1.6.0" +description = "Patch asyncio to allow nested event loops" +optional = false +python-versions = ">=3.5" +files = [ + {file = "nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c"}, + {file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"}, +] + [[package]] name = "nodeenv" version = "1.9.1" @@ -3464,4 +3475,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "e2a7a6edefe63d4035c2bbbaa7f25708e764044c01de7227dea66e718375a92d" +content-hash = "e80d572b14371929f2fc9a459a28c97463d91755c21698564a3eba7637198413" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 3273b818..7b19853c 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -18,7 +18,7 @@ classifiers = [ ] [tool.poetry.dependencies] -python = "^3.10" +python = "^3.11" streamlit-quill = "0.0.3" python-dotenv = "1.0.1" litellm = "^1.44.0" @@ -47,6 +47,7 @@ daytona_sdk = "^0.12.0" boto3 = "^1.34.0" openai = "^1.72.0" streamlit = "^1.44.1" +nest-asyncio = "^1.6.0" [tool.poetry.scripts] agentpress = "agentpress.cli:main" diff --git a/backend/services/llm.py b/backend/services/llm.py index 2a16e75d..76c42e9a 100644 --- a/backend/services/llm.py +++ b/backend/services/llm.py @@ -191,7 +191,7 @@ async def make_llm_api_call( LLMRetryError: If API call fails after retries LLMError: For other API-related errors """ - logger.info(f"Making LLM API call to model: {model_name}") + logger.debug(f"Making LLM API call to model: {model_name}") params = prepare_params( messages=messages, model_name=model_name, @@ -214,7 +214,7 @@ async def make_llm_api_call( # logger.debug(f"API request parameters: {json.dumps(params, indent=2)}") response = await litellm.acompletion(**params) - logger.info(f"Successfully received API response from {model_name}") + logger.debug(f"Successfully received API response from {model_name}") logger.debug(f"Response: {response}") return response diff --git a/backend/services/supabase.py b/backend/services/supabase.py index 0f18e8ee..75ee1c91 100644 --- a/backend/services/supabase.py +++ b/backend/services/supabase.py @@ -37,11 +37,11 @@ class DBConnection: logger.error("Missing required environment variables for Supabase connection") raise RuntimeError("SUPABASE_URL and a key (SERVICE_ROLE_KEY or ANON_KEY) environment variables must be set.") - logger.info("Initializing Supabase connection") + logger.debug("Initializing Supabase connection") self._client = await create_async_client(supabase_url, supabase_key) self._initialized = True key_type = "SERVICE_ROLE_KEY" if os.getenv('SUPABASE_SERVICE_ROLE_KEY') else "ANON_KEY" - logger.info(f"Database connection initialized with Supabase using {key_type}") + logger.debug(f"Database connection initialized with Supabase using {key_type}") except Exception as e: logger.error(f"Database initialization error: {e}") raise RuntimeError(f"Failed to initialize database connection: {str(e)}")