ivas 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ivas/__init__.py +27 -0
- ivas/brain.py +406 -0
- ivas/cli.py +179 -0
- ivas/corrections.py +240 -0
- ivas/einherjar.py +281 -0
- ivas/manifest.py +170 -0
- ivas/session.py +275 -0
- ivas-0.1.0.dist-info/METADATA +198 -0
- ivas-0.1.0.dist-info/RECORD +12 -0
- ivas-0.1.0.dist-info/WHEEL +5 -0
- ivas-0.1.0.dist-info/entry_points.txt +2 -0
- ivas-0.1.0.dist-info/top_level.txt +1 -0
ivas/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
IVAS -- Immortal Virtual Agent Sessions.
|
|
3
|
+
|
|
4
|
+
Persistent memory and session continuity for any AI agent.
|
|
5
|
+
Your agent remembers everything. Forever. Across crashes, restarts, and updates.
|
|
6
|
+
|
|
7
|
+
Quick start:
|
|
8
|
+
from ivas import Brain
|
|
9
|
+
|
|
10
|
+
brain = Brain("my-agent")
|
|
11
|
+
brain.remember("user", "preferences", "User prefers dark mode")
|
|
12
|
+
results = brain.recall("dark mode")
|
|
13
|
+
|
|
14
|
+
Patent: 64/019,813 (Provisional) -- Multi-Instance AI Agent Session Relay System
|
|
15
|
+
with Persistent State Recovery
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
__version__ = "0.1.0"
|
|
19
|
+
__author__ = "Arvind Ramamoorthy"
|
|
20
|
+
|
|
21
|
+
from ivas.brain import Brain
|
|
22
|
+
from ivas.session import Session
|
|
23
|
+
from ivas.manifest import RecallManifest
|
|
24
|
+
from ivas.corrections import Corrections
|
|
25
|
+
from ivas.einherjar import Einherjar
|
|
26
|
+
|
|
27
|
+
__all__ = ["Brain", "Session", "RecallManifest", "Corrections", "Einherjar"]
|
ivas/brain.py
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
"""
|
|
2
|
+
IVAS Brain -- Persistent memory with full-text search.
|
|
3
|
+
|
|
4
|
+
SQLite + FTS5 for zero-dependency, cross-platform, crash-proof memory.
|
|
5
|
+
No MongoDB. No Redis. No Docker. One file. Works everywhere.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
brain = Brain("my-agent")
|
|
9
|
+
brain.remember("meeting", "standup", "Decided to ship v2 Friday")
|
|
10
|
+
brain.recall("ship v2") # -> list of memories
|
|
11
|
+
brain.forget(memory_id) # -> remove a memory
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import sqlite3
|
|
18
|
+
import time
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Optional
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class Memory:
|
|
26
|
+
"""A single memory entry."""
|
|
27
|
+
id: int
|
|
28
|
+
category: str
|
|
29
|
+
subject: str
|
|
30
|
+
content: str
|
|
31
|
+
confidence: float
|
|
32
|
+
created_at: float
|
|
33
|
+
source: Optional[str] = None
|
|
34
|
+
metadata: dict = field(default_factory=dict)
|
|
35
|
+
|
|
36
|
+
def age_hours(self) -> float:
|
|
37
|
+
return (time.time() - self.created_at) / 3600
|
|
38
|
+
|
|
39
|
+
def age_days(self) -> float:
|
|
40
|
+
return self.age_hours() / 24
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Brain:
|
|
44
|
+
"""Persistent memory store with full-text search.
|
|
45
|
+
|
|
46
|
+
Each agent gets its own SQLite database. Memories are categorized,
|
|
47
|
+
full-text searchable, and survive crashes, restarts, and updates.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
agent_id: Unique identifier for this agent (used as DB filename).
|
|
51
|
+
data_dir: Directory to store the database. Defaults to ~/.ivas/
|
|
52
|
+
auto_vacuum: Run SQLite VACUUM on open (default False).
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
agent_id: str,
|
|
58
|
+
data_dir: Optional[str | Path] = None,
|
|
59
|
+
auto_vacuum: bool = False,
|
|
60
|
+
):
|
|
61
|
+
self.agent_id = agent_id
|
|
62
|
+
self.data_dir = Path(data_dir) if data_dir else Path.home() / ".ivas"
|
|
63
|
+
self.data_dir.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
self.db_path = self.data_dir / f"{agent_id}.db"
|
|
65
|
+
self._conn = self._connect(auto_vacuum)
|
|
66
|
+
self._setup_tables()
|
|
67
|
+
|
|
68
|
+
def _connect(self, auto_vacuum: bool) -> sqlite3.Connection:
|
|
69
|
+
conn = sqlite3.connect(str(self.db_path), timeout=10)
|
|
70
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
71
|
+
conn.execute("PRAGMA synchronous=NORMAL")
|
|
72
|
+
conn.execute("PRAGMA busy_timeout=5000")
|
|
73
|
+
if auto_vacuum:
|
|
74
|
+
conn.execute("VACUUM")
|
|
75
|
+
return conn
|
|
76
|
+
|
|
77
|
+
def _setup_tables(self):
|
|
78
|
+
self._conn.executescript("""
|
|
79
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
80
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
81
|
+
category TEXT NOT NULL,
|
|
82
|
+
subject TEXT NOT NULL,
|
|
83
|
+
content TEXT NOT NULL,
|
|
84
|
+
confidence REAL DEFAULT 0.8,
|
|
85
|
+
source TEXT,
|
|
86
|
+
metadata TEXT DEFAULT '{}',
|
|
87
|
+
created_at REAL NOT NULL,
|
|
88
|
+
updated_at REAL
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
92
|
+
category, subject, content,
|
|
93
|
+
content='memories',
|
|
94
|
+
content_rowid='id',
|
|
95
|
+
tokenize='porter unicode61'
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
|
|
99
|
+
INSERT INTO memories_fts(rowid, category, subject, content)
|
|
100
|
+
VALUES (new.id, new.category, new.subject, new.content);
|
|
101
|
+
END;
|
|
102
|
+
|
|
103
|
+
CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
|
|
104
|
+
INSERT INTO memories_fts(memories_fts, rowid, category, subject, content)
|
|
105
|
+
VALUES ('delete', old.id, old.category, old.subject, old.content);
|
|
106
|
+
END;
|
|
107
|
+
|
|
108
|
+
CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
|
|
109
|
+
INSERT INTO memories_fts(memories_fts, rowid, category, subject, content)
|
|
110
|
+
VALUES ('delete', old.id, old.category, old.subject, old.content);
|
|
111
|
+
INSERT INTO memories_fts(rowid, category, subject, content)
|
|
112
|
+
VALUES (new.id, new.category, new.subject, new.content);
|
|
113
|
+
END;
|
|
114
|
+
|
|
115
|
+
CREATE INDEX IF NOT EXISTS idx_memories_category ON memories(category);
|
|
116
|
+
CREATE INDEX IF NOT EXISTS idx_memories_created ON memories(created_at);
|
|
117
|
+
""")
|
|
118
|
+
self._conn.commit()
|
|
119
|
+
|
|
120
|
+
def remember(
|
|
121
|
+
self,
|
|
122
|
+
category: str,
|
|
123
|
+
subject: str,
|
|
124
|
+
content: str,
|
|
125
|
+
confidence: float = 0.8,
|
|
126
|
+
source: Optional[str] = None,
|
|
127
|
+
metadata: Optional[dict] = None,
|
|
128
|
+
) -> int:
|
|
129
|
+
"""Store a memory. Returns the memory ID.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
category: High-level category (e.g., "user", "decision", "error").
|
|
133
|
+
subject: Specific topic (e.g., "dark_mode_preference").
|
|
134
|
+
content: The actual memory content.
|
|
135
|
+
confidence: How confident you are (0.0 to 1.0).
|
|
136
|
+
source: Where this memory came from (optional).
|
|
137
|
+
metadata: Additional key-value data (optional).
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
The integer ID of the stored memory.
|
|
141
|
+
"""
|
|
142
|
+
now = time.time()
|
|
143
|
+
cursor = self._conn.execute(
|
|
144
|
+
"""INSERT INTO memories (category, subject, content, confidence, source, metadata, created_at)
|
|
145
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)""",
|
|
146
|
+
(category, subject, content, confidence, source,
|
|
147
|
+
json.dumps(metadata or {}), now),
|
|
148
|
+
)
|
|
149
|
+
self._conn.commit()
|
|
150
|
+
return cursor.lastrowid
|
|
151
|
+
|
|
152
|
+
# Stopwords to skip when building auto OR-join queries
|
|
153
|
+
_STOPWORDS = frozenset({
|
|
154
|
+
"a", "an", "the", "is", "are", "was", "were", "be", "been", "being",
|
|
155
|
+
"have", "has", "had", "do", "does", "did", "will", "would", "shall",
|
|
156
|
+
"should", "may", "might", "can", "could", "to", "of", "in", "for",
|
|
157
|
+
"on", "with", "at", "by", "from", "as", "into", "about", "that",
|
|
158
|
+
"this", "it", "its", "i", "you", "he", "she", "we", "they", "my",
|
|
159
|
+
"your", "his", "her", "our", "their", "what", "which", "who", "whom",
|
|
160
|
+
"when", "where", "how", "why", "not", "no", "and", "or", "but", "if",
|
|
161
|
+
"so", "than", "too", "very", "just", "also", "then", "there", "here",
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
@staticmethod
|
|
165
|
+
def _is_explicit_fts5(query: str) -> bool:
|
|
166
|
+
"""Return True if the query already contains explicit FTS5 syntax."""
|
|
167
|
+
import re
|
|
168
|
+
# Explicit FTS5 markers: OR / AND / NOT (uppercase), quoted phrases
|
|
169
|
+
return bool(re.search(r'\bOR\b|\bAND\b|\bNOT\b|"', query))
|
|
170
|
+
|
|
171
|
+
def _sanitize_query(self, query: str) -> str:
|
|
172
|
+
"""Convert a plain natural-language query into an FTS5 OR-joined query.
|
|
173
|
+
|
|
174
|
+
If the query already uses explicit FTS5 syntax (OR, AND, NOT, quotes)
|
|
175
|
+
it is returned unchanged. Otherwise punctuation is stripped, stopwords
|
|
176
|
+
are removed, and the remaining content words are joined with OR so that
|
|
177
|
+
multi-word queries like "notification preference" find memories that
|
|
178
|
+
mention either word.
|
|
179
|
+
"""
|
|
180
|
+
import re
|
|
181
|
+
if self._is_explicit_fts5(query):
|
|
182
|
+
return query
|
|
183
|
+
# Strip punctuation, keep word characters and spaces
|
|
184
|
+
clean = re.sub(r'[^\w\s]', ' ', query)
|
|
185
|
+
words = [
|
|
186
|
+
w for w in clean.split()
|
|
187
|
+
if len(w) > 2 and w.lower() not in self._STOPWORDS
|
|
188
|
+
]
|
|
189
|
+
if not words:
|
|
190
|
+
# Fallback: use cleaned query as-is to avoid empty MATCH
|
|
191
|
+
return re.sub(r'[^\w\s]', ' ', query).strip() or query
|
|
192
|
+
return " OR ".join(words)
|
|
193
|
+
|
|
194
|
+
def recall(
|
|
195
|
+
self,
|
|
196
|
+
query: str,
|
|
197
|
+
limit: int = 10,
|
|
198
|
+
category: Optional[str] = None,
|
|
199
|
+
min_confidence: float = 0.0,
|
|
200
|
+
) -> list[Memory]:
|
|
201
|
+
"""Search memories using full-text search.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
query: Search query (supports FTS5 syntax: AND, OR, NOT, quotes).
|
|
205
|
+
Plain natural-language queries are automatically OR-joined
|
|
206
|
+
so that partial matches are returned.
|
|
207
|
+
limit: Maximum results to return.
|
|
208
|
+
category: Filter to this category only (optional).
|
|
209
|
+
min_confidence: Minimum confidence threshold.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
List of Memory objects, ranked by relevance then recency.
|
|
213
|
+
"""
|
|
214
|
+
# Sanitize plain queries into FTS5 OR-join; pass explicit syntax through
|
|
215
|
+
fts_query = self._sanitize_query(query)
|
|
216
|
+
|
|
217
|
+
# Build FTS query with optional category filter
|
|
218
|
+
params: list = []
|
|
219
|
+
sql = """
|
|
220
|
+
SELECT m.id, m.category, m.subject, m.content, m.confidence,
|
|
221
|
+
m.created_at, m.source, m.metadata,
|
|
222
|
+
rank
|
|
223
|
+
FROM memories_fts fts
|
|
224
|
+
JOIN memories m ON m.id = fts.rowid
|
|
225
|
+
WHERE memories_fts MATCH ?
|
|
226
|
+
"""
|
|
227
|
+
params.append(fts_query)
|
|
228
|
+
|
|
229
|
+
if category:
|
|
230
|
+
sql += " AND m.category = ?"
|
|
231
|
+
params.append(category)
|
|
232
|
+
|
|
233
|
+
if min_confidence > 0:
|
|
234
|
+
sql += " AND m.confidence >= ?"
|
|
235
|
+
params.append(min_confidence)
|
|
236
|
+
|
|
237
|
+
# Primary sort: FTS5 rank (more negative = more relevant).
|
|
238
|
+
# Tiebreaker: newest memory first, so corrections/updates surface above
|
|
239
|
+
# older entries when relevance scores are equal.
|
|
240
|
+
sql += " ORDER BY rank, m.created_at DESC LIMIT ?"
|
|
241
|
+
params.append(limit)
|
|
242
|
+
|
|
243
|
+
rows = self._conn.execute(sql, params).fetchall()
|
|
244
|
+
return [self._row_to_memory(r) for r in rows]
|
|
245
|
+
|
|
246
|
+
def get(self, memory_id: int) -> Optional[Memory]:
|
|
247
|
+
"""Get a specific memory by ID."""
|
|
248
|
+
row = self._conn.execute(
|
|
249
|
+
"""SELECT id, category, subject, content, confidence,
|
|
250
|
+
created_at, source, metadata, 0 as rank
|
|
251
|
+
FROM memories WHERE id = ?""",
|
|
252
|
+
(memory_id,),
|
|
253
|
+
).fetchone()
|
|
254
|
+
return self._row_to_memory(row) if row else None
|
|
255
|
+
|
|
256
|
+
def update(
|
|
257
|
+
self,
|
|
258
|
+
memory_id: int,
|
|
259
|
+
content: Optional[str] = None,
|
|
260
|
+
confidence: Optional[float] = None,
|
|
261
|
+
metadata: Optional[dict] = None,
|
|
262
|
+
) -> bool:
|
|
263
|
+
"""Update an existing memory. Returns True if found and updated."""
|
|
264
|
+
existing = self.get(memory_id)
|
|
265
|
+
if not existing:
|
|
266
|
+
return False
|
|
267
|
+
|
|
268
|
+
updates = []
|
|
269
|
+
params = []
|
|
270
|
+
if content is not None:
|
|
271
|
+
updates.append("content = ?")
|
|
272
|
+
params.append(content)
|
|
273
|
+
if confidence is not None:
|
|
274
|
+
updates.append("confidence = ?")
|
|
275
|
+
params.append(confidence)
|
|
276
|
+
if metadata is not None:
|
|
277
|
+
updates.append("metadata = ?")
|
|
278
|
+
params.append(json.dumps(metadata))
|
|
279
|
+
|
|
280
|
+
if not updates:
|
|
281
|
+
return True
|
|
282
|
+
|
|
283
|
+
updates.append("updated_at = ?")
|
|
284
|
+
params.append(time.time())
|
|
285
|
+
params.append(memory_id)
|
|
286
|
+
|
|
287
|
+
self._conn.execute(
|
|
288
|
+
f"UPDATE memories SET {', '.join(updates)} WHERE id = ?",
|
|
289
|
+
params,
|
|
290
|
+
)
|
|
291
|
+
self._conn.commit()
|
|
292
|
+
return True
|
|
293
|
+
|
|
294
|
+
def forget(self, memory_id: int) -> bool:
|
|
295
|
+
"""Delete a memory. Returns True if it existed."""
|
|
296
|
+
cursor = self._conn.execute(
|
|
297
|
+
"DELETE FROM memories WHERE id = ?", (memory_id,)
|
|
298
|
+
)
|
|
299
|
+
self._conn.commit()
|
|
300
|
+
return cursor.rowcount > 0
|
|
301
|
+
|
|
302
|
+
def recent(self, limit: int = 20, category: Optional[str] = None) -> list[Memory]:
|
|
303
|
+
"""Get the most recent memories."""
|
|
304
|
+
if category:
|
|
305
|
+
rows = self._conn.execute(
|
|
306
|
+
"""SELECT id, category, subject, content, confidence,
|
|
307
|
+
created_at, source, metadata, 0 as rank
|
|
308
|
+
FROM memories WHERE category = ?
|
|
309
|
+
ORDER BY created_at DESC LIMIT ?""",
|
|
310
|
+
(category, limit),
|
|
311
|
+
).fetchall()
|
|
312
|
+
else:
|
|
313
|
+
rows = self._conn.execute(
|
|
314
|
+
"""SELECT id, category, subject, content, confidence,
|
|
315
|
+
created_at, source, metadata, 0 as rank
|
|
316
|
+
FROM memories ORDER BY created_at DESC LIMIT ?""",
|
|
317
|
+
(limit,),
|
|
318
|
+
).fetchall()
|
|
319
|
+
return [self._row_to_memory(r) for r in rows]
|
|
320
|
+
|
|
321
|
+
def stats(self) -> dict:
|
|
322
|
+
"""Get memory statistics."""
|
|
323
|
+
total = self._conn.execute("SELECT COUNT(*) FROM memories").fetchone()[0]
|
|
324
|
+
categories = self._conn.execute(
|
|
325
|
+
"SELECT category, COUNT(*) FROM memories GROUP BY category ORDER BY COUNT(*) DESC"
|
|
326
|
+
).fetchall()
|
|
327
|
+
oldest = self._conn.execute(
|
|
328
|
+
"SELECT MIN(created_at) FROM memories"
|
|
329
|
+
).fetchone()[0]
|
|
330
|
+
newest = self._conn.execute(
|
|
331
|
+
"SELECT MAX(created_at) FROM memories"
|
|
332
|
+
).fetchone()[0]
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
"agent_id": self.agent_id,
|
|
336
|
+
"total_memories": total,
|
|
337
|
+
"categories": {c: n for c, n in categories},
|
|
338
|
+
"oldest_memory": oldest,
|
|
339
|
+
"newest_memory": newest,
|
|
340
|
+
"db_path": str(self.db_path),
|
|
341
|
+
"db_size_mb": round(self.db_path.stat().st_size / 1024 / 1024, 2),
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
def export(self, path: Optional[str | Path] = None) -> str:
|
|
345
|
+
"""Export all memories to JSON. Returns the JSON string."""
|
|
346
|
+
rows = self._conn.execute(
|
|
347
|
+
"""SELECT id, category, subject, content, confidence,
|
|
348
|
+
created_at, source, metadata
|
|
349
|
+
FROM memories ORDER BY created_at"""
|
|
350
|
+
).fetchall()
|
|
351
|
+
memories = []
|
|
352
|
+
for r in rows:
|
|
353
|
+
memories.append({
|
|
354
|
+
"id": r[0], "category": r[1], "subject": r[2],
|
|
355
|
+
"content": r[3], "confidence": r[4], "created_at": r[5],
|
|
356
|
+
"source": r[6], "metadata": json.loads(r[7] or "{}"),
|
|
357
|
+
})
|
|
358
|
+
data = json.dumps({"agent_id": self.agent_id, "memories": memories}, indent=2)
|
|
359
|
+
if path:
|
|
360
|
+
Path(path).write_text(data)
|
|
361
|
+
return data
|
|
362
|
+
|
|
363
|
+
def import_from(self, path: str | Path) -> int:
|
|
364
|
+
"""Import memories from a JSON export. Returns count imported."""
|
|
365
|
+
data = json.loads(Path(path).read_text())
|
|
366
|
+
count = 0
|
|
367
|
+
for m in data.get("memories", []):
|
|
368
|
+
self.remember(
|
|
369
|
+
category=m["category"],
|
|
370
|
+
subject=m["subject"],
|
|
371
|
+
content=m["content"],
|
|
372
|
+
confidence=m.get("confidence", 0.8),
|
|
373
|
+
source=m.get("source"),
|
|
374
|
+
metadata=m.get("metadata"),
|
|
375
|
+
)
|
|
376
|
+
count += 1
|
|
377
|
+
return count
|
|
378
|
+
|
|
379
|
+
def close(self):
|
|
380
|
+
"""Close the database connection."""
|
|
381
|
+
self._conn.close()
|
|
382
|
+
|
|
383
|
+
def __enter__(self):
|
|
384
|
+
return self
|
|
385
|
+
|
|
386
|
+
def __exit__(self, *args):
|
|
387
|
+
self.close()
|
|
388
|
+
|
|
389
|
+
def __len__(self) -> int:
|
|
390
|
+
return self._conn.execute("SELECT COUNT(*) FROM memories").fetchone()[0]
|
|
391
|
+
|
|
392
|
+
def __repr__(self) -> str:
|
|
393
|
+
return f"Brain(agent_id={self.agent_id!r}, memories={len(self)}, db={self.db_path})"
|
|
394
|
+
|
|
395
|
+
@staticmethod
|
|
396
|
+
def _row_to_memory(row) -> Memory:
|
|
397
|
+
return Memory(
|
|
398
|
+
id=row[0],
|
|
399
|
+
category=row[1],
|
|
400
|
+
subject=row[2],
|
|
401
|
+
content=row[3],
|
|
402
|
+
confidence=row[4],
|
|
403
|
+
created_at=row[5],
|
|
404
|
+
source=row[6],
|
|
405
|
+
metadata=json.loads(row[7] or "{}"),
|
|
406
|
+
)
|
ivas/cli.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""
|
|
2
|
+
IVAS CLI -- Command-line interface for IVAS.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
ivas remember <category> <subject> <content>
|
|
6
|
+
ivas recall <query>
|
|
7
|
+
ivas stats
|
|
8
|
+
ivas export [--output <path>]
|
|
9
|
+
ivas import <path>
|
|
10
|
+
ivas session restore
|
|
11
|
+
ivas session save <task> [<thought>]
|
|
12
|
+
ivas version
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import json
|
|
19
|
+
import sys
|
|
20
|
+
import time
|
|
21
|
+
|
|
22
|
+
from ivas.brain import Brain
|
|
23
|
+
from ivas.session import Session
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _get_brain(args) -> Brain:
|
|
27
|
+
agent_id = getattr(args, "agent", None) or "default"
|
|
28
|
+
data_dir = getattr(args, "data_dir", None)
|
|
29
|
+
return Brain(agent_id, data_dir=data_dir)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def cmd_remember(args):
|
|
33
|
+
brain = _get_brain(args)
|
|
34
|
+
mem_id = brain.remember(args.category, args.subject, args.content)
|
|
35
|
+
print(f"Remembered #{mem_id}: [{args.category}] {args.subject}")
|
|
36
|
+
brain.close()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def cmd_recall(args):
|
|
40
|
+
brain = _get_brain(args)
|
|
41
|
+
results = brain.recall(args.query, limit=args.limit)
|
|
42
|
+
if not results:
|
|
43
|
+
print("No memories found.")
|
|
44
|
+
else:
|
|
45
|
+
for m in results:
|
|
46
|
+
age = m.age_hours()
|
|
47
|
+
age_str = f"{age:.1f}h" if age < 24 else f"{age/24:.1f}d"
|
|
48
|
+
print(f" [{m.id}] {m.category}/{m.subject} ({age_str} ago)")
|
|
49
|
+
print(f" {m.content[:120]}")
|
|
50
|
+
print()
|
|
51
|
+
brain.close()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def cmd_stats(args):
|
|
55
|
+
brain = _get_brain(args)
|
|
56
|
+
stats = brain.stats()
|
|
57
|
+
print(f"Agent: {stats['agent_id']}")
|
|
58
|
+
print(f"Total memories: {stats['total_memories']}")
|
|
59
|
+
print(f"Database: {stats['db_path']} ({stats['db_size_mb']} MB)")
|
|
60
|
+
if stats['categories']:
|
|
61
|
+
print("Categories:")
|
|
62
|
+
for cat, count in stats['categories'].items():
|
|
63
|
+
print(f" {cat}: {count}")
|
|
64
|
+
brain.close()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def cmd_export(args):
|
|
68
|
+
brain = _get_brain(args)
|
|
69
|
+
output = args.output or f"{brain.agent_id}_export.json"
|
|
70
|
+
data = brain.export(output)
|
|
71
|
+
count = len(json.loads(data).get("memories", []))
|
|
72
|
+
print(f"Exported {count} memories to {output}")
|
|
73
|
+
brain.close()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def cmd_import(args):
|
|
77
|
+
brain = _get_brain(args)
|
|
78
|
+
count = brain.import_from(args.path)
|
|
79
|
+
print(f"Imported {count} memories from {args.path}")
|
|
80
|
+
brain.close()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def cmd_session_save(args):
|
|
84
|
+
brain = _get_brain(args)
|
|
85
|
+
session = Session(brain.agent_id, brain=brain)
|
|
86
|
+
state = session.save_state(
|
|
87
|
+
active_task=args.task,
|
|
88
|
+
current_thought=args.thought,
|
|
89
|
+
)
|
|
90
|
+
print(f"State saved: {state.active_task}")
|
|
91
|
+
brain.close()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def cmd_session_restore(args):
|
|
95
|
+
brain = _get_brain(args)
|
|
96
|
+
session = Session(brain.agent_id, brain=brain)
|
|
97
|
+
state = session.restore()
|
|
98
|
+
if state:
|
|
99
|
+
print(f"Restored from: {state.restored_from}")
|
|
100
|
+
print(f"Active task: {state.active_task}")
|
|
101
|
+
print(f"Current thought: {state.current_thought}")
|
|
102
|
+
print(f"Decisions: {json.dumps(state.decisions, indent=2)}")
|
|
103
|
+
print(f"Pending: {state.pending_actions}")
|
|
104
|
+
age = state.age_seconds()
|
|
105
|
+
print(f"Age: {age:.0f}s ({age/3600:.1f}h)")
|
|
106
|
+
else:
|
|
107
|
+
print("No prior state found.")
|
|
108
|
+
brain.close()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def cmd_version(_args):
|
|
112
|
+
from ivas import __version__
|
|
113
|
+
print(f"ivas {__version__}")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def main(argv: list[str] | None = None):
|
|
117
|
+
parser = argparse.ArgumentParser(
|
|
118
|
+
prog="ivas",
|
|
119
|
+
description="IVAS -- Immortal Virtual Agent Sessions",
|
|
120
|
+
)
|
|
121
|
+
parser.add_argument("--agent", "-a", default="default", help="Agent ID")
|
|
122
|
+
parser.add_argument("--data-dir", "-d", help="Data directory (default: ~/.ivas/)")
|
|
123
|
+
|
|
124
|
+
sub = parser.add_subparsers(dest="command")
|
|
125
|
+
|
|
126
|
+
# remember
|
|
127
|
+
p_rem = sub.add_parser("remember", help="Store a memory")
|
|
128
|
+
p_rem.add_argument("category")
|
|
129
|
+
p_rem.add_argument("subject")
|
|
130
|
+
p_rem.add_argument("content")
|
|
131
|
+
|
|
132
|
+
# recall
|
|
133
|
+
p_rec = sub.add_parser("recall", help="Search memories")
|
|
134
|
+
p_rec.add_argument("query")
|
|
135
|
+
p_rec.add_argument("--limit", "-n", type=int, default=10)
|
|
136
|
+
|
|
137
|
+
# stats
|
|
138
|
+
sub.add_parser("stats", help="Show memory statistics")
|
|
139
|
+
|
|
140
|
+
# export
|
|
141
|
+
p_exp = sub.add_parser("export", help="Export memories to JSON")
|
|
142
|
+
p_exp.add_argument("--output", "-o")
|
|
143
|
+
|
|
144
|
+
# import
|
|
145
|
+
p_imp = sub.add_parser("import", help="Import memories from JSON")
|
|
146
|
+
p_imp.add_argument("path")
|
|
147
|
+
|
|
148
|
+
# session save
|
|
149
|
+
p_ss = sub.add_parser("save", help="Save session state")
|
|
150
|
+
p_ss.add_argument("task", help="Current task description")
|
|
151
|
+
p_ss.add_argument("thought", nargs="?", help="Current thought")
|
|
152
|
+
|
|
153
|
+
# session restore
|
|
154
|
+
sub.add_parser("restore", help="Restore last session state")
|
|
155
|
+
|
|
156
|
+
# version
|
|
157
|
+
sub.add_parser("version", help="Show version")
|
|
158
|
+
|
|
159
|
+
args = parser.parse_args(argv)
|
|
160
|
+
|
|
161
|
+
commands = {
|
|
162
|
+
"remember": cmd_remember,
|
|
163
|
+
"recall": cmd_recall,
|
|
164
|
+
"stats": cmd_stats,
|
|
165
|
+
"export": cmd_export,
|
|
166
|
+
"import": cmd_import,
|
|
167
|
+
"save": cmd_session_save,
|
|
168
|
+
"restore": cmd_session_restore,
|
|
169
|
+
"version": cmd_version,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if args.command in commands:
|
|
173
|
+
commands[args.command](args)
|
|
174
|
+
else:
|
|
175
|
+
parser.print_help()
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
if __name__ == "__main__":
|
|
179
|
+
main()
|