ltcai 6.5.0 → 6.7.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 (56) hide show
  1. package/README.md +35 -35
  2. package/docs/CHANGELOG.md +59 -2
  3. package/frontend/openapi.json +65 -1
  4. package/frontend/src/App.tsx +78 -3
  5. package/frontend/src/api/client.ts +1 -0
  6. package/frontend/src/api/openapi.ts +86 -0
  7. package/frontend/src/features/brain/BrainComposer.tsx +20 -1
  8. package/frontend/src/features/brain/BrainConversation.tsx +57 -4
  9. package/frontend/src/features/brain/BrainHome.tsx +41 -1
  10. package/frontend/src/features/brain/BrainOverviewPanel.tsx +62 -1
  11. package/frontend/src/features/brain/brainData.ts +54 -1
  12. package/frontend/src/features/brain/types.ts +37 -0
  13. package/frontend/src/i18n.ts +54 -0
  14. package/frontend/src/routes.ts +31 -17
  15. package/frontend/src/styles.css +216 -0
  16. package/lattice_brain/__init__.py +1 -1
  17. package/lattice_brain/graph/ingest.py +41 -6
  18. package/lattice_brain/ingestion.py +1 -0
  19. package/lattice_brain/runtime/multi_agent.py +1 -1
  20. package/latticeai/__init__.py +1 -1
  21. package/latticeai/api/knowledge_graph.py +11 -0
  22. package/latticeai/api/memory.py +14 -0
  23. package/latticeai/app_factory.py +2 -0
  24. package/latticeai/core/marketplace.py +1 -1
  25. package/latticeai/core/workspace_os.py +1 -1
  26. package/latticeai/runtime/chat_wiring.py +2 -0
  27. package/latticeai/runtime/router_registration.py +3 -0
  28. package/latticeai/services/memory_service.py +122 -2
  29. package/latticeai/services/router_context.py +1 -0
  30. package/latticeai/services/upload_service.py +12 -0
  31. package/package.json +1 -1
  32. package/src-tauri/Cargo.lock +1 -1
  33. package/src-tauri/Cargo.toml +1 -1
  34. package/src-tauri/tauri.conf.json +1 -1
  35. package/static/app/asset-manifest.json +84 -6
  36. package/static/app/assets/Act-DGN37eR4.js +2 -0
  37. package/static/app/assets/Act-DGN37eR4.js.map +1 -0
  38. package/static/app/assets/Brain-HDcIwPHW.js +322 -0
  39. package/static/app/assets/Brain-HDcIwPHW.js.map +1 -0
  40. package/static/app/assets/Capture--eZfb_Ex.js +2 -0
  41. package/static/app/assets/Capture--eZfb_Ex.js.map +1 -0
  42. package/static/app/assets/Library-BbagRFyd.js +2 -0
  43. package/static/app/assets/Library-BbagRFyd.js.map +1 -0
  44. package/static/app/assets/System-C6pd3Mp_.js +2 -0
  45. package/static/app/assets/System-C6pd3Mp_.js.map +1 -0
  46. package/static/app/assets/index-C950aES_.js +17 -0
  47. package/static/app/assets/index-C950aES_.js.map +1 -0
  48. package/static/app/assets/index-ztppMg0c.css +2 -0
  49. package/static/app/assets/primitives-Bcm5lZRu.js +2 -0
  50. package/static/app/assets/primitives-Bcm5lZRu.js.map +1 -0
  51. package/static/app/assets/textarea-C2PLu6_O.js +2 -0
  52. package/static/app/assets/textarea-C2PLu6_O.js.map +1 -0
  53. package/static/app/index.html +2 -2
  54. package/static/app/assets/index-C1J_1441.js +0 -16
  55. package/static/app/assets/index-C1J_1441.js.map +0 -1
  56. package/static/app/assets/index-CdCVz_i4.css +0 -2
@@ -115,6 +115,26 @@ class MemoryService:
115
115
  return data
116
116
  return []
117
117
 
118
+ def _scoped_conversations(self, *, user_email: Optional[str], workspace_id: Optional[str]) -> List[Dict[str, Any]]:
119
+ if not user_email:
120
+ return self._conversations()
121
+ target_workspace = workspace_id or "personal"
122
+ scoped: List[Dict[str, Any]] = []
123
+ for conversation in self._conversations():
124
+ messages = conversation.get("messages") or []
125
+ if not isinstance(messages, list):
126
+ continue
127
+ kept = [
128
+ message
129
+ for message in messages
130
+ if isinstance(message, dict)
131
+ and message.get("user_email") == user_email
132
+ and (message.get("workspace_id") or "personal") == target_workspace
133
+ ]
134
+ if kept:
135
+ scoped.append({**conversation, "messages": kept})
136
+ return scoped
137
+
118
138
  def _kg_stats(self) -> Optional[Dict[str, Any]]:
119
139
  if not self._enable_graph:
120
140
  return None
@@ -193,7 +213,7 @@ class MemoryService:
193
213
  else:
194
214
  project_mem = self._workspace_memories(user_email=user_email, workspace_id=workspace_id)
195
215
  snaps = self._snapshots(workspace_id=workspace_id)
196
- convs = self._conversations()
216
+ convs = self._scoped_conversations(user_email=user_email, workspace_id=workspace_id)
197
217
  kg_stats = self._kg_stats()
198
218
  kg_index = self._kg_index()
199
219
 
@@ -273,6 +293,106 @@ class MemoryService:
273
293
  """Return the backend-owned Brain readiness signal for API consumers."""
274
294
  return self.manager(user_email=user_email, workspace_id=workspace_id)["brain_readiness"]
275
295
 
296
+ def brain_proof(
297
+ self,
298
+ *,
299
+ user_email: Optional[str] = None,
300
+ workspace_id: Optional[str] = None,
301
+ active_model: Optional[str] = None,
302
+ recall_query: str = "",
303
+ limit: int = 3,
304
+ ) -> Dict[str, Any]:
305
+ """Return the proof points that make Brain feel model-independent.
306
+
307
+ This is intentionally backend-owned instead of UI-derived: the first-run
308
+ Brain screen needs to show the durable stores it can actually recall
309
+ from, and the model continuity claim must be independent from whichever
310
+ LLM is currently loaded.
311
+ """
312
+ manager = self.manager(user_email=user_email, workspace_id=workspace_id)
313
+ sources = {str(source.get("id")): source for source in manager.get("sources", []) if isinstance(source, dict)}
314
+ readiness = manager.get("brain_readiness") or {}
315
+ conversation_count = int(sources.get("conversation", {}).get("count") or 0)
316
+ workspace_count = int(sources.get("workspace", {}).get("count") or 0)
317
+ graph_count = int(sources.get("graph", {}).get("count") or 0)
318
+ vector_count = int(sources.get("vector", {}).get("count") or 0)
319
+ query = recall_query.strip() or self._latest_recall_query(user_email=user_email, workspace_id=workspace_id)
320
+ recall = self.recall(query, user_email=user_email, workspace_id=workspace_id, limit=limit) if query else {"query": "", "results": [], "count": 0, "source": "live"}
321
+ recall_items = [
322
+ {
323
+ "id": item.get("id"),
324
+ "source": item.get("source"),
325
+ "title": item.get("title"),
326
+ "snippet": item.get("snippet"),
327
+ "score": item.get("score", 0),
328
+ }
329
+ for item in recall.get("results", [])[: max(1, min(limit, 8))]
330
+ ]
331
+ durable_items = workspace_count + conversation_count + graph_count
332
+ # Capability and proof are deliberately separated. The brain is
333
+ # architecturally model-independent, so the capability is always true.
334
+ # The proof stays false until there is durable evidence on disk.
335
+ has_durable_evidence = durable_items > 0
336
+ proven_continuity = has_durable_evidence
337
+ return {
338
+ "status": readiness.get("state") or "quiet",
339
+ "readiness": readiness,
340
+ "model_continuity": {
341
+ "active_model": active_model or "",
342
+ "brain_owner": "lattice_brain",
343
+ # Design capability: the brain is built to outlive any model.
344
+ "capability": True,
345
+ # Proof: only true when durable evidence exists to recall.
346
+ "survives_model_switch": proven_continuity,
347
+ "proven": proven_continuity,
348
+ "context_store": "workspace + conversation + graph + vector",
349
+ },
350
+ "proofs": {
351
+ "durable_items": durable_items,
352
+ "has_durable_evidence": has_durable_evidence,
353
+ "workspace_memories": workspace_count,
354
+ "conversations": conversation_count,
355
+ "graph_concepts": graph_count,
356
+ "vector_items": vector_count,
357
+ "healthy_sources": readiness.get("signals", {}).get("healthy_sources", 0),
358
+ },
359
+ "recall": {
360
+ "query": recall.get("query") or query,
361
+ "items": recall_items,
362
+ "count": recall.get("count", 0),
363
+ },
364
+ "claims": {
365
+ "can_recall_user_context": bool(recall_items or durable_items > 0),
366
+ # Proven, not asserted: no durable evidence means no continuity claim.
367
+ "keeps_context_across_models": proven_continuity,
368
+ "is_knowledge_store": bool(graph_count or vector_count or workspace_count or conversation_count),
369
+ },
370
+ "generated_at": _now(),
371
+ }
372
+
373
+ def _latest_recall_query(self, *, user_email: Optional[str], workspace_id: Optional[str]) -> str:
374
+ for memory in self._workspace_memories(user_email=user_email, workspace_id=workspace_id or "personal"):
375
+ content = str(memory.get("content") or "").strip()
376
+ if content:
377
+ return content[:96]
378
+ for conversation in self._conversations():
379
+ messages = conversation.get("messages") or []
380
+ if not isinstance(messages, list):
381
+ continue
382
+ for message in reversed(messages[-8:]):
383
+ if not isinstance(message, dict):
384
+ continue
385
+ if user_email and message.get("user_email") != user_email:
386
+ continue
387
+ message_workspace = message.get("workspace_id") or "personal"
388
+ target_workspace = workspace_id or "personal"
389
+ if message_workspace != target_workspace:
390
+ continue
391
+ content = str(message.get("content") or "").strip()
392
+ if content:
393
+ return content[:96]
394
+ return ""
395
+
276
396
  def tiers(self) -> Dict[str, Any]:
277
397
  return {"tiers": list(TIERS), "workspace_kinds": list(WORKSPACE_KINDS)}
278
398
 
@@ -349,7 +469,7 @@ class MemoryService:
349
469
  items = self._snapshots(workspace_id=workspace_id)[:limit]
350
470
  return {"source": source, "items": items, "count": len(items)}
351
471
  if source == "conversation":
352
- convs = self._conversations()
472
+ convs = self._scoped_conversations(user_email=user_email, workspace_id=workspace_id)
353
473
  items = [{"id": c.get("id"), "title": c.get("title") or c.get("id"), "messages": len(c.get("messages") or [])} for c in convs[:limit]]
354
474
  return {"source": source, "items": items, "count": len(convs)}
355
475
  if source == "graph":
@@ -54,3 +54,4 @@ class InteractionRouterContext:
54
54
  agent_registry: Any
55
55
  memory_service: Any
56
56
  platform: Any
57
+ active_model_getter: Any = None
@@ -6,6 +6,7 @@ import logging
6
6
  import tempfile
7
7
  from datetime import datetime
8
8
  from pathlib import Path
9
+ from typing import Optional
9
10
 
10
11
  from fastapi import HTTPException, Request, UploadFile
11
12
 
@@ -13,6 +14,14 @@ from lattice_brain.ingestion import IngestionItem
13
14
  from tools import ToolError, read_document
14
15
 
15
16
 
17
+ def _workspace_scope_from_request(request: Request) -> Optional[str]:
18
+ header = request.headers.get("X-Workspace-Id")
19
+ if header and header.strip():
20
+ return header.strip()
21
+ query = request.query_params.get("workspace_id")
22
+ return query.strip() if query and query.strip() else None
23
+
24
+
16
25
  async def process_uploaded_document(
17
26
  *,
18
27
  request: Request,
@@ -28,6 +37,7 @@ async def process_uploaded_document(
28
37
  hooks=None,
29
38
  ) -> dict:
30
39
  enforce_rate_limit(current_user, "upload")
40
+ workspace_id = _workspace_scope_from_request(request)
31
41
  suffix = Path(file.filename or "upload").suffix.lower()
32
42
  allowed = {".pdf", ".docx", ".xlsx", ".pptx", ".txt", ".md", ".csv"}
33
43
  if suffix not in allowed:
@@ -83,6 +93,7 @@ async def process_uploaded_document(
83
93
  path=tmp_path,
84
94
  mime_type=file.content_type,
85
95
  owner=current_user,
96
+ workspace_id=workspace_id,
86
97
  conversation_id=request.query_params.get("conversation_id"),
87
98
  metadata={"extracted": result},
88
99
  ),
@@ -101,6 +112,7 @@ async def process_uploaded_document(
101
112
  original_filename=file.filename,
102
113
  mime_type=file.content_type,
103
114
  uploader=current_user,
115
+ workspace_id=workspace_id,
104
116
  conversation_id=request.query_params.get("conversation_id"),
105
117
  extracted=result,
106
118
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ltcai",
3
- "version": "6.5.0",
3
+ "version": "6.7.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": {
@@ -1654,7 +1654,7 @@ dependencies = [
1654
1654
 
1655
1655
  [[package]]
1656
1656
  name = "lattice-ai-desktop"
1657
- version = "6.5.0"
1657
+ version = "6.7.0"
1658
1658
  dependencies = [
1659
1659
  "plist",
1660
1660
  "serde",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "lattice-ai-desktop"
3
- version = "6.5.0"
3
+ version = "6.7.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": "6.5.0",
4
+ "version": "6.7.0",
5
5
  "identifier": "ai.lattice.desktop",
6
6
  "build": {
7
7
  "beforeDevCommand": "npm run frontend:dev",
@@ -1,13 +1,20 @@
1
1
  {
2
- "version": "6.5.0",
2
+ "version": "6.7.0",
3
3
  "generated_at": "vite",
4
4
  "entrypoints": {
5
5
  "app": "/static/app/index.html"
6
6
  },
7
7
  "assets": {
8
8
  "../node_modules/@tauri-apps/api/core.js": "/static/app/assets/core-CwxXejkd.js",
9
- "index.html": "/static/app/assets/index-C1J_1441.js",
10
- "assets/index-CdCVz_i4.css": "/static/app/assets/index-CdCVz_i4.css"
9
+ "_primitives-Bcm5lZRu.js": "/static/app/assets/primitives-Bcm5lZRu.js",
10
+ "_textarea-C2PLu6_O.js": "/static/app/assets/textarea-C2PLu6_O.js",
11
+ "index.html": "/static/app/assets/index-C950aES_.js",
12
+ "assets/index-ztppMg0c.css": "/static/app/assets/index-ztppMg0c.css",
13
+ "src/pages/Act.tsx": "/static/app/assets/Act-DGN37eR4.js",
14
+ "src/pages/Brain.tsx": "/static/app/assets/Brain-HDcIwPHW.js",
15
+ "src/pages/Capture.tsx": "/static/app/assets/Capture--eZfb_Ex.js",
16
+ "src/pages/Library.tsx": "/static/app/assets/Library-BbagRFyd.js",
17
+ "src/pages/System.tsx": "/static/app/assets/System-C6pd3Mp_.js"
11
18
  },
12
19
  "vite": {
13
20
  "../node_modules/@tauri-apps/api/core.js": {
@@ -16,16 +23,87 @@
16
23
  "src": "../node_modules/@tauri-apps/api/core.js",
17
24
  "isDynamicEntry": true
18
25
  },
26
+ "_primitives-Bcm5lZRu.js": {
27
+ "file": "assets/primitives-Bcm5lZRu.js",
28
+ "name": "primitives",
29
+ "imports": [
30
+ "index.html"
31
+ ]
32
+ },
33
+ "_textarea-C2PLu6_O.js": {
34
+ "file": "assets/textarea-C2PLu6_O.js",
35
+ "name": "textarea",
36
+ "imports": [
37
+ "index.html"
38
+ ]
39
+ },
19
40
  "index.html": {
20
- "file": "assets/index-C1J_1441.js",
41
+ "file": "assets/index-C950aES_.js",
21
42
  "name": "index",
22
43
  "src": "index.html",
23
44
  "isEntry": true,
24
45
  "dynamicImports": [
25
- "../node_modules/@tauri-apps/api/core.js"
46
+ "../node_modules/@tauri-apps/api/core.js",
47
+ "src/pages/Act.tsx",
48
+ "src/pages/Brain.tsx",
49
+ "src/pages/Capture.tsx",
50
+ "src/pages/Library.tsx",
51
+ "src/pages/System.tsx"
26
52
  ],
27
53
  "css": [
28
- "assets/index-CdCVz_i4.css"
54
+ "assets/index-ztppMg0c.css"
55
+ ]
56
+ },
57
+ "src/pages/Act.tsx": {
58
+ "file": "assets/Act-DGN37eR4.js",
59
+ "name": "Act",
60
+ "src": "src/pages/Act.tsx",
61
+ "isDynamicEntry": true,
62
+ "imports": [
63
+ "index.html",
64
+ "_primitives-Bcm5lZRu.js",
65
+ "_textarea-C2PLu6_O.js"
66
+ ]
67
+ },
68
+ "src/pages/Brain.tsx": {
69
+ "file": "assets/Brain-HDcIwPHW.js",
70
+ "name": "Brain",
71
+ "src": "src/pages/Brain.tsx",
72
+ "isDynamicEntry": true,
73
+ "imports": [
74
+ "index.html",
75
+ "_primitives-Bcm5lZRu.js",
76
+ "_textarea-C2PLu6_O.js"
77
+ ]
78
+ },
79
+ "src/pages/Capture.tsx": {
80
+ "file": "assets/Capture--eZfb_Ex.js",
81
+ "name": "Capture",
82
+ "src": "src/pages/Capture.tsx",
83
+ "isDynamicEntry": true,
84
+ "imports": [
85
+ "index.html",
86
+ "_primitives-Bcm5lZRu.js"
87
+ ]
88
+ },
89
+ "src/pages/Library.tsx": {
90
+ "file": "assets/Library-BbagRFyd.js",
91
+ "name": "Library",
92
+ "src": "src/pages/Library.tsx",
93
+ "isDynamicEntry": true,
94
+ "imports": [
95
+ "index.html",
96
+ "_primitives-Bcm5lZRu.js"
97
+ ]
98
+ },
99
+ "src/pages/System.tsx": {
100
+ "file": "assets/System-C6pd3Mp_.js",
101
+ "name": "System",
102
+ "src": "src/pages/System.tsx",
103
+ "isDynamicEntry": true,
104
+ "imports": [
105
+ "index.html",
106
+ "_primitives-Bcm5lZRu.js"
29
107
  ]
30
108
  }
31
109
  }