smolcode / engine /tools.py
seanpoyner's picture
Upload folder using huggingface_hub
daea45b verified
Raw
History Blame Contribute Delete
6.66 kB
"""Coding tools exposed to the LiteForge agent.
Each tool is a Python callable registered via `liteforge.create_tool`. The agent
(running in Rust) decides when to call them; LiteForge invokes the callable with
a single `dict` of arguments and feeds the returned JSON-able dict back to the
model. All file/exec tools are confined to one `Workspace`.
Tool surface (kept deliberately small so a 3B model can use it reliably):
write_file(path, content) -> create/overwrite a file
read_file(path) -> read a file back
list_files() -> list workspace files
run_python(path) -> execute a file, return stdout/stderr/exit
run_tests() -> run pytest in the workspace
"""
from __future__ import annotations
import liteforge as lf
from . import browsercheck
from .preview import inline_app
from .sandbox import Workspace
from .trace_collector import TraceCollector
def _wrap(name: str, fn, collector: TraceCollector | None):
if collector is None:
return fn
def wrapped(args: dict):
collector.record_tool_call(name, args)
result = fn(args)
collector.record_tool_result(name, result)
return result
return wrapped
# Tool names in the order _tools() returns them — lets a registry select a
# subset by name without relying on attributes of the opaque lf tool object.
_TOOL_ORDER = ("write_file", "read_file", "list_files", "run_python", "run_tests")
# Tools the web builder needs. Static apps are "verified" by rendering, not by
# running Python, so we drop run_python/run_tests — a smaller, less confusing
# surface for a 3B model that should be writing HTML, not spawning processes.
_WEB_TOOLS = ("write_file", "read_file", "list_files")
def _registry(workspace: Workspace, names, collector: TraceCollector | None = None) -> lf.ToolRegistry:
reg = lf.ToolRegistry()
for name, tool in zip(_TOOL_ORDER, _tools(workspace, collector)):
if name in names:
reg.register(tool)
return reg
def build_registry(workspace: Workspace, collector: TraceCollector | None = None) -> lf.ToolRegistry:
"""Return a ToolRegistry of all coding tools bound to `workspace`."""
return _registry(workspace, _TOOL_ORDER, collector)
def build_web_registry(workspace: Workspace, collector: TraceCollector | None = None) -> lf.ToolRegistry:
"""Return the smolbuilder web agent's tools: file ops + a headless app check."""
reg = _registry(workspace, _WEB_TOOLS, collector)
reg.register(_check_app_tool(workspace, collector))
return reg
def check_app_impl(ws: Workspace, collector: TraceCollector | None, args: dict) -> dict:
"""Run check_app logic (shared by LiteForge tool and Rust python callback)."""
if not any(f == "index.html" for f in ws.list_files()):
return {"ok": False,
"errors": ["index.html not found: create it first with write_file."]}
files = {}
for rel in ws.list_files():
r = ws.read_file(rel)
if r.get("ok"):
files[rel] = r["content"]
ok, errors = browsercheck.check_html(inline_app(files))
if ok is None:
return {"ok": True, "errors": [],
"note": "runtime check unavailable here; assuming ok"}
if ok:
return {"ok": True, "errors": [],
"message": "The app loads and every button works."}
return {"ok": False, "errors": errors,
"hint": "Fix these JavaScript errors in index.html, then call check_app again."}
def _check_app_tool(ws: Workspace, collector: TraceCollector | None = None):
"""A `check_app` tool: actually run the built app and report JS errors."""
def check_app(args: dict) -> dict:
return check_app_impl(ws, collector, args)
check_app = _wrap("check_app", check_app, collector)
return lf.create_tool(
"check_app",
"Run the current web app in a headless browser: load index.html, execute "
"its JavaScript, click every button, and report any errors. Use this to "
"verify the app actually works before finishing.",
{"type": "object", "properties": {}},
check_app,
)
def _tools(ws: Workspace, collector: TraceCollector | None = None) -> list:
def write_file(args: dict) -> dict:
return ws.write_file(args["path"], args.get("content", ""))
def read_file(args: dict) -> dict:
return ws.read_file(args["path"])
def list_files(args: dict) -> dict:
return {"ok": True, "files": ws.list_files()}
def run_python(args: dict) -> dict:
return ws.run_python(path=args["path"]).as_tool_payload()
def run_tests(args: dict) -> dict:
return ws.run_tests().as_tool_payload()
write_file = _wrap("write_file", write_file, collector)
read_file = _wrap("read_file", read_file, collector)
list_files = _wrap("list_files", list_files, collector)
run_python = _wrap("run_python", run_python, collector)
run_tests = _wrap("run_tests", run_tests, collector)
return [
lf.create_tool(
"write_file",
"Create or overwrite a file in the workspace with the given text content.",
{
"type": "object",
"properties": {
"path": {"type": "string", "description": "Relative path, e.g. main.py"},
"content": {"type": "string", "description": "Full file contents"},
},
"required": ["path", "content"],
},
write_file,
),
lf.create_tool(
"read_file",
"Read a file from the workspace and return its contents.",
{
"type": "object",
"properties": {"path": {"type": "string"}},
"required": ["path"],
},
read_file,
),
lf.create_tool(
"list_files",
"List all files currently in the workspace.",
{"type": "object", "properties": {}},
list_files,
),
lf.create_tool(
"run_python",
"Run a Python file in the workspace. Returns stdout, stderr and exit code.",
{
"type": "object",
"properties": {"path": {"type": "string", "description": "File to run, e.g. main.py"}},
"required": ["path"],
},
run_python,
),
lf.create_tool(
"run_tests",
"Run the test suite (pytest) in the workspace. Returns pass/fail output.",
{"type": "object", "properties": {}},
run_tests,
),
]