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
@@ -1421,7 +1421,150 @@ body {
1421
1421
  line-height: 1.35;
1422
1422
  }
1423
1423
 
1424
+ .brain-readiness-strip {
1425
+ display: grid;
1426
+ grid-template-columns: minmax(0, 1fr) minmax(5.6rem, 0.6fr) auto;
1427
+ align-items: center;
1428
+ gap: 0.7rem;
1429
+ margin-top: 0.75rem;
1430
+ border: 1px solid hsl(var(--brain-core) / 0.28);
1431
+ border-radius: 8px;
1432
+ background: hsl(var(--brain-core) / 0.06);
1433
+ padding: 0.66rem;
1434
+ }
1435
+
1436
+ .brain-readiness-strip span {
1437
+ display: block;
1438
+ color: hsl(var(--fg-muted));
1439
+ font-size: 0.64rem;
1440
+ font-weight: 820;
1441
+ text-transform: uppercase;
1442
+ }
1443
+
1444
+ .brain-readiness-strip strong {
1445
+ display: block;
1446
+ margin-top: 0.12rem;
1447
+ font-size: 0.82rem;
1448
+ line-height: 1.2;
1449
+ }
1450
+
1451
+ .brain-readiness-strip button {
1452
+ min-height: 2rem;
1453
+ border: 1px solid hsl(var(--brain-core) / 0.34);
1454
+ border-radius: 999px;
1455
+ background: hsl(var(--surface) / 0.78);
1456
+ color: hsl(var(--fg));
1457
+ padding: 0 0.72rem;
1458
+ font-size: 0.76rem;
1459
+ font-weight: 760;
1460
+ white-space: nowrap;
1461
+ }
1462
+
1463
+ .brain-readiness-meter {
1464
+ height: 0.42rem;
1465
+ overflow: hidden;
1466
+ border-radius: 999px;
1467
+ background: hsl(var(--bg) / 0.55);
1468
+ }
1469
+
1470
+ .brain-readiness-meter i {
1471
+ display: block;
1472
+ height: 100%;
1473
+ border-radius: inherit;
1474
+ background: linear-gradient(90deg, hsl(var(--memory)), hsl(var(--knowledge)), hsl(var(--brain-core)));
1475
+ transition: width 260ms ease;
1476
+ }
1477
+
1478
+ .brain-proof-strip {
1479
+ display: grid;
1480
+ grid-template-columns: repeat(3, minmax(0, 1fr));
1481
+ gap: 0.58rem;
1482
+ margin-top: 0.62rem;
1483
+ }
1484
+
1485
+ .brain-proof-point {
1486
+ display: grid;
1487
+ grid-template-columns: auto minmax(0, 1fr);
1488
+ align-items: center;
1489
+ gap: 0.12rem 0.44rem;
1490
+ min-height: 4.1rem;
1491
+ border: 1px solid hsl(var(--border) / 0.5);
1492
+ border-radius: 8px;
1493
+ background: hsl(var(--surface) / 0.56);
1494
+ padding: 0.58rem;
1495
+ color: hsl(var(--fg));
1496
+ }
1497
+
1498
+ .brain-proof-point svg {
1499
+ grid-row: span 2;
1500
+ color: hsl(var(--knowledge));
1501
+ }
1502
+
1503
+ .brain-proof-point span {
1504
+ overflow: hidden;
1505
+ color: hsl(var(--fg-muted));
1506
+ font-size: 0.62rem;
1507
+ font-weight: 820;
1508
+ text-overflow: ellipsis;
1509
+ text-transform: uppercase;
1510
+ white-space: nowrap;
1511
+ }
1512
+
1513
+ .brain-proof-point strong {
1514
+ overflow: hidden;
1515
+ font-size: 0.75rem;
1516
+ line-height: 1.2;
1517
+ text-overflow: ellipsis;
1518
+ white-space: nowrap;
1519
+ }
1520
+
1521
+ .brain-recall-proof {
1522
+ display: grid;
1523
+ width: 100%;
1524
+ gap: 0.2rem;
1525
+ margin-top: 0.62rem;
1526
+ border: 1px solid hsl(var(--memory) / 0.34);
1527
+ border-radius: 8px;
1528
+ background: hsl(var(--memory) / 0.07);
1529
+ color: hsl(var(--fg));
1530
+ padding: 0.68rem;
1531
+ text-align: left;
1532
+ }
1533
+
1534
+ .brain-recall-proof.is-empty {
1535
+ border-color: hsl(var(--border) / 0.5);
1536
+ background: hsl(var(--surface) / 0.5);
1537
+ }
1538
+
1539
+ .brain-recall-proof span {
1540
+ color: hsl(var(--fg-muted));
1541
+ font-size: 0.64rem;
1542
+ font-weight: 820;
1543
+ text-transform: uppercase;
1544
+ }
1545
+
1546
+ .brain-recall-proof strong,
1547
+ .brain-recall-proof small {
1548
+ display: block;
1549
+ overflow: hidden;
1550
+ text-overflow: ellipsis;
1551
+ white-space: nowrap;
1552
+ }
1553
+
1554
+ .brain-recall-proof strong {
1555
+ font-size: 0.86rem;
1556
+ }
1557
+
1558
+ .brain-recall-proof small {
1559
+ color: hsl(var(--fg-muted));
1560
+ font-size: 0.76rem;
1561
+ }
1562
+
1424
1563
  .brain-save-feedback {
1564
+ display: grid;
1565
+ grid-template-columns: auto minmax(0, 1fr);
1566
+ align-items: center;
1567
+ gap: 0.22rem 0.48rem;
1425
1568
  border: 1px solid hsl(var(--memory) / 0.38);
1426
1569
  border-radius: 8px;
1427
1570
  background: hsl(var(--memory) / 0.08);
@@ -1431,6 +1574,13 @@ body {
1431
1574
  font-weight: 720;
1432
1575
  }
1433
1576
 
1577
+ .brain-save-feedback small {
1578
+ grid-column: 2;
1579
+ color: hsl(var(--fg-muted));
1580
+ font-size: 0.72rem;
1581
+ font-weight: 560;
1582
+ }
1583
+
1434
1584
  .brain-message {
1435
1585
  max-width: 78%;
1436
1586
  margin-bottom: 1.1rem;
@@ -1911,6 +2061,59 @@ body {
1911
2061
  outline: none;
1912
2062
  }
1913
2063
 
2064
+ .brain-depth-rail {
2065
+ position: absolute;
2066
+ left: 50%;
2067
+ top: 0.85rem;
2068
+ z-index: 6;
2069
+ display: flex;
2070
+ width: min(92vw, 35rem);
2071
+ transform: translateX(-50%);
2072
+ gap: 0.28rem;
2073
+ border: 1px solid hsl(var(--border) / 0.48);
2074
+ border-radius: 999px;
2075
+ background: hsl(var(--surface-glass));
2076
+ padding: 0.32rem;
2077
+ box-shadow: 0 12px 30px hsl(200 30% 3% / 0.18);
2078
+ backdrop-filter: blur(18px);
2079
+ }
2080
+
2081
+ .brain-depth-rail button {
2082
+ display: inline-grid;
2083
+ min-width: 0;
2084
+ flex: 1 1 0;
2085
+ align-items: center;
2086
+ gap: 0.1rem;
2087
+ border: 0;
2088
+ border-radius: 999px;
2089
+ background: transparent;
2090
+ color: hsl(var(--fg-muted));
2091
+ padding: 0.36rem 0.42rem;
2092
+ text-align: center;
2093
+ }
2094
+
2095
+ .brain-depth-rail span {
2096
+ font-size: 0.58rem;
2097
+ font-weight: 840;
2098
+ }
2099
+
2100
+ .brain-depth-rail strong {
2101
+ overflow: hidden;
2102
+ text-overflow: ellipsis;
2103
+ white-space: nowrap;
2104
+ font-size: 0.66rem;
2105
+ line-height: 1.05;
2106
+ }
2107
+
2108
+ .brain-depth-rail button.is-revealed {
2109
+ background: hsl(var(--brain-core) / 0.1);
2110
+ color: hsl(var(--fg));
2111
+ }
2112
+
2113
+ .brain-depth-rail button[aria-current="step"] {
2114
+ box-shadow: 0 0 0 1px hsl(var(--brain-core) / 0.38) inset;
2115
+ }
2116
+
1914
2117
  .memory-fragment,
1915
2118
  .concept-signal,
1916
2119
  .graph-node {
@@ -2648,6 +2851,7 @@ body {
2648
2851
  font-size: 0.88rem;
2649
2852
  }
2650
2853
 
2854
+ .brain-document-input,
2651
2855
  .brain-image-input {
2652
2856
  display: inline-flex;
2653
2857
  cursor: pointer;
@@ -2660,6 +2864,19 @@ body {
2660
2864
  font-size: 0.78rem;
2661
2865
  }
2662
2866
 
2867
+ .brain-document-input {
2868
+ border-color: hsl(var(--memory) / 0.42);
2869
+ background: hsl(var(--memory) / 0.08);
2870
+ color: hsl(var(--fg));
2871
+ font-weight: 760;
2872
+ }
2873
+
2874
+ .brain-document-input.is-disabled,
2875
+ .mind-empty-upload.is-disabled {
2876
+ cursor: wait;
2877
+ opacity: 0.68;
2878
+ }
2879
+
2663
2880
  .mind-empty-kicker {
2664
2881
  margin-bottom: 0.4rem;
2665
2882
  font-size: 0.72rem;
@@ -2746,6 +2963,37 @@ body {
2746
2963
  line-height: 1.45;
2747
2964
  }
2748
2965
 
2966
+ .mind-empty-upload {
2967
+ display: inline-flex;
2968
+ cursor: pointer;
2969
+ align-items: center;
2970
+ justify-content: center;
2971
+ gap: 0.42rem;
2972
+ margin-top: 1rem;
2973
+ border: 1px solid hsl(var(--memory) / 0.46);
2974
+ border-radius: 999px;
2975
+ background: hsl(var(--memory) / 0.1);
2976
+ color: hsl(var(--fg));
2977
+ padding: 0.55rem 0.9rem;
2978
+ font-size: 0.82rem;
2979
+ font-weight: 820;
2980
+ transition: border-color 150ms ease, background 150ms ease, transform 150ms ease;
2981
+ }
2982
+
2983
+ .mind-empty-upload:hover,
2984
+ .mind-empty-upload:focus-within {
2985
+ border-color: hsl(var(--memory) / 0.72);
2986
+ background: hsl(var(--memory) / 0.16);
2987
+ transform: translateY(-1px);
2988
+ }
2989
+
2990
+ .mind-empty-upload-hint {
2991
+ display: block;
2992
+ margin-top: 0.42rem;
2993
+ color: hsl(var(--fg-muted));
2994
+ font-size: 0.72rem;
2995
+ }
2996
+
2749
2997
  .mind-empty-prompts {
2750
2998
  display: flex;
2751
2999
  flex-wrap: wrap;
@@ -2833,9 +3081,22 @@ body {
2833
3081
  .brain-depth-actions button {
2834
3082
  flex: 0 0 auto;
2835
3083
  }
3084
+ .brain-depth-rail {
3085
+ top: 0.35rem;
3086
+ width: min(100vw - 1rem, 25rem);
3087
+ }
3088
+ .brain-depth-rail strong {
3089
+ display: none;
3090
+ }
3091
+ .brain-readiness-strip {
3092
+ grid-template-columns: 1fr;
3093
+ }
2836
3094
  .brain-overview-grid {
2837
3095
  grid-template-columns: 1fr;
2838
3096
  }
3097
+ .brain-proof-strip {
3098
+ grid-template-columns: 1fr;
3099
+ }
2839
3100
  .brain-overview-head {
2840
3101
  align-items: flex-start;
2841
3102
  flex-direction: column;
@@ -26,7 +26,7 @@ from .storage import (
26
26
  storage_from_env,
27
27
  )
28
28
 
29
- __version__ = "6.4.0"
29
+ __version__ = "6.6.0"
30
30
 
31
31
  __all__ = [
32
32
  "AgentRuntime",
@@ -15,6 +15,7 @@ class KnowledgeGraphIngestMixin:
15
15
  user_nickname: Optional[str] = None,
16
16
  source: Optional[str] = None,
17
17
  conversation_id: Optional[str] = None,
18
+ workspace_id: Optional[str] = None,
18
19
  raw: Optional[Dict[str, Any]] = None,
19
20
  ) -> Dict[str, Any]:
20
21
  content = str(content or "")
@@ -28,6 +29,7 @@ class KnowledgeGraphIngestMixin:
28
29
  "role": role,
29
30
  "source": source,
30
31
  "conversation_id": conversation_id,
32
+ "workspace_id": workspace_id,
31
33
  "user_email": user_email,
32
34
  "user_nickname": user_nickname,
33
35
  "chars": len(content),
@@ -47,7 +49,9 @@ class KnowledgeGraphIngestMixin:
47
49
  "Chat",
48
50
  chat_title,
49
51
  summary=_clean_text(content)[:400],
50
- metadata={"source": source, "conversation_id": conversation_id},
52
+ metadata={"source": source, "conversation_id": conversation_id, "workspace_id": workspace_id},
53
+ owner=user_email,
54
+ workspace_id=workspace_id,
51
55
  )
52
56
 
53
57
  # ── 2. Person node (점: 명사 — 사람) ─────────────────────────────
@@ -61,6 +65,8 @@ class KnowledgeGraphIngestMixin:
61
65
  "Person",
62
66
  user_nickname or user_email or "Unknown",
63
67
  metadata={"email": user_email, "nickname": user_nickname},
68
+ owner=user_email,
69
+ workspace_id=workspace_id,
64
70
  )
65
71
  # 선: 동사 — Person이 Chat을 "작성함"
66
72
  self._upsert_edge(
@@ -81,6 +87,8 @@ class KnowledgeGraphIngestMixin:
81
87
  summary=_clean_text(content)[:500],
82
88
  metadata=metadata,
83
89
  raw=raw or metadata,
90
+ owner=user_email,
91
+ workspace_id=workspace_id,
84
92
  )
85
93
  # 선: Chat이 메시지를 "포함함"
86
94
  self._upsert_edge(
@@ -97,6 +105,8 @@ class KnowledgeGraphIngestMixin:
97
105
  f"chunk {index + 1}",
98
106
  summary=chunk[:500],
99
107
  metadata={"index": index, "source_node": node_id},
108
+ owner=user_email,
109
+ workspace_id=workspace_id,
100
110
  )
101
111
  self._upsert_chunk(
102
112
  conn,
@@ -118,7 +128,9 @@ class KnowledgeGraphIngestMixin:
118
128
  cid,
119
129
  node_t,
120
130
  concept,
121
- metadata={"auto_extracted": True, "source": source},
131
+ metadata={"auto_extracted": True, "source": source, "workspace_id": workspace_id},
132
+ owner=user_email,
133
+ workspace_id=workspace_id,
122
134
  )
123
135
  # 선: Chat이 개념을 "언급함"
124
136
  self._upsert_edge(
@@ -155,8 +167,10 @@ class KnowledgeGraphIngestMixin:
155
167
  sem_type,
156
168
  sem_title,
157
169
  summary=item["summary"],
158
- metadata={"auto_extracted": True, "source_node": node_id},
170
+ metadata={"auto_extracted": True, "source_node": node_id, "workspace_id": workspace_id},
159
171
  raw=item,
172
+ owner=user_email,
173
+ workspace_id=workspace_id,
160
174
  )
161
175
  # 선: Chat이 Task/Decision을 "생성함"
162
176
  self._upsert_edge(conn, conv_id, sem_id, "생성함", weight=0.9)
@@ -236,6 +250,8 @@ class KnowledgeGraphIngestMixin:
236
250
  summary=(text or filename)[:500],
237
251
  metadata=metadata,
238
252
  raw=metadata,
253
+ owner=owner or uploader,
254
+ workspace_id=workspace_id,
239
255
  )
240
256
  self._ingest_structure_nodes(conn, file_id, filename, doc_meta)
241
257
 
@@ -265,6 +281,8 @@ class KnowledgeGraphIngestMixin:
265
281
  "Person",
266
282
  uploader,
267
283
  metadata={"email": uploader},
284
+ owner=uploader,
285
+ workspace_id=workspace_id,
268
286
  )
269
287
  # 선: 동사 — Person이 Document를 "업로드함"
270
288
  self._upsert_edge(conn, person_id, file_id, "업로드함", weight=1.0)
@@ -272,7 +290,15 @@ class KnowledgeGraphIngestMixin:
272
290
  # ── Chat 노드와 연결 ──────────────────────────────────────────────
273
291
  if conversation_id:
274
292
  conv_id = f"conversation:{_slug(conversation_id)}"
275
- self._upsert_node(conn, conv_id, "Chat", conversation_id)
293
+ self._upsert_node(
294
+ conn,
295
+ conv_id,
296
+ "Chat",
297
+ conversation_id,
298
+ metadata={"conversation_id": conversation_id, "workspace_id": workspace_id},
299
+ owner=owner or uploader,
300
+ workspace_id=workspace_id,
301
+ )
276
302
  # 선: 동사 — Chat이 Document를 "언급함"
277
303
  self._upsert_edge(conn, conv_id, file_id, "언급함", weight=0.8)
278
304
 
@@ -286,7 +312,9 @@ class KnowledgeGraphIngestMixin:
286
312
  "Chunk",
287
313
  f"{filename} chunk {index + 1}",
288
314
  summary=chunk[:500],
289
- metadata={"index": index, "source_node": file_id},
315
+ metadata={"index": index, "source_node": file_id, "workspace_id": workspace_id},
316
+ owner=owner or uploader,
317
+ workspace_id=workspace_id,
290
318
  )
291
319
  self._upsert_chunk(
292
320
  conn,
@@ -308,7 +336,9 @@ class KnowledgeGraphIngestMixin:
308
336
  cid,
309
337
  node_t,
310
338
  concept,
311
- metadata={"auto_extracted": True, "source_file": filename},
339
+ metadata={"auto_extracted": True, "source_file": filename, "workspace_id": workspace_id},
340
+ owner=owner or uploader,
341
+ workspace_id=workspace_id,
312
342
  )
313
343
  # 선: 동사 — Document가 Concept을 "포함함"
314
344
  self._upsert_edge(conn, file_id, cid, "포함함", weight=0.8)
@@ -342,8 +372,11 @@ class KnowledgeGraphIngestMixin:
342
372
  "auto_extracted": True,
343
373
  "source_node": file_id,
344
374
  "filename": filename,
375
+ "workspace_id": workspace_id,
345
376
  },
346
377
  raw=item,
378
+ owner=owner or uploader,
379
+ workspace_id=workspace_id,
347
380
  )
348
381
  # 선: Document가 Task/Decision을 "포함함"
349
382
  self._upsert_edge(conn, file_id, sem_id, "포함함", weight=0.9)
@@ -474,6 +507,8 @@ class KnowledgeGraphIngestMixin:
474
507
  label,
475
508
  summary=str(source_uri or title or source_type)[:400],
476
509
  metadata=meta,
510
+ owner=meta.get("owner"),
511
+ workspace_id=meta.get("workspace_id"),
477
512
  )
478
513
  # 선: 콘텐츠 노드가 "이 출처에서 색인됨" (indexed_from → SOURCE)
479
514
  self._upsert_edge(
@@ -267,6 +267,7 @@ class IngestionPipeline:
267
267
  user_nickname=meta.get("user_nickname"),
268
268
  source=meta.get("source") or source_type,
269
269
  conversation_id=item.conversation_id,
270
+ workspace_id=item.workspace_id,
270
271
  raw=meta.get("raw"),
271
272
  )
272
273
  # ingest_message reports message/response node ids; normalize the keys
@@ -19,7 +19,7 @@ from datetime import datetime
19
19
  from typing import Any, Callable, Dict, List, Optional
20
20
 
21
21
 
22
- MULTI_AGENT_VERSION = "6.4.0"
22
+ MULTI_AGENT_VERSION = "6.6.0"
23
23
 
24
24
  AGENT_ROLES = ("researcher", "planner", "executor", "reviewer", "release")
25
25
  CORE_PIPELINE = ("planner", "executor", "reviewer")
@@ -1,3 +1,3 @@
1
1
  """Lattice AI - modular server package."""
2
2
 
3
- __version__ = "6.4.0"
3
+ __version__ = "6.6.0"
@@ -27,6 +27,14 @@ class KnowledgeGraphIngestRequest(BaseModel):
27
27
  metadata: Optional[Dict[str, Any]] = None
28
28
 
29
29
 
30
+ def _workspace_scope_from_request(request: Request) -> Optional[str]:
31
+ header = request.headers.get("X-Workspace-Id")
32
+ if header and header.strip():
33
+ return header.strip()
34
+ query = request.query_params.get("workspace_id")
35
+ return query.strip() if query and query.strip() else None
36
+
37
+
30
38
  def _format_context(matches: list, limit: int) -> str:
31
39
  """Mirror ``KnowledgeGraphRetrievalMixin.context_for_query`` formatting for a
32
40
  pre-filtered match list, so scoped callers get identical context lines minus
@@ -178,6 +186,7 @@ def create_knowledge_graph_router(
178
186
  async def knowledge_graph_ingest(req: KnowledgeGraphIngestRequest, request: Request):
179
187
  current_user = require_user(request)
180
188
  kg = graph()
189
+ workspace_id = _workspace_scope_from_request(request)
181
190
  event_type = (req.type or "").strip().lower()
182
191
  if event_type not in {"message", "ai_response", "note"}:
183
192
  raise HTTPException(status_code=400, detail="지원하는 type: message, ai_response, note")
@@ -189,10 +198,12 @@ def create_knowledge_graph_router(
189
198
  user_nickname=req.user_nickname,
190
199
  source=req.source or "mcp",
191
200
  conversation_id=req.conversation_id,
201
+ workspace_id=workspace_id,
192
202
  raw={
193
203
  "type": req.type,
194
204
  "title": req.title,
195
205
  "content": req.content,
206
+ "workspace_id": workspace_id,
196
207
  "metadata": req.metadata or {},
197
208
  },
198
209
  )
@@ -42,6 +42,7 @@ def create_memory_router(
42
42
  gate_read: Callable[[Request], Optional[str]],
43
43
  gate_write: Callable[[Request], Optional[str]],
44
44
  append_audit_event: Callable[..., None],
45
+ active_model_getter: Callable[[], str] | None = None,
45
46
  ) -> APIRouter:
46
47
  router = APIRouter()
47
48
 
@@ -51,6 +52,25 @@ def create_memory_router(
51
52
  scope = gate_read(request)
52
53
  return service.manager(user_email=user, workspace_id=scope)
53
54
 
55
+ @router.get("/api/memory/brain-quality")
56
+ async def brain_quality_summary(request: Request):
57
+ user = require_user(request)
58
+ scope = gate_read(request)
59
+ return service.brain_quality_summary(user_email=user, workspace_id=scope)
60
+
61
+ @router.get("/api/memory/brain-proof")
62
+ async def brain_proof(request: Request, q: str = "", limit: int = 3):
63
+ user = require_user(request)
64
+ scope = gate_read(request)
65
+ active_model = active_model_getter() if active_model_getter else ""
66
+ return service.brain_proof(
67
+ user_email=user,
68
+ workspace_id=scope,
69
+ active_model=active_model,
70
+ recall_query=q,
71
+ limit=limit,
72
+ )
73
+
54
74
  @router.get("/api/memory/tiers")
55
75
  async def memory_tiers(request: Request):
56
76
  require_user(request)
@@ -736,6 +736,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
736
736
  source_type="chat_message",
737
737
  text=message,
738
738
  owner=user_email,
739
+ workspace_id=workspace_id,
739
740
  conversation_id=conversation_id,
740
741
  metadata={
741
742
  "role": role,
@@ -1447,6 +1448,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1447
1448
  agent_registry=AGENT_REGISTRY,
1448
1449
  memory_service=MEMORY_SERVICE,
1449
1450
  platform=PLATFORM,
1451
+ active_model_getter=lambda: router.current_model_id or "",
1450
1452
  )
1451
1453
  register_interaction_routers(
1452
1454
  app,
@@ -11,7 +11,7 @@ from copy import deepcopy
11
11
  from typing import Any, Dict, List, Optional
12
12
 
13
13
 
14
- MARKETPLACE_VERSION = "6.4.0"
14
+ MARKETPLACE_VERSION = "6.6.0"
15
15
  TEMPLATE_KINDS = ("plugin", "workflow", "agent")
16
16
 
17
17
 
@@ -19,7 +19,7 @@ from pathlib import Path
19
19
  from typing import Any, Callable, Dict, Iterable, List, Optional
20
20
 
21
21
 
22
- WORKSPACE_OS_VERSION = "6.4.0"
22
+ WORKSPACE_OS_VERSION = "6.6.0"
23
23
 
24
24
  # Workspace types separate single-user Personal workspaces from shared
25
25
  # Organization workspaces. Both keep the same local-first JSON store; the type
@@ -63,6 +63,7 @@ def build_interaction_contexts(
63
63
  agent_registry: Any,
64
64
  memory_service: Any,
65
65
  platform: Any,
66
+ active_model_getter: Any = None,
66
67
  ) -> tuple[ToolRouterContext, InteractionRouterContext]:
67
68
  tool_router_context = ToolRouterContext(
68
69
  config=config,
@@ -101,6 +102,7 @@ def build_interaction_contexts(
101
102
  agent_registry=agent_registry,
102
103
  memory_service=memory_service,
103
104
  platform=platform,
105
+ active_model_getter=active_model_getter,
104
106
  )
105
107
  return tool_router_context, interaction_router_context
106
108
 
@@ -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