Full sync - all projects, memory, configs
This commit is contained in:
47
tools/tool-forge/README.md
Normal file
47
tools/tool-forge/README.md
Normal file
@ -0,0 +1,47 @@
|
||||
# Tool Forge — Self-Creating Tools System
|
||||
|
||||
Agents detect when they need a tool that doesn't exist and write it on the fly.
|
||||
RAG-backed tool discovery keeps context lean by only surfacing relevant tools.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Agent needs capability → search tool registry (RAG) → found? use it : create it → index new tool → execute
|
||||
```
|
||||
|
||||
## Components
|
||||
|
||||
- **registry.py** — ChromaDB-backed tool registry with semantic search
|
||||
- **forge.py** — Tool creation engine (generates Python tools from natural language specs)
|
||||
- **runner.py** — Safe tool execution sandbox
|
||||
- **cli.py** — CLI interface: `search`, `create`, `run`, `list`, `describe`
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Search for an existing tool
|
||||
python3 cli.py search "convert CSV to JSON"
|
||||
|
||||
# Create a new tool on the fly
|
||||
python3 cli.py create "convert CSV to JSON" --desc "Takes a CSV file path, returns JSON array"
|
||||
|
||||
# Run a tool
|
||||
python3 cli.py run csv_to_json --args '{"file_path": "data.csv"}'
|
||||
|
||||
# List all tools
|
||||
python3 cli.py list
|
||||
|
||||
# Index existing tools from the tools/ directory
|
||||
python3 cli.py index-existing
|
||||
```
|
||||
|
||||
## For Agents
|
||||
|
||||
```python
|
||||
from tool_forge import ToolForge
|
||||
|
||||
forge = ToolForge()
|
||||
# Returns matching tools or creates one if none found
|
||||
tool = forge.ensure_tool("convert CSV to JSON", auto_create=True)
|
||||
result = tool.run(file_path="data.csv")
|
||||
```
|
||||
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,
|
||||
)
|
||||
106
tools/tool-forge/registry.py
Normal file
106
tools/tool-forge/registry.py
Normal file
@ -0,0 +1,106 @@
|
||||
"""
|
||||
Tool Registry — ChromaDB-backed semantic search over tool catalog.
|
||||
Keeps agent context lean by only surfacing relevant tools via RAG.
|
||||
"""
|
||||
|
||||
import chromadb
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
DB_PATH = os.path.join(os.path.dirname(__file__), ".chromadb")
|
||||
TOOLS_DIR = os.path.join(os.path.dirname(__file__), "created_tools")
|
||||
|
||||
class ToolRegistry:
|
||||
def __init__(self, db_path: str = DB_PATH):
|
||||
self.client = chromadb.PersistentClient(path=db_path)
|
||||
self.collection = self.client.get_or_create_collection(
|
||||
name="tool_registry",
|
||||
metadata={"hnsw:space": "cosine"}
|
||||
)
|
||||
os.makedirs(TOOLS_DIR, exist_ok=True)
|
||||
|
||||
def register(self, name: str, description: str, source_path: str,
|
||||
params: dict = None, tags: list = None) -> dict:
|
||||
"""Register a tool in the semantic index."""
|
||||
doc = f"{name}: {description}"
|
||||
if tags:
|
||||
doc += f" tags: {', '.join(tags)}"
|
||||
|
||||
metadata = {
|
||||
"name": name,
|
||||
"description": description,
|
||||
"source_path": source_path,
|
||||
"params_json": json.dumps(params or {}),
|
||||
"tags": ",".join(tags or []),
|
||||
}
|
||||
|
||||
self.collection.upsert(
|
||||
ids=[name],
|
||||
documents=[doc],
|
||||
metadatas=[metadata],
|
||||
)
|
||||
return metadata
|
||||
|
||||
def search(self, query: str, n: int = 5, threshold: float = 0.6) -> list[dict]:
|
||||
"""Semantic search for tools. Returns matches above threshold."""
|
||||
results = self.collection.query(
|
||||
query_texts=[query],
|
||||
n_results=min(n, max(self.collection.count(), 1)),
|
||||
)
|
||||
if not results["ids"][0]:
|
||||
return []
|
||||
|
||||
tools = []
|
||||
for i, id_ in enumerate(results["ids"][0]):
|
||||
distance = results["distances"][0][i] if results["distances"] else 1.0
|
||||
similarity = 1 - distance
|
||||
if similarity >= threshold:
|
||||
meta = results["metadatas"][0][i]
|
||||
meta["similarity"] = round(similarity, 3)
|
||||
tools.append(meta)
|
||||
return tools
|
||||
|
||||
def get(self, name: str) -> Optional[dict]:
|
||||
"""Get a specific tool by name."""
|
||||
try:
|
||||
result = self.collection.get(ids=[name])
|
||||
if result["ids"]:
|
||||
return result["metadatas"][0]
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
def list_all(self) -> list[dict]:
|
||||
"""List all registered tools."""
|
||||
result = self.collection.get()
|
||||
return result["metadatas"] if result["metadatas"] else []
|
||||
|
||||
def delete(self, name: str):
|
||||
"""Remove a tool from the registry."""
|
||||
self.collection.delete(ids=[name])
|
||||
|
||||
def index_existing_tools(self, tools_dir: str = None) -> int:
|
||||
"""Index existing Python tools from a directory by extracting docstrings."""
|
||||
tools_dir = tools_dir or os.path.join(os.path.dirname(__file__), "..")
|
||||
count = 0
|
||||
for f in Path(tools_dir).glob("*.py"):
|
||||
if f.name.startswith("_"):
|
||||
continue
|
||||
try:
|
||||
source = f.read_text()
|
||||
# Extract first docstring
|
||||
desc = ""
|
||||
if '"""' in source:
|
||||
parts = source.split('"""')
|
||||
if len(parts) >= 3:
|
||||
desc = parts[1].strip().split("\n")[0]
|
||||
if not desc:
|
||||
desc = f"Tool: {f.stem}"
|
||||
name = f.stem.replace("-", "_")
|
||||
self.register(name, desc, str(f.resolve()), tags=["existing"])
|
||||
count += 1
|
||||
except Exception:
|
||||
continue
|
||||
return count
|
||||
Reference in New Issue
Block a user