Files
workspace/tools/tool-forge/forge.py

149 lines
4.6 KiB
Python

"""
Tool Forge — Creates new Python tools from natural language specifications.
Generates standalone, safe, importable tool modules.
"""
import os
import re
import json
import textwrap
from pathlib import Path
from registry import ToolRegistry, TOOLS_DIR
TOOL_TEMPLATE = '''\
"""
{description}
Auto-generated by Tool Forge.
Parameters: {params_doc}
"""
import json
import sys
{extra_imports}
def run({param_signature}):
"""{description}"""
{body}
def main():
"""CLI entry point."""
import argparse
parser = argparse.ArgumentParser(description="{description}")
{argparse_args}
args = parser.parse_args()
result = run(**vars(args))
if result is not None:
if isinstance(result, (dict, list)):
print(json.dumps(result, indent=2, default=str))
else:
print(result)
if __name__ == "__main__":
main()
'''
class ToolForge:
def __init__(self, registry: ToolRegistry = None):
self.registry = registry or ToolRegistry()
def create_tool(self, name: str, description: str, params: dict = None,
body: str = None, imports: list = None, tags: list = None) -> dict:
"""
Create a new tool and register it.
Args:
name: Tool name (snake_case)
description: What the tool does
params: Dict of {param_name: {"type": str, "desc": str, "default": any}}
body: Python function body (indented 4 spaces). If None, generates a stub.
imports: Extra import lines
tags: Searchable tags
"""
name = re.sub(r'[^a-z0-9_]', '_', name.lower().replace('-', '_').replace(' ', '_'))
name = re.sub(r'_+', '_', name).strip('_')
params = params or {}
tags = tags or []
imports = imports or []
# Build parameter signature
required = {k: v for k, v in params.items() if "default" not in v}
optional = {k: v for k, v in params.items() if "default" in v}
sig_parts = list(required.keys())
for k, v in optional.items():
sig_parts.append(f"{k}={repr(v['default'])}")
param_signature = ", ".join(sig_parts) if sig_parts else ""
# Params doc
params_doc = ", ".join(f"{k} ({v.get('type', 'any')}): {v.get('desc', '')}"
for k, v in params.items()) or "none"
# Argparse args
argparse_lines = []
for k, v in params.items():
type_str = v.get("type", "str")
py_type = {"str": "str", "int": "int", "float": "float", "bool": "bool"}.get(type_str, "str")
if "default" in v:
argparse_lines.append(
f' parser.add_argument("--{k}", type={py_type}, default={repr(v["default"])}, help="{v.get("desc", "")}")')
else:
argparse_lines.append(
f' parser.add_argument("{k}", type={py_type}, help="{v.get("desc", "")}")')
argparse_args = "\n".join(argparse_lines) if argparse_lines else ' pass'
# Body
if not body:
body = f' # TODO: Implement {name}\n raise NotImplementedError("Tool {name} needs implementation")'
extra_imports = "\n".join(f"import {i}" for i in imports)
source = TOOL_TEMPLATE.format(
description=description,
params_doc=params_doc,
param_signature=param_signature,
body=body,
extra_imports=extra_imports,
argparse_args=argparse_args,
)
# Write tool file
tool_path = os.path.join(TOOLS_DIR, f"{name}.py")
Path(tool_path).write_text(source)
os.chmod(tool_path, 0o755)
# Register in RAG
self.registry.register(name, description, tool_path, params, tags + ["auto-created"])
return {
"name": name,
"path": tool_path,
"description": description,
"params": params,
}
def ensure_tool(self, query: str, auto_create: bool = True,
params: dict = None, body: str = None) -> dict:
"""
Search for existing tool; create one if not found and auto_create is True.
Returns tool metadata.
"""
matches = self.registry.search(query, n=3, threshold=0.5)
if matches:
return matches[0]
if not auto_create:
return None
# Derive a name from the query
name = re.sub(r'[^a-z0-9 ]', '', query.lower())
name = '_'.join(name.split()[:5])
return self.create_tool(
name=name,
description=query,
params=params,
body=body,
)