nexo-brain 3.1.2 → 3.1.4
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/package.json +1 -1
- package/src/dashboard/app.py +131 -106
- package/src/db/__init__.py +3 -2
- package/src/db/_core.py +20 -1
- package/src/db/_reminders.py +499 -129
- package/src/db/_schema.py +30 -0
- package/src/scripts/deep-sleep/apply_findings.py +37 -34
- package/src/scripts/nexo-daily-self-audit.py +26 -42
- package/src/scripts/nexo-followup-hygiene.py +29 -2
- package/src/server.py +121 -28
- package/src/tools_learnings.py +12 -2
- package/src/tools_reminders.py +8 -5
- package/src/tools_reminders_crud.py +200 -18
|
@@ -2,13 +2,77 @@
|
|
|
2
2
|
|
|
3
3
|
from db import (
|
|
4
4
|
create_reminder, update_reminder, complete_reminder, delete_reminder,
|
|
5
|
-
|
|
5
|
+
restore_reminder, add_reminder_note, get_reminder,
|
|
6
6
|
create_followup, update_followup, complete_followup, delete_followup,
|
|
7
|
-
|
|
7
|
+
restore_followup, add_followup_note, get_followup,
|
|
8
|
+
validate_item_read_token,
|
|
8
9
|
find_decisions_by_context_ref, update_decision_outcome,
|
|
9
10
|
)
|
|
10
11
|
|
|
11
12
|
|
|
13
|
+
def _require_item_read(item_type: str, item_id: str, read_token: str) -> str | None:
|
|
14
|
+
ok, message = validate_item_read_token(read_token, item_type, item_id)
|
|
15
|
+
if ok:
|
|
16
|
+
return None
|
|
17
|
+
prefix = "followup" if item_type == "followup" else "reminder"
|
|
18
|
+
return f"ERROR: {message} Use nexo_{prefix}_get(id='{item_id}') first."
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _history_lines(history: list[dict]) -> list[str]:
|
|
22
|
+
if not history:
|
|
23
|
+
return ["- (no history)"]
|
|
24
|
+
lines: list[str] = []
|
|
25
|
+
for event in history:
|
|
26
|
+
created_at = event.get("created_at") or "?"
|
|
27
|
+
event_type = event.get("event_type") or "event"
|
|
28
|
+
actor = event.get("actor") or "system"
|
|
29
|
+
note = (event.get("note") or "").strip()
|
|
30
|
+
suffix = f" — {note}" if note else ""
|
|
31
|
+
lines.append(f"- {created_at} [{event_type}] ({actor}){suffix}")
|
|
32
|
+
return lines
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _format_reminder_payload(reminder: dict) -> str:
|
|
36
|
+
lines = [
|
|
37
|
+
f"REMINDER {reminder['id']}",
|
|
38
|
+
f"Description: {reminder.get('description') or ''}",
|
|
39
|
+
f"Date: {reminder.get('date') or '—'}",
|
|
40
|
+
f"Status: {reminder.get('status') or '—'}",
|
|
41
|
+
f"Category: {reminder.get('category') or 'general'}",
|
|
42
|
+
]
|
|
43
|
+
history_rules = reminder.get("history_rules") or []
|
|
44
|
+
if history_rules:
|
|
45
|
+
lines.append("Usage rules:")
|
|
46
|
+
lines.extend(f"- {rule}" for rule in history_rules)
|
|
47
|
+
lines.append("History:")
|
|
48
|
+
lines.extend(_history_lines(reminder.get("history") or []))
|
|
49
|
+
if reminder.get("read_token"):
|
|
50
|
+
lines.append(f"READ_TOKEN: {reminder['read_token']}")
|
|
51
|
+
return "\n".join(lines)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _format_followup_payload(followup: dict) -> str:
|
|
55
|
+
lines = [
|
|
56
|
+
f"FOLLOWUP {followup['id']}",
|
|
57
|
+
f"Description: {followup.get('description') or ''}",
|
|
58
|
+
f"Date: {followup.get('date') or '—'}",
|
|
59
|
+
f"Status: {followup.get('status') or '—'}",
|
|
60
|
+
f"Verification: {followup.get('verification') or '—'}",
|
|
61
|
+
f"Reasoning: {followup.get('reasoning') or '—'}",
|
|
62
|
+
f"Recurrence: {followup.get('recurrence') or '—'}",
|
|
63
|
+
f"Priority: {followup.get('priority') or 'medium'}",
|
|
64
|
+
]
|
|
65
|
+
history_rules = followup.get("history_rules") or []
|
|
66
|
+
if history_rules:
|
|
67
|
+
lines.append("Usage rules:")
|
|
68
|
+
lines.extend(f"- {rule}" for rule in history_rules)
|
|
69
|
+
lines.append("History:")
|
|
70
|
+
lines.extend(_history_lines(followup.get("history") or []))
|
|
71
|
+
if followup.get("read_token"):
|
|
72
|
+
lines.append(f"READ_TOKEN: {followup['read_token']}")
|
|
73
|
+
return "\n".join(lines)
|
|
74
|
+
|
|
75
|
+
|
|
12
76
|
# ── Reminders ──────────────────────────────────────────────────────────────────
|
|
13
77
|
|
|
14
78
|
def handle_reminder_create(id: str, description: str, date: str = '', category: str = 'general') -> str:
|
|
@@ -25,8 +89,27 @@ def handle_reminder_create(id: str, description: str, date: str = '', category:
|
|
|
25
89
|
return f"Reminder created. Date: {date_str}. Category: {category}."
|
|
26
90
|
|
|
27
91
|
|
|
28
|
-
def
|
|
92
|
+
def handle_reminder_get(id: str) -> str:
|
|
93
|
+
"""Read a reminder with history and return a read token for safe mutations."""
|
|
94
|
+
result = get_reminder(id=id, include_history=True)
|
|
95
|
+
if not result:
|
|
96
|
+
return f"ERROR: Reminder {id} not found."
|
|
97
|
+
return _format_reminder_payload(result)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def handle_reminder_update(
|
|
101
|
+
id: str,
|
|
102
|
+
description: str = '',
|
|
103
|
+
date: str = '',
|
|
104
|
+
status: str = '',
|
|
105
|
+
category: str = '',
|
|
106
|
+
read_token: str = '',
|
|
107
|
+
) -> str:
|
|
29
108
|
"""Update one or more fields of an existing reminder."""
|
|
109
|
+
error = _require_item_read("reminder", id, read_token)
|
|
110
|
+
if error:
|
|
111
|
+
return error
|
|
112
|
+
|
|
30
113
|
fields: dict = {}
|
|
31
114
|
if description:
|
|
32
115
|
fields['description'] = description
|
|
@@ -41,8 +124,9 @@ def handle_reminder_update(id: str, description: str = '', date: str = '', statu
|
|
|
41
124
|
return f"ERROR: No fields specified to update for {id}."
|
|
42
125
|
|
|
43
126
|
result = update_reminder(id=id, **fields)
|
|
44
|
-
if not result:
|
|
45
|
-
|
|
127
|
+
if not result or "error" in result:
|
|
128
|
+
error_msg = result.get("error", f"Reminder {id} not found.") if isinstance(result, dict) else f"Reminder {id} not found."
|
|
129
|
+
return f"ERROR: {error_msg}"
|
|
46
130
|
|
|
47
131
|
changed = ', '.join(fields.keys())
|
|
48
132
|
return f"Reminder {id} updated: {changed}."
|
|
@@ -57,18 +141,55 @@ def handle_reminder_complete(id: str) -> str:
|
|
|
57
141
|
return f"Reminder {id} marked COMPLETED."
|
|
58
142
|
|
|
59
143
|
|
|
60
|
-
def
|
|
61
|
-
"""
|
|
144
|
+
def handle_reminder_note(id: str, note: str, read_token: str = '', actor: str = 'nexo') -> str:
|
|
145
|
+
"""Append a note to reminder history."""
|
|
146
|
+
if not note.strip():
|
|
147
|
+
return "ERROR: note is required."
|
|
148
|
+
error = _require_item_read("reminder", id, read_token)
|
|
149
|
+
if error:
|
|
150
|
+
return error
|
|
151
|
+
result = add_reminder_note(id=id, note=note.strip(), actor=actor or "nexo")
|
|
152
|
+
if not result or "error" in result:
|
|
153
|
+
error_msg = result.get("error", f"Reminder {id} not found.") if isinstance(result, dict) else f"Reminder {id} not found."
|
|
154
|
+
return f"ERROR: {error_msg}"
|
|
155
|
+
return f"Reminder {id} note added."
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def handle_reminder_restore(id: str, read_token: str = '') -> str:
|
|
159
|
+
"""Restore a soft-deleted reminder."""
|
|
160
|
+
error = _require_item_read("reminder", id, read_token)
|
|
161
|
+
if error:
|
|
162
|
+
return error
|
|
163
|
+
result = restore_reminder(id=id)
|
|
164
|
+
if not result or "error" in result:
|
|
165
|
+
error_msg = result.get("error", f"Reminder {id} not found.") if isinstance(result, dict) else f"Reminder {id} not found."
|
|
166
|
+
return f"ERROR: {error_msg}"
|
|
167
|
+
return f"Reminder {id} restored to PENDING."
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def handle_reminder_delete(id: str, read_token: str = '') -> str:
|
|
171
|
+
"""Soft-delete a reminder."""
|
|
172
|
+
error = _require_item_read("reminder", id, read_token)
|
|
173
|
+
if error:
|
|
174
|
+
return error
|
|
62
175
|
result = delete_reminder(id=id)
|
|
63
176
|
if not result:
|
|
64
177
|
return f"ERROR: Reminder {id} not found."
|
|
65
178
|
|
|
66
|
-
return f"Reminder {id} deleted."
|
|
179
|
+
return f"Reminder {id} soft-deleted."
|
|
67
180
|
|
|
68
181
|
|
|
69
182
|
# ── Followups ──────────────────────────────────────────────────────────────────
|
|
70
183
|
|
|
71
|
-
def handle_followup_create(
|
|
184
|
+
def handle_followup_create(
|
|
185
|
+
id: str,
|
|
186
|
+
description: str,
|
|
187
|
+
date: str = '',
|
|
188
|
+
verification: str = '',
|
|
189
|
+
reasoning: str = '',
|
|
190
|
+
recurrence: str = '',
|
|
191
|
+
priority: str = 'medium',
|
|
192
|
+
) -> str:
|
|
72
193
|
"""Create a new NEXO followup. id must start with 'NF'.
|
|
73
194
|
|
|
74
195
|
Args:
|
|
@@ -83,20 +204,49 @@ def handle_followup_create(id: str, description: str, date: str = '', verificati
|
|
|
83
204
|
if not id.startswith('NF'):
|
|
84
205
|
return f"ERROR: Followup ID must start with 'NF' (received: '{id}')."
|
|
85
206
|
|
|
86
|
-
result = create_followup(
|
|
207
|
+
result = create_followup(
|
|
208
|
+
id=id,
|
|
209
|
+
description=description,
|
|
210
|
+
date=date or None,
|
|
211
|
+
verification=verification,
|
|
212
|
+
reasoning=reasoning,
|
|
213
|
+
recurrence=recurrence or None,
|
|
214
|
+
priority=priority or "medium",
|
|
215
|
+
)
|
|
87
216
|
if not result or "error" in result:
|
|
88
217
|
error_msg = result.get("error", "unknown") if isinstance(result, dict) else "unknown"
|
|
89
218
|
return f"ERROR: {error_msg}"
|
|
90
219
|
|
|
91
220
|
date_str = date if date else 'no date'
|
|
92
221
|
rec_str = f" Recurrence: {recurrence}." if recurrence else ""
|
|
222
|
+
priority_str = f" Priority: {priority or 'medium'}."
|
|
93
223
|
warning = result.get("warning", "")
|
|
94
224
|
warn_str = f"\n{warning}" if warning else ""
|
|
95
|
-
return f"Followup created. Date: {date_str}.{rec_str}{warn_str}"
|
|
225
|
+
return f"Followup created. Date: {date_str}.{priority_str}{rec_str}{warn_str}"
|
|
96
226
|
|
|
97
227
|
|
|
98
|
-
def
|
|
228
|
+
def handle_followup_get(id: str) -> str:
|
|
229
|
+
"""Read a followup with history and return a read token for safe mutations."""
|
|
230
|
+
result = get_followup(id=id, include_history=True)
|
|
231
|
+
if not result:
|
|
232
|
+
return f"ERROR: Followup {id} not found."
|
|
233
|
+
return _format_followup_payload(result)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def handle_followup_update(
|
|
237
|
+
id: str,
|
|
238
|
+
description: str = '',
|
|
239
|
+
date: str = '',
|
|
240
|
+
verification: str = '',
|
|
241
|
+
status: str = '',
|
|
242
|
+
priority: str = '',
|
|
243
|
+
read_token: str = '',
|
|
244
|
+
) -> str:
|
|
99
245
|
"""Update one or more fields of an existing followup."""
|
|
246
|
+
error = _require_item_read("followup", id, read_token)
|
|
247
|
+
if error:
|
|
248
|
+
return error
|
|
249
|
+
|
|
100
250
|
fields: dict = {}
|
|
101
251
|
if description:
|
|
102
252
|
fields['description'] = description
|
|
@@ -106,16 +256,19 @@ def handle_followup_update(id: str, description: str = '', date: str = '', verif
|
|
|
106
256
|
fields['verification'] = verification
|
|
107
257
|
if status:
|
|
108
258
|
fields['status'] = status
|
|
259
|
+
if priority:
|
|
260
|
+
fields['priority'] = priority
|
|
109
261
|
|
|
110
262
|
if not fields:
|
|
111
263
|
return f"ERROR: No fields specified to update for {id}."
|
|
112
264
|
|
|
113
265
|
result = update_followup(id=id, **fields)
|
|
114
|
-
if not result:
|
|
115
|
-
|
|
266
|
+
if not result or "error" in result:
|
|
267
|
+
error_msg = result.get("error", f"Followup {id} not found.") if isinstance(result, dict) else f"Followup {id} not found."
|
|
268
|
+
return f"ERROR: {error_msg}"
|
|
116
269
|
|
|
117
270
|
changed = ', '.join(fields.keys())
|
|
118
|
-
return f"Followup updated: {changed}."
|
|
271
|
+
return f"Followup {id} updated: {changed}."
|
|
119
272
|
|
|
120
273
|
|
|
121
274
|
def handle_followup_complete(id: str, result: str = '') -> str:
|
|
@@ -157,10 +310,39 @@ def handle_followup_complete(id: str, result: str = '') -> str:
|
|
|
157
310
|
return msg
|
|
158
311
|
|
|
159
312
|
|
|
160
|
-
def
|
|
161
|
-
"""
|
|
313
|
+
def handle_followup_note(id: str, note: str, read_token: str = '', actor: str = 'nexo') -> str:
|
|
314
|
+
"""Append a note to followup history."""
|
|
315
|
+
if not note.strip():
|
|
316
|
+
return "ERROR: note is required."
|
|
317
|
+
error = _require_item_read("followup", id, read_token)
|
|
318
|
+
if error:
|
|
319
|
+
return error
|
|
320
|
+
result = add_followup_note(id=id, note=note.strip(), actor=actor or "nexo")
|
|
321
|
+
if not result or "error" in result:
|
|
322
|
+
error_msg = result.get("error", f"Followup {id} not found.") if isinstance(result, dict) else f"Followup {id} not found."
|
|
323
|
+
return f"ERROR: {error_msg}"
|
|
324
|
+
return f"Followup {id} note added."
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def handle_followup_restore(id: str, read_token: str = '') -> str:
|
|
328
|
+
"""Restore a soft-deleted followup."""
|
|
329
|
+
error = _require_item_read("followup", id, read_token)
|
|
330
|
+
if error:
|
|
331
|
+
return error
|
|
332
|
+
result = restore_followup(id=id)
|
|
333
|
+
if not result or "error" in result:
|
|
334
|
+
error_msg = result.get("error", f"Followup {id} not found.") if isinstance(result, dict) else f"Followup {id} not found."
|
|
335
|
+
return f"ERROR: {error_msg}"
|
|
336
|
+
return f"Followup {id} restored to PENDING."
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def handle_followup_delete(id: str, read_token: str = '') -> str:
|
|
340
|
+
"""Soft-delete a followup."""
|
|
341
|
+
error = _require_item_read("followup", id, read_token)
|
|
342
|
+
if error:
|
|
343
|
+
return error
|
|
162
344
|
result = delete_followup(id=id)
|
|
163
345
|
if not result:
|
|
164
346
|
return f"ERROR: Followup {id} not found."
|
|
165
347
|
|
|
166
|
-
return f"Followup deleted."
|
|
348
|
+
return f"Followup {id} soft-deleted."
|