Full sync - all projects, memory, configs
This commit is contained in:
148
tools/tool-forge/forge.py
Normal file
148
tools/tool-forge/forge.py
Normal file
@ -0,0 +1,148 @@
|
||||
"""
|
||||
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,
|
||||
)
|
||||
Reference in New Issue
Block a user