ltcai 11.7.0 → 12.0.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 (174) hide show
  1. package/README.md +100 -76
  2. package/docs/BENCHMARKS.md +9 -2
  3. package/docs/CHANGELOG.md +249 -0
  4. package/docs/CI_AND_RELEASE_GATES.md +126 -41
  5. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  6. package/docs/DEVELOPMENT.md +271 -103
  7. package/docs/ENTERPRISE.md +1 -1
  8. package/docs/LEGACY_COMPATIBILITY.md +10 -6
  9. package/docs/MULTI_AGENT_RUNTIME.md +4 -4
  10. package/docs/ONBOARDING.md +16 -4
  11. package/docs/OPERATIONS.md +14 -1
  12. package/docs/PERMISSION_MODE.md +14 -9
  13. package/docs/REALTIME_COLLABORATION.md +1 -1
  14. package/docs/ROADMAP.md +113 -0
  15. package/docs/TRUST_MODEL.md +28 -7
  16. package/docs/USABILITY_AUDIT.md +5 -0
  17. package/docs/WHY_LATTICE.md +13 -5
  18. package/docs/WORKFLOW_DESIGNER.md +2 -2
  19. package/docs/kg-schema.md +57 -7
  20. package/docs/mcp-tools.md +93 -82
  21. package/docs/security-model.md +6 -3
  22. package/lattice_brain/__init__.py +1 -1
  23. package/lattice_brain/graph/_kg_common/__init__.py +1 -54
  24. package/lattice_brain/graph/_kg_common/extraction.py +459 -105
  25. package/lattice_brain/graph/_kg_common/normalize.py +305 -0
  26. package/lattice_brain/graph/_kg_common/patterns.py +275 -0
  27. package/lattice_brain/graph/_kg_common/relations.py +12 -3
  28. package/lattice_brain/graph/_kg_common/sections.py +107 -0
  29. package/lattice_brain/graph/_kg_common/text.py +14 -450
  30. package/lattice_brain/graph/_kg_constants.py +7 -0
  31. package/lattice_brain/ingestion/__init__.py +6 -3
  32. package/lattice_brain/multimodal/__init__.py +9 -3
  33. package/latticeai/__init__.py +1 -1
  34. package/latticeai/api/agent_worker_seam.py +44 -1
  35. package/latticeai/api/models.py +18 -110
  36. package/latticeai/api/search.py +7 -30
  37. package/latticeai/api/worker_compute.py +127 -106
  38. package/latticeai/api/worker_seams.py +17 -2
  39. package/latticeai/core/embedding_providers/__init__.py +16 -0
  40. package/latticeai/core/embedding_providers/autodetect.py +302 -0
  41. package/latticeai/core/embedding_providers/base.py +25 -0
  42. package/latticeai/core/embedding_providers/profiles.py +44 -0
  43. package/latticeai/core/embedding_providers/text.py +74 -8
  44. package/latticeai/core/http_origin.py +3 -3
  45. package/latticeai/core/messages.py +0 -5
  46. package/latticeai/core/policy.py +1 -6
  47. package/latticeai/core/quiet.py +1 -20
  48. package/latticeai/core/security.py +29 -83
  49. package/latticeai/core/sessions.py +95 -4
  50. package/latticeai/core/users.py +0 -38
  51. package/latticeai/core/vector_index/__init__.py +61 -0
  52. package/latticeai/core/vector_index/hnsw.py +383 -0
  53. package/latticeai/core/vector_index/sidecar.py +329 -0
  54. package/latticeai/models/router/catalog.py +2 -2
  55. package/latticeai/models/router/generation.py +176 -30
  56. package/latticeai/models/router/loading.py +150 -9
  57. package/latticeai/runtime/access_runtime.py +7 -4
  58. package/latticeai/runtime/brain_runtime.py +43 -9
  59. package/latticeai/runtime/build_phases/features.py +8 -31
  60. package/latticeai/runtime/build_phases/foundation.py +7 -16
  61. package/latticeai/runtime/build_phases/web.py +3 -3
  62. package/latticeai/runtime/build_phases/worker_profile.py +29 -27
  63. package/latticeai/runtime/runtime_context.py +0 -2
  64. package/latticeai/services/architecture_readiness.py +18 -19
  65. package/latticeai/services/process_audit.py +1 -22
  66. package/latticeai/services/product_readiness.py +39 -12
  67. package/latticeai/services/search_service.py +7 -0
  68. package/latticeai/services/voice_capture.py +8 -28
  69. package/latticeai/tools/__init__.py +12 -47
  70. package/latticeai/tools/commands.py +9 -15
  71. package/latticeai/tools/documents.py +12 -0
  72. package/latticeai/tools/knowledge.py +0 -6
  73. package/latticeai/tools/markup.py +152 -0
  74. package/package.json +4 -5
  75. package/requirements.txt +0 -1
  76. package/scripts/check_current_release_docs.mjs +1 -1
  77. package/scripts/check_openapi_drift.mjs +3 -2
  78. package/scripts/check_server_i18n.mjs +5 -4
  79. package/scripts/compose_openapi.py +4 -1
  80. package/scripts/export_openapi.py +5 -4
  81. package/scripts/gen_worker_allowlist_fixture.py +2 -2
  82. package/scripts/openapi_route_families.json +19 -74
  83. package/scripts/publish_release.mjs +157 -0
  84. package/scripts/release_screen_claims.json +144 -28
  85. package/src-tauri/Cargo.lock +45 -10
  86. package/src-tauri/Cargo.toml +1 -1
  87. package/src-tauri/tauri.conf.json +1 -1
  88. package/static/app/asset-manifest.json +47 -41
  89. package/static/app/assets/Act-Cf1L2709.js +2 -0
  90. package/static/app/assets/AdminConsole-DPAbLTYV.js +1 -0
  91. package/static/app/assets/Brain-DqamGrj-.js +2 -0
  92. package/static/app/assets/BrainHome-MHe2_RYs.js +2 -0
  93. package/static/app/assets/BrainSignals-CQPPfyyH.js +1 -0
  94. package/static/app/assets/Capture-DGdIH_Zc.js +1 -0
  95. package/static/app/assets/Chronicle-C-UlCJoJ.js +1 -0
  96. package/static/app/assets/CommandPalette-WNT4EqUX.js +1 -0
  97. package/static/app/assets/DigitalBrainExplorer-CEBH5Cwc.js +321 -0
  98. package/static/app/assets/Library-C6xd1dlf.js +1 -0
  99. package/static/app/assets/LivingBrain-BEk-0ohw.js +1 -0
  100. package/static/app/assets/ProductFlow-CZLm5iXh.js +1 -0
  101. package/static/app/assets/QueryClientProvider-B3OjqSyJ.js +1 -0
  102. package/static/app/assets/{ReviewCard-HXRle3qq.js → ReviewCard-CEHG6evf.js} +2 -2
  103. package/static/app/assets/RunsListPanel-CLtEJSRW.js +1 -0
  104. package/static/app/assets/System-CAxwBUXw.js +1 -0
  105. package/static/app/assets/WorkflowGraph-Dj10RuGE.js +1 -0
  106. package/static/app/assets/WorkflowsPanel-Kyeh_LIT.js +2 -0
  107. package/static/app/assets/actHelpers-CtSmK9Dw.js +1 -0
  108. package/static/app/assets/arrow-left-CRl5EO4D.js +1 -0
  109. package/static/app/assets/{bot-Cn8bWRuq.js → bot-DhUGRel2.js} +1 -1
  110. package/static/app/assets/brain-CLkhHsHF.js +1 -0
  111. package/static/app/assets/button-CmaEqG1T.js +1 -0
  112. package/static/app/assets/circle-check-CFgejkOS.js +1 -0
  113. package/static/app/assets/{circle-pause-CmzC_apg.js → circle-pause-l96izbxj.js} +1 -1
  114. package/static/app/assets/{circle-play-D8mW2aQ7.js → circle-play-CrZa25_q.js} +1 -1
  115. package/static/app/assets/{cpu-DZcdd0PZ.js → cpu-BaXudqwl.js} +1 -1
  116. package/static/app/assets/{download-bv1KEPGQ.js → download-hCVFPiyc.js} +1 -1
  117. package/static/app/assets/{folder-open-d-Pip5gr.js → folder-open-CHL82Yp7.js} +1 -1
  118. package/static/app/assets/{hard-drive-D20iavUb.js → hard-drive-DDzET7lk.js} +1 -1
  119. package/static/app/assets/index-CB93CZWW.css +2 -0
  120. package/static/app/assets/index-D2H-wSl6.js +13 -0
  121. package/static/app/assets/input-Df1CAY_I.js +1 -0
  122. package/static/app/assets/jsx-runtime-bzQ4Vb5N.js +1 -0
  123. package/static/app/assets/{link-2-BPJOFlAy.js → link-2-xNnTIX1_.js} +1 -1
  124. package/static/app/assets/{permissionCopy-ChdJd493.js → permissionCopy-D3aWHco-.js} +1 -1
  125. package/static/app/assets/primitives-BioD2slS.js +1 -0
  126. package/static/app/assets/search-BzBw8YcW.js +1 -0
  127. package/static/app/assets/{share-2-YNX_NtMU.js → share-2-FkzGf8Df.js} +1 -1
  128. package/static/app/assets/{shield-alert-DuQ3zrVL.js → shield-alert-B3dwzik4.js} +1 -1
  129. package/static/app/assets/sourceMeta-DQSY_tah.js +1 -0
  130. package/static/app/assets/textarea-P8o6pvOP.js +1 -0
  131. package/static/app/assets/useFocusTrap-hswOIkXE.js +1 -0
  132. package/static/app/assets/useMutation-OJLrYSRA.js +1 -0
  133. package/static/app/assets/workspace-BCuk3Ku9.js +1 -0
  134. package/static/app/index.html +4 -4
  135. package/static/sw.js +1 -1
  136. package/lattice_brain/ingestion/pipeline.py +0 -108
  137. package/latticeai/api/local_files.py +0 -44
  138. package/latticeai/api/tools.py +0 -126
  139. package/latticeai/api/voice_capture.py +0 -32
  140. package/latticeai/core/agent_permission.py +0 -85
  141. package/scripts/agent_eval.py +0 -34
  142. package/scripts/brain_quality_eval.py +0 -37
  143. package/scripts/check_legacy_debt.mjs +0 -91
  144. package/scripts/check_python.py +0 -100
  145. package/scripts/chunking_parity_corpus.py +0 -449
  146. package/scripts/generate_agent_parity_fixtures.py +0 -771
  147. package/scripts/generate_chunking_parity_fixtures.py +0 -259
  148. package/static/app/assets/Act-BPcVAbOL.js +0 -1
  149. package/static/app/assets/AdminConsole-Bw1ATQL0.js +0 -1
  150. package/static/app/assets/Brain-CT92Kos0.js +0 -321
  151. package/static/app/assets/BrainHome-CFBkt1K_.js +0 -2
  152. package/static/app/assets/BrainSignals-ReLWF2H8.js +0 -1
  153. package/static/app/assets/Capture-BsTokYkk.js +0 -1
  154. package/static/app/assets/Chronicle-B6f0T9id.js +0 -1
  155. package/static/app/assets/CommandPalette-CuvjTv1u.js +0 -1
  156. package/static/app/assets/Library-BGJbG9Hd.js +0 -1
  157. package/static/app/assets/LivingBrain-DGYK_Jsa.js +0 -1
  158. package/static/app/assets/ProductFlow-DXBC6brE.js +0 -1
  159. package/static/app/assets/System-CMHSO9qM.js +0 -1
  160. package/static/app/assets/arrow-left-BfmkskWx.js +0 -1
  161. package/static/app/assets/brain-CQJberbE.js +0 -1
  162. package/static/app/assets/button-Ct9f2_oT.js +0 -1
  163. package/static/app/assets/circle-check-DruOxB-4.js +0 -1
  164. package/static/app/assets/index-D9x-kSNy.css +0 -2
  165. package/static/app/assets/index-Do83hDzJ.js +0 -10
  166. package/static/app/assets/input-BLXVNmj1.js +0 -1
  167. package/static/app/assets/primitives-Cv5tbZBY.js +0 -1
  168. package/static/app/assets/search-CT9aho2j.js +0 -1
  169. package/static/app/assets/textarea-DqwLnli4.js +0 -1
  170. package/static/app/assets/useFocusTrap-ZVI98jaW.js +0 -1
  171. package/static/app/assets/useMutation-CVC4qv_D.js +0 -1
  172. package/static/app/assets/useQuery-C7BeG4HU.js +0 -1
  173. package/static/app/assets/utils-CiFtIdZq.js +0 -4
  174. package/static/app/assets/workspace-DQz9vIId.js +0 -1
@@ -3,8 +3,14 @@
3
3
  Plan §설계 결정 2 (revised) hands **every write** to Rust — platform state and
4
4
  the knowledge graph both. What is left for Python is the part Rust cannot do
5
5
  without shipping a model runtime and half of PyPI: turning text into vectors,
6
- turning a document into text, turning a spec into document bytes, turning audio
7
- into a transcript, and turning a picture into facts about it.
6
+ turning a document into text, turning a spec into document bytes, and turning
7
+ audio into a transcript.
8
+
9
+ A ninth seam, ``POST /worker/multimodal/describe``, was here until v11.8.0. It
10
+ wrapped :func:`lattice_brain.multimodal.extract_image_facts` for a native image
11
+ ingest that was never built, so nothing in the tree ever called it — not
12
+ ``lattice-ingest``, not the gateway. The Brain Core functions behind it are
13
+ untouched; what went is the door nobody opened.
8
14
 
9
15
  ``worker_seams.py`` holds the *state* seams (``/worker/chat/record-turn``,
10
16
  ``/worker/graph/mutate``) that Wave 2 codes against and Wave 2.5 §W3 retires.
@@ -13,7 +19,7 @@ here opens a database, writes a file, or reaches a store. Each handler is a
13
19
  function of its request body, so a caller can retry it, cache it, or run two of
14
20
  them at once without asking who else is writing.
15
21
 
16
- The five seams, and what each was extracted from:
22
+ The nine seams, and what each was extracted from:
17
23
 
18
24
  ``POST /worker/embed``
19
25
  ``EmbeddingProvider.embed_batch`` on the *resolved* provider — the same
@@ -30,22 +36,15 @@ The five seams, and what each was extracted from:
30
36
 
31
37
  ``POST /worker/render/{docx,xlsx,pptx,pdf}``
32
38
  The *building* half of ``tools.documents.create_*`` — same libraries, same
33
- layout decisions, same filename sanitising — with ``save(path)`` replaced by
34
- ``save(BytesIO)``. Rust places the file; this seam never learns where.
39
+ layout decisions — with ``save(path)`` replaced by ``save(BytesIO)``. Rust
40
+ places the file, sanitises the name and resolves the target; this seam
41
+ never learns where and answers with bytes only.
35
42
 
36
43
  ``POST /worker/asr``
37
44
  ``VoiceCaptureService._transcribe``'s contract without the ingest: the same
38
45
  injected transcriber port, the same container whitelist and size ceiling,
39
46
  and the same refusal to call an empty transcript "text".
40
47
 
41
- ``POST /worker/multimodal/describe``
42
- :func:`lattice_brain.multimodal.extract_image_facts` plus
43
- :func:`~lattice_brain.multimodal.image_quality_score` — exactly what
44
- ``IngestionRoutingMixin._ingest_image`` computes before it writes a node.
45
- The describe step *is* separable here because Brain Core already split it:
46
- observing an image and writing it are two functions, and only the first one
47
- is compute.
48
-
49
48
  ``POST /worker/extract``
50
49
  :func:`~lattice_brain.graph._kg_common.extraction._extract_concepts` (LLM-first),
51
50
  :func:`~lattice_brain.graph._kg_common.extraction._extract_triples` and
@@ -54,6 +53,12 @@ The five seams, and what each was extracted from:
54
53
  consume, already classified. Rust writes the Concept/Task/Decision subgraph;
55
54
  this seam never opens a store.
56
55
 
56
+ ``POST /worker/vector/query``
57
+ Approximate nearest-neighbour over the ``.hnsw`` sidecar next to the
58
+ brain database. The one compute seam that *reads* the store (row count
59
+ + embeddings, never a write) so it can refresh the sidecar and say
60
+ ``index: "none"`` when there is nothing to serve.
61
+
57
62
  Gating is the seam gate the rest of the worker uses — ``LATTICEAI_AGENT_TOOL_SEAM``
58
63
  read per request through :func:`latticeai.api.agent_worker_seam._seam_open`,
59
64
  then ``require_user`` and the ``agent_seam`` rate bucket. Off ⇒ 404. Mounted
@@ -67,8 +72,10 @@ import asyncio
67
72
  import base64
68
73
  import binascii
69
74
  import contextlib
75
+ import importlib.util
70
76
  import io
71
77
  import logging
78
+ import sys
72
79
  import tempfile
73
80
  from pathlib import Path
74
81
  from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple
@@ -85,7 +92,7 @@ from latticeai.services.voice_capture import (
85
92
  SUPPORTED_AUDIO_EXTENSIONS,
86
93
  )
87
94
  from latticeai.tools import _CJK_FONT_CANDIDATES, ToolError
88
- from latticeai.tools.documents import _body_to_str, _safe_filename, read_document
95
+ from latticeai.tools.documents import _body_to_str, read_document
89
96
 
90
97
  logger = logging.getLogger(__name__)
91
98
 
@@ -107,15 +114,11 @@ EXTRACT_LIMITS: Dict[str, int] = {"message": 12, "document": 15}
107
114
  #: write-side module would tie this compute seam to a module W1 is replacing.
108
115
  PASSAGE_MAX_CHARS = 50_000
109
116
 
110
- #: Render kind → the suffix ``_safe_filename`` enforces. Same table as
111
- #: ``documents._DOCUMENT_TOOL_TARGETS``, minus the output directory: where the
112
- #: file lands is Rust's decision now, so this seam does not carry it.
113
- RENDER_SUFFIXES: Dict[str, str] = {
114
- "docx": ".docx",
115
- "xlsx": ".xlsx",
116
- "pptx": ".pptx",
117
- "pdf": ".pdf",
118
- }
117
+ #: Upper bound on ``texts`` for one ``POST /worker/embed``. Ingest already
118
+ #: batches; a caller that dumps a whole vault into one request is a bug, not
119
+ #: a use case. 256 sits in the 128–256 band G-RAG-A recommended. Additive:
120
+ #: the request/response shape is unchanged.
121
+ EMBED_MAX_BATCH = 256
119
122
 
120
123
  #: The CJK-capable fonts ``create_pdf`` looks for, as a module attribute so a
121
124
  #: test can point the probe at a font that exists (and at one that does not)
@@ -163,9 +166,40 @@ WORKER_COMPUTE_MESSAGES: Dict[str, Dict[str, str]] = {
163
166
  "ko": "'{kind}' 은(는) 추출 종류가 아닙니다. {allowed} 중 하나여야 합니다.",
164
167
  "en": "'{kind}' is not an extraction kind. Use one of {allowed}.",
165
168
  },
169
+ "worker_compute.embed_batch_too_large": {
170
+ "ko": "임베딩 배치가 {count}개입니다. 한 번에 {limit}개까지입니다.",
171
+ "en": "The embed batch has {count} texts; the limit is {limit}.",
172
+ },
173
+ "worker_compute.vector_query_invalid": {
174
+ "ko": "벡터 질의는 embedding_model, embedding_dim, vector 가 필요합니다.",
175
+ "en": "A vector query needs embedding_model, embedding_dim, and vector.",
176
+ },
166
177
  }
167
178
 
168
179
 
180
+ def pointer_tools_available() -> bool:
181
+ """Whether this interpreter can import ``pyautogui``.
182
+
183
+ Cheap and side-effect free: ``find_spec`` does not load the module, so a
184
+ headless worker without a display is not punished for answering the
185
+ question. The platform computer-use status route reads this through
186
+ ``GET /worker/sysinfo`` rather than guessing from its own process.
187
+ """
188
+ return importlib.util.find_spec("pyautogui") is not None
189
+
190
+
191
+ def sysinfo_payload_extras() -> Dict[str, Any]:
192
+ """Additive fields for ``GET /worker/sysinfo``.
193
+
194
+ Existing GPU keys stay owned by ``worker_seams.probe_gpu_memory``. This
195
+ dict is merged in; it must never reuse those names.
196
+ """
197
+ return {
198
+ "capabilities": {"pointer_tools": pointer_tools_available()},
199
+ "python_version": "{}.{}.{}".format(*sys.version_info[:3]),
200
+ }
201
+
202
+
169
203
  def register_worker_compute_messages() -> None:
170
204
  """Publish this module's messages into the one shared catalog."""
171
205
  for key, entry in WORKER_COMPUTE_MESSAGES.items():
@@ -415,16 +449,6 @@ class AsrRequest(BaseModel):
415
449
  filename: Optional[str] = None
416
450
 
417
451
 
418
- class DescribeRequest(BaseModel):
419
- """One picture, with the two observation switches ``extract_image_facts`` takes."""
420
-
421
- content_b64: str
422
- mime: Optional[str] = None
423
- filename: Optional[str] = None
424
- ocr: bool = True
425
- thumbnail: bool = True
426
-
427
-
428
452
  class ExtractRequest(BaseModel):
429
453
  """The text one ingest door would hand ``_extract_concepts``."""
430
454
 
@@ -432,6 +456,16 @@ class ExtractRequest(BaseModel):
432
456
  kind: str = "message"
433
457
 
434
458
 
459
+ class VectorQueryRequest(BaseModel):
460
+ """Approximate neighbours from the HNSW sidecar. ``k`` is capped at 200."""
461
+
462
+ workspace: Optional[str] = None
463
+ embedding_model: str
464
+ embedding_dim: int
465
+ vector: List[float] = Field(default_factory=list)
466
+ k: int = 10
467
+
468
+
435
469
  def build_extract_reply(text: str, kind: str) -> Dict[str, Any]:
436
470
  """The structures ``ingest_message`` / ``ingest_document`` / ``ingest_source`` consume.
437
471
 
@@ -484,18 +518,17 @@ def create_worker_compute_router(
484
518
  *,
485
519
  embedder: Any,
486
520
  transcriber: Optional[Callable[[str], str]] = None,
487
- multimodal_ports: Any = None,
488
521
  require_user: Callable[[Request], Any],
489
522
  enforce_rate_limit: Callable[[str, str], None],
523
+ db_path: Any = None,
490
524
  ) -> APIRouter:
491
525
  """The nine compute seams, wired to what this worker actually resolved.
492
526
 
493
527
  ``embedder`` is the :class:`~latticeai.core.embedding_providers.text.ResolvedEmbedder`
494
528
  ``phase_brain`` built (``None`` ⇒ 503, because a worker with no embedder is
495
- a configuration rather than a crash). ``transcriber`` and
496
- ``multimodal_ports`` are the same injected ports the voice and ingestion
497
- paths hold; absent, the relevant seam reports the absence instead of
498
- inventing a transcript or a caption.
529
+ a configuration rather than a crash). ``transcriber`` is the injected port
530
+ the voice path holds; absent, ``/worker/asr`` reports the absence instead of
531
+ inventing a transcript.
499
532
  """
500
533
  router = APIRouter()
501
534
 
@@ -522,11 +555,18 @@ def create_worker_compute_router(
522
555
  async def _render(
523
556
  kind: str,
524
557
  builder: Callable[[], bytes],
525
- filename: str,
526
558
  language: str,
527
559
  **extra: Any,
528
560
  ) -> Dict[str, Any]:
529
- """Build one document off the event loop and hand back its bytes."""
561
+ """Build one document off the event loop and hand back its bytes.
562
+
563
+ The reply is the bytes and what they cost, and nothing about *where*
564
+ they go: ``lattice-agent``'s ``documents::document_output_target`` runs
565
+ its own ``safe_filename`` and ``Workspace::resolve`` over the request's
566
+ ``filename`` **before** it calls here, and writes to the target it
567
+ resolved. A second sanitisation on this side produced a name no caller
568
+ ever read — two spellings of one rule, one of them invisible.
569
+ """
530
570
  try:
531
571
  payload = await asyncio.to_thread(builder)
532
572
  except ToolError as exc:
@@ -541,7 +581,6 @@ def create_worker_compute_router(
541
581
  500, "worker_compute.render_failed", language, kind=kind, reason=str(exc)
542
582
  ) from exc
543
583
  return {
544
- "filename": _safe_filename(filename, RENDER_SUFFIXES[kind]),
545
584
  "content_b64": base64.b64encode(payload).decode("ascii"),
546
585
  "bytes": len(payload),
547
586
  **extra,
@@ -572,15 +611,37 @@ def create_worker_compute_router(
572
611
  )
573
612
  provider = embedder.provider
574
613
  texts = list(req.texts)
614
+ if len(texts) > EMBED_MAX_BATCH:
615
+ raise http_error(
616
+ 422,
617
+ "worker_compute.embed_batch_too_large",
618
+ language,
619
+ count=str(len(texts)),
620
+ limit=str(EMBED_MAX_BATCH),
621
+ )
575
622
  if kind == "passage":
576
623
  texts = [text[:PASSAGE_MAX_CHARS] for text in texts]
577
- vectors = await asyncio.to_thread(provider.embed_batch, texts)
624
+ # `embed_batch_for` rather than `embed_batch`: an asymmetric model (the
625
+ # E5 family) needs to know whether this text is the question or the
626
+ # answer, and `kind` is exactly that. Symmetric providers ignore it.
627
+ # Resolved by name because the embedder arrives injected: a stand-in
628
+ # that predates the role-aware method still embeds, it just cannot be
629
+ # told which role it is embedding for.
630
+ role_aware = getattr(provider, "embed_batch_for", None)
631
+ vectors = (
632
+ await asyncio.to_thread(role_aware, texts, kind)
633
+ if callable(role_aware)
634
+ else await asyncio.to_thread(provider.embed_batch, texts)
635
+ )
578
636
  return {
579
637
  "vectors": vectors,
580
638
  "dim": provider.dim,
581
639
  "provider": embedder.active,
582
640
  "model_id": provider.model_id,
583
641
  "kind": kind,
642
+ # Additive (v12.0.0): `fallback` means these vectors are feature
643
+ # hashes, not meaning. A caller that stores them can say so.
644
+ "grade": getattr(provider, "grade", "fallback"),
584
645
  }
585
646
 
586
647
  @router.post("/worker/parse")
@@ -616,7 +677,6 @@ def create_worker_compute_router(
616
677
  return await _render(
617
678
  "docx",
618
679
  lambda: build_docx_bytes(req.title, req.body),
619
- req.filename,
620
680
  resolve_language(request),
621
681
  )
622
682
 
@@ -628,7 +688,6 @@ def create_worker_compute_router(
628
688
  return await _render(
629
689
  "xlsx",
630
690
  lambda: build_xlsx_bytes(req.rows, req.sheet_name),
631
- req.filename,
632
691
  resolve_language(request),
633
692
  rows=len(req.rows),
634
693
  )
@@ -641,7 +700,6 @@ def create_worker_compute_router(
641
700
  return await _render(
642
701
  "pptx",
643
702
  lambda: build_pptx_bytes(req.title, req.slides),
644
- req.filename,
645
703
  resolve_language(request),
646
704
  slides=len(req.slides) + 1,
647
705
  )
@@ -654,7 +712,6 @@ def create_worker_compute_router(
654
712
  return await _render(
655
713
  "pdf",
656
714
  lambda: build_pdf_bytes(req.title, req.body),
657
- req.filename,
658
715
  resolve_language(request),
659
716
  )
660
717
 
@@ -731,64 +788,6 @@ def create_worker_compute_router(
731
788
  "detail": "",
732
789
  }
733
790
 
734
- @router.post("/worker/multimodal/describe")
735
- async def worker_multimodal_describe(req: DescribeRequest, request: Request):
736
- """Everything ``_ingest_image`` knows before it writes a node.
737
-
738
- ``metadata`` is ``ImageFacts.as_metadata()`` — the exact dict
739
- ``write_image_memory`` merges onto the node — and ``index_text`` is the
740
- exact text it chunks. ``embedding`` is present only when a vision model
741
- produced one, which is the one property an image vector must have.
742
- """
743
- _require_seam(request)
744
- _admit(request)
745
- language = resolve_language(request)
746
- data = _decode(req.content_b64, language)
747
- suffix = _suffix_for(req.filename, req.mime, ".png")
748
-
749
- from lattice_brain.ingestion import _quality_level
750
- from lattice_brain.multimodal import (
751
- MODALITY_IMAGE,
752
- MultimodalPorts,
753
- extract_image_facts,
754
- image_quality_score,
755
- )
756
-
757
- ports = multimodal_ports or MultimodalPorts()
758
- with _temp_payload(data, suffix) as tmp_path:
759
- facts = await asyncio.to_thread(
760
- lambda: extract_image_facts(
761
- tmp_path, ports=ports, ocr=req.ocr, thumbnail=req.thumbnail
762
- )
763
- )
764
- quality = image_quality_score(facts)
765
- return {
766
- "modality": MODALITY_IMAGE,
767
- "readable": facts.readable,
768
- "error": facts.error,
769
- "width": facts.width,
770
- "height": facts.height,
771
- "format": facts.image_format,
772
- "mode": facts.mode,
773
- "ocr_status": facts.ocr_status,
774
- "ocr_text": facts.ocr_text,
775
- "ocr_detail": facts.ocr_detail,
776
- "caption_status": facts.caption_status,
777
- "caption": facts.caption,
778
- "embedding_status": facts.embedding_status,
779
- "embedding": facts.embedding,
780
- "embedding_detail": facts.embedding_detail,
781
- "thumbnail": facts.thumbnail,
782
- "index_text": facts.index_text(),
783
- "metadata": facts.as_metadata(),
784
- "quality": {
785
- "score": quality["score"],
786
- "level": _quality_level(quality["score"]),
787
- "reasons": quality["reasons"],
788
- },
789
- "ports": ports.describe(),
790
- }
791
-
792
791
  @router.post("/worker/extract")
793
792
  async def worker_extract(req: ExtractRequest, request: Request):
794
793
  """Concepts, triples and Task/Decision items for this text.
@@ -812,22 +811,42 @@ def create_worker_compute_router(
812
811
  )
813
812
  return await asyncio.to_thread(build_extract_reply, req.text, kind)
814
813
 
814
+ @router.post("/worker/vector/query")
815
+ async def worker_vector_query(req: VectorQueryRequest, request: Request):
816
+ """Top-k ids from the HNSW sidecar, or ``index: "none"`` honestly."""
817
+ _require_seam(request)
818
+ _admit(request)
819
+ language = resolve_language(request)
820
+ if not req.embedding_model or req.embedding_dim <= 0 or not req.vector:
821
+ raise http_error(422, "worker_compute.vector_query_invalid", language)
822
+ from latticeai.core.vector_index import query_sidecar
823
+
824
+ return await asyncio.to_thread(
825
+ query_sidecar,
826
+ workspace=req.workspace,
827
+ embedding_model=req.embedding_model,
828
+ embedding_dim=req.embedding_dim,
829
+ vector=req.vector,
830
+ k=req.k,
831
+ db_path=db_path,
832
+ )
833
+
815
834
  return router
816
835
 
817
836
 
818
837
  __all__ = [
819
838
  "CJK_FONT_CANDIDATES",
820
839
  "EMBED_KINDS",
840
+ "EMBED_MAX_BATCH",
821
841
  "EXTRACT_KINDS",
822
842
  "EXTRACT_LIMITS",
823
843
  "PASSAGE_MAX_CHARS",
824
- "RENDER_SUFFIXES",
825
844
  "WORKER_COMPUTE_MESSAGES",
826
845
  "AsrRequest",
827
- "DescribeRequest",
828
846
  "EmbedRequest",
829
847
  "ExtractRequest",
830
848
  "ParseRequest",
849
+ "VectorQueryRequest",
831
850
  "RenderDocxRequest",
832
851
  "RenderPdfRequest",
833
852
  "RenderPptxRequest",
@@ -838,5 +857,7 @@ __all__ = [
838
857
  "build_pptx_bytes",
839
858
  "build_xlsx_bytes",
840
859
  "create_worker_compute_router",
860
+ "pointer_tools_available",
841
861
  "register_worker_compute_messages",
862
+ "sysinfo_payload_extras",
842
863
  ]
@@ -85,21 +85,36 @@ def probe_gpu_memory() -> Dict[str, Any]:
85
85
  total = _total_memory_bytes()
86
86
  except Exception as exc: # noqa: BLE001 — an absent GPU is not a failure
87
87
  quiet("MLX unified-memory probe")
88
- return {
88
+ payload = {
89
89
  "mlx_available": False,
90
90
  "gpu_mem_gb": 0.0,
91
91
  "gpu_mem_pct": 0.0,
92
92
  "total_bytes": 0,
93
93
  "detail": str(exc),
94
94
  }
95
+ payload.update(_sysinfo_extras())
96
+ return payload
95
97
  used = active + cached
96
- return {
98
+ payload = {
97
99
  "mlx_available": True,
98
100
  "gpu_mem_gb": round(used / (1024 ** 3), 2),
99
101
  "gpu_mem_pct": round(used / total * 100, 1) if total else 0.0,
100
102
  "total_bytes": total,
101
103
  "detail": None,
102
104
  }
105
+ payload.update(_sysinfo_extras())
106
+ return payload
107
+
108
+
109
+ def _sysinfo_extras() -> Dict[str, Any]:
110
+ """Additive capability / interpreter facts. Fail closed to empty extras."""
111
+ try:
112
+ from latticeai.api.worker_compute import sysinfo_payload_extras
113
+
114
+ extras = sysinfo_payload_extras()
115
+ except Exception: # noqa: BLE001 — a missing extra must not hide the GPU reading
116
+ return {}
117
+ return extras if isinstance(extras, dict) else {}
103
118
 
104
119
 
105
120
  # ── request bodies ──────────────────────────────────────────────────────────
@@ -68,6 +68,14 @@ from __future__ import annotations
68
68
  from lattice_brain.embeddings import DEFAULT_EMBEDDING_DIM as DEFAULT_EMBEDDING_DIM
69
69
  from lattice_brain.embeddings import LocalEmbeddingModel as LocalEmbeddingModel
70
70
 
71
+ from .autodetect import AUTO_PROVIDER as AUTO_PROVIDER
72
+ from .autodetect import AUTODETECT_ENV as AUTODETECT_ENV
73
+ from .autodetect import LOCAL_MLX_MODELS as LOCAL_MLX_MODELS
74
+ from .autodetect import Detection as Detection
75
+ from .autodetect import detect_embedder as detect_embedder
76
+ from .autodetect import detect_local_mlx as detect_local_mlx
77
+ from .autodetect import detect_ollama as detect_ollama
78
+ from .autodetect import resolve_auto_provider as resolve_auto_provider
71
79
  from .base import _KNOWN_DIMS as _KNOWN_DIMS
72
80
  from .base import EmbeddingProvider as EmbeddingProvider
73
81
  from .base import EmbeddingUnavailable as EmbeddingUnavailable
@@ -113,6 +121,14 @@ from .vision import build_vision_provider as build_vision_provider
113
121
  from .vision import resolve_vision_embedder as resolve_vision_embedder
114
122
 
115
123
  __all__ = [
124
+ "AUTODETECT_ENV",
125
+ "AUTO_PROVIDER",
126
+ "LOCAL_MLX_MODELS",
127
+ "Detection",
128
+ "detect_embedder",
129
+ "detect_local_mlx",
130
+ "detect_ollama",
131
+ "resolve_auto_provider",
116
132
  "DEFAULT_CAPTION_PROMPT",
117
133
  "DEFAULT_VISION_DIM",
118
134
  "VISION_CAPTION_TARGET_ENV",