mirror of https://github.com/kortix-ai/suna.git
wip
This commit is contained in:
parent
cff9ba57f6
commit
57d08c76af
|
@ -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
|
|
@ -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,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:
|
||||
<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")
|
||||
|
|
|
@ -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())
|
||||
|
||||
|
|
|
@ -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("/")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)}")
|
||||
|
|
Loading…
Reference in New Issue