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.
- package/README.md +100 -76
- package/docs/BENCHMARKS.md +9 -2
- package/docs/CHANGELOG.md +249 -0
- package/docs/CI_AND_RELEASE_GATES.md +126 -41
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +271 -103
- package/docs/ENTERPRISE.md +1 -1
- package/docs/LEGACY_COMPATIBILITY.md +10 -6
- package/docs/MULTI_AGENT_RUNTIME.md +4 -4
- package/docs/ONBOARDING.md +16 -4
- package/docs/OPERATIONS.md +14 -1
- package/docs/PERMISSION_MODE.md +14 -9
- package/docs/REALTIME_COLLABORATION.md +1 -1
- package/docs/ROADMAP.md +113 -0
- package/docs/TRUST_MODEL.md +28 -7
- package/docs/USABILITY_AUDIT.md +5 -0
- package/docs/WHY_LATTICE.md +13 -5
- package/docs/WORKFLOW_DESIGNER.md +2 -2
- package/docs/kg-schema.md +57 -7
- package/docs/mcp-tools.md +93 -82
- package/docs/security-model.md +6 -3
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/graph/_kg_common/__init__.py +1 -54
- package/lattice_brain/graph/_kg_common/extraction.py +459 -105
- package/lattice_brain/graph/_kg_common/normalize.py +305 -0
- package/lattice_brain/graph/_kg_common/patterns.py +275 -0
- package/lattice_brain/graph/_kg_common/relations.py +12 -3
- package/lattice_brain/graph/_kg_common/sections.py +107 -0
- package/lattice_brain/graph/_kg_common/text.py +14 -450
- package/lattice_brain/graph/_kg_constants.py +7 -0
- package/lattice_brain/ingestion/__init__.py +6 -3
- package/lattice_brain/multimodal/__init__.py +9 -3
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/agent_worker_seam.py +44 -1
- package/latticeai/api/models.py +18 -110
- package/latticeai/api/search.py +7 -30
- package/latticeai/api/worker_compute.py +127 -106
- package/latticeai/api/worker_seams.py +17 -2
- package/latticeai/core/embedding_providers/__init__.py +16 -0
- package/latticeai/core/embedding_providers/autodetect.py +302 -0
- package/latticeai/core/embedding_providers/base.py +25 -0
- package/latticeai/core/embedding_providers/profiles.py +44 -0
- package/latticeai/core/embedding_providers/text.py +74 -8
- package/latticeai/core/http_origin.py +3 -3
- package/latticeai/core/messages.py +0 -5
- package/latticeai/core/policy.py +1 -6
- package/latticeai/core/quiet.py +1 -20
- package/latticeai/core/security.py +29 -83
- package/latticeai/core/sessions.py +95 -4
- package/latticeai/core/users.py +0 -38
- package/latticeai/core/vector_index/__init__.py +61 -0
- package/latticeai/core/vector_index/hnsw.py +383 -0
- package/latticeai/core/vector_index/sidecar.py +329 -0
- package/latticeai/models/router/catalog.py +2 -2
- package/latticeai/models/router/generation.py +176 -30
- package/latticeai/models/router/loading.py +150 -9
- package/latticeai/runtime/access_runtime.py +7 -4
- package/latticeai/runtime/brain_runtime.py +43 -9
- package/latticeai/runtime/build_phases/features.py +8 -31
- package/latticeai/runtime/build_phases/foundation.py +7 -16
- package/latticeai/runtime/build_phases/web.py +3 -3
- package/latticeai/runtime/build_phases/worker_profile.py +29 -27
- package/latticeai/runtime/runtime_context.py +0 -2
- package/latticeai/services/architecture_readiness.py +18 -19
- package/latticeai/services/process_audit.py +1 -22
- package/latticeai/services/product_readiness.py +39 -12
- package/latticeai/services/search_service.py +7 -0
- package/latticeai/services/voice_capture.py +8 -28
- package/latticeai/tools/__init__.py +12 -47
- package/latticeai/tools/commands.py +9 -15
- package/latticeai/tools/documents.py +12 -0
- package/latticeai/tools/knowledge.py +0 -6
- package/latticeai/tools/markup.py +152 -0
- package/package.json +4 -5
- package/requirements.txt +0 -1
- package/scripts/check_current_release_docs.mjs +1 -1
- package/scripts/check_openapi_drift.mjs +3 -2
- package/scripts/check_server_i18n.mjs +5 -4
- package/scripts/compose_openapi.py +4 -1
- package/scripts/export_openapi.py +5 -4
- package/scripts/gen_worker_allowlist_fixture.py +2 -2
- package/scripts/openapi_route_families.json +19 -74
- package/scripts/publish_release.mjs +157 -0
- package/scripts/release_screen_claims.json +144 -28
- package/src-tauri/Cargo.lock +45 -10
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +47 -41
- package/static/app/assets/Act-Cf1L2709.js +2 -0
- package/static/app/assets/AdminConsole-DPAbLTYV.js +1 -0
- package/static/app/assets/Brain-DqamGrj-.js +2 -0
- package/static/app/assets/BrainHome-MHe2_RYs.js +2 -0
- package/static/app/assets/BrainSignals-CQPPfyyH.js +1 -0
- package/static/app/assets/Capture-DGdIH_Zc.js +1 -0
- package/static/app/assets/Chronicle-C-UlCJoJ.js +1 -0
- package/static/app/assets/CommandPalette-WNT4EqUX.js +1 -0
- package/static/app/assets/DigitalBrainExplorer-CEBH5Cwc.js +321 -0
- package/static/app/assets/Library-C6xd1dlf.js +1 -0
- package/static/app/assets/LivingBrain-BEk-0ohw.js +1 -0
- package/static/app/assets/ProductFlow-CZLm5iXh.js +1 -0
- package/static/app/assets/QueryClientProvider-B3OjqSyJ.js +1 -0
- package/static/app/assets/{ReviewCard-HXRle3qq.js → ReviewCard-CEHG6evf.js} +2 -2
- package/static/app/assets/RunsListPanel-CLtEJSRW.js +1 -0
- package/static/app/assets/System-CAxwBUXw.js +1 -0
- package/static/app/assets/WorkflowGraph-Dj10RuGE.js +1 -0
- package/static/app/assets/WorkflowsPanel-Kyeh_LIT.js +2 -0
- package/static/app/assets/actHelpers-CtSmK9Dw.js +1 -0
- package/static/app/assets/arrow-left-CRl5EO4D.js +1 -0
- package/static/app/assets/{bot-Cn8bWRuq.js → bot-DhUGRel2.js} +1 -1
- package/static/app/assets/brain-CLkhHsHF.js +1 -0
- package/static/app/assets/button-CmaEqG1T.js +1 -0
- package/static/app/assets/circle-check-CFgejkOS.js +1 -0
- package/static/app/assets/{circle-pause-CmzC_apg.js → circle-pause-l96izbxj.js} +1 -1
- package/static/app/assets/{circle-play-D8mW2aQ7.js → circle-play-CrZa25_q.js} +1 -1
- package/static/app/assets/{cpu-DZcdd0PZ.js → cpu-BaXudqwl.js} +1 -1
- package/static/app/assets/{download-bv1KEPGQ.js → download-hCVFPiyc.js} +1 -1
- package/static/app/assets/{folder-open-d-Pip5gr.js → folder-open-CHL82Yp7.js} +1 -1
- package/static/app/assets/{hard-drive-D20iavUb.js → hard-drive-DDzET7lk.js} +1 -1
- package/static/app/assets/index-CB93CZWW.css +2 -0
- package/static/app/assets/index-D2H-wSl6.js +13 -0
- package/static/app/assets/input-Df1CAY_I.js +1 -0
- package/static/app/assets/jsx-runtime-bzQ4Vb5N.js +1 -0
- package/static/app/assets/{link-2-BPJOFlAy.js → link-2-xNnTIX1_.js} +1 -1
- package/static/app/assets/{permissionCopy-ChdJd493.js → permissionCopy-D3aWHco-.js} +1 -1
- package/static/app/assets/primitives-BioD2slS.js +1 -0
- package/static/app/assets/search-BzBw8YcW.js +1 -0
- package/static/app/assets/{share-2-YNX_NtMU.js → share-2-FkzGf8Df.js} +1 -1
- package/static/app/assets/{shield-alert-DuQ3zrVL.js → shield-alert-B3dwzik4.js} +1 -1
- package/static/app/assets/sourceMeta-DQSY_tah.js +1 -0
- package/static/app/assets/textarea-P8o6pvOP.js +1 -0
- package/static/app/assets/useFocusTrap-hswOIkXE.js +1 -0
- package/static/app/assets/useMutation-OJLrYSRA.js +1 -0
- package/static/app/assets/workspace-BCuk3Ku9.js +1 -0
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/lattice_brain/ingestion/pipeline.py +0 -108
- package/latticeai/api/local_files.py +0 -44
- package/latticeai/api/tools.py +0 -126
- package/latticeai/api/voice_capture.py +0 -32
- package/latticeai/core/agent_permission.py +0 -85
- package/scripts/agent_eval.py +0 -34
- package/scripts/brain_quality_eval.py +0 -37
- package/scripts/check_legacy_debt.mjs +0 -91
- package/scripts/check_python.py +0 -100
- package/scripts/chunking_parity_corpus.py +0 -449
- package/scripts/generate_agent_parity_fixtures.py +0 -771
- package/scripts/generate_chunking_parity_fixtures.py +0 -259
- package/static/app/assets/Act-BPcVAbOL.js +0 -1
- package/static/app/assets/AdminConsole-Bw1ATQL0.js +0 -1
- package/static/app/assets/Brain-CT92Kos0.js +0 -321
- package/static/app/assets/BrainHome-CFBkt1K_.js +0 -2
- package/static/app/assets/BrainSignals-ReLWF2H8.js +0 -1
- package/static/app/assets/Capture-BsTokYkk.js +0 -1
- package/static/app/assets/Chronicle-B6f0T9id.js +0 -1
- package/static/app/assets/CommandPalette-CuvjTv1u.js +0 -1
- package/static/app/assets/Library-BGJbG9Hd.js +0 -1
- package/static/app/assets/LivingBrain-DGYK_Jsa.js +0 -1
- package/static/app/assets/ProductFlow-DXBC6brE.js +0 -1
- package/static/app/assets/System-CMHSO9qM.js +0 -1
- package/static/app/assets/arrow-left-BfmkskWx.js +0 -1
- package/static/app/assets/brain-CQJberbE.js +0 -1
- package/static/app/assets/button-Ct9f2_oT.js +0 -1
- package/static/app/assets/circle-check-DruOxB-4.js +0 -1
- package/static/app/assets/index-D9x-kSNy.css +0 -2
- package/static/app/assets/index-Do83hDzJ.js +0 -10
- package/static/app/assets/input-BLXVNmj1.js +0 -1
- package/static/app/assets/primitives-Cv5tbZBY.js +0 -1
- package/static/app/assets/search-CT9aho2j.js +0 -1
- package/static/app/assets/textarea-DqwLnli4.js +0 -1
- package/static/app/assets/useFocusTrap-ZVI98jaW.js +0 -1
- package/static/app/assets/useMutation-CVC4qv_D.js +0 -1
- package/static/app/assets/useQuery-C7BeG4HU.js +0 -1
- package/static/app/assets/utils-CiFtIdZq.js +0 -4
- 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
|
|
7
|
-
into a transcript
|
|
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
|
|
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
|
|
34
|
-
|
|
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,
|
|
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
|
-
#:
|
|
111
|
-
#:
|
|
112
|
-
#:
|
|
113
|
-
|
|
114
|
-
|
|
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``
|
|
496
|
-
|
|
497
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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",
|