cli-memory-os 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.
- cli/__init__.py +1 -0
- cli/commands/__init__.py +1 -0
- cli/commands/benchmark.py +151 -0
- cli/commands/config_cmd.py +57 -0
- cli/commands/doctor.py +61 -0
- cli/commands/export.py +106 -0
- cli/commands/import_cmd.py +143 -0
- cli/commands/init.py +205 -0
- cli/commands/logs.py +38 -0
- cli/commands/monitor.py +63 -0
- cli/commands/plugins.py +37 -0
- cli/commands/start.py +24 -0
- cli/commands/status.py +92 -0
- cli/commands/stop.py +19 -0
- cli/commands/version.py +25 -0
- cli/commands/workspace.py +154 -0
- cli/main.py +496 -0
- cli/parser.py +188 -0
- cli_memory_os-0.1.0.dist-info/METADATA +231 -0
- cli_memory_os-0.1.0.dist-info/RECORD +50 -0
- cli_memory_os-0.1.0.dist-info/WHEEL +5 -0
- cli_memory_os-0.1.0.dist-info/entry_points.txt +2 -0
- cli_memory_os-0.1.0.dist-info/licenses/LICENSE +21 -0
- cli_memory_os-0.1.0.dist-info/top_level.txt +6 -0
- connectors/__init__.py +1 -0
- connectors/base.py +39 -0
- connectors/github.py +273 -0
- connectors/gmail.py +116 -0
- connectors/notion.py +156 -0
- connectors/registry.py +60 -0
- core/__init__.py +1 -0
- core/chunker.py +136 -0
- core/context_builder.py +407 -0
- core/embedder.py +42 -0
- core/llm.py +301 -0
- core/vector_store.py +629 -0
- infrastructure/__init__.py +1 -0
- infrastructure/compose.py +136 -0
- infrastructure/config.py +280 -0
- infrastructure/docker.py +61 -0
- infrastructure/health.py +338 -0
- infrastructure/observability.py +160 -0
- infrastructure/workspace.py +152 -0
- models/__init__.py +1 -0
- models/memory.py +28 -0
- storage/__init__.py +1 -0
- storage/db.py +528 -0
- storage/graph.py +324 -0
- storage/schema.sql +57 -0
- storage/tech_detector.py +117 -0
infrastructure/health.py
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Infrastructure: Service health checks.
|
|
3
|
+
|
|
4
|
+
Individual check functions for Neo4j, Qdrant, SQLite, Groq, Composio,
|
|
5
|
+
embedding model, and connectors. Used by the ``doctor`` command.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("infrastructure.health")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def check_python() -> tuple[bool, str]:
|
|
16
|
+
"""Check Python version."""
|
|
17
|
+
version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
|
18
|
+
ok = sys.version_info >= (3, 12)
|
|
19
|
+
return ok, version
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def check_neo4j() -> tuple[bool, str]:
|
|
23
|
+
"""Attempt a Neo4j bolt connection."""
|
|
24
|
+
try:
|
|
25
|
+
from neo4j import GraphDatabase
|
|
26
|
+
uri = os.getenv("NEO4J_URI", "bolt://localhost:7687")
|
|
27
|
+
user = os.getenv("NEO4J_USER", "neo4j")
|
|
28
|
+
password = os.getenv("NEO4J_PASSWORD", "password")
|
|
29
|
+
driver = GraphDatabase.driver(uri, auth=(user, password))
|
|
30
|
+
driver.verify_connectivity()
|
|
31
|
+
driver.close()
|
|
32
|
+
return True, "Healthy"
|
|
33
|
+
except Exception as e:
|
|
34
|
+
return False, str(e)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def check_qdrant() -> tuple[bool, str]:
|
|
38
|
+
"""Ping the Qdrant HTTP health endpoint."""
|
|
39
|
+
try:
|
|
40
|
+
import urllib.request
|
|
41
|
+
port = os.getenv("QDRANT_PORT", "6333")
|
|
42
|
+
url = f"http://localhost:{port}/healthz"
|
|
43
|
+
req = urllib.request.urlopen(url, timeout=5)
|
|
44
|
+
if req.status == 200:
|
|
45
|
+
return True, "Healthy"
|
|
46
|
+
return False, f"HTTP {req.status}"
|
|
47
|
+
except Exception as e:
|
|
48
|
+
return False, str(e)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def check_sqlite(db_path: str | None = None) -> tuple[bool, str]:
|
|
52
|
+
"""Verify a SQLite database file is valid."""
|
|
53
|
+
import sqlite3
|
|
54
|
+
if db_path is None:
|
|
55
|
+
db_path = os.getenv("MEMORY_OS_DB_PATH", "memory.db")
|
|
56
|
+
if not os.path.exists(db_path):
|
|
57
|
+
return False, f"File not found: {db_path}"
|
|
58
|
+
try:
|
|
59
|
+
conn = sqlite3.connect(db_path)
|
|
60
|
+
cursor = conn.cursor()
|
|
61
|
+
cursor.execute("PRAGMA integrity_check")
|
|
62
|
+
result = cursor.fetchone()
|
|
63
|
+
conn.close()
|
|
64
|
+
if result and result[0] == "ok":
|
|
65
|
+
return True, "Healthy"
|
|
66
|
+
return False, f"Integrity check: {result}"
|
|
67
|
+
except Exception as e:
|
|
68
|
+
return False, str(e)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def check_groq_api() -> tuple[bool, str]:
|
|
72
|
+
"""Make a minimal Groq API test call."""
|
|
73
|
+
try:
|
|
74
|
+
from groq import Groq
|
|
75
|
+
key = os.getenv("GROQ_API_KEY")
|
|
76
|
+
if not key:
|
|
77
|
+
return False, "No API key"
|
|
78
|
+
client = Groq(api_key=key)
|
|
79
|
+
# List models as a lightweight connectivity test
|
|
80
|
+
client.models.list()
|
|
81
|
+
return True, "Connected"
|
|
82
|
+
except Exception as e:
|
|
83
|
+
return False, str(e)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def check_composio() -> tuple[bool, str]:
|
|
87
|
+
"""Verify Composio toolkit status."""
|
|
88
|
+
try:
|
|
89
|
+
from composio import Composio
|
|
90
|
+
c = Composio()
|
|
91
|
+
s = c.create(user_id=os.getenv("COMPOSIO_USER_ID", "user_123"))
|
|
92
|
+
toolkits = s.toolkits()
|
|
93
|
+
count = len(toolkits.items) if toolkits and toolkits.items else 0
|
|
94
|
+
return True, f"Connected ({count} toolkits)"
|
|
95
|
+
except Exception as e:
|
|
96
|
+
return False, str(e)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def check_embedding_model() -> tuple[bool, str]:
|
|
100
|
+
"""Check if the sentence-transformers model is cached locally."""
|
|
101
|
+
try:
|
|
102
|
+
from sentence_transformers import SentenceTransformer
|
|
103
|
+
model_name = os.getenv("EMBEDDING_MODEL", "all-MiniLM-L6-v2")
|
|
104
|
+
# Try loading from cache only
|
|
105
|
+
SentenceTransformer(model_name, local_files_only=True)
|
|
106
|
+
return True, model_name
|
|
107
|
+
except Exception:
|
|
108
|
+
model_name = os.getenv("EMBEDDING_MODEL", "all-MiniLM-L6-v2")
|
|
109
|
+
return False, f"{model_name} (not cached)"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def check_connector(name: str) -> tuple[bool, str]:
|
|
113
|
+
"""Check if a Composio connector (github, gmail, notion) is active."""
|
|
114
|
+
try:
|
|
115
|
+
from composio import Composio
|
|
116
|
+
c = Composio()
|
|
117
|
+
s = c.create(user_id=os.getenv("COMPOSIO_USER_ID", "user_123"))
|
|
118
|
+
toolkits = s.toolkits()
|
|
119
|
+
tk = next((t for t in toolkits.items if t.slug == name), None)
|
|
120
|
+
if tk and tk.connection and tk.connection.is_active:
|
|
121
|
+
return True, "Connected"
|
|
122
|
+
return False, "Not connected"
|
|
123
|
+
except Exception as e:
|
|
124
|
+
return False, str(e)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def check_workspace() -> tuple[bool, str]:
|
|
128
|
+
"""Check if the workspace directory exists and is writable."""
|
|
129
|
+
from infrastructure.workspace import get_workspace_root
|
|
130
|
+
root = get_workspace_root()
|
|
131
|
+
if not root.exists():
|
|
132
|
+
return False, "Not initialized"
|
|
133
|
+
# Check writable
|
|
134
|
+
test_file = root / ".write_test"
|
|
135
|
+
try:
|
|
136
|
+
test_file.write_text("test", encoding="utf-8")
|
|
137
|
+
test_file.unlink()
|
|
138
|
+
return True, str(root)
|
|
139
|
+
except Exception as e:
|
|
140
|
+
return False, f"Not writable: {e}"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def check_disk_usage() -> tuple[bool, str]:
|
|
144
|
+
"""Calculate workspace disk usage."""
|
|
145
|
+
from infrastructure.workspace import get_workspace_root, get_workspace_size
|
|
146
|
+
root = get_workspace_root()
|
|
147
|
+
if not root.exists():
|
|
148
|
+
return False, "Workspace not found"
|
|
149
|
+
size_bytes = get_workspace_size()
|
|
150
|
+
if size_bytes < 1024 * 1024:
|
|
151
|
+
size_str = f"{size_bytes / 1024:.0f} KB"
|
|
152
|
+
elif size_bytes < 1024 * 1024 * 1024:
|
|
153
|
+
size_str = f"{size_bytes / (1024 * 1024):.0f} MB"
|
|
154
|
+
else:
|
|
155
|
+
size_str = f"{size_bytes / (1024 * 1024 * 1024):.1f} GB"
|
|
156
|
+
return True, size_str
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def check_memory_usage() -> tuple[bool, str]:
|
|
160
|
+
"""Report current process memory usage."""
|
|
161
|
+
try:
|
|
162
|
+
import psutil
|
|
163
|
+
process = psutil.Process(os.getpid())
|
|
164
|
+
mem = process.memory_info().rss
|
|
165
|
+
if mem < 1024 * 1024:
|
|
166
|
+
return True, f"{mem / 1024:.0f} KB"
|
|
167
|
+
return True, f"{mem / (1024 * 1024):.0f} MB"
|
|
168
|
+
except ImportError:
|
|
169
|
+
# psutil not available, use a rough estimate
|
|
170
|
+
return True, "N/A (psutil not installed)"
|
|
171
|
+
except Exception as e:
|
|
172
|
+
return False, str(e)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def check_config_validation() -> tuple[bool, str]:
|
|
176
|
+
"""Verify configuration validation."""
|
|
177
|
+
from infrastructure.config import get_config, DEFAULTS
|
|
178
|
+
try:
|
|
179
|
+
config = get_config()
|
|
180
|
+
for key in DEFAULTS:
|
|
181
|
+
if key not in config:
|
|
182
|
+
return False, f"Missing section '{key}'"
|
|
183
|
+
if isinstance(DEFAULTS[key], dict):
|
|
184
|
+
if not isinstance(config[key], dict):
|
|
185
|
+
return False, f"Section '{key}' must be a dictionary"
|
|
186
|
+
for subkey in DEFAULTS[key]:
|
|
187
|
+
if subkey not in config[key]:
|
|
188
|
+
return False, f"Missing subkey '{key}.{subkey}'"
|
|
189
|
+
return True, "Valid"
|
|
190
|
+
except Exception as e:
|
|
191
|
+
return False, str(e)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def check_active_plugins() -> tuple[bool, str]:
|
|
195
|
+
"""Verify connector plugins registration and connection status."""
|
|
196
|
+
try:
|
|
197
|
+
from connectors.registry import discover_connectors
|
|
198
|
+
active = []
|
|
199
|
+
for conn in discover_connectors():
|
|
200
|
+
if conn.authenticate():
|
|
201
|
+
active.append(conn.name)
|
|
202
|
+
if active:
|
|
203
|
+
return True, f"{len(active)} active ({', '.join(active)})"
|
|
204
|
+
return False, "0 active"
|
|
205
|
+
except Exception as e:
|
|
206
|
+
return False, str(e)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def run_all_checks() -> list[tuple[str, bool, str]]:
|
|
210
|
+
"""Run all health checks and return a list of (name, passed, detail)."""
|
|
211
|
+
from infrastructure.docker import check_docker_installed, check_docker_compose_installed
|
|
212
|
+
|
|
213
|
+
checks = []
|
|
214
|
+
|
|
215
|
+
# Python
|
|
216
|
+
ok, detail = check_python()
|
|
217
|
+
checks.append(("Python", ok, detail))
|
|
218
|
+
|
|
219
|
+
# Docker
|
|
220
|
+
ok, detail = check_docker_installed()
|
|
221
|
+
checks.append(("Docker", ok, detail))
|
|
222
|
+
|
|
223
|
+
# Docker Compose
|
|
224
|
+
ok, detail = check_docker_compose_installed()
|
|
225
|
+
checks.append(("Docker Compose", ok, detail))
|
|
226
|
+
|
|
227
|
+
# SQLite
|
|
228
|
+
ok, detail = check_sqlite()
|
|
229
|
+
checks.append(("SQLite", ok, detail))
|
|
230
|
+
|
|
231
|
+
# Neo4j
|
|
232
|
+
ok, detail = check_neo4j()
|
|
233
|
+
checks.append(("Neo4j", ok, detail))
|
|
234
|
+
|
|
235
|
+
# Qdrant
|
|
236
|
+
ok, detail = check_qdrant()
|
|
237
|
+
checks.append(("Qdrant", ok, detail))
|
|
238
|
+
|
|
239
|
+
# Embedding Model
|
|
240
|
+
ok, detail = check_embedding_model()
|
|
241
|
+
checks.append(("Embedding Model", ok, detail))
|
|
242
|
+
|
|
243
|
+
# Groq
|
|
244
|
+
ok, detail = check_groq_api()
|
|
245
|
+
checks.append(("Groq", ok, detail))
|
|
246
|
+
|
|
247
|
+
# Composio
|
|
248
|
+
ok, detail = check_composio()
|
|
249
|
+
checks.append(("Composio", ok, detail))
|
|
250
|
+
|
|
251
|
+
# Config Validation
|
|
252
|
+
ok, detail = check_config_validation()
|
|
253
|
+
checks.append(("Config Validation", ok, detail))
|
|
254
|
+
|
|
255
|
+
# Active Plugins
|
|
256
|
+
ok, detail = check_active_plugins()
|
|
257
|
+
checks.append(("Active Plugins", ok, detail))
|
|
258
|
+
|
|
259
|
+
# Connectors
|
|
260
|
+
for name in ["github", "gmail", "notion"]:
|
|
261
|
+
ok, detail = check_connector(name)
|
|
262
|
+
checks.append((name.capitalize(), ok, detail))
|
|
263
|
+
|
|
264
|
+
# Workspace
|
|
265
|
+
ok, detail = check_workspace()
|
|
266
|
+
checks.append(("Workspace", ok, detail))
|
|
267
|
+
|
|
268
|
+
# Disk Usage
|
|
269
|
+
ok, detail = check_disk_usage()
|
|
270
|
+
checks.append(("Disk Usage", ok, detail))
|
|
271
|
+
|
|
272
|
+
# Memory Usage
|
|
273
|
+
ok, detail = check_memory_usage()
|
|
274
|
+
checks.append(("Memory Usage", ok, detail))
|
|
275
|
+
|
|
276
|
+
# Version
|
|
277
|
+
checks.append(("Version", True, _get_version()))
|
|
278
|
+
|
|
279
|
+
# Workspace Profile & Database Counts
|
|
280
|
+
try:
|
|
281
|
+
from infrastructure.workspace import get_active_profile
|
|
282
|
+
checks.append(("Workspace Profile", True, get_active_profile()))
|
|
283
|
+
except Exception:
|
|
284
|
+
pass
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
from core.vector_store import get_vector_index_stats
|
|
288
|
+
vectors = get_vector_index_stats().get("vectors", 0)
|
|
289
|
+
checks.append(("Vector Count", True, f"{vectors:,}"))
|
|
290
|
+
except Exception:
|
|
291
|
+
checks.append(("Vector Count", True, "0"))
|
|
292
|
+
|
|
293
|
+
try:
|
|
294
|
+
from storage.db import get_repo_count, get_email_count
|
|
295
|
+
repos = get_repo_count()
|
|
296
|
+
emails = get_email_count()
|
|
297
|
+
checks.append(("Repositories", True, str(repos)))
|
|
298
|
+
checks.append(("Emails", True, str(emails)))
|
|
299
|
+
except Exception:
|
|
300
|
+
checks.append(("Repositories", True, "0"))
|
|
301
|
+
checks.append(("Emails", True, "0"))
|
|
302
|
+
|
|
303
|
+
try:
|
|
304
|
+
from storage.graph import GraphStore
|
|
305
|
+
nodes = 0
|
|
306
|
+
rels = 0
|
|
307
|
+
graph = GraphStore()
|
|
308
|
+
if graph.is_fallback:
|
|
309
|
+
from storage.db import get_connection
|
|
310
|
+
conn = get_connection()
|
|
311
|
+
cursor = conn.cursor()
|
|
312
|
+
cursor.execute("SELECT COUNT(*) FROM graph_nodes")
|
|
313
|
+
nodes = cursor.fetchone()[0]
|
|
314
|
+
cursor.execute("SELECT COUNT(*) FROM graph_relationships")
|
|
315
|
+
rels = cursor.fetchone()[0]
|
|
316
|
+
conn.close()
|
|
317
|
+
else:
|
|
318
|
+
with graph.driver.session() as session:
|
|
319
|
+
r_nodes = session.run("MATCH (n) RETURN count(n) AS c")
|
|
320
|
+
nodes = r_nodes.single()["c"]
|
|
321
|
+
r_rels = session.run("MATCH ()-[r]->() RETURN count(r) AS c")
|
|
322
|
+
rels = r_rels.single()["c"]
|
|
323
|
+
checks.append(("Graph Nodes", True, str(nodes)))
|
|
324
|
+
checks.append(("Graph Relationships", True, str(rels)))
|
|
325
|
+
except Exception:
|
|
326
|
+
checks.append(("Graph Nodes", True, "0"))
|
|
327
|
+
checks.append(("Graph Relationships", True, "0"))
|
|
328
|
+
|
|
329
|
+
return checks
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _get_version() -> str:
|
|
333
|
+
"""Get the Memory-OS package version."""
|
|
334
|
+
try:
|
|
335
|
+
from importlib.metadata import version
|
|
336
|
+
return version("memory-os")
|
|
337
|
+
except Exception:
|
|
338
|
+
return "0.1.0"
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Infrastructure: Observability and Performance Monitoring.
|
|
3
|
+
|
|
4
|
+
Parses logs from the active log file (or tracks metrics) to measure:
|
|
5
|
+
- Sync execution time
|
|
6
|
+
- Embedding generation latency
|
|
7
|
+
- Qdrant upload time
|
|
8
|
+
- Neo4j insertion time
|
|
9
|
+
- LLM call count and durations
|
|
10
|
+
- RAG query execution time
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from infrastructure.workspace import get_workspace_root
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_log_file_path() -> Path:
|
|
20
|
+
"""Return path to active memory_os.log."""
|
|
21
|
+
return get_workspace_root() / "logs" / "memory_os.log"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def parse_observability_metrics() -> dict:
|
|
25
|
+
"""Scan the memory_os.log file and parse latency/observability metrics."""
|
|
26
|
+
log_path = get_log_file_path()
|
|
27
|
+
|
|
28
|
+
# Fallback to local logs directory if not in home workspace yet
|
|
29
|
+
if not log_path.exists():
|
|
30
|
+
log_path = Path("logs/memory_os.log")
|
|
31
|
+
|
|
32
|
+
metrics = {
|
|
33
|
+
"sync_times": [],
|
|
34
|
+
"embedding_times": [], # (duration, num_chunks)
|
|
35
|
+
"qdrant_uploads": [], # (duration, num_vectors)
|
|
36
|
+
"neo4j_inserts": [], # (duration, nodes, relationships)
|
|
37
|
+
"llm_calls": [],
|
|
38
|
+
"retrievals": [],
|
|
39
|
+
"rag_times": [],
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if not log_path.exists():
|
|
43
|
+
return metrics
|
|
44
|
+
|
|
45
|
+
# Regex definitions matching existing logs in core/storage/main
|
|
46
|
+
r_sync = re.compile(r"Sync completed in (\d+\.\d+)s")
|
|
47
|
+
r_rebuild = re.compile(r"Full rebuild completed in (\d+\.\d+)s")
|
|
48
|
+
r_embed = re.compile(r"Embedding generation complete for (\d+) chunks in (\d+\.\d+)s")
|
|
49
|
+
r_upload = re.compile(r"Vector upload complete for (\d+) vectors in (\d+\.\d+)s")
|
|
50
|
+
r_graph = re.compile(r"Graph Sync Complete.*Nodes: (\d+), Relationships: (\d+)\. Duration: (\d+\.\d+)s")
|
|
51
|
+
r_llm = re.compile(r"Groq LLM generation finished in (\d+\.\d+)s")
|
|
52
|
+
r_retrieval = re.compile(r"Hybrid retrieval finished for query:.*Retrieved (\d+) items in (\d+\.\d+)s")
|
|
53
|
+
r_rag = re.compile(r"RAG pipeline complete for question:.*in (\d+\.\d+)s")
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
with open(log_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
57
|
+
for line in f:
|
|
58
|
+
# Sync
|
|
59
|
+
m = r_sync.search(line) or r_rebuild.search(line)
|
|
60
|
+
if m:
|
|
61
|
+
metrics["sync_times"].append(float(m.group(1)))
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
# Embeddings
|
|
65
|
+
m = r_embed.search(line)
|
|
66
|
+
if m:
|
|
67
|
+
metrics["embedding_times"].append((float(m.group(2)), int(m.group(1))))
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
# Qdrant Uploads
|
|
71
|
+
m = r_upload.search(line)
|
|
72
|
+
if m:
|
|
73
|
+
metrics["qdrant_uploads"].append((float(m.group(2)), int(m.group(1))))
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
# Neo4j Graph Sync
|
|
77
|
+
m = r_graph.search(line)
|
|
78
|
+
if m:
|
|
79
|
+
metrics["neo4j_inserts"].append((float(m.group(3)), int(m.group(1)), int(m.group(2))))
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
# LLM Calls
|
|
83
|
+
m = r_llm.search(line)
|
|
84
|
+
if m:
|
|
85
|
+
metrics["llm_calls"].append(float(m.group(1)))
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
# Hybrid Retrieval
|
|
89
|
+
m = r_retrieval.search(line)
|
|
90
|
+
if m:
|
|
91
|
+
metrics["retrievals"].append(float(m.group(2)))
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
# RAG Times
|
|
95
|
+
m = r_rag.search(line)
|
|
96
|
+
if m:
|
|
97
|
+
metrics["rag_times"].append(float(m.group(1)))
|
|
98
|
+
continue
|
|
99
|
+
except Exception:
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
return metrics
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def get_performance_summary() -> dict:
|
|
106
|
+
"""Format metrics into a user-friendly statistics summary."""
|
|
107
|
+
data = parse_observability_metrics()
|
|
108
|
+
|
|
109
|
+
def avg(lst):
|
|
110
|
+
return sum(lst) / len(lst) if lst else 0.0
|
|
111
|
+
|
|
112
|
+
# Sync
|
|
113
|
+
syncs = data["sync_times"]
|
|
114
|
+
last_sync = syncs[-1] if syncs else 0.0
|
|
115
|
+
avg_sync = avg(syncs)
|
|
116
|
+
|
|
117
|
+
# Embeddings
|
|
118
|
+
embeds = data["embedding_times"]
|
|
119
|
+
total_embed_time = sum(t for t, _ in embeds)
|
|
120
|
+
total_chunks = sum(c for _, c in embeds)
|
|
121
|
+
avg_chunk_latency = (total_embed_time / total_chunks) if total_chunks > 0 else 0.0
|
|
122
|
+
|
|
123
|
+
# Qdrant uploads
|
|
124
|
+
uploads = data["qdrant_uploads"]
|
|
125
|
+
total_upload_time = sum(t for t, _ in uploads)
|
|
126
|
+
total_vectors = sum(v for _, v in uploads)
|
|
127
|
+
avg_vector_latency = (total_upload_time / total_vectors) if total_vectors > 0 else 0.0
|
|
128
|
+
|
|
129
|
+
# Neo4j
|
|
130
|
+
inserts = data["neo4j_inserts"]
|
|
131
|
+
avg_neo4j = avg([t for t, _, _ in inserts])
|
|
132
|
+
total_nodes = sum(n for _, n, _ in inserts)
|
|
133
|
+
total_rels = sum(r for _, _, r in inserts)
|
|
134
|
+
|
|
135
|
+
# LLM
|
|
136
|
+
llms = data["llm_calls"]
|
|
137
|
+
total_llm_calls = len(llms)
|
|
138
|
+
avg_llm = avg(llms)
|
|
139
|
+
|
|
140
|
+
# Retrieval & RAG
|
|
141
|
+
retrievals = data["retrievals"]
|
|
142
|
+
avg_retrieval = avg(retrievals)
|
|
143
|
+
|
|
144
|
+
rags = data["rag_times"]
|
|
145
|
+
avg_rag = avg(rags)
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
"sync_count": len(syncs),
|
|
149
|
+
"last_sync_duration": last_sync,
|
|
150
|
+
"avg_sync_duration": avg_sync,
|
|
151
|
+
"avg_chunk_latency": avg_chunk_latency,
|
|
152
|
+
"avg_vector_latency": avg_vector_latency,
|
|
153
|
+
"avg_neo4j_sync": avg_neo4j,
|
|
154
|
+
"total_nodes_synced": total_nodes,
|
|
155
|
+
"total_rels_synced": total_rels,
|
|
156
|
+
"llm_call_count": total_llm_calls,
|
|
157
|
+
"avg_llm_duration": avg_llm,
|
|
158
|
+
"avg_retrieval_duration": avg_retrieval,
|
|
159
|
+
"avg_rag_duration": avg_rag,
|
|
160
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Infrastructure: Workspace directory and profile management.
|
|
3
|
+
|
|
4
|
+
Manages the ~/.memory-os/ directory tree and workspace profiles
|
|
5
|
+
(default, work, personal, etc.).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import shutil
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_workspace_root() -> Path:
|
|
14
|
+
"""Return the root workspace directory (~/.memory-os/)."""
|
|
15
|
+
root = os.getenv("MEMORY_OS_WORKSPACE", "~/.memory-os")
|
|
16
|
+
return Path(os.path.expanduser(root))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def ensure_workspace(profile: str = "default"):
|
|
20
|
+
"""Create the full workspace directory tree for a profile.
|
|
21
|
+
|
|
22
|
+
Creates:
|
|
23
|
+
~/.memory-os/
|
|
24
|
+
├── config.toml (not created here, handled by config.py)
|
|
25
|
+
├── active_workspace
|
|
26
|
+
├── workspaces/
|
|
27
|
+
│ └── <profile>/
|
|
28
|
+
│ ├── workspace.db (created by SQLite on first use)
|
|
29
|
+
│ ├── qdrant/
|
|
30
|
+
│ ├── neo4j/
|
|
31
|
+
│ └── embeddings/
|
|
32
|
+
├── logs/
|
|
33
|
+
├── cache/
|
|
34
|
+
└── backups/
|
|
35
|
+
"""
|
|
36
|
+
root = get_workspace_root()
|
|
37
|
+
|
|
38
|
+
# Top-level shared directories
|
|
39
|
+
(root / "logs").mkdir(parents=True, exist_ok=True)
|
|
40
|
+
(root / "cache").mkdir(parents=True, exist_ok=True)
|
|
41
|
+
(root / "backups").mkdir(parents=True, exist_ok=True)
|
|
42
|
+
(root / "workspaces").mkdir(parents=True, exist_ok=True)
|
|
43
|
+
|
|
44
|
+
# Profile-specific directories
|
|
45
|
+
profile_dir = root / "workspaces" / profile
|
|
46
|
+
(profile_dir / "qdrant").mkdir(parents=True, exist_ok=True)
|
|
47
|
+
(profile_dir / "neo4j").mkdir(parents=True, exist_ok=True)
|
|
48
|
+
(profile_dir / "embeddings").mkdir(parents=True, exist_ok=True)
|
|
49
|
+
|
|
50
|
+
# Set active workspace if no active_workspace file exists
|
|
51
|
+
active_file = root / "active_workspace"
|
|
52
|
+
if not active_file.exists():
|
|
53
|
+
active_file.write_text(profile, encoding="utf-8")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def get_active_profile() -> str:
|
|
57
|
+
"""Read the currently active workspace profile name."""
|
|
58
|
+
active_file = get_workspace_root() / "active_workspace"
|
|
59
|
+
if active_file.exists():
|
|
60
|
+
return active_file.read_text(encoding="utf-8").strip()
|
|
61
|
+
return "default"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def set_active_profile(name: str):
|
|
65
|
+
"""Set the active workspace profile."""
|
|
66
|
+
root = get_workspace_root()
|
|
67
|
+
profile_dir = root / "workspaces" / name
|
|
68
|
+
if not profile_dir.exists():
|
|
69
|
+
raise FileNotFoundError(f"Workspace profile '{name}' does not exist.")
|
|
70
|
+
active_file = root / "active_workspace"
|
|
71
|
+
active_file.write_text(name, encoding="utf-8")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def list_profiles() -> list[str]:
|
|
75
|
+
"""List all workspace profile names."""
|
|
76
|
+
workspaces_dir = get_workspace_root() / "workspaces"
|
|
77
|
+
if not workspaces_dir.exists():
|
|
78
|
+
return []
|
|
79
|
+
return sorted([
|
|
80
|
+
d.name for d in workspaces_dir.iterdir()
|
|
81
|
+
if d.is_dir()
|
|
82
|
+
])
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def create_profile(name: str):
|
|
86
|
+
"""Create a new workspace profile."""
|
|
87
|
+
root = get_workspace_root()
|
|
88
|
+
profile_dir = root / "workspaces" / name
|
|
89
|
+
if profile_dir.exists():
|
|
90
|
+
raise FileExistsError(f"Workspace profile '{name}' already exists.")
|
|
91
|
+
ensure_workspace(profile=name)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def delete_profile(name: str):
|
|
95
|
+
"""Delete a workspace profile directory."""
|
|
96
|
+
if name == get_active_profile():
|
|
97
|
+
raise ValueError(f"Cannot delete the active workspace profile '{name}'. Switch first.")
|
|
98
|
+
profile_dir = get_workspace_root() / "workspaces" / name
|
|
99
|
+
if not profile_dir.exists():
|
|
100
|
+
raise FileNotFoundError(f"Workspace profile '{name}' does not exist.")
|
|
101
|
+
shutil.rmtree(profile_dir)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def get_profile_path(name: str | None = None) -> Path:
|
|
105
|
+
"""Return the path for a specific profile (or the active one)."""
|
|
106
|
+
if name is None:
|
|
107
|
+
name = get_active_profile()
|
|
108
|
+
return get_workspace_root() / "workspaces" / name
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def get_db_path(profile: str | None = None) -> Path:
|
|
112
|
+
"""Return the SQLite database path for a workspace profile."""
|
|
113
|
+
return get_profile_path(profile) / "workspace.db"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def get_qdrant_path(profile: str | None = None) -> Path:
|
|
117
|
+
"""Return the Qdrant storage path for a workspace profile."""
|
|
118
|
+
return get_profile_path(profile) / "qdrant"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def get_neo4j_path(profile: str | None = None) -> Path:
|
|
122
|
+
"""Return the Neo4j data path for a workspace profile."""
|
|
123
|
+
return get_profile_path(profile) / "neo4j"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def get_logs_path() -> Path:
|
|
127
|
+
"""Return the shared logs directory path."""
|
|
128
|
+
return get_workspace_root() / "logs"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def get_backups_path() -> Path:
|
|
132
|
+
"""Return the shared backups directory path."""
|
|
133
|
+
return get_workspace_root() / "backups"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def get_cache_path() -> Path:
|
|
137
|
+
"""Return the shared cache directory path."""
|
|
138
|
+
return get_workspace_root() / "cache"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def get_workspace_size(profile: str | None = None) -> int:
|
|
142
|
+
"""Calculate total disk usage in bytes for a workspace profile."""
|
|
143
|
+
path = get_profile_path(profile) if profile else get_workspace_root()
|
|
144
|
+
total = 0
|
|
145
|
+
for dirpath, dirnames, filenames in os.walk(path):
|
|
146
|
+
for f in filenames:
|
|
147
|
+
fp = os.path.join(dirpath, f)
|
|
148
|
+
try:
|
|
149
|
+
total += os.path.getsize(fp)
|
|
150
|
+
except OSError:
|
|
151
|
+
pass
|
|
152
|
+
return total
|
models/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# models package
|
models/memory.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
class Repository:
|
|
2
|
+
def __init__(self, repo_name: str, description: str, language: str, visibility: str, stars: int, forks: int, open_issues: int, default_branch: str, updated_at: str, url: str):
|
|
3
|
+
self.repo_name = repo_name
|
|
4
|
+
self.description = description
|
|
5
|
+
self.language = language
|
|
6
|
+
self.visibility = visibility
|
|
7
|
+
self.stars = stars
|
|
8
|
+
self.forks = forks
|
|
9
|
+
self.open_issues = open_issues
|
|
10
|
+
self.default_branch = default_branch
|
|
11
|
+
self.updated_at = updated_at
|
|
12
|
+
self.url = url
|
|
13
|
+
|
|
14
|
+
class RepositoryDocument:
|
|
15
|
+
def __init__(self, repo_name: str, file_name: str, content: str, source: str, synced_at: str):
|
|
16
|
+
self.repo_name = repo_name
|
|
17
|
+
self.file_name = file_name
|
|
18
|
+
self.content = content
|
|
19
|
+
self.source = source
|
|
20
|
+
self.synced_at = synced_at
|
|
21
|
+
|
|
22
|
+
class Email:
|
|
23
|
+
def __init__(self, message_id: str, subject: str, sender: str, snippet: str, received_at: str):
|
|
24
|
+
self.message_id = message_id
|
|
25
|
+
self.subject = subject
|
|
26
|
+
self.sender = sender
|
|
27
|
+
self.snippet = snippet
|
|
28
|
+
self.received_at = received_at
|
storage/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# storage package
|