ltcai 11.5.0 → 11.5.2
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 +86 -47
- package/docs/CHANGELOG.md +76 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/HYBRID_CLOUD_KG_STREAMING.md +8 -4
- package/docs/MULTI_AGENT_RUNTIME.md +1 -1
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +1 -1
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/WORKFLOW_DESIGNER.md +1 -1
- package/docs/architecture.md +4 -4
- package/docs/kg-schema.md +1 -1
- package/docs/v11.5.1_RUST_FULL_LOOP_PLAN.md +85 -0
- package/docs/v11.5.2_TIGHT_SHIP_PLAN.md +144 -0
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/graph/proactive.py +1 -1
- package/lattice_brain/graph/projection/v2_schema.py +0 -28
- package/lattice_brain/graph/vector_index/base.py +0 -3
- package/lattice_brain/graph/vector_index/brute_force.py +0 -4
- package/lattice_brain/graph/vector_index/hnsw.py +0 -3
- package/lattice_brain/graph/vector_index/quantized.py +0 -3
- package/lattice_brain/ingestion_jobs.py +0 -3
- package/lattice_brain/memory.py +0 -27
- package/lattice_brain/portability/sharing.py +0 -4
- package/lattice_brain/quality.py +0 -49
- package/lattice_brain/runtime/agent_runtime.py +1 -1
- package/lattice_brain/runtime/hooks.py +0 -13
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/lattice_brain/sealed_box.py +0 -4
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +12 -12
- package/latticeai/api/agent_worker_seam.py +389 -0
- package/latticeai/api/auth.py +25 -2
- package/latticeai/api/chat.py +3 -3
- package/latticeai/api/chat_agent_http.py +3 -12
- package/latticeai/api/chat_helpers.py +10 -13
- package/latticeai/api/computer_use.py +31 -42
- package/latticeai/api/health.py +21 -1
- package/latticeai/api/local_files.py +14 -0
- package/latticeai/api/permissions.py +38 -12
- package/latticeai/api/search.py +24 -0
- package/latticeai/api/static_routes.py +4 -1
- package/latticeai/cli/runtime.py +7 -3
- package/latticeai/core/config.py +47 -3
- package/latticeai/core/csrf.py +28 -2
- package/latticeai/core/embedding_providers/__init__.py +3 -5
- package/latticeai/core/embedding_providers/base.py +1 -1
- package/latticeai/core/embedding_providers/text.py +1 -1
- package/latticeai/core/enterprise.py +1 -4
- package/latticeai/core/http_origin.py +146 -0
- package/latticeai/core/invitations.py +3 -2
- package/latticeai/core/io_utils.py +2 -11
- package/latticeai/core/legacy_compatibility.py +1 -1
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/messages.py +40 -0
- package/latticeai/core/model_compat.py +2 -8
- package/latticeai/core/module_probe.py +37 -0
- package/latticeai/core/run_explain.py +5 -2
- package/latticeai/core/run_store.py +2 -2
- package/latticeai/core/security.py +13 -0
- package/latticeai/core/sessions.py +3 -2
- package/latticeai/core/sse.py +25 -0
- package/latticeai/core/users.py +0 -9
- package/latticeai/core/workspace_graph_trace.py +2 -8
- package/latticeai/core/workspace_os.py +0 -10
- package/latticeai/core/workspace_os_constants.py +1 -1
- package/latticeai/core/workspace_os_utils.py +34 -0
- package/latticeai/core/workspace_review_items.py +2 -8
- package/latticeai/core/workspace_runs.py +2 -8
- package/latticeai/core/workspace_skills.py +2 -7
- package/latticeai/models/router/documents.py +0 -35
- package/latticeai/models/router/registry.py +0 -4
- package/latticeai/runtime/access_runtime.py +16 -4
- package/latticeai/runtime/build_phases/features.py +25 -2
- package/latticeai/runtime/build_phases/web.py +2 -0
- package/latticeai/runtime/feature_toggle_wiring.py +12 -18
- package/latticeai/runtime/network_boundary_wiring.py +8 -19
- package/latticeai/runtime/permission_mode_wiring.py +6 -13
- package/latticeai/runtime/router_registration.py +4 -0
- package/latticeai/runtime/runtime_context.py +1 -13
- package/latticeai/runtime/service_singletons.py +55 -0
- package/latticeai/services/architecture_readiness.py +1 -1
- package/latticeai/services/brain_intelligence/proposals.py +0 -5
- package/latticeai/services/change_proposals.py +2 -36
- package/latticeai/services/chronicle.py +4 -6
- package/latticeai/services/command_center.py +4 -4
- package/latticeai/services/evidence_actions.py +5 -2
- package/latticeai/services/hybrid_chat.py +2 -2
- package/latticeai/services/hybrid_policy.py +8 -57
- package/latticeai/services/mode_store.py +132 -0
- package/latticeai/services/model_capability_registry.py +0 -15
- package/latticeai/services/model_catalog.py +0 -7
- package/latticeai/services/model_loading.py +3 -2
- package/latticeai/services/model_runtime/__init__.py +0 -57
- package/latticeai/services/model_runtime/engines.py +27 -128
- package/latticeai/services/model_runtime/loading.py +2 -2
- package/latticeai/services/network_boundary_service.py +7 -44
- package/latticeai/services/permission_mode_service.py +8 -56
- package/latticeai/services/product_readiness.py +1 -1
- package/latticeai/services/setup_detection.py +67 -1
- package/latticeai/services/tool_dispatch.py +0 -4
- package/latticeai/services/upload_service.py +7 -20
- package/latticeai/services/workspace_service.py +0 -6
- package/latticeai/setup/auto_setup.py +15 -26
- package/latticeai/setup/wizard/catalog.py +5 -2
- package/latticeai/setup/wizard/detect.py +6 -27
- package/latticeai/setup/wizard/paths.py +2 -5
- package/latticeai/setup/wizard/plans.py +2 -2
- package/package.json +2 -4
- package/scripts/bump_version.py +14 -1
- package/scripts/check_current_release_docs.mjs +1 -1
- package/scripts/check_legacy_debt.mjs +1 -1
- package/scripts/check_server_i18n.mjs +1 -0
- package/scripts/generate_agent_loop_fixtures.py +994 -0
- package/scripts/generate_rust_parity_fixtures.py +72 -161
- package/scripts/parity_fixture_corpus_context.py +162 -0
- package/scripts/parity_fixture_corpus_docgen.py +341 -0
- package/scripts/release_screen_claims.json +20 -0
- package/src-tauri/Cargo.lock +13 -7
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/src/backend.rs +76 -2
- package/src-tauri/src/main.rs +13 -3
- package/src-tauri/tauri.conf.json +2 -2
- package/static/app/asset-manifest.json +41 -41
- package/static/app/assets/{Act-CWnxSCgN.js → Act-DcQizkl1.js} +1 -1
- package/static/app/assets/{AdminConsole-BEQYU6kF.js → AdminConsole-cf4npybT.js} +1 -1
- package/static/app/assets/{Brain-DWu1BhFg.js → Brain-3VCSHFcn.js} +1 -1
- package/static/app/assets/{BrainHome-95Hilr9R.js → BrainHome-Qm8eaztx.js} +1 -1
- package/static/app/assets/{BrainSignals-QdeqCpAF.js → BrainSignals-DS9BtKOW.js} +1 -1
- package/static/app/assets/{Capture-BHpCxnzb.js → Capture-DiQ219jW.js} +1 -1
- package/static/app/assets/{Chronicle-B4xYKoed.js → Chronicle-BGvuAchH.js} +1 -1
- package/static/app/assets/{CommandPalette-BVXnttSz.js → CommandPalette-Bqhm0Urn.js} +1 -1
- package/static/app/assets/{Library-DgYcHome.js → Library-BV6NnF0a.js} +1 -1
- package/static/app/assets/{LivingBrain-CrJLDbf7.js → LivingBrain-GzenJchP.js} +1 -1
- package/static/app/assets/{ProductFlow-DFlScKoJ.js → ProductFlow-DEP6-vML.js} +1 -1
- package/static/app/assets/{ReviewCard-Cy5f48Pj.js → ReviewCard-CNZ7XjWG.js} +1 -1
- package/static/app/assets/{System-NF8IfhTa.js → System-CieofHQa.js} +1 -1
- package/static/app/assets/arrow-left-kfsrk0mv.js +1 -0
- package/static/app/assets/{bot-CucuhLhm.js → bot-B_K1Tdmw.js} +1 -1
- package/static/app/assets/{brain-BBnSryW_.js → brain-DWyaV1L1.js} +1 -1
- package/static/app/assets/{button-C2GUj2Ai.js → button-aTn4s84A.js} +1 -1
- package/static/app/assets/circle-check-qqLug9nU.js +1 -0
- package/static/app/assets/{circle-pause-CbkWzBmG.js → circle-pause-xKgeGXkT.js} +1 -1
- package/static/app/assets/{circle-play-7lEaqHdJ.js → circle-play-DkT6tYPX.js} +1 -1
- package/static/app/assets/{cpu-DAlCXlIy.js → cpu-85xYObUC.js} +1 -1
- package/static/app/assets/{download-RNhuuJwh.js → download-B5Fm7YXo.js} +1 -1
- package/static/app/assets/{folder-open-CLW4odzM.js → folder-open-kk2Xa52u.js} +1 -1
- package/static/app/assets/{hard-drive-NKEiDIAJ.js → hard-drive-DkA3zBW_.js} +1 -1
- package/static/app/assets/{index-DMurvUuR.js → index-BMPdTmlY.js} +3 -3
- package/static/app/assets/index-DxmOfNRi.css +2 -0
- package/static/app/assets/{input-D2UhPC1X.js → input-B0nRf2jO.js} +1 -1
- package/static/app/assets/{link-2-6amKbP_P.js → link-2-Dwb4gnTc.js} +1 -1
- package/static/app/assets/{permissionCopy-Cu9TZtdR.js → permissionCopy-CQDUBrOZ.js} +1 -1
- package/static/app/assets/{primitives-gPsccucr.js → primitives-SNp0LRJz.js} +1 -1
- package/static/app/assets/search-BcHqkjoy.js +1 -0
- package/static/app/assets/{share-2-Bau7KkPq.js → share-2-BsrxFglO.js} +1 -1
- package/static/app/assets/{shield-alert-BufNYypi.js → shield-alert-5BStfp2_.js} +1 -1
- package/static/app/assets/{textarea-BQnVWhYs.js → textarea-Cg8IUA-k.js} +1 -1
- package/static/app/assets/{useFocusTrap-B3_w60si.js → useFocusTrap-CYKvE46M.js} +1 -1
- package/static/app/assets/{useMutation-BHhCflT6.js → useMutation-CSn9t1op.js} +1 -1
- package/static/app/assets/{useQuery-rBWfI-5t.js → useQuery-CY2OI2uy.js} +1 -1
- package/static/app/assets/{utils-V_5-wxr5.js → utils-Ddol2RWD.js} +1 -1
- package/static/app/assets/{workspace-K1zjYUHj.js → workspace-BqDwOz_p.js} +1 -1
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/desktop/electron/README.md +0 -9
- package/desktop/electron/main.cjs +0 -58
- package/desktop/electron/preload.cjs +0 -5
- package/latticeai/core/graph_curator.py +0 -11
- package/latticeai/core/hooks.py +0 -11
- package/latticeai/core/local_embeddings.py +0 -104
- package/latticeai/core/multi_agent.py +0 -11
- package/latticeai/core/workflow_engine.py +0 -11
- package/latticeai/services/ingestion.py +0 -11
- package/latticeai/services/kg_portability.py +0 -11
- package/latticeai/services/multimodal_streaming.py +0 -129
- package/scripts/measure_brain_home_fill.mjs +0 -141
- package/static/app/assets/arrow-left-DwkSYrjR.js +0 -1
- package/static/app/assets/circle-check-CxOVPwYq.js +0 -1
- package/static/app/assets/index-BLPb5lmE.css +0 -2
- package/static/app/assets/search-Cj_TKk_2.js +0 -1
|
@@ -6,27 +6,23 @@ something keeps proving it is still one. This script is the Python half of that
|
|
|
6
6
|
proof: it builds a small, fully deterministic Brain with the **real** write path
|
|
7
7
|
(``KnowledgeGraphStore._upsert_node`` / ``_upsert_chunk`` / ``_upsert_edge``,
|
|
8
8
|
the real hash embedder, the real v2 projection and trigram FTS index), then runs
|
|
9
|
-
the real
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
Determinism is the whole design constraint: every timestamp is written by the
|
|
27
|
-
real code and then **backdated**; the two ``datetime.now()`` calls the ports
|
|
28
|
-
reach are frozen at :data:`FROZEN_NOW` (recorded in the manifest); LLM concept
|
|
29
|
-
extraction is forced off; every environment knob is pinned to its default.
|
|
9
|
+
the real engines over it and writes their answers to ``rust/fixtures/golden/``.
|
|
10
|
+
The suite set widens with the port (:data:`SUITES`): the search engines
|
|
11
|
+
(v11.4.0); the KG relationship/traverse reads, the service layer, the durable
|
|
12
|
+
history reads and the context assembler (v11.5.0); the document-generation
|
|
13
|
+
search, traversal and context builder (v11.5.1, whose corpus and runners live
|
|
14
|
+
in ``scripts/parity_fixture_corpus_docgen.py``).
|
|
15
|
+
|
|
16
|
+
Two consumers read what it writes: ``tests/unit/test_rust_parity_contract.py``
|
|
17
|
+
re-runs the Python entry points against the committed database and asserts the
|
|
18
|
+
goldens still hold — so a change to Python semantics fails loudly instead of
|
|
19
|
+
silently invalidating the contract the Rust side is pinned to — and
|
|
20
|
+
``rust/lattice-retrieval/tests/{parity,suites}.rs`` run the Rust ports against
|
|
21
|
+
the same database and the same goldens. Determinism is the whole design
|
|
22
|
+
constraint: every timestamp is written by the real code and then **backdated**;
|
|
23
|
+
every ``datetime.now()`` the ports reach is frozen at :data:`FROZEN_NOW`
|
|
24
|
+
(recorded in the manifest); LLM concept extraction is forced off; every
|
|
25
|
+
environment knob is pinned to its default.
|
|
30
26
|
|
|
31
27
|
Usage::
|
|
32
28
|
|
|
@@ -47,8 +43,15 @@ from pathlib import Path
|
|
|
47
43
|
from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple
|
|
48
44
|
|
|
49
45
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
# The repo (for the product packages) and this directory: `scripts` is not a
|
|
47
|
+
# package, so the docgen corpus is imported by name off the script directory —
|
|
48
|
+
# the same by-path convention the contract test uses to import this file.
|
|
49
|
+
for _import_root in (REPO_ROOT, Path(__file__).resolve().parent):
|
|
50
|
+
if str(_import_root) not in sys.path:
|
|
51
|
+
sys.path.insert(0, str(_import_root))
|
|
52
|
+
|
|
53
|
+
import parity_fixture_corpus_context as context_corpus # noqa: E402
|
|
54
|
+
import parity_fixture_corpus_docgen as docgen # noqa: E402
|
|
52
55
|
|
|
53
56
|
FIXTURE_DIR = REPO_ROOT / "rust" / "fixtures"
|
|
54
57
|
GOLDEN_DIR = FIXTURE_DIR / "golden"
|
|
@@ -80,11 +83,11 @@ WS_BETA = "ws-beta"
|
|
|
80
83
|
# (node_id, type, title, summary, metadata, workspace_id, updated_at)
|
|
81
84
|
#
|
|
82
85
|
# Shaped on purpose: every ``type_boost`` type appears and so do types outside it;
|
|
83
|
-
# titles/summaries are half Korean (
|
|
84
|
-
#
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
-
#
|
|
86
|
+
# titles/summaries are half Korean (tokenizer, classifier and extractor all branch
|
|
87
|
+
# on script); the ``tie:`` block is five rows sharing one timestamp, one type and
|
|
88
|
+
# nothing to match, pinning the (hits, boost, updated_at) → id ASC tie-break; two
|
|
89
|
+
# workspaces plus NULL-workspace rows cover all three scoping answers (no scoping
|
|
90
|
+
# / empty set / a specific workspace). The v11.5.1 tail is in the docgen corpus.
|
|
88
91
|
NODES: List[Tuple[str, str, str, str, Dict[str, Any], Optional[str], str]] = [
|
|
89
92
|
("dec:fusion-alpha", "Decision", "Hybrid retrieval fusion stays alpha weighted",
|
|
90
93
|
"We decided the ranking keeps alpha fusion: lexical rank plus max normalized vector score.",
|
|
@@ -186,14 +189,14 @@ NODES: List[Tuple[str, str, str, str, Dict[str, Any], Optional[str], str]] = [
|
|
|
186
189
|
("legacy:global-note", "Document", "Legacy global note",
|
|
187
190
|
"A legacy row with no workspace, visible only with include_legacy_global.",
|
|
188
191
|
{}, None, "2026-06-20T10:00:00"),
|
|
192
|
+
*docgen.DOCGEN_NODES,
|
|
189
193
|
]
|
|
190
194
|
|
|
191
195
|
# (chunk_id, parent_node_id, index, node_title, text, chunk_fields, workspace, updated_at)
|
|
192
196
|
#
|
|
193
197
|
# The shape mirrors ``KnowledgeGraphIngestMixin``: every chunk is BOTH a ``Chunk``
|
|
194
|
-
# node (lexical lane + workspace scoping) and a ``chunks`` row with its own
|
|
195
|
-
#
|
|
196
|
-
# the whole reason chunk-heavy queries are in the query set.
|
|
198
|
+
# node (lexical lane + workspace scoping) and a ``chunks`` row with its own embedding
|
|
199
|
+
# (vector lane, rolled up to its parent) — the duality chunk-heavy queries pin.
|
|
197
200
|
CHUNKS: List[Tuple[str, str, int, str, str, Dict[str, Any], Optional[str], str]] = [
|
|
198
201
|
("chunk:handbook:1", "doc:handbook", 0, "handbook.pdf chunk 1",
|
|
199
202
|
"온보딩 체크리스트: 첫째, 폴더를 연결합니다. 둘째, 질문을 합니다. 셋째, 근거를 확인합니다.",
|
|
@@ -218,10 +221,10 @@ CHUNKS: List[Tuple[str, str, int, str, str, Dict[str, Any], Optional[str], str]]
|
|
|
218
221
|
# orders by ``weight DESC, created_at DESC, id ASC`` and ``traverse`` caps every BFS
|
|
219
222
|
# round with ``ORDER BY weight DESC, id ASC``, so an edge set sharing one weight and
|
|
220
223
|
# one clock proves nothing. Three shapes are deliberate — a weight tie broken by
|
|
221
|
-
# ``created_at`` (
|
|
222
|
-
#
|
|
223
|
-
#
|
|
224
|
-
#
|
|
224
|
+
# ``created_at`` (``org:lattice``), a weight *and* clock tie broken by edge id
|
|
225
|
+
# (``topic:quality``/``deck:review``), and legacy-global endpoints for scoping. The
|
|
226
|
+
# write door canonicalizes types (``relates_to``/``owns`` → ``MENTIONS``), which is
|
|
227
|
+
# why the goldens record uppercase names the fixture never spells.
|
|
225
228
|
EDGES: List[Tuple[str, str, str, float, str]] = [
|
|
226
229
|
("dec:fusion-alpha", "concept:retrieval", "mentions", 0.9, "2026-07-01T00:00:00"),
|
|
227
230
|
("dec:fusion-alpha", "concept:ranking", "mentions", 0.8, "2026-07-02T00:00:00"),
|
|
@@ -246,6 +249,7 @@ EDGES: List[Tuple[str, str, str, float, str]] = [
|
|
|
246
249
|
("task:onboarding-checklist", "page:onboarding", "relates_to", 0.15, "2026-05-01T00:00:00"),
|
|
247
250
|
("doc:handbook", "doc:retrieval-spec", "relates_to", 0.1, "2026-05-02T00:00:00"),
|
|
248
251
|
("tie:a", "tie:b", "relates_to", 1.0, "2026-07-03T00:00:00"),
|
|
252
|
+
*docgen.DOCGEN_EDGES,
|
|
249
253
|
]
|
|
250
254
|
|
|
251
255
|
# ── the conversation corpus (episodic memory, same database file) ────────────
|
|
@@ -257,7 +261,7 @@ EDGES: List[Tuple[str, str, str, float, str]] = [
|
|
|
257
261
|
# ``conversation_id`` (the ``legacy-previous-history`` bucket), a whitespace-only
|
|
258
262
|
# first message (the ``새 대화`` placeholder and its later upgrade), an
|
|
259
263
|
# assistant-first conversation (no upgrade), an empty timestamp (the ``or ""``
|
|
260
|
-
# fallbacks), extra keys ``metadata_json`` merges flat,
|
|
264
|
+
# fallbacks), extra keys ``metadata_json`` merges flat, ko/en content.
|
|
261
265
|
MESSAGES: List[Tuple[Any, ...]] = [
|
|
262
266
|
("conv-a", "user", "온보딩 체크리스트 어떻게 시작해?", "jiwon@lattice.ai", "지원", "web", "2026-07-20T09:00:00", WS_ALPHA, "org-1", {}),
|
|
263
267
|
("conv-a", "assistant", "먼저 폴더를 연결하세요. 그다음 질문하면 됩니다.", "jiwon@lattice.ai", None, "web", "2026-07-20T09:00:05", WS_ALPHA, "org-1", {}),
|
|
@@ -307,39 +311,20 @@ QUERIES: List[Dict[str, Any]] = [
|
|
|
307
311
|
{"key": "ws_alpha", "query": "hybrid retrieval ranking", "allowed": [WS_ALPHA]},
|
|
308
312
|
{"key": "ws_alpha_legacy", "query": "회의 결정 사항", "allowed": [WS_ALPHA], "legacy": True},
|
|
309
313
|
{"key": "ws_beta", "query": "온보딩 체크리스트", "allowed": [WS_BETA]},
|
|
310
|
-
# A vector floor nothing clears
|
|
311
|
-
#
|
|
314
|
+
# A vector floor nothing clears: the stale-embedder probe and the
|
|
315
|
+
# lexical-only fusion label, without breaking the store.
|
|
312
316
|
{"key": "min_vector_floor", "query": "hybrid retrieval ranking", "min_vector": 0.95},
|
|
313
317
|
# top_k small enough that the rerank window (top_k * 2) cuts the candidates.
|
|
314
318
|
{"key": "top_k_small", "query": "hybrid retrieval ranking", "top_k": 3},
|
|
315
|
-
# Pinned alpha: no policy, so no class
|
|
316
|
-
# query that would otherwise be recency-classed.
|
|
319
|
+
# Pinned alpha: no policy, so no class/rewrite/age decay on a recency query.
|
|
317
320
|
{"key": "alpha_pinned", "query": "지난주 회의 기록", "alpha": 0.2},
|
|
318
|
-
# A limit below the FTS hit count, so `ORDER BY rank LIMIT ?` decides which
|
|
319
|
-
#
|
|
320
|
-
# version difference between the two runtimes) is observable.
|
|
321
|
+
# A limit below the FTS hit count, so `ORDER BY rank LIMIT ?` decides which rows
|
|
322
|
+
# exist at all — the one place bm25 ordering (a SQLite version difference) shows.
|
|
321
323
|
{"key": "fts_rank_cut", "query": "Tie candidate", "limit": 2, "top_k": 2},
|
|
322
324
|
]
|
|
323
325
|
|
|
324
326
|
#: Every branch of the memories section: a workspace hit, one with no kind and an
|
|
325
327
|
#: empty snippet, a non-workspace row to drop, and one past the limit.
|
|
326
|
-
CONTEXT_MEMORIES: Dict[str, Any] = {
|
|
327
|
-
"results": [
|
|
328
|
-
{"id": "mem-1", "kind": "preference", "snippet": "답변은 한국어로", "score": 0.91, "source": "workspace"},
|
|
329
|
-
{"id": "mem-2", "kind": None, "snippet": "", "score": 0.0, "source": "workspace"},
|
|
330
|
-
{"id": "mem-3", "kind": "decision", "snippet": "ranking keeps alpha fusion", "score": 0.4, "source": "personal"},
|
|
331
|
-
{"id": "mem-4", "kind": "fact", "snippet": "온보딩은 다섯 걸음", "score": 0.3, "source": "workspace"},
|
|
332
|
-
]
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
#: A ledger with a pathless row, a non-dict row, and more than the ten-row cut.
|
|
336
|
-
CONTEXT_ARTIFACTS: List[Any] = [
|
|
337
|
-
{"path": "notes/ranking.md", "at": "2026-07-20T09:00:00", "run_id": "run-1"},
|
|
338
|
-
{"path": "notes/onboarding.md", "run_id": "run-2"},
|
|
339
|
-
{"path": "", "at": "2026-07-20T09:00:01"},
|
|
340
|
-
"not-a-dict",
|
|
341
|
-
] + [{"path": f"out/file-{index}.md", "at": None, "run_id": f"r{index}"} for index in range(10)]
|
|
342
|
-
|
|
343
328
|
#: The Phase-2/3 suites: one spec list per ported entry point.
|
|
344
329
|
SUITES: Dict[str, List[Dict[str, Any]]] = {
|
|
345
330
|
"relationship": [
|
|
@@ -403,8 +388,8 @@ SUITES: Dict[str, List[Dict[str, Any]]] = {
|
|
|
403
388
|
{"key": "en_code", "query": "vector_search() returns"},
|
|
404
389
|
{"key": "ko_person", "query": "담당자 누구"},
|
|
405
390
|
{"key": "en_filler", "query": " what is the retrieval specification please "},
|
|
406
|
-
# Explicit weights disable BOTH
|
|
407
|
-
#
|
|
391
|
+
# Explicit weights disable BOTH rewrite and age decay on a query that
|
|
392
|
+
# would otherwise get both — that asymmetry is the contract.
|
|
408
393
|
{"key": "pinned_recency", "query": "지난주 회의 기록", "weights": {"keyword": 0.5, "vector": 0.3, "graph": 0.2}},
|
|
409
394
|
{"key": "pinned_partial", "query": "hybrid retrieval ranking", "weights": {"graph": 1.0}},
|
|
410
395
|
{"key": "pinned_zero", "query": "회의 결정 사항", "weights": {"keyword": 0.0, "vector": 0.0, "graph": 0.0}},
|
|
@@ -461,21 +446,9 @@ SUITES: Dict[str, List[Dict[str, Any]]] = {
|
|
|
461
446
|
{"key": "limit_one", "query": "ranking", "limit": 1},
|
|
462
447
|
{"key": "scoped_strict", "query": "ranking", "allowed": [WS_BETA], "legacy": False},
|
|
463
448
|
],
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
{"key": "no_seams", "query": "hybrid retrieval ranking", "knowledge": False},
|
|
468
|
-
{"key": "memories_only", "query": "온보딩", "knowledge": False, "memories": CONTEXT_MEMORIES, "memory_limit": 2},
|
|
469
|
-
{"key": "artifacts_only", "query": "온보딩", "knowledge": False, "artifacts": CONTEXT_ARTIFACTS},
|
|
470
|
-
{"key": "notes_blank", "query": "온보딩", "knowledge": False, "notes": " "},
|
|
471
|
-
{"key": "recent_conversation", "query": "온보딩", "knowledge": False, "recent": {"conversation_id": "conv-a", "limit": 10}},
|
|
472
|
-
{"key": "recent_personal_workspace", "query": "온보딩", "knowledge": False, "recent": {"workspace_id": "personal", "limit": 6}},
|
|
473
|
-
{"key": "recent_user_scoped", "query": "온보딩", "knowledge": False, "recent": {"user_email": "jiwon@lattice.ai", "limit": 5}},
|
|
474
|
-
{"key": "budget_tiny", "query": "회의 결정 사항", "memories": CONTEXT_MEMORIES, "artifacts": CONTEXT_ARTIFACTS, "notes": "정원 노트: 랭킹은 alpha 융합을 유지한다.", "recent": {"limit": 4}, "budget": 20},
|
|
475
|
-
{"key": "budget_one", "query": "회의 결정 사항", "memories": CONTEXT_MEMORIES, "artifacts": CONTEXT_ARTIFACTS, "notes": "노트", "recent": {"limit": 4}, "budget": 1},
|
|
476
|
-
{"key": "budget_zero", "query": "회의 결정 사항", "memories": CONTEXT_MEMORIES, "notes": "노트", "budget": 0},
|
|
477
|
-
{"key": "knowledge_limit_one", "query": "hybrid retrieval ranking", "knowledge_limit": 1},
|
|
478
|
-
],
|
|
449
|
+
**context_corpus.CONTEXT_SUITES,
|
|
450
|
+
# v11.5.1: the document-generation ports, specs and runners alike.
|
|
451
|
+
**docgen.DOCGEN_SUITES,
|
|
479
452
|
}
|
|
480
453
|
|
|
481
454
|
#: Texts whose tokenizer output, hashes and vectors pin the embedding port.
|
|
@@ -517,10 +490,12 @@ def pinned_environment() -> Iterator[None]:
|
|
|
517
490
|
def frozen_clock() -> Iterator[None]:
|
|
518
491
|
"""Freeze every ported ``datetime.now()`` at :data:`FROZEN_NOW`.
|
|
519
492
|
|
|
520
|
-
Only the recency
|
|
521
|
-
|
|
522
|
-
|
|
493
|
+
Only the recency decay reads the clock, through the ``datetime`` name its
|
|
494
|
+
module imported, so rebinding that name is the whole patch. Three modules
|
|
495
|
+
reach it: the graph-layer and service-layer ``hybrid_search``, and the
|
|
496
|
+
document-generation search's own 14-day half-life.
|
|
523
497
|
"""
|
|
498
|
+
from lattice_brain.graph import retrieval_docgen as docgen_module
|
|
524
499
|
from lattice_brain.graph.retrieval import hybrid as hybrid_module
|
|
525
500
|
from latticeai.services import search_service as service_module
|
|
526
501
|
|
|
@@ -531,7 +506,7 @@ def frozen_clock() -> Iterator[None]:
|
|
|
531
506
|
def now(cls, tz=None): # noqa: ARG003 — mirrors datetime.now's signature
|
|
532
507
|
return frozen
|
|
533
508
|
|
|
534
|
-
modules = (hybrid_module, service_module)
|
|
509
|
+
modules = (hybrid_module, service_module, docgen_module)
|
|
535
510
|
originals = [module.datetime for module in modules]
|
|
536
511
|
for module in modules:
|
|
537
512
|
module.datetime = _FrozenDatetime
|
|
@@ -546,8 +521,8 @@ def frozen_clock() -> Iterator[None]:
|
|
|
546
521
|
def rules_only_extraction() -> Iterator[None]:
|
|
547
522
|
"""Force ``_topic_candidates`` down its rule-based path.
|
|
548
523
|
|
|
549
|
-
The LLM path needs a bound router no fixture run has, but "no router
|
|
550
|
-
to be bound" is an accident
|
|
524
|
+
The LLM path needs a bound router no fixture run has, but "no router
|
|
525
|
+
happens to be bound" is an accident this contract cannot rest on.
|
|
551
526
|
"""
|
|
552
527
|
from lattice_brain.graph._kg_common import extraction
|
|
553
528
|
|
|
@@ -618,13 +593,12 @@ def build_store(db_path: Path) -> None:
|
|
|
618
593
|
for from_node, to_node, edge_type, weight, stamp in EDGES:
|
|
619
594
|
# ``_upsert_edge`` stamps ``created_at`` from the wall clock in BOTH
|
|
620
595
|
# tables, and the read path is the ``kgv2_edges`` view over ``edges_v2``:
|
|
621
|
-
# backdating only the legacy table
|
|
622
|
-
# the relationship ordering moving with the clock.
|
|
596
|
+
# backdating only the legacy table left the ordering moving with the clock.
|
|
623
597
|
edge_id = store._upsert_edge(conn, from_node, to_node, edge_type, weight, {})
|
|
624
598
|
conn.execute("UPDATE edges SET created_at=? WHERE id=?", (stamp, edge_id))
|
|
625
599
|
conn.execute("UPDATE edges_v2 SET created_at=? WHERE id=?", (stamp, edge_id))
|
|
626
|
-
# ``indexed_at`` decides the candidate scan order (and, when the cap
|
|
627
|
-
# which candidates exist
|
|
600
|
+
# ``indexed_at`` decides the candidate scan order (and, when the cap
|
|
601
|
+
# bites, which candidates exist), so it is assigned rather than inherited.
|
|
628
602
|
item_ids = [row["item_id"] for row in conn.execute(
|
|
629
603
|
"SELECT item_id FROM vector_embeddings ORDER BY item_id ASC"
|
|
630
604
|
).fetchall()]
|
|
@@ -636,8 +610,8 @@ def build_store(db_path: Path) -> None:
|
|
|
636
610
|
store.record_embedder_fingerprint()
|
|
637
611
|
write_conversations(db_path)
|
|
638
612
|
|
|
639
|
-
#
|
|
640
|
-
#
|
|
613
|
+
# One self-contained file: checkpoint the WAL, drop to a rollback journal
|
|
614
|
+
# so no sidecar is committed, and compact.
|
|
641
615
|
with sqlite3.connect(str(db_path)) as conn:
|
|
642
616
|
conn.execute("PRAGMA wal_checkpoint(TRUNCATE)")
|
|
643
617
|
conn.execute("PRAGMA journal_mode=DELETE")
|
|
@@ -678,18 +652,18 @@ def run_engine(store, engine: str, spec: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
678
652
|
return ENGINES[engine](store, spec)
|
|
679
653
|
|
|
680
654
|
|
|
681
|
-
# ── suites (v11.5.0)
|
|
655
|
+
# ── the suites beyond search (v11.5.0/v11.5.1) ───────────────────────────────
|
|
682
656
|
#
|
|
683
657
|
# The Phase-1 engines share one query shape (a query set × an engine set); the
|
|
684
|
-
#
|
|
685
|
-
#
|
|
658
|
+
# later ports do not, so each gets its own spec list and runner, both carried in
|
|
659
|
+
# the manifest so Rust and Python enumerate exactly the same work.
|
|
686
660
|
|
|
687
661
|
|
|
688
662
|
class Harness:
|
|
689
663
|
"""Every Python entry point the v11.5.0 goldens are produced from.
|
|
690
664
|
|
|
691
665
|
``require_auth=False`` is the loopback-owner configuration the native routes
|
|
692
|
-
reproduce: the history scope is whatever the caller passes
|
|
666
|
+
reproduce: the history scope is whatever the caller passes.
|
|
693
667
|
"""
|
|
694
668
|
|
|
695
669
|
def __init__(self, db_path: Path):
|
|
@@ -718,8 +692,7 @@ def _history_scope(spec: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
718
692
|
"""The identity/workspace scope every history read takes.
|
|
719
693
|
|
|
720
694
|
``include_legacy_global`` defaults to ``True`` — ``ConversationStore``'s own
|
|
721
|
-
default, the opposite of the graph layer's and
|
|
722
|
-
gets wrong.
|
|
695
|
+
default, the opposite of the graph layer's and an asymmetry a port gets wrong.
|
|
723
696
|
"""
|
|
724
697
|
allowed = spec.get("allowed")
|
|
725
698
|
return {
|
|
@@ -796,68 +769,6 @@ def _run_history_search(h: Harness, spec: Dict[str, Any]) -> List[Dict[str, Any]
|
|
|
796
769
|
)
|
|
797
770
|
|
|
798
771
|
|
|
799
|
-
def _context_seams(h: Harness, spec: Dict[str, Any]) -> Dict[str, Any]:
|
|
800
|
-
"""The seam set for one context spec — data seams plus the real engines.
|
|
801
|
-
|
|
802
|
-
``memories`` / ``artifacts`` / ``notes`` are *data* seams: the payload is the
|
|
803
|
-
spec, so both runtimes feed the assembler the same bytes and what is under
|
|
804
|
-
test is the assembler. ``knowledge`` and ``recent`` are real — the
|
|
805
|
-
service-layer hybrid search and the durable history reader.
|
|
806
|
-
"""
|
|
807
|
-
from latticeai.api.chat_helpers import build_recent_chat_context
|
|
808
|
-
|
|
809
|
-
# Signatures matter: the assembler inspects them to decide which context
|
|
810
|
-
# fields a seam may be handed, so each one declares exactly what it accepts.
|
|
811
|
-
seams: Dict[str, Any] = {}
|
|
812
|
-
if spec.get("memories") is not None:
|
|
813
|
-
memories = spec["memories"]
|
|
814
|
-
seams["memory_recall"] = (
|
|
815
|
-
lambda query, *, user_email=None, workspace_id=None, limit=5: memories
|
|
816
|
-
)
|
|
817
|
-
if spec.get("artifacts") is not None:
|
|
818
|
-
artifacts = spec["artifacts"]
|
|
819
|
-
seams["recent_artifacts"] = (
|
|
820
|
-
lambda *, user_email=None, conversation_id=None, workspace_id=None: artifacts
|
|
821
|
-
)
|
|
822
|
-
if spec.get("knowledge", True):
|
|
823
|
-
# Loopback trust: no workspace scoping, exactly as on the native route.
|
|
824
|
-
seams["hybrid_search"] = (
|
|
825
|
-
lambda query, *, limit=5, user_email=None, workspace_id=None:
|
|
826
|
-
h.service.hybrid_search(query, limit=limit)
|
|
827
|
-
)
|
|
828
|
-
if spec.get("notes") is not None:
|
|
829
|
-
notes = spec["notes"]
|
|
830
|
-
seams["notes_context"] = lambda query, *, user_email=None, workspace_id=None: notes
|
|
831
|
-
if spec.get("recent") is not None:
|
|
832
|
-
recent = spec["recent"]
|
|
833
|
-
seams["recent_chat"] = (
|
|
834
|
-
lambda *, user_email=None, conversation_id=None, workspace_id=None:
|
|
835
|
-
build_recent_chat_context(
|
|
836
|
-
get_history=h.history_runtime["get_history"],
|
|
837
|
-
limit=recent.get("limit", 10),
|
|
838
|
-
include_image_missing_replies=recent.get("images", True),
|
|
839
|
-
user_email=recent.get("user_email"),
|
|
840
|
-
conversation_id=recent.get("conversation_id"),
|
|
841
|
-
workspace_id=recent.get("workspace_id"),
|
|
842
|
-
)
|
|
843
|
-
)
|
|
844
|
-
return seams
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
def _run_context_assemble(h: Harness, spec: Dict[str, Any]) -> Dict[str, Any]:
|
|
848
|
-
from lattice_brain.context import ContextAssembler
|
|
849
|
-
|
|
850
|
-
assembled = ContextAssembler(**_context_seams(h, spec)).assemble(
|
|
851
|
-
spec["query"], user_email=spec.get("user_email"),
|
|
852
|
-
workspace_id=spec.get("workspace_id"),
|
|
853
|
-
conversation_id=spec.get("conversation_id"), budget=spec.get("budget", 2000),
|
|
854
|
-
memory_limit=spec.get("memory_limit", 5),
|
|
855
|
-
knowledge_limit=spec.get("knowledge_limit", 5),
|
|
856
|
-
)
|
|
857
|
-
return {"text": assembled.text, "approx_tokens": assembled.approx_tokens,
|
|
858
|
-
"trace": assembled.trace()}
|
|
859
|
-
|
|
860
|
-
|
|
861
772
|
SUITE_RUNNERS: Dict[str, Callable[[Harness, Dict[str, Any]], Any]] = {
|
|
862
773
|
"relationship": _run_relationship,
|
|
863
774
|
"traverse": _run_traverse,
|
|
@@ -867,7 +778,8 @@ SUITE_RUNNERS: Dict[str, Callable[[Harness, Dict[str, Any]], Any]] = {
|
|
|
867
778
|
"conversations": _run_conversations,
|
|
868
779
|
"conversation_messages": _run_conversation_messages,
|
|
869
780
|
"history_search": _run_history_search,
|
|
870
|
-
|
|
781
|
+
**context_corpus.CONTEXT_RUNNERS,
|
|
782
|
+
**docgen.DOCGEN_RUNNERS,
|
|
871
783
|
}
|
|
872
784
|
|
|
873
785
|
|
|
@@ -901,7 +813,7 @@ def suite_payload(suite: str, spec: Dict[str, Any], result: Any) -> Dict[str, An
|
|
|
901
813
|
"""One suite golden: the spec that produced it, verbatim, and the answer.
|
|
902
814
|
|
|
903
815
|
The spec rides along rather than being flattened into a fixed ``params``
|
|
904
|
-
block
|
|
816
|
+
block — these entry points share no parameter shape.
|
|
905
817
|
"""
|
|
906
818
|
return {"suite": suite, "key": spec["key"], "spec": spec, "result": result}
|
|
907
819
|
|
|
@@ -982,8 +894,7 @@ def main() -> int:
|
|
|
982
894
|
_dump(GOLDEN_DIR / "embeddings_golden.json", embeddings_golden())
|
|
983
895
|
_dump(GOLDEN_DIR / "rounding_golden.json", rounding_golden())
|
|
984
896
|
_dump(GOLDEN_DIR / "manifest.json", manifest())
|
|
985
|
-
#
|
|
986
|
-
# fixture has no blobs; an empty directory beside an artefact is just noise.
|
|
897
|
+
# The store creates its blob directory eagerly and the fixture has no blobs.
|
|
987
898
|
blob_dir = STORE_PATH.parent / "blobs"
|
|
988
899
|
if blob_dir.is_dir() and not any(blob_dir.iterdir()):
|
|
989
900
|
blob_dir.rmdir()
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""The context-assembly half of the Python↔Rust parity corpus (v11.5.2).
|
|
3
|
+
|
|
4
|
+
``scripts/generate_rust_parity_fixtures.py`` owns the store, the harness and the
|
|
5
|
+
goldens; this module owns the specs and runners for the two context ports, so
|
|
6
|
+
neither file grows past the tree's file-size ceiling. The split follows the
|
|
7
|
+
``parity_fixture_corpus_docgen`` precedent: specs and their runners live side by
|
|
8
|
+
side, because a spec list that arrives without the runner that answers it is a
|
|
9
|
+
suite that silently checks nothing.
|
|
10
|
+
|
|
11
|
+
Two ports, deliberately separate:
|
|
12
|
+
|
|
13
|
+
* **``context_assemble``** pins :class:`~lattice_brain.context.ContextAssembler`
|
|
14
|
+
over the seams the *production* assembler supplies
|
|
15
|
+
(``latticeai/runtime/context_runtime.py``): memories, artifacts, hybrid
|
|
16
|
+
knowledge and garden notes. It does **not** supply ``recent_chat`` — that seam
|
|
17
|
+
has never been wired live, and a golden that pinned it claimed cross-runtime
|
|
18
|
+
agreement about a code path the product does not execute.
|
|
19
|
+
* **``recent_chat``** pins the recent-conversation transcript as the product
|
|
20
|
+
really builds it: ``chat.py`` prepends ``build_recent_chat_context(...)`` on
|
|
21
|
+
every non-hybrid turn and the agent loop injects the same text each executor
|
|
22
|
+
step. Its Rust twin is ``lattice_retrieval::history::recent_chat``.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from typing import Any, Callable, Dict, List
|
|
28
|
+
|
|
29
|
+
#: ``h`` is the generator's ``Harness``. It is typed ``Any`` rather than
|
|
30
|
+
#: imported: the harness owns the live store and the service layer, so importing
|
|
31
|
+
#: it here would make the corpus depend on the generator that imports *it*.
|
|
32
|
+
|
|
33
|
+
#: The organization workspace the corpus seeds; the same literal the generator
|
|
34
|
+
#: and the docgen corpus use.
|
|
35
|
+
WS_ALPHA = "ws-alpha"
|
|
36
|
+
|
|
37
|
+
CONTEXT_MEMORIES: Dict[str, Any] = {
|
|
38
|
+
"results": [
|
|
39
|
+
{"id": "mem-1", "kind": "preference", "snippet": "답변은 한국어로", "score": 0.91, "source": "workspace"},
|
|
40
|
+
{"id": "mem-2", "kind": None, "snippet": "", "score": 0.0, "source": "workspace"},
|
|
41
|
+
{"id": "mem-3", "kind": "decision", "snippet": "ranking keeps alpha fusion", "score": 0.4, "source": "personal"},
|
|
42
|
+
{"id": "mem-4", "kind": "fact", "snippet": "온보딩은 다섯 걸음", "score": 0.3, "source": "workspace"},
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
#: A ledger with a pathless row, a non-dict row, and more than the ten-row cut.
|
|
47
|
+
CONTEXT_ARTIFACTS: List[Any] = [
|
|
48
|
+
{"path": "notes/ranking.md", "at": "2026-07-20T09:00:00", "run_id": "run-1"},
|
|
49
|
+
{"path": "notes/onboarding.md", "run_id": "run-2"},
|
|
50
|
+
{"path": "", "at": "2026-07-20T09:00:01"},
|
|
51
|
+
"not-a-dict",
|
|
52
|
+
] + [{"path": f"out/file-{index}.md", "at": None, "run_id": f"r{index}"} for index in range(10)]
|
|
53
|
+
|
|
54
|
+
def _context_seams(h: Any, spec: Dict[str, Any]) -> Dict[str, Any]:
|
|
55
|
+
"""The seam set for one context spec — data seams plus the real engines.
|
|
56
|
+
|
|
57
|
+
``memories`` / ``artifacts`` / ``notes`` are *data* seams: the payload is the
|
|
58
|
+
spec, so both runtimes feed the assembler the same bytes and what is under
|
|
59
|
+
test is the assembler. ``knowledge`` is a real engine — the service-layer
|
|
60
|
+
hybrid search.
|
|
61
|
+
|
|
62
|
+
There is deliberately no ``recent_chat`` seam here: the production
|
|
63
|
+
assembler does not supply one, so a golden that did would pin agreement
|
|
64
|
+
about a path neither runtime reaches in the product. The recent transcript
|
|
65
|
+
is pinned by the ``recent_chat`` suite instead.
|
|
66
|
+
"""
|
|
67
|
+
# Signatures matter: the assembler inspects them to decide which context
|
|
68
|
+
# fields a seam may be handed, so each one declares exactly what it accepts.
|
|
69
|
+
seams: Dict[str, Any] = {}
|
|
70
|
+
if spec.get("memories") is not None:
|
|
71
|
+
memories = spec["memories"]
|
|
72
|
+
seams["memory_recall"] = (
|
|
73
|
+
lambda query, *, user_email=None, workspace_id=None, limit=5: memories
|
|
74
|
+
)
|
|
75
|
+
if spec.get("artifacts") is not None:
|
|
76
|
+
artifacts = spec["artifacts"]
|
|
77
|
+
seams["recent_artifacts"] = (
|
|
78
|
+
lambda *, user_email=None, conversation_id=None, workspace_id=None: artifacts
|
|
79
|
+
)
|
|
80
|
+
if spec.get("knowledge", True):
|
|
81
|
+
# Loopback trust: no workspace scoping, exactly as on the native route.
|
|
82
|
+
seams["hybrid_search"] = (
|
|
83
|
+
lambda query, *, limit=5, user_email=None, workspace_id=None:
|
|
84
|
+
h.service.hybrid_search(query, limit=limit)
|
|
85
|
+
)
|
|
86
|
+
if spec.get("notes") is not None:
|
|
87
|
+
notes = spec["notes"]
|
|
88
|
+
seams["notes_context"] = lambda query, *, user_email=None, workspace_id=None: notes
|
|
89
|
+
return seams
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _run_context_assemble(h: Any, spec: Dict[str, Any]) -> Dict[str, Any]:
|
|
93
|
+
from lattice_brain.context import ContextAssembler
|
|
94
|
+
|
|
95
|
+
assembled = ContextAssembler(**_context_seams(h, spec)).assemble(
|
|
96
|
+
spec["query"], user_email=spec.get("user_email"),
|
|
97
|
+
workspace_id=spec.get("workspace_id"),
|
|
98
|
+
conversation_id=spec.get("conversation_id"), budget=spec.get("budget", 2000),
|
|
99
|
+
memory_limit=spec.get("memory_limit", 5),
|
|
100
|
+
knowledge_limit=spec.get("knowledge_limit", 5),
|
|
101
|
+
)
|
|
102
|
+
return {"text": assembled.text, "approx_tokens": assembled.approx_tokens,
|
|
103
|
+
"trace": assembled.trace()}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _run_recent_chat(h: Any, spec: Dict[str, Any]) -> str:
|
|
107
|
+
"""The recent-conversation transcript, exactly as ``/chat`` builds it."""
|
|
108
|
+
from latticeai.api.chat_helpers import build_recent_chat_context
|
|
109
|
+
|
|
110
|
+
return build_recent_chat_context(
|
|
111
|
+
get_history=h.history_runtime["get_history"],
|
|
112
|
+
limit=spec.get("limit", 10),
|
|
113
|
+
include_image_missing_replies=spec.get("images", True),
|
|
114
|
+
user_email=spec.get("user_email"),
|
|
115
|
+
conversation_id=spec.get("conversation_id"),
|
|
116
|
+
workspace_id=spec.get("workspace_id"),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
#: The two context suites, as the generator merges them into :data:`SUITES` /
|
|
121
|
+
#: :data:`SUITE_RUNNERS`.
|
|
122
|
+
CONTEXT_SUITES: Dict[str, List[Dict[str, Any]]] = {
|
|
123
|
+
# 11.5.2: these pin the assembler over the seams the *production* assembler
|
|
124
|
+
# supplies (`latticeai/runtime/context_runtime.py`). The `recent_chat` seam
|
|
125
|
+
# is not one of them — it has never been wired live — so pinning it here
|
|
126
|
+
# claimed cross-runtime agreement about a code path the product does not
|
|
127
|
+
# run. The recent-conversation text itself is still pinned, in the
|
|
128
|
+
# `recent_chat` suite below, against the function chat really calls.
|
|
129
|
+
"context_assemble": [
|
|
130
|
+
{"key": "all_seams", "query": "회의 결정 사항", "memories": CONTEXT_MEMORIES, "artifacts": CONTEXT_ARTIFACTS, "notes": "정원 노트: 랭킹은 alpha 융합을 유지한다."},
|
|
131
|
+
{"key": "knowledge_only", "query": "hybrid retrieval ranking"},
|
|
132
|
+
{"key": "no_seams", "query": "hybrid retrieval ranking", "knowledge": False},
|
|
133
|
+
{"key": "memories_only", "query": "온보딩", "knowledge": False, "memories": CONTEXT_MEMORIES, "memory_limit": 2},
|
|
134
|
+
{"key": "artifacts_only", "query": "온보딩", "knowledge": False, "artifacts": CONTEXT_ARTIFACTS},
|
|
135
|
+
{"key": "notes_blank", "query": "온보딩", "knowledge": False, "notes": " "},
|
|
136
|
+
{"key": "budget_tiny", "query": "회의 결정 사항", "memories": CONTEXT_MEMORIES, "artifacts": CONTEXT_ARTIFACTS, "notes": "정원 노트: 랭킹은 alpha 융합을 유지한다.", "budget": 20},
|
|
137
|
+
{"key": "budget_one", "query": "회의 결정 사항", "memories": CONTEXT_MEMORIES, "artifacts": CONTEXT_ARTIFACTS, "notes": "노트", "budget": 1},
|
|
138
|
+
{"key": "budget_zero", "query": "회의 결정 사항", "memories": CONTEXT_MEMORIES, "notes": "노트", "budget": 0},
|
|
139
|
+
{"key": "knowledge_limit_one", "query": "hybrid retrieval ranking", "knowledge_limit": 1},
|
|
140
|
+
],
|
|
141
|
+
# The recent-conversation transcript as the product actually builds it:
|
|
142
|
+
# `chat.py` prepends `build_recent_chat_context(...)` on every non-hybrid
|
|
143
|
+
# turn, and the agent loop injects the same text each executor step. This
|
|
144
|
+
# is the live twin of `lattice_retrieval::history::recent_chat`.
|
|
145
|
+
"recent_chat": [
|
|
146
|
+
{"key": "everything"},
|
|
147
|
+
{"key": "conversation", "conversation_id": "conv-a"},
|
|
148
|
+
{"key": "conversation_missing", "conversation_id": "nope"},
|
|
149
|
+
{"key": "personal_workspace", "workspace_id": "personal", "limit": 6},
|
|
150
|
+
{"key": "workspace_alpha", "workspace_id": WS_ALPHA},
|
|
151
|
+
{"key": "user_scoped", "user_email": "jiwon@lattice.ai", "limit": 5},
|
|
152
|
+
{"key": "user_and_conversation", "user_email": "jiwon@lattice.ai", "conversation_id": "conv-a"},
|
|
153
|
+
{"key": "limit_one", "limit": 1},
|
|
154
|
+
{"key": "limit_zero", "limit": 0},
|
|
155
|
+
{"key": "image_replies_dropped", "images": False},
|
|
156
|
+
],
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
CONTEXT_RUNNERS: Dict[str, Callable[..., Any]] = {
|
|
160
|
+
"context_assemble": _run_context_assemble,
|
|
161
|
+
"recent_chat": _run_recent_chat,
|
|
162
|
+
}
|