195 lines
5.2 KiB
Python
195 lines
5.2 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""
|
||
|
|
Tool Registry - Layer 2 Component
|
||
|
|
|
||
|
|
Defines and manages tools available to the Claw runtime.
|
||
|
|
Tools are executed locally, not sent to the intelligence layer.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import re
|
||
|
|
import json
|
||
|
|
import subprocess
|
||
|
|
from typing import Dict, Any, List, Callable, Optional
|
||
|
|
from dataclasses import dataclass
|
||
|
|
from datetime import datetime
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class Tool:
|
||
|
|
name: str
|
||
|
|
description: str
|
||
|
|
parameters: Dict[str, Any]
|
||
|
|
handler: Callable
|
||
|
|
|
||
|
|
|
||
|
|
class ToolRegistry:
|
||
|
|
"""
|
||
|
|
Registry of available tools for the Claw runtime.
|
||
|
|
|
||
|
|
Tools are pattern-matched from user messages and executed
|
||
|
|
before routing to the intelligence layer.
|
||
|
|
"""
|
||
|
|
|
||
|
|
def __init__(self):
|
||
|
|
self.tools: Dict[str, Tool] = {}
|
||
|
|
self._register_builtin_tools()
|
||
|
|
|
||
|
|
def _register_builtin_tools(self):
|
||
|
|
"""Register built-in tools."""
|
||
|
|
self.register(Tool(
|
||
|
|
name="time",
|
||
|
|
description="Get current time",
|
||
|
|
parameters={},
|
||
|
|
handler=self._get_time
|
||
|
|
))
|
||
|
|
|
||
|
|
self.register(Tool(
|
||
|
|
name="status",
|
||
|
|
description="Get system status",
|
||
|
|
parameters={},
|
||
|
|
handler=self._get_status
|
||
|
|
))
|
||
|
|
|
||
|
|
self.register(Tool(
|
||
|
|
name="echo",
|
||
|
|
description="Echo a message back",
|
||
|
|
parameters={"message": "string"},
|
||
|
|
handler=self._echo
|
||
|
|
))
|
||
|
|
|
||
|
|
self.register(Tool(
|
||
|
|
name="ollama_list",
|
||
|
|
description="List available Ollama models",
|
||
|
|
parameters={},
|
||
|
|
handler=self._ollama_list
|
||
|
|
))
|
||
|
|
|
||
|
|
def register(self, tool: Tool):
|
||
|
|
"""Register a new tool."""
|
||
|
|
self.tools[tool.name] = tool
|
||
|
|
|
||
|
|
def parse_tool_call(self, message: str) -> Optional[Dict[str, Any]]:
|
||
|
|
"""
|
||
|
|
Parse a message for tool invocation.
|
||
|
|
|
||
|
|
Patterns:
|
||
|
|
- /tool_name
|
||
|
|
- /tool_name param=value
|
||
|
|
- @tool_name
|
||
|
|
"""
|
||
|
|
# Match /tool_name or @tool_name patterns
|
||
|
|
match = re.match(r'^[/@](\w+)(?:\s+(.+))?$', message.strip())
|
||
|
|
if not match:
|
||
|
|
return None
|
||
|
|
|
||
|
|
tool_name = match.group(1)
|
||
|
|
args_str = match.group(2) or ""
|
||
|
|
|
||
|
|
if tool_name not in self.tools:
|
||
|
|
return None
|
||
|
|
|
||
|
|
# Parse parameters
|
||
|
|
params = {}
|
||
|
|
if args_str:
|
||
|
|
# Simple key=value parsing
|
||
|
|
for pair in args_str.split():
|
||
|
|
if '=' in pair:
|
||
|
|
key, value = pair.split('=', 1)
|
||
|
|
params[key] = value
|
||
|
|
else:
|
||
|
|
# Positional argument as 'message'
|
||
|
|
params["message"] = args_str
|
||
|
|
|
||
|
|
return {
|
||
|
|
"name": tool_name,
|
||
|
|
"parameters": params
|
||
|
|
}
|
||
|
|
|
||
|
|
def execute(self, tool_call: Dict[str, Any]) -> str:
|
||
|
|
"""Execute a tool call."""
|
||
|
|
tool_name = tool_call["name"]
|
||
|
|
params = tool_call.get("parameters", {})
|
||
|
|
|
||
|
|
if tool_name not in self.tools:
|
||
|
|
return f"Error: Unknown tool '{tool_name}'"
|
||
|
|
|
||
|
|
tool = self.tools[tool_name]
|
||
|
|
|
||
|
|
try:
|
||
|
|
result = tool.handler(**params)
|
||
|
|
return f"[Tool: {tool_name}]\n{result}"
|
||
|
|
except Exception as e:
|
||
|
|
return f"[Tool Error: {tool_name}]\n{str(e)}"
|
||
|
|
|
||
|
|
def list_tools(self) -> List[str]:
|
||
|
|
"""List all available tools."""
|
||
|
|
return [
|
||
|
|
f"{name}: {tool.description}"
|
||
|
|
for name, tool in self.tools.items()
|
||
|
|
]
|
||
|
|
|
||
|
|
# --- Tool Handlers ---
|
||
|
|
|
||
|
|
def _get_time(self) -> str:
|
||
|
|
"""Get current time."""
|
||
|
|
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||
|
|
|
||
|
|
def _get_status(self) -> str:
|
||
|
|
"""Get system status."""
|
||
|
|
return json.dumps({
|
||
|
|
"runtime": "claw",
|
||
|
|
"version": "1.0.0-poc",
|
||
|
|
"status": "operational",
|
||
|
|
"tools_available": len(self.tools)
|
||
|
|
}, indent=2)
|
||
|
|
|
||
|
|
def _echo(self, message: str = "") -> str:
|
||
|
|
"""Echo a message."""
|
||
|
|
return message
|
||
|
|
|
||
|
|
def _ollama_list(self) -> str:
|
||
|
|
"""List Ollama models."""
|
||
|
|
try:
|
||
|
|
result = subprocess.run(
|
||
|
|
["curl", "-s", "http://localhost:11434/api/tags"],
|
||
|
|
capture_output=True,
|
||
|
|
text=True
|
||
|
|
)
|
||
|
|
data = json.loads(result.stdout)
|
||
|
|
models = [m["name"] for m in data.get("models", [])]
|
||
|
|
return f"Available models:\n" + "\n".join(f" - {m}" for m in models)
|
||
|
|
except Exception as e:
|
||
|
|
return f"Error listing models: {e}"
|
||
|
|
|
||
|
|
|
||
|
|
def main():
|
||
|
|
"""CLI for testing tool registry."""
|
||
|
|
registry = ToolRegistry()
|
||
|
|
|
||
|
|
print("Available tools:")
|
||
|
|
for tool_info in registry.list_tools():
|
||
|
|
print(f" {tool_info}")
|
||
|
|
|
||
|
|
print("\nTest parsing:")
|
||
|
|
test_messages = [
|
||
|
|
"/time",
|
||
|
|
"/status",
|
||
|
|
"/echo Hello world",
|
||
|
|
"/ollama_list",
|
||
|
|
"Regular message without tool"
|
||
|
|
]
|
||
|
|
|
||
|
|
for msg in test_messages:
|
||
|
|
parsed = registry.parse_tool_call(msg)
|
||
|
|
if parsed:
|
||
|
|
result = registry.execute(parsed)
|
||
|
|
print(f"\n> {msg}")
|
||
|
|
print(result)
|
||
|
|
else:
|
||
|
|
print(f"\n> {msg}")
|
||
|
|
print("(No tool call detected)")
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|