nexo-brain 2.3.0 → 2.3.2
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.
- package/README.md +1 -1
- package/bin/nexo-brain.js +92 -9
- package/bin/postinstall.js +22 -15
- package/package.json +7 -4
- package/src/auto_update.py +194 -5
- package/src/crons/sync.py +6 -2
- package/src/db/_core.py +1 -0
- package/src/db/_entities.py +1 -0
- package/src/db/_episodic.py +1 -0
- package/src/db/_learnings.py +1 -0
- package/src/db/_reminders.py +1 -0
- package/src/db/_schema.py +11 -1
- package/src/db/_sessions.py +1 -0
- package/src/db/_skills.py +1 -0
- package/src/hooks/capture-tool-logs.sh +23 -6
- package/src/hooks/session-start.sh +4 -3
- package/src/plugin_loader.py +1 -0
- package/src/plugins/update.py +377 -26
- package/src/scripts/deep-sleep/apply_findings.py +1 -0
- package/src/scripts/deep-sleep/collect.py +1 -0
- package/src/scripts/deep-sleep/extract.py +1 -0
- package/src/scripts/deep-sleep/synthesize.py +1 -0
- package/src/scripts/nexo-catchup.py +29 -4
- package/src/scripts/nexo-daily-self-audit.py +21 -1
- package/src/scripts/nexo-evolution-run.py +21 -1
- package/src/scripts/nexo-learning-housekeep.py +1 -0
- package/src/scripts/nexo-postmortem-consolidator.py +34 -9
- package/src/scripts/nexo-sleep.py +32 -10
- package/src/scripts/nexo-synthesis.py +29 -9
- package/src/scripts/nexo-update.sh +109 -7
- package/src/scripts/nexo-watchdog.sh +122 -58
- package/src/server.py +66 -1
- package/src/tools_coordination.py +1 -0
- package/src/tools_sessions.py +1 -0
- package/scripts/migrate-to-unified 2.sh +0 -813
- package/scripts/migrate-to-unified.sh +0 -813
- package/scripts/migrate-v1.5-to-v1.6 2.py +0 -778
- package/scripts/migrate-v1.5-to-v1.6.py +0 -778
- package/scripts/migrate-v1.7-to-v1.8 2.py +0 -214
- package/scripts/migrate-v1.7-to-v1.8.py +0 -214
- package/scripts/nexo-preflight.sh +0 -236
- package/scripts/pre-commit-check 2.sh +0 -55
- package/scripts/pre-commit-check.sh +0 -55
- package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
- package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
- package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
- package/src/__pycache__/hnsw_index.cpython-314.pyc +0 -0
- package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
- package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
- package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
- package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
- package/src/auto_close_sessions 2.py +0 -159
- package/src/auto_update 2.py +0 -634
- package/src/claim_graph 2.py +0 -323
- package/src/cognitive/__init__ 2.py +0 -62
- package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-314.pyc +0 -0
- package/src/cognitive/_core 2.py +0 -567
- package/src/cognitive/_decay 2.py +0 -382
- package/src/cognitive/_ingest 2.py +0 -892
- package/src/cognitive/_memory 2.py +0 -912
- package/src/cognitive/_search 2.py +0 -949
- package/src/cognitive/_trust 2.py +0 -464
- package/src/crons/__pycache__/sync.cpython-314.pyc +0 -0
- package/src/crons/manifest 2.json +0 -106
- package/src/crons/sync 2.py +0 -217
- package/src/dashboard/__init__ 2.py +0 -0
- package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
- package/src/dashboard/app 2.py +0 -789
- package/src/db/__init__ 2.py +0 -89
- package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_cron_runs.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_cron_runs.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_skills.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_skills.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_skills.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
- package/src/db/_core 2.py +0 -417
- package/src/db/_credentials 2.py +0 -124
- package/src/db/_entities 2.py +0 -178
- package/src/db/_episodic 2.py +0 -738
- package/src/db/_evolution 2.py +0 -54
- package/src/db/_fts 2.py +0 -406
- package/src/db/_learnings 2.py +0 -168
- package/src/db/_reminders 2.py +0 -338
- package/src/db/_schema 2.py +0 -364
- package/src/db/_sessions 2.py +0 -300
- package/src/db/_tasks 2.py +0 -91
- package/src/evolution_cycle 2.py +0 -266
- package/src/hnsw_index 2.py +0 -254
- package/src/hooks/auto_capture 2.py +0 -208
- package/src/hooks/caffeinate-guard 2.sh +0 -8
- package/src/hooks/capture-session 2.sh +0 -21
- package/src/hooks/capture-tool-logs 2.sh +0 -127
- package/src/hooks/daily-briefing-check 2.sh +0 -33
- package/src/hooks/inbox-hook 2.sh +0 -76
- package/src/hooks/post-compact 2.sh +0 -148
- package/src/hooks/pre-compact 2.sh +0 -151
- package/src/hooks/session-start 2.sh +0 -268
- package/src/hooks/session-stop 2.sh +0 -140
- package/src/kg_populate 2.py +0 -290
- package/src/knowledge_graph 2.py +0 -257
- package/src/maintenance 2.py +0 -59
- package/src/migrate_embeddings 2.py +0 -122
- package/src/plugin_loader 2.py +0 -202
- package/src/plugins/__init__ 2.py +0 -0
- package/src/plugins/__pycache__/__init__ 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/agents 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/artifact_registry 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/backup 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cognitive_memory 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/core_rules 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cortex 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/entities 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/episodic_memory 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/evolution 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/guard 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/knowledge_graph_tools 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/preferences 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/schedule.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/schedule.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/skills.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/skills.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/update 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
- package/src/plugins/adaptive_mode 2.py +0 -805
- package/src/plugins/agents 2.py +0 -52
- package/src/plugins/artifact_registry 2.py +0 -450
- package/src/plugins/backup 2.py +0 -104
- package/src/plugins/cognitive_memory 2.py +0 -564
- package/src/plugins/core_rules 2.py +0 -252
- package/src/plugins/cortex 2.py +0 -299
- package/src/plugins/entities 2.py +0 -67
- package/src/plugins/episodic_memory 2.py +0 -533
- package/src/plugins/evolution 2.py +0 -115
- package/src/plugins/guard 2.py +0 -746
- package/src/plugins/knowledge_graph_tools 2.py +0 -105
- package/src/plugins/preferences 2.py +0 -47
- package/src/plugins/update 2.py +0 -256
- package/src/requirements 2.txt +0 -12
- package/src/rules/__init__ 2.py +0 -0
- package/src/rules/core-rules 2.json +0 -331
- package/src/rules/migrate 2.py +0 -207
- package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-install.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-migrate.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
- package/src/scripts/check-context 2.py +0 -264
- package/src/scripts/nexo-auto-update 2.py +0 -6
- package/src/scripts/nexo-backup 2.sh +0 -25
- package/src/scripts/nexo-brain-activation 2.sh +0 -140
- package/src/scripts/nexo-catchup 2.py +0 -242
- package/src/scripts/nexo-cognitive-decay 2.py +0 -182
- package/src/scripts/nexo-daily-self-audit 2.py +0 -552
- package/src/scripts/nexo-deep-sleep 2.sh +0 -97
- package/src/scripts/nexo-evolution-run 2.py +0 -597
- package/src/scripts/nexo-followup-hygiene 2.py +0 -112
- package/src/scripts/nexo-github-monitor 2.py +0 -256
- package/src/scripts/nexo-immune 2.py +0 -927
- package/src/scripts/nexo-inbox-hook 2.sh +0 -74
- package/src/scripts/nexo-install 2.py +0 -6
- package/src/scripts/nexo-learning-housekeep 2.py +0 -245
- package/src/scripts/nexo-learning-validator 2.py +0 -207
- package/src/scripts/nexo-migrate 2.py +0 -232
- package/src/scripts/nexo-postmortem-consolidator 2.py +0 -421
- package/src/scripts/nexo-pre-commit 2.py +0 -120
- package/src/scripts/nexo-prevent-sleep 2.sh +0 -29
- package/src/scripts/nexo-proactive-dashboard 2.py +0 -345
- package/src/scripts/nexo-reflection 2.py +0 -253
- package/src/scripts/nexo-runtime-preflight 2.py +0 -274
- package/src/scripts/nexo-send-email 2.py +0 -25
- package/src/scripts/nexo-send-email.py +0 -25
- package/src/scripts/nexo-send-reply 2.py +0 -178
- package/src/scripts/nexo-send-reply.py +0 -178
- package/src/scripts/nexo-sleep 2.py +0 -592
- package/src/scripts/nexo-snapshot-restore 2.sh +0 -35
- package/src/scripts/nexo-synthesis 2.py +0 -253
- package/src/scripts/nexo-tcc-approve 2.sh +0 -79
- package/src/scripts/nexo-update 2.sh +0 -161
- package/src/scripts/nexo-watchdog 2.sh +0 -878
- package/src/scripts/nexo-watchdog-smoke 2.py +0 -119
- package/src/server 2.py +0 -733
- package/src/storage_router 2.py +0 -32
- package/src/tools_coordination 2.py +0 -102
- package/src/tools_credentials 2.py +0 -68
- package/src/tools_learnings 2.py +0 -220
- package/src/tools_menu 2.py +0 -227
- package/src/tools_reminders 2.py +0 -86
- package/src/tools_reminders_crud 2.py +0 -159
- package/src/tools_sessions 2.py +0 -476
- package/src/tools_task_history 2.py +0 -57
- package/templates/CLAUDE.md 2.template +0 -63
- package/templates/openclaw 2.json +0 -13
- package/tests/__init__ 2.py +0 -0
- package/tests/__init__.py +0 -0
- package/tests/conftest 2.py +0 -71
- package/tests/conftest.py +0 -71
- package/tests/test_cognitive 2.py +0 -205
- package/tests/test_cognitive.py +0 -205
- package/tests/test_knowledge_graph 2.py +0 -140
- package/tests/test_knowledge_graph.py +0 -140
- package/tests/test_migrations 2.py +0 -137
- package/tests/test_migrations.py +0 -137
package/src/db/_tasks 2.py
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
"""NEXO DB — Tasks module."""
|
|
2
|
-
from db._core import get_db, now_epoch
|
|
3
|
-
|
|
4
|
-
# ── Task History & Frequencies ─────────────────────────────────────
|
|
5
|
-
|
|
6
|
-
def log_task(task_num: str, task_name: str, notes: str = '', reasoning: str = '') -> dict:
|
|
7
|
-
"""Log a task execution with optional reasoning."""
|
|
8
|
-
conn = get_db()
|
|
9
|
-
now = now_epoch()
|
|
10
|
-
cursor = conn.execute(
|
|
11
|
-
"INSERT INTO task_history (task_num, task_name, executed_at, notes, reasoning) "
|
|
12
|
-
"VALUES (?, ?, ?, ?, ?)",
|
|
13
|
-
(task_num, task_name, now, notes, reasoning)
|
|
14
|
-
)
|
|
15
|
-
conn.commit()
|
|
16
|
-
row = conn.execute(
|
|
17
|
-
"SELECT * FROM task_history WHERE id = ?", (cursor.lastrowid,)
|
|
18
|
-
).fetchone()
|
|
19
|
-
return dict(row)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def list_task_history(task_num: str = None, days: int = 30) -> list[dict]:
|
|
23
|
-
"""List task execution history, optionally filtered by task_num."""
|
|
24
|
-
conn = get_db()
|
|
25
|
-
cutoff = now_epoch() - (days * 86400)
|
|
26
|
-
if task_num:
|
|
27
|
-
rows = conn.execute(
|
|
28
|
-
"SELECT * FROM task_history WHERE task_num = ? AND executed_at >= ? "
|
|
29
|
-
"ORDER BY executed_at DESC",
|
|
30
|
-
(task_num, cutoff)
|
|
31
|
-
).fetchall()
|
|
32
|
-
else:
|
|
33
|
-
rows = conn.execute(
|
|
34
|
-
"SELECT * FROM task_history WHERE executed_at >= ? "
|
|
35
|
-
"ORDER BY executed_at DESC",
|
|
36
|
-
(cutoff,)
|
|
37
|
-
).fetchall()
|
|
38
|
-
return [dict(r) for r in rows]
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def set_task_frequency(task_num: str, task_name: str,
|
|
42
|
-
frequency_days: int, description: str = '') -> dict:
|
|
43
|
-
"""Set or update the expected frequency for a task."""
|
|
44
|
-
conn = get_db()
|
|
45
|
-
conn.execute(
|
|
46
|
-
"INSERT OR REPLACE INTO task_frequencies (task_num, task_name, frequency_days, description) "
|
|
47
|
-
"VALUES (?, ?, ?, ?)",
|
|
48
|
-
(task_num, task_name, frequency_days, description)
|
|
49
|
-
)
|
|
50
|
-
conn.commit()
|
|
51
|
-
row = conn.execute(
|
|
52
|
-
"SELECT * FROM task_frequencies WHERE task_num = ?", (task_num,)
|
|
53
|
-
).fetchone()
|
|
54
|
-
return dict(row)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def get_overdue_tasks() -> list[dict]:
|
|
58
|
-
"""Get tasks where last execution exceeds the configured frequency."""
|
|
59
|
-
conn = get_db()
|
|
60
|
-
freqs = conn.execute("SELECT * FROM task_frequencies").fetchall()
|
|
61
|
-
now = now_epoch()
|
|
62
|
-
overdue = []
|
|
63
|
-
for f in freqs:
|
|
64
|
-
last = conn.execute(
|
|
65
|
-
"SELECT MAX(executed_at) as last_exec FROM task_history WHERE task_num = ?",
|
|
66
|
-
(f["task_num"],)
|
|
67
|
-
).fetchone()
|
|
68
|
-
last_exec = last["last_exec"] if last and last["last_exec"] else None
|
|
69
|
-
threshold = f["frequency_days"] * 86400
|
|
70
|
-
if last_exec is None or (now - last_exec) > threshold:
|
|
71
|
-
days_ago = round((now - last_exec) / 86400, 1) if last_exec else None
|
|
72
|
-
overdue.append({
|
|
73
|
-
"task_num": f["task_num"],
|
|
74
|
-
"task_name": f["task_name"],
|
|
75
|
-
"frequency_days": f["frequency_days"],
|
|
76
|
-
"last_executed": last_exec,
|
|
77
|
-
"days_since_last": days_ago,
|
|
78
|
-
"description": f["description"]
|
|
79
|
-
})
|
|
80
|
-
return overdue
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def get_task_frequencies() -> list[dict]:
|
|
84
|
-
"""Get all configured task frequencies."""
|
|
85
|
-
conn = get_db()
|
|
86
|
-
rows = conn.execute(
|
|
87
|
-
"SELECT * FROM task_frequencies ORDER BY task_num ASC"
|
|
88
|
-
).fetchall()
|
|
89
|
-
return [dict(r) for r in rows]
|
|
90
|
-
|
|
91
|
-
|
package/src/evolution_cycle 2.py
DELETED
|
@@ -1,266 +0,0 @@
|
|
|
1
|
-
"""NEXO Evolution Cycle — Self-improvement via Opus API.
|
|
2
|
-
|
|
3
|
-
Runs weekly after DMN. Analyzes patterns, proposes improvements.
|
|
4
|
-
v1: observe-only (all proposals logged as 'proposed' for the user to review).
|
|
5
|
-
v1.1 (future): sandbox execution of auto-approved changes.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import json
|
|
9
|
-
import os
|
|
10
|
-
import shutil
|
|
11
|
-
import subprocess
|
|
12
|
-
import sqlite3
|
|
13
|
-
import time
|
|
14
|
-
from datetime import datetime, date, timedelta
|
|
15
|
-
from pathlib import Path
|
|
16
|
-
|
|
17
|
-
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
18
|
-
NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(NEXO_HOME)))
|
|
19
|
-
NEXO_DB = NEXO_HOME / "data" / "nexo.db"
|
|
20
|
-
SANDBOX_DIR = NEXO_HOME / "sandbox" / "workspace"
|
|
21
|
-
SNAPSHOTS_DIR = NEXO_HOME / "snapshots"
|
|
22
|
-
RESTORE_LOG = NEXO_HOME / "logs" / "snapshot-restores.log"
|
|
23
|
-
|
|
24
|
-
# Evolution config: brain/ (canonical) > cortex/ (legacy) > NEXO_CODE (dev)
|
|
25
|
-
def _resolve_evolution_file(name: str) -> Path:
|
|
26
|
-
for candidate in [NEXO_HOME / "brain" / name, NEXO_HOME / "cortex" / name, NEXO_CODE / name]:
|
|
27
|
-
if candidate.exists():
|
|
28
|
-
return candidate
|
|
29
|
-
return NEXO_HOME / "brain" / name # default canonical path
|
|
30
|
-
|
|
31
|
-
OBJECTIVE_FILE = _resolve_evolution_file("evolution-objective.json")
|
|
32
|
-
PROMPT_FILE = _resolve_evolution_file("evolution-prompt.md")
|
|
33
|
-
|
|
34
|
-
MAX_SNAPSHOTS = 8
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def load_objective() -> dict:
|
|
38
|
-
if OBJECTIVE_FILE.exists():
|
|
39
|
-
return json.loads(OBJECTIVE_FILE.read_text())
|
|
40
|
-
return {}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def save_objective(obj: dict):
|
|
44
|
-
OBJECTIVE_FILE.write_text(json.dumps(obj, indent=2, ensure_ascii=False))
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def get_week_data(db_path: str) -> dict:
|
|
48
|
-
"""Gather last 7 days of learnings, decisions, changes, diaries."""
|
|
49
|
-
conn = sqlite3.connect(db_path, timeout=10)
|
|
50
|
-
conn.row_factory = sqlite3.Row
|
|
51
|
-
cutoff_epoch = time.time() - 7 * 86400
|
|
52
|
-
cutoff_date = (date.today() - timedelta(days=7)).isoformat()
|
|
53
|
-
|
|
54
|
-
data = {}
|
|
55
|
-
|
|
56
|
-
rows = conn.execute(
|
|
57
|
-
"SELECT category, title, content FROM learnings WHERE created_at > ? ORDER BY created_at DESC LIMIT 50",
|
|
58
|
-
(cutoff_epoch,)
|
|
59
|
-
).fetchall()
|
|
60
|
-
data["learnings"] = [dict(r) for r in rows]
|
|
61
|
-
|
|
62
|
-
rows = conn.execute(
|
|
63
|
-
"SELECT domain, decision, alternatives, based_on, confidence, outcome FROM decisions "
|
|
64
|
-
"WHERE created_at > ? ORDER BY created_at DESC LIMIT 20",
|
|
65
|
-
(cutoff_date,)
|
|
66
|
-
).fetchall()
|
|
67
|
-
data["decisions"] = [dict(r) for r in rows]
|
|
68
|
-
|
|
69
|
-
rows = conn.execute(
|
|
70
|
-
"SELECT files, what_changed, why, affects, risks FROM change_log "
|
|
71
|
-
"WHERE created_at > ? ORDER BY created_at DESC LIMIT 30",
|
|
72
|
-
(cutoff_date,)
|
|
73
|
-
).fetchall()
|
|
74
|
-
data["changes"] = [dict(r) for r in rows]
|
|
75
|
-
|
|
76
|
-
rows = conn.execute(
|
|
77
|
-
"SELECT summary, decisions as diary_decisions, pending, mental_state, domain, user_signals "
|
|
78
|
-
"FROM session_diary WHERE created_at > ? ORDER BY created_at DESC LIMIT 20",
|
|
79
|
-
(cutoff_date,)
|
|
80
|
-
).fetchall()
|
|
81
|
-
data["diaries"] = [dict(r) for r in rows]
|
|
82
|
-
|
|
83
|
-
rows = conn.execute(
|
|
84
|
-
"SELECT * FROM evolution_log ORDER BY id DESC LIMIT 20"
|
|
85
|
-
).fetchall()
|
|
86
|
-
data["evolution_history"] = [dict(r) for r in rows]
|
|
87
|
-
|
|
88
|
-
rows = conn.execute(
|
|
89
|
-
"SELECT dimension, score, delta, measured_at FROM evolution_metrics "
|
|
90
|
-
"WHERE id IN (SELECT MAX(id) FROM evolution_metrics GROUP BY dimension)"
|
|
91
|
-
).fetchall()
|
|
92
|
-
data["current_metrics"] = {r["dimension"]: dict(r) for r in rows}
|
|
93
|
-
|
|
94
|
-
conn.close()
|
|
95
|
-
return data
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def create_snapshot(files_to_backup: list) -> str:
|
|
99
|
-
"""Create a snapshot of specific files before modification."""
|
|
100
|
-
ts = datetime.now().strftime("%Y-%m-%dT%H:%M")
|
|
101
|
-
snap_dir = SNAPSHOTS_DIR / ts
|
|
102
|
-
files_dir = snap_dir / "files"
|
|
103
|
-
|
|
104
|
-
manifest = {
|
|
105
|
-
"created_at": datetime.now().isoformat(),
|
|
106
|
-
"files": [],
|
|
107
|
-
"reason": "evolution_cycle"
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
for filepath in files_to_backup:
|
|
111
|
-
fp = Path(filepath).expanduser()
|
|
112
|
-
if fp.exists():
|
|
113
|
-
rel = str(fp).replace(str(Path.home()) + "/", "")
|
|
114
|
-
dest = files_dir / rel
|
|
115
|
-
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
116
|
-
if os.path.abspath(str(fp)) == os.path.abspath(str(dest)):
|
|
117
|
-
continue # Skip: source and destination are the same file
|
|
118
|
-
shutil.copy2(fp, dest)
|
|
119
|
-
manifest["files"].append(rel)
|
|
120
|
-
|
|
121
|
-
snap_dir.mkdir(parents=True, exist_ok=True)
|
|
122
|
-
(snap_dir / "manifest.json").write_text(json.dumps(manifest, indent=2))
|
|
123
|
-
|
|
124
|
-
latest = SNAPSHOTS_DIR / "latest"
|
|
125
|
-
if latest.is_symlink():
|
|
126
|
-
latest.unlink()
|
|
127
|
-
latest.symlink_to(snap_dir)
|
|
128
|
-
|
|
129
|
-
_cleanup_snapshots()
|
|
130
|
-
return str(snap_dir)
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
def _cleanup_snapshots():
|
|
134
|
-
"""Remove old snapshots, keeping MAX_SNAPSHOTS most recent + golden."""
|
|
135
|
-
if not SNAPSHOTS_DIR.exists():
|
|
136
|
-
return
|
|
137
|
-
snaps = sorted(
|
|
138
|
-
[d for d in SNAPSHOTS_DIR.iterdir()
|
|
139
|
-
if d.is_dir() and d.name not in ("latest", "golden")],
|
|
140
|
-
key=lambda d: d.stat().st_mtime,
|
|
141
|
-
reverse=True
|
|
142
|
-
)
|
|
143
|
-
for old in snaps[MAX_SNAPSHOTS:]:
|
|
144
|
-
shutil.rmtree(old)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
def dry_run_restore_test() -> bool:
|
|
148
|
-
"""Test that snapshot+restore works before making real changes."""
|
|
149
|
-
test_file = SANDBOX_DIR / "restore-test.txt"
|
|
150
|
-
test_file.parent.mkdir(parents=True, exist_ok=True)
|
|
151
|
-
test_file.write_text("original_content")
|
|
152
|
-
|
|
153
|
-
snap_dir = create_snapshot([str(test_file)])
|
|
154
|
-
|
|
155
|
-
test_file.write_text("modified_content")
|
|
156
|
-
|
|
157
|
-
# Find restore script: NEXO_CODE/scripts/ first, then NEXO_HOME/scripts/
|
|
158
|
-
_nexo_code = Path(os.environ.get("NEXO_CODE", ""))
|
|
159
|
-
restore_script = None
|
|
160
|
-
for candidate in [_nexo_code / "scripts" / "nexo-snapshot-restore.sh",
|
|
161
|
-
NEXO_HOME / "scripts" / "nexo-snapshot-restore.sh"]:
|
|
162
|
-
if candidate.exists():
|
|
163
|
-
restore_script = candidate
|
|
164
|
-
break
|
|
165
|
-
if not restore_script:
|
|
166
|
-
test_file.unlink(missing_ok=True)
|
|
167
|
-
return False # No restore script available
|
|
168
|
-
|
|
169
|
-
try:
|
|
170
|
-
subprocess.run(
|
|
171
|
-
[str(restore_script), snap_dir],
|
|
172
|
-
capture_output=True, timeout=10, check=True
|
|
173
|
-
)
|
|
174
|
-
content = test_file.read_text()
|
|
175
|
-
test_file.unlink(missing_ok=True)
|
|
176
|
-
# Clean up test snapshot
|
|
177
|
-
snap_path = Path(snap_dir)
|
|
178
|
-
if snap_path.exists():
|
|
179
|
-
shutil.rmtree(snap_path)
|
|
180
|
-
return content == "original_content"
|
|
181
|
-
except Exception:
|
|
182
|
-
test_file.unlink(missing_ok=True)
|
|
183
|
-
return False
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
def build_evolution_prompt(week_data: dict, objective: dict) -> str:
|
|
187
|
-
"""Build a SHORT prompt — CLI investigates on its own using tools."""
|
|
188
|
-
|
|
189
|
-
# Summary stats only — CLI will dig deeper with tools
|
|
190
|
-
stats = {
|
|
191
|
-
"learnings_this_week": len(week_data.get("learnings", [])),
|
|
192
|
-
"decisions_this_week": len(week_data.get("decisions", [])),
|
|
193
|
-
"changes_this_week": len(week_data.get("changes", [])),
|
|
194
|
-
"diaries_this_week": len(week_data.get("diaries", [])),
|
|
195
|
-
"evolution_history": len(week_data.get("evolution_history", [])),
|
|
196
|
-
"current_scores": {dim: m["score"] for dim, m in week_data.get("current_metrics", {}).items()},
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
mode = objective.get("evolution_mode", "auto")
|
|
200
|
-
total = objective.get("total_evolutions", 0)
|
|
201
|
-
max_auto = max_auto_changes(total)
|
|
202
|
-
|
|
203
|
-
prompt = f"""You are NEXO Evolution — the weekly self-improvement cycle.
|
|
204
|
-
|
|
205
|
-
YOUR JOB: Analyze the past week and propose concrete improvements to NEXO's codebase.
|
|
206
|
-
|
|
207
|
-
WEEK SUMMARY:
|
|
208
|
-
- {stats['learnings_this_week']} new learnings
|
|
209
|
-
- {stats['decisions_this_week']} decisions made
|
|
210
|
-
- {stats['changes_this_week']} code changes deployed
|
|
211
|
-
- {stats['diaries_this_week']} session diaries
|
|
212
|
-
- {stats['evolution_history']} past evolution proposals
|
|
213
|
-
- Current scores: {json.dumps(stats['current_scores'])}
|
|
214
|
-
|
|
215
|
-
MODE: {mode} ({"proposals only, owner reviews" if mode == "review" else f"max {max_auto} auto-applied changes"})
|
|
216
|
-
CYCLE: #{total + 1}
|
|
217
|
-
|
|
218
|
-
INVESTIGATE using these tools:
|
|
219
|
-
1. Bash: sqlite3 {NEXO_DB} "SELECT category, title FROM learnings WHERE created_at > {time.time() - 7*86400} ORDER BY created_at DESC LIMIT 30"
|
|
220
|
-
2. Bash: sqlite3 {NEXO_DB} "SELECT area, COUNT(*) as cnt FROM error_repetitions GROUP BY area ORDER BY cnt DESC LIMIT 10"
|
|
221
|
-
3. Read ~/.nexo/coordination/daily-synthesis.md — today's context
|
|
222
|
-
4. Read ~/.nexo/coordination/postmortem-daily.md — self-critique patterns
|
|
223
|
-
5. Read ~/.nexo/logs/self-audit-summary.json — system health
|
|
224
|
-
6. Glob ~/.nexo/scripts/*.py — existing scripts
|
|
225
|
-
7. Glob ~/.nexo/plugins/*.py — existing plugins
|
|
226
|
-
|
|
227
|
-
LOOK FOR:
|
|
228
|
-
- Repeated errors that guard isn't preventing
|
|
229
|
-
- Scripts or processes that are failing or underperforming
|
|
230
|
-
- Missing functionality that session diaries keep asking for
|
|
231
|
-
- Redundant code or config that could be simplified
|
|
232
|
-
- Patterns in self-critique that suggest systemic issues
|
|
233
|
-
|
|
234
|
-
SAFETY:
|
|
235
|
-
- Safe zones for auto changes: ~/.nexo/scripts/, ~/.nexo/plugins/, ~/.nexo/brain/
|
|
236
|
-
- IMMUTABLE files (never touch): db.py, server.py, plugin_loader.py, cognitive.py, CLAUDE.md
|
|
237
|
-
- Every change needs: what file, what to change, why, risk, how to verify
|
|
238
|
-
|
|
239
|
-
OUTPUT FORMAT (JSON):
|
|
240
|
-
{{
|
|
241
|
-
"analysis": "one paragraph summary of what you found",
|
|
242
|
-
"patterns": [{{"type": "...", "description": "...", "frequency": "..."}}],
|
|
243
|
-
"proposals": [
|
|
244
|
-
{{
|
|
245
|
-
"classification": "auto" or "propose",
|
|
246
|
-
"dimension": "reliability|proactivity|efficiency|safety|learning",
|
|
247
|
-
"action": "what to do",
|
|
248
|
-
"reasoning": "why",
|
|
249
|
-
"scope": "local",
|
|
250
|
-
"changes": [{{"file": "path", "operation": "create|replace|append", "search": "text to find", "content": "new text"}}]
|
|
251
|
-
}}
|
|
252
|
-
]
|
|
253
|
-
}}
|
|
254
|
-
|
|
255
|
-
Max 3 proposals. Quality over quantity. If nothing needs improving, say so."""
|
|
256
|
-
|
|
257
|
-
return prompt
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
def max_auto_changes(total_evolutions: int) -> int:
|
|
261
|
-
"""Progressive trust: 1 for first 4 cycles, 2 for next 4, then 3."""
|
|
262
|
-
if total_evolutions < 4:
|
|
263
|
-
return 1
|
|
264
|
-
elif total_evolutions < 8:
|
|
265
|
-
return 2
|
|
266
|
-
return 3
|
package/src/hnsw_index 2.py
DELETED
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
"""NEXO HNSW Vector Index — Optional acceleration for cognitive search.
|
|
2
|
-
|
|
3
|
-
When memory count exceeds THRESHOLD (default 10_000), this module builds and
|
|
4
|
-
maintains an HNSW index for approximate nearest neighbor search. Falls back
|
|
5
|
-
gracefully to brute-force when hnswlib is not available or index is cold.
|
|
6
|
-
|
|
7
|
-
Usage in cognitive.search():
|
|
8
|
-
from hnsw_index import hnsw_search
|
|
9
|
-
candidates = hnsw_search(query_vec, store="stm", top_k=50)
|
|
10
|
-
# candidates is a list of (memory_id, distance) or None if not available
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
import os
|
|
14
|
-
import sqlite3
|
|
15
|
-
import threading
|
|
16
|
-
import numpy as np
|
|
17
|
-
from pathlib import Path
|
|
18
|
-
from typing import Optional
|
|
19
|
-
|
|
20
|
-
try:
|
|
21
|
-
import hnswlib
|
|
22
|
-
HNSWLIB_AVAILABLE = True
|
|
23
|
-
except ImportError:
|
|
24
|
-
HNSWLIB_AVAILABLE = False
|
|
25
|
-
|
|
26
|
-
# When to activate HNSW (below this, brute force is fine)
|
|
27
|
-
ACTIVATION_THRESHOLD = int(os.environ.get("NEXO_HNSW_THRESHOLD", "10000"))
|
|
28
|
-
|
|
29
|
-
# Index params
|
|
30
|
-
EMBEDDING_DIM = 768
|
|
31
|
-
EF_CONSTRUCTION = 200 # Higher = better recall during build, slower
|
|
32
|
-
M = 16 # Connections per node (16 is good for 768-dim)
|
|
33
|
-
EF_SEARCH = 50 # Higher = better recall during search
|
|
34
|
-
|
|
35
|
-
# Index file paths
|
|
36
|
-
_INDEX_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "hnsw_indices")
|
|
37
|
-
|
|
38
|
-
# In-memory indices (one per store)
|
|
39
|
-
_indices: dict = {} # {"stm": hnswlib.Index, "ltm": hnswlib.Index}
|
|
40
|
-
_index_lock = threading.Lock()
|
|
41
|
-
_id_maps: dict = {} # {"stm": {internal_id: db_id}, "ltm": {internal_id: db_id}}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def is_available() -> bool:
|
|
45
|
-
"""Check if HNSW is available and should be used."""
|
|
46
|
-
return HNSWLIB_AVAILABLE
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def _index_path(store: str) -> str:
|
|
50
|
-
return os.path.join(_INDEX_DIR, f"{store}.bin")
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def _id_map_path(store: str) -> str:
|
|
54
|
-
return os.path.join(_INDEX_DIR, f"{store}_ids.npy")
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def should_activate(store: str = "both") -> bool:
|
|
58
|
-
"""Check if memory count exceeds threshold, making HNSW worthwhile."""
|
|
59
|
-
if not HNSWLIB_AVAILABLE:
|
|
60
|
-
return False
|
|
61
|
-
try:
|
|
62
|
-
import cognitive
|
|
63
|
-
db = cognitive._get_db()
|
|
64
|
-
total = 0
|
|
65
|
-
if store in ("both", "stm"):
|
|
66
|
-
total += db.execute("SELECT COUNT(*) FROM stm_memories WHERE promoted_to_ltm = 0").fetchone()[0]
|
|
67
|
-
if store in ("both", "ltm"):
|
|
68
|
-
total += db.execute("SELECT COUNT(*) FROM ltm_memories WHERE is_dormant = 0").fetchone()[0]
|
|
69
|
-
return total >= ACTIVATION_THRESHOLD
|
|
70
|
-
except Exception:
|
|
71
|
-
return False
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def build_index(store: str) -> dict:
|
|
75
|
-
"""Build HNSW index from all active memories in the given store.
|
|
76
|
-
|
|
77
|
-
Args:
|
|
78
|
-
store: "stm" or "ltm"
|
|
79
|
-
|
|
80
|
-
Returns:
|
|
81
|
-
{"count": N, "store": store, "status": "built"} or error dict
|
|
82
|
-
"""
|
|
83
|
-
if not HNSWLIB_AVAILABLE:
|
|
84
|
-
return {"error": "hnswlib not installed"}
|
|
85
|
-
|
|
86
|
-
try:
|
|
87
|
-
import cognitive
|
|
88
|
-
db = cognitive._get_db()
|
|
89
|
-
except Exception as e:
|
|
90
|
-
return {"error": str(e)}
|
|
91
|
-
|
|
92
|
-
table = "stm_memories" if store == "stm" else "ltm_memories"
|
|
93
|
-
where = "promoted_to_ltm = 0" if store == "stm" else "is_dormant = 0"
|
|
94
|
-
|
|
95
|
-
rows = db.execute(f"SELECT id, embedding FROM {table} WHERE {where}").fetchall()
|
|
96
|
-
if not rows:
|
|
97
|
-
return {"count": 0, "store": store, "status": "empty"}
|
|
98
|
-
|
|
99
|
-
count = len(rows)
|
|
100
|
-
index = hnswlib.Index(space='cosine', dim=EMBEDDING_DIM)
|
|
101
|
-
index.init_index(max_elements=max(count * 2, 1000), ef_construction=EF_CONSTRUCTION, M=M)
|
|
102
|
-
index.set_ef(EF_SEARCH)
|
|
103
|
-
|
|
104
|
-
id_map = {}
|
|
105
|
-
vectors = []
|
|
106
|
-
internal_ids = []
|
|
107
|
-
|
|
108
|
-
for i, row in enumerate(rows):
|
|
109
|
-
vec = np.frombuffer(row["embedding"], dtype=np.float32)
|
|
110
|
-
if len(vec) != EMBEDDING_DIM:
|
|
111
|
-
continue
|
|
112
|
-
vectors.append(vec)
|
|
113
|
-
internal_ids.append(i)
|
|
114
|
-
id_map[i] = row["id"]
|
|
115
|
-
|
|
116
|
-
if not vectors:
|
|
117
|
-
return {"count": 0, "store": store, "status": "no_valid_vectors"}
|
|
118
|
-
|
|
119
|
-
data = np.array(vectors, dtype=np.float32)
|
|
120
|
-
ids = np.array(internal_ids, dtype=np.int64)
|
|
121
|
-
index.add_items(data, ids)
|
|
122
|
-
|
|
123
|
-
# Save to disk
|
|
124
|
-
Path(_INDEX_DIR).mkdir(exist_ok=True)
|
|
125
|
-
index.save_index(_index_path(store))
|
|
126
|
-
np.save(_id_map_path(store), id_map)
|
|
127
|
-
|
|
128
|
-
with _index_lock:
|
|
129
|
-
_indices[store] = index
|
|
130
|
-
_id_maps[store] = id_map
|
|
131
|
-
|
|
132
|
-
return {"count": count, "store": store, "status": "built"}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def load_index(store: str) -> bool:
|
|
136
|
-
"""Load a previously built index from disk."""
|
|
137
|
-
if not HNSWLIB_AVAILABLE:
|
|
138
|
-
return False
|
|
139
|
-
|
|
140
|
-
idx_path = _index_path(store)
|
|
141
|
-
map_path = _id_map_path(store) + ".npy" if not _id_map_path(store).endswith(".npy") else _id_map_path(store)
|
|
142
|
-
|
|
143
|
-
if not os.path.exists(idx_path):
|
|
144
|
-
return False
|
|
145
|
-
|
|
146
|
-
try:
|
|
147
|
-
index = hnswlib.Index(space='cosine', dim=EMBEDDING_DIM)
|
|
148
|
-
index.load_index(idx_path)
|
|
149
|
-
index.set_ef(EF_SEARCH)
|
|
150
|
-
|
|
151
|
-
id_map = np.load(map_path, allow_pickle=True).item()
|
|
152
|
-
|
|
153
|
-
with _index_lock:
|
|
154
|
-
_indices[store] = index
|
|
155
|
-
_id_maps[store] = id_map
|
|
156
|
-
return True
|
|
157
|
-
except Exception:
|
|
158
|
-
return False
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
def search(query_vec: np.ndarray, store: str = "stm", top_k: int = 50) -> Optional[list[tuple[int, float]]]:
|
|
162
|
-
"""Search the HNSW index for approximate nearest neighbors.
|
|
163
|
-
|
|
164
|
-
Args:
|
|
165
|
-
query_vec: Query embedding (768-dim float32)
|
|
166
|
-
store: "stm" or "ltm"
|
|
167
|
-
top_k: Number of results
|
|
168
|
-
|
|
169
|
-
Returns:
|
|
170
|
-
List of (db_memory_id, cosine_distance) or None if index not available.
|
|
171
|
-
Note: hnswlib with cosine space returns 1 - cosine_similarity as distance.
|
|
172
|
-
"""
|
|
173
|
-
with _index_lock:
|
|
174
|
-
index = _indices.get(store)
|
|
175
|
-
id_map = _id_maps.get(store)
|
|
176
|
-
|
|
177
|
-
if index is None or id_map is None:
|
|
178
|
-
# Try loading from disk
|
|
179
|
-
if load_index(store):
|
|
180
|
-
with _index_lock:
|
|
181
|
-
index = _indices.get(store)
|
|
182
|
-
id_map = _id_maps.get(store)
|
|
183
|
-
if index is None:
|
|
184
|
-
return None
|
|
185
|
-
|
|
186
|
-
try:
|
|
187
|
-
query = query_vec.reshape(1, -1).astype(np.float32)
|
|
188
|
-
labels, distances = index.knn_query(query, k=min(top_k, index.get_current_count()))
|
|
189
|
-
results = []
|
|
190
|
-
for label, dist in zip(labels[0], distances[0]):
|
|
191
|
-
db_id = id_map.get(int(label))
|
|
192
|
-
if db_id is not None:
|
|
193
|
-
# Convert cosine distance to similarity: sim = 1 - dist
|
|
194
|
-
results.append((db_id, float(1.0 - dist)))
|
|
195
|
-
return results
|
|
196
|
-
except Exception:
|
|
197
|
-
return None
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
def add_item(store: str, db_id: int, embedding: np.ndarray) -> bool:
|
|
201
|
-
"""Incrementally add a single item to the index (for new ingestions)."""
|
|
202
|
-
with _index_lock:
|
|
203
|
-
index = _indices.get(store)
|
|
204
|
-
id_map = _id_maps.get(store)
|
|
205
|
-
|
|
206
|
-
if index is None or id_map is None:
|
|
207
|
-
return False
|
|
208
|
-
|
|
209
|
-
try:
|
|
210
|
-
internal_id = max(id_map.keys()) + 1 if id_map else 0
|
|
211
|
-
# Resize if needed
|
|
212
|
-
if index.get_current_count() >= index.get_max_elements() - 1:
|
|
213
|
-
index.resize_index(index.get_max_elements() * 2)
|
|
214
|
-
|
|
215
|
-
vec = embedding.reshape(1, -1).astype(np.float32)
|
|
216
|
-
index.add_items(vec, np.array([internal_id], dtype=np.int64))
|
|
217
|
-
|
|
218
|
-
with _index_lock:
|
|
219
|
-
id_map[internal_id] = db_id
|
|
220
|
-
return True
|
|
221
|
-
except Exception:
|
|
222
|
-
return False
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
def invalidate(store: str = "both"):
|
|
226
|
-
"""Remove indices from memory (forces rebuild on next use)."""
|
|
227
|
-
with _index_lock:
|
|
228
|
-
if store in ("both", "stm"):
|
|
229
|
-
_indices.pop("stm", None)
|
|
230
|
-
_id_maps.pop("stm", None)
|
|
231
|
-
if store in ("both", "ltm"):
|
|
232
|
-
_indices.pop("ltm", None)
|
|
233
|
-
_id_maps.pop("ltm", None)
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
def stats() -> dict:
|
|
237
|
-
"""Return HNSW index statistics."""
|
|
238
|
-
result = {
|
|
239
|
-
"hnswlib_available": HNSWLIB_AVAILABLE,
|
|
240
|
-
"activation_threshold": ACTIVATION_THRESHOLD,
|
|
241
|
-
"indices": {},
|
|
242
|
-
}
|
|
243
|
-
with _index_lock:
|
|
244
|
-
for store in ("stm", "ltm"):
|
|
245
|
-
idx = _indices.get(store)
|
|
246
|
-
if idx:
|
|
247
|
-
result["indices"][store] = {
|
|
248
|
-
"count": idx.get_current_count(),
|
|
249
|
-
"max_elements": idx.get_max_elements(),
|
|
250
|
-
"ef_search": EF_SEARCH,
|
|
251
|
-
}
|
|
252
|
-
else:
|
|
253
|
-
result["indices"][store] = {"status": "not_loaded"}
|
|
254
|
-
return result
|