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.
@@ -2,13 +2,77 @@
2
2
 
3
3
  from db import (
4
4
  create_reminder, update_reminder, complete_reminder, delete_reminder,
5
- get_reminders, get_reminder,
5
+ restore_reminder, add_reminder_note, get_reminder,
6
6
  create_followup, update_followup, complete_followup, delete_followup,
7
- get_followups, get_followup,
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 handle_reminder_update(id: str, description: str = '', date: str = '', status: str = '', category: str = '') -> str:
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
- return f"ERROR: Reminder {id} not found."
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 handle_reminder_delete(id: str) -> str:
61
- """Delete a reminder permanently."""
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(id: str, description: str, date: str = '', verification: str = '', reasoning: str = '', recurrence: str = '') -> str:
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(id=id, description=description, date=date or None, verification=verification, reasoning=reasoning, recurrence=recurrence or None)
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 handle_followup_update(id: str, description: str = '', date: str = '', verification: str = '', status: str = '') -> str:
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
- return f"ERROR: Followup {id} not found."
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 handle_followup_delete(id: str) -> str:
161
- """Delete a followup permanently."""
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."