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.
Files changed (247) hide show
  1. package/README.md +46 -53
  2. package/docs/CHANGELOG.md +61 -0
  3. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  4. package/docs/DEVELOPMENT.md +1 -1
  5. package/docs/MULTI_AGENT_RUNTIME.md +1 -1
  6. package/docs/ONBOARDING.md +1 -1
  7. package/docs/OPERATIONS.md +6 -2
  8. package/docs/PERMISSION_MODE.md +1 -1
  9. package/docs/TRUST_MODEL.md +1 -1
  10. package/docs/WHY_LATTICE.md +1 -1
  11. package/docs/kg-schema.md +2 -2
  12. package/docs/v11.3.0_PLAN.md +202 -0
  13. package/docs/v11.4.0_RUST_FOUNDATION_PLAN.md +176 -0
  14. package/lattice_brain/__init__.py +1 -1
  15. package/lattice_brain/graph/_kg_common/__init__.py +287 -0
  16. package/lattice_brain/graph/_kg_common/extraction.py +516 -0
  17. package/lattice_brain/graph/_kg_common/relations.py +161 -0
  18. package/lattice_brain/graph/_kg_common/text.py +479 -0
  19. package/lattice_brain/graph/discovery_index/__init__.py +35 -0
  20. package/lattice_brain/graph/discovery_index/cleanup.py +182 -0
  21. package/lattice_brain/graph/discovery_index/extract.py +137 -0
  22. package/lattice_brain/graph/discovery_index/scan.py +411 -0
  23. package/lattice_brain/graph/discovery_index/upsert.py +495 -0
  24. package/lattice_brain/graph/projection/__init__.py +42 -0
  25. package/lattice_brain/graph/projection/curation.py +500 -0
  26. package/lattice_brain/graph/{projection.py → projection/v2_schema.py} +15 -477
  27. package/lattice_brain/graph/retrieval/__init__.py +54 -0
  28. package/lattice_brain/graph/retrieval/context.py +197 -0
  29. package/lattice_brain/graph/retrieval/graph_view.py +319 -0
  30. package/lattice_brain/graph/retrieval/hybrid.py +488 -0
  31. package/lattice_brain/graph/retrieval/maintenance.py +121 -0
  32. package/lattice_brain/graph/retrieval/signals.py +95 -0
  33. package/lattice_brain/graph/retrieval_vector/__init__.py +42 -0
  34. package/lattice_brain/graph/retrieval_vector/fingerprint.py +97 -0
  35. package/lattice_brain/graph/retrieval_vector/indexing.py +347 -0
  36. package/lattice_brain/graph/retrieval_vector/search.py +560 -0
  37. package/lattice_brain/graph/retrieval_vector/status.py +374 -0
  38. package/lattice_brain/ingestion/__init__.py +130 -0
  39. package/lattice_brain/ingestion/_contract.py +90 -0
  40. package/lattice_brain/ingestion/constants.py +127 -0
  41. package/lattice_brain/ingestion/folder_scan.py +57 -0
  42. package/lattice_brain/ingestion/folders.py +258 -0
  43. package/lattice_brain/ingestion/hashing.py +26 -0
  44. package/lattice_brain/ingestion/jobs_api.py +107 -0
  45. package/lattice_brain/ingestion/models.py +80 -0
  46. package/lattice_brain/ingestion/pipeline.py +486 -0
  47. package/lattice_brain/ingestion/quality.py +209 -0
  48. package/lattice_brain/ingestion/routing.py +295 -0
  49. package/lattice_brain/multimodal/__init__.py +164 -0
  50. package/lattice_brain/multimodal/audio.py +77 -0
  51. package/lattice_brain/multimodal/common.py +118 -0
  52. package/lattice_brain/multimodal/images.py +498 -0
  53. package/lattice_brain/multimodal/ports.py +169 -0
  54. package/lattice_brain/multimodal/video.py +410 -0
  55. package/lattice_brain/portability/__init__.py +90 -0
  56. package/lattice_brain/portability/_contract.py +42 -0
  57. package/lattice_brain/portability/backups.py +338 -0
  58. package/lattice_brain/portability/bundles.py +136 -0
  59. package/lattice_brain/portability/constants.py +93 -0
  60. package/lattice_brain/portability/fsops.py +138 -0
  61. package/lattice_brain/portability/service.py +41 -0
  62. package/lattice_brain/{portability.py → portability/sharing.py} +44 -677
  63. package/lattice_brain/runtime/__init__.py +1 -1
  64. package/lattice_brain/runtime/multi_agent.py +1 -1
  65. package/latticeai/__init__.py +1 -1
  66. package/latticeai/api/chronicle.py +63 -0
  67. package/latticeai/core/agent/__init__.py +93 -0
  68. package/latticeai/core/agent/_contract.py +79 -0
  69. package/latticeai/core/agent/context.py +57 -0
  70. package/latticeai/core/agent/deps.py +125 -0
  71. package/latticeai/core/agent/execution.py +622 -0
  72. package/latticeai/core/agent/planning.py +145 -0
  73. package/latticeai/core/agent/recovery.py +157 -0
  74. package/latticeai/core/agent/runtime.py +210 -0
  75. package/latticeai/core/agent/verification.py +231 -0
  76. package/latticeai/core/embedding_providers/__init__.py +151 -0
  77. package/latticeai/core/embedding_providers/base.py +199 -0
  78. package/latticeai/core/embedding_providers/captions.py +162 -0
  79. package/latticeai/core/embedding_providers/profiles.py +126 -0
  80. package/latticeai/core/embedding_providers/text.py +350 -0
  81. package/latticeai/core/embedding_providers/vision.py +352 -0
  82. package/latticeai/core/file_generation/__init__.py +115 -0
  83. package/latticeai/core/file_generation/bundles.py +76 -0
  84. package/latticeai/core/file_generation/extraction.py +154 -0
  85. package/latticeai/core/file_generation/inference.py +235 -0
  86. package/latticeai/core/file_generation/orchestration.py +152 -0
  87. package/latticeai/core/file_generation/prompting.py +117 -0
  88. package/latticeai/core/file_generation/repair.py +114 -0
  89. package/latticeai/core/file_generation/sanitize.py +61 -0
  90. package/latticeai/core/file_generation/validation.py +201 -0
  91. package/latticeai/core/legacy_compatibility.py +1 -1
  92. package/latticeai/core/marketplace.py +1 -1
  93. package/latticeai/core/messages.py +9 -0
  94. package/latticeai/core/workspace_os_constants.py +1 -1
  95. package/latticeai/integrations/telegram_bot/__init__.py +123 -0
  96. package/latticeai/integrations/telegram_bot/__main__.py +17 -0
  97. package/latticeai/integrations/telegram_bot/config.py +86 -0
  98. package/latticeai/integrations/telegram_bot/dispatch.py +311 -0
  99. package/latticeai/integrations/telegram_bot/flows.py +478 -0
  100. package/latticeai/integrations/telegram_bot/helpers.py +322 -0
  101. package/latticeai/integrations/telegram_bot/screens.py +394 -0
  102. package/latticeai/models/router/__init__.py +88 -0
  103. package/latticeai/models/router/_contract.py +66 -0
  104. package/latticeai/models/router/branding.py +56 -0
  105. package/latticeai/models/router/catalog.py +69 -0
  106. package/latticeai/models/router/documents.py +199 -0
  107. package/latticeai/models/router/errors.py +37 -0
  108. package/latticeai/models/router/generation.py +258 -0
  109. package/latticeai/models/router/loading.py +291 -0
  110. package/latticeai/models/router/local_models.py +85 -0
  111. package/latticeai/models/router/registry.py +147 -0
  112. package/latticeai/runtime/build_phases/__init__.py +82 -0
  113. package/latticeai/runtime/build_phases/features.py +407 -0
  114. package/latticeai/runtime/build_phases/foundation.py +555 -0
  115. package/latticeai/runtime/build_phases/web.py +492 -0
  116. package/latticeai/runtime/runtime_context.py +1 -0
  117. package/latticeai/services/architecture_readiness.py +48 -19
  118. package/latticeai/services/brain_intelligence/__init__.py +58 -0
  119. package/latticeai/services/brain_intelligence/_contract.py +71 -0
  120. package/latticeai/services/brain_intelligence/consistency.py +193 -0
  121. package/latticeai/services/brain_intelligence/constants.py +47 -0
  122. package/latticeai/services/brain_intelligence/digest.py +258 -0
  123. package/latticeai/services/brain_intelligence/health.py +331 -0
  124. package/latticeai/services/brain_intelligence/proposals.py +264 -0
  125. package/latticeai/services/brain_intelligence/sampling.py +84 -0
  126. package/latticeai/services/brain_intelligence/service.py +48 -0
  127. package/latticeai/services/chronicle.py +557 -0
  128. package/latticeai/services/memory_service/__init__.py +52 -0
  129. package/latticeai/services/memory_service/_contract.py +100 -0
  130. package/latticeai/services/memory_service/brief.py +431 -0
  131. package/latticeai/services/memory_service/constants.py +57 -0
  132. package/latticeai/services/memory_service/maintenance.py +138 -0
  133. package/latticeai/services/memory_service/manager.py +186 -0
  134. package/latticeai/services/memory_service/proof.py +136 -0
  135. package/latticeai/services/memory_service/recall.py +225 -0
  136. package/latticeai/services/memory_service/service.py +48 -0
  137. package/latticeai/services/memory_service/stores.py +110 -0
  138. package/latticeai/services/model_runtime/__init__.py +322 -0
  139. package/latticeai/services/model_runtime/cloud.py +87 -0
  140. package/latticeai/services/model_runtime/download.py +282 -0
  141. package/latticeai/services/model_runtime/engines.py +341 -0
  142. package/latticeai/services/model_runtime/loading.py +178 -0
  143. package/latticeai/services/model_runtime/service.py +129 -0
  144. package/latticeai/services/model_runtime/state.py +131 -0
  145. package/latticeai/services/model_runtime/status.py +255 -0
  146. package/latticeai/services/product_readiness.py +15 -7
  147. package/latticeai/setup/wizard/__init__.py +126 -0
  148. package/latticeai/setup/wizard/catalog.py +172 -0
  149. package/latticeai/setup/wizard/detect.py +323 -0
  150. package/latticeai/setup/wizard/install.py +348 -0
  151. package/latticeai/setup/wizard/paths.py +168 -0
  152. package/latticeai/setup/wizard/plans.py +74 -0
  153. package/latticeai/setup/wizard/recommend.py +320 -0
  154. package/package.json +6 -2
  155. package/scripts/bump_version.py +14 -0
  156. package/scripts/capture_release_evidence.mjs +33 -21
  157. package/scripts/check_current_release_docs.mjs +1 -1
  158. package/scripts/check_i18n_namespace_coverage.mjs +41 -4
  159. package/scripts/check_max_file_lines.mjs +102 -0
  160. package/scripts/check_release_evidence_bound.mjs +30 -15
  161. package/scripts/check_screenshot_pixel_delta.py +34 -4
  162. package/scripts/check_server_i18n.mjs +1 -0
  163. package/scripts/generate_rust_parity_fixtures.py +562 -0
  164. package/scripts/lib/mock_server_fingerprint.mjs +94 -0
  165. package/scripts/release_screen_claims.json +31 -2
  166. package/src-tauri/Cargo.lock +361 -3
  167. package/src-tauri/Cargo.toml +6 -1
  168. package/src-tauri/src/backend.rs +349 -0
  169. package/src-tauri/src/folder.rs +33 -0
  170. package/src-tauri/src/main.rs +97 -399
  171. package/src-tauri/tauri.conf.json +1 -1
  172. package/static/app/asset-manifest.json +41 -37
  173. package/static/app/assets/Act-yYpYnn0v.js +1 -0
  174. package/static/app/assets/AdminConsole-DL3Cr5pL.js +1 -0
  175. package/static/app/assets/{Brain-tuhI4sOC.js → Brain-C1HBN0Wf.js} +2 -2
  176. package/static/app/assets/BrainHome-DoXRhUUC.js +2 -0
  177. package/static/app/assets/BrainSignals-6yR6ir5t.js +1 -0
  178. package/static/app/assets/Capture-CFIRsFNE.js +1 -0
  179. package/static/app/assets/Chronicle-BZbEgiwN.js +1 -0
  180. package/static/app/assets/CommandPalette-D2pMxC2I.js +1 -0
  181. package/static/app/assets/Library-DwO3yZST.js +1 -0
  182. package/static/app/assets/{LivingBrain-DBwhto14.js → LivingBrain-Jn1GK0-S.js} +1 -1
  183. package/static/app/assets/ProductFlow-B-w1R4Oo.js +1 -0
  184. package/static/app/assets/ReviewCard-6B27X8Vg.js +3 -0
  185. package/static/app/assets/System-DW8F-2xL.js +1 -0
  186. package/static/app/assets/arrow-left-DXvKg9U6.js +1 -0
  187. package/static/app/assets/{bot-Cia42c2h.js → bot-IM_E_Y12.js} +1 -1
  188. package/static/app/assets/brain-Ci1CkWjM.js +1 -0
  189. package/static/app/assets/{button-2j2Ijzgq.js → button-COwyqfHM.js} +1 -1
  190. package/static/app/assets/circle-check-DfInj-qD.js +1 -0
  191. package/static/app/assets/{circle-pause-BEFeWpVW.js → circle-pause-DEM4A1Y5.js} +1 -1
  192. package/static/app/assets/{circle-play-ujXMcHxl.js → circle-play-C9djDuLd.js} +1 -1
  193. package/static/app/assets/{cpu-k4awryFq.js → cpu-DFdo1gw-.js} +1 -1
  194. package/static/app/assets/{download-DFbLJ_ig.js → download-SnJL6oqk.js} +1 -1
  195. package/static/app/assets/{folder-open-7y_b6xkM.js → folder-open-CqZeDkjE.js} +1 -1
  196. package/static/app/assets/{hard-drive-Bidh02Kr.js → hard-drive-j1jJXYYf.js} +1 -1
  197. package/static/app/assets/{index-DwDl9-8Y.css → index-BLPb5lmE.css} +1 -1
  198. package/static/app/assets/index-_u5iUHDr.js +10 -0
  199. package/static/app/assets/input-B0lPdRQZ.js +1 -0
  200. package/static/app/assets/link-2-CoFbooHS.js +1 -0
  201. package/static/app/assets/{permissionCopy-Bpb83Hx9.js → permissionCopy-BsyLxtao.js} +1 -1
  202. package/static/app/assets/primitives-DEbN-d6p.js +1 -0
  203. package/static/app/assets/search-BybIWPNd.js +1 -0
  204. package/static/app/assets/{share-2-BH1M-WNi.js → share-2-CVtZ_ewX.js} +1 -1
  205. package/static/app/assets/{shield-alert-BlKdBXcG.js → shield-alert-CBi2GNWM.js} +1 -1
  206. package/static/app/assets/{textarea-CCWbUfFB.js → textarea-DNMpB5ih.js} +1 -1
  207. package/static/app/assets/{useFocusTrap-YdHQ7pJ1.js → useFocusTrap-C83t3GXF.js} +1 -1
  208. package/static/app/assets/useMutation-DtbJDoyz.js +1 -0
  209. package/static/app/assets/{useQuery-CXQiwbVT.js → useQuery-Dcp1OChy.js} +1 -1
  210. package/static/app/assets/utils-BlZr7Pd4.js +4 -0
  211. package/static/app/assets/workspace-jJY4RuAV.js +1 -0
  212. package/static/app/index.html +4 -4
  213. package/static/sw.js +1 -1
  214. package/lattice_brain/graph/_kg_common.py +0 -1331
  215. package/lattice_brain/graph/discovery_index.py +0 -1141
  216. package/lattice_brain/graph/retrieval.py +0 -1120
  217. package/lattice_brain/graph/retrieval_vector.py +0 -1293
  218. package/lattice_brain/ingestion.py +0 -1525
  219. package/lattice_brain/multimodal.py +0 -1258
  220. package/latticeai/core/agent.py +0 -1465
  221. package/latticeai/core/embedding_providers.py +0 -1196
  222. package/latticeai/core/file_generation.py +0 -1047
  223. package/latticeai/integrations/telegram_bot.py +0 -1390
  224. package/latticeai/models/router.py +0 -1007
  225. package/latticeai/runtime/build_phases.py +0 -1450
  226. package/latticeai/services/brain_intelligence.py +0 -1083
  227. package/latticeai/services/memory_service.py +0 -1177
  228. package/latticeai/services/model_runtime.py +0 -1281
  229. package/latticeai/setup/wizard.py +0 -1310
  230. package/static/app/assets/Act-AWf0SAKp.js +0 -1
  231. package/static/app/assets/AdminConsole-D0u8Tiyj.js +0 -1
  232. package/static/app/assets/BrainHome-Ts7G_Ila.js +0 -2
  233. package/static/app/assets/BrainSignals-jMYgQ2Ar.js +0 -1
  234. package/static/app/assets/Capture-CqOSzyPr.js +0 -1
  235. package/static/app/assets/CommandPalette-DC0Bzh-I.js +0 -1
  236. package/static/app/assets/Library-CX-bbhmK.js +0 -1
  237. package/static/app/assets/ProductFlow-BHA2cfKI.js +0 -1
  238. package/static/app/assets/ReviewCard-BUhCKRNM.js +0 -3
  239. package/static/app/assets/System-Bu2t5hn1.js +0 -1
  240. package/static/app/assets/arrow-left-Dzwa5zRb.js +0 -1
  241. package/static/app/assets/brain-DJMoqrwx.js +0 -1
  242. package/static/app/assets/index-BpYkzcVm.js +0 -10
  243. package/static/app/assets/input-DSlJJxRs.js +0 -1
  244. package/static/app/assets/primitives-BCx6TvfG.js +0 -1
  245. package/static/app/assets/search-Cgy8cCFJ.js +0 -1
  246. package/static/app/assets/utils-zqPZJxdx.js +0 -4
  247. 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