File size: 18,466 Bytes
4b97c3b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
"""
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)