""" TokioAI Engine v3.0 -- Router / Intent Classifier Classifies user intent and builds optimal prompts with dynamic few-shot injection. v3.0 changes: - System prompt rewritten with INVESTIGATION PLAYBOOKS - "esta funcionando X?" is ALWAYS tool_call, never text - Better Spanish + investigation pattern detection - Knowledge about the local system (Pi, HA, Docker) """ import re from typing import Dict, List, Optional, Tuple from .schemas import TOOL_DEFINITIONS, tools_to_prompt_text # ============================================================ # INTENT CLASSIFICATION (zero-cost, pure regex/heuristics) # ============================================================ TOOL_INDICATORS = [ # Direct action verbs r'\b(run|execute|check|show|list|find|search|scan|install|deploy|restart|stop|start|kill|create|delete|remove|move|copy|rename)\b', # System queries (these ALWAYS need real data) r'\b(disk|memory|cpu|ram|process|port|network|service|docker|container|pid|uptime|load)\b', r'\b(file|directory|folder|path|config|log|permission)\b', # Specific commands r'\b(ls|cat|grep|find|ps|df|du|top|htop|netstat|ss|curl|wget|ping|nmap|apt|pip|npm|systemctl)\b', # Target references r'\b(raspi|raspberry|gcp|router|server|vm|machine|host)\b', # File operations r'\b(read|write|edit|modify|update|append|save)\s+(the\s+)?file\b', r'/[a-z]+(/[a-z0-9_./-]+)+', # Unix paths # Diagnostic r'\b(diagnos|health|status)\b', # STATE QUERIES -- these ALWAYS need tools (never answer from "knowledge") r'\b(running|installed|active|enabled|listening|open|up|down|alive|funciona|funcionando|corriendo|prendid[oa]|encendid[oa]|andando)\b', r'\b(version|cuant[oa]s?|cuales|which|where|donde|what.*running|que.*corre|que.*hay)\b', # Investigation patterns r'\b(check|verify|test|try|connect|access|reach|investigate)\b', # Home automation r'\b(home\s*assistant|hass|hassio|zigbee|zwave|mqtt|tasmota|esphome|automation|sensor|switch|light)\b', ] CODE_INDICATORS = [ r'\b(write|create|build|implement|code|script|function|class|program)\b.*\b(in|using|with)\s+(python|bash|javascript|rust|go|c\+\+|java|typescript)\b', r'\b(write|create|build)\b.*\b(python|bash|javascript|rust|go|java|typescript)\b.*\b(script|function|program|code)\b', r'\b(algorithm|data\s+structure|design\s+pattern|architecture)\b', r'\b(bug|error|fix|debug|traceback|exception|syntax)\b.*\b(code|script|program|file)\b', ] TEXT_INDICATORS = [ # Greetings (EN + ES) r'^(hi|hello|hey|hola|que\s+tal|buenos?\s+d[ií]as?|buenas?\s+(?:tardes?|noches?))\s*[.!?]*$', r'^(thanks?|thank\s+you|gracias|thx|de\s+nada)\s*[.!?]*$', # Conversational follow-ups r'^(como\s+(va|estas?|andas?|te\s+va)|how\s+are\s+you|how\'?s\s+it\s+going|what\'?s\s+up)\s*[.!?]*$', r'^(todo\s+bien|bien\s+y\s+tu|genial|cool|nice|great|awesome)\s*[.!?]*$', r'^(que\s+onda|que\s+hay|que\s+pasa|que\s+cuentas)\s*[.!?]*$', # Short affirmations r'^(yes|no|ok|okay|sure|maybe|si|no|dale|genial|perfecto|claro|obvio|exacto|listo)\s*[.!?]*$', # Identity / meta questions r'\b(who\s+are\s+you|what\s+can\s+you|tell\s+me\s+about\s+yourself)\b', r'\b(opinion|think\s+about|recommend|suggest|advice)\b', # Explicit CONCEPT explanations (NOT status checks) r'^(explain|describe|define)\s+(the\s+)?(concept|theory|protocol|idea|principle|difference)\b', r'^what\s+is\s+(a\s+)?(concept|theory|protocol|principle|idea|technique|method)\b', ] class IntentType: TOOL_CALL = "tool_call" CODE = "code" TEXT = "text" DECISION = "decision" def classify_intent(user_message: str, conversation_history: list = None) -> Tuple[str, float]: """ Classify user intent. Returns (intent_type, confidence). v3.0 RULE: Any question about whether something IS RUNNING, IS INSTALLED, IS WORKING, etc. is ALWAYS a tool_call. We can never answer these from "knowledge" -- we must CHECK THE ACTUAL SYSTEM. """ msg = user_message.lower().strip() # Investigation prompts from the engine itself if msg.startswith("investigation"): return IntentType.DECISION, 0.9 if "gathered enough" in msg or "final answer" in msg or "what i found" in msg: return IntentType.DECISION, 0.8 # Empty or very short if len(msg) < 3: return IntentType.TEXT, 0.9 # ===== STRONG TEXT SIGNALS (pure greetings, thanks, etc.) ===== # These are ONLY pure conversational. "hola" alone = text. # But "hola, esta funcionando HA?" = tool_call (caught by tool indicators below) for pattern in TEXT_INDICATORS: if re.match(pattern, msg, re.IGNORECASE): return IntentType.TEXT, 0.95 # ===== STATE QUESTIONS ARE ALWAYS TOOL CALLS ===== # "esta funcionando X?" / "is X running?" / "funciona el HA?" / etc. state_patterns = [ r'\b(esta|está)\s+(funcionando|corriendo|andando|prendid[oa]|activ[oa]|instalad[oa])\b', r'\b(funciona|corre|anda)\s+(el|la|lo|los|las)?\s*\w+', r'\bis\s+\w+\s+(running|installed|active|working|up|down|alive|enabled|listening)\b', r'\b(running|installed|active|working|funciona)\b.*\?', r'\bcheck\s+(if|whether|that)\b', # Port/network state queries r'\b(puerto|port)\s+\d+', r'\bhay\s+algo\s+(en|corriendo|escuchando)', r'\b(que|qué)\s+(hay|corre|esta)\s+(en|corriendo)', r'\b(listening|escuchando)\s+(on|en)\b', # Who/what is queries about system state r'\b(quien|quién|who)\s+(esta|está|is)\s+(logueado|logged|conectado|connected)\b', r'\b(puertos?|ports?)\s+(estan|están|are)\s+(escuchando|listening|abiertos?|open)\b', r'\b(vulnerabilidad|vulnerability|vulnerabilidades|vulnerabilities)\b', r'\b(audit|auditoria|auditoría)\b.*\b(seguridad|security)\b', r'\b(seguridad|security)\b.*\b(audit|auditoria|auditoría)\b', r'\b(logueado|logged\s+in|conectado)\b', # Vision / robot camera queries r'\b(que\s+ves|qué\s+ves|what\s+do\s+you\s+see|what\s+can\s+you\s+see)\b', r'\b(mira|look|fijate|fíjate|observa|observe)\b', r'\b(hay\s+alguien|is\s+anyone|is\s+there\s+anyone|who\s+is\s+there|quien\s+hay)\b', r'\b(muestra|show\s+me|take\s+a\s+(photo|picture|snapshot)|saca\s+una?\s+foto)\b', r'\b(camara|cámara|camera)\b', r'\b(ves\s+algo|see\s+anything|what.s\s+around)\b', r'\b(describe\s+(the\s+)?room|describe\s+(la\s+)?habitaci[oó]n|describe\s+(el\s+)?entorno)\b', ] for pattern in state_patterns: if re.search(pattern, msg, re.IGNORECASE): return IntentType.TOOL_CALL, 0.95 # ===== "what is X?" knowledge questions (concept, not status) ===== # "what is docker?" = text. "what is running on port 8123?" = tool. if re.match(r'^(what\s+is|what\s+are|explain|describe|define|que\s+es|qué\s+es)\s+', msg): # Check if it's asking about runtime state if re.search(r'\b(running|on\s+port|installed|active|listening|my\s+server|this\s+machine)\b', msg): return IntentType.TOOL_CALL, 0.9 return IntentType.TEXT, 0.85 # Score each category tool_score = 0 code_score = 0 text_score = 0 for pattern in TOOL_INDICATORS: if re.search(pattern, msg, re.IGNORECASE): tool_score += 1 for pattern in CODE_INDICATORS: if re.search(pattern, msg, re.IGNORECASE): code_score += 1 for pattern in TEXT_INDICATORS: if re.search(pattern, msg, re.IGNORECASE): text_score += 1 # Boost: previous turn context if conversation_history: last = conversation_history[-1] if conversation_history else None if last and last.get("role") == "tool": tool_score += 2 if last and last.get("role") == "assistant": content = last.get("content", "") if "I ran:" in content or "Result of" in content: tool_score += 1 # Determine winner scores = { IntentType.TOOL_CALL: tool_score, IntentType.CODE: code_score, IntentType.TEXT: text_score, } max_score = max(scores.values()) total = sum(scores.values()) or 1 if max_score == 0: if len(msg) < 30: return IntentType.TEXT, 0.6 # Longer messages with no clear indicators -> decision return IntentType.DECISION, 0.4 winner = max(scores, key=scores.get) confidence = max_score / (total + 1) # Close scores -> decision if tool_score > 0 and text_score > 0 and abs(tool_score - text_score) <= 1: return IntentType.DECISION, 0.5 return winner, min(confidence, 0.95) # ============================================================ # SYSTEM PROMPT (v3.0 -- with investigation playbooks) # ============================================================ SYSTEM_PROMPT_BASE = """You are TokioAI -- an elite cybersecurity and DevOps AI agent. You execute commands, fix bugs, deploy infrastructure, and solve problems. You have full access to the user's systems via tools. ## CRITICAL RULES -- NEVER VIOLATE THESE 1. NEVER simulate or fake tool execution in your text responses. WRONG: writing "```bash\ndf -h\n```\nResult: ..." in your text. RIGHT: call the execute_local tool with {"command": "df -h"}. The engine will execute the tool and give you the real result. 2. When you need system information, ALWAYS use a tool call. NEVER make up or guess command output. NEVER write fake results. 3. Your text responses should contain ONLY your analysis and conclusions. NEVER include "I ran:", "Result of tool:", or "```bash" blocks that simulate command execution. 4. If you already have real tool results in the conversation, summarize them in plain language. Do NOT echo back raw tool output format. ## ENVIRONMENT You are running on a Raspberry Pi 5 (your-device, Debian/aarch64). Key services: - Home Assistant: runs as Docker container "homeassistant" (NOT systemd). Check with: docker ps | grep home - Docker: available. Most services run as containers. - Ollama: runs locally, accessed via SSH tunnel to L4 GPU on port 8200 - TokioAI Engine: this server (port 8080) - SSH tunnels: several active to GCP services When checking if something is running, ALWAYS check both systemd AND docker. ## ROBOT VISION (robot_vision tool) You have a camera on the PiCar-X robot. Use the robot_vision tool to SEE what the robot sees. - Use it when the user asks: "que ves?", "what do you see?", "look around", "describe the room", "hay alguien?", "who is there?" - The tool captures a live photo from the robot's camera and analyzes it with a vision AI model (moondream) - You can pass a specific question: robot_vision(question="how many people are in the room?") - Or leave question empty for a general description - The robot camera is at a low angle (ground level), so perspective may be different from human height - Response includes image size, capture time, analysis time, and the visual description - ALWAYS use robot_vision for visual questions. NEVER say "I can't see" -- you CAN see through the robot. ## HOME ASSISTANT VERIFICATION (CRITICAL) After calling ANY HA service (media, lights, switches, etc.), you MUST: 1. Execute the service call 2. Wait 3 seconds (sleep 3) 3. GET the entity state to confirm it ACTUALLY changed 4. Report what is ACTUALLY happening, not what you hoped HA auth: HA_TOKEN=$(cat ~/.ha_token) && curl -s -H "Authorization: Bearer $HA_TOKEN" ... Base URL: http://localhost:8123 For music changes specifically: After play_media, run: sleep 3 && HA_TOKEN=$(cat ~/.ha_token) && curl -s -H "Authorization: Bearer $HA_TOKEN" http://localhost:8123/api/states/media_player.YOUR_SPEAKER | python3 -c "import sys,json;s=json.load(sys.stdin);a=s['attributes'];print(f'State: {s[\"state\"]}, Title: {a.get(\"media_title\",\"?\")}, Artist: {a.get(\"media_artist\",\"?\")}')" If title/artist didn't change or state is not "playing", tell the user it FAILED. A 200 OK from the API only means HA received the request, NOT that it worked. ALWAYS verify. ## HOME ASSISTANT COOKBOOK (execute via execute_local tool) Auth pattern: HA_TOKEN=$(cat ~/.ha_token) && curl -s -H "Authorization: Bearer $HA_TOKEN" ... Base: http://localhost:8123 MEDIA (YOUR_SPEAKER (e.g. Amazon Echo), entity: media_player.YOUR_SPEAKER): - What's playing: GET /api/states/media_player.YOUR_SPEAKER (check media_title, media_artist, state) - CHANGE MUSIC: POST /api/services/media_player/play_media with {"entity_id":"media_player.YOUR_SPEAKER","media_content_type":"music","media_content_id":"QUERY"} (e.g. "blues", "rock", "Beatles") - Volume: POST /api/services/media_player/volume_set with {"entity_id":"media_player.YOUR_SPEAKER","volume_level":0.5} - Play/Pause: POST /api/services/media_player/media_play_pause with {"entity_id":"media_player.YOUR_SPEAKER"} - Next/Prev: POST /api/services/media_player/media_next_track or media_previous_track - TTS (Alexa speaks): POST /api/services/notify/alexa_media_YOUR_SPEAKER with {"message":"TEXT","data":{"type":"tts"}} - Announce: POST /api/services/notify/alexa_media_YOUR_SPEAKER with {"message":"TEXT","data":{"type":"announce"}} NEVER use notify to change music. notify is ONLY for TTS/announcements. LIGHTS: light.living_room, light.bedroom, light.kitchen (customize for your setup) - Turn on/off: POST /api/services/light/turn_on or turn_off with {"entity_id":"light.ENTITY"} - Brightness: turn_on with {"entity_id":"light.ENTITY","brightness":128} (0-255) - Color: turn_on with {"entity_id":"light.ENTITY","rgb_color":[255,0,0]} SWITCHES: switch.YOUR_SWITCH (customize) SENSORS: sensor.YOUR_TEMPERATURE, weather.forecast_home (customize) VACUUM: vacuum.YOUR_VACUUM (start, stop, return_to_base) ## CORE RULES - Be DIRECT. Act first, explain after. - NEVER give up. If something fails, try alternatives. - When investigating: diagnose -> try alternatives -> root cause -> answer. - NEVER output passwords, API keys, or tokens in plain text. - Be concise but thorough. ## INVESTIGATION BEHAVIOR You are an INVESTIGATOR. When asked about system state, security, or any technical question: 1. ALWAYS check the actual system. NEVER speculate or say "you should check X" -- CHECK IT YOURSELF. 2. If the first command doesn't give the full picture, run MORE commands automatically. 3. For security questions: check configs, ports, services, permissions, logs. Don't stop at 1 command. 4. For "is X running?": check systemctl, docker, ps, ports. Try multiple approaches. 5. NEVER ask "do you want me to check more?" -- just CHECK IT. You are the expert. 6. NEVER say "you should review X" -- YOU review it and report findings. 7. Think like a senior security engineer: be thorough, check multiple angles, report specifics. 8. Always use TOOL CALLS -- never simulate commands in your text response. ## ANSWER FORMAT When you've gathered enough information: - State what you found CLEARLY - If something is running: say what it is, where, which port, how it was started - If something is NOT available: say what you tried and why you're confident it's not there - NEVER give generic "recommendations" -- give FACTS based on what you actually found - NEVER guess or assume what service runs on a port -- only report what the tool output ACTUALLY shows - If ss/netstat shows a process name, report THAT name. Do NOT map port numbers to "well-known" services unless the output confirms it - Report EXACTLY what the command output says. If a port shows "ssh" as the process, say it's an SSH tunnel, not "Vault" or "Redis" etc.""" PROMPT_TOOL_CALL = """ The user wants you to DO something or CHECK something. Use the appropriate tool. - For shell commands: use execute_local - For file reading: use read_file - For searching: use search_files - For diagnostics: use diagnose - For seeing/looking/camera/vision: use robot_vision Call the tool IMMEDIATELY. Do not explain what you will do -- just do it. If you need multiple pieces of information, call the MOST IMPORTANT tool first. If a tool fails, you'll get a chance to try again with a different approach. NEVER ask the user "want me to check more?" -- you ARE the expert, so CHECK IT YOURSELF. NEVER recommend actions without doing them first. ACT, then REPORT.""" PROMPT_CODE = """ The user wants code or technical explanation. - If they want to CREATE a file: use write_file with the complete code - If they want to MODIFY code: use read_file first, then edit_file - If they want EXPLANATION: respond directly with concise, expert-level text - Show code, not descriptions of code""" PROMPT_TEXT = """ The user is having a conversation or asking a knowledge question. Respond directly WITHOUT using any tools. Be concise and helpful. Answer from your knowledge. Do NOT call any tools for pure knowledge questions. Do NOT simulate tool execution in your text. Just answer naturally.""" PROMPT_DECISION = """ You must decide: does this need a tool, or can you answer from knowledge? Guidelines: - "What is X?" (concept) = text answer - "Is X running?" / "Check X" / "Show X" = USE A TOOL (requires system state) - Anything about the current system state = USE A TOOL - Anything you can answer from pure knowledge = text answer - When in doubt, USE A TOOL -- it's better to check than to guess""" INTENT_PROMPTS = { IntentType.TOOL_CALL: PROMPT_TOOL_CALL, IntentType.CODE: PROMPT_CODE, IntentType.TEXT: PROMPT_TEXT, IntentType.DECISION: PROMPT_DECISION, } def build_system_prompt( intent: str, few_shot_examples: List[Dict] = None, extra_context: str = None, ) -> str: """Build the optimal system prompt for a given intent.""" parts = [SYSTEM_PROMPT_BASE] # Add intent-specific instructions parts.append(INTENT_PROMPTS.get(intent, PROMPT_DECISION)) # Add tool descriptions (for tool/decision intents) if intent in (IntentType.TOOL_CALL, IntentType.DECISION): parts.append("\n" + tools_to_prompt_text()) # Add few-shot examples if few_shot_examples: parts.append("\n--- RELEVANT EXAMPLES ---") for ex in few_shot_examples[:3]: parts.append(f"User: {ex.get('input', '')}") parts.append(f"Response: {ex.get('output', '')}") parts.append("--- END EXAMPLES ---") if extra_context: parts.append(f"\n{extra_context}") return "\n".join(parts)