File size: 13,665 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
"""
TokioAI Engine -- Adaptive Memory / Few-Shot Learning
Maintains a persistent store of successful interactions for dynamic few-shot injection.

This is the "fine-tuning in real time" -- instead of modifying weights,
we modify the context the model sees, injecting relevant examples that
worked before.

Storage: JSON file, indexed by intent + keywords for fast retrieval.
"""

import json
import os
import re
import time
from collections import Counter
from typing import Dict, List, Optional

DEFAULT_MEMORY_PATH = os.path.expanduser("~/.tokioai-engine/memory.json")
MAX_EXAMPLES_PER_CATEGORY = 100
MAX_EXAMPLE_AGE_DAYS = 90


class EngineMemory:
    """
    Persistent few-shot memory that makes the model smarter over time.
    
    Stores successful (user_input -> model_output) pairs, tagged by:
    - intent (tool_call, code, text, decision)
    - keywords (extracted from user input)
    - success (was the output valid/useful?)
    - timestamp
    
    On each query, retrieves the most relevant examples for few-shot injection.
    """

    def __init__(self, path: str = None):
        self.path = path or DEFAULT_MEMORY_PATH
        os.makedirs(os.path.dirname(self.path), exist_ok=True)
        self.examples: List[Dict] = []
        self._load()

    def _load(self):
        if os.path.exists(self.path):
            try:
                with open(self.path, "r") as f:
                    self.examples = json.load(f)
            except (json.JSONDecodeError, IOError):
                self.examples = []
        else:
            self.examples = self._seed_examples()
            self._save()

    def _save(self):
        with open(self.path, "w") as f:
            json.dump(self.examples, f, indent=2, ensure_ascii=False)

    def add(
        self,
        user_input: str,
        model_output: str,
        intent: str,
        success: bool = True,
        tool_name: str = None,
        latency_ms: int = None,
    ):
        """Record a successful interaction as a few-shot example."""
        # NEVER store errors, empty outputs, or hallucinated tool output
        if not model_output or len(model_output.strip()) < 5:
            return
        if "Error calling model" in model_output or "Client Error" in model_output:
            return
        if "Server Error" in model_output or "Traceback" in model_output:
            return
        if not success:
            return
        # Skip duplicate: same user input + same first 100 chars of output
        key = (user_input[:100], model_output[:100])
        for ex in self.examples[-50:]:  # Check last 50 only for speed
            if (ex.get("user", "")[:100], ex.get("assistant", "")[:100]) == key:
                return

        keywords = self._extract_keywords(user_input)

        entry = {
            "user": user_input[:500],
            "assistant": model_output[:1000],
            "intent": intent,
            "keywords": keywords,
            "success": success,
            "tool_name": tool_name,
            "latency_ms": latency_ms,
            "timestamp": time.time(),
            "uses": 0,  # How many times this was used as few-shot
        }

        self.examples.append(entry)

        # Prune old/excess entries
        self._prune()
        self._save()

    def retrieve(
        self,
        user_input: str,
        intent: str = None,
        top_k: int = 3,
    ) -> List[Dict]:
        """
        Retrieve the most relevant few-shot examples for a given query.
        Uses keyword overlap + intent matching + recency for ranking.
        """
        if not self.examples:
            return []

        query_keywords = set(self._extract_keywords(user_input))
        scored = []

        for ex in self.examples:
            if not ex.get("success", True):
                continue  # Skip failed examples

            score = 0.0

            # Keyword overlap (most important)
            ex_keywords = set(ex.get("keywords", []))
            overlap = query_keywords & ex_keywords
            if overlap:
                score += len(overlap) * 3.0

            # Intent match
            if intent and ex.get("intent") == intent:
                score += 2.0

            # Recency bonus (newer = better, decay over 30 days)
            age_days = (time.time() - ex.get("timestamp", 0)) / 86400
            recency = max(0, 1.0 - age_days / 30)
            score += recency

            # Usage penalty (avoid showing the same examples every time)
            score -= ex.get("uses", 0) * 0.5

            if score > 0:
                scored.append((score, ex))

        # Sort by score descending
        scored.sort(key=lambda x: x[0], reverse=True)

        # Mark as used
        results = []
        for score, ex in scored[:top_k]:
            ex["uses"] = ex.get("uses", 0) + 1
            results.append(ex)

        if results:
            self._save()

        return results

    def get_stats(self) -> Dict:
        """Return memory statistics."""
        if not self.examples:
            return {"total": 0}

        intents = Counter(ex.get("intent", "unknown") for ex in self.examples)
        tools = Counter(ex.get("tool_name", "none") for ex in self.examples if ex.get("tool_name"))
        success_rate = sum(1 for ex in self.examples if ex.get("success", True)) / len(self.examples)

        return {
            "total": len(self.examples),
            "by_intent": dict(intents),
            "by_tool": dict(tools),
            "success_rate": round(success_rate, 2),
            "oldest_days": round((time.time() - min(ex.get("timestamp", time.time()) for ex in self.examples)) / 86400, 1),
        }

    def feedback(self, index: int, success: bool):
        """Update success status for an example (for learning from failures)."""
        if 0 <= index < len(self.examples):
            self.examples[index]["success"] = success
            self._save()

    def _extract_keywords(self, text: str) -> List[str]:
        """Extract meaningful keywords from text."""
        # Lowercase, split, remove common words
        stopwords = {
            "the", "a", "an", "is", "are", "was", "were", "be", "been",
            "have", "has", "had", "do", "does", "did", "will", "would",
            "could", "should", "may", "might", "can", "shall", "must",
            "to", "of", "in", "for", "on", "with", "at", "by", "from",
            "it", "its", "this", "that", "these", "those", "my", "your",
            "i", "me", "we", "us", "you", "he", "she", "they", "them",
            "and", "or", "but", "not", "if", "then", "else", "so",
            "what", "how", "why", "when", "where", "which", "who",
            "que", "el", "la", "los", "las", "un", "una", "de", "en",
            "por", "para", "con", "es", "no", "si", "como", "me",
            "please", "help", "want", "need", "tell",
        }

        words = re.findall(r'[a-z0-9_./:-]+', text.lower())
        keywords = [w for w in words if w not in stopwords and len(w) > 1]

        # Also extract paths and commands
        paths = re.findall(r'/[\w./+-]+', text)
        keywords.extend(paths)

        return keywords[:20]  # Cap at 20 keywords

    def _prune(self):
        """Remove old or excess entries."""
        now = time.time()
        max_age = MAX_EXAMPLE_AGE_DAYS * 86400

        # Remove old entries
        self.examples = [
            ex for ex in self.examples
            if now - ex.get("timestamp", now) < max_age
        ]

        # If still too many, keep the most successful/used ones
        if len(self.examples) > MAX_EXAMPLES_PER_CATEGORY * 5:
            # Sort by (success, -age) and keep top N
            self.examples.sort(
                key=lambda ex: (ex.get("success", True), -ex.get("timestamp", 0)),
                reverse=True,
            )
            self.examples = self.examples[:MAX_EXAMPLES_PER_CATEGORY * 5]

    def _seed_examples(self) -> List[Dict]:
        """Seed initial few-shot examples for cold start."""
        now = time.time()
        seeds = [
            # Tool call examples
            {
                "user": "check disk space",
                "assistant": '{"name": "execute_local", "arguments": {"command": "df -h"}}',
                "intent": "tool_call",
                "keywords": ["check", "disk", "space"],
                "tool_name": "execute_local",
                "success": True,
                "timestamp": now,
                "uses": 0,
            },
            {
                "user": "what processes are using the most memory",
                "assistant": '{"name": "execute_local", "arguments": {"command": "ps aux --sort=-%mem | head -20"}}',
                "intent": "tool_call",
                "keywords": ["processes", "memory"],
                "tool_name": "execute_local",
                "success": True,
                "timestamp": now,
                "uses": 0,
            },
            {
                "user": "read the nginx config",
                "assistant": '{"name": "read_file", "arguments": {"path": "/etc/nginx/nginx.conf"}}',
                "intent": "tool_call",
                "keywords": ["read", "nginx", "config"],
                "tool_name": "read_file",
                "success": True,
                "timestamp": now,
                "uses": 0,
            },
            {
                "user": "find all python files with TODO comments",
                "assistant": '{"name": "search_files", "arguments": {"pattern": "TODO", "glob": "*.py"}}',
                "intent": "tool_call",
                "keywords": ["find", "python", "todo"],
                "tool_name": "search_files",
                "success": True,
                "timestamp": now,
                "uses": 0,
            },
            {
                "user": "check system health",
                "assistant": '{"name": "diagnose", "arguments": {"target": "local"}}',
                "intent": "tool_call",
                "keywords": ["check", "system", "health"],
                "tool_name": "diagnose",
                "success": True,
                "timestamp": now,
                "uses": 0,
            },
            {
                "user": "show me the contents of /etc/hostname",
                "assistant": '{"name": "read_file", "arguments": {"path": "/etc/hostname"}}',
                "intent": "tool_call",
                "keywords": ["show", "contents", "/etc/hostname"],
                "tool_name": "read_file",
                "success": True,
                "timestamp": now,
                "uses": 0,
            },
            {
                "user": "list docker containers",
                "assistant": '{"name": "execute_local", "arguments": {"command": "docker ps -a"}}',
                "intent": "tool_call",
                "keywords": ["docker", "containers", "list"],
                "tool_name": "execute_local",
                "success": True,
                "timestamp": now,
                "uses": 0,
            },
            {
                "user": "scan ports on 192.168.1.1",
                "assistant": '{"name": "execute_local", "arguments": {"command": "nmap -sV 192.168.1.1"}}',
                "intent": "tool_call",
                "keywords": ["scan", "ports", "nmap", "192.168.1.1"],
                "tool_name": "execute_local",
                "success": True,
                "timestamp": now,
                "uses": 0,
            },
            # Text examples (NO tool calls)
            {
                "user": "what is SQL injection",
                "assistant": "SQL injection is an attack where malicious SQL code is inserted into application queries through user input. The attacker can read, modify, or delete database data, bypass authentication, or even execute system commands. Prevention: parameterized queries, input validation, least-privilege DB accounts, WAF rules.",
                "intent": "text",
                "keywords": ["sql", "injection"],
                "success": True,
                "timestamp": now,
                "uses": 0,
            },
            {
                "user": "explain the TCP 3-way handshake",
                "assistant": "TCP 3-way handshake establishes a connection: (1) Client sends SYN with initial sequence number, (2) Server responds SYN-ACK with its own sequence number and acknowledges client's, (3) Client sends ACK. After this, data flows bidirectionally. Termination uses a 4-way FIN handshake.",
                "intent": "text",
                "keywords": ["tcp", "handshake"],
                "success": True,
                "timestamp": now,
                "uses": 0,
            },
            {
                "user": "hi",
                "assistant": "Hey. What do you need?",
                "intent": "text",
                "keywords": ["hi"],
                "success": True,
                "timestamp": now,
                "uses": 0,
            },
            # Decision examples (model correctly chose NOT to use tool)
            {
                "user": "what is docker",
                "assistant": "Docker is a containerization platform that packages applications with their dependencies into isolated containers. Containers share the host kernel (unlike VMs), making them lightweight and fast. Key concepts: images (blueprints), containers (running instances), Dockerfile (build instructions), docker-compose (multi-container apps).",
                "intent": "decision",
                "keywords": ["docker"],
                "success": True,
                "timestamp": now,
                "uses": 0,
            },
        ]
        return seeds