""" TokioAI Engine -- Tool Schemas & JSON Validation Defines all available tools, their parameter schemas, and validation logic. """ import json import re from typing import Any, Dict, List, Optional # ============================================================ # TOOL DEFINITIONS (OpenAI function-calling format) # ============================================================ TOOL_DEFINITIONS = [ { "type": "function", "function": { "name": "execute_local", "description": "Execute a shell command on the local machine. Use for system operations: install packages, check processes, network scans, file operations.", "parameters": { "type": "object", "properties": { "command": { "type": "string", "description": "Shell command to execute (bash)" }, "timeout": { "type": "integer", "description": "Timeout in seconds (default 30, max 120)" } }, "required": ["command"] } } }, { "type": "function", "function": { "name": "execute_raspi", "description": "Execute a command on the Raspberry Pi via SSH.", "parameters": { "type": "object", "properties": { "command": { "type": "string", "description": "Command to run on Raspi" }, "timeout": { "type": "integer", "description": "Timeout in seconds (default 30, max 120)" } }, "required": ["command"] } } }, { "type": "function", "function": { "name": "execute_gcp", "description": "Execute a command on the GCP VM via SSH.", "parameters": { "type": "object", "properties": { "command": { "type": "string", "description": "Command to run on GCP" }, "timeout": { "type": "integer", "description": "Timeout in seconds (default 30, max 120)" } }, "required": ["command"] } } }, { "type": "function", "function": { "name": "execute_router", "description": "Execute a command on the router via SSH (root).", "parameters": { "type": "object", "properties": { "command": { "type": "string", "description": "Command to run on router as root" } }, "required": ["command"] } } }, { "type": "function", "function": { "name": "read_file", "description": "Read a file from local or remote systems. Use raspi:/path or gcp:/path for remote.", "parameters": { "type": "object", "properties": { "path": { "type": "string", "description": "File path. Use raspi:/path or gcp:/path for remote" }, "lines": { "type": "integer", "description": "Max lines to read (default: all)" } }, "required": ["path"] } } }, { "type": "function", "function": { "name": "write_file", "description": "Write content to a file. Prefix with raspi: or gcp: for remote.", "parameters": { "type": "object", "properties": { "path": { "type": "string", "description": "File path. Use raspi:/path or gcp:/path for remote" }, "content": { "type": "string", "description": "File content to write" } }, "required": ["path", "content"] } } }, { "type": "function", "function": { "name": "edit_file", "description": "Replace a specific string in a file. old_text must be unique.", "parameters": { "type": "object", "properties": { "path": { "type": "string", "description": "File path. Use raspi:/path or gcp:/path for remote" }, "old_text": { "type": "string", "description": "Exact text to find (must be unique)" }, "new_text": { "type": "string", "description": "Replacement text" } }, "required": ["path", "old_text", "new_text"] } } }, { "type": "function", "function": { "name": "search_files", "description": "Search for text patterns across files. Like grep -rn.", "parameters": { "type": "object", "properties": { "pattern": { "type": "string", "description": "Text or regex pattern to search" }, "path": { "type": "string", "description": "Directory to search. Use raspi:/path or gcp:/path" }, "glob": { "type": "string", "description": "File filter glob (e.g., '*.py')" } }, "required": ["pattern"] } } }, { "type": "function", "function": { "name": "robot_vision", "description": "Capture a photo from the robot's camera and analyze it with a vision AI model. Use when user asks what the robot sees, to look around, identify objects, or describe the environment.", "parameters": { "type": "object", "properties": { "question": { "type": "string", "description": "What to look for or ask about the image (e.g. 'how many people?', 'what objects are on the table?', 'describe the room'). Default: general description." } }, "required": [] } } }, { "type": "function", "function": { "name": "diagnose", "description": "Run system health diagnostics on local, raspi, gcp, or all targets.", "parameters": { "type": "object", "properties": { "target": { "type": "string", "enum": ["local", "raspi", "gcp", "router", "all"], "description": "Target: local, raspi, gcp, router, all" } }, "required": ["target"] } } }, { "type": "function", "function": { "name": "memory", "description": "Persistent memory. Actions: read, write, append, clear.", "parameters": { "type": "object", "properties": { "action": { "type": "string", "enum": ["read", "write", "append", "clear"], "description": "Action to perform" }, "content": { "type": "string", "description": "Content to write/append" } }, "required": ["action"] } } }, { "type": "function", "function": { "name": "task", "description": "Persistent task tracker. Actions: add, update, list, remove, clear_done.", "parameters": { "type": "object", "properties": { "action": { "type": "string", "enum": ["add", "update", "list", "remove", "clear_done"], "description": "Action to perform" }, "task": { "type": "string", "description": "Task description (for add)" }, "id": { "type": "integer", "description": "Task ID (for update/remove)" }, "status": { "type": "string", "enum": ["pending", "in_progress", "done", "blocked"], "description": "New status" } }, "required": ["action"] } } } ] # ============================================================ # TOOL NAME INDEX (for quick lookup) # ============================================================ VALID_TOOL_NAMES = {t["function"]["name"] for t in TOOL_DEFINITIONS} TOOL_SCHEMAS = { t["function"]["name"]: t["function"]["parameters"] for t in TOOL_DEFINITIONS } # ============================================================ # VALIDATION # ============================================================ def validate_tool_call(name: str, arguments: Dict[str, Any]) -> tuple[bool, str]: """ Validate a tool call against its schema. Returns (is_valid, error_message). """ if name not in VALID_TOOL_NAMES: return False, f"Unknown tool '{name}'. Valid: {', '.join(sorted(VALID_TOOL_NAMES))}" schema = TOOL_SCHEMAS[name] properties = schema.get("properties", {}) required = schema.get("required", []) # Check required params for req in required: if req not in arguments: return False, f"Missing required parameter '{req}' for tool '{name}'" # Check param types for param_name, param_value in arguments.items(): if param_name not in properties: continue # Extra params are OK (flexible) expected_type = properties[param_name].get("type", "string") if expected_type == "string" and not isinstance(param_value, str): return False, f"Parameter '{param_name}' must be string, got {type(param_value).__name__}" if expected_type == "integer" and not isinstance(param_value, int): return False, f"Parameter '{param_name}' must be integer, got {type(param_value).__name__}" # Check enum constraints for param_name, param_value in arguments.items(): if param_name in properties: enum_vals = properties[param_name].get("enum") if enum_vals and param_value not in enum_vals: return False, f"Parameter '{param_name}' must be one of {enum_vals}, got '{param_value}'" return True, "" def extract_tool_calls_from_text(text: str) -> List[Dict]: """ Extract tool calls from model output text. Supports multiple formats: 1. OpenAI-style JSON: {"name": "...", "arguments": {...}} 2. Markdown code blocks with JSON 3. Raw function-call-like patterns Returns list of {"name": str, "arguments": dict} """ calls = [] # Strategy 1: Find JSON objects with "name" and "arguments" keys # Handle both single objects and arrays json_patterns = [ # Array of tool calls r'\[\s*\{[^{]*?"name"\s*:\s*"[^"]+?".*?\}\s*\]', # Single tool call object r'\{\s*"(?:name|function)"\s*:\s*"[^"]+?".*?"arguments"\s*:\s*\{.*?\}\s*\}', ] for pattern in json_patterns: for match in re.finditer(pattern, text, re.DOTALL): try: data = json.loads(match.group()) if isinstance(data, list): for item in data: call = _normalize_tool_call(item) if call: calls.append(call) elif isinstance(data, dict): call = _normalize_tool_call(data) if call: calls.append(call) except json.JSONDecodeError: continue if calls: return calls # Strategy 1.5: Anthropic/XML-style tool calls # Format: value # Also: {"name": "...", "arguments": {...}} xml_patterns = [ # value... r'(.*?)', # JSON r'\s*(.*?)\s*', ] for pattern in xml_patterns: for match in re.finditer(pattern, text, re.DOTALL): groups = match.groups() if len(groups) == 2 and not groups[0].startswith('{'): # value format func_name = groups[0] params_text = groups[1] if func_name in VALID_TOOL_NAMES: # Extract parameters param_dict = {} for pm in re.finditer(r'\s*(.*?)\s*', params_text, re.DOTALL): param_dict[pm.group(1)] = pm.group(2).strip() calls.append({"name": func_name, "arguments": param_dict}) elif len(groups) >= 1: # JSON format try: data = json.loads(groups[-1]) call = _normalize_tool_call(data) if call: calls.append(call) except (json.JSONDecodeError, TypeError): pass if calls: return calls # Strategy 2: Look for ```json code blocks code_blocks = re.findall(r'```(?:json)?\s*\n(.*?)\n\s*```', text, re.DOTALL) for block in code_blocks: try: data = json.loads(block.strip()) if isinstance(data, list): for item in data: call = _normalize_tool_call(item) if call: calls.append(call) elif isinstance(data, dict): call = _normalize_tool_call(data) if call: calls.append(call) except json.JSONDecodeError: continue if calls: return calls # Strategy 3: Brute-force -- find ANY valid JSON object in the text brace_depth = 0 start = None for i, ch in enumerate(text): if ch == '{': if brace_depth == 0: start = i brace_depth += 1 elif ch == '}': brace_depth -= 1 if brace_depth == 0 and start is not None: candidate = text[start:i+1] try: data = json.loads(candidate) call = _normalize_tool_call(data) if call: calls.append(call) except json.JSONDecodeError: pass start = None return calls def _normalize_tool_call(data: dict) -> Optional[Dict]: """Normalize various tool call formats to {"name": str, "arguments": dict}.""" if not isinstance(data, dict): return None # Get name -- handle multiple formats name = data.get("name") if not name: func = data.get("function") if isinstance(func, dict): name = func.get("name") elif isinstance(func, str): name = func args = data.get("arguments") or data.get("parameters") or data.get("params") or data.get("args") if isinstance(args, str): try: args = json.loads(args) except json.JSONDecodeError: return None if not name or not isinstance(name, str): return None if args is None: args = {} if not isinstance(args, dict): return None # Only accept valid tool names if name not in VALID_TOOL_NAMES: return None return {"name": name, "arguments": args} def tools_to_prompt_text() -> str: """Generate a concise tool description for injection into prompts.""" lines = ["Available tools:"] for tool in TOOL_DEFINITIONS: f = tool["function"] params = f["parameters"].get("properties", {}) required = f["parameters"].get("required", []) param_strs = [] for pname, pinfo in params.items(): req_marker = "*" if pname in required else "" param_strs.append(f"{pname}{req_marker}: {pinfo.get('type', 'string')}") params_text = ", ".join(param_strs) if param_strs else "none" lines.append(f" - {f['name']}({params_text}): {f['description'][:80]}") return "\n".join(lines)