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.
- package/README.md +22 -20
- package/docs/CHANGELOG.md +34 -0
- package/frontend/openapi.json +65 -1
- package/frontend/src/api/client.ts +1 -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 +37 -2
- package/frontend/src/features/brain/BrainHome.tsx +41 -1
- package/frontend/src/features/brain/BrainOverviewPanel.tsx +62 -1
- package/frontend/src/features/brain/brainData.ts +54 -1
- package/frontend/src/features/brain/types.ts +37 -0
- package/frontend/src/i18n.ts +44 -0
- package/frontend/src/styles.css +133 -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 +14 -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 +122 -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-CdCVz_i4.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-C1J_1441.js +0 -16
- 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.
|
|
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.
|
|
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":
|
|
@@ -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
|
}
|