Turojin commited on
Commit
fb283e5
·
verified ·
1 Parent(s): 79f8a03

Upload jarvis/memory.py

Browse files
Files changed (1) hide show
  1. jarvis/memory.py +205 -0
jarvis/memory.py ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Memory layer: SQLite + ChromaDB for J.A.R.V.I.S."""
2
+ import sqlite3
3
+ import json
4
+ import uuid
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+ from typing import List, Dict, Optional, Any
8
+
9
+ import chromadb
10
+ from chromadb.config import Settings
11
+ from sentence_transformers import SentenceTransformer
12
+
13
+ from jarvis.config import MemoryConfig
14
+
15
+
16
+ class SQLiteStore:
17
+ """Structured data: user state, open loops, preferences, mirror log."""
18
+
19
+ def __init__(self, db_path: str):
20
+ self.db_path = db_path
21
+ Path(db_path).parent.mkdir(parents=True, exist_ok=True)
22
+ self._init_tables()
23
+
24
+ def _connect(self):
25
+ return sqlite3.connect(self.db_path)
26
+
27
+ def _init_tables(self):
28
+ with self._connect() as conn:
29
+ conn.executescript("""
30
+ CREATE TABLE IF NOT EXISTS user_state (
31
+ key TEXT PRIMARY KEY,
32
+ value TEXT,
33
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
34
+ );
35
+ CREATE TABLE IF NOT EXISTS open_loops (
36
+ id TEXT PRIMARY KEY,
37
+ title TEXT,
38
+ description TEXT,
39
+ status TEXT DEFAULT 'open',
40
+ priority INTEGER DEFAULT 5,
41
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
42
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
43
+ last_nudged_at TIMESTAMP
44
+ );
45
+ CREATE TABLE IF NOT EXISTS mirror_check_log (
46
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
47
+ turn_id TEXT,
48
+ contracts TEXT,
49
+ dials TEXT,
50
+ drift_detected BOOLEAN,
51
+ notes TEXT,
52
+ checked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
53
+ );
54
+ CREATE TABLE IF NOT EXISTS exchanges (
55
+ id TEXT PRIMARY KEY,
56
+ user_message TEXT,
57
+ jarvis_response TEXT,
58
+ contract TEXT,
59
+ dial INTEGER,
60
+ archetype_mix TEXT,
61
+ score REAL,
62
+ timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
63
+ );
64
+ """)
65
+
66
+ # ---- User State ----
67
+ def set_state(self, key: str, value: Any):
68
+ with self._connect() as conn:
69
+ conn.execute(
70
+ "INSERT INTO user_state (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=CURRENT_TIMESTAMP",
71
+ (key, json.dumps(value)),
72
+ )
73
+
74
+ def get_state(self, key: str, default: Any = None) -> Any:
75
+ with self._connect() as conn:
76
+ row = conn.execute("SELECT value FROM user_state WHERE key=?", (key,)).fetchone()
77
+ if row:
78
+ return json.loads(row[0])
79
+ return default
80
+
81
+ # ---- Open Loops ----
82
+ def add_loop(self, title: str, description: str = "", priority: int = 5) -> str:
83
+ lid = str(uuid.uuid4())
84
+ with self._connect() as conn:
85
+ conn.execute(
86
+ "INSERT INTO open_loops (id, title, description, priority) VALUES (?, ?, ?, ?)",
87
+ (lid, title, description, priority),
88
+ )
89
+ return lid
90
+
91
+ def list_loops(self, status: Optional[str] = None) -> List[Dict]:
92
+ with self._connect() as conn:
93
+ if status:
94
+ rows = conn.execute("SELECT * FROM open_loops WHERE status=? ORDER BY priority DESC", (status,)).fetchall()
95
+ else:
96
+ rows = conn.execute("SELECT * FROM open_loops ORDER BY priority DESC").fetchall()
97
+ cols = [d[0] for d in conn.execute("SELECT * FROM open_loops LIMIT 0").description]
98
+ return [dict(zip(cols, row)) for row in rows]
99
+
100
+ def update_loop(self, loop_id: str, **kwargs):
101
+ sets = ", ".join(f"{k}=?" for k in kwargs)
102
+ vals = list(kwargs.values()) + [loop_id]
103
+ with self._connect() as conn:
104
+ conn.execute(f"UPDATE open_loops SET {sets}, updated_at=CURRENT_TIMESTAMP WHERE id=?", vals)
105
+
106
+ # ---- Exchanges ----
107
+ def log_exchange(self, user_msg: str, response: str, contract: str, dial: int,
108
+ archetype_mix: Dict[str, float], score: Optional[float] = None) -> str:
109
+ eid = str(uuid.uuid4())
110
+ with self._connect() as conn:
111
+ conn.execute(
112
+ "INSERT INTO exchanges (id, user_message, jarvis_response, contract, dial, archetype_mix, score) VALUES (?, ?, ?, ?, ?, ?, ?)",
113
+ (eid, user_msg, response, contract, dial, json.dumps(archetype_mix), score),
114
+ )
115
+ return eid
116
+
117
+ def get_recent_exchanges(self, n: int = 5) -> List[Dict]:
118
+ with self._connect() as conn:
119
+ rows = conn.execute(
120
+ "SELECT * FROM exchanges ORDER BY timestamp DESC LIMIT ?", (n,)
121
+ ).fetchall()
122
+ if not rows:
123
+ return []
124
+ cols = [d[0] for d in conn.execute("SELECT * FROM exchanges LIMIT 0").description]
125
+ return [dict(zip(cols, row)) for row in reversed(rows)]
126
+
127
+
128
+ class ChromaStore:
129
+ """Vector memory for semantic recall of conversation history."""
130
+
131
+ def __init__(self, persist_dir: str, embedding_model: str = "all-MiniLM-L6-v2"):
132
+ Path(persist_dir).mkdir(parents=True, exist_ok=True)
133
+ self.client = chromadb.PersistentClient(
134
+ path=persist_dir,
135
+ settings=Settings(anonymized_telemetry=False),
136
+ )
137
+ self.collection = self.client.get_or_create_collection("jarvis_memory")
138
+ self.embedder = SentenceTransformer(embedding_model)
139
+
140
+ def add(self, text: str, metadata: Optional[Dict] = None, doc_id: Optional[str] = None):
141
+ doc_id = doc_id or str(uuid.uuid4())
142
+ embedding = self.embedder.encode(text).tolist()
143
+ self.collection.add(
144
+ ids=[doc_id],
145
+ embeddings=[embedding],
146
+ documents=[text],
147
+ metadatas=[metadata or {}],
148
+ )
149
+
150
+ def query(self, query_text: str, n_results: int = 5) -> List[Dict]:
151
+ embedding = self.embedder.encode(query_text).tolist()
152
+ results = self.collection.query(
153
+ query_embeddings=[embedding],
154
+ n_results=n_results,
155
+ include=["documents", "metadatas", "distances"],
156
+ )
157
+ out = []
158
+ for i in range(len(results["ids"][0])):
159
+ out.append({
160
+ "id": results["ids"][0][i],
161
+ "document": results["documents"][0][i],
162
+ "metadata": results["metadatas"][0][i],
163
+ "distance": results["distances"][0][i],
164
+ })
165
+ return out
166
+
167
+
168
+ class Memory:
169
+ """Unified memory interface."""
170
+
171
+ def __init__(self, config: MemoryConfig):
172
+ self.sqlite = SQLiteStore(config.sqlite_path)
173
+ self.chroma = None
174
+ if config.use_chroma:
175
+ self.chroma = ChromaStore(config.chroma_path, config.embedding_model)
176
+ self.max_context_turns = config.max_context_turns
177
+
178
+ def log_turn(self, user_msg: str, response: str, contract: str, dial: int,
179
+ archetype_mix: Dict[str, float], score: Optional[float] = None):
180
+ eid = self.sqlite.log_exchange(user_msg, response, contract, dial, archetype_mix, score)
181
+ if self.chroma:
182
+ self.chroma.add(
183
+ text=f"User: {user_msg}\nJ.A.R.V.I.S.: {response}",
184
+ metadata={
185
+ "exchange_id": eid,
186
+ "contract": contract,
187
+ "dial": dial,
188
+ "timestamp": datetime.utcnow().isoformat(),
189
+ },
190
+ doc_id=eid,
191
+ )
192
+
193
+ def get_recent_context(self, n: Optional[int] = None) -> str:
194
+ n = n or self.max_context_turns
195
+ exchanges = self.sqlite.get_recent_exchanges(n)
196
+ lines = []
197
+ for ex in exchanges:
198
+ lines.append(f"User: {ex['user_message']}")
199
+ lines.append(f"J.A.R.V.I.S.: {ex['jarvis_response']}")
200
+ return "\n".join(lines)
201
+
202
+ def semantic_recall(self, query: str, n: int = 3) -> List[Dict]:
203
+ if self.chroma is None:
204
+ return []
205
+ return self.chroma.query(query, n_results=n)