This commit is contained in:
marko-kraemer 2025-04-10 17:02:21 +01:00
parent cff9ba57f6
commit 57d08c76af
11 changed files with 287 additions and 215 deletions

View File

@ -20,7 +20,7 @@ You excel at the following tasks:
</language_settings>
<system_capability>
- 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
</system_capability>
<event_stream>
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
</event_stream>
<methodical_workflow>
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:
</agent_loop>
<planner_module>
- 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
</planner_module>
<knowledge_module>
- 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
</knowledge_module>
<datasource_module>
- 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
</datasource_module>
<datasource_module_code_example>
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--
\`\`\`
</datasource_module_code_example>
<todo_format>
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]`
</file_rules>
<info_rules>
- 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

View File

@ -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"<Unserializable object of type {type(obj).__name__}>"
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,8 +62,8 @@ 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 = "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-7-sonnet-latest"
# model_name = "openai/gpt-4o"
@ -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:
<current_workspace_state>
{json.dumps(files_state, indent=2)}
{json.dumps(files_state, indent=2, cls=CustomJSONEncoder)}
</current_workspace_state>
"""
}
@ -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")

View File

@ -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='''
<search-files path="path/to/search" pattern="text-of-interest">
<search-files path="path/to/search" pattern="text-of-interest" recursive="true">
</search-files>
'''
)
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='''
<replace-in-files>
<files>
<file>path/to/file1.txt</file>
<file>path/to/file2.txt</file>
</files>
<replace-in-file file="path/to/file.txt">
<pattern>old_text</pattern>
<new_value>new_text</new_value>
</replace-in-files>
</replace-in-file>
'''
)
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())

View File

@ -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"""
@app.get("/health")
async def health_check():
status = {
"status": "healthy"
}
logger.debug(f"Health check: {status}")
return status
logger.debug("Uploading browser API script to sandbox")
sandbox.fs.upload_file(sandbox.get_user_root_dir() + "/browser_api.py", sandbox_browser_api)
if __name__ == "__main__":
logger.info("Starting website server")
uvicorn.run(app, host="0.0.0.0", port=8080, reload=True)
'''
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
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
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}")
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("/")

View File

@ -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

View File

@ -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,

View File

@ -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.

13
backend/poetry.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -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

View File

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