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.
- package/README.md +31 -28
- package/docs/CHANGELOG.md +62 -0
- package/frontend/openapi.json +65 -1
- package/frontend/src/api/client.ts +2 -0
- package/frontend/src/api/openapi.ts +86 -0
- package/frontend/src/features/brain/BrainComposer.tsx +20 -1
- package/frontend/src/features/brain/BrainConversation.tsx +48 -4
- package/frontend/src/features/brain/BrainHome.tsx +61 -1
- package/frontend/src/features/brain/BrainMemoryLayer.tsx +8 -1
- package/frontend/src/features/brain/BrainOverviewPanel.tsx +76 -1
- package/frontend/src/features/brain/brainData.ts +125 -1
- package/frontend/src/features/brain/types.ts +52 -0
- package/frontend/src/i18n.ts +66 -0
- package/frontend/src/styles.css +261 -0
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/graph/ingest.py +41 -6
- package/lattice_brain/ingestion.py +1 -0
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/knowledge_graph.py +11 -0
- package/latticeai/api/memory.py +20 -0
- package/latticeai/app_factory.py +2 -0
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/runtime/chat_wiring.py +2 -0
- package/latticeai/runtime/router_registration.py +3 -0
- package/latticeai/services/memory_service.py +188 -2
- package/latticeai/services/router_context.py +1 -0
- package/latticeai/services/upload_service.py +12 -0
- package/package.json +1 -1
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +5 -5
- package/static/app/assets/{index-Div5vMlq.css → index-C8toxDpv.css} +1 -1
- package/static/app/assets/index-CbvcAQ6B.js +16 -0
- package/static/app/assets/index-CbvcAQ6B.js.map +1 -0
- package/static/app/index.html +2 -2
- package/static/app/assets/index-t1jx1BR9.js +0 -16
- 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.
|
|
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.
|
|
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":
|
|
@@ -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
package/src-tauri/Cargo.lock
CHANGED
package/src-tauri/Cargo.toml
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "6.
|
|
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-
|
|
10
|
-
"assets/index-
|
|
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-
|
|
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-
|
|
28
|
+
"assets/index-C8toxDpv.css"
|
|
29
29
|
]
|
|
30
30
|
}
|
|
31
31
|
}
|