ltcai 6.4.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 (40) hide show
  1. package/README.md +31 -28
  2. package/docs/CHANGELOG.md +62 -0
  3. package/frontend/openapi.json +65 -1
  4. package/frontend/src/api/client.ts +2 -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 +48 -4
  8. package/frontend/src/features/brain/BrainHome.tsx +61 -1
  9. package/frontend/src/features/brain/BrainMemoryLayer.tsx +8 -1
  10. package/frontend/src/features/brain/BrainOverviewPanel.tsx +76 -1
  11. package/frontend/src/features/brain/brainData.ts +125 -1
  12. package/frontend/src/features/brain/types.ts +52 -0
  13. package/frontend/src/i18n.ts +66 -0
  14. package/frontend/src/styles.css +261 -0
  15. package/lattice_brain/__init__.py +1 -1
  16. package/lattice_brain/graph/ingest.py +41 -6
  17. package/lattice_brain/ingestion.py +1 -0
  18. package/lattice_brain/runtime/multi_agent.py +1 -1
  19. package/latticeai/__init__.py +1 -1
  20. package/latticeai/api/knowledge_graph.py +11 -0
  21. package/latticeai/api/memory.py +20 -0
  22. package/latticeai/app_factory.py +2 -0
  23. package/latticeai/core/marketplace.py +1 -1
  24. package/latticeai/core/workspace_os.py +1 -1
  25. package/latticeai/runtime/chat_wiring.py +2 -0
  26. package/latticeai/runtime/router_registration.py +3 -0
  27. package/latticeai/services/memory_service.py +188 -2
  28. package/latticeai/services/router_context.py +1 -0
  29. package/latticeai/services/upload_service.py +12 -0
  30. package/package.json +1 -1
  31. package/src-tauri/Cargo.lock +1 -1
  32. package/src-tauri/Cargo.toml +1 -1
  33. package/src-tauri/tauri.conf.json +1 -1
  34. package/static/app/asset-manifest.json +5 -5
  35. package/static/app/assets/{index-Div5vMlq.css → index-C8toxDpv.css} +1 -1
  36. package/static/app/assets/index-CbvcAQ6B.js +16 -0
  37. package/static/app/assets/index-CbvcAQ6B.js.map +1 -0
  38. package/static/app/index.html +2 -2
  39. package/static/app/assets/index-t1jx1BR9.js +0 -16
  40. package/static/app/assets/index-t1jx1BR9.js.map +0 -1
@@ -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
@@ -132,6 +152,60 @@ class MemoryService:
132
152
  return None
133
153
 
134
154
  # ── Memory Manager: sources / usage / health ──────────────────────────
155
+ def _brain_readiness(
156
+ self,
157
+ *,
158
+ memory_count: int,
159
+ concept_count: Optional[int],
160
+ relationship_count: Optional[int],
161
+ healthy_sources: int,
162
+ ) -> Dict[str, Any]:
163
+ """Summarize how ready the user's Brain is for the product UI.
164
+
165
+ The frontend should render this signal instead of re-deriving it from
166
+ UI fragments. Keeping it here makes the score auditable and keeps the
167
+ Memory Manager as the single owner of cross-tier Brain growth signals.
168
+ """
169
+ concepts = max(0, int(concept_count or 0))
170
+ relationships = max(0, int(relationship_count or 0))
171
+ memories = max(0, int(memory_count or 0))
172
+ healthy = max(0, int(healthy_sources or 0))
173
+ score = min(100, round(memories * 12 + concepts * 8 + relationships * 4 + healthy * 3))
174
+
175
+ if memories < 1 and concepts < 1:
176
+ state = "quiet"
177
+ depth = 2
178
+ title_key = "brain.readiness.quiet"
179
+ action_key = "brain.readiness.start"
180
+ score = max(12, score)
181
+ elif concepts < 3 or relationships < 2:
182
+ state = "forming"
183
+ depth = 3 if concepts < 3 else 4
184
+ title_key = "brain.readiness.forming"
185
+ action_key = "brain.readiness.grow"
186
+ score = max(38, score)
187
+ else:
188
+ state = "alive"
189
+ depth = 5
190
+ title_key = "brain.readiness.alive"
191
+ action_key = "brain.readiness.map"
192
+ score = max(72, score)
193
+
194
+ return {
195
+ "score": score,
196
+ "state": state,
197
+ "depth": depth,
198
+ "title_key": title_key,
199
+ "action_key": action_key,
200
+ "signals": {
201
+ "memory_count": memories,
202
+ "concept_count": concepts,
203
+ "relationship_count": relationships,
204
+ "healthy_sources": healthy,
205
+ },
206
+ "source": "memory_service",
207
+ }
208
+
135
209
  def manager(self, *, user_email: Optional[str] = None, workspace_id: Optional[str] = None) -> Dict[str, Any]:
136
210
  ws_mem = self._workspace_memories(user_email=user_email, workspace_id=workspace_id or "personal")
137
211
  if workspace_id is None:
@@ -139,7 +213,7 @@ class MemoryService:
139
213
  else:
140
214
  project_mem = self._workspace_memories(user_email=user_email, workspace_id=workspace_id)
141
215
  snaps = self._snapshots(workspace_id=workspace_id)
142
- convs = self._conversations()
216
+ convs = self._scoped_conversations(user_email=user_email, workspace_id=workspace_id)
143
217
  kg_stats = self._kg_stats()
144
218
  kg_index = self._kg_index()
145
219
 
@@ -198,15 +272,127 @@ class MemoryService:
198
272
  total_bytes = ws_bytes + kg_bytes + conv_bytes
199
273
  healthy = sum(1 for s in sources if s["health"] == "ok")
200
274
  overall = "ok" if healthy >= 4 else "degraded" if healthy >= 1 else "unavailable"
275
+ memory_ids = {m.get("id") for m in [*ws_mem, *project_mem] if m.get("id")}
276
+ memory_count = len(memory_ids) + len(snaps) + len(convs)
201
277
  return {
202
278
  "sources": sources,
203
279
  "tiers": list(TIERS),
204
280
  "usage": {"total_items": total_items, "total_bytes": total_bytes, "sources": len(sources)},
281
+ "brain_readiness": self._brain_readiness(
282
+ memory_count=memory_count,
283
+ concept_count=node_total,
284
+ relationship_count=edge_total,
285
+ healthy_sources=healthy,
286
+ ),
205
287
  "health": overall,
206
288
  "graph_enabled": self._enable_graph,
207
289
  "generated_at": _now(),
208
290
  }
209
291
 
292
+ def brain_quality_summary(self, *, user_email: Optional[str] = None, workspace_id: Optional[str] = None) -> Dict[str, Any]:
293
+ """Return the backend-owned Brain readiness signal for API consumers."""
294
+ return self.manager(user_email=user_email, workspace_id=workspace_id)["brain_readiness"]
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
+
210
396
  def tiers(self) -> Dict[str, Any]:
211
397
  return {"tiers": list(TIERS), "workspace_kinds": list(WORKSPACE_KINDS)}
212
398
 
@@ -283,7 +469,7 @@ class MemoryService:
283
469
  items = self._snapshots(workspace_id=workspace_id)[:limit]
284
470
  return {"source": source, "items": items, "count": len(items)}
285
471
  if source == "conversation":
286
- convs = self._conversations()
472
+ convs = self._scoped_conversations(user_email=user_email, workspace_id=workspace_id)
287
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]]
288
474
  return {"source": source, "items": items, "count": len(convs)}
289
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.4.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.4.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.4.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.4.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.4.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-t1jx1BR9.js",
10
- "assets/index-Div5vMlq.css": "/static/app/assets/index-Div5vMlq.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-t1jx1BR9.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-Div5vMlq.css"
28
+ "assets/index-C8toxDpv.css"
29
29
  ]
30
30
  }
31
31
  }