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,352 @@
1
+ """Images on the same contract as text — with two deliberate differences.
2
+
3
+ **No fallback.** The hash embedder turns text into a real, if crude, cosine
4
+ signal; hashing a file path says nothing about the picture, so an unavailable
5
+ vision model is reported unavailable and the caller skips the embedding rather
6
+ than storing a decoy.
7
+
8
+ **A separate space by default.** A CLIP-family image vector is not comparable
9
+ with a BGE text vector, so ``space == "image"`` means "index these apart and
10
+ join them by late fusion". Only a genuinely shared-space model may declare
11
+ ``space == "shared"``, and only then can a text query be scored against image
12
+ vectors.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import os
18
+ from dataclasses import dataclass
19
+ from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence
20
+
21
+ from .base import (
22
+ EmbeddingProvider,
23
+ EmbeddingUnavailable,
24
+ _l2_normalize,
25
+ _load_dotted,
26
+ _RemoteConfig,
27
+ )
28
+
29
+ #: CLIP ViT-B/32 width — the most common local image-embedding output.
30
+ DEFAULT_VISION_DIM = 512
31
+ #: Image vectors live in their own index and join text results by late fusion.
32
+ VISION_SPACE_IMAGE = "image"
33
+ #: A genuinely multimodal model (CLIP-style) can share the text space — opt-in.
34
+ VISION_SPACE_SHARED = "shared"
35
+ VISION_SPACES = (VISION_SPACE_IMAGE, VISION_SPACE_SHARED)
36
+ VISION_PROVIDER_TYPES = ("mlx", "custom")
37
+ VISION_TARGET_ENV = "LATTICEAI_VISION_EMBEDDING_TARGET"
38
+
39
+
40
+ _KNOWN_VISION_DIMS = {
41
+ "clip-vit-base-patch32": 512,
42
+ "clip-vit-base-patch16": 512,
43
+ "clip-vit-large-patch14": 768,
44
+ "siglip-base-patch16-224": 768,
45
+ "siglip-large-patch16-384": 1024,
46
+ }
47
+
48
+
49
+ def _guess_vision_dim(model: str, default: int) -> int:
50
+ key = str(model or "").split("/")[-1].strip().lower()
51
+ key = key.split(":")[0]
52
+ return _KNOWN_VISION_DIMS.get(key, default)
53
+
54
+
55
+ def _normalize_space(value: Any) -> str:
56
+ space = str(value or "").strip().lower()
57
+ return space if space in VISION_SPACES else VISION_SPACE_IMAGE
58
+
59
+
60
+ class VisionEmbeddingProvider(EmbeddingProvider):
61
+ """Turns an image *file path* into a vector.
62
+
63
+ Subclasses implement :meth:`embed_images`; everything else — the single
64
+ ``embed_image``, L2 normalization, locking the index identity to the width
65
+ the model actually returned, and the refusal to embed text outside a shared
66
+ space — is shared here.
67
+ """
68
+
69
+ provider = "vision"
70
+ grade = "production"
71
+ #: ``"image"`` (own index, late fusion) or ``"shared"`` (same space as text)
72
+ space: str = VISION_SPACE_IMAGE
73
+
74
+ def __init__(self, cfg: _RemoteConfig):
75
+ self._cfg = cfg
76
+ self.dim = int(cfg.dim or DEFAULT_VISION_DIM)
77
+ self.space = _normalize_space(cfg.extra.get("space"))
78
+
79
+ # ── required ──────────────────────────────────────────────────────────
80
+ def embed_images(self, paths: Sequence[str]) -> List[List[float]]:
81
+ raise NotImplementedError
82
+
83
+ # ── derived (shared) ──────────────────────────────────────────────────
84
+ @property
85
+ def shares_text_space(self) -> bool:
86
+ """True when a text query may be scored against these vectors."""
87
+ return self.space == VISION_SPACE_SHARED
88
+
89
+ def embed_image(self, path: str) -> List[float]:
90
+ vectors = self.embed_images([path])
91
+ if not vectors:
92
+ raise EmbeddingUnavailable(f"{self.model_id} returned no vector for {path}")
93
+ return vectors[0]
94
+
95
+ def embed_batch(self, texts: Sequence[str]) -> List[List[float]]:
96
+ """Text side of a multimodal model — only in a shared space.
97
+
98
+ Scoring a BGE query vector against CLIP image vectors produces a number
99
+ with no meaning, so the image-space default refuses rather than
100
+ returning something a caller would rank on.
101
+ """
102
+ if not self.shares_text_space:
103
+ raise EmbeddingUnavailable(
104
+ f"{self.model_id} embeds images into a separate space; text "
105
+ "queries reach image nodes through late fusion, not this index"
106
+ )
107
+ return self._normalize_rows(self._embed_texts_raw(texts))
108
+
109
+ def _embed_texts_raw(self, texts: Sequence[str]) -> List[List[float]]:
110
+ raise NotImplementedError
111
+
112
+ def _normalize_rows(self, rows: Iterable[Any]) -> List[List[float]]:
113
+ """L2-normalize, and lock the index identity to the real width."""
114
+ out: List[List[float]] = []
115
+ for row in rows:
116
+ vec = [float(x) for x in (row or [])]
117
+ if not vec:
118
+ raise EmbeddingUnavailable(f"{self.model_id} produced an empty vector")
119
+ self.dim = len(vec)
120
+ self.model_id = self._model_id_with_dim(self.dim)
121
+ out.append(_l2_normalize(vec))
122
+ return out
123
+
124
+ def metadata(self) -> Dict[str, Any]:
125
+ data = super().metadata()
126
+ data.update(
127
+ {
128
+ "modality": "image",
129
+ "space": self.space,
130
+ "shares_text_space": self.shares_text_space,
131
+ }
132
+ )
133
+ return data
134
+
135
+
136
+ class MLXVisionEmbeddingProvider(VisionEmbeddingProvider):
137
+ """Local CLIP-family image embedder loaded through ``mlx_clip``.
138
+
139
+ Guarded import, opt-in, never a core dependency: the module must expose
140
+ ``load(model)`` returning an encoder with ``encode_image(paths)`` and —
141
+ for a shared space — ``encode_text(texts)``.
142
+ """
143
+
144
+ provider = "mlx-vision"
145
+
146
+ def __init__(self, cfg: _RemoteConfig):
147
+ super().__init__(cfg)
148
+ if not cfg.dim:
149
+ self.dim = _guess_vision_dim(cfg.model, DEFAULT_VISION_DIM)
150
+ self.model_id = f"mlx-vision:{cfg.model}:{self.dim}"
151
+ self._encoder: Optional[Any] = None
152
+
153
+ def _load(self) -> Any:
154
+ if self._encoder is not None:
155
+ return self._encoder
156
+ try: # optional dependency; only imported when this provider is used
157
+ import mlx_clip # type: ignore
158
+
159
+ self._encoder = mlx_clip.load(self._cfg.model)
160
+ except Exception as exc:
161
+ raise EmbeddingUnavailable(f"MLX vision model unavailable: {exc}") from exc
162
+ return self._encoder
163
+
164
+ def embed_images(self, paths: Sequence[str]) -> List[List[float]]:
165
+ encoder = self._load()
166
+ try:
167
+ rows = encoder.encode_image(list(paths))
168
+ except Exception as exc:
169
+ raise EmbeddingUnavailable(f"MLX vision embedding failed: {exc}") from exc
170
+ return self._normalize_rows(rows)
171
+
172
+ def _embed_texts_raw(self, texts: Sequence[str]) -> List[List[float]]:
173
+ encoder = self._load()
174
+ encode_text = getattr(encoder, "encode_text", None)
175
+ if not callable(encode_text):
176
+ raise EmbeddingUnavailable(
177
+ f"{self.model_id} has no text encoder, so it cannot back a shared space"
178
+ )
179
+ try:
180
+ return list(encode_text(list(texts)))
181
+ except Exception as exc:
182
+ raise EmbeddingUnavailable(f"MLX vision text embedding failed: {exc}") from exc
183
+
184
+ def health(self) -> Dict[str, Any]:
185
+ try:
186
+ self._load()
187
+ return {"status": "ok", "detail": f"MLX vision model {self._cfg.model} loaded"}
188
+ except Exception as exc:
189
+ return {"status": "unavailable", "detail": str(exc)}
190
+
191
+
192
+ class CustomVisionEmbeddingProvider(VisionEmbeddingProvider):
193
+ """A user-supplied ``module:callable`` that embeds image paths.
194
+
195
+ The callable receives ``List[str]`` (paths) and returns
196
+ ``List[List[float]]``. Configured via ``LATTICEAI_VISION_EMBEDDING_TARGET``.
197
+ """
198
+
199
+ provider = "custom-vision"
200
+
201
+ def __init__(self, cfg: _RemoteConfig):
202
+ super().__init__(cfg)
203
+ self._target_ref = str(cfg.extra.get("target") or os.getenv(VISION_TARGET_ENV, ""))
204
+ self.model_id = f"custom-vision:{cfg.model or self._target_ref or 'callable'}:{self.dim}"
205
+ self._fn: Optional[Callable[..., Any]] = None
206
+
207
+ def _load(self) -> Callable[..., Any]:
208
+ if self._fn is not None:
209
+ return self._fn
210
+ self._fn = _load_dotted(self._target_ref, VISION_TARGET_ENV, "vision embedding")
211
+ return self._fn
212
+
213
+ def embed_images(self, paths: Sequence[str]) -> List[List[float]]:
214
+ fn = self._load()
215
+ try:
216
+ rows = list(fn(list(paths)))
217
+ except Exception as exc:
218
+ raise EmbeddingUnavailable(f"custom vision embedding failed: {exc}") from exc
219
+ return self._normalize_rows(rows)
220
+
221
+ def _embed_texts_raw(self, texts: Sequence[str]) -> List[List[float]]:
222
+ # A dotted image embedder is one callable over paths; a shared space
223
+ # would need a second, text-side entry point this contract has no slot
224
+ # for. Saying so beats scoring a query against the wrong function.
225
+ raise EmbeddingUnavailable(
226
+ f"{self.model_id} is an image-only callable and has no text encoder"
227
+ )
228
+
229
+ def health(self) -> Dict[str, Any]:
230
+ try:
231
+ self._load()
232
+ return {"status": "ok", "detail": f"custom vision target {self._target_ref} loaded"}
233
+ except Exception as exc:
234
+ return {"status": "unavailable", "detail": str(exc)}
235
+
236
+
237
+ def build_vision_provider(
238
+ provider: str,
239
+ *,
240
+ model: str = "",
241
+ dim: int = 0,
242
+ space: str = VISION_SPACE_IMAGE,
243
+ timeout: float = 30.0,
244
+ extra: Optional[Dict[str, Any]] = None,
245
+ ) -> VisionEmbeddingProvider:
246
+ """Construct a vision provider by name. Never makes a network call."""
247
+ kind = str(provider or "").strip().lower()
248
+ cfg = _RemoteConfig(
249
+ model=model,
250
+ dim=int(dim or 0),
251
+ timeout=float(timeout or 30.0),
252
+ extra={"space": space, **(extra or {})},
253
+ )
254
+ if kind == "mlx":
255
+ return MLXVisionEmbeddingProvider(cfg)
256
+ if kind == "custom":
257
+ return CustomVisionEmbeddingProvider(cfg)
258
+ raise ValueError(
259
+ f"unknown vision embedding provider: {provider!r} (expected one of {VISION_PROVIDER_TYPES})"
260
+ )
261
+
262
+
263
+ @dataclass
264
+ class ResolvedVisionEmbedder:
265
+ """A vision provider, or an honest account of why there isn't one."""
266
+
267
+ provider: Optional[VisionEmbeddingProvider]
268
+ requested: str
269
+ health: Dict[str, Any]
270
+ detail: str = ""
271
+
272
+ @property
273
+ def available(self) -> bool:
274
+ return self.provider is not None
275
+
276
+ @property
277
+ def space(self) -> str:
278
+ return self.provider.space if self.provider is not None else VISION_SPACE_IMAGE
279
+
280
+ def as_port(self) -> Optional[Callable[[str], List[float]]]:
281
+ """The one-argument seam Brain Core injects (``None`` when absent).
282
+
283
+ ``lattice_brain`` must not import ``latticeai``, so the ingestion
284
+ pipeline never sees this class — only the callable it hands over.
285
+ """
286
+ if self.provider is None:
287
+ return None
288
+ return self.provider.embed_image
289
+
290
+ def as_dict(self) -> Dict[str, Any]:
291
+ payload: Dict[str, Any] = {
292
+ "requested_provider": self.requested,
293
+ "available": self.available,
294
+ "health": self.health,
295
+ "detail": self.detail,
296
+ }
297
+ if self.provider is not None:
298
+ payload.update(self.provider.metadata())
299
+ return payload
300
+
301
+
302
+ def resolve_vision_embedder(
303
+ provider: str = "",
304
+ *,
305
+ model: str = "",
306
+ dim: int = 0,
307
+ space: str = VISION_SPACE_IMAGE,
308
+ timeout: float = 30.0,
309
+ extra: Optional[Dict[str, Any]] = None,
310
+ probe: bool = True,
311
+ ) -> ResolvedVisionEmbedder:
312
+ """Build the requested vision provider, or report it unavailable.
313
+
314
+ Unlike :func:`resolve_embedder` there is no fallback: a hashed file path is
315
+ not a picture. An empty ``provider`` means the user never asked for image
316
+ embeddings, which is a configuration state rather than a failure.
317
+ """
318
+ requested = str(provider or "").strip().lower()
319
+ if not requested:
320
+ return ResolvedVisionEmbedder(
321
+ None,
322
+ "",
323
+ {"status": "unavailable", "detail": "no vision provider configured"},
324
+ "image embeddings are off; set a vision provider to enable them",
325
+ )
326
+ try:
327
+ prov = build_vision_provider(
328
+ requested, model=model, dim=dim, space=space, timeout=timeout, extra=extra
329
+ )
330
+ except Exception as exc:
331
+ return ResolvedVisionEmbedder(
332
+ None,
333
+ requested,
334
+ {"status": "unavailable", "detail": str(exc)},
335
+ f"could not construct vision provider {requested}",
336
+ )
337
+ if not probe:
338
+ return ResolvedVisionEmbedder(
339
+ prov, requested, {"status": "unknown", "detail": "not probed"}, ""
340
+ )
341
+ try:
342
+ health = prov.health()
343
+ except Exception as exc: # a provider probe must never crash startup
344
+ health = {"status": "unavailable", "detail": str(exc)}
345
+ if health.get("status") != "ok":
346
+ return ResolvedVisionEmbedder(
347
+ None,
348
+ requested,
349
+ health,
350
+ f"{requested} vision model unavailable ({health.get('detail', '')})",
351
+ )
352
+ return ResolvedVisionEmbedder(prov, requested, health, "")
@@ -0,0 +1,115 @@
1
+ """Model-agnostic file content generation pipeline.
2
+
3
+ Small local models (gemma/qwen/llama 7B class) asked to "generate an HTML
4
+ file" commonly wrap the payload in chat noise: leading commentary ("Sure!
5
+ Here is your page:"), Markdown fences, ``<think>`` reasoning blocks,
6
+ trailing explanations, or an incomplete document. The previous direct-write
7
+ path saved that reply nearly verbatim, so weak models produced broken files.
8
+
9
+ This package makes the file-creation flow robust regardless of which LLM is
10
+ loaded, by treating the model as an untrusted content source:
11
+
12
+ 1. Prompt — extension-aware instructions anchored with the exact first
13
+ line the reply must start with (small models follow examples, not rules).
14
+ → :mod:`.prompting`
15
+ 2. Extract — strip reasoning blocks and conversational framing, pick the
16
+ best fenced block, slice known document boundaries. → :mod:`.extraction`
17
+ 3. Validate — per-extension structural checks (HTML document shape, JSON
18
+ parses, CSS has rule blocks, refusal/chat detection). → :mod:`.validation`
19
+ 4. Retry — one corrective attempt that tells the model what was wrong.
20
+ → :mod:`.orchestration`
21
+ 5. Repair — deterministic scaffolds guarantee the user still gets a valid
22
+ file even when the model never produces usable output. → :mod:`.repair`
23
+
24
+ Alongside the five steps: :mod:`.sanitize` is the single write-side door every
25
+ entry point runs untrusted content through, :mod:`.inference` guesses a
26
+ filename or a whole project manifest when the user named neither, and
27
+ :mod:`.bundles` checks that a multi-file project's links hold together.
28
+
29
+ The pipeline is pure (no I/O, no FastAPI); the chat layer injects an async
30
+ ``generate(context) -> str`` callable.
31
+
32
+ Split into these submodules in v11.3.0 with no behaviour change. Every name
33
+ the single module exposed still resolves from ``latticeai.core.file_generation``.
34
+
35
+ Stubbing note: a name rebound *here* changes only this module's binding — the
36
+ submodule that calls it holds its own. A test standing in for one of the
37
+ regex/helper seams patches the submodule that reads it (for example
38
+ ``latticeai.core.file_generation.inference._PKG_NAME_RE``).
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ # The single module bound this for its own use, which made it part of the
44
+ # surface callers could reach. Re-exported in the redundant-alias form so it
45
+ # reads as deliberate rather than as a leftover import.
46
+ from latticeai.core.quiet import quiet as quiet
47
+
48
+ from .bundles import _EXTERNAL_REF_PREFIXES as _EXTERNAL_REF_PREFIXES
49
+ from .bundles import _HTML_LOCAL_REF_RE as _HTML_LOCAL_REF_RE
50
+ from .bundles import _local_bundle_refs as _local_bundle_refs
51
+ from .bundles import repair_bundle_references as repair_bundle_references
52
+ from .bundles import validate_project_bundle as validate_project_bundle
53
+ from .extraction import _CHAT_LINE_RE as _CHAT_LINE_RE
54
+ from .extraction import _EXT_FENCE_LANGS as _EXT_FENCE_LANGS
55
+ from .extraction import _FENCE_RE as _FENCE_RE
56
+ from .extraction import _THINK_BLOCK_RE as _THINK_BLOCK_RE
57
+ from .extraction import _THINK_OPEN_RE as _THINK_OPEN_RE
58
+ from .extraction import _ext as _ext
59
+ from .extraction import _slice_html_document as _slice_html_document
60
+ from .extraction import _slice_json_document as _slice_json_document
61
+ from .extraction import _strip_chat_lines as _strip_chat_lines
62
+ from .extraction import extract_file_content as extract_file_content
63
+ from .inference import _CREATE_VERB_RE as _CREATE_VERB_RE
64
+ from .inference import _CSS_HINT_RE as _CSS_HINT_RE
65
+ from .inference import _EXPLICIT_FILENAME_RE as _EXPLICIT_FILENAME_RE
66
+ from .inference import _HTML_HINT_RE as _HTML_HINT_RE
67
+ from .inference import _JS_HINT_RE as _JS_HINT_RE
68
+ from .inference import _PACKAGE_HINT_RE as _PACKAGE_HINT_RE
69
+ from .inference import _PKG_NAME_RE as _PKG_NAME_RE
70
+ from .inference import _PROJECT_NAME_RE as _PROJECT_NAME_RE
71
+ from .inference import _PYTHON_HINT_RE as _PYTHON_HINT_RE
72
+ from .inference import _REACT_HINT_RE as _REACT_HINT_RE
73
+ from .inference import _TYPE_KEYWORDS as _TYPE_KEYWORDS
74
+ from .inference import _VITE_HINT_RE as _VITE_HINT_RE
75
+ from .inference import _python_package_manifest as _python_package_manifest
76
+ from .inference import _react_manifest as _react_manifest
77
+ from .inference import infer_file_target as infer_file_target
78
+ from .inference import infer_project_manifest as infer_project_manifest
79
+ from .orchestration import _salvage_score as _salvage_score
80
+ from .orchestration import generate_file_content as generate_file_content
81
+ from .prompting import _BUNDLE_HTML_MODULE_RULE as _BUNDLE_HTML_MODULE_RULE
82
+ from .prompting import _BUNDLE_HTML_RULE as _BUNDLE_HTML_RULE
83
+ from .prompting import _FIRST_LINE_HINTS as _FIRST_LINE_HINTS
84
+ from .prompting import _TYPE_RULES as _TYPE_RULES
85
+ from .prompting import _bundle_html_rule as _bundle_html_rule
86
+ from .prompting import build_file_generation_context as build_file_generation_context
87
+ from .repair import _repair_html as _repair_html
88
+ from .repair import repair_file_content as repair_file_content
89
+ from .sanitize import PREVIEWABLE_EXTENSIONS as PREVIEWABLE_EXTENSIONS
90
+ from .sanitize import sanitize_write_content as sanitize_write_content
91
+ from .validation import _BRACED_CODE_EXTENSIONS as _BRACED_CODE_EXTENSIONS
92
+ from .validation import _COMMENTARY_RE as _COMMENTARY_RE
93
+ from .validation import _COMPONENT_EXTENSIONS as _COMPONENT_EXTENSIONS
94
+ from .validation import _REFUSAL_RE as _REFUSAL_RE
95
+ from .validation import _check_balanced_delimiters as _check_balanced_delimiters
96
+ from .validation import _check_component_blocks as _check_component_blocks
97
+ from .validation import _looks_like_commentary as _looks_like_commentary
98
+ from .validation import _strip_code_literals as _strip_code_literals
99
+ from .validation import looks_like_refusal as looks_like_refusal
100
+ from .validation import validate_file_content as validate_file_content
101
+
102
+ __all__ = [
103
+ "PREVIEWABLE_EXTENSIONS",
104
+ "build_file_generation_context",
105
+ "extract_file_content",
106
+ "generate_file_content",
107
+ "infer_file_target",
108
+ "infer_project_manifest",
109
+ "looks_like_refusal",
110
+ "repair_bundle_references",
111
+ "repair_file_content",
112
+ "sanitize_write_content",
113
+ "validate_file_content",
114
+ "validate_project_bundle",
115
+ ]
@@ -0,0 +1,76 @@
1
+ """Bundle-level checks: does this set of files hold together?
2
+
3
+ A multi-file project is only as valid as its links. These helpers find the
4
+ local references an HTML document makes, repoint the ones a weak model got
5
+ almost right, and report every file that is invalid or dangling.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from typing import Any, Dict, List, Tuple
12
+
13
+ from .extraction import _ext
14
+ from .validation import validate_file_content
15
+
16
+ _HTML_LOCAL_REF_RE = re.compile(
17
+ r"(?:href|src)\s*=\s*[\"']([^\"'#?]+)[\"']", re.IGNORECASE
18
+ )
19
+ _EXTERNAL_REF_PREFIXES = ("http://", "https://", "//", "data:", "mailto:", "tel:", "javascript:")
20
+
21
+
22
+ def _local_bundle_refs(html: str) -> List[str]:
23
+ """File references inside an HTML document that must exist in the bundle."""
24
+ refs: List[str] = []
25
+ for ref in _HTML_LOCAL_REF_RE.findall(html or ""):
26
+ candidate = ref.strip()
27
+ if not candidate or candidate.startswith(_EXTERNAL_REF_PREFIXES):
28
+ continue
29
+ if "." not in candidate.rsplit("/", 1)[-1]:
30
+ continue # anchors / routes, not files
31
+ refs.append(candidate)
32
+ return refs
33
+
34
+
35
+ def repair_bundle_references(files: Dict[str, str]) -> Tuple[Dict[str, str], List[str]]:
36
+ """Deterministically point dangling HTML refs at real bundle files.
37
+
38
+ A weak model asked for ``style.css`` sometimes links ``styles.css``. When
39
+ a referenced file is missing but the bundle contains exactly one file of
40
+ the same extension, the reference is rewritten. Returns ``(files, fixes)``.
41
+ """
42
+ names = {p.rsplit("/", 1)[-1] for p in files}
43
+ fixes: List[str] = []
44
+ repaired = dict(files)
45
+ for path, content in files.items():
46
+ if _ext(path) not in (".html", ".htm"):
47
+ continue
48
+ updated = content
49
+ for ref in _local_bundle_refs(content):
50
+ base = ref.rsplit("/", 1)[-1]
51
+ if base in names:
52
+ continue
53
+ same_ext = [n for n in names if _ext(n) == _ext(base)]
54
+ if len(same_ext) == 1:
55
+ updated = updated.replace(ref, same_ext[0])
56
+ fixes.append(f"{path}: '{ref}' -> '{same_ext[0]}'")
57
+ if updated != content:
58
+ repaired[path] = updated
59
+ return repaired, fixes
60
+
61
+
62
+ def validate_project_bundle(files: Dict[str, str]) -> Dict[str, Any]:
63
+ """Bundle-level verification: every file valid, every HTML ref resolvable."""
64
+ issues: List[str] = []
65
+ per_file: Dict[str, Dict[str, Any]] = {}
66
+ names = {p.rsplit("/", 1)[-1] for p in files}
67
+ for path, content in files.items():
68
+ ok, reason = validate_file_content(content, path)
69
+ per_file[path] = {"valid": ok, "reason": reason}
70
+ if not ok:
71
+ issues.append(f"{path}: {reason}")
72
+ if _ext(path) in (".html", ".htm"):
73
+ for ref in _local_bundle_refs(content):
74
+ if ref.rsplit("/", 1)[-1] not in names:
75
+ issues.append(f"{path}: references missing file '{ref}'")
76
+ return {"ok": not issues, "issues": issues, "files": per_file}