Spaces:
Paused
Paused
| """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, | |
| ), | |
| ] | |