149 lines
4.6 KiB
Python
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,
|
|
)
|