ltcai 6.5.0 → 6.6.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 (39) hide show
  1. package/README.md +22 -20
  2. package/docs/CHANGELOG.md +34 -0
  3. package/frontend/openapi.json +65 -1
  4. package/frontend/src/api/client.ts +1 -0
  5. package/frontend/src/api/openapi.ts +86 -0
  6. package/frontend/src/features/brain/BrainComposer.tsx +20 -1
  7. package/frontend/src/features/brain/BrainConversation.tsx +37 -2
  8. package/frontend/src/features/brain/BrainHome.tsx +41 -1
  9. package/frontend/src/features/brain/BrainOverviewPanel.tsx +62 -1
  10. package/frontend/src/features/brain/brainData.ts +54 -1
  11. package/frontend/src/features/brain/types.ts +37 -0
  12. package/frontend/src/i18n.ts +44 -0
  13. package/frontend/src/styles.css +133 -0
  14. package/lattice_brain/__init__.py +1 -1
  15. package/lattice_brain/graph/ingest.py +41 -6
  16. package/lattice_brain/ingestion.py +1 -0
  17. package/lattice_brain/runtime/multi_agent.py +1 -1
  18. package/latticeai/__init__.py +1 -1
  19. package/latticeai/api/knowledge_graph.py +11 -0
  20. package/latticeai/api/memory.py +14 -0
  21. package/latticeai/app_factory.py +2 -0
  22. package/latticeai/core/marketplace.py +1 -1
  23. package/latticeai/core/workspace_os.py +1 -1
  24. package/latticeai/runtime/chat_wiring.py +2 -0
  25. package/latticeai/runtime/router_registration.py +3 -0
  26. package/latticeai/services/memory_service.py +122 -2
  27. package/latticeai/services/router_context.py +1 -0
  28. package/latticeai/services/upload_service.py +12 -0
  29. package/package.json +1 -1
  30. package/src-tauri/Cargo.lock +1 -1
  31. package/src-tauri/Cargo.toml +1 -1
  32. package/src-tauri/tauri.conf.json +1 -1
  33. package/static/app/asset-manifest.json +5 -5
  34. package/static/app/assets/{index-CdCVz_i4.css → index-C8toxDpv.css} +1 -1
  35. package/static/app/assets/index-CbvcAQ6B.js +16 -0
  36. package/static/app/assets/index-CbvcAQ6B.js.map +1 -0
  37. package/static/app/index.html +2 -2
  38. package/static/app/assets/index-C1J_1441.js +0 -16
  39. package/static/app/assets/index-C1J_1441.js.map +0 -1
@@ -451,6 +451,7 @@ def register_interaction_routers(
451
451
  create_memory_router: Any,
452
452
  memory_service: Any = None,
453
453
  platform: Any = None,
454
+ active_model_getter: Any = None,
454
455
  ) -> tuple[Any, ...]:
455
456
  """Register chat/search/tools/hooks/registry/memory routes in order."""
456
457
 
@@ -468,6 +469,7 @@ def register_interaction_routers(
468
469
  agent_registry = interaction_context.agent_registry
469
470
  memory_service = interaction_context.memory_service
470
471
  platform = interaction_context.platform
472
+ active_model_getter = interaction_context.active_model_getter
471
473
 
472
474
  return register_routers(
473
475
  app,
@@ -521,6 +523,7 @@ def register_interaction_routers(
521
523
  gate_read=platform.gate_read,
522
524
  gate_write=platform.gate_write,
523
525
  append_audit_event=append_audit_event,
526
+ active_model_getter=active_model_getter,
524
527
  ),
525
528
  )
526
529
 
@@ -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.6.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.6.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.6.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.6.0",
5
5
  "identifier": "ai.lattice.desktop",
6
6
  "build": {
7
7
  "beforeDevCommand": "npm run frontend:dev",
@@ -1,13 +1,13 @@
1
1
  {
2
- "version": "6.5.0",
2
+ "version": "6.6.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
+ "index.html": "/static/app/assets/index-CbvcAQ6B.js",
10
+ "assets/index-C8toxDpv.css": "/static/app/assets/index-C8toxDpv.css"
11
11
  },
12
12
  "vite": {
13
13
  "../node_modules/@tauri-apps/api/core.js": {
@@ -17,7 +17,7 @@
17
17
  "isDynamicEntry": true
18
18
  },
19
19
  "index.html": {
20
- "file": "assets/index-C1J_1441.js",
20
+ "file": "assets/index-CbvcAQ6B.js",
21
21
  "name": "index",
22
22
  "src": "index.html",
23
23
  "isEntry": true,
@@ -25,7 +25,7 @@
25
25
  "../node_modules/@tauri-apps/api/core.js"
26
26
  ],
27
27
  "css": [
28
- "assets/index-CdCVz_i4.css"
28
+ "assets/index-C8toxDpv.css"
29
29
  ]
30
30
  }
31
31
  }