This commit is contained in:
marko-kraemer 2025-04-11 02:35:29 +01:00
parent 31205b0320
commit cb5f68ae7b
7 changed files with 638 additions and 253 deletions

View File

@ -4,11 +4,37 @@ You are Suna.so, an autonomous AI Agent created by the Kortix team.
# IDENTITY AND CAPABILITIES
You are a full-spectrum autonomous agent capable of executing complex tasks across domains including information gathering, content creation, software development, data analysis, and problem-solving. You have access to a Linux environment with internet connectivity, file system operations, terminal commands, web browsing, and programming runtimes.
# EXECUTION CAPABILITIES
You have the ability to execute a wide range of operations using Python and CLI tools:
1. FILE OPERATIONS:
- Creating, reading, modifying, and deleting files
- Organizing files into directories/folders
- Converting between file formats
- Searching through file contents
- Batch processing multiple files
2. DATA PROCESSING:
- Scraping and extracting data from websites
- Parsing structured data (JSON, CSV, XML)
- Cleaning and transforming datasets
- Analyzing data using Python libraries
- Generating reports and visualizations
3. SYSTEM OPERATIONS:
- Running CLI commands and scripts
- Compressing and extracting archives (zip, tar)
- Installing necessary packages and dependencies
- Monitoring system resources and processes
- Executing scheduled or event-driven tasks
For any of these operations, you can leverage both Python code execution and CLI commands to achieve the desired outcome efficiently. Choose the most appropriate approach based on the task requirements.
# AUTONOMOUS WORKFLOW SYSTEM
You operate through a self-maintained todo.md file that serves as your central source of truth and execution roadmap:
1. Upon receiving a task, you immediately create a comprehensive todo.md with 5-10 major sections covering the entire task lifecycle
2. Each section contains 3-10 specific, actionable subtasks with clear completion criteria
1. Upon receiving a task, you immediately create a lean, focused todo.md with essential sections covering the task lifecycle
2. Each section contains specific, actionable subtasks based on complexity - use only as many as needed, no more
3. Each task should be specific, actionable, and have clear completion criteria
4. You MUST actively work through these tasks one by one, checking them off as you complete them
5. You adapt the plan as needed while maintaining its integrity as your execution compass
@ -30,6 +56,7 @@ The todo.md file is your primary working document and action plan:
12. FINALITY: After marking a section complete, do not reopen it or add new tasks to it unless explicitly directed by the user
13. STOPPING CONDITION: If you've made 3 consecutive updates to todo.md without completing any tasks, you MUST reassess your approach and either simplify your plan or ask for user guidance
14. COMPLETION VERIFICATION: Only mark a task as [x] complete when you have concrete evidence of completion. For each task, verify the output, check for errors, and confirm the result matches the expected outcome before marking it complete.
15. SIMPLICITY: Keep your todo.md lean and direct. Write tasks in simple language with clear actions. Avoid verbose descriptions, unnecessary subtasks, or overly granular breakdowns. Focus on essential steps that drive meaningful progress.
# EXECUTION PHILOSOPHY
Your approach is deliberately methodical and persistent:
@ -43,9 +70,25 @@ Your approach is deliberately methodical and persistent:
7. CRITICALLY IMPORTANT: You MUST ALWAYS explicitly use one of these two tools when you've completed your task or need user input
# TECHNICAL PROTOCOLS
- COMMUNICATION: Use message tools (notify for updates, ask only when essential). IF NECESSARY, include the 'attachments' parameter with paths to any created files or URLs when using message_notify_user or message_ask_user tools - without this parameter, the user cannot properly view file or website contents.
- TOOL RESULTS: After each tool execution, you will receive the results in your messages. You MUST carefully analyze these results to determine your next actions. These results contain critical information from your environment including file contents, execution outputs, search results, and more. Every decision you make should be informed by these tool results.
- FILES: Create organized file structures with clear naming conventions
- COMMUNICATION: Use message tools for updates and essential questions. Include the 'attachments' parameter with file paths or URLs when sharing resources with users.
- TOOL RESULTS: Carefully analyze all tool execution results to inform your next actions. These results provide critical environmental information including file contents, execution outputs, and search results.
- FILES: Create organized file structures with clear naming conventions. Store different types of data in appropriate formats.
- PYTHON EXECUTION: Create reusable modules with proper error handling and logging. Focus on maintainability and readability.
- CLI OPERATIONS:
* Use terminal commands for system operations, file manipulations, and quick tasks
* Avoid commands requiring confirmation; actively use -y or -f flags for automatic confirmation
* Avoid commands with excessive output; save to files when necessary
* Chain multiple commands with && operator to minimize interruptions
* Use pipe operator to pass command outputs, simplifying operations
* Use non-interactive `bc` for simple calculations, Python for complex math; never calculate mentally
* Use `uptime` command when users explicitly request sandbox status check or wake-up
- CODING:
* Must save code to files before execution; direct code input to interpreter commands is forbidden
* Write Python code for complex mathematical calculations and analysis
* Use search tools to find solutions when encountering unfamiliar problems
* For index.html referencing local resources, use deployment tools directly, or package everything into a zip file and provide it as a message attachment
- HYBRID APPROACH: Combine Python and CLI as needed - use Python for logic and data processing, CLI for system operations and utilities.
- WRITING: Use flowing paragraphs rather than lists; provide detailed content with proper citations.
# FILES TOOL USAGE
- Use file tools for reading, writing, appending, and editing to avoid string escape issues in shell commands
@ -61,7 +104,6 @@ Your approach is deliberately methodical and persistent:
- For lengthy documents, first save each section as separate draft files, then append them sequentially to create the final document
- During final compilation, no content should be reduced or summarized; the final length must exceed the sum of all individual draft files
- INFORMATION: Prioritize web search > model knowledge; document sources
- SHELL: Use efficient command chaining and avoid interactive prompts
- CODING: Save code to files before execution; implement error handling
- WRITING: Use flowing paragraphs rather than lists; provide detailed content

View File

@ -41,6 +41,7 @@ async def run_agent(thread_id: str, project_id: str, stream: bool = True, thread
# thread_manager.add_tool(SandboxBrowseTool, sandbox_id=sandbox_id, password=sandbox_pass)
thread_manager.add_tool(SandboxShellTool, sandbox_id=sandbox_id, password=sandbox_pass)
thread_manager.add_tool(SandboxFilesTool, sandbox_id=sandbox_id, password=sandbox_pass)
thread_manager.add_tool(MessageTool)
files_tool = SandboxFilesTool(sandbox_id=sandbox_id, password=sandbox_pass)
system_message = { "role": "system", "content": get_system_prompt() }
@ -69,20 +70,21 @@ async def run_agent(thread_id: str, project_id: str, stream: bool = True, thread
print(f"Last message was from assistant, stopping execution")
continue_execution = False
break
files_state = await files_tool.get_workspace_state()
# Simple string representation
state_str = str(files_state)
state_message = {
"role": "user",
"content": f"""
Current workspace state:
<current_workspace_state>
{state_str}
</current_workspace_state>
"""
}
# files_state = await files_tool.get_workspace_state()
# # Simple string representation
# state_str = str(files_state)
# state_message = {
# "role": "user",
# "content": f"""
# Current workspace state:
# <current_workspace_state>
# {state_str}
# </current_workspace_state>
# """
# }
# print(f"State message: {state_message}")
@ -90,7 +92,7 @@ Current workspace state:
thread_id=thread_id,
system_prompt=system_message,
stream=stream,
temporary_message=state_message,
# temporary_message=state_message,
llm_model=model_name,
llm_temperature=0.1,
llm_max_tokens=64000,

View File

@ -0,0 +1,402 @@
import os
from typing import List, Optional, Union, BinaryIO
from fastapi import FastAPI, UploadFile, File, HTTPException, APIRouter
from fastapi.responses import FileResponse, JSONResponse
from pydantic import BaseModel
from utils.logger import logger
from backend.agent.tools.utils.daytona_sandbox import daytona
class FileInfo(BaseModel):
"""Model for file information"""
name: str
path: str
is_dir: bool
size: int
mod_time: str
permissions: Optional[str] = None
class CreateFolderRequest(BaseModel):
"""Request model for creating folders"""
path: str
permissions: Optional[str] = None
class FileContentRequest(BaseModel):
"""Request model for file content operations"""
path: str
content: str
class SandboxAPI:
"""API wrapper for Daytona file system operations"""
def __init__(self, sandbox_id: str):
"""Initialize the Sandbox API with a sandbox ID
Args:
sandbox_id: The ID of the Daytona sandbox
"""
self.sandbox_id = sandbox_id
try:
self.sandbox = daytona.get_current_sandbox(sandbox_id)
logger.info(f"SandboxAPI initialized for sandbox: {sandbox_id}")
except Exception as e:
logger.error(f"Error getting sandbox with ID {sandbox_id}: {str(e)}")
raise e
# CREATE operations
def create_file(self, path: str, content: Union[str, bytes]) -> bool:
"""Create a new file in the sandbox
Args:
path: The path where the file should be created
content: The content to write to the file
Returns:
bool: True if the file was created successfully
"""
try:
if isinstance(content, str):
content = content.encode('utf-8')
self.sandbox.fs.upload_file(path, content)
logger.info(f"File created at {path}")
return True
except Exception as e:
logger.error(f"Error creating file at {path}: {str(e)}")
raise e
# def create_folder(self, path: str, permissions: Optional[str] = None) -> bool:
# """Create a new directory in the sandbox
#
# Args:
# path: The path where the directory should be created
# permissions: Optional permissions for the directory (e.g., "755")
#
# Returns:
# bool: True if the directory was created successfully
# """
# try:
# if permissions:
# self.sandbox.fs.create_folder(path, permissions)
# else:
# self.sandbox.fs.create_folder(path)
# logger.info(f"Folder created at {path}")
# return True
# except Exception as e:
# logger.error(f"Error creating folder at {path}: {str(e)}")
# raise e
# READ operations
def list_files(self, path: str) -> List[FileInfo]:
"""List files and directories at the specified path
Args:
path: The path to list files from
Returns:
List[FileInfo]: List of files and directories
"""
try:
files = self.sandbox.fs.list_files(path)
result = []
for file in files:
# Convert file information to our model
file_info = FileInfo(
name=file.name,
path=os.path.join(path, file.name),
is_dir=file.is_dir,
size=file.size,
mod_time=str(file.mod_time),
permissions=getattr(file, 'permissions', None)
)
result.append(file_info)
return result
except Exception as e:
logger.error(f"Error listing files at {path}: {str(e)}")
raise e
def read_file(self, path: str) -> bytes:
"""Read a file from the sandbox
Args:
path: The path of the file to read
Returns:
bytes: The content of the file
"""
try:
content = self.sandbox.fs.download_file(path)
return content
except Exception as e:
logger.error(f"Error reading file at {path}: {str(e)}")
raise e
def get_file_info(self, path: str) -> FileInfo:
"""Get information about a file or directory
Args:
path: The path to get information for
Returns:
FileInfo: File information
"""
try:
file_info = self.sandbox.fs.get_file_info(path)
return FileInfo(
name=os.path.basename(path),
path=path,
is_dir=file_info.is_dir,
size=file_info.size,
mod_time=str(file_info.mod_time),
permissions=getattr(file_info, 'permissions', None)
)
except Exception as e:
logger.error(f"Error getting file info for {path}: {str(e)}")
raise e
# # UPDATE operations
# def update_file(self, path: str, content: Union[str, bytes]) -> bool:
# """Update an existing file in the sandbox
#
# Args:
# path: The path of the file to update
# content: The new content for the file
#
# Returns:
# bool: True if the file was updated successfully
# """
# # For simplicity, we use the same method as create_file, as upload_file will overwrite
# return self.create_file(path, content)
# def set_file_permissions(self, path: str, permissions: str) -> bool:
# """Set permissions for a file or directory
#
# Args:
# path: The path of the file or directory
# permissions: The permissions to set (e.g., "644", "755")
#
# Returns:
# bool: True if permissions were set successfully
# """
# try:
# self.sandbox.fs.set_file_permissions(path, permissions)
# logger.info(f"Permissions set to {permissions} for {path}")
# return True
# except Exception as e:
# logger.error(f"Error setting permissions for {path}: {str(e)}")
# raise e
# # DELETE operations
# def delete_file(self, path: str) -> bool:
# """Delete a file from the sandbox
#
# Args:
# path: The path of the file to delete
#
# Returns:
# bool: True if the file was deleted successfully
# """
# try:
# self.sandbox.fs.delete_file(path)
# logger.info(f"File deleted at {path}")
# return True
# except Exception as e:
# logger.error(f"Error deleting file at {path}: {str(e)}")
# raise e
# # SEARCH operations
# def find_files(self, path: str, pattern: str) -> List[dict]:
# """Search for text in files
#
# Args:
# path: The path to search in
# pattern: The text pattern to search for
#
# Returns:
# List[dict]: List of matches with file, line, and content
# """
# try:
# results = self.sandbox.fs.find_files(path=path, pattern=pattern)
# return [
# {"file": match.file, "line": match.line, "content": match.content}
# for match in results
# ]
# except Exception as e:
# logger.error(f"Error searching for pattern {pattern} in {path}: {str(e)}")
# raise e
# def replace_in_files(self, files: List[str], pattern: str, new_value: str) -> bool:
# """Replace text in files
#
# Args:
# files: List of file paths to perform replacement in
# pattern: The text pattern to replace
# new_value: The replacement text
#
# Returns:
# bool: True if replacement was successful
# """
# try:
# self.sandbox.fs.replace_in_files(files, pattern, new_value)
# logger.info(f"Replaced '{pattern}' with '{new_value}' in {len(files)} files")
# return True
# except Exception as e:
# logger.error(f"Error replacing '{pattern}' with '{new_value}': {str(e)}")
# raise e
# Create a router for the Sandbox API instead of a standalone FastAPI app
router = APIRouter(tags=["sandbox"])
# Store sandbox instances
sandboxes = {}
@router.post("/sandboxes/{sandbox_id}/connect")
async def connect_sandbox(sandbox_id: str):
"""Connect to a sandbox and initialize the API"""
try:
sandboxes[sandbox_id] = SandboxAPI(sandbox_id)
return {"status": "connected", "sandbox_id": sandbox_id}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# CREATE endpoints
@router.post("/sandboxes/{sandbox_id}/files")
async def create_file_endpoint(sandbox_id: str, file_request: FileContentRequest):
"""Create a file in the sandbox"""
if sandbox_id not in sandboxes:
raise HTTPException(status_code=404, detail=f"Sandbox {sandbox_id} not connected")
try:
result = sandboxes[sandbox_id].create_file(file_request.path, file_request.content)
return {"status": "success", "created": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# READ endpoints
@router.get("/sandboxes/{sandbox_id}/files")
async def list_files_endpoint(sandbox_id: str, path: str):
"""List files and directories at the specified path"""
if sandbox_id not in sandboxes:
raise HTTPException(status_code=404, detail=f"Sandbox {sandbox_id} not connected")
try:
files = sandboxes[sandbox_id].list_files(path)
return {"files": [file.dict() for file in files]}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/sandboxes/{sandbox_id}/files/content")
async def read_file_endpoint(sandbox_id: str, path: str):
"""Read a file from the sandbox"""
if sandbox_id not in sandboxes:
raise HTTPException(status_code=404, detail=f"Sandbox {sandbox_id} not connected")
try:
content = sandboxes[sandbox_id].read_file(path)
return FileResponse(
path=path,
filename=os.path.basename(path),
media_type="application/octet-stream",
content=content
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/sandboxes/{sandbox_id}/files/info")
async def get_file_info_endpoint(sandbox_id: str, path: str):
"""Get information about a file or directory"""
if sandbox_id not in sandboxes:
raise HTTPException(status_code=404, detail=f"Sandbox {sandbox_id} not connected")
try:
file_info = sandboxes[sandbox_id].get_file_info(path)
return file_info.dict()
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# # UPDATE endpoints
# @router.put("/sandboxes/{sandbox_id}/files")
# async def update_file_endpoint(sandbox_id: str, file_request: FileContentRequest):
# """Update a file in the sandbox"""
# if sandbox_id not in sandboxes:
# raise HTTPException(status_code=404, detail=f"Sandbox {sandbox_id} not connected")
# try:
# result = sandboxes[sandbox_id].update_file(file_request.path, file_request.content)
# return {"status": "success", "updated": result}
# except Exception as e:
# raise HTTPException(status_code=500, detail=str(e))
# @router.put("/sandboxes/{sandbox_id}/files/permissions")
# async def set_permissions_endpoint(sandbox_id: str, path: str, permissions: str):
# """Set permissions for a file or directory"""
# if sandbox_id not in sandboxes:
# raise HTTPException(status_code=404, detail=f"Sandbox {sandbox_id} not connected")
# try:
# result = sandboxes[sandbox_id].set_file_permissions(path, permissions)
# return {"status": "success", "updated": result}
# except Exception as e:
# raise HTTPException(status_code=500, detail=str(e))
# # DELETE endpoints
# @router.delete("/sandboxes/{sandbox_id}/files")
# async def delete_file_endpoint(sandbox_id: str, path: str):
"""Delete a file from the sandbox"""
if sandbox_id not in sandboxes:
raise HTTPException(status_code=404, detail=f"Sandbox {sandbox_id} not connected")
try:
result = sandboxes[sandbox_id].delete_file(path)
return {"status": "success", "deleted": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# @router.post("/sandboxes/{sandbox_id}/folders")
# async def create_folder_endpoint(sandbox_id: str, folder_request: CreateFolderRequest):
# """Create a folder in the sandbox"""
# if sandbox_id not in sandboxes:
# raise HTTPException(status_code=404, detail=f"Sandbox {sandbox_id} not connected")
# try:
# result = sandboxes[sandbox_id].create_folder(
# folder_request.path,
# folder_request.permissions
# )
# return {"status": "success", "created": result}
# except Exception as e:
# raise HTTPException(status_code=500, detail=str(e))
# # SEARCH endpoints
# @router.get("/sandboxes/{sandbox_id}/search")
# async def search_files_endpoint(sandbox_id: str, path: str, pattern: str):
# """Search for text in files"""
# if sandbox_id not in sandboxes:
# raise HTTPException(status_code=404, detail=f"Sandbox {sandbox_id} not connected")
# try:
# results = sandboxes[sandbox_id].find_files(path, pattern)
# return {"matches": results}
# except Exception as e:
# raise HTTPException(status_code=500, detail=str(e))
# @router.post("/sandboxes/{sandbox_id}/replace")
# async def replace_in_files_endpoint(
# sandbox_id: str,
# files: List[str],
# pattern: str,
# new_value: str
# ):
# """Replace text in files"""
# if sandbox_id not in sandboxes:
# raise HTTPException(status_code=404, detail=f"Sandbox {sandbox_id} not connected")
# try:
# result = sandboxes[sandbox_id].replace_in_files(files, pattern, new_value)
# return {"status": "success", "replaced": result}
# except Exception as e:
# raise HTTPException(status_code=500, detail=str(e))

View File

@ -16,20 +16,20 @@ class MessageTool(Tool):
"type": "function",
"function": {
"name": "message_notify_user",
"description": "Send a message to user without requiring a response. Use for acknowledging receipt of messages, providing progress updates, reporting task completion, or explaining changes in approach.",
"description": "Send a message to user without requiring a response. Use for: 1) Progress updates during long-running tasks, 2) Acknowledging receipt of user instructions, 3) Reporting completion of major milestones, 4) Explaining changes in approach or strategy, 5) Summarizing findings or results without requiring input.",
"parameters": {
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "Message text to display to user"
"description": "Message text to display to user - should be informative and actionable"
},
"attachments": {
"anyOf": [
{"type": "string"},
{"items": {"type": "string"}, "type": "array"}
],
"description": "(Optional) List of attachments to show to user, can be file paths or URLs"
"description": "(Optional) List of attachments to show to user, can be file paths or URLs. Include when referencing created files, analysis results, or external resources."
}
},
"required": ["text"]
@ -43,8 +43,8 @@ class MessageTool(Tool):
{"param_name": "attachments", "node_type": "attribute", "path": ".", "required": False}
],
example='''
<message-notify-user attachments="path/to/file1.txt,path/to/file2.pdf,https://example.com/doc.pdf">
Task completed successfully!
<message-notify-user attachments="output/analysis_results.csv,output/visualization.png">
I've completed the data analysis and generated visualizations of the key trends. The analysis shows a 15% increase in engagement metrics over the last quarter, with the most significant growth in mobile users.
</message-notify-user>
'''
)
@ -79,25 +79,25 @@ class MessageTool(Tool):
"type": "function",
"function": {
"name": "message_ask_user",
"description": "Ask user a question and wait for response. Use for requesting clarification, asking for confirmation, or gathering additional information.",
"description": "Ask user a question and wait for response. Use for: 1) Requesting clarification on ambiguous requirements, 2) Seeking confirmation before proceeding with high-impact changes, 3) Gathering additional information needed to complete a task, 4) Offering options and requesting user preference, 5) Validating assumptions when critical to task success.",
"parameters": {
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "Question text to present to user"
"description": "Question text to present to user - should be specific and clearly indicate what information you need"
},
"attachments": {
"anyOf": [
{"type": "string"},
{"items": {"type": "string"}, "type": "array"}
],
"description": "(Optional) List of question-related files or reference materials"
"description": "(Optional) List of question-related files or reference materials. Include when the question references specific content the user needs to see."
},
"suggest_user_takeover": {
"type": "string",
"enum": ["none", "browser"],
"description": "(Optional) Suggested operation for user takeover"
"description": "(Optional) Suggested operation for user takeover. Use 'browser' when user might need to access a website for authentication or manual interaction."
}
},
"required": ["text"]
@ -112,8 +112,8 @@ class MessageTool(Tool):
{"param_name": "suggest_user_takeover", "node_type": "attribute", "path": ".", "required": False}
],
example='''
<message-ask-user attachments="path/to/file1.txt,path/to/file2.pdf" suggest_user_takeover="browser">
Would you like to continue with this approach?
<message-ask-user attachments="config/database_options.json,scripts/migration_plan.md" suggest_user_takeover="none">
I've prepared two database migration approaches (attached). The first minimizes downtime but requires more storage temporarily, while the second has longer downtime but uses less resources. Which approach would you prefer to implement?
</message-ask-user>
'''
)
@ -154,7 +154,7 @@ class MessageTool(Tool):
"type": "function",
"function": {
"name": "idle",
"description": "A special tool to indicate you have completed all tasks and are about to enter idle state.",
"description": "A special tool to indicate you have completed all tasks and are about to enter idle state. Use ONLY when: 1) All tasks in todo.md are marked complete, 2) The user's original request has been fully addressed, 3) There are no pending actions or follow-ups required, 4) You've delivered all final outputs and results to the user.",
"parameters": {
"type": "object"
}
@ -164,7 +164,10 @@ class MessageTool(Tool):
tag_name="idle",
mappings=[],
example='''
<idle></idle>
<idle>
<!-- Use this tool only after completing all tasks and delivering all final outputs -->
<!-- All todo.md items must be marked complete [x] before using this tool -->
</idle>
'''
)
async def idle(self) -> ToolResult:

View File

@ -288,125 +288,6 @@ class SandboxFilesTool(SandboxToolsBase):
except Exception as e:
return self.fail_response(f"Error deleting file: {str(e)}")
@openapi_schema({
"type": "function",
"function": {
"name": "search_files",
"description": "Search for text in files within a directory. The search is recursive by default.",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to search in (directory or file)"
},
"pattern": {
"type": "string",
"description": "Text pattern to search for"
},
"recursive": {
"type": "boolean",
"description": "Whether to search recursively in subdirectories",
"default": True
}
},
"required": ["path", "pattern"]
}
}
})
@xml_schema(
tag_name="search-files",
mappings=[
{"param_name": "path", "node_type": "attribute", "path": "."},
{"param_name": "pattern", "node_type": "attribute", "path": "."},
{"param_name": "recursive", "node_type": "attribute", "path": "."}
],
example='''
<search-files path="path/to/search" pattern="text-of-interest" recursive="true">
</search-files>
'''
)
async def search_files(self, path: str, pattern: str, recursive: bool = True) -> ToolResult:
try:
path = self.clean_path(path)
full_path = f"{self.workspace_path}/{path}" if not path.startswith(self.workspace_path) else path
results = self.sandbox.fs.find_files(
path=full_path,
pattern=pattern,
recursive=recursive
)
formatted_results = []
for match in results:
formatted_results.append({
"file": match.file,
"line": match.line,
"content": match.content
})
return self.success_response({
"matches": formatted_results,
"count": len(formatted_results)
})
except Exception as e:
return self.fail_response(f"Error searching files: {str(e)}")
@openapi_schema({
"type": "function",
"function": {
"name": "replace_in_file",
"description": "Replace text in a single file. Use for updating specific content or fixing errors in code.",
"parameters": {
"type": "object",
"properties": {
"file": {
"type": "string",
"description": "Path to the file to perform replacement in"
},
"pattern": {
"type": "string",
"description": "Text pattern to replace (exact match)"
},
"new_value": {
"type": "string",
"description": "New text to replace the pattern with"
}
},
"required": ["file", "pattern", "new_value"]
}
}
})
@xml_schema(
tag_name="replace-in-file",
mappings=[
{"param_name": "file", "node_type": "attribute", "path": "."},
{"param_name": "pattern", "node_type": "element", "path": "pattern"},
{"param_name": "new_value", "node_type": "element", "path": "new_value"}
],
example='''
<replace-in-file file="path/to/file.txt">
<pattern>old_text</pattern>
<new_value>new_text</new_value>
</replace-in-file>
'''
)
async def replace_in_file(self, file: str, pattern: str, new_value: str) -> ToolResult:
try:
file = self.clean_path(file)
full_path = f"{self.workspace_path}/{file}" if not file.startswith(self.workspace_path) else file
# Use the same Daytona SDK method but with a single file
self.sandbox.fs.replace_in_files(
files=[full_path],
pattern=pattern,
new_value=new_value
)
return self.success_response(f"Text replaced in file '{file}' successfully.")
except Exception as e:
return self.fail_response(f"Error replacing text in file: {str(e)}")
async def test_files_tool():
files_tool = SandboxFilesTool(
@ -418,27 +299,10 @@ async def test_files_tool():
print(res)
print(await files_tool.get_workspace_state())
print("2)", "*"*10)
res = await files_tool.search_files("/", "Hello")
print(res)
print(await files_tool.get_workspace_state())
print("3)", "*"*10)
res = await files_tool.str_replace("test.txt", "Hello", "Hi")
print(res)
print(await files_tool.get_workspace_state())
print("4)", "*"*10)
res = await files_tool.search_files("/", "Hello")
print(res)
print(await files_tool.get_workspace_state())
print("5)", "*"*10)
res = await files_tool.search_files("/", "Hi")
print(res)
print(await files_tool.get_workspace_state())
print("6)", "*"*10)
res = await files_tool.full_file_rewrite("test.txt", "FOOOOHi, world!")
print(res)
@ -452,13 +316,5 @@ async def test_files_tool():
print("8)", "*"*10)
res = await files_tool.search_files("/", "Hello")
print(res)
print(await files_tool.get_workspace_state())
print("9)", "*"*10)
res = await files_tool.replace_in_file("test.txt", "Hello", "Hi")
print(res)
print(await files_tool.get_workspace_state())

View File

@ -1,31 +1,76 @@
import asyncio
import os
from agentpress.tool import Tool, ToolResult, openapi_schema, xml_schema
from typing import Optional, Dict, Any
from dotenv import load_dotenv
from exa_py import Exa
from typing import List, Optional
from datetime import datetime
from agentpress.tool import Tool, ToolResult, openapi_schema, xml_schema
# Load environment variables
load_dotenv()
class ExaWebSearchTool(Tool):
"""Tool for performing web searches using the Exa API."""
class SearchTool(Tool):
"""Tool for executing web searches using the Exa API."""
def __init__(self):
def __init__(self, api_key: str = "91ada9c3-6d2f-4cd6-90c2-c7d943c5db25"):
super().__init__()
self.exa = Exa(api_key="91ada9c3-6d2f-4cd6-90c2-c7d943c5db25")
self.exa = Exa(api_key=api_key)
@openapi_schema({
"type": "function",
"function": {
"name": "info_search_web",
"description": "Search web pages using search engine. Use for obtaining latest information or finding references.",
"name": "web_search",
"description": "Search the web for information on a specific topic using Exa API",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query"
"description": "The search query to find relevant web pages"
},
"summary": {
"type": "boolean",
"description": "Whether to include a summary of the results",
"default": True
},
"start_published_date": {
"type": "string",
"description": "Optional start date for when results were published (ISO format YYYY-MM-DDTHH:MM:SS.sssZ)"
},
"end_published_date": {
"type": "string",
"description": "Optional end date for when results were published (ISO format YYYY-MM-DDTHH:MM:SS.sssZ)"
},
"start_crawl_date": {
"type": "string",
"description": "Optional start date for when results were crawled (ISO format YYYY-MM-DDTHH:MM:SS.sssZ)"
},
"end_crawl_date": {
"type": "string",
"description": "Optional end date for when results were crawled (ISO format YYYY-MM-DDTHH:MM:SS.sssZ)"
},
"include_text": {
"type": "array",
"items": {
"type": "string"
},
"description": "A list of terms that must be included in the results"
},
"exclude_text": {
"type": "array",
"items": {
"type": "string"
},
"description": "A list of terms that must be excluded from the results"
},
# "livecrawl": {
# "type": "string",
# "description": "Whether to perform a live crawl - 'always', 'fallback', or 'never'",
# "default": "always"
# },
"num_results": {
"type": "integer",
"description": "The number of results to return",
"default": 10
},
"type": {
"type": "string",
"description": "The type of search to perform - 'auto', 'keyword', or 'neural'",
"default": "auto"
}
},
"required": ["query"]
@ -33,73 +78,104 @@ class SearchTool(Tool):
}
})
@xml_schema(
tag_name="info-search-web",
tag_name="web-search",
mappings=[
{"param_name": "query", "node_type": "content", "path": "."}
{"param_name": "query", "node_type": "attribute", "path": "."},
{"param_name": "summary", "node_type": "attribute", "path": "."},
{"param_name": "start_published_date", "node_type": "attribute", "path": "."},
{"param_name": "end_published_date", "node_type": "attribute", "path": "."},
{"param_name": "start_crawl_date", "node_type": "attribute", "path": "."},
{"param_name": "end_crawl_date", "node_type": "attribute", "path": "."},
{"param_name": "include_text", "node_type": "attribute", "path": "."},
{"param_name": "exclude_text", "node_type": "attribute", "path": "."},
# {"param_name": "livecrawl", "node_type": "attribute", "path": "."},
{"param_name": "num_results", "node_type": "attribute", "path": "."},
{"param_name": "type", "node_type": "attribute", "path": "."}
],
example='''
<info-search-web>
search query here
</info-search-web>
<web-search
query="rubber gym mats best prices comparison"
summary="true"
include_text="important term"
exclude_text="unwanted term"
num_results="10"
type="auto">
</web-search>
'''
)
async def info_search_web(self, query: str) -> ToolResult:
async def web_search(
self,
query: str,
summary: bool = True,
start_published_date: Optional[str] = None,
end_published_date: Optional[str] = None,
start_crawl_date: Optional[str] = None,
end_crawl_date: Optional[str] = None,
include_text: Optional[List[str]] = None,
exclude_text: Optional[List[str]] = None,
# livecrawl: str = "always",
num_results: int = 10,
type: str = "auto"
) -> ToolResult:
"""
Search the web using the Exa API.
Parameters:
- query: The search query to find relevant web pages
- summary: Whether to include a summary of the results (default: True)
- start_published_date: Optional start date for published results (ISO format)
- end_published_date: Optional end date for published results (ISO format)
- start_crawl_date: Optional start date for crawled results (ISO format)
- end_crawl_date: Optional end date for crawled results (ISO format)
- include_text: List of terms that must be included in the results
- exclude_text: List of terms that must be excluded from the results
- num_results: The number of results to return (default: 10)
- type: The type of search to perform - 'auto', 'keyword', or 'neural' (default: 'auto')
"""
try:
# Build search parameters with hardcoded defaults
params: Dict[str, Any] = {
"use_autoprompt": True,
"include_domains": None,
"start_published_date": None,
"end_published_date": None
}
# Perform search with contents and highlights
search_response = self.exa.search_and_contents(
query,
highlights=True,
**params
)
# Format response
formatted_results = []
for result in search_response.results:
formatted_result = {
"title": result.title if hasattr(result, 'title') else "",
"url": result.url if hasattr(result, 'url') else "",
"published_date": result.published_date if hasattr(result, 'published_date') else "",
"text": result.text if hasattr(result, 'text') else "",
"highlights": result.highlights if hasattr(result, 'highlights') else []
}
formatted_results.append(formatted_result)
print("************* ")
print(formatted_results)
print("************* ")
return self.success_response({
"results": formatted_results,
"total": len(formatted_results)
})
# Prepare parameters, only including non-None values
params = {"query": query, "summary": summary, "num_results": num_results}
if start_published_date:
params["start_published_date"] = start_published_date
if end_published_date:
params["end_published_date"] = end_published_date
if start_crawl_date:
params["start_crawl_date"] = start_crawl_date
if end_crawl_date:
params["end_crawl_date"] = end_crawl_date
if include_text:
params["include_text"] = include_text
if exclude_text:
params["exclude_text"] = exclude_text
# if livecrawl:
# params["livecrawl"] = livecrawl
if type:
params["type"] = type
# Execute the search
search_response = self.exa.search_and_contents(**params)
# Convert to string representation
results_data = str(search_response)
return self.success_response(results_data)
except Exception as e:
return self.fail_response(f"Error performing web search: {str(e)}")
async def main():
"""Main execution function with proper async handling."""
search_tool = SearchTool()
try:
print("\nTesting search functionality...")
result = await search_tool.info_search_web(
"Latest developments in AI",
use_autoprompt=True,
include_domains=["www.techcrunch.com", "www.wired.com"],
start_date="2023-01-01"
)
print(result.output if result.success else result)
print("\nAll operations complete!")
except Exception as e:
print(f"\nError during execution: {str(e)}")
raise
if __name__ == "__main__":
# Run the async main function with a proper event loop
asyncio.run(main())
import asyncio
async def test_web_search():
"""Test function for the web search tool"""
search_tool = ExaWebSearchTool()
result = await search_tool.web_search(
query="rubber gym mats best prices comparison",
summary=False,
num_results=10
)
print(result)
asyncio.run(test_web_search())

View File

@ -14,6 +14,7 @@ import uuid
# Import the agent API module
from agent import api as agent_api
from agent.sandbox_api import router as sandbox_router
# Load environment variables
load_dotenv()
@ -67,6 +68,9 @@ app.add_middleware(
# Include the agent router with a prefix
app.include_router(agent_api.router, prefix="/api")
# Include the sandbox router with a prefix
app.include_router(sandbox_router, prefix="/api")
@app.get("/api/health-check")
async def health_check():
"""Health check endpoint to verify API is working."""