read and write tools, moved schemas to tool/__init__
This commit is contained in:
+8
-19
@@ -1,27 +1,16 @@
|
|||||||
from tools.bash import bash
|
from tools import TOOL_SCHEMAS, bash, read_file, write_file
|
||||||
|
|
||||||
TOOL_SCHEMAS = [
|
|
||||||
{
|
|
||||||
"name": "bash",
|
|
||||||
"description": "Execute a bash command in the isolated sandbox environment. Use this to run shell commands, install packages, run scripts, etc.",
|
|
||||||
"input_schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"command": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The bash command to execute (e.g., 'ls -la', 'python script.py', 'pip install requests')",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["command"],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
async def dispatch_tool(tool_name: str, tool_input: dict, sandbox) -> str:
|
async def dispatch_tool(tool_name: str, tool_input: dict, sandbox):
|
||||||
"""Route tool calls to implementations."""
|
"""Route tool calls to implementations."""
|
||||||
|
|
||||||
if tool_name == "bash":
|
if tool_name == "bash":
|
||||||
return await bash(command=tool_input["command"], sandbox=sandbox)
|
return await bash(command=tool_input["command"], sandbox=sandbox)
|
||||||
|
elif tool_name == "read_file":
|
||||||
|
return await read_file(tool_input["filepath"], sandbox=sandbox)
|
||||||
|
elif tool_name == "write_file":
|
||||||
|
return await write_file(
|
||||||
|
tool_input["filepath"], tool_input["content"], sandbox=sandbox
|
||||||
|
)
|
||||||
|
|
||||||
return f"Unknown tool: {tool_name}"
|
return f"Unknown tool: {tool_name}"
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import asyncio
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from tools.files import read_file, write_file
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
async def test_read_file_with_no_sandbox():
|
||||||
|
"""Test read_file handles missing sandbox."""
|
||||||
|
result = await read_file("test.txt", sandbox=None)
|
||||||
|
assert "error" in result.lower()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.integration
|
||||||
|
async def test_real_write_then_read():
|
||||||
|
"""Integration: Write file, then read it back."""
|
||||||
|
from sandbox.session import PodmanSandbox
|
||||||
|
|
||||||
|
async with PodmanSandbox() as sb:
|
||||||
|
# Write
|
||||||
|
result = await write_file("test.txt", "Hello, world!", sb)
|
||||||
|
assert "✓" in result
|
||||||
|
|
||||||
|
# Read back
|
||||||
|
content = await read_file("test.txt", sb)
|
||||||
|
assert "Hello, world!" in content
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
await asyncio.to_thread(sb.run, "rm test.txt")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.integration
|
||||||
|
async def test_real_write_creates_directories():
|
||||||
|
"""Integration: write_file creates parent directories."""
|
||||||
|
from sandbox.session import PodmanSandbox
|
||||||
|
|
||||||
|
async with PodmanSandbox() as sb:
|
||||||
|
result = await write_file("dir1/dir2/test.txt", "nested", sb)
|
||||||
|
assert "✓" in result
|
||||||
|
|
||||||
|
# Verify file exists
|
||||||
|
ls_result = await asyncio.to_thread(sb.run, "ls -R")
|
||||||
|
assert "dir1" in ls_result
|
||||||
|
assert "dir2" in ls_result
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# tools/__init__.py
|
||||||
|
from tools.files import READ_FILE_SCHEMA, WRITE_FILE_SCHEMA, read_file, write_file
|
||||||
|
|
||||||
|
from tools.bash import BASH_SCHEMA, bash
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"bash",
|
||||||
|
"read_file",
|
||||||
|
"write_file",
|
||||||
|
"TOOL_SCHEMAS",
|
||||||
|
]
|
||||||
|
|
||||||
|
TOOL_SCHEMAS = [
|
||||||
|
BASH_SCHEMA,
|
||||||
|
READ_FILE_SCHEMA,
|
||||||
|
WRITE_FILE_SCHEMA,
|
||||||
|
]
|
||||||
@@ -25,3 +25,19 @@ async def bash(command: str, sandbox=None) -> str:
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error: Unexpected failure: {e}"
|
return f"Error: Unexpected failure: {e}"
|
||||||
|
|
||||||
|
|
||||||
|
BASH_SCHEMA = {
|
||||||
|
"name": "bash",
|
||||||
|
"description": "Execute a bash command in the isolated sandbox environment. Use this to run shell commands, install packages, run scripts, etc.",
|
||||||
|
"input_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"command": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The bash command to execute (e.g., 'ls -la', 'python script.py', 'pip install requests')",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["command"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import asyncio
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
async def read_file(filepath: str, sandbox=None) -> str:
|
||||||
|
"""Read a file from workspace."""
|
||||||
|
if sandbox is None:
|
||||||
|
return "Error: No sandbox available"
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await asyncio.to_thread(sandbox.run, f"cat {filepath}")
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
return f"Error reading file: {e}"
|
||||||
|
|
||||||
|
|
||||||
|
async def write_file(filepath: str, content: str, sandbox=None) -> str:
|
||||||
|
"""Write content to a file."""
|
||||||
|
if sandbox is None:
|
||||||
|
return "Error: No sandbox available"
|
||||||
|
|
||||||
|
# Validate path
|
||||||
|
if filepath.startswith("/") or ".." in filepath:
|
||||||
|
return f"Error: Invalid path '{filepath}'. Use relative paths within workspace."
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Escape content for shell
|
||||||
|
escaped = content.replace("'", "'\\''")
|
||||||
|
|
||||||
|
# Create parent dirs if needed
|
||||||
|
parent = str(Path(filepath).parent)
|
||||||
|
if parent and parent != ".":
|
||||||
|
await asyncio.to_thread(sandbox.run, f"mkdir -p {parent}")
|
||||||
|
|
||||||
|
# Write file
|
||||||
|
await asyncio.to_thread(
|
||||||
|
sandbox.run, f"cat > {filepath} << 'EOF'\n{content}\nEOF"
|
||||||
|
)
|
||||||
|
|
||||||
|
return f"✓ Wrote {filepath}"
|
||||||
|
except Exception as e:
|
||||||
|
return f"Error writing file: {e}"
|
||||||
|
|
||||||
|
|
||||||
|
# Schemas
|
||||||
|
READ_FILE_SCHEMA = {
|
||||||
|
"name": "read_file",
|
||||||
|
"description": "Read the contents of a file from the workspace",
|
||||||
|
"input_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"filepath": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Path to file relative to workspace (e.g., 'app.py', 'src/utils.py')",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["filepath"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
WRITE_FILE_SCHEMA = {
|
||||||
|
"name": "write_file",
|
||||||
|
"description": "Write content to a file in the workspace. Creates parent directories if needed.",
|
||||||
|
"input_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"filepath": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Path to file relative to workspace (e.g., 'app.py', 'data/config.json')",
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Content to write to the file",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["filepath", "content"],
|
||||||
|
},
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user