nexo-brain 3.2.0 → 4.0.1
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/.claude-plugin/plugin.json +1 -1
- package/README.md +10 -0
- package/package.json +1 -1
- package/src/agent_runner.py +1 -0
- package/src/auto_update.py +53 -0
- package/src/claim_graph.py +128 -15
- package/src/cognitive/_trust.py +2 -2
- package/src/compaction_memory.py +227 -0
- package/src/dashboard/app.py +15 -12
- package/src/doctor/providers/runtime.py +140 -11
- package/src/hook_guardrails.py +147 -9
- package/src/hooks/pre-compact.sh +18 -0
- package/src/media_memory.py +303 -0
- package/src/memory_backends.py +71 -0
- package/src/plugins/claims_tools.py +119 -0
- package/src/plugins/cognitive_memory.py +16 -1
- package/src/plugins/media_memory_tools.py +98 -0
- package/src/plugins/memory_export.py +196 -0
- package/src/plugins/user_state_tools.py +43 -0
- package/src/script_registry.py +31 -14
- package/src/scripts/deep-sleep/collect.py +6 -1
- package/src/server.py +1 -0
- package/src/system_catalog.py +383 -16
- package/src/tools_sessions.py +69 -0
- package/src/user_state_model.py +170 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Inspectable user-state model built from multiple NEXO signals."""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from datetime import datetime, timedelta, timezone
|
|
7
|
+
|
|
8
|
+
import cognitive
|
|
9
|
+
from db import get_db
|
|
10
|
+
from db._hot_context import search_hot_context
|
|
11
|
+
from memory_backends import get_backend
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def init_tables() -> None:
|
|
15
|
+
conn = get_db()
|
|
16
|
+
conn.executescript(
|
|
17
|
+
"""
|
|
18
|
+
CREATE TABLE IF NOT EXISTS user_state_snapshots (
|
|
19
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
20
|
+
state_label TEXT NOT NULL,
|
|
21
|
+
confidence REAL DEFAULT 0.0,
|
|
22
|
+
guidance TEXT DEFAULT '',
|
|
23
|
+
signals TEXT DEFAULT '{}',
|
|
24
|
+
backend_key TEXT DEFAULT 'sqlite',
|
|
25
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
26
|
+
);
|
|
27
|
+
CREATE INDEX IF NOT EXISTS idx_user_state_snapshots_created ON user_state_snapshots(created_at);
|
|
28
|
+
CREATE INDEX IF NOT EXISTS idx_user_state_snapshots_label ON user_state_snapshots(state_label);
|
|
29
|
+
"""
|
|
30
|
+
)
|
|
31
|
+
conn.commit()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _recent_correction_count(days: int) -> int:
|
|
35
|
+
cutoff = (datetime.now(timezone.utc) - timedelta(days=days)).isoformat()
|
|
36
|
+
row = cognitive._get_db().execute(
|
|
37
|
+
"SELECT COUNT(*) FROM memory_corrections WHERE created_at >= ?",
|
|
38
|
+
(cutoff,),
|
|
39
|
+
).fetchone()
|
|
40
|
+
return int((row[0] if row else 0) or 0)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _recent_trust_event_count(days: int, event_name: str) -> int:
|
|
44
|
+
cutoff = (datetime.now(timezone.utc) - timedelta(days=days)).isoformat()
|
|
45
|
+
row = cognitive._get_db().execute(
|
|
46
|
+
"SELECT COUNT(*) FROM trust_score WHERE created_at >= ? AND event = ?",
|
|
47
|
+
(cutoff, event_name),
|
|
48
|
+
).fetchone()
|
|
49
|
+
return int((row[0] if row else 0) or 0)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _recent_diary_signal_count(days: int) -> int:
|
|
53
|
+
cutoff = (datetime.now(timezone.utc) - timedelta(days=days)).isoformat(timespec="seconds")
|
|
54
|
+
row = get_db().execute(
|
|
55
|
+
"SELECT COUNT(*) FROM session_diary WHERE created_at >= ? AND user_signals != ''",
|
|
56
|
+
(cutoff,),
|
|
57
|
+
).fetchone()
|
|
58
|
+
return int((row[0] if row else 0) or 0)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def build_user_state(days: int = 7, *, persist: bool = False) -> dict:
|
|
62
|
+
init_tables()
|
|
63
|
+
trust = float(cognitive.get_trust_score())
|
|
64
|
+
history = cognitive.get_trust_history(days=days)
|
|
65
|
+
sentiments = history.get("sentiment_distribution", {})
|
|
66
|
+
negative = int((sentiments.get("negative") or {}).get("count", 0) or 0)
|
|
67
|
+
urgent = int((sentiments.get("urgent") or {}).get("count", 0) or 0)
|
|
68
|
+
positive = int((sentiments.get("positive") or {}).get("count", 0) or 0)
|
|
69
|
+
corrections = _recent_correction_count(days)
|
|
70
|
+
repeated_errors = _recent_trust_event_count(days, "repeated_error")
|
|
71
|
+
productive_sessions = _recent_trust_event_count(days, "session_productive")
|
|
72
|
+
delegation_events = _recent_trust_event_count(days, "delegation")
|
|
73
|
+
diaries_with_signals = _recent_diary_signal_count(days)
|
|
74
|
+
active_contexts = len(search_hot_context("", hours=min(max(days, 1) * 24, 168), limit=50, state="active"))
|
|
75
|
+
waiting_contexts = len(search_hot_context("", hours=min(max(days, 1) * 24, 168), limit=50, state="waiting_user"))
|
|
76
|
+
blocked_contexts = len(search_hot_context("", hours=min(max(days, 1) * 24, 168), limit=50, state="blocked"))
|
|
77
|
+
|
|
78
|
+
frustration_score = negative * 1.5 + corrections * 0.8 + repeated_errors * 1.2 + (1 if trust < 45 else 0)
|
|
79
|
+
flow_score = positive * 1.2 + productive_sessions * 1.0 + delegation_events * 0.8 + (1 if trust > 60 else 0)
|
|
80
|
+
urgency_score = urgent * 2.0 + blocked_contexts * 0.6
|
|
81
|
+
|
|
82
|
+
if urgency_score >= max(2.0, frustration_score, flow_score):
|
|
83
|
+
label = "urgent"
|
|
84
|
+
guidance = "Immediate execution. Keep answers short. Avoid speculative detours."
|
|
85
|
+
confidence = min(0.98, 0.45 + urgency_score * 0.12)
|
|
86
|
+
elif frustration_score >= max(2.0, flow_score):
|
|
87
|
+
label = "frustrated"
|
|
88
|
+
guidance = "Ultra-concise mode. Show concrete progress and avoid avoidable questions."
|
|
89
|
+
confidence = min(0.98, 0.4 + frustration_score * 0.1)
|
|
90
|
+
elif flow_score >= 2.5:
|
|
91
|
+
label = "in_flow"
|
|
92
|
+
guidance = "Keep momentum. Bias toward execution and only interrupt for real blockers."
|
|
93
|
+
confidence = min(0.98, 0.4 + flow_score * 0.09)
|
|
94
|
+
elif waiting_contexts > 0 or active_contexts > 6:
|
|
95
|
+
label = "loaded"
|
|
96
|
+
guidance = "Prefer batching, tight summaries, and explicit next actions."
|
|
97
|
+
confidence = 0.68
|
|
98
|
+
else:
|
|
99
|
+
label = "stable"
|
|
100
|
+
guidance = "Normal mode. Clear, direct execution with selective initiative."
|
|
101
|
+
confidence = 0.6
|
|
102
|
+
|
|
103
|
+
snapshot = {
|
|
104
|
+
"state_label": label,
|
|
105
|
+
"confidence": round(confidence, 2),
|
|
106
|
+
"guidance": guidance,
|
|
107
|
+
"trust_score": round(trust, 1),
|
|
108
|
+
"signals": {
|
|
109
|
+
"negative_sentiment": negative,
|
|
110
|
+
"urgent_sentiment": urgent,
|
|
111
|
+
"positive_sentiment": positive,
|
|
112
|
+
"recent_corrections": corrections,
|
|
113
|
+
"repeated_errors": repeated_errors,
|
|
114
|
+
"productive_sessions": productive_sessions,
|
|
115
|
+
"delegation_events": delegation_events,
|
|
116
|
+
"diaries_with_user_signals": diaries_with_signals,
|
|
117
|
+
"active_contexts": active_contexts,
|
|
118
|
+
"waiting_contexts": waiting_contexts,
|
|
119
|
+
"blocked_contexts": blocked_contexts,
|
|
120
|
+
},
|
|
121
|
+
"backend": get_backend().key,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if persist:
|
|
125
|
+
conn = get_db()
|
|
126
|
+
conn.execute(
|
|
127
|
+
"INSERT INTO user_state_snapshots (state_label, confidence, guidance, signals, backend_key) VALUES (?, ?, ?, ?, ?)",
|
|
128
|
+
(
|
|
129
|
+
snapshot["state_label"],
|
|
130
|
+
snapshot["confidence"],
|
|
131
|
+
snapshot["guidance"],
|
|
132
|
+
json.dumps(snapshot["signals"], ensure_ascii=True, sort_keys=True),
|
|
133
|
+
snapshot["backend"],
|
|
134
|
+
),
|
|
135
|
+
)
|
|
136
|
+
conn.commit()
|
|
137
|
+
|
|
138
|
+
return snapshot
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def list_user_state_snapshots(limit: int = 20) -> list[dict]:
|
|
142
|
+
init_tables()
|
|
143
|
+
rows = get_db().execute(
|
|
144
|
+
"SELECT * FROM user_state_snapshots ORDER BY created_at DESC, id DESC LIMIT ?",
|
|
145
|
+
(max(1, int(limit or 20)),),
|
|
146
|
+
).fetchall()
|
|
147
|
+
results = []
|
|
148
|
+
for row in rows:
|
|
149
|
+
item = dict(row)
|
|
150
|
+
try:
|
|
151
|
+
item["signals"] = json.loads(item.get("signals") or "{}")
|
|
152
|
+
except Exception:
|
|
153
|
+
item["signals"] = {}
|
|
154
|
+
results.append(item)
|
|
155
|
+
return results
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def user_state_stats(days: int = 30) -> dict:
|
|
159
|
+
init_tables()
|
|
160
|
+
cutoff = (datetime.now(timezone.utc) - timedelta(days=days)).isoformat(timespec="seconds")
|
|
161
|
+
rows = get_db().execute(
|
|
162
|
+
"SELECT state_label, COUNT(*) AS cnt FROM user_state_snapshots WHERE created_at >= ? GROUP BY state_label",
|
|
163
|
+
(cutoff,),
|
|
164
|
+
).fetchall()
|
|
165
|
+
return {
|
|
166
|
+
"window_days": days,
|
|
167
|
+
"snapshots": sum(int(row["cnt"]) for row in rows),
|
|
168
|
+
"by_state": {row["state_label"]: row["cnt"] for row in rows},
|
|
169
|
+
"backend": get_backend().key,
|
|
170
|
+
}
|