nexo-brain 7.28.0 → 7.30.0

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.
@@ -0,0 +1,281 @@
1
+ """Morning briefing persistence and Desktop-facing accessors."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ import db as nexo_db
11
+ from paths import operations_dir
12
+
13
+
14
+ LATEST_MARKDOWN_FILE = operations_dir() / "morning-briefing-latest.md"
15
+ LATEST_HTML_FILE = operations_dir() / "morning-briefing-latest.html"
16
+ LATEST_JSON_FILE = operations_dir() / "morning-briefing-latest.json"
17
+
18
+ PRESENTATION_COLUMNS = {
19
+ "body_text": "TEXT DEFAULT ''",
20
+ "body_html": "TEXT DEFAULT ''",
21
+ "artifact_json": "TEXT DEFAULT ''",
22
+ "desktop_shown_at": "TEXT DEFAULT NULL",
23
+ "desktop_opened_at": "TEXT DEFAULT NULL",
24
+ "desktop_dismissed_at": "TEXT DEFAULT NULL",
25
+ }
26
+
27
+
28
+ def _now() -> str:
29
+ return datetime.now().astimezone().isoformat()
30
+
31
+
32
+ def _conn():
33
+ nexo_db.init_db()
34
+ return nexo_db.get_db()
35
+
36
+
37
+ def ensure_morning_briefing_runs_table(conn=None) -> None:
38
+ conn = conn or _conn()
39
+ conn.execute(
40
+ """CREATE TABLE IF NOT EXISTS morning_briefing_runs (
41
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
42
+ local_date TEXT NOT NULL,
43
+ recipient TEXT NOT NULL,
44
+ status TEXT NOT NULL DEFAULT 'in_progress',
45
+ subject TEXT DEFAULT '',
46
+ body_text TEXT DEFAULT '',
47
+ body_html TEXT DEFAULT '',
48
+ artifact_json TEXT DEFAULT '',
49
+ send_output TEXT DEFAULT '',
50
+ error TEXT DEFAULT '',
51
+ desktop_shown_at TEXT DEFAULT NULL,
52
+ desktop_opened_at TEXT DEFAULT NULL,
53
+ desktop_dismissed_at TEXT DEFAULT NULL,
54
+ started_at TEXT DEFAULT (datetime('now')),
55
+ finished_at TEXT DEFAULT NULL,
56
+ updated_at TEXT DEFAULT (datetime('now')),
57
+ UNIQUE(local_date, recipient)
58
+ )"""
59
+ )
60
+ existing = {
61
+ str(row[1])
62
+ for row in conn.execute("PRAGMA table_info(morning_briefing_runs)").fetchall()
63
+ }
64
+ for name, ddl in PRESENTATION_COLUMNS.items():
65
+ if name not in existing:
66
+ conn.execute(f"ALTER TABLE morning_briefing_runs ADD COLUMN {name} {ddl}")
67
+ conn.execute(
68
+ "CREATE INDEX IF NOT EXISTS idx_morning_briefing_runs_date "
69
+ "ON morning_briefing_runs(local_date)"
70
+ )
71
+ conn.execute(
72
+ "CREATE INDEX IF NOT EXISTS idx_morning_briefing_runs_status "
73
+ "ON morning_briefing_runs(status)"
74
+ )
75
+ conn.execute(
76
+ "CREATE INDEX IF NOT EXISTS idx_morning_briefing_runs_desktop "
77
+ "ON morning_briefing_runs(status, desktop_shown_at, finished_at)"
78
+ )
79
+
80
+
81
+ def _row_to_dict(row) -> dict[str, Any]:
82
+ if row is None:
83
+ return {}
84
+ try:
85
+ return dict(row)
86
+ except Exception:
87
+ return {}
88
+
89
+
90
+ def _artifact_paths() -> dict[str, str]:
91
+ return {
92
+ "markdown": str(LATEST_MARKDOWN_FILE),
93
+ "html": str(LATEST_HTML_FILE),
94
+ "json": str(LATEST_JSON_FILE),
95
+ }
96
+
97
+
98
+ def write_latest_briefing_artifacts(
99
+ *,
100
+ recipient: str,
101
+ subject: str,
102
+ body_text: str,
103
+ body_html: str,
104
+ local_date: str = "",
105
+ run_id: int | None = None,
106
+ ) -> dict[str, Any]:
107
+ generated_at = _now()
108
+ LATEST_MARKDOWN_FILE.parent.mkdir(parents=True, exist_ok=True)
109
+ payload = {
110
+ "schema": "nexo.morning_briefing.v1",
111
+ "generated_at": generated_at,
112
+ "local_date": local_date,
113
+ "run_id": run_id,
114
+ "recipient": recipient,
115
+ "subject": subject,
116
+ "body_text": body_text,
117
+ "body_html": body_html,
118
+ "artifacts": _artifact_paths(),
119
+ }
120
+ markdown = (
121
+ "# Morning briefing\n\n"
122
+ f"- Generated at: {generated_at}\n"
123
+ f"- To: {recipient}\n"
124
+ f"- Subject: {subject}\n\n"
125
+ f"{body_text}\n"
126
+ )
127
+ LATEST_MARKDOWN_FILE.write_text(markdown, encoding="utf-8")
128
+ LATEST_HTML_FILE.write_text(body_html, encoding="utf-8")
129
+ LATEST_JSON_FILE.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
130
+ return payload
131
+
132
+
133
+ def mark_morning_briefing_sent(
134
+ *,
135
+ local_date: str,
136
+ recipient: str,
137
+ subject: str,
138
+ body_text: str,
139
+ body_html: str,
140
+ send_output: str = "",
141
+ artifact_payload: dict[str, Any] | None = None,
142
+ ) -> dict[str, Any]:
143
+ conn = _conn()
144
+ ensure_morning_briefing_runs_table(conn)
145
+ now = _now()
146
+ artifact_json = json.dumps(artifact_payload or {}, ensure_ascii=False)
147
+ conn.execute(
148
+ """
149
+ UPDATE morning_briefing_runs
150
+ SET status = 'sent',
151
+ subject = ?,
152
+ body_text = ?,
153
+ body_html = ?,
154
+ artifact_json = ?,
155
+ send_output = ?,
156
+ error = '',
157
+ finished_at = ?,
158
+ updated_at = ?
159
+ WHERE local_date = ? AND recipient = ?
160
+ """,
161
+ (
162
+ str(subject or ""),
163
+ str(body_text or ""),
164
+ str(body_html or ""),
165
+ artifact_json,
166
+ str(send_output or ""),
167
+ now,
168
+ now,
169
+ str(local_date or ""),
170
+ str(recipient or ""),
171
+ ),
172
+ )
173
+ conn.commit()
174
+ return latest_morning_briefing()
175
+
176
+
177
+ def latest_morning_briefing(*, include_non_sent: bool = False) -> dict[str, Any]:
178
+ conn = _conn()
179
+ ensure_morning_briefing_runs_table(conn)
180
+ if include_non_sent:
181
+ row = conn.execute(
182
+ """
183
+ SELECT * FROM morning_briefing_runs
184
+ ORDER BY COALESCE(finished_at, updated_at, started_at) DESC, id DESC
185
+ LIMIT 1
186
+ """
187
+ ).fetchone()
188
+ else:
189
+ row = conn.execute(
190
+ """
191
+ SELECT * FROM morning_briefing_runs
192
+ WHERE status = 'sent'
193
+ ORDER BY COALESCE(finished_at, updated_at, started_at) DESC, id DESC
194
+ LIMIT 1
195
+ """
196
+ ).fetchone()
197
+ payload = public_briefing_payload(_row_to_dict(row))
198
+ return {"ok": True, "briefing": payload}
199
+
200
+
201
+ def public_briefing_payload(row: dict[str, Any]) -> dict[str, Any] | None:
202
+ if not row:
203
+ return None
204
+ artifact_payload: dict[str, Any] = {}
205
+ try:
206
+ parsed = json.loads(row.get("artifact_json") or "{}")
207
+ if isinstance(parsed, dict):
208
+ artifact_payload = parsed
209
+ except Exception:
210
+ artifact_payload = {}
211
+ return {
212
+ "id": row.get("id"),
213
+ "local_date": row.get("local_date") or "",
214
+ "recipient": row.get("recipient") or "",
215
+ "status": row.get("status") or "",
216
+ "subject": row.get("subject") or "",
217
+ "body_text": row.get("body_text") or "",
218
+ "body_html": row.get("body_html") or "",
219
+ "send_output": row.get("send_output") or "",
220
+ "error": row.get("error") or "",
221
+ "started_at": row.get("started_at") or "",
222
+ "finished_at": row.get("finished_at") or "",
223
+ "updated_at": row.get("updated_at") or "",
224
+ "desktop_shown_at": row.get("desktop_shown_at") or "",
225
+ "desktop_opened_at": row.get("desktop_opened_at") or "",
226
+ "desktop_dismissed_at": row.get("desktop_dismissed_at") or "",
227
+ "unseen": not bool(row.get("desktop_shown_at")),
228
+ "artifacts": artifact_payload.get("artifacts") or _artifact_paths(),
229
+ "schema": "nexo.morning_briefing.v1",
230
+ }
231
+
232
+
233
+ def mark_desktop_state(action: str, *, briefing_id: int | None = None) -> dict[str, Any]:
234
+ field_by_action = {
235
+ "shown": "desktop_shown_at",
236
+ "opened": "desktop_opened_at",
237
+ "dismissed": "desktop_dismissed_at",
238
+ }
239
+ field = field_by_action.get(str(action or "").strip().lower())
240
+ if not field:
241
+ return {"ok": False, "error": f"Unknown briefing mark action: {action}"}
242
+ conn = _conn()
243
+ ensure_morning_briefing_runs_table(conn)
244
+ if briefing_id:
245
+ row = _row_to_dict(conn.execute(
246
+ "SELECT * FROM morning_briefing_runs WHERE id = ? LIMIT 1",
247
+ (int(briefing_id),),
248
+ ).fetchone())
249
+ else:
250
+ row = (latest_morning_briefing().get("briefing") or {})
251
+ if row:
252
+ row = _row_to_dict(conn.execute(
253
+ "SELECT * FROM morning_briefing_runs WHERE id = ? LIMIT 1",
254
+ (int(row.get("id") or 0),),
255
+ ).fetchone())
256
+ if not row:
257
+ return {"ok": False, "error": "No morning briefing found."}
258
+ now = _now()
259
+ conn.execute(
260
+ f"UPDATE morning_briefing_runs SET {field} = ?, updated_at = ? WHERE id = ?",
261
+ (now, now, int(row.get("id"))),
262
+ )
263
+ conn.commit()
264
+ updated = _row_to_dict(conn.execute(
265
+ "SELECT * FROM morning_briefing_runs WHERE id = ? LIMIT 1",
266
+ (int(row.get("id")),),
267
+ ).fetchone())
268
+ return {"ok": True, "briefing": public_briefing_payload(updated), "marked": action}
269
+
270
+
271
+ __all__ = [
272
+ "LATEST_HTML_FILE",
273
+ "LATEST_JSON_FILE",
274
+ "LATEST_MARKDOWN_FILE",
275
+ "ensure_morning_briefing_runs_table",
276
+ "latest_morning_briefing",
277
+ "mark_desktop_state",
278
+ "mark_morning_briefing_sent",
279
+ "public_briefing_payload",
280
+ "write_latest_briefing_artifacts",
281
+ ]
@@ -0,0 +1,63 @@
1
+ """Agent-facing catalog and mutation tools for NEXO Desktop preferences."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+
7
+
8
+ def handle_desktop_preferences_catalog(query: str = "", include_values: bool = True, locale: str = "es") -> str:
9
+ from preference_catalog import build_preference_catalog
10
+
11
+ return json.dumps(
12
+ build_preference_catalog(
13
+ include_values=bool(include_values),
14
+ query=str(query or "").strip() or None,
15
+ locale=str(locale or "es"),
16
+ ),
17
+ ensure_ascii=False,
18
+ )
19
+
20
+
21
+ def handle_desktop_preference_get(id: str) -> str:
22
+ from preference_catalog import explain_preference
23
+
24
+ return json.dumps(explain_preference(id), ensure_ascii=False)
25
+
26
+
27
+ def handle_desktop_preference_explain(id: str) -> str:
28
+ from preference_catalog import explain_preference
29
+
30
+ return json.dumps(explain_preference(id), ensure_ascii=False)
31
+
32
+
33
+ def handle_desktop_preference_set(id: str, value: str, dry_run: bool = False) -> str:
34
+ from preference_catalog import set_preference
35
+
36
+ return json.dumps(
37
+ set_preference(id, value, dry_run=bool(dry_run)),
38
+ ensure_ascii=False,
39
+ )
40
+
41
+
42
+ TOOLS = [
43
+ (
44
+ handle_desktop_preferences_catalog,
45
+ "nexo_desktop_preferences_catalog",
46
+ "List the settings and automation preferences NEXO can explain or change for the operator.",
47
+ ),
48
+ (
49
+ handle_desktop_preference_get,
50
+ "nexo_desktop_preference_get",
51
+ "Read one preference by id or alias, including its current value when available.",
52
+ ),
53
+ (
54
+ handle_desktop_preference_explain,
55
+ "nexo_desktop_preference_explain",
56
+ "Explain what one Desktop/Brain preference means and where it is stored.",
57
+ ),
58
+ (
59
+ handle_desktop_preference_set,
60
+ "nexo_desktop_preference_set",
61
+ "Change one supported preference by id or alias. Use dry_run=true to preview.",
62
+ ),
63
+ ]
@@ -253,6 +253,7 @@ def handle_automation_schedule(
253
253
  name: str,
254
254
  every_seconds: int = 0,
255
255
  daily_at: str = "",
256
+ weekdays: str = "",
256
257
  clear: bool = False,
257
258
  ) -> str:
258
259
  init_db()
@@ -262,6 +263,7 @@ def handle_automation_schedule(
262
263
  name,
263
264
  interval_seconds=interval_seconds,
264
265
  daily_at=str(daily_at or "").strip() or None,
266
+ weekdays=str(weekdays or "").strip() or None,
265
267
  clear=bool(clear),
266
268
  ),
267
269
  ensure_ascii=False,
@@ -1829,9 +1829,13 @@ def handle_update(
1829
1829
  from client_sync import sync_all_clients
1830
1830
  from client_preferences import normalize_client_preferences
1831
1831
  from model_defaults import heal_runtime_profiles
1832
+ from auto_update import _refresh_resonance_tiers_model_defaults
1832
1833
 
1833
1834
  schedule_path = paths.config_dir() / "schedule.json"
1834
1835
  schedule_payload = json.loads(schedule_path.read_text()) if schedule_path.exists() else {}
1836
+ for action in _refresh_resonance_tiers_model_defaults(NEXO_HOME):
1837
+ _emit_progress(progress_fn, action)
1838
+ steps_done.append("resonance-default-refresh")
1835
1839
  # Heal Claude-family models written into Codex profile by earlier
1836
1840
  # buggy versions. Must run BEFORE normalize so healed values
1837
1841
  # propagate into the saved preferences.