ltcai 4.3.3 → 4.5.1

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 (138) hide show
  1. package/README.md +53 -20
  2. package/docs/CHANGELOG.md +122 -0
  3. package/docs/V4_4_0_EXTRACTION_REPORT.md +239 -0
  4. package/docs/V4_5_0_GEMMA_RUNTIME_COMPATIBILITY_REPORT.md +49 -0
  5. package/docs/V4_5_0_GRAPH_UX_REPORT.md +34 -0
  6. package/docs/V4_5_0_MODEL_RUNTIME_UX_REPORT.md +40 -0
  7. package/docs/V4_5_0_ONBOARDING_REPORT.md +31 -0
  8. package/docs/V4_5_0_PRODUCT_EXPERIENCE_RECOVERY_REPORT.md +49 -0
  9. package/docs/V4_5_0_VALIDATION_REPORT.md +60 -0
  10. package/docs/V4_5_1_GRAPH_EXPERIENCE_REPORT.md +33 -0
  11. package/docs/V4_5_1_MODEL_EXPERIENCE_REPORT.md +37 -0
  12. package/docs/V4_5_1_NAVIGATION_REPORT.md +37 -0
  13. package/docs/V4_5_1_ONBOARDING_REPORT.md +29 -0
  14. package/docs/V4_5_1_PRODUCT_REIMAGINING_REPORT.md +61 -0
  15. package/docs/V4_5_1_RC_ARTIFACTS.md +44 -0
  16. package/docs/V4_5_1_UX_REPORT.md +45 -0
  17. package/docs/V4_5_1_VALIDATION_REPORT.md +54 -0
  18. package/docs/V4_5_1_VISUAL_DESIGN_REPORT.md +30 -0
  19. package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +16 -16
  20. package/docs/architecture.md +8 -4
  21. package/frontend/src/App.tsx +152 -91
  22. package/frontend/src/api/client.ts +83 -1
  23. package/frontend/src/components/FirstRunGuide.tsx +99 -0
  24. package/frontend/src/components/primitives.tsx +131 -25
  25. package/frontend/src/components/ui/badge.tsx +2 -2
  26. package/frontend/src/components/ui/button.tsx +7 -7
  27. package/frontend/src/components/ui/card.tsx +5 -5
  28. package/frontend/src/components/ui/input.tsx +1 -1
  29. package/frontend/src/components/ui/textarea.tsx +1 -1
  30. package/frontend/src/pages/Act.tsx +58 -28
  31. package/frontend/src/pages/Ask.tsx +51 -19
  32. package/frontend/src/pages/Brain.tsx +60 -42
  33. package/frontend/src/pages/Capture.tsx +24 -24
  34. package/frontend/src/pages/Library.tsx +222 -32
  35. package/frontend/src/pages/System.tsx +56 -34
  36. package/frontend/src/routes.ts +15 -13
  37. package/frontend/src/store/appStore.ts +8 -1
  38. package/frontend/src/styles.css +666 -36
  39. package/lattice_brain/__init__.py +38 -23
  40. package/lattice_brain/_kg_common.py +11 -1
  41. package/lattice_brain/context.py +212 -2
  42. package/lattice_brain/conversations.py +234 -1
  43. package/lattice_brain/discovery.py +11 -1
  44. package/lattice_brain/documents.py +11 -1
  45. package/lattice_brain/graph/__init__.py +28 -0
  46. package/lattice_brain/graph/_kg_common.py +1123 -0
  47. package/lattice_brain/graph/curator.py +473 -0
  48. package/lattice_brain/graph/discovery.py +1455 -0
  49. package/lattice_brain/graph/documents.py +218 -0
  50. package/lattice_brain/graph/identity.py +175 -0
  51. package/lattice_brain/graph/ingest.py +644 -0
  52. package/lattice_brain/graph/network.py +205 -0
  53. package/lattice_brain/graph/projection.py +571 -0
  54. package/lattice_brain/graph/provenance.py +401 -0
  55. package/lattice_brain/graph/retrieval.py +1341 -0
  56. package/lattice_brain/graph/schema.py +640 -0
  57. package/lattice_brain/graph/store.py +237 -0
  58. package/lattice_brain/graph/write_master.py +225 -0
  59. package/lattice_brain/identity.py +11 -13
  60. package/lattice_brain/ingest.py +11 -1
  61. package/lattice_brain/ingestion.py +318 -0
  62. package/lattice_brain/memory.py +100 -1
  63. package/lattice_brain/network.py +11 -1
  64. package/lattice_brain/portability.py +431 -0
  65. package/lattice_brain/projection.py +11 -1
  66. package/lattice_brain/provenance.py +11 -1
  67. package/lattice_brain/retrieval.py +11 -1
  68. package/lattice_brain/runtime/__init__.py +32 -0
  69. package/lattice_brain/runtime/agent_runtime.py +569 -0
  70. package/lattice_brain/runtime/hooks.py +754 -0
  71. package/lattice_brain/runtime/multi_agent.py +795 -0
  72. package/lattice_brain/schema.py +11 -1
  73. package/lattice_brain/store.py +10 -2
  74. package/lattice_brain/workflow.py +461 -0
  75. package/lattice_brain/write_master.py +11 -1
  76. package/latticeai/__init__.py +1 -1
  77. package/latticeai/api/agents.py +2 -2
  78. package/latticeai/api/browser.py +1 -1
  79. package/latticeai/api/chat.py +1 -1
  80. package/latticeai/api/computer_use.py +1 -1
  81. package/latticeai/api/hooks.py +2 -2
  82. package/latticeai/api/mcp.py +1 -1
  83. package/latticeai/api/models.py +107 -18
  84. package/latticeai/api/tools.py +1 -1
  85. package/latticeai/api/workflow_designer.py +2 -2
  86. package/latticeai/app_factory.py +4 -4
  87. package/latticeai/brain/__init__.py +24 -6
  88. package/latticeai/brain/_kg_common.py +11 -1117
  89. package/latticeai/brain/context.py +12 -208
  90. package/latticeai/brain/conversations.py +12 -231
  91. package/latticeai/brain/discovery.py +13 -1451
  92. package/latticeai/brain/documents.py +13 -214
  93. package/latticeai/brain/identity.py +11 -169
  94. package/latticeai/brain/ingest.py +13 -640
  95. package/latticeai/brain/memory.py +12 -97
  96. package/latticeai/brain/network.py +12 -200
  97. package/latticeai/brain/projection.py +13 -567
  98. package/latticeai/brain/provenance.py +13 -397
  99. package/latticeai/brain/retrieval.py +13 -1337
  100. package/latticeai/brain/schema.py +12 -635
  101. package/latticeai/brain/store.py +13 -233
  102. package/latticeai/brain/write_master.py +13 -221
  103. package/latticeai/core/agent.py +1 -1
  104. package/latticeai/core/agent_registry.py +2 -2
  105. package/latticeai/core/builtin_hooks.py +2 -2
  106. package/latticeai/core/graph_curator.py +6 -468
  107. package/latticeai/core/hooks.py +6 -749
  108. package/latticeai/core/marketplace.py +1 -1
  109. package/latticeai/core/model_compat.py +250 -0
  110. package/latticeai/core/multi_agent.py +6 -790
  111. package/latticeai/core/workflow_engine.py +6 -456
  112. package/latticeai/core/workspace_os.py +1 -1
  113. package/latticeai/models/router.py +136 -32
  114. package/latticeai/services/agent_runtime.py +6 -564
  115. package/latticeai/services/ingestion.py +6 -313
  116. package/latticeai/services/kg_portability.py +6 -426
  117. package/latticeai/services/model_catalog.py +2 -2
  118. package/latticeai/services/model_recommendation.py +8 -1
  119. package/latticeai/services/model_runtime.py +18 -3
  120. package/latticeai/services/platform_runtime.py +3 -3
  121. package/latticeai/services/run_executor.py +1 -1
  122. package/latticeai/services/upload_service.py +1 -1
  123. package/p_reinforce.py +1 -1
  124. package/package.json +1 -1
  125. package/scripts/build_frontend_assets.mjs +12 -1
  126. package/scripts/bump_version.py +1 -1
  127. package/scripts/wheel_smoke.py +7 -0
  128. package/src-tauri/Cargo.lock +1 -1
  129. package/src-tauri/Cargo.toml +1 -1
  130. package/src-tauri/tauri.conf.json +1 -1
  131. package/static/app/asset-manifest.json +5 -5
  132. package/static/app/assets/index-3G8qcrIS.js +336 -0
  133. package/static/app/assets/index-3G8qcrIS.js.map +1 -0
  134. package/static/app/assets/index-C0wYZp7k.css +2 -0
  135. package/static/app/index.html +2 -2
  136. package/static/app/assets/index-CHHal8Zl.css +0 -2
  137. package/static/app/assets/index-pdzil9ac.js +0 -333
  138. package/static/app/assets/index-pdzil9ac.js.map +0 -1
@@ -1,431 +1,11 @@
1
- """Knowledge Graph portability local export / import / backup / restore.
1
+ """Compatibility shim: physically moved to ``lattice_brain.portability``.
2
2
 
3
- The Knowledge Graph is the user's durable asset, so it must be portable without
4
- any cloud service. Two complementary mechanisms, both fully local:
5
-
6
- * **Logical export/import** (JSON): nodes/edges/chunks/sources/provenance with a
7
- versioned header (schema + projection + embed-dim). Re-embeds on import, so it
8
- is portable across machines.
9
- * **Binary backup/restore** (ZIP): a faithful snapshot of the SQLite DB (incl.
10
- vector embeddings) plus the blob directory, integrity-checked, for
11
- same-machine recovery.
3
+ Aliases itself to the physical module so identity, module-level state, and
4
+ monkeypatching keep working through the old import path.
12
5
  """
13
6
 
14
- from __future__ import annotations
15
-
16
- import hashlib
17
- import json
18
- import shutil
19
- import tempfile
20
- import zipfile
21
- from datetime import datetime, timezone
22
- from pathlib import Path, PurePosixPath
23
- from typing import Any, Dict, Optional
24
-
25
- from lattice_brain.archive import BrainArchivePaths, EncryptedBrainArchive
26
- from lattice_brain.storage import (
27
- DockerPostgresWizard,
28
- PostgresEngine,
29
- SQLiteToPostgresMigrator,
30
- )
31
-
32
- FORMAT = "latticeai.kg.export"
33
- FORMAT_VERSION = 1
34
- BACKUP_FORMAT = "latticeai.kg.backup"
35
-
36
-
37
- def _now_iso() -> str:
38
- return datetime.now(timezone.utc).isoformat()
39
-
40
-
41
- def _stamp() -> str:
42
- return _now_iso().replace(":", "").replace("-", "").replace(".", "")[:15]
43
-
44
-
45
- def _sha256_file(path: Path) -> str:
46
- h = hashlib.sha256()
47
- with open(path, "rb") as fh:
48
- for block in iter(lambda: fh.read(65536), b""):
49
- h.update(block)
50
- return h.hexdigest()
51
-
52
-
53
- def _safe_zip_names(names) -> None:
54
- for name in names:
55
- path = PurePosixPath(name)
56
- if path.is_absolute() or ".." in path.parts:
57
- raise ValueError(f"Backup archive contains unsafe path: {name}")
58
-
59
-
60
- class KGPortabilityService:
61
- def __init__(self, *, knowledge_graph: Any, data_dir, enable_graph: bool = True, device_identity: Any = None) -> None:
62
- self._kg = knowledge_graph
63
- self._data_dir = Path(data_dir)
64
- self._enable = bool(enable_graph)
65
- self._exports_dir = self._data_dir / "workspace_exports"
66
- # v4 sovereignty: when a DeviceIdentity is wired, exports are signed
67
- # and imports record origin provenance. Pre-v4 unsigned bundles stay
68
- # importable locally (origin='unsigned-legacy') — signatures are
69
- # mandatory only on the Brain Network peer path.
70
- self._identity = device_identity
71
-
72
- def available(self) -> bool:
73
- return self._enable and self._kg is not None
74
-
75
- def _require(self) -> None:
76
- if not self.available():
77
- raise RuntimeError("Knowledge Graph is disabled (LATTICEAI_ENABLE_GRAPH).")
78
-
79
- # ── logical export / import ──────────────────────────────────────────────
80
- def export(self, *, workspace_id: Optional[str] = None) -> Dict[str, Any]:
81
- self._require()
82
- data = self._kg.export_graph_data(workspace_id=workspace_id)
83
- header = {
84
- "format": FORMAT,
85
- "format_version": FORMAT_VERSION,
86
- **self._kg.schema_versions(),
87
- "exported_at": _now_iso(),
88
- "workspace_id": workspace_id,
89
- "counts": data.get("counts"),
90
- }
91
- artifact = {"header": header, **data}
92
- if self._identity is not None:
93
- artifact["signature"] = self._identity.sign_manifest(header)
94
- return artifact
95
-
96
- def export_to_file(self, path=None, *, workspace_id: Optional[str] = None) -> Dict[str, Any]:
97
- artifact = self.export(workspace_id=workspace_id)
98
- self._exports_dir.mkdir(parents=True, exist_ok=True)
99
- path = Path(path) if path else self._exports_dir / f"kg-export-{_stamp()}.json"
100
- path.write_text(json.dumps(artifact, ensure_ascii=False, indent=2), encoding="utf-8")
101
- return {"path": str(path), "header": artifact["header"], "bytes": path.stat().st_size}
102
-
103
- def import_data(self, artifact: Dict[str, Any], *, mode: str = "merge", dry_run: bool = False) -> Dict[str, Any]:
104
- self._require()
105
- if not isinstance(artifact, dict) or "nodes" not in artifact:
106
- raise ValueError("Invalid Knowledge Graph export artifact.")
107
- if mode not in ("merge", "replace"):
108
- raise ValueError("mode must be 'merge' or 'replace'.")
109
- origin = "unsigned-legacy"
110
- signature = artifact.get("signature")
111
- if signature:
112
- from lattice_brain.identity import verify_manifest
113
-
114
- if not verify_manifest(artifact.get("header") or {}, signature):
115
- raise ValueError("Bundle signature verification failed — refusing to import.")
116
- origin = f"device:{signature.get('fingerprint') or 'unknown'}"
117
- result = self._kg.import_graph_data(artifact, mode=mode, dry_run=dry_run)
118
- result["header"] = artifact.get("header")
119
- result["origin"] = origin
120
- result["signed"] = bool(signature)
121
- if not dry_run:
122
- try:
123
- self._kg.record_provenance(
124
- node_id="import:" + str((artifact.get("header") or {}).get("exported_at") or _now_iso()),
125
- source_type="bundle_import",
126
- pipeline="kg-portability",
127
- owner=None,
128
- metadata={"origin": origin, "mode": mode,
129
- "counts": (artifact.get("header") or {}).get("counts")},
130
- )
131
- except Exception:
132
- pass
133
- return result
134
-
135
- def import_from_file(self, path, *, mode: str = "merge", dry_run: bool = False) -> Dict[str, Any]:
136
- artifact = json.loads(Path(path).read_text(encoding="utf-8"))
137
- return self.import_data(artifact, mode=mode, dry_run=dry_run)
138
-
139
- # ── binary backup / restore ──────────────────────────────────────────────
140
- def backup(self, dest_path=None) -> Dict[str, Any]:
141
- self._require()
142
- self._exports_dir.mkdir(parents=True, exist_ok=True)
143
- dest = Path(dest_path) if dest_path else self._exports_dir / f"kg-backup-{_stamp()}.zip"
144
- with tempfile.TemporaryDirectory() as tmp_s:
145
- tmp = Path(tmp_s)
146
- db_copy = tmp / "knowledge_graph.sqlite"
147
- self._kg.backup_database(db_copy)
148
- manifest = {
149
- "format": BACKUP_FORMAT,
150
- "format_version": FORMAT_VERSION,
151
- **self._kg.schema_versions(),
152
- "created_at": _now_iso(),
153
- "db_sha256": _sha256_file(db_copy),
154
- "has_blobs": Path(self._kg.blob_dir).exists(),
155
- }
156
- with zipfile.ZipFile(dest, "w", zipfile.ZIP_DEFLATED) as zf:
157
- zf.write(db_copy, "knowledge_graph.sqlite")
158
- zf.writestr("manifest.json", json.dumps(manifest, ensure_ascii=False, indent=2))
159
- blob_dir = Path(self._kg.blob_dir)
160
- if blob_dir.exists():
161
- for f in blob_dir.rglob("*"):
162
- if f.is_file():
163
- zf.write(f, f"blobs/{f.relative_to(blob_dir)}")
164
- return {"path": str(dest), "bytes": dest.stat().st_size, "manifest": manifest}
165
-
166
- def restore(
167
- self,
168
- archive_path,
169
- *,
170
- verify: bool = True,
171
- dry_run: bool = False,
172
- confirm: bool = False,
173
- ) -> Dict[str, Any]:
174
- self._require()
175
- archive = Path(archive_path)
176
- if not archive.exists():
177
- raise FileNotFoundError(f"Backup archive not found: {archive}")
178
- if not dry_run and not confirm:
179
- raise ValueError("Explicit confirmation is required before restoring a Knowledge Graph backup.")
180
- with zipfile.ZipFile(archive) as zf:
181
- names = zf.namelist()
182
- _safe_zip_names(names)
183
- if "knowledge_graph.sqlite" not in names:
184
- raise ValueError("Archive is missing knowledge_graph.sqlite.")
185
- manifest = json.loads(zf.read("manifest.json")) if "manifest.json" in names else {}
186
- with tempfile.TemporaryDirectory() as tmp_s:
187
- tmp = Path(tmp_s)
188
- zf.extractall(tmp)
189
- db_src = tmp / "knowledge_graph.sqlite"
190
- if verify and manifest.get("db_sha256"):
191
- if _sha256_file(db_src) != manifest["db_sha256"]:
192
- raise ValueError("Backup integrity check failed (db sha256 mismatch).")
193
- if dry_run:
194
- return {
195
- "restored": False,
196
- "dry_run": True,
197
- "verified": True,
198
- "manifest": manifest,
199
- "planned": {
200
- "database": str(self._kg.db_path),
201
- "blobs": str(self._kg.blob_dir),
202
- "archive": str(archive),
203
- },
204
- }
205
- db_dest = Path(self._kg.db_path)
206
- blob_dest = Path(self._kg.blob_dir)
207
- db_dest.parent.mkdir(parents=True, exist_ok=True)
208
- # Drop the live DB + stale WAL/SHM siblings so the restored copy
209
- # is authoritative (no stale journal overlaying old pages).
210
- for sib in (db_dest, Path(str(db_dest) + "-wal"), Path(str(db_dest) + "-shm")):
211
- if sib.exists():
212
- sib.unlink()
213
- shutil.copyfile(db_src, db_dest)
214
- blob_src = tmp / "blobs"
215
- if blob_src.exists():
216
- if blob_dest.exists():
217
- shutil.rmtree(blob_dest)
218
- shutil.copytree(blob_src, blob_dest)
219
- else:
220
- blob_dest.mkdir(parents=True, exist_ok=True)
221
- stats = self._kg.stats()
222
- return {
223
- "restored": True,
224
- "manifest": manifest,
225
- "nodes": sum(stats.get("nodes", {}).values()),
226
- }
227
-
228
- def verify_backup(self, archive_path) -> Dict[str, Any]:
229
- archive = Path(archive_path)
230
- if not archive.exists():
231
- return {"ok": False, "path": str(archive), "errors": [f"Backup archive not found: {archive}"]}
232
- try:
233
- with zipfile.ZipFile(archive) as zf:
234
- names = zf.namelist()
235
- _safe_zip_names(names)
236
- if "knowledge_graph.sqlite" not in names:
237
- raise ValueError("Archive is missing knowledge_graph.sqlite.")
238
- manifest = json.loads(zf.read("manifest.json")) if "manifest.json" in names else {}
239
- with tempfile.TemporaryDirectory() as tmp_s:
240
- tmp = Path(tmp_s)
241
- zf.extract("knowledge_graph.sqlite", tmp)
242
- db_src = tmp / "knowledge_graph.sqlite"
243
- if manifest.get("db_sha256") and _sha256_file(db_src) != manifest["db_sha256"]:
244
- raise ValueError("Backup integrity check failed (db sha256 mismatch).")
245
- return {"ok": True, "path": str(archive), "manifest": manifest, "errors": []}
246
- except (ValueError, zipfile.BadZipFile, OSError, json.JSONDecodeError) as exc:
247
- return {"ok": False, "path": str(archive), "errors": [str(exc)]}
248
-
249
- # ── encrypted .latticebrain archive ───────────────────────────────────
250
- def encrypted_archive(self, dest_path=None, *, passphrase: str) -> Dict[str, Any]:
251
- self._require()
252
- self._exports_dir.mkdir(parents=True, exist_ok=True)
253
- dest = Path(dest_path) if dest_path else self._exports_dir / f"brain-{_stamp()}.latticebrain"
254
- metadata = {
255
- "storage": self.storage_status().get("active", {}),
256
- "snapshot": self.snapshot_metadata(),
257
- "device_identity": self._identity.describe() if self._identity is not None else {},
258
- "provenance": {"exported_at": _now_iso(), "source": "kg-portability"},
259
- }
260
- archive = EncryptedBrainArchive(
261
- BrainArchivePaths(
262
- db_path=Path(self._kg.db_path),
263
- blob_dir=Path(self._kg.blob_dir),
264
- data_dir=self._data_dir,
265
- metadata=metadata,
266
- )
267
- )
268
- return archive.create(dest, passphrase=passphrase)
269
-
270
- def inspect_encrypted_archive(self, archive_path, *, passphrase: Optional[str] = None) -> Dict[str, Any]:
271
- archive = EncryptedBrainArchive(
272
- BrainArchivePaths(
273
- db_path=Path(self._kg.db_path),
274
- blob_dir=Path(self._kg.blob_dir),
275
- data_dir=self._data_dir,
276
- )
277
- )
278
- return archive.inspect(Path(archive_path), passphrase=passphrase)
279
-
280
- def verify_encrypted_archive(self, archive_path, *, passphrase: str) -> Dict[str, Any]:
281
- archive = EncryptedBrainArchive(
282
- BrainArchivePaths(
283
- db_path=Path(self._kg.db_path),
284
- blob_dir=Path(self._kg.blob_dir),
285
- data_dir=self._data_dir,
286
- )
287
- )
288
- return archive.verify(Path(archive_path), passphrase=passphrase)
289
-
290
- def restore_encrypted_archive(
291
- self,
292
- archive_path,
293
- *,
294
- passphrase: str,
295
- dry_run: bool = False,
296
- confirm: bool = False,
297
- ) -> Dict[str, Any]:
298
- self._require()
299
- archive = EncryptedBrainArchive(
300
- BrainArchivePaths(
301
- db_path=Path(self._kg.db_path),
302
- blob_dir=Path(self._kg.blob_dir),
303
- data_dir=self._data_dir,
304
- )
305
- )
306
- return archive.restore(
307
- Path(archive_path),
308
- passphrase=passphrase,
309
- target=BrainArchivePaths(
310
- db_path=Path(self._kg.db_path),
311
- blob_dir=Path(self._kg.blob_dir),
312
- data_dir=self._data_dir,
313
- ),
314
- dry_run=dry_run,
315
- confirm=confirm,
316
- )
317
-
318
- def import_encrypted_archive(
319
- self,
320
- archive_path,
321
- *,
322
- passphrase: str,
323
- dry_run: bool = False,
324
- confirm: bool = False,
325
- ) -> Dict[str, Any]:
326
- result = self.restore_encrypted_archive(
327
- archive_path,
328
- passphrase=passphrase,
329
- dry_run=dry_run,
330
- confirm=confirm,
331
- )
332
- result["operation"] = "import"
333
- return result
334
-
335
- # ── status surface ───────────────────────────────────────────────────────
336
- def snapshot_metadata(self) -> Dict[str, Any]:
337
- if not self.available():
338
- return {"available": False}
339
- return {
340
- "available": True,
341
- **self._kg.schema_versions(),
342
- "stats": self._kg.stats(),
343
- "provenance": self._kg.provenance_stats(),
344
- "storage": (
345
- self._kg.storage_engine.capabilities().as_dict()
346
- if getattr(self._kg, "storage_engine", None) is not None
347
- else {"engine": "sqlite", "available": True}
348
- ),
349
- }
350
-
351
- def storage_status(self) -> Dict[str, Any]:
352
- if not self.available():
353
- return {"available": False}
354
- return {
355
- "available": True,
356
- "active": (
357
- self._kg.storage_engine.capabilities().as_dict()
358
- if getattr(self._kg, "storage_engine", None) is not None
359
- else {"engine": "sqlite", "available": True}
360
- ),
361
- "postgres": PostgresEngine("", schema="lattice_brain").capabilities().as_dict(),
362
- "backup_health": self.backup_health(),
363
- }
364
-
365
- def backup_health(self) -> Dict[str, Any]:
366
- self._exports_dir.mkdir(parents=True, exist_ok=True)
367
- backups = sorted(
368
- [
369
- p for p in self._exports_dir.glob("*")
370
- if p.is_file() and (p.suffix == ".zip" or p.suffix == ".latticebrain")
371
- ],
372
- key=lambda p: p.stat().st_mtime,
373
- reverse=True,
374
- )
375
- latest = backups[0] if backups else None
376
- return {
377
- "available": True,
378
- "directory": str(self._exports_dir),
379
- "count": len(backups),
380
- "latest": str(latest) if latest else None,
381
- "latest_bytes": latest.stat().st_size if latest else 0,
382
- "encrypted_archives": sum(1 for p in backups if p.suffix == ".latticebrain"),
383
- "zip_backups": sum(1 for p in backups if p.suffix == ".zip"),
384
- }
385
-
386
- def postgres_docker_setup(
387
- self,
388
- *,
389
- consent: bool,
390
- dry_run: bool = False,
391
- port: int = 5432,
392
- ) -> Dict[str, Any]:
393
- wizard = DockerPostgresWizard(self._data_dir / "postgres", port=port)
394
- return wizard.start(consent=consent, dry_run=dry_run)
7
+ import sys
395
8
 
396
- def migrate_sqlite_to_postgres(
397
- self,
398
- *,
399
- dsn: str,
400
- schema: str = "lattice_brain",
401
- dry_run: bool = True,
402
- ) -> Dict[str, Any]:
403
- self._require()
404
- if not dsn:
405
- raise ValueError("Postgres DSN is required for SQLite to Postgres migration.")
406
- migrator = SQLiteToPostgresMigrator(
407
- Path(self._kg.db_path),
408
- PostgresEngine(dsn, schema=schema),
409
- )
410
- if dry_run:
411
- return migrator.migrate(dry_run=True)
412
- backup = self.backup()
413
- verification = self.verify_backup(backup["path"])
414
- if not verification.get("ok"):
415
- raise RuntimeError(
416
- "Pre-migration backup verification failed; Postgres migration was not started: "
417
- + "; ".join(verification.get("errors") or [])
418
- )
419
- result = migrator.migrate(dry_run=False)
420
- result["pre_migration_backup"] = {
421
- "path": backup["path"],
422
- "verified": True,
423
- "manifest": backup.get("manifest"),
424
- }
425
- return result
9
+ import lattice_brain.portability as _impl
426
10
 
427
- def recent_ingestions(self, *, limit: int = 50, source_type: Optional[str] = None) -> Dict[str, Any]:
428
- """Recent provenance records (newest first) for the ingestion-sources UI."""
429
- if not self.available():
430
- return {"items": [], "count": 0}
431
- return self._kg.list_provenance(limit=limit, source_type=source_type)
11
+ sys.modules[__name__] = _impl
@@ -17,8 +17,8 @@ from typing import Dict, List, Optional
17
17
 
18
18
  ENGINE_INSTALLERS = {
19
19
  "local_mlx": {
20
- "command": [sys.executable, "-m", "pip", "install", "--upgrade", "mlx-vlm", "huggingface_hub[cli]"],
21
- "label": "Install MLX-VLM runtime",
20
+ "command": [sys.executable, "-m", "pip", "install", "--upgrade", "mlx-vlm>=0.6.3", "mlx-lm", "huggingface_hub[cli]"],
21
+ "label": "Install MLX runtime",
22
22
  },
23
23
  "openai": {
24
24
  "command": [sys.executable, "-m", "pip", "install", "openai"],
@@ -18,6 +18,7 @@ from __future__ import annotations
18
18
  import re
19
19
  from typing import Any, Dict, List, Optional
20
20
 
21
+ from latticeai.core.model_compat import model_runtime_compatibility
21
22
  from latticeai.services.model_catalog import ENGINE_MODEL_CATALOG
22
23
 
23
24
  # ── status vocabulary ─────────────────────────────────────────────────────────
@@ -85,14 +86,19 @@ def _engine_available(engine: str, profile: Dict[str, Any]) -> bool:
85
86
  def _classify_one(
86
87
  model: Dict[str, Any],
87
88
  *,
89
+ engine: str,
88
90
  engine_available: bool,
89
91
  ram_gb: float,
90
92
  ) -> Dict[str, Any]:
91
93
  size_gb = parse_size_gb(model.get("size"))
92
94
  need_gb = estimated_ram_gb(size_gb) if size_gb is not None else None
95
+ runtime = model_runtime_compatibility(str(model.get("id") or ""), engine=engine)
93
96
 
94
97
  if not engine_available:
95
98
  status, reason = NOT_RECOMMENDED, "Apple Silicon과 MLX-VLM이 필요합니다"
99
+ elif runtime.get("supported") is False:
100
+ status = NOT_RECOMMENDED
101
+ reason = str(runtime.get("user_message") or "이 모델은 현재 설치된 실행 런타임에서 지원되지 않습니다")
96
102
  elif need_gb is None:
97
103
  # Tool-managed/pull models have no fixed on-disk size, so treat them as
98
104
  # compatible and let the execution tool validate the exact model.
@@ -124,6 +130,7 @@ def _classify_one(
124
130
  "run_location": model.get("run_location"),
125
131
  "internet_requirement": model.get("internet_requirement"),
126
132
  "source_display_order": model.get("source_display_order"),
133
+ "runtime_compatibility": runtime,
127
134
  }
128
135
 
129
136
 
@@ -148,7 +155,7 @@ def recommend_catalog(profile: Dict[str, Any], *, engine: str = "local_mlx") ->
148
155
  ram_gb = _ram_gb(profile)
149
156
 
150
157
  classified = [
151
- _classify_one(m, engine_available=engine_available, ram_gb=ram_gb)
158
+ _classify_one(m, engine=engine, engine_available=engine_available, ram_gb=ram_gb)
152
159
  for m in models
153
160
  ]
154
161
 
@@ -40,6 +40,8 @@ from latticeai.core.model_compat import (
40
40
  classify_smoke_response as _classify_smoke_response,
41
41
  ensure_profile as _ensure_compat_profile,
42
42
  fast_postprocess as _compat_fast_postprocess,
43
+ friendly_model_runtime_error as _friendly_model_runtime_error,
44
+ model_runtime_compatibility as _model_runtime_compatibility,
43
45
  record_smoke_result as _record_smoke_result,
44
46
  )
45
47
  from latticeai.core.model_resolution import ModelResolution as _ModelResolution
@@ -931,7 +933,10 @@ def ensure_llamacpp_server(model_name: str) -> None:
931
933
 
932
934
  def engine_installed(engine: str) -> bool:
933
935
  if engine == "local_mlx":
934
- return bool(importlib.util.find_spec("mlx") and importlib.util.find_spec("mlx_vlm"))
936
+ return bool(
937
+ importlib.util.find_spec("mlx")
938
+ and (importlib.util.find_spec("mlx_vlm") or importlib.util.find_spec("mlx_lm"))
939
+ )
935
940
  if engine == "ollama":
936
941
  return local_binary("ollama") is not None
937
942
  if engine == "vllm":
@@ -1042,7 +1047,7 @@ def engine_status() -> List[Dict]:
1042
1047
  "id": "local_mlx",
1043
1048
  "name": "MLX",
1044
1049
  "kind": "local",
1045
- "description": "Apple Silicon GPU에서 MLX/MLX-VLM 모델을 직접 실행합니다.",
1050
+ "description": "Apple Silicon GPU에서 MLX-VLM 모델을 직접 실행하고, Gemma 4는 필요 시 MLX-LM 텍스트 경로로 재시도합니다.",
1046
1051
  "installed": engine_installed("local_mlx"),
1047
1052
  "installable": True,
1048
1053
  "install_label": ENGINE_INSTALLERS["local_mlx"]["label"],
@@ -1369,6 +1374,9 @@ async def prepare_and_load_model(
1369
1374
  parsed_provider, parsed_model = parse_model_ref(model_id)
1370
1375
  if parsed_provider == "mlx":
1371
1376
  parsed_provider = "local_mlx"
1377
+ compatibility = _model_runtime_compatibility(parsed_model, engine=parsed_provider)
1378
+ if compatibility.get("supported") is False:
1379
+ raise HTTPException(status_code=400, detail=compatibility)
1372
1380
 
1373
1381
  local_engines = {"local_mlx", "ollama", "vllm", "lmstudio", "llamacpp"}
1374
1382
  install_result: Dict[str, object] = {}
@@ -1488,6 +1496,9 @@ async def prepare_and_load_model_stream(
1488
1496
  parsed_provider, parsed_model = parse_model_ref(model_id)
1489
1497
  if parsed_provider == "mlx":
1490
1498
  parsed_provider = "local_mlx"
1499
+ compatibility = _model_runtime_compatibility(parsed_model, engine=parsed_provider)
1500
+ if compatibility.get("supported") is False:
1501
+ raise HTTPException(status_code=400, detail=compatibility)
1491
1502
 
1492
1503
  work_queue: "queue.Queue[Dict[str, object]]" = queue.Queue()
1493
1504
  work_result: Dict[str, object] = {}
@@ -1651,7 +1662,11 @@ async def prepare_and_load_model_stream(
1651
1662
  work_queue.put({"kind": "error", "status_code": exc.status_code, "detail": exc.detail})
1652
1663
  except Exception as exc:
1653
1664
  logging.exception("model prepare stream worker failed")
1654
- work_queue.put({"kind": "error", "status_code": 500, "detail": str(exc)[-2000:]})
1665
+ work_queue.put({
1666
+ "kind": "error",
1667
+ "status_code": 500,
1668
+ "detail": _friendly_model_runtime_error(exc, model_id=model_id, engine=parsed_provider),
1669
+ })
1655
1670
 
1656
1671
  worker = threading.Thread(target=blocking_prepare, daemon=True)
1657
1672
  worker.start()
@@ -17,9 +17,9 @@ from typing import Any, Callable, Dict, Optional, Set
17
17
 
18
18
  from fastapi import HTTPException, Request
19
19
 
20
- from latticeai.core.hooks import dispatch_tool
21
- from latticeai.core.multi_agent import MultiAgentOrchestrator, default_role_runner, llm_role_runner
22
- from latticeai.core.workflow_engine import ApprovalRequired, WorkflowEngine
20
+ from lattice_brain.runtime.hooks import dispatch_tool
21
+ from lattice_brain.runtime.multi_agent import MultiAgentOrchestrator, default_role_runner, llm_role_runner
22
+ from lattice_brain.workflow import ApprovalRequired, WorkflowEngine
23
23
  from tools import execute_tool
24
24
 
25
25
 
@@ -13,7 +13,7 @@ from dataclasses import dataclass
13
13
  from datetime import datetime
14
14
  from typing import Any, Callable, Dict, Optional
15
15
 
16
- from latticeai.core.workflow_engine import WorkflowEngine
16
+ from lattice_brain.workflow import WorkflowEngine
17
17
 
18
18
 
19
19
  ACTIVE_STATUSES = {"queued", "running", "in_progress", "retrying", "cancelling"}
@@ -9,7 +9,7 @@ from pathlib import Path
9
9
 
10
10
  from fastapi import HTTPException, Request, UploadFile
11
11
 
12
- from latticeai.services.ingestion import IngestionItem
12
+ from lattice_brain.ingestion import IngestionItem
13
13
  from tools import ToolError, read_document
14
14
 
15
15
 
package/p_reinforce.py CHANGED
@@ -118,7 +118,7 @@ class PReinforceGardener:
118
118
  if self._pipeline is None:
119
119
  return {"graph": "unavailable", "graph_detail": "ingestion pipeline not wired"}
120
120
  try:
121
- from latticeai.services.ingestion import IngestionItem
121
+ from lattice_brain.ingestion import IngestionItem
122
122
 
123
123
  ingest = self._pipeline.ingest(
124
124
  IngestionItem(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ltcai",
3
- "version": "4.3.3",
3
+ "version": "4.5.1",
4
4
  "description": "Lattice AI — local-first Digital Brain Platform (knowledge graph, durable memory, hybrid search, agents, portable encrypted brain archives)",
5
5
  "homepage": "https://github.com/TaeSooPark-PTS/LatticeAI#readme",
6
6
  "repository": {
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
 
5
5
  const repo = join(import.meta.dirname, "..");
@@ -8,6 +8,17 @@ const nestedViteManifest = join(appDir, ".vite", "asset-manifest.json");
8
8
  const publicManifest = join(appDir, "asset-manifest.json");
9
9
  const pkg = JSON.parse(readFileSync(join(repo, "package.json"), "utf8"));
10
10
 
11
+ const assetsDir = join(appDir, "assets");
12
+ if (existsSync(assetsDir)) {
13
+ for (const name of readdirSync(assetsDir)) {
14
+ if (!/\.(?:css|js)$/.test(name)) continue;
15
+ const file = join(assetsDir, name);
16
+ const text = readFileSync(file, "utf8");
17
+ const normalized = text.replace(/[ \t]+$/gm, "");
18
+ if (normalized !== text) writeFileSync(file, normalized, "utf8");
19
+ }
20
+ }
21
+
11
22
  const viteManifest = existsSync(nestedViteManifest) ? nestedViteManifest : publicManifest;
12
23
  if (!existsSync(viteManifest)) {
13
24
  console.error("Vite manifest missing. Run `vite build` before build_frontend_assets.mjs.");
@@ -26,7 +26,7 @@ TARGETS = [
26
26
  ("lattice_brain/__init__.py", "regex", r'(__version__ = ")([^"]+)(")'),
27
27
  ("latticeai/core/workspace_os.py", "regex", r'(WORKSPACE_OS_VERSION = ")([^"]+)(")'),
28
28
  ("latticeai/core/marketplace.py", "regex", r'(MARKETPLACE_VERSION = ")([^"]+)(")'),
29
- ("latticeai/core/multi_agent.py", "regex", r'(MULTI_AGENT_VERSION = ")([^"]+)(")'),
29
+ ("lattice_brain/runtime/multi_agent.py", "regex", r'(MULTI_AGENT_VERSION = ")([^"]+)(")'),
30
30
  ("pyproject.toml", "regex", r'(^version = ")([^"]+)(")'),
31
31
  ("package.json", "json", "version"),
32
32
  ("package-lock.json", "package-lock", None),
@@ -34,8 +34,15 @@ REPO_ROOT = Path(__file__).resolve().parents[1]
34
34
  WHEEL_MODULES = [
35
35
  "setup_wizard",
36
36
  "lattice_brain",
37
+ "lattice_brain.graph",
38
+ "lattice_brain.graph.store",
39
+ "lattice_brain.runtime",
40
+ "lattice_brain.runtime.agent_runtime",
37
41
  "lattice_brain.storage",
38
42
  "lattice_brain.archive",
43
+ "lattice_brain.ingestion",
44
+ "lattice_brain.portability",
45
+ "lattice_brain.workflow",
39
46
  "latticeai",
40
47
  "latticeai.server_app",
41
48
  "latticeai.app_factory",