ltcai 9.4.0 → 9.5.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.
Files changed (40) hide show
  1. package/README.md +50 -41
  2. package/docs/CHANGELOG.md +25 -0
  3. package/docs/COMMUNITY_AND_PLUGINS.md +2 -2
  4. package/docs/DEVELOPMENT.md +8 -8
  5. package/docs/LEGACY_COMPATIBILITY.md +1 -1
  6. package/docs/ONBOARDING.md +2 -2
  7. package/docs/OPERATIONS.md +1 -1
  8. package/docs/TRUST_MODEL.md +1 -1
  9. package/docs/WHY_LATTICE.md +1 -1
  10. package/docs/kg-schema.md +1 -1
  11. package/docs/mcp-tools.md +1 -1
  12. package/lattice_brain/__init__.py +1 -1
  13. package/lattice_brain/runtime/multi_agent.py +1 -1
  14. package/latticeai/__init__.py +1 -1
  15. package/latticeai/api/command_center.py +51 -0
  16. package/latticeai/app_factory.py +19 -0
  17. package/latticeai/core/legacy_compatibility.py +1 -1
  18. package/latticeai/core/marketplace.py +1 -1
  19. package/latticeai/core/workspace_os.py +1 -1
  20. package/latticeai/services/architecture_readiness.py +1 -1
  21. package/latticeai/services/command_center.py +433 -0
  22. package/latticeai/services/product_readiness.py +1 -1
  23. package/package.json +1 -1
  24. package/scripts/check_current_release_docs.mjs +1 -1
  25. package/src-tauri/Cargo.lock +1 -1
  26. package/src-tauri/Cargo.toml +1 -1
  27. package/src-tauri/tauri.conf.json +1 -1
  28. package/static/app/asset-manifest.json +11 -11
  29. package/static/app/assets/{Act-t9oveJO7.js → Act-Cdfqx4wN.js} +1 -1
  30. package/static/app/assets/{Brain-DP-gpcEJ.js → Brain-w_tAuyg6.js} +1 -1
  31. package/static/app/assets/{Capture-DYknDKy8.js → Capture-iJwi9LmS.js} +1 -1
  32. package/static/app/assets/{Library-DLyc_g8c.js → Library-Do-jjhzi.js} +1 -1
  33. package/static/app/assets/{System-BZgJ7tGu.js → System-DFg_q3E6.js} +1 -1
  34. package/static/app/assets/{index-Cl4S_9Id.css → index-BN6HIWVC.css} +1 -1
  35. package/static/app/assets/index-aJuRi-Xo.js +17 -0
  36. package/static/app/assets/{primitives-DawfkPR4.js → primitives-ZTUlU7pR.js} +1 -1
  37. package/static/app/assets/{textarea-a4Ir3SZS.js → textarea-CMfhqPhE.js} +1 -1
  38. package/static/app/index.html +2 -2
  39. package/static/sw.js +1 -1
  40. package/static/app/assets/index-BeQ77vPs.js +0 -17
@@ -0,0 +1,433 @@
1
+ """Command Center: daily briefing + universal command search (v9.5.0).
2
+
3
+ The Brain accumulates a lot of surfaces — knowledge, conversations,
4
+ automations, review queue, health. The Command Center condenses them into two
5
+ deterministic, local, read-only entry points:
6
+
7
+ * **briefing** — one payload answering "what does my Brain know about today?":
8
+ recent knowledge, conversation activity, automation state, pending reviews,
9
+ a health snapshot, and top automation suggestions. Each section degrades
10
+ independently — a missing backend never breaks the briefing.
11
+ * **search** — one query across every surface at once: knowledge nodes
12
+ (graph keyword search), the user's own conversations, and installed
13
+ automations. Powers the Cmd+K command palette.
14
+ * **quick_actions** — state-derived next steps ("N items waiting for review",
15
+ "enable your draft automation") with stable ids, so the UI can render
16
+ one-click jumps without guessing.
17
+
18
+ Everything here is scoped to the requesting user and workspace, mirrors the
19
+ scoped-read conventions of the automation intelligence service, and never
20
+ calls a model.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import logging
26
+ from typing import Any, Dict, List, Optional
27
+
28
+ from latticeai.core.timeutil import now_iso as _now
29
+
30
+ LOGGER = logging.getLogger(__name__)
31
+
32
+ _RECENT_NODE_LIMIT = 6
33
+ _SEARCH_HISTORY_LIMIT = 2000
34
+ _BRIEFING_HISTORY_LIMIT = 2000
35
+
36
+
37
+ def _clip(text: Any, limit: int = 160) -> str:
38
+ value = str(text or "").strip()
39
+ return value[:limit]
40
+
41
+
42
+ class CommandCenterService:
43
+ def __init__(
44
+ self,
45
+ *,
46
+ conversation_store: Any = None,
47
+ knowledge_graph: Any = None,
48
+ store: Any = None,
49
+ search_service: Any = None,
50
+ brain_intelligence: Any = None,
51
+ automation_intelligence: Any = None,
52
+ review_queue: Any = None,
53
+ enable_graph: bool = True,
54
+ ) -> None:
55
+ self._conversations = conversation_store
56
+ self._kg = knowledge_graph
57
+ self._store = store
58
+ self._search = search_service
59
+ self._brain = brain_intelligence
60
+ self._automation = automation_intelligence
61
+ self._review_queue = review_queue
62
+ self._enable_graph = bool(enable_graph and knowledge_graph is not None)
63
+
64
+ # ── scoped reads ─────────────────────────────────────────────────────
65
+
66
+ def _scope_kwargs(self, workspace_id: Optional[str]) -> Dict[str, Any]:
67
+ return {
68
+ "allowed_workspaces": {workspace_id} if workspace_id is not None else None,
69
+ "include_legacy_global": workspace_id is None,
70
+ }
71
+
72
+ def _history(
73
+ self, *, user_email: Optional[str], workspace_id: Optional[str], limit: int
74
+ ) -> List[Dict[str, Any]]:
75
+ if self._conversations is None:
76
+ return []
77
+ try:
78
+ return list(
79
+ self._conversations.history(
80
+ user_email=user_email,
81
+ limit=limit,
82
+ **self._scope_kwargs(workspace_id),
83
+ )
84
+ )
85
+ except Exception:
86
+ LOGGER.exception("command center history read failed")
87
+ return []
88
+
89
+ def _workflows(self, *, workspace_id: Optional[str]) -> List[Dict[str, Any]]:
90
+ if self._store is None:
91
+ return []
92
+ try:
93
+ return list(self._store.list_workflows(workspace_id=workspace_id).get("workflows") or [])
94
+ except Exception:
95
+ LOGGER.exception("command center workflow read failed")
96
+ return []
97
+
98
+ # ── briefing sections ────────────────────────────────────────────────
99
+
100
+ def _knowledge_section(self, *, workspace_id: Optional[str]) -> Dict[str, Any]:
101
+ if not self._enable_graph or not hasattr(self._kg, "graph"):
102
+ return {"available": False, "recent": []}
103
+ try:
104
+ snapshot = self._kg.graph(limit=50, **self._scope_kwargs(workspace_id))
105
+ except Exception:
106
+ LOGGER.exception("command center knowledge read failed")
107
+ return {"available": False, "recent": []}
108
+ nodes = list(snapshot.get("nodes") or [])
109
+ recent = [
110
+ {
111
+ "id": node.get("id"),
112
+ "title": _clip(node.get("title"), 120),
113
+ "type": node.get("type"),
114
+ "updated_at": node.get("updated_at"),
115
+ }
116
+ for node in nodes[:_RECENT_NODE_LIMIT]
117
+ ]
118
+ return {"available": True, "recent": recent, "sampled_nodes": len(nodes)}
119
+
120
+ def _conversation_section(
121
+ self, *, user_email: Optional[str], workspace_id: Optional[str]
122
+ ) -> Dict[str, Any]:
123
+ items = self._history(
124
+ user_email=user_email, workspace_id=workspace_id, limit=_BRIEFING_HISTORY_LIMIT
125
+ )
126
+ if not items:
127
+ return {"available": self._conversations is not None, "messages": 0, "questions": 0}
128
+ user_items = [item for item in items if item.get("role") == "user"]
129
+ last = items[-1]
130
+ return {
131
+ "available": True,
132
+ "messages": len(items),
133
+ "questions": len(user_items),
134
+ "last_active": str(last.get("timestamp") or ""),
135
+ "last_question": _clip(
136
+ next(
137
+ (item.get("content") for item in reversed(user_items)),
138
+ "",
139
+ ),
140
+ 120,
141
+ ),
142
+ }
143
+
144
+ def _automation_section(self, *, workspace_id: Optional[str]) -> Dict[str, Any]:
145
+ if self._store is None:
146
+ return {"available": False, "total": 0, "enabled": 0, "drafts": 0}
147
+ workflows = self._workflows(workspace_id=workspace_id)
148
+ enabled = 0
149
+ drafts = 0
150
+ for workflow in workflows:
151
+ metadata = (workflow or {}).get("metadata") or {}
152
+ if metadata.get("automation_state") == "enabled":
153
+ enabled += 1
154
+ elif metadata.get("automation_state") == "draft_disabled":
155
+ drafts += 1
156
+ return {
157
+ "available": True,
158
+ "total": len(workflows),
159
+ "enabled": enabled,
160
+ "drafts": drafts,
161
+ }
162
+
163
+ def _review_section(
164
+ self, *, user_email: Optional[str], workspace_id: Optional[str]
165
+ ) -> Dict[str, Any]:
166
+ if self._review_queue is None:
167
+ return {"available": False, "pending": 0}
168
+ try:
169
+ listed = self._review_queue.list(
170
+ workspace_id=workspace_id, user_email=user_email, status="pending"
171
+ )
172
+ except Exception:
173
+ LOGGER.exception("command center review read failed")
174
+ return {"available": False, "pending": 0}
175
+ return {"available": True, "pending": len(listed.get("items") or [])}
176
+
177
+ def _health_section(
178
+ self, *, user_email: Optional[str], workspace_id: Optional[str]
179
+ ) -> Dict[str, Any]:
180
+ if self._brain is None:
181
+ return {"available": False}
182
+ try:
183
+ report = self._brain.health_report(
184
+ user_email=user_email, workspace_id=workspace_id
185
+ )
186
+ except Exception:
187
+ LOGGER.exception("command center health read failed")
188
+ return {"available": False}
189
+ overall = report.get("overall") or {}
190
+ return {
191
+ "available": overall.get("score") is not None,
192
+ "grade": overall.get("grade"),
193
+ "score": overall.get("score"),
194
+ "recommended_actions": list(report.get("recommended_actions") or [])[:3],
195
+ }
196
+
197
+ def _suggestion_section(
198
+ self, *, user_email: Optional[str], workspace_id: Optional[str]
199
+ ) -> Dict[str, Any]:
200
+ if self._automation is None:
201
+ return {"available": False, "count": 0, "top": []}
202
+ try:
203
+ payload = self._automation.suggestions(
204
+ user_email=user_email, workspace_id=workspace_id
205
+ )
206
+ except Exception:
207
+ LOGGER.exception("command center suggestion read failed")
208
+ return {"available": False, "count": 0, "top": []}
209
+ suggestions = [
210
+ item
211
+ for item in (payload.get("suggestions") or [])
212
+ if not item.get("installed")
213
+ ]
214
+ top = [
215
+ {
216
+ "id": item.get("id"),
217
+ "kind": item.get("kind"),
218
+ "title": _clip(item.get("title"), 120),
219
+ }
220
+ for item in suggestions[:3]
221
+ ]
222
+ return {"available": True, "count": len(suggestions), "top": top}
223
+
224
+ # ── quick actions ────────────────────────────────────────────────────
225
+
226
+ def _quick_actions(self, sections: Dict[str, Dict[str, Any]]) -> List[Dict[str, Any]]:
227
+ """State-derived next steps with stable ids and app hash targets."""
228
+ actions: List[Dict[str, Any]] = []
229
+ review = sections.get("review") or {}
230
+ if review.get("pending"):
231
+ actions.append(
232
+ {
233
+ "id": "review-pending",
234
+ "kind": "review",
235
+ "count": review["pending"],
236
+ "target": "/act/review",
237
+ }
238
+ )
239
+ automations = sections.get("automations") or {}
240
+ if automations.get("drafts"):
241
+ actions.append(
242
+ {
243
+ "id": "enable-drafts",
244
+ "kind": "automation",
245
+ "count": automations["drafts"],
246
+ "target": "/act/workflows",
247
+ }
248
+ )
249
+ suggestions = sections.get("suggestions") or {}
250
+ if suggestions.get("count"):
251
+ actions.append(
252
+ {
253
+ "id": "install-suggestion",
254
+ "kind": "suggestion",
255
+ "count": suggestions["count"],
256
+ "target": "/act/workflows",
257
+ }
258
+ )
259
+ knowledge = sections.get("knowledge") or {}
260
+ if knowledge.get("available") and not knowledge.get("recent"):
261
+ actions.append(
262
+ {
263
+ "id": "connect-knowledge",
264
+ "kind": "capture",
265
+ "count": 0,
266
+ "target": "/capture/files",
267
+ }
268
+ )
269
+ health = sections.get("health") or {}
270
+ if health.get("available") and isinstance(health.get("score"), (int, float)) and health["score"] < 70:
271
+ actions.append(
272
+ {
273
+ "id": "check-health",
274
+ "kind": "health",
275
+ "count": 0,
276
+ "target": "/brain/graph",
277
+ }
278
+ )
279
+ if not actions:
280
+ actions.append(
281
+ {"id": "ask-brain", "kind": "chat", "count": 0, "target": "/brain"}
282
+ )
283
+ return actions
284
+
285
+ def briefing(
286
+ self, *, user_email: Optional[str] = None, workspace_id: Optional[str] = None
287
+ ) -> Dict[str, Any]:
288
+ sections = {
289
+ "knowledge": self._knowledge_section(workspace_id=workspace_id),
290
+ "conversations": self._conversation_section(
291
+ user_email=user_email, workspace_id=workspace_id
292
+ ),
293
+ "automations": self._automation_section(workspace_id=workspace_id),
294
+ "review": self._review_section(
295
+ user_email=user_email, workspace_id=workspace_id
296
+ ),
297
+ "health": self._health_section(
298
+ user_email=user_email, workspace_id=workspace_id
299
+ ),
300
+ "suggestions": self._suggestion_section(
301
+ user_email=user_email, workspace_id=workspace_id
302
+ ),
303
+ }
304
+ return {
305
+ "generated_at": _now(),
306
+ "sections": sections,
307
+ "quick_actions": self._quick_actions(sections),
308
+ }
309
+
310
+ # ── universal search ─────────────────────────────────────────────────
311
+
312
+ def _search_knowledge(
313
+ self, query: str, *, workspace_id: Optional[str], limit: int
314
+ ) -> List[Dict[str, Any]]:
315
+ if self._search is None or not self._enable_graph:
316
+ return []
317
+ try:
318
+ payload = self._search.keyword_search(
319
+ query, limit=limit, **self._scope_kwargs(workspace_id)
320
+ )
321
+ except Exception:
322
+ LOGGER.exception("command center knowledge search failed")
323
+ return []
324
+ return [
325
+ {
326
+ "id": item.get("id"),
327
+ "title": _clip(item.get("title"), 120),
328
+ "summary": _clip(item.get("summary"), 160),
329
+ "type": item.get("type"),
330
+ }
331
+ for item in (payload.get("results") or [])[:limit]
332
+ ]
333
+
334
+ def _search_conversations(
335
+ self,
336
+ query: str,
337
+ *,
338
+ user_email: Optional[str],
339
+ workspace_id: Optional[str],
340
+ limit: int,
341
+ ) -> List[Dict[str, Any]]:
342
+ needle = query.lower()
343
+ matches: List[Dict[str, Any]] = []
344
+ seen_conversations: set = set()
345
+ items = self._history(
346
+ user_email=user_email, workspace_id=workspace_id, limit=_SEARCH_HISTORY_LIMIT
347
+ )
348
+ for item in reversed(items):
349
+ content = str(item.get("content") or "")
350
+ if needle not in content.lower():
351
+ continue
352
+ conversation_id = str(item.get("conversation_id") or "")
353
+ if conversation_id and conversation_id in seen_conversations:
354
+ continue
355
+ if conversation_id:
356
+ seen_conversations.add(conversation_id)
357
+ matches.append(
358
+ {
359
+ "conversation_id": conversation_id,
360
+ "role": item.get("role"),
361
+ "snippet": _clip(content, 140),
362
+ "timestamp": str(item.get("timestamp") or ""),
363
+ }
364
+ )
365
+ if len(matches) >= limit:
366
+ break
367
+ return matches
368
+
369
+ def _search_automations(
370
+ self, query: str, *, workspace_id: Optional[str], limit: int
371
+ ) -> List[Dict[str, Any]]:
372
+ needle = query.lower()
373
+ matches: List[Dict[str, Any]] = []
374
+ for workflow in self._workflows(workspace_id=workspace_id):
375
+ name = str(workflow.get("name") or "")
376
+ if needle not in name.lower():
377
+ continue
378
+ metadata = (workflow or {}).get("metadata") or {}
379
+ matches.append(
380
+ {
381
+ "id": workflow.get("id"),
382
+ "name": _clip(name, 120),
383
+ "enabled": metadata.get("automation_state") == "enabled",
384
+ }
385
+ )
386
+ if len(matches) >= limit:
387
+ break
388
+ return matches
389
+
390
+ def search(
391
+ self,
392
+ query: str,
393
+ *,
394
+ user_email: Optional[str] = None,
395
+ workspace_id: Optional[str] = None,
396
+ limit: int = 8,
397
+ ) -> Dict[str, Any]:
398
+ query = str(query or "").strip()
399
+ limit = max(1, min(int(limit or 8), 20))
400
+ if not query:
401
+ return {"query": "", "groups": [], "generated_at": _now()}
402
+ groups = [
403
+ {
404
+ "kind": "knowledge",
405
+ "items": self._search_knowledge(
406
+ query, workspace_id=workspace_id, limit=limit
407
+ ),
408
+ },
409
+ {
410
+ "kind": "conversation",
411
+ "items": self._search_conversations(
412
+ query,
413
+ user_email=user_email,
414
+ workspace_id=workspace_id,
415
+ limit=limit,
416
+ ),
417
+ },
418
+ {
419
+ "kind": "automation",
420
+ "items": self._search_automations(
421
+ query, workspace_id=workspace_id, limit=limit
422
+ ),
423
+ },
424
+ ]
425
+ return {
426
+ "query": query,
427
+ "groups": [group for group in groups if group["items"]],
428
+ "total": sum(len(group["items"]) for group in groups),
429
+ "generated_at": _now(),
430
+ }
431
+
432
+
433
+ __all__ = ["CommandCenterService"]
@@ -18,7 +18,7 @@ from typing import Any, Dict, List
18
18
 
19
19
  from latticeai.services.architecture_readiness import architecture_readiness
20
20
 
21
- PRODUCT_VERSION_TARGET = "9.4.0"
21
+ PRODUCT_VERSION_TARGET = "9.5.0"
22
22
 
23
23
 
24
24
  @dataclass(frozen=True)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ltcai",
3
- "version": "9.4.0",
3
+ "version": "9.5.0",
4
4
  "description": "Lattice AI — local-first Digital Brain that keeps your knowledge durable across any AI model.",
5
5
  "homepage": "https://github.com/TaeSooPark-PTS/LatticeAI#readme",
6
6
  "repository": {
@@ -6,7 +6,7 @@ const root = process.cwd();
6
6
  const pkg = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8"));
7
7
  const version = pkg.version;
8
8
  const releaseDir = `output/release/v${version}`;
9
- const releaseTheme = "Question-Driven Everyday Automation";
9
+ const releaseTheme = "Command Center";
10
10
  const title = `${version} — ${releaseTheme}`;
11
11
  const escapedVersion = version.replaceAll(".", "\\.");
12
12
 
@@ -1584,7 +1584,7 @@ dependencies = [
1584
1584
 
1585
1585
  [[package]]
1586
1586
  name = "lattice-ai-desktop"
1587
- version = "9.4.0"
1587
+ version = "9.5.0"
1588
1588
  dependencies = [
1589
1589
  "plist",
1590
1590
  "serde",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "lattice-ai-desktop"
3
- version = "9.4.0"
3
+ version = "9.5.0"
4
4
  description = "Lattice AI Digital Brain desktop shell"
5
5
  authors = ["TaeSoo Park"]
6
6
  edition = "2021"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://schema.tauri.app/config/2",
3
3
  "productName": "Lattice AI",
4
- "version": "9.4.0",
4
+ "version": "9.5.0",
5
5
  "identifier": "ai.lattice.desktop",
6
6
  "build": {
7
7
  "beforeDevCommand": "npm run frontend:dev",
@@ -1,19 +1,19 @@
1
1
  {
2
- "version": "9.4.0",
2
+ "version": "9.5.0",
3
3
  "generated_at": "vite",
4
4
  "entrypoints": {
5
- "app": "/static/app/assets/index-BeQ77vPs.js"
5
+ "app": "/static/app/assets/index-aJuRi-Xo.js"
6
6
  },
7
7
  "assets": {
8
8
  "../node_modules/@tauri-apps/api/core.js": "/static/app/assets/core-CwxXejkd.js",
9
- "_primitives-DawfkPR4.js": "/static/app/assets/primitives-DawfkPR4.js",
10
- "_textarea-a4Ir3SZS.js": "/static/app/assets/textarea-a4Ir3SZS.js",
11
- "index.html": "/static/app/assets/index-BeQ77vPs.js",
12
- "assets/index-Cl4S_9Id.css": "/static/app/assets/index-Cl4S_9Id.css",
13
- "src/pages/Act.tsx": "/static/app/assets/Act-t9oveJO7.js",
14
- "src/pages/Brain.tsx": "/static/app/assets/Brain-DP-gpcEJ.js",
15
- "src/pages/Capture.tsx": "/static/app/assets/Capture-DYknDKy8.js",
16
- "src/pages/Library.tsx": "/static/app/assets/Library-DLyc_g8c.js",
17
- "src/pages/System.tsx": "/static/app/assets/System-BZgJ7tGu.js"
9
+ "_primitives-ZTUlU7pR.js": "/static/app/assets/primitives-ZTUlU7pR.js",
10
+ "_textarea-CMfhqPhE.js": "/static/app/assets/textarea-CMfhqPhE.js",
11
+ "index.html": "/static/app/assets/index-aJuRi-Xo.js",
12
+ "assets/index-BN6HIWVC.css": "/static/app/assets/index-BN6HIWVC.css",
13
+ "src/pages/Act.tsx": "/static/app/assets/Act-Cdfqx4wN.js",
14
+ "src/pages/Brain.tsx": "/static/app/assets/Brain-w_tAuyg6.js",
15
+ "src/pages/Capture.tsx": "/static/app/assets/Capture-iJwi9LmS.js",
16
+ "src/pages/Library.tsx": "/static/app/assets/Library-Do-jjhzi.js",
17
+ "src/pages/System.tsx": "/static/app/assets/System-DFg_q3E6.js"
18
18
  }
19
19
  }