ltcai 11.2.0 → 11.4.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 +46 -53
- package/docs/CHANGELOG.md +61 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/MULTI_AGENT_RUNTIME.md +1 -1
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +6 -2
- package/docs/PERMISSION_MODE.md +1 -1
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/kg-schema.md +2 -2
- package/docs/v11.3.0_PLAN.md +202 -0
- package/docs/v11.4.0_RUST_FOUNDATION_PLAN.md +176 -0
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/graph/_kg_common/__init__.py +287 -0
- package/lattice_brain/graph/_kg_common/extraction.py +516 -0
- package/lattice_brain/graph/_kg_common/relations.py +161 -0
- package/lattice_brain/graph/_kg_common/text.py +479 -0
- package/lattice_brain/graph/discovery_index/__init__.py +35 -0
- package/lattice_brain/graph/discovery_index/cleanup.py +182 -0
- package/lattice_brain/graph/discovery_index/extract.py +137 -0
- package/lattice_brain/graph/discovery_index/scan.py +411 -0
- package/lattice_brain/graph/discovery_index/upsert.py +495 -0
- package/lattice_brain/graph/projection/__init__.py +42 -0
- package/lattice_brain/graph/projection/curation.py +500 -0
- package/lattice_brain/graph/{projection.py → projection/v2_schema.py} +15 -477
- package/lattice_brain/graph/retrieval/__init__.py +54 -0
- package/lattice_brain/graph/retrieval/context.py +197 -0
- package/lattice_brain/graph/retrieval/graph_view.py +319 -0
- package/lattice_brain/graph/retrieval/hybrid.py +488 -0
- package/lattice_brain/graph/retrieval/maintenance.py +121 -0
- package/lattice_brain/graph/retrieval/signals.py +95 -0
- package/lattice_brain/graph/retrieval_vector/__init__.py +42 -0
- package/lattice_brain/graph/retrieval_vector/fingerprint.py +97 -0
- package/lattice_brain/graph/retrieval_vector/indexing.py +347 -0
- package/lattice_brain/graph/retrieval_vector/search.py +560 -0
- package/lattice_brain/graph/retrieval_vector/status.py +374 -0
- package/lattice_brain/ingestion/__init__.py +130 -0
- package/lattice_brain/ingestion/_contract.py +90 -0
- package/lattice_brain/ingestion/constants.py +127 -0
- package/lattice_brain/ingestion/folder_scan.py +57 -0
- package/lattice_brain/ingestion/folders.py +258 -0
- package/lattice_brain/ingestion/hashing.py +26 -0
- package/lattice_brain/ingestion/jobs_api.py +107 -0
- package/lattice_brain/ingestion/models.py +80 -0
- package/lattice_brain/ingestion/pipeline.py +486 -0
- package/lattice_brain/ingestion/quality.py +209 -0
- package/lattice_brain/ingestion/routing.py +295 -0
- package/lattice_brain/multimodal/__init__.py +164 -0
- package/lattice_brain/multimodal/audio.py +77 -0
- package/lattice_brain/multimodal/common.py +118 -0
- package/lattice_brain/multimodal/images.py +498 -0
- package/lattice_brain/multimodal/ports.py +169 -0
- package/lattice_brain/multimodal/video.py +410 -0
- package/lattice_brain/portability/__init__.py +90 -0
- package/lattice_brain/portability/_contract.py +42 -0
- package/lattice_brain/portability/backups.py +338 -0
- package/lattice_brain/portability/bundles.py +136 -0
- package/lattice_brain/portability/constants.py +93 -0
- package/lattice_brain/portability/fsops.py +138 -0
- package/lattice_brain/portability/service.py +41 -0
- package/lattice_brain/{portability.py → portability/sharing.py} +44 -677
- package/lattice_brain/runtime/__init__.py +1 -1
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/chronicle.py +63 -0
- package/latticeai/core/agent/__init__.py +93 -0
- package/latticeai/core/agent/_contract.py +79 -0
- package/latticeai/core/agent/context.py +57 -0
- package/latticeai/core/agent/deps.py +125 -0
- package/latticeai/core/agent/execution.py +622 -0
- package/latticeai/core/agent/planning.py +145 -0
- package/latticeai/core/agent/recovery.py +157 -0
- package/latticeai/core/agent/runtime.py +210 -0
- package/latticeai/core/agent/verification.py +231 -0
- package/latticeai/core/embedding_providers/__init__.py +151 -0
- package/latticeai/core/embedding_providers/base.py +199 -0
- package/latticeai/core/embedding_providers/captions.py +162 -0
- package/latticeai/core/embedding_providers/profiles.py +126 -0
- package/latticeai/core/embedding_providers/text.py +350 -0
- package/latticeai/core/embedding_providers/vision.py +352 -0
- package/latticeai/core/file_generation/__init__.py +115 -0
- package/latticeai/core/file_generation/bundles.py +76 -0
- package/latticeai/core/file_generation/extraction.py +154 -0
- package/latticeai/core/file_generation/inference.py +235 -0
- package/latticeai/core/file_generation/orchestration.py +152 -0
- package/latticeai/core/file_generation/prompting.py +117 -0
- package/latticeai/core/file_generation/repair.py +114 -0
- package/latticeai/core/file_generation/sanitize.py +61 -0
- package/latticeai/core/file_generation/validation.py +201 -0
- package/latticeai/core/legacy_compatibility.py +1 -1
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/messages.py +9 -0
- package/latticeai/core/workspace_os_constants.py +1 -1
- package/latticeai/integrations/telegram_bot/__init__.py +123 -0
- package/latticeai/integrations/telegram_bot/__main__.py +17 -0
- package/latticeai/integrations/telegram_bot/config.py +86 -0
- package/latticeai/integrations/telegram_bot/dispatch.py +311 -0
- package/latticeai/integrations/telegram_bot/flows.py +478 -0
- package/latticeai/integrations/telegram_bot/helpers.py +322 -0
- package/latticeai/integrations/telegram_bot/screens.py +394 -0
- package/latticeai/models/router/__init__.py +88 -0
- package/latticeai/models/router/_contract.py +66 -0
- package/latticeai/models/router/branding.py +56 -0
- package/latticeai/models/router/catalog.py +69 -0
- package/latticeai/models/router/documents.py +199 -0
- package/latticeai/models/router/errors.py +37 -0
- package/latticeai/models/router/generation.py +258 -0
- package/latticeai/models/router/loading.py +291 -0
- package/latticeai/models/router/local_models.py +85 -0
- package/latticeai/models/router/registry.py +147 -0
- package/latticeai/runtime/build_phases/__init__.py +82 -0
- package/latticeai/runtime/build_phases/features.py +407 -0
- package/latticeai/runtime/build_phases/foundation.py +555 -0
- package/latticeai/runtime/build_phases/web.py +492 -0
- package/latticeai/runtime/runtime_context.py +1 -0
- package/latticeai/services/architecture_readiness.py +48 -19
- package/latticeai/services/brain_intelligence/__init__.py +58 -0
- package/latticeai/services/brain_intelligence/_contract.py +71 -0
- package/latticeai/services/brain_intelligence/consistency.py +193 -0
- package/latticeai/services/brain_intelligence/constants.py +47 -0
- package/latticeai/services/brain_intelligence/digest.py +258 -0
- package/latticeai/services/brain_intelligence/health.py +331 -0
- package/latticeai/services/brain_intelligence/proposals.py +264 -0
- package/latticeai/services/brain_intelligence/sampling.py +84 -0
- package/latticeai/services/brain_intelligence/service.py +48 -0
- package/latticeai/services/chronicle.py +557 -0
- package/latticeai/services/memory_service/__init__.py +52 -0
- package/latticeai/services/memory_service/_contract.py +100 -0
- package/latticeai/services/memory_service/brief.py +431 -0
- package/latticeai/services/memory_service/constants.py +57 -0
- package/latticeai/services/memory_service/maintenance.py +138 -0
- package/latticeai/services/memory_service/manager.py +186 -0
- package/latticeai/services/memory_service/proof.py +136 -0
- package/latticeai/services/memory_service/recall.py +225 -0
- package/latticeai/services/memory_service/service.py +48 -0
- package/latticeai/services/memory_service/stores.py +110 -0
- package/latticeai/services/model_runtime/__init__.py +322 -0
- package/latticeai/services/model_runtime/cloud.py +87 -0
- package/latticeai/services/model_runtime/download.py +282 -0
- package/latticeai/services/model_runtime/engines.py +341 -0
- package/latticeai/services/model_runtime/loading.py +178 -0
- package/latticeai/services/model_runtime/service.py +129 -0
- package/latticeai/services/model_runtime/state.py +131 -0
- package/latticeai/services/model_runtime/status.py +255 -0
- package/latticeai/services/product_readiness.py +15 -7
- package/latticeai/setup/wizard/__init__.py +126 -0
- package/latticeai/setup/wizard/catalog.py +172 -0
- package/latticeai/setup/wizard/detect.py +323 -0
- package/latticeai/setup/wizard/install.py +348 -0
- package/latticeai/setup/wizard/paths.py +168 -0
- package/latticeai/setup/wizard/plans.py +74 -0
- package/latticeai/setup/wizard/recommend.py +320 -0
- package/package.json +6 -2
- package/scripts/bump_version.py +14 -0
- package/scripts/capture_release_evidence.mjs +33 -21
- package/scripts/check_current_release_docs.mjs +1 -1
- package/scripts/check_i18n_namespace_coverage.mjs +41 -4
- package/scripts/check_max_file_lines.mjs +102 -0
- package/scripts/check_release_evidence_bound.mjs +30 -15
- package/scripts/check_screenshot_pixel_delta.py +34 -4
- package/scripts/check_server_i18n.mjs +1 -0
- package/scripts/generate_rust_parity_fixtures.py +562 -0
- package/scripts/lib/mock_server_fingerprint.mjs +94 -0
- package/scripts/release_screen_claims.json +31 -2
- package/src-tauri/Cargo.lock +361 -3
- package/src-tauri/Cargo.toml +6 -1
- package/src-tauri/src/backend.rs +349 -0
- package/src-tauri/src/folder.rs +33 -0
- package/src-tauri/src/main.rs +97 -399
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +41 -37
- package/static/app/assets/Act-yYpYnn0v.js +1 -0
- package/static/app/assets/AdminConsole-DL3Cr5pL.js +1 -0
- package/static/app/assets/{Brain-tuhI4sOC.js → Brain-C1HBN0Wf.js} +2 -2
- package/static/app/assets/BrainHome-DoXRhUUC.js +2 -0
- package/static/app/assets/BrainSignals-6yR6ir5t.js +1 -0
- package/static/app/assets/Capture-CFIRsFNE.js +1 -0
- package/static/app/assets/Chronicle-BZbEgiwN.js +1 -0
- package/static/app/assets/CommandPalette-D2pMxC2I.js +1 -0
- package/static/app/assets/Library-DwO3yZST.js +1 -0
- package/static/app/assets/{LivingBrain-DBwhto14.js → LivingBrain-Jn1GK0-S.js} +1 -1
- package/static/app/assets/ProductFlow-B-w1R4Oo.js +1 -0
- package/static/app/assets/ReviewCard-6B27X8Vg.js +3 -0
- package/static/app/assets/System-DW8F-2xL.js +1 -0
- package/static/app/assets/arrow-left-DXvKg9U6.js +1 -0
- package/static/app/assets/{bot-Cia42c2h.js → bot-IM_E_Y12.js} +1 -1
- package/static/app/assets/brain-Ci1CkWjM.js +1 -0
- package/static/app/assets/{button-2j2Ijzgq.js → button-COwyqfHM.js} +1 -1
- package/static/app/assets/circle-check-DfInj-qD.js +1 -0
- package/static/app/assets/{circle-pause-BEFeWpVW.js → circle-pause-DEM4A1Y5.js} +1 -1
- package/static/app/assets/{circle-play-ujXMcHxl.js → circle-play-C9djDuLd.js} +1 -1
- package/static/app/assets/{cpu-k4awryFq.js → cpu-DFdo1gw-.js} +1 -1
- package/static/app/assets/{download-DFbLJ_ig.js → download-SnJL6oqk.js} +1 -1
- package/static/app/assets/{folder-open-7y_b6xkM.js → folder-open-CqZeDkjE.js} +1 -1
- package/static/app/assets/{hard-drive-Bidh02Kr.js → hard-drive-j1jJXYYf.js} +1 -1
- package/static/app/assets/{index-DwDl9-8Y.css → index-BLPb5lmE.css} +1 -1
- package/static/app/assets/index-_u5iUHDr.js +10 -0
- package/static/app/assets/input-B0lPdRQZ.js +1 -0
- package/static/app/assets/link-2-CoFbooHS.js +1 -0
- package/static/app/assets/{permissionCopy-Bpb83Hx9.js → permissionCopy-BsyLxtao.js} +1 -1
- package/static/app/assets/primitives-DEbN-d6p.js +1 -0
- package/static/app/assets/search-BybIWPNd.js +1 -0
- package/static/app/assets/{share-2-BH1M-WNi.js → share-2-CVtZ_ewX.js} +1 -1
- package/static/app/assets/{shield-alert-BlKdBXcG.js → shield-alert-CBi2GNWM.js} +1 -1
- package/static/app/assets/{textarea-CCWbUfFB.js → textarea-DNMpB5ih.js} +1 -1
- package/static/app/assets/{useFocusTrap-YdHQ7pJ1.js → useFocusTrap-C83t3GXF.js} +1 -1
- package/static/app/assets/useMutation-DtbJDoyz.js +1 -0
- package/static/app/assets/{useQuery-CXQiwbVT.js → useQuery-Dcp1OChy.js} +1 -1
- package/static/app/assets/utils-BlZr7Pd4.js +4 -0
- package/static/app/assets/workspace-jJY4RuAV.js +1 -0
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/lattice_brain/graph/_kg_common.py +0 -1331
- package/lattice_brain/graph/discovery_index.py +0 -1141
- package/lattice_brain/graph/retrieval.py +0 -1120
- package/lattice_brain/graph/retrieval_vector.py +0 -1293
- package/lattice_brain/ingestion.py +0 -1525
- package/lattice_brain/multimodal.py +0 -1258
- package/latticeai/core/agent.py +0 -1465
- package/latticeai/core/embedding_providers.py +0 -1196
- package/latticeai/core/file_generation.py +0 -1047
- package/latticeai/integrations/telegram_bot.py +0 -1390
- package/latticeai/models/router.py +0 -1007
- package/latticeai/runtime/build_phases.py +0 -1450
- package/latticeai/services/brain_intelligence.py +0 -1083
- package/latticeai/services/memory_service.py +0 -1177
- package/latticeai/services/model_runtime.py +0 -1281
- package/latticeai/setup/wizard.py +0 -1310
- package/static/app/assets/Act-AWf0SAKp.js +0 -1
- package/static/app/assets/AdminConsole-D0u8Tiyj.js +0 -1
- package/static/app/assets/BrainHome-Ts7G_Ila.js +0 -2
- package/static/app/assets/BrainSignals-jMYgQ2Ar.js +0 -1
- package/static/app/assets/Capture-CqOSzyPr.js +0 -1
- package/static/app/assets/CommandPalette-DC0Bzh-I.js +0 -1
- package/static/app/assets/Library-CX-bbhmK.js +0 -1
- package/static/app/assets/ProductFlow-BHA2cfKI.js +0 -1
- package/static/app/assets/ReviewCard-BUhCKRNM.js +0 -3
- package/static/app/assets/System-Bu2t5hn1.js +0 -1
- package/static/app/assets/arrow-left-Dzwa5zRb.js +0 -1
- package/static/app/assets/brain-DJMoqrwx.js +0 -1
- package/static/app/assets/index-BpYkzcVm.js +0 -10
- package/static/app/assets/input-DSlJJxRs.js +0 -1
- package/static/app/assets/primitives-BCx6TvfG.js +0 -1
- package/static/app/assets/search-Cgy8cCFJ.js +0 -1
- package/static/app/assets/utils-zqPZJxdx.js +0 -4
- package/static/app/assets/workspace-DXTihhfU.js +0 -1
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""VERIFY — the critic's verdict, and the facts that outrank it.
|
|
2
|
+
|
|
3
|
+
Fail-closed by construction. A critic whose output cannot be parsed (after one
|
|
4
|
+
strict repair retry) never fabricates a PASS; a PASS over a transcript with no
|
|
5
|
+
execution evidence is not a completion; and a PASS that leaves a *requested
|
|
6
|
+
file* unwritten is a fact, not a judgement, so it is enforced rather than
|
|
7
|
+
merely reported back to the critic.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from typing import Any, Dict, Optional
|
|
14
|
+
|
|
15
|
+
from latticeai.core.agent_helpers import (
|
|
16
|
+
_truncate_strings,
|
|
17
|
+
artifact_checklist,
|
|
18
|
+
extract_action_details,
|
|
19
|
+
format_artifact_checklist,
|
|
20
|
+
format_requirement_coverage,
|
|
21
|
+
requirement_coverage,
|
|
22
|
+
)
|
|
23
|
+
from latticeai.core.agent_state import AgentState
|
|
24
|
+
|
|
25
|
+
from ._contract import AgentCore as _Core
|
|
26
|
+
from .context import AgentRunContext
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _VerificationMixin(_Core):
|
|
30
|
+
"""The VERIFY phase of :class:`SingleAgentRuntime`."""
|
|
31
|
+
|
|
32
|
+
# ── VERIFY ───────────────────────────────────────────────────────
|
|
33
|
+
def _has_execution_evidence(self, ctx: AgentRunContext) -> bool:
|
|
34
|
+
"""Deterministic evidence check: at least one executing step actually
|
|
35
|
+
produced a result (tool ran, or a governed change was staged as a
|
|
36
|
+
proposal). ``final``/parse-error/blocked steps carry no result and do
|
|
37
|
+
not count — a critic PASS over an evidence-free transcript must not
|
|
38
|
+
become DONE."""
|
|
39
|
+
for step in ctx.transcript:
|
|
40
|
+
if step.get("state") != AgentState.EXECUTING.value:
|
|
41
|
+
continue
|
|
42
|
+
if step.get("action") in (None, "final", "parse_error"):
|
|
43
|
+
continue
|
|
44
|
+
if isinstance(step.get("result"), dict):
|
|
45
|
+
return True
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
async def verify(
|
|
49
|
+
self, ctx: AgentRunContext, req: Any, lang_hint: str, current_user: str,
|
|
50
|
+
max_retry: int = 3, model_id: Optional[str] = None,
|
|
51
|
+
) -> None:
|
|
52
|
+
"""VERIFYING: Critic role evaluates transcript → DONE / EXECUTING (retry) / ROLLBACK / NEEDS_REVIEW / FAILED.
|
|
53
|
+
|
|
54
|
+
Fail-closed: a critic whose output cannot be parsed (after one strict
|
|
55
|
+
repair retry) never fabricates a PASS — the run terminates as
|
|
56
|
+
NEEDS_REVIEW so the user is told to check the result themselves.
|
|
57
|
+
"""
|
|
58
|
+
d = self.deps
|
|
59
|
+
# The critic must see every step (evidence completeness), but not
|
|
60
|
+
# every byte of tool output — long bodies are capped per string so
|
|
61
|
+
# verification stays affordable on long runs (review Wave 0.3).
|
|
62
|
+
verify_transcript = _truncate_strings(
|
|
63
|
+
ctx.transcript, self.transcript_budget.verify_chars
|
|
64
|
+
)
|
|
65
|
+
# Deterministic artifact facts (review L4): the critic sees the
|
|
66
|
+
# sanitize/repair honesty flags per written file, not just prose.
|
|
67
|
+
checklist = artifact_checklist(ctx.transcript, d.file_create_actions)
|
|
68
|
+
checklist_hint = (
|
|
69
|
+
f"\n\n{format_artifact_checklist(checklist)}" if checklist else ""
|
|
70
|
+
)
|
|
71
|
+
# Requirement coverage (review 루프 §2): the critic previously judged
|
|
72
|
+
# "did this fulfill the request?" from prose alone. It now also sees
|
|
73
|
+
# which requested files actually exist and which requirements the user
|
|
74
|
+
# spelled out.
|
|
75
|
+
coverage = requirement_coverage(
|
|
76
|
+
req.message, ctx.transcript, d.file_create_actions
|
|
77
|
+
)
|
|
78
|
+
context = (
|
|
79
|
+
f"{d.critic_prompt}\n\n"
|
|
80
|
+
f"[LANGUAGE HINT: {lang_hint}]\n\n"
|
|
81
|
+
f"Original request: {req.message}\n"
|
|
82
|
+
f"Plan goal: {ctx.plan.get('goal', req.message)}{checklist_hint}"
|
|
83
|
+
f"{format_requirement_coverage(coverage)}\n\n"
|
|
84
|
+
f"Full transcript:\n{json.dumps(verify_transcript, ensure_ascii=False, indent=2)}"
|
|
85
|
+
)
|
|
86
|
+
raw = await d.generate_as(
|
|
87
|
+
model_id,
|
|
88
|
+
message="Review the execution transcript and return your verdict JSON.",
|
|
89
|
+
context=context, max_tokens=self.phase_budgets.verify_tokens, temperature=0.1,
|
|
90
|
+
)
|
|
91
|
+
ctx.trace.llm_call("verify", model=model_id)
|
|
92
|
+
verdict: Optional[Dict[str, Any]] = None
|
|
93
|
+
try:
|
|
94
|
+
verdict, verdict_repairs = extract_action_details(str(raw))
|
|
95
|
+
ctx.trace.repair("verify", repairs=verdict_repairs)
|
|
96
|
+
except ValueError as exc:
|
|
97
|
+
# One strict repair retry — re-ask the critic for the exact wire
|
|
98
|
+
# format instead of fabricating a verdict.
|
|
99
|
+
ctx.trace.parse_error("verify", error=str(exc), recovered=True)
|
|
100
|
+
strict_context = (
|
|
101
|
+
f"{context}\n\n"
|
|
102
|
+
"Your previous verdict was not parseable JSON. Reply with EXACTLY one "
|
|
103
|
+
'JSON object like {"action": "verdict", "verdict": "PASS", '
|
|
104
|
+
'"next_state": "DONE", "reason": "...", "corrections": []} '
|
|
105
|
+
"and nothing else. verdict must be PASS or FAIL; next_state must be "
|
|
106
|
+
"one of DONE, EXECUTING, ROLLBACK, FAILED."
|
|
107
|
+
)
|
|
108
|
+
raw = await d.generate_as(
|
|
109
|
+
model_id,
|
|
110
|
+
message="Return your verdict as one strict JSON object.",
|
|
111
|
+
context=strict_context, max_tokens=self.phase_budgets.verify_tokens,
|
|
112
|
+
temperature=0.0,
|
|
113
|
+
)
|
|
114
|
+
ctx.trace.llm_call("verify", model=model_id)
|
|
115
|
+
try:
|
|
116
|
+
verdict, verdict_repairs = extract_action_details(str(raw))
|
|
117
|
+
ctx.trace.repair("verify", repairs=verdict_repairs)
|
|
118
|
+
except ValueError as retry_exc:
|
|
119
|
+
ctx.trace.parse_error("verify", error=str(retry_exc), recovered=False)
|
|
120
|
+
verdict = None
|
|
121
|
+
|
|
122
|
+
has_evidence = self._has_execution_evidence(ctx)
|
|
123
|
+
|
|
124
|
+
if verdict is None:
|
|
125
|
+
# Verifier unavailable — fail closed, never DONE.
|
|
126
|
+
ctx.transcript.append({
|
|
127
|
+
"state": AgentState.VERIFYING.value,
|
|
128
|
+
"verdict": "UNAVAILABLE",
|
|
129
|
+
"reason": "critic output unparseable after strict retry",
|
|
130
|
+
"verifier_available": False,
|
|
131
|
+
"verdict_valid": False,
|
|
132
|
+
"evidence": has_evidence,
|
|
133
|
+
})
|
|
134
|
+
ctx.trace.decision(
|
|
135
|
+
"verify", decision="verification_unavailable",
|
|
136
|
+
verifier_available=False, verdict_valid=False, evidence=has_evidence,
|
|
137
|
+
)
|
|
138
|
+
self._emit_step(ctx, "verify", "verdict", verdict="UNAVAILABLE")
|
|
139
|
+
ctx.final_message = (
|
|
140
|
+
"검증을 완료하지 못했습니다 — 검증 모델의 응답을 해석할 수 없었습니다. "
|
|
141
|
+
"실행 결과를 직접 확인해 주시고, 필요하면 다시 시도해 주세요."
|
|
142
|
+
)
|
|
143
|
+
ctx.state = AgentState.NEEDS_REVIEW
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
ctx.corrections = verdict.get("corrections", [])
|
|
147
|
+
# Normalize legacy verdict next_state strings to current AgentState names
|
|
148
|
+
raw_next = verdict.get("next_state", "")
|
|
149
|
+
next_s = {"COMPLETE": "DONE", "RETRY": "EXECUTING"}.get(raw_next, raw_next)
|
|
150
|
+
|
|
151
|
+
ctx.transcript.append({
|
|
152
|
+
"state": AgentState.VERIFYING.value,
|
|
153
|
+
"verdict": verdict.get("verdict", ""),
|
|
154
|
+
"reason": verdict.get("reason", ""),
|
|
155
|
+
"corrections": ctx.corrections,
|
|
156
|
+
"confidence": verdict.get("confidence", 0.9),
|
|
157
|
+
"next_state": next_s,
|
|
158
|
+
"verifier_available": True,
|
|
159
|
+
"verdict_valid": True,
|
|
160
|
+
"evidence": has_evidence,
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
ctx.trace.decision(
|
|
164
|
+
"verify", decision=str(verdict.get("verdict", "")), next_state=next_s,
|
|
165
|
+
verifier_available=True, verdict_valid=True, evidence=has_evidence,
|
|
166
|
+
)
|
|
167
|
+
self._emit_step(
|
|
168
|
+
ctx, "verify", "verdict",
|
|
169
|
+
verdict=str(verdict.get("verdict", "")), next_state=next_s,
|
|
170
|
+
)
|
|
171
|
+
if verdict.get("verdict") == "PASS":
|
|
172
|
+
# DONE requires both: a validly parsed PASS verdict AND
|
|
173
|
+
# deterministic execution evidence in the transcript. A PASS over
|
|
174
|
+
# an evidence-free run is not a completion.
|
|
175
|
+
if not has_evidence:
|
|
176
|
+
ctx.trace.decision("verify", decision="needs_review_no_evidence")
|
|
177
|
+
ctx.final_message = (
|
|
178
|
+
"검증자는 통과를 보고했지만 실제 실행 근거(도구 실행 기록)가 없어 "
|
|
179
|
+
"완료로 처리하지 않았습니다. 결과를 직접 확인해 주세요."
|
|
180
|
+
)
|
|
181
|
+
ctx.state = AgentState.NEEDS_REVIEW
|
|
182
|
+
return
|
|
183
|
+
if not coverage["complete"]:
|
|
184
|
+
# A PASS that leaves a *requested file* unwritten is not a
|
|
185
|
+
# completion — this is a fact, not a judgement, so it is
|
|
186
|
+
# enforced rather than merely reported to the critic.
|
|
187
|
+
missing = ", ".join(coverage["missing_files"])
|
|
188
|
+
ctx.trace.decision(
|
|
189
|
+
"verify", decision="needs_review_missing_files",
|
|
190
|
+
missing=len(coverage["missing_files"]),
|
|
191
|
+
)
|
|
192
|
+
ctx.transcript.append({
|
|
193
|
+
"state": AgentState.VERIFYING.value,
|
|
194
|
+
"requirement_coverage": coverage,
|
|
195
|
+
})
|
|
196
|
+
ctx.final_message = (
|
|
197
|
+
f"요청한 파일 중 일부가 만들어지지 않아 완료로 처리하지 않았습니다: {missing}"
|
|
198
|
+
)
|
|
199
|
+
ctx.state = AgentState.NEEDS_REVIEW
|
|
200
|
+
return
|
|
201
|
+
if not ctx.final_message:
|
|
202
|
+
ctx.final_message = verdict.get("reason", "작업이 완료되었습니다.")
|
|
203
|
+
ctx.state = AgentState.DONE
|
|
204
|
+
elif next_s == "ROLLBACK":
|
|
205
|
+
ctx.state = AgentState.ROLLBACK
|
|
206
|
+
elif next_s == "EXECUTING":
|
|
207
|
+
if ctx.retry_count >= max_retry:
|
|
208
|
+
ctx.final_message = "처리 중 문제가 발생했습니다. 다시 시도해 주세요."
|
|
209
|
+
ctx.state = AgentState.FAILED
|
|
210
|
+
else:
|
|
211
|
+
ctx.retry_count += 1
|
|
212
|
+
ctx.trace.retry("verify", attempt=ctx.retry_count)
|
|
213
|
+
ctx.transcript.append({
|
|
214
|
+
"state": AgentState.EXECUTING.value,
|
|
215
|
+
"retry_attempt": ctx.retry_count,
|
|
216
|
+
"corrections": ctx.corrections,
|
|
217
|
+
})
|
|
218
|
+
ctx.state = AgentState.EXECUTING
|
|
219
|
+
elif next_s == "DONE":
|
|
220
|
+
# Contradictory verdict: the critic asked for DONE without a PASS.
|
|
221
|
+
# The loose "or next_state == DONE" success path is gone — this is
|
|
222
|
+
# a non-success that the user must review.
|
|
223
|
+
ctx.trace.decision("verify", decision="needs_review_inconsistent_verdict")
|
|
224
|
+
ctx.final_message = (
|
|
225
|
+
"검증 결과가 일관되지 않아 완료로 처리하지 않았습니다. "
|
|
226
|
+
"실행 결과를 직접 확인해 주세요."
|
|
227
|
+
)
|
|
228
|
+
ctx.state = AgentState.NEEDS_REVIEW
|
|
229
|
+
else:
|
|
230
|
+
ctx.final_message = verdict.get("reason", "검증자가 인식되지 않은 다음 상태를 반환했습니다.")
|
|
231
|
+
ctx.state = AgentState.FAILED
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Provider-backed embeddings for Lattice AI retrieval.
|
|
2
|
+
|
|
3
|
+
The knowledge graph stores dense vectors keyed by ``(embedding_model,
|
|
4
|
+
embedding_dim)`` and only ever compares vectors that share those keys
|
|
5
|
+
(``knowledge_graph.vector_search``). That contract means the *embedder* can be
|
|
6
|
+
swapped behind a single interface as long as every implementation agrees on:
|
|
7
|
+
|
|
8
|
+
* ``model_id`` / ``dim`` — the index identity (a change forces a re-index, which
|
|
9
|
+
``index_status`` already reports as ``stale``/``needs_reindex``);
|
|
10
|
+
* ``encode`` / ``decode`` — the on-disk float32 codec (shared by all providers);
|
|
11
|
+
* ``embed`` returns an **L2-normalized** vector, so ``similarity`` is a plain dot
|
|
12
|
+
product and equals cosine similarity regardless of provider.
|
|
13
|
+
|
|
14
|
+
:mod:`.base` defines that :class:`EmbeddingProvider` interface; :mod:`.text`
|
|
15
|
+
holds the five concrete text implementations:
|
|
16
|
+
|
|
17
|
+
1. :class:`HashEmbeddingProvider` — deterministic, offline, always-available
|
|
18
|
+
fallback (wraps the legacy :class:`~latticeai.core.local_embeddings.LocalEmbeddingModel`).
|
|
19
|
+
2. :class:`MLXEmbeddingProvider` — local Apple-Silicon embedding models.
|
|
20
|
+
3. :class:`OllamaEmbeddingProvider` — a local/remote Ollama server.
|
|
21
|
+
4. :class:`OpenAICompatibleEmbeddingProvider` — any ``/v1/embeddings`` endpoint
|
|
22
|
+
(OpenAI, LM Studio, vLLM, llama.cpp, Together, …).
|
|
23
|
+
5. :class:`CustomEmbeddingProvider` — a user-supplied dotted callable.
|
|
24
|
+
|
|
25
|
+
:func:`resolve_embedder` builds the configured provider and, when that provider
|
|
26
|
+
is unavailable, degrades to the hash fallback while *reporting* the requested
|
|
27
|
+
vs. active provider — nothing is silently faked. :mod:`.profiles` is the named
|
|
28
|
+
list of supported provider/model/dimension combinations the setup surfaces
|
|
29
|
+
offer.
|
|
30
|
+
|
|
31
|
+
Vision seam (v11.1.0, Track 3)
|
|
32
|
+
------------------------------
|
|
33
|
+
Images join the same contract through :class:`VisionEmbeddingProvider` in
|
|
34
|
+
:mod:`.vision`, with two deliberate differences from the text side:
|
|
35
|
+
|
|
36
|
+
* **No fallback.** The hash embedder turns *text* into a real, if crude, cosine
|
|
37
|
+
signal. There is no equivalent for pixels: hashing a file path produces a
|
|
38
|
+
vector that says nothing about the picture, so an unavailable vision model is
|
|
39
|
+
reported as unavailable (:class:`EmbeddingUnavailable` /
|
|
40
|
+
``ResolvedVisionEmbedder.available == False``) and the caller skips the
|
|
41
|
+
embedding instead of storing a decoy.
|
|
42
|
+
* **A separate space by default.** A CLIP-family image vector is not comparable
|
|
43
|
+
with a BGE text vector, so ``space == "image"`` means "index these apart and
|
|
44
|
+
join them by late fusion". Only a genuinely shared-space model may declare
|
|
45
|
+
``space == "shared"`` (opt-in), and only then can a *text* query be scored
|
|
46
|
+
against image vectors.
|
|
47
|
+
|
|
48
|
+
:class:`VisionCaptioner` in :mod:`.captions` is the matching seam for
|
|
49
|
+
descriptions. Its default implementation returns ``None``: a caption is what a
|
|
50
|
+
vision-language model said about an image, so with no VLM loaded there is no
|
|
51
|
+
caption — never a sentence assembled from the filename and passed off as one.
|
|
52
|
+
|
|
53
|
+
Split into these submodules in v11.3.0 with no behaviour change. Every name the
|
|
54
|
+
single module exposed still resolves from
|
|
55
|
+
``latticeai.core.embedding_providers``.
|
|
56
|
+
|
|
57
|
+
Stubbing note: a name rebound *here* changes only this module's binding — the
|
|
58
|
+
submodule that calls it holds its own, so a test standing in for a helper
|
|
59
|
+
patches the submodule that reads it.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
from __future__ import annotations
|
|
63
|
+
|
|
64
|
+
# The single module had no ``__all__`` restriction on what callers could reach:
|
|
65
|
+
# the two names it imported for its own use were part of its surface, and the
|
|
66
|
+
# suite imports them from here. Re-exported in the redundant-alias form so they
|
|
67
|
+
# read as deliberate rather than as leftover imports.
|
|
68
|
+
from latticeai.core.local_embeddings import (
|
|
69
|
+
DEFAULT_EMBEDDING_DIM as DEFAULT_EMBEDDING_DIM,
|
|
70
|
+
)
|
|
71
|
+
from latticeai.core.local_embeddings import LocalEmbeddingModel as LocalEmbeddingModel
|
|
72
|
+
|
|
73
|
+
from .base import _KNOWN_DIMS as _KNOWN_DIMS
|
|
74
|
+
from .base import EmbeddingProvider as EmbeddingProvider
|
|
75
|
+
from .base import EmbeddingUnavailable as EmbeddingUnavailable
|
|
76
|
+
from .base import _guess_dim as _guess_dim
|
|
77
|
+
from .base import _l2_normalize as _l2_normalize
|
|
78
|
+
from .base import _load_dotted as _load_dotted
|
|
79
|
+
from .base import _NetworkEmbeddingProvider as _NetworkEmbeddingProvider
|
|
80
|
+
from .base import _RemoteConfig as _RemoteConfig
|
|
81
|
+
from .captions import DEFAULT_CAPTION_PROMPT as DEFAULT_CAPTION_PROMPT
|
|
82
|
+
from .captions import VISION_CAPTION_TARGET_ENV as VISION_CAPTION_TARGET_ENV
|
|
83
|
+
from .captions import CustomVisionCaptioner as CustomVisionCaptioner
|
|
84
|
+
from .captions import MLXVisionCaptioner as MLXVisionCaptioner
|
|
85
|
+
from .captions import VisionCaptioner as VisionCaptioner
|
|
86
|
+
from .captions import resolve_vision_captioner as resolve_vision_captioner
|
|
87
|
+
from .captions import vision_caption_port as vision_caption_port
|
|
88
|
+
from .profiles import PRODUCTION_PROVIDER_PROFILES as PRODUCTION_PROVIDER_PROFILES
|
|
89
|
+
from .profiles import embedding_provider_profiles as embedding_provider_profiles
|
|
90
|
+
from .profiles import resolve_embedding_profile as resolve_embedding_profile
|
|
91
|
+
from .text import PROVIDER_TYPES as PROVIDER_TYPES
|
|
92
|
+
from .text import CustomEmbeddingProvider as CustomEmbeddingProvider
|
|
93
|
+
from .text import HashEmbeddingProvider as HashEmbeddingProvider
|
|
94
|
+
from .text import MLXEmbeddingProvider as MLXEmbeddingProvider
|
|
95
|
+
from .text import OllamaEmbeddingProvider as OllamaEmbeddingProvider
|
|
96
|
+
from .text import OpenAICompatibleEmbeddingProvider as OpenAICompatibleEmbeddingProvider
|
|
97
|
+
from .text import ResolvedEmbedder as ResolvedEmbedder
|
|
98
|
+
from .text import _as_float_list as _as_float_list
|
|
99
|
+
from .text import build_embedding_provider as build_embedding_provider
|
|
100
|
+
from .text import resolve_embedder as resolve_embedder
|
|
101
|
+
from .vision import _KNOWN_VISION_DIMS as _KNOWN_VISION_DIMS
|
|
102
|
+
from .vision import DEFAULT_VISION_DIM as DEFAULT_VISION_DIM
|
|
103
|
+
from .vision import VISION_PROVIDER_TYPES as VISION_PROVIDER_TYPES
|
|
104
|
+
from .vision import VISION_SPACE_IMAGE as VISION_SPACE_IMAGE
|
|
105
|
+
from .vision import VISION_SPACE_SHARED as VISION_SPACE_SHARED
|
|
106
|
+
from .vision import VISION_SPACES as VISION_SPACES
|
|
107
|
+
from .vision import VISION_TARGET_ENV as VISION_TARGET_ENV
|
|
108
|
+
from .vision import CustomVisionEmbeddingProvider as CustomVisionEmbeddingProvider
|
|
109
|
+
from .vision import MLXVisionEmbeddingProvider as MLXVisionEmbeddingProvider
|
|
110
|
+
from .vision import ResolvedVisionEmbedder as ResolvedVisionEmbedder
|
|
111
|
+
from .vision import VisionEmbeddingProvider as VisionEmbeddingProvider
|
|
112
|
+
from .vision import _guess_vision_dim as _guess_vision_dim
|
|
113
|
+
from .vision import _normalize_space as _normalize_space
|
|
114
|
+
from .vision import build_vision_provider as build_vision_provider
|
|
115
|
+
from .vision import resolve_vision_embedder as resolve_vision_embedder
|
|
116
|
+
|
|
117
|
+
__all__ = [
|
|
118
|
+
"DEFAULT_CAPTION_PROMPT",
|
|
119
|
+
"DEFAULT_VISION_DIM",
|
|
120
|
+
"VISION_CAPTION_TARGET_ENV",
|
|
121
|
+
"VISION_PROVIDER_TYPES",
|
|
122
|
+
"VISION_SPACES",
|
|
123
|
+
"VISION_SPACE_IMAGE",
|
|
124
|
+
"VISION_SPACE_SHARED",
|
|
125
|
+
"VISION_TARGET_ENV",
|
|
126
|
+
"CustomVisionCaptioner",
|
|
127
|
+
"CustomVisionEmbeddingProvider",
|
|
128
|
+
"EmbeddingProvider",
|
|
129
|
+
"EmbeddingUnavailable",
|
|
130
|
+
"MLXVisionCaptioner",
|
|
131
|
+
"MLXVisionEmbeddingProvider",
|
|
132
|
+
"ResolvedVisionEmbedder",
|
|
133
|
+
"VisionCaptioner",
|
|
134
|
+
"VisionEmbeddingProvider",
|
|
135
|
+
"build_vision_provider",
|
|
136
|
+
"resolve_vision_captioner",
|
|
137
|
+
"resolve_vision_embedder",
|
|
138
|
+
"vision_caption_port",
|
|
139
|
+
"HashEmbeddingProvider",
|
|
140
|
+
"MLXEmbeddingProvider",
|
|
141
|
+
"OllamaEmbeddingProvider",
|
|
142
|
+
"OpenAICompatibleEmbeddingProvider",
|
|
143
|
+
"CustomEmbeddingProvider",
|
|
144
|
+
"ResolvedEmbedder",
|
|
145
|
+
"build_embedding_provider",
|
|
146
|
+
"resolve_embedder",
|
|
147
|
+
"resolve_embedding_profile",
|
|
148
|
+
"embedding_provider_profiles",
|
|
149
|
+
"PRODUCTION_PROVIDER_PROFILES",
|
|
150
|
+
"PROVIDER_TYPES",
|
|
151
|
+
]
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""The contract every embedder implements, and the machinery they share.
|
|
2
|
+
|
|
3
|
+
``EmbeddingProvider`` is the interface: set ``model_id`` and ``dim``, implement
|
|
4
|
+
``embed_batch``, and inherit the float32 codec plus the dot-product similarity
|
|
5
|
+
that equals cosine because every ``embed`` returns an L2-normalized vector.
|
|
6
|
+
``_NetworkEmbeddingProvider`` adds what any provider that calls a model or a
|
|
7
|
+
server needs — input clamping, normalization, and locking the index identity to
|
|
8
|
+
the width the model actually returned.
|
|
9
|
+
|
|
10
|
+
Nothing here reaches a network or a model; the concrete providers in
|
|
11
|
+
:mod:`.text` and :mod:`.vision` do.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import importlib
|
|
17
|
+
import math
|
|
18
|
+
import struct
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence
|
|
21
|
+
|
|
22
|
+
from latticeai.core.local_embeddings import DEFAULT_EMBEDDING_DIM
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class EmbeddingUnavailable(RuntimeError):
|
|
26
|
+
"""Raised when a configured provider cannot produce an embedding.
|
|
27
|
+
|
|
28
|
+
Callers in the hot path (``vector_search``) translate this into a clear
|
|
29
|
+
503/"provider unavailable" rather than a misleading empty result.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Best-known output dimensionality for common embedding models, so the index
|
|
34
|
+
# identity is stable before the first (possibly remote) call. A configured
|
|
35
|
+
# ``dim`` always wins; an unknown model falls back to a one-time live probe.
|
|
36
|
+
_KNOWN_DIMS = {
|
|
37
|
+
"bge-m3": 1024,
|
|
38
|
+
"nomic-embed-text": 768,
|
|
39
|
+
"mxbai-embed-large": 1024,
|
|
40
|
+
"all-minilm": 384,
|
|
41
|
+
"all-minilm-l6-v2": 384,
|
|
42
|
+
"bge-small-en": 384,
|
|
43
|
+
"bge-base-en": 768,
|
|
44
|
+
"bge-large-en": 1024,
|
|
45
|
+
"gte-small": 384,
|
|
46
|
+
"gte-base": 768,
|
|
47
|
+
"gte-large": 1024,
|
|
48
|
+
"e5-large": 1024,
|
|
49
|
+
"multilingual-e5-large": 1024,
|
|
50
|
+
"text-embedding-3-small": 1536,
|
|
51
|
+
"text-embedding-3-large": 3072,
|
|
52
|
+
"text-embedding-ada-002": 1536,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _guess_dim(model: str, default: int) -> int:
|
|
57
|
+
key = str(model or "").split("/")[-1].strip().lower()
|
|
58
|
+
key = key.split(":")[0]
|
|
59
|
+
return _KNOWN_DIMS.get(key, default)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _l2_normalize(vector: Sequence[float]) -> List[float]:
|
|
63
|
+
norm = math.sqrt(sum(float(v) * float(v) for v in vector))
|
|
64
|
+
if norm <= 0:
|
|
65
|
+
return [float(v) for v in vector]
|
|
66
|
+
return [float(v) / norm for v in vector]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class EmbeddingProvider:
|
|
70
|
+
"""Interface every embedder implements.
|
|
71
|
+
|
|
72
|
+
Subclasses must set ``model_id`` and ``dim`` and implement
|
|
73
|
+
:meth:`embed_batch`; the rest (single embed, codec, similarity) is shared.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
#: stable identity stored alongside every vector — change ⇒ re-index
|
|
77
|
+
model_id: str = ""
|
|
78
|
+
#: vector dimensionality
|
|
79
|
+
dim: int = DEFAULT_EMBEDDING_DIM
|
|
80
|
+
#: short provider kind ("hash" | "mlx" | "ollama" | "openai" | "custom")
|
|
81
|
+
provider: str = "hash"
|
|
82
|
+
#: "fallback" (hash) | "production" (real semantic model)
|
|
83
|
+
grade: str = "production"
|
|
84
|
+
|
|
85
|
+
# ── required ──────────────────────────────────────────────────────────
|
|
86
|
+
def embed_batch(self, texts: Sequence[str]) -> List[List[float]]:
|
|
87
|
+
raise NotImplementedError
|
|
88
|
+
|
|
89
|
+
# ── derived (shared) ──────────────────────────────────────────────────
|
|
90
|
+
def _model_id_with_dim(self, dim: int) -> str:
|
|
91
|
+
"""This provider's ``model_id`` restated at ``dim``.
|
|
92
|
+
|
|
93
|
+
Every provider spells its identity ``<kind>:<model>:<dim>``, so the
|
|
94
|
+
numeric tail is what moves when a live call reveals the model's true
|
|
95
|
+
width. An id without such a tail does not encode a dimension and is
|
|
96
|
+
returned unchanged — the index keys on ``model_id`` *and* ``dim``, so
|
|
97
|
+
nothing becomes ambiguous.
|
|
98
|
+
"""
|
|
99
|
+
head, sep, tail = self.model_id.rpartition(":")
|
|
100
|
+
if sep and tail.isdigit():
|
|
101
|
+
return f"{head}:{dim}"
|
|
102
|
+
return self.model_id
|
|
103
|
+
|
|
104
|
+
def embed(self, text: str) -> List[float]:
|
|
105
|
+
result = self.embed_batch([text])
|
|
106
|
+
return result[0] if result else [0.0] * self.dim
|
|
107
|
+
|
|
108
|
+
def encode(self, vector: Iterable[float]) -> bytes:
|
|
109
|
+
values = [float(v) for v in vector]
|
|
110
|
+
return struct.pack(f"<{len(values)}f", *values)
|
|
111
|
+
|
|
112
|
+
def decode(self, payload: bytes, dim: Optional[int] = None) -> List[float]:
|
|
113
|
+
if not payload:
|
|
114
|
+
return []
|
|
115
|
+
count = int(dim or self.dim)
|
|
116
|
+
if len(payload) != count * 4:
|
|
117
|
+
count = len(payload) // 4
|
|
118
|
+
return list(struct.unpack(f"<{count}f", payload[: count * 4]))
|
|
119
|
+
|
|
120
|
+
def similarity(self, left: Iterable[float], right: Iterable[float]) -> float:
|
|
121
|
+
# strict=True: a dimension mismatch means the two vectors came from
|
|
122
|
+
# different embedding models. Truncating to the shorter one produces a
|
|
123
|
+
# plausible-looking similarity that is meaningless — exactly the silent
|
|
124
|
+
# wrongness this codebase keeps finding. Callers that can hit a model
|
|
125
|
+
# swap already handle failure and fall back to lexical search.
|
|
126
|
+
left_v, right_v = list(left), list(right)
|
|
127
|
+
if len(left_v) != len(right_v):
|
|
128
|
+
raise ValueError(
|
|
129
|
+
f"embedding dimension mismatch: {len(left_v)} vs {len(right_v)}; "
|
|
130
|
+
"the vector index was built with a different model"
|
|
131
|
+
)
|
|
132
|
+
return float(sum(a * b for a, b in zip(left_v, right_v, strict=True)))
|
|
133
|
+
|
|
134
|
+
# ── observability ─────────────────────────────────────────────────────
|
|
135
|
+
def health(self) -> Dict[str, Any]:
|
|
136
|
+
"""Return ``{status, detail}``; status ∈ ok | unavailable."""
|
|
137
|
+
return {"status": "ok", "detail": "ready"}
|
|
138
|
+
|
|
139
|
+
def metadata(self) -> Dict[str, Any]:
|
|
140
|
+
return {
|
|
141
|
+
"provider": self.provider,
|
|
142
|
+
"model": self.model_id,
|
|
143
|
+
"model_id": self.model_id,
|
|
144
|
+
"dim": self.dim,
|
|
145
|
+
"grade": self.grade,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@dataclass
|
|
150
|
+
class _RemoteConfig:
|
|
151
|
+
model: str
|
|
152
|
+
base_url: str = ""
|
|
153
|
+
api_key: str = ""
|
|
154
|
+
dim: int = DEFAULT_EMBEDDING_DIM
|
|
155
|
+
timeout: float = 30.0
|
|
156
|
+
extra: Dict[str, Any] = field(default_factory=dict)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class _NetworkEmbeddingProvider(EmbeddingProvider):
|
|
160
|
+
"""Common machinery for providers that call a model/server to embed."""
|
|
161
|
+
|
|
162
|
+
def __init__(self, cfg: _RemoteConfig):
|
|
163
|
+
self._cfg = cfg
|
|
164
|
+
self.dim = int(cfg.dim or DEFAULT_EMBEDDING_DIM)
|
|
165
|
+
|
|
166
|
+
# subclasses implement the raw call
|
|
167
|
+
def _embed_raw(self, texts: Sequence[str]) -> List[List[float]]:
|
|
168
|
+
raise NotImplementedError
|
|
169
|
+
|
|
170
|
+
def embed_batch(self, texts: Sequence[str]) -> List[List[float]]:
|
|
171
|
+
clean = [str(t or "")[:50_000] for t in texts]
|
|
172
|
+
if not clean:
|
|
173
|
+
return []
|
|
174
|
+
vectors = self._embed_raw(clean)
|
|
175
|
+
out: List[List[float]] = []
|
|
176
|
+
for vec in vectors:
|
|
177
|
+
vec = [float(x) for x in (vec or [])]
|
|
178
|
+
if vec:
|
|
179
|
+
# lock the index identity to the true model dimensionality —
|
|
180
|
+
# the id carries that dimension, so it moves with it or the
|
|
181
|
+
# vectors end up filed under a width they do not have
|
|
182
|
+
self.dim = len(vec)
|
|
183
|
+
self.model_id = self._model_id_with_dim(self.dim)
|
|
184
|
+
out.append(_l2_normalize(vec) if vec else [0.0] * self.dim)
|
|
185
|
+
return out
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _load_dotted(ref: str, env_name: str, label: str) -> Callable[..., Any]:
|
|
189
|
+
"""Import ``module:callable`` (or ``module.callable``) or explain why not."""
|
|
190
|
+
if not ref:
|
|
191
|
+
raise EmbeddingUnavailable(f"{label} target not configured ({env_name})")
|
|
192
|
+
module_name, _, attr = ref.replace(":", ".").rpartition(".")
|
|
193
|
+
if not module_name:
|
|
194
|
+
raise EmbeddingUnavailable(f"invalid {label} target: {ref}")
|
|
195
|
+
try:
|
|
196
|
+
module = importlib.import_module(module_name)
|
|
197
|
+
return getattr(module, attr) # type: ignore[no-any-return]
|
|
198
|
+
except Exception as exc:
|
|
199
|
+
raise EmbeddingUnavailable(f"{label} target unavailable: {exc}") from exc
|