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,162 @@
1
+ """Descriptions of an image — what a vision-language model said, or nothing.
2
+
3
+ Every "clever" fallback here would be a lie: ``Image IMG_2381.png (JPEG
4
+ 3024x4032)`` is metadata wearing a caption's clothes, and once it is in the
5
+ graph nothing downstream can tell it from a model's actual description. So the
6
+ base :class:`VisionCaptioner` returns ``None`` and reports itself unavailable,
7
+ and ``vision_caption_port`` hands Brain Core ``None`` rather than a decoy.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ from typing import Any, Callable, Dict, Optional, Tuple
14
+
15
+ from .base import EmbeddingUnavailable, _load_dotted
16
+
17
+ VISION_CAPTION_TARGET_ENV = "LATTICEAI_VISION_CAPTION_TARGET"
18
+
19
+
20
+ class VisionCaptioner:
21
+ """Describes an image — the null implementation, which describes nothing.
22
+
23
+ Every "clever" fallback here is a lie: ``Image IMG_2381.png (JPEG
24
+ 3024x4032)`` is metadata wearing a caption's clothes, and once it is in the
25
+ graph nothing downstream can tell it from a model's actual description. So
26
+ the base class returns ``None`` and :meth:`available` says ``False``.
27
+ """
28
+
29
+ provider = "none"
30
+ model_id = ""
31
+
32
+ def available(self) -> bool:
33
+ return False
34
+
35
+ def caption(self, path: str, *, prompt: str = "") -> Optional[str]:
36
+ return None
37
+
38
+ def health(self) -> Dict[str, Any]:
39
+ return {"status": "unavailable", "detail": "no vision-language model is loaded"}
40
+
41
+ def metadata(self) -> Dict[str, Any]:
42
+ return {
43
+ "provider": self.provider,
44
+ "model": self.model_id,
45
+ "available": self.available(),
46
+ }
47
+
48
+
49
+ #: Short, literal instruction — a caption is a description, not an essay.
50
+ DEFAULT_CAPTION_PROMPT = "Describe this image in one factual sentence."
51
+
52
+
53
+ class MLXVisionCaptioner(VisionCaptioner):
54
+ """Caption through a locally loaded ``mlx_vlm`` model (guarded import)."""
55
+
56
+ provider = "mlx-vlm"
57
+
58
+ def __init__(self, model: str, *, prompt: str = DEFAULT_CAPTION_PROMPT, max_tokens: int = 64):
59
+ self.model_id = str(model or "")
60
+ self._prompt = prompt or DEFAULT_CAPTION_PROMPT
61
+ self._max_tokens = max(1, int(max_tokens))
62
+ self._loaded: Optional[Tuple[Any, Any]] = None
63
+
64
+ def _load(self) -> Tuple[Any, Any]:
65
+ if self._loaded is not None:
66
+ return self._loaded
67
+ try: # optional dependency (`pip install "ltcai[local]"`)
68
+ from mlx_vlm import load as vlm_load # type: ignore
69
+
70
+ model, processor = vlm_load(self.model_id)
71
+ self._loaded = (model, processor)
72
+ except Exception as exc:
73
+ raise EmbeddingUnavailable(f"vision-language model unavailable: {exc}") from exc
74
+ return self._loaded
75
+
76
+ def available(self) -> bool:
77
+ try:
78
+ self._load()
79
+ return True
80
+ except EmbeddingUnavailable:
81
+ return False
82
+
83
+ def caption(self, path: str, *, prompt: str = "") -> Optional[str]:
84
+ try:
85
+ model, processor = self._load()
86
+ from mlx_vlm import generate as vlm_generate # type: ignore
87
+
88
+ text = vlm_generate(
89
+ model,
90
+ processor,
91
+ str(path),
92
+ prompt or self._prompt,
93
+ max_tokens=self._max_tokens,
94
+ )
95
+ except Exception:
96
+ # A caption the model did not produce is not a caption. Absence is
97
+ # the honest answer, and every caller already handles it.
98
+ return None
99
+ cleaned = str(text or "").strip()
100
+ return cleaned or None
101
+
102
+ def health(self) -> Dict[str, Any]:
103
+ try:
104
+ self._load()
105
+ return {"status": "ok", "detail": f"VLM {self.model_id} loaded"}
106
+ except EmbeddingUnavailable as exc:
107
+ return {"status": "unavailable", "detail": str(exc)}
108
+
109
+
110
+ class CustomVisionCaptioner(VisionCaptioner):
111
+ """A user-supplied ``module:callable`` that captions an image path."""
112
+
113
+ provider = "custom-vlm"
114
+
115
+ def __init__(self, target: str = ""):
116
+ self._target_ref = str(target or os.getenv(VISION_CAPTION_TARGET_ENV, ""))
117
+ self.model_id = self._target_ref
118
+ self._fn: Optional[Callable[..., Any]] = None
119
+
120
+ def _load(self) -> Callable[..., Any]:
121
+ if self._fn is None:
122
+ self._fn = _load_dotted(self._target_ref, VISION_CAPTION_TARGET_ENV, "vision caption")
123
+ return self._fn
124
+
125
+ def available(self) -> bool:
126
+ try:
127
+ self._load()
128
+ return True
129
+ except EmbeddingUnavailable:
130
+ return False
131
+
132
+ def caption(self, path: str, *, prompt: str = "") -> Optional[str]:
133
+ try:
134
+ text = self._load()(str(path), prompt or DEFAULT_CAPTION_PROMPT)
135
+ except Exception:
136
+ return None
137
+ cleaned = str(text or "").strip()
138
+ return cleaned or None
139
+
140
+ def health(self) -> Dict[str, Any]:
141
+ try:
142
+ self._load()
143
+ return {"status": "ok", "detail": f"custom captioner {self._target_ref} loaded"}
144
+ except EmbeddingUnavailable as exc:
145
+ return {"status": "unavailable", "detail": str(exc)}
146
+
147
+
148
+ def resolve_vision_captioner(
149
+ provider: str = "", *, model: str = "", target: str = ""
150
+ ) -> VisionCaptioner:
151
+ """Build a captioner, or the null one that honestly captions nothing."""
152
+ kind = str(provider or "").strip().lower()
153
+ if kind in {"mlx", "mlx-vlm", "mlx_vlm"} and model:
154
+ return MLXVisionCaptioner(model)
155
+ if kind in {"custom", "custom-vlm"}:
156
+ return CustomVisionCaptioner(target)
157
+ return VisionCaptioner()
158
+
159
+
160
+ def vision_caption_port(captioner: VisionCaptioner) -> Optional[Callable[[str], Optional[str]]]:
161
+ """The caption seam Brain Core injects — ``None`` when no VLM is loaded."""
162
+ return captioner.caption if captioner.available() else None
@@ -0,0 +1,126 @@
1
+ """The production embedding profiles the setup and admin surfaces offer.
2
+
3
+ A literal table, deliberately: a profile is a *named, supported* combination of
4
+ provider, model and dimensionality, so the UI can offer a short list of things
5
+ known to work instead of asking the user to assemble one.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any, Dict, List
11
+
12
+ PRODUCTION_PROVIDER_PROFILES: Dict[str, Dict[str, Any]] = {
13
+ "local:bge-m3": {
14
+ "id": "local:bge-m3",
15
+ "provider": "mlx",
16
+ "model": "bge-m3",
17
+ "dimensions": 1024,
18
+ "grade": "production",
19
+ "family": "local",
20
+ "label": "BGE-M3 local",
21
+ "detail": "Multilingual semantic embeddings for local retrieval.",
22
+ },
23
+ "local:nomic-embed-text": {
24
+ "id": "local:nomic-embed-text",
25
+ "provider": "ollama",
26
+ "model": "nomic-embed-text",
27
+ "dimensions": 768,
28
+ "grade": "production",
29
+ "family": "local",
30
+ "label": "Nomic Embed Text local",
31
+ "detail": "General-purpose local semantic embeddings.",
32
+ },
33
+ "local:e5-large": {
34
+ "id": "local:e5-large",
35
+ "provider": "mlx",
36
+ "model": "e5-large",
37
+ "dimensions": 1024,
38
+ "grade": "production",
39
+ "family": "local",
40
+ "label": "E5 Large local",
41
+ "detail": "High-recall local retrieval profile.",
42
+ },
43
+ "local:gte-large": {
44
+ "id": "local:gte-large",
45
+ "provider": "mlx",
46
+ "model": "gte-large",
47
+ "dimensions": 1024,
48
+ "grade": "production",
49
+ "family": "local",
50
+ "label": "GTE Large local",
51
+ "detail": "Large local semantic embedding profile.",
52
+ },
53
+ "ollama:nomic-embed-text": {
54
+ "id": "ollama:nomic-embed-text",
55
+ "provider": "ollama",
56
+ "model": "nomic-embed-text",
57
+ "dimensions": 768,
58
+ "grade": "production",
59
+ "family": "ollama",
60
+ "label": "Ollama Nomic Embed Text",
61
+ "detail": "Production semantic embeddings through Ollama.",
62
+ },
63
+ "ollama:mxbai-embed-large": {
64
+ "id": "ollama:mxbai-embed-large",
65
+ "provider": "ollama",
66
+ "model": "mxbai-embed-large",
67
+ "dimensions": 1024,
68
+ "grade": "production",
69
+ "family": "ollama",
70
+ "label": "Ollama MXBAI Embed Large",
71
+ "detail": "High-quality local semantic embeddings through Ollama.",
72
+ },
73
+ "ollama:bge-m3": {
74
+ "id": "ollama:bge-m3",
75
+ "provider": "ollama",
76
+ "model": "bge-m3",
77
+ "dimensions": 1024,
78
+ "grade": "production",
79
+ "family": "ollama",
80
+ "label": "Ollama BGE-M3-compatible",
81
+ "detail": "BGE-M3-compatible providers exposed through Ollama.",
82
+ },
83
+ "mlx:bge-m3": {
84
+ "id": "mlx:bge-m3",
85
+ "provider": "mlx",
86
+ "model": "bge-m3",
87
+ "dimensions": 1024,
88
+ "grade": "production",
89
+ "family": "mlx",
90
+ "label": "MLX BGE-M3",
91
+ "detail": "Apple Silicon optimized local embeddings.",
92
+ },
93
+ "openai:text-embedding-3-small": {
94
+ "id": "openai:text-embedding-3-small",
95
+ "provider": "openai",
96
+ "model": "text-embedding-3-small",
97
+ "dimensions": 1536,
98
+ "grade": "production",
99
+ "family": "openai-compatible",
100
+ "label": "OpenAI-compatible small",
101
+ "detail": "OpenAI-compatible /v1/embeddings endpoint.",
102
+ },
103
+ "openai:text-embedding-3-large": {
104
+ "id": "openai:text-embedding-3-large",
105
+ "provider": "openai",
106
+ "model": "text-embedding-3-large",
107
+ "dimensions": 3072,
108
+ "grade": "production",
109
+ "family": "openai-compatible",
110
+ "label": "OpenAI-compatible large",
111
+ "detail": "Highest-dimensional OpenAI-compatible embedding profile.",
112
+ },
113
+ }
114
+
115
+
116
+ def embedding_provider_profiles() -> List[Dict[str, Any]]:
117
+ return [dict(PRODUCTION_PROVIDER_PROFILES[key]) for key in sorted(PRODUCTION_PROVIDER_PROFILES)]
118
+
119
+
120
+ def resolve_embedding_profile(profile: str) -> Dict[str, Any]:
121
+ if not profile:
122
+ return {}
123
+ key = str(profile).strip().lower()
124
+ if key in PRODUCTION_PROVIDER_PROFILES:
125
+ return dict(PRODUCTION_PROVIDER_PROFILES[key])
126
+ raise ValueError(f"unknown embedding profile: {profile!r}")
@@ -0,0 +1,350 @@
1
+ """The five text embedders, their factory, and the resolution that never fails.
2
+
3
+ Hash (offline fallback), MLX, Ollama, OpenAI-compatible and a user-supplied
4
+ dotted callable. ``build_embedding_provider`` constructs one by name without
5
+ ever making a network call; ``resolve_embedder`` probes it and degrades to the
6
+ hash fallback while *reporting* requested vs. active, so a down provider is
7
+ never quietly presented as live.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import importlib
13
+ import os
14
+ from dataclasses import dataclass
15
+ from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple
16
+
17
+ from latticeai.core.local_embeddings import DEFAULT_EMBEDDING_DIM, LocalEmbeddingModel
18
+
19
+ from .base import (
20
+ EmbeddingProvider,
21
+ EmbeddingUnavailable,
22
+ _guess_dim,
23
+ _NetworkEmbeddingProvider,
24
+ _RemoteConfig,
25
+ )
26
+
27
+
28
+ class HashEmbeddingProvider(EmbeddingProvider):
29
+ """Deterministic feature-hashing embedder — no network, always available."""
30
+
31
+ provider = "hash"
32
+ grade = "fallback"
33
+
34
+ def __init__(self, dim: int = DEFAULT_EMBEDDING_DIM):
35
+ self._model = LocalEmbeddingModel(dim=dim)
36
+ self.dim = self._model.dim
37
+ self.model_id = self._model.model_id
38
+
39
+ def embed(self, text: str) -> List[float]:
40
+ return self._model.embed(text) # already L2-normalized
41
+
42
+ def embed_batch(self, texts: Sequence[str]) -> List[List[float]]:
43
+ return [self._model.embed(t) for t in texts]
44
+
45
+ def health(self) -> Dict[str, Any]:
46
+ return {"status": "ok", "detail": "deterministic local fallback"}
47
+
48
+
49
+ def _as_float_list(value: Any) -> List[Any]:
50
+ """A pooled embedding row as a flat list.
51
+
52
+ ``mx.array.tolist()`` is typed as scalar-or-nested; at this call site the
53
+ array is always 1-D, so a scalar would be a bug worth surfacing.
54
+ """
55
+ if isinstance(value, (int, float)):
56
+ raise EmbeddingUnavailable("MLX embedding produced a scalar, not a vector")
57
+ return list(value)
58
+
59
+
60
+ class MLXEmbeddingProvider(_NetworkEmbeddingProvider):
61
+ provider = "mlx"
62
+
63
+ def __init__(self, cfg: _RemoteConfig):
64
+ super().__init__(cfg)
65
+ if not cfg.dim:
66
+ self.dim = _guess_dim(cfg.model, DEFAULT_EMBEDDING_DIM)
67
+ self.model_id = f"mlx:{cfg.model}:{self.dim}"
68
+ self._encoder: Optional[Tuple[str, Any, Any]] = None
69
+
70
+ def _load(self):
71
+ if self._encoder is not None:
72
+ return self._encoder
73
+ try: # optional dependency; only imported when this provider is used
74
+ from mlx_embeddings.utils import load as mlx_load # type: ignore
75
+
76
+ model, tokenizer = mlx_load(self._cfg.model)
77
+ self._encoder = ("mlx_embeddings", model, tokenizer)
78
+ return self._encoder
79
+ except Exception as exc: # pragma: no cover - environment dependent
80
+ raise EmbeddingUnavailable(f"MLX embedding model unavailable: {exc}") from exc
81
+
82
+ def _embed_raw(self, texts: Sequence[str]) -> List[List[float]]:
83
+ kind, model, tokenizer = self._load()
84
+ try:
85
+ import mlx.core as mx # type: ignore
86
+
87
+ out: List[List[float]] = []
88
+ for text in texts:
89
+ ids = tokenizer.encode(text)
90
+ tokens = mx.array([ids])
91
+ result = model(tokens)
92
+ pooled = result[0] if isinstance(result, (tuple, list)) else result
93
+ vec = mx.mean(pooled, axis=1)[0] if pooled.ndim == 3 else pooled[0]
94
+ out.append([float(x) for x in _as_float_list(vec.tolist())])
95
+ return out
96
+ except EmbeddingUnavailable:
97
+ raise
98
+ except Exception as exc: # pragma: no cover - environment dependent
99
+ raise EmbeddingUnavailable(f"MLX embedding failed: {exc}") from exc
100
+
101
+ def health(self) -> Dict[str, Any]:
102
+ try:
103
+ self._load()
104
+ return {"status": "ok", "detail": f"MLX model {self._cfg.model} loaded"}
105
+ except Exception as exc:
106
+ return {"status": "unavailable", "detail": str(exc)}
107
+
108
+
109
+ class OllamaEmbeddingProvider(_NetworkEmbeddingProvider):
110
+ provider = "ollama"
111
+
112
+ def __init__(self, cfg: _RemoteConfig):
113
+ super().__init__(cfg)
114
+ self._base = (cfg.base_url or "http://127.0.0.1:11434").rstrip("/")
115
+ if not cfg.dim:
116
+ self.dim = _guess_dim(cfg.model, DEFAULT_EMBEDDING_DIM)
117
+ self.model_id = f"ollama:{cfg.model}:{self.dim}"
118
+
119
+ def _embed_raw(self, texts: Sequence[str]) -> List[List[float]]:
120
+ out: List[List[float]] = []
121
+ try:
122
+ import httpx
123
+
124
+ with httpx.Client(timeout=self._cfg.timeout) as client:
125
+ # /api/embed supports batching; fall back to /api/embeddings.
126
+ resp = client.post(
127
+ f"{self._base}/api/embed",
128
+ json={"model": self._cfg.model, "input": list(texts)},
129
+ )
130
+ if resp.status_code == 404:
131
+ for text in texts:
132
+ r = client.post(
133
+ f"{self._base}/api/embeddings",
134
+ json={"model": self._cfg.model, "prompt": text},
135
+ )
136
+ r.raise_for_status()
137
+ out.append(r.json().get("embedding") or [])
138
+ return out
139
+ resp.raise_for_status()
140
+ data = resp.json()
141
+ return data.get("embeddings") or [data.get("embedding") or []]
142
+ except Exception as exc:
143
+ raise EmbeddingUnavailable(f"Ollama embedding failed: {exc}") from exc
144
+
145
+ def health(self) -> Dict[str, Any]:
146
+ try:
147
+ import httpx
148
+
149
+ with httpx.Client(timeout=min(self._cfg.timeout, 5.0)) as client:
150
+ r = client.get(f"{self._base}/api/tags")
151
+ r.raise_for_status()
152
+ return {"status": "ok", "detail": f"Ollama reachable at {self._base}"}
153
+ except Exception as exc:
154
+ return {"status": "unavailable", "detail": f"Ollama unreachable: {exc}"}
155
+
156
+
157
+ class OpenAICompatibleEmbeddingProvider(_NetworkEmbeddingProvider):
158
+ provider = "openai"
159
+
160
+ def __init__(self, cfg: _RemoteConfig):
161
+ super().__init__(cfg)
162
+ self._base = (cfg.base_url or "https://api.openai.com/v1").rstrip("/")
163
+ if not cfg.dim:
164
+ self.dim = _guess_dim(cfg.model, DEFAULT_EMBEDDING_DIM)
165
+ self.model_id = f"openai:{cfg.model}:{self.dim}"
166
+
167
+ def _headers(self) -> Dict[str, str]:
168
+ headers = {"Content-Type": "application/json"}
169
+ if self._cfg.api_key:
170
+ headers["Authorization"] = f"Bearer {self._cfg.api_key}"
171
+ return headers
172
+
173
+ def _embed_raw(self, texts: Sequence[str]) -> List[List[float]]:
174
+ try:
175
+ import httpx
176
+
177
+ with httpx.Client(timeout=self._cfg.timeout) as client:
178
+ r = client.post(
179
+ f"{self._base}/embeddings",
180
+ headers=self._headers(),
181
+ json={"model": self._cfg.model, "input": list(texts)},
182
+ )
183
+ r.raise_for_status()
184
+ rows = sorted(r.json().get("data", []), key=lambda d: d.get("index", 0))
185
+ return [row.get("embedding") or [] for row in rows]
186
+ except Exception as exc:
187
+ raise EmbeddingUnavailable(f"OpenAI-compatible embedding failed: {exc}") from exc
188
+
189
+ def health(self) -> Dict[str, Any]:
190
+ try:
191
+ self._embed_raw(["ping"])
192
+ return {"status": "ok", "detail": f"{self._base} reachable"}
193
+ except Exception as exc:
194
+ return {"status": "unavailable", "detail": str(exc)}
195
+
196
+
197
+ class CustomEmbeddingProvider(_NetworkEmbeddingProvider):
198
+ """Loads a dotted ``module:callable`` (or ``module.callable``).
199
+
200
+ The callable receives ``List[str]`` and returns ``List[List[float]]``.
201
+ Configured via ``LATTICEAI_EMBEDDING_CUSTOM_TARGET``.
202
+ """
203
+
204
+ provider = "custom"
205
+
206
+ def __init__(self, cfg: _RemoteConfig):
207
+ super().__init__(cfg)
208
+ self._target_ref = str(cfg.extra.get("target") or os.getenv("LATTICEAI_EMBEDDING_CUSTOM_TARGET", ""))
209
+ self.model_id = f"custom:{cfg.model or self._target_ref or 'callable'}:{self.dim}"
210
+ self._fn: Optional[Callable[..., Any]] = None
211
+
212
+ def _load(self):
213
+ if self._fn is not None:
214
+ return self._fn
215
+ ref = self._target_ref
216
+ if not ref:
217
+ raise EmbeddingUnavailable("custom embedding target not configured (LATTICEAI_EMBEDDING_CUSTOM_TARGET)")
218
+ module_name, _, attr = ref.replace(":", ".").rpartition(".")
219
+ if not module_name:
220
+ raise EmbeddingUnavailable(f"invalid custom embedding target: {ref}")
221
+ try:
222
+ module = importlib.import_module(module_name)
223
+ self._fn = getattr(module, attr)
224
+ return self._fn
225
+ except Exception as exc:
226
+ raise EmbeddingUnavailable(f"custom embedding target unavailable: {exc}") from exc
227
+
228
+ def _embed_raw(self, texts: Sequence[str]) -> List[List[float]]:
229
+ fn = self._load()
230
+ try:
231
+ return list(fn(list(texts)))
232
+ except Exception as exc:
233
+ raise EmbeddingUnavailable(f"custom embedding failed: {exc}") from exc
234
+
235
+ def health(self) -> Dict[str, Any]:
236
+ try:
237
+ self._load()
238
+ return {"status": "ok", "detail": f"custom target {self._target_ref} loaded"}
239
+ except Exception as exc:
240
+ return {"status": "unavailable", "detail": str(exc)}
241
+
242
+
243
+ # ── factory + resolution ──────────────────────────────────────────────────────
244
+ PROVIDER_TYPES = ("hash", "mlx", "ollama", "openai", "custom")
245
+
246
+
247
+ def build_embedding_provider(
248
+ provider: str,
249
+ *,
250
+ model: str = "",
251
+ base_url: str = "",
252
+ api_key: str = "",
253
+ dim: int = 0,
254
+ timeout: float = 30.0,
255
+ extra: Optional[Dict[str, Any]] = None,
256
+ ) -> EmbeddingProvider:
257
+ """Construct a provider by name. Never makes a network call."""
258
+ kind = str(provider or "hash").strip().lower()
259
+ if kind in {"", "hash", "local", "fallback"}:
260
+ return HashEmbeddingProvider(dim=int(dim or DEFAULT_EMBEDDING_DIM))
261
+ cfg = _RemoteConfig(
262
+ model=model,
263
+ base_url=base_url,
264
+ api_key=api_key,
265
+ dim=int(dim or 0),
266
+ timeout=float(timeout or 30.0),
267
+ extra=dict(extra or {}),
268
+ )
269
+ if kind == "mlx":
270
+ return MLXEmbeddingProvider(cfg)
271
+ if kind == "ollama":
272
+ return OllamaEmbeddingProvider(cfg)
273
+ if kind in {"openai", "openai-compatible", "openai_compatible"}:
274
+ return OpenAICompatibleEmbeddingProvider(cfg)
275
+ if kind == "custom":
276
+ return CustomEmbeddingProvider(cfg)
277
+ raise ValueError(f"unknown embedding provider: {provider!r} (expected one of {PROVIDER_TYPES})")
278
+
279
+
280
+ @dataclass
281
+ class ResolvedEmbedder:
282
+ provider: EmbeddingProvider
283
+ requested: str
284
+ active: str
285
+ fell_back: bool
286
+ health: Dict[str, Any]
287
+ detail: str = ""
288
+
289
+ def as_dict(self) -> Dict[str, Any]:
290
+ return {
291
+ "requested_provider": self.requested,
292
+ "active_provider": self.active,
293
+ "fell_back": self.fell_back,
294
+ "health": self.health,
295
+ "detail": self.detail,
296
+ **self.provider.metadata(),
297
+ }
298
+
299
+
300
+ def resolve_embedder(
301
+ provider: str = "",
302
+ *,
303
+ model: str = "",
304
+ base_url: str = "",
305
+ api_key: str = "",
306
+ dim: int = 0,
307
+ timeout: float = 30.0,
308
+ extra: Optional[Dict[str, Any]] = None,
309
+ probe: bool = True,
310
+ ) -> ResolvedEmbedder:
311
+ """Build the requested provider, degrading to hash if it is unavailable.
312
+
313
+ Local-first guarantee: the app always gets a working embedder. When the
314
+ requested provider is unreachable we return the hash fallback but record
315
+ ``fell_back=True`` and the failing health detail so the UI shows it as
316
+ *Unavailable* — the system never pretends a down provider is live.
317
+ """
318
+ requested = str(provider or "hash").strip().lower() or "hash"
319
+ if requested in {"hash", "local", "fallback", ""}:
320
+ hash_prov = HashEmbeddingProvider(dim=int(dim or DEFAULT_EMBEDDING_DIM))
321
+ return ResolvedEmbedder(
322
+ hash_prov, "hash", "hash", False, hash_prov.health(), "deterministic local fallback"
323
+ )
324
+
325
+ try:
326
+ prov = build_embedding_provider(
327
+ requested, model=model, base_url=base_url, api_key=api_key, dim=dim, timeout=timeout, extra=extra
328
+ )
329
+ except Exception as exc:
330
+ fallback = HashEmbeddingProvider(dim=int(dim or DEFAULT_EMBEDDING_DIM))
331
+ return ResolvedEmbedder(
332
+ fallback, requested, "hash", True,
333
+ {"status": "unavailable", "detail": str(exc)},
334
+ f"could not construct {requested}; using hash fallback",
335
+ )
336
+
337
+ if probe:
338
+ try:
339
+ health = prov.health()
340
+ except Exception as exc: # provider health must never crash startup
341
+ health = {"status": "unavailable", "detail": str(exc)}
342
+ else:
343
+ health = {"status": "unknown", "detail": "not probed"}
344
+ if probe and health.get("status") != "ok":
345
+ fallback = HashEmbeddingProvider(dim=int(dim or DEFAULT_EMBEDDING_DIM))
346
+ return ResolvedEmbedder(
347
+ fallback, requested, "hash", True, health,
348
+ f"{requested} unavailable ({health.get('detail', '')}); using hash fallback",
349
+ )
350
+ return ResolvedEmbedder(prov, requested, prov.provider, False, health, "")