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)}")