ltcai 4.0.1 → 4.2.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 (192) hide show
  1. package/README.md +33 -24
  2. package/desktop/electron/main.cjs +44 -0
  3. package/docs/CHANGELOG.md +84 -0
  4. package/docs/V4_1_FRONTEND_ARCHITECTURE_REVIEW.md +65 -0
  5. package/docs/V4_1_FRONTEND_MIGRATION_REPORT.md +70 -0
  6. package/docs/V4_1_VALIDATION_REPORT.md +47 -0
  7. package/docs/V4_2_BRAIN_CORE_ARCHITECTURE.md +97 -0
  8. package/docs/V4_2_STORAGE_MIGRATION_REPORT.md +91 -0
  9. package/docs/V4_2_VALIDATION_REPORT.md +89 -0
  10. package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +31 -26
  11. package/frontend/index.html +24 -0
  12. package/frontend/openapi.json +14436 -0
  13. package/frontend/src/App.tsx +184 -0
  14. package/frontend/src/api/client.ts +320 -0
  15. package/frontend/src/api/openapi.ts +16921 -0
  16. package/frontend/src/components/primitives.tsx +204 -0
  17. package/frontend/src/components/ui/badge.tsx +27 -0
  18. package/frontend/src/components/ui/button.tsx +37 -0
  19. package/frontend/src/components/ui/card.tsx +22 -0
  20. package/frontend/src/components/ui/input.tsx +16 -0
  21. package/frontend/src/components/ui/textarea.tsx +16 -0
  22. package/frontend/src/lib/utils.ts +33 -0
  23. package/frontend/src/main.tsx +23 -0
  24. package/frontend/src/pages/Act.tsx +245 -0
  25. package/frontend/src/pages/Ask.tsx +200 -0
  26. package/frontend/src/pages/Brain.tsx +267 -0
  27. package/frontend/src/pages/Capture.tsx +158 -0
  28. package/frontend/src/pages/Library.tsx +187 -0
  29. package/frontend/src/pages/System.tsx +378 -0
  30. package/frontend/src/routes.ts +85 -0
  31. package/frontend/src/store/appStore.ts +54 -0
  32. package/frontend/src/styles.css +107 -0
  33. package/kg_schema.py +1 -1
  34. package/knowledge_graph.py +4 -4
  35. package/lattice_brain/__init__.py +70 -0
  36. package/lattice_brain/_kg_common.py +1 -0
  37. package/lattice_brain/archive.py +133 -0
  38. package/lattice_brain/context.py +3 -0
  39. package/lattice_brain/conversations.py +3 -0
  40. package/lattice_brain/core.py +82 -0
  41. package/lattice_brain/discovery.py +1 -0
  42. package/lattice_brain/documents.py +1 -0
  43. package/lattice_brain/embeddings.py +82 -0
  44. package/lattice_brain/identity.py +13 -0
  45. package/lattice_brain/ingest.py +1 -0
  46. package/lattice_brain/memory.py +3 -0
  47. package/lattice_brain/network.py +1 -0
  48. package/lattice_brain/projection.py +1 -0
  49. package/lattice_brain/provenance.py +1 -0
  50. package/lattice_brain/retrieval.py +1 -0
  51. package/lattice_brain/schema.py +1 -0
  52. package/lattice_brain/storage/__init__.py +22 -0
  53. package/lattice_brain/storage/base.py +72 -0
  54. package/lattice_brain/storage/docker.py +105 -0
  55. package/lattice_brain/storage/factory.py +31 -0
  56. package/lattice_brain/storage/migration.py +190 -0
  57. package/lattice_brain/storage/postgres.py +123 -0
  58. package/lattice_brain/storage/sqlite.py +128 -0
  59. package/lattice_brain/store.py +3 -0
  60. package/lattice_brain/write_master.py +1 -0
  61. package/latticeai/__init__.py +1 -1
  62. package/latticeai/api/portability.py +69 -0
  63. package/latticeai/api/setup.py +5 -4
  64. package/latticeai/api/static_routes.py +4 -4
  65. package/latticeai/app_factory.py +17 -10
  66. package/latticeai/brain/__init__.py +6 -6
  67. package/latticeai/brain/_kg_common.py +1 -1
  68. package/latticeai/brain/network.py +1 -1
  69. package/latticeai/brain/retrieval.py +15 -0
  70. package/latticeai/brain/store.py +22 -6
  71. package/latticeai/core/config.py +8 -0
  72. package/latticeai/core/marketplace.py +1 -1
  73. package/latticeai/core/multi_agent.py +1 -1
  74. package/latticeai/core/workspace_os.py +1 -1
  75. package/latticeai/services/kg_portability.py +82 -1
  76. package/package.json +55 -15
  77. package/scripts/build_frontend_assets.mjs +38 -0
  78. package/scripts/bump_version.py +4 -1
  79. package/scripts/export_openapi.py +31 -0
  80. package/scripts/lint_frontend.mjs +91 -0
  81. package/scripts/migrate_brain_storage.py +53 -0
  82. package/scripts/run_python.mjs +47 -0
  83. package/scripts/wheel_smoke.py +3 -0
  84. package/src-tauri/Cargo.lock +4833 -0
  85. package/src-tauri/Cargo.toml +19 -0
  86. package/src-tauri/build.rs +3 -0
  87. package/src-tauri/capabilities/default.json +7 -0
  88. package/src-tauri/src/main.rs +78 -0
  89. package/src-tauri/tauri.conf.json +39 -0
  90. package/static/app/asset-manifest.json +32 -0
  91. package/static/app/assets/core-CwxXejkd.js +2 -0
  92. package/static/app/assets/core-CwxXejkd.js.map +1 -0
  93. package/static/app/assets/index-CDjiH_se.css +2 -0
  94. package/static/app/assets/index-C_HAkbAg.js +333 -0
  95. package/static/app/assets/index-C_HAkbAg.js.map +1 -0
  96. package/static/app/index.html +25 -0
  97. package/static/manifest.json +2 -2
  98. package/static/sw.js +4 -4
  99. package/scripts/build_v3_assets.mjs +0 -170
  100. package/scripts/lint_v3.mjs +0 -120
  101. package/static/v3/asset-manifest.json +0 -63
  102. package/static/v3/css/lattice.base.49deefb5.css +0 -128
  103. package/static/v3/css/lattice.base.css +0 -128
  104. package/static/v3/css/lattice.components.cde18231.css +0 -472
  105. package/static/v3/css/lattice.components.css +0 -472
  106. package/static/v3/css/lattice.shell.29d36d85.css +0 -452
  107. package/static/v3/css/lattice.shell.css +0 -452
  108. package/static/v3/css/lattice.tokens.304cbc40.css +0 -135
  109. package/static/v3/css/lattice.tokens.css +0 -135
  110. package/static/v3/css/lattice.views.0a18b6c5.css +0 -360
  111. package/static/v3/css/lattice.views.css +0 -360
  112. package/static/v3/index.html +0 -68
  113. package/static/v3/js/app.c5c80c46.js +0 -26
  114. package/static/v3/js/app.js +0 -26
  115. package/static/v3/js/core/api.ba0fbf14.js +0 -625
  116. package/static/v3/js/core/api.js +0 -625
  117. package/static/v3/js/core/components.f25b3b93.js +0 -230
  118. package/static/v3/js/core/components.js +0 -230
  119. package/static/v3/js/core/dom.a2773eb0.js +0 -148
  120. package/static/v3/js/core/dom.js +0 -148
  121. package/static/v3/js/core/i18n.880e1fec.js +0 -575
  122. package/static/v3/js/core/i18n.js +0 -575
  123. package/static/v3/js/core/router.584570f2.js +0 -37
  124. package/static/v3/js/core/router.js +0 -37
  125. package/static/v3/js/core/routes.37522821.js +0 -101
  126. package/static/v3/js/core/routes.js +0 -101
  127. package/static/v3/js/core/shell.e3f6bbfa.js +0 -420
  128. package/static/v3/js/core/shell.js +0 -420
  129. package/static/v3/js/core/store.7b2aa044.js +0 -123
  130. package/static/v3/js/core/store.js +0 -123
  131. package/static/v3/js/views/account.eff40715.js +0 -143
  132. package/static/v3/js/views/account.js +0 -143
  133. package/static/v3/js/views/activity.0d271ef9.js +0 -67
  134. package/static/v3/js/views/activity.js +0 -67
  135. package/static/v3/js/views/admin-audit.660a1fb1.js +0 -185
  136. package/static/v3/js/views/admin-audit.js +0 -185
  137. package/static/v3/js/views/admin-permissions.a7ae5f09.js +0 -177
  138. package/static/v3/js/views/admin-permissions.js +0 -177
  139. package/static/v3/js/views/admin-policies.3658fd86.js +0 -102
  140. package/static/v3/js/views/admin-policies.js +0 -102
  141. package/static/v3/js/views/admin-private-vpc.7d342d36.js +0 -135
  142. package/static/v3/js/views/admin-private-vpc.js +0 -135
  143. package/static/v3/js/views/admin-security.07c66b72.js +0 -180
  144. package/static/v3/js/views/admin-security.js +0 -180
  145. package/static/v3/js/views/admin-users.f7ac7b43.js +0 -166
  146. package/static/v3/js/views/admin-users.js +0 -166
  147. package/static/v3/js/views/agents.17c5288d.js +0 -564
  148. package/static/v3/js/views/agents.js +0 -564
  149. package/static/v3/js/views/chat.e250e2cc.js +0 -624
  150. package/static/v3/js/views/chat.js +0 -624
  151. package/static/v3/js/views/files.adad14c1.js +0 -365
  152. package/static/v3/js/views/files.js +0 -365
  153. package/static/v3/js/views/graph-canvas.17c15d65.js +0 -509
  154. package/static/v3/js/views/graph-canvas.js +0 -509
  155. package/static/v3/js/views/home.24f8b8ae.js +0 -200
  156. package/static/v3/js/views/home.js +0 -200
  157. package/static/v3/js/views/hooks.37895880.js +0 -220
  158. package/static/v3/js/views/hooks.js +0 -220
  159. package/static/v3/js/views/hybrid-search.2fb63ed9.js +0 -194
  160. package/static/v3/js/views/hybrid-search.js +0 -194
  161. package/static/v3/js/views/knowledge-graph.4d09c537.js +0 -529
  162. package/static/v3/js/views/knowledge-graph.js +0 -529
  163. package/static/v3/js/views/marketplace.ab0583d4.js +0 -141
  164. package/static/v3/js/views/marketplace.js +0 -141
  165. package/static/v3/js/views/mcp.99b5c6a7.js +0 -114
  166. package/static/v3/js/views/mcp.js +0 -114
  167. package/static/v3/js/views/memory.4ebdf474.js +0 -147
  168. package/static/v3/js/views/memory.js +0 -147
  169. package/static/v3/js/views/models.a1ffa147.js +0 -256
  170. package/static/v3/js/views/models.js +0 -256
  171. package/static/v3/js/views/my-computer.d9d9ae1c.js +0 -463
  172. package/static/v3/js/views/my-computer.js +0 -463
  173. package/static/v3/js/views/network.52a4f181.js +0 -97
  174. package/static/v3/js/views/network.js +0 -97
  175. package/static/v3/js/views/pipeline.c522f1ce.js +0 -157
  176. package/static/v3/js/views/pipeline.js +0 -157
  177. package/static/v3/js/views/planning.4876fd77.js +0 -174
  178. package/static/v3/js/views/planning.js +0 -174
  179. package/static/v3/js/views/runs.b63b2afa.js +0 -144
  180. package/static/v3/js/views/runs.js +0 -144
  181. package/static/v3/js/views/settings.b7140634.js +0 -317
  182. package/static/v3/js/views/settings.js +0 -317
  183. package/static/v3/js/views/skills.c6c2f965.js +0 -109
  184. package/static/v3/js/views/skills.js +0 -109
  185. package/static/v3/js/views/snapshots.6f5db095.js +0 -135
  186. package/static/v3/js/views/snapshots.js +0 -135
  187. package/static/v3/js/views/tools.e4f11276.js +0 -108
  188. package/static/v3/js/views/tools.js +0 -108
  189. package/static/v3/js/views/workflows.7752225a.js +0 -213
  190. package/static/v3/js/views/workflows.js +0 -213
  191. package/static/v3/js/views/workspace-admin.c466029b.js +0 -156
  192. package/static/v3/js/views/workspace-admin.js +0 -156
@@ -0,0 +1,72 @@
1
+ """StorageEngine contracts for the independent Brain Core package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from dataclasses import dataclass, field
7
+ from pathlib import Path
8
+ from typing import Any, Dict, Optional
9
+
10
+
11
+ class StorageUnavailable(RuntimeError):
12
+ """Raised when an explicitly requested storage engine cannot be used."""
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class StorageCapabilities:
17
+ engine: str
18
+ available: bool
19
+ reason: Optional[str] = None
20
+ vector_backend: str = "none"
21
+ vector_available: bool = False
22
+ backup_restore: bool = False
23
+ migrations: bool = False
24
+ encrypted_archives: bool = False
25
+ metadata: Dict[str, Any] = field(default_factory=dict)
26
+
27
+ def as_dict(self) -> Dict[str, Any]:
28
+ return {
29
+ "engine": self.engine,
30
+ "available": self.available,
31
+ "reason": self.reason,
32
+ "vector_backend": self.vector_backend,
33
+ "vector_available": self.vector_available,
34
+ "backup_restore": self.backup_restore,
35
+ "migrations": self.migrations,
36
+ "encrypted_archives": self.encrypted_archives,
37
+ "metadata": self.metadata,
38
+ }
39
+
40
+
41
+ class StorageEngine(ABC):
42
+ """Unified storage interface used by Brain Core.
43
+
44
+ The knowledge graph currently uses SQL directly, so ``connect`` is part of
45
+ the contract. Engines must fail loudly when unavailable; callers must not
46
+ silently fall back to SQLite after an explicit Postgres selection.
47
+ """
48
+
49
+ name: str
50
+
51
+ @abstractmethod
52
+ def capabilities(self) -> StorageCapabilities:
53
+ """Return an honest capability report."""
54
+
55
+ @abstractmethod
56
+ def initialize(self) -> Dict[str, Any]:
57
+ """Create required storage structures or raise ``StorageUnavailable``."""
58
+
59
+ @abstractmethod
60
+ def connect(self) -> Any:
61
+ """Return a DB-API-like connection for this engine."""
62
+
63
+ @abstractmethod
64
+ def backup(self, destination: Path) -> Dict[str, Any]:
65
+ """Create a faithful engine backup at ``destination``."""
66
+
67
+ @abstractmethod
68
+ def restore(self, source: Path) -> Dict[str, Any]:
69
+ """Restore a faithful engine backup from ``source``."""
70
+
71
+
72
+ __all__ = ["StorageCapabilities", "StorageEngine", "StorageUnavailable"]
@@ -0,0 +1,105 @@
1
+ """Explicit-consent Docker setup wizard for Postgres/pgvector."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import Dict, List
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class DockerPostgresPlan:
13
+ compose_path: Path
14
+ project_name: str
15
+ service_name: str = "postgres"
16
+ port: int = 5432
17
+
18
+ def command(self) -> List[str]:
19
+ return [
20
+ "docker",
21
+ "compose",
22
+ "-p",
23
+ self.project_name,
24
+ "-f",
25
+ str(self.compose_path),
26
+ "up",
27
+ "-d",
28
+ self.service_name,
29
+ ]
30
+
31
+
32
+ class DockerPostgresWizard:
33
+ """Creates and starts a local Postgres container only after consent."""
34
+
35
+ def __init__(self, data_dir: Path, *, port: int = 5432) -> None:
36
+ self.data_dir = Path(data_dir)
37
+ self.port = int(port)
38
+
39
+ def write_compose(self) -> DockerPostgresPlan:
40
+ self.data_dir.mkdir(parents=True, exist_ok=True)
41
+ compose = self.data_dir / "postgres.compose.yml"
42
+ compose.write_text(
43
+ f"""services:
44
+ postgres:
45
+ image: pgvector/pgvector:pg16
46
+ restart: unless-stopped
47
+ environment:
48
+ POSTGRES_DB: lattice_brain
49
+ POSTGRES_USER: lattice
50
+ POSTGRES_PASSWORD: lattice-local-only
51
+ ports:
52
+ - "127.0.0.1:{self.port}:5432"
53
+ volumes:
54
+ - ./postgres-data:/var/lib/postgresql/data
55
+ """,
56
+ encoding="utf-8",
57
+ )
58
+ return DockerPostgresPlan(
59
+ compose_path=compose,
60
+ project_name="lattice-brain",
61
+ port=self.port,
62
+ )
63
+
64
+ def start(
65
+ self,
66
+ *,
67
+ consent: bool,
68
+ dry_run: bool = False,
69
+ runner=subprocess.run,
70
+ ) -> Dict[str, object]:
71
+ plan = self.write_compose()
72
+ if not consent:
73
+ return {
74
+ "status": "consent_required",
75
+ "started": False,
76
+ "compose_path": str(plan.compose_path),
77
+ "command": plan.command(),
78
+ }
79
+ if dry_run:
80
+ return {
81
+ "status": "dry_run",
82
+ "started": False,
83
+ "compose_path": str(plan.compose_path),
84
+ "command": plan.command(),
85
+ }
86
+ completed = runner(plan.command(), check=False, capture_output=True, text=True)
87
+ if completed.returncode != 0:
88
+ return {
89
+ "status": "failed",
90
+ "started": False,
91
+ "compose_path": str(plan.compose_path),
92
+ "returncode": completed.returncode,
93
+ "stdout": completed.stdout,
94
+ "stderr": completed.stderr,
95
+ }
96
+ return {
97
+ "status": "started",
98
+ "started": True,
99
+ "compose_path": str(plan.compose_path),
100
+ "stdout": completed.stdout,
101
+ "stderr": completed.stderr,
102
+ }
103
+
104
+
105
+ __all__ = ["DockerPostgresPlan", "DockerPostgresWizard"]
@@ -0,0 +1,31 @@
1
+ """StorageEngine construction from environment/config values."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Mapping
7
+
8
+ from .base import StorageEngine, StorageUnavailable
9
+ from .postgres import PostgresEngine
10
+ from .sqlite import SQLiteEngine
11
+
12
+
13
+ def storage_from_env(env: Mapping[str, str], *, data_dir: Path) -> StorageEngine:
14
+ engine = (env.get("LATTICEAI_STORAGE_ENGINE") or "sqlite").strip().lower()
15
+ if engine in {"", "sqlite"}:
16
+ return SQLiteEngine(Path(data_dir) / "knowledge_graph.sqlite")
17
+ if engine in {"postgres", "pg", "pgvector"}:
18
+ dsn = env.get("LATTICEAI_POSTGRES_DSN") or ""
19
+ if not dsn:
20
+ raise StorageUnavailable(
21
+ "LATTICEAI_STORAGE_ENGINE=postgres requires LATTICEAI_POSTGRES_DSN; "
22
+ "SQLite fallback is disabled for explicit Postgres selection."
23
+ )
24
+ return PostgresEngine(
25
+ dsn,
26
+ schema=env.get("LATTICEAI_POSTGRES_SCHEMA") or "lattice_brain",
27
+ )
28
+ raise StorageUnavailable(f"Unknown Brain Core storage engine: {engine}")
29
+
30
+
31
+ __all__ = ["storage_from_env"]
@@ -0,0 +1,190 @@
1
+ """Safe SQLite to Postgres migration tooling."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sqlite3
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import Any, Dict, List
9
+
10
+ from .postgres import PostgresEngine, _quote_ident
11
+
12
+
13
+ def _pg_type(sqlite_type: str) -> str:
14
+ t = str(sqlite_type or "").upper()
15
+ if "INT" in t:
16
+ return "bigint"
17
+ if any(token in t for token in ("REAL", "FLOA", "DOUB")):
18
+ return "double precision"
19
+ if "BLOB" in t:
20
+ return "bytea"
21
+ return "text"
22
+
23
+
24
+ def _adapt_value(value: Any) -> Any:
25
+ if isinstance(value, memoryview):
26
+ return bytes(value)
27
+ return value
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class TablePlan:
32
+ name: str
33
+ columns: List[Dict[str, str]]
34
+ rows: int
35
+ conflict_key: str
36
+ conflict_columns: List[str]
37
+ rowid_available: bool
38
+
39
+
40
+ class SQLiteToPostgresMigrator:
41
+ """Copies every user table from a Lattice SQLite brain into Postgres.
42
+
43
+ The migration is idempotent: tables with an ``id`` column upsert on ``id``;
44
+ tables without one use the preserved SQLite rowid in ``__source_rowid``.
45
+ SQLite remains untouched throughout.
46
+ """
47
+
48
+ def __init__(self, sqlite_path: Path, target: PostgresEngine) -> None:
49
+ self.sqlite_path = Path(sqlite_path)
50
+ self.target = target
51
+
52
+ def plan(self) -> Dict[str, Any]:
53
+ if not self.sqlite_path.exists():
54
+ raise FileNotFoundError(f"SQLite brain database not found: {self.sqlite_path}")
55
+ with sqlite3.connect(str(self.sqlite_path)) as conn:
56
+ conn.row_factory = sqlite3.Row
57
+ table_names = [
58
+ row["name"]
59
+ for row in conn.execute(
60
+ """
61
+ SELECT name FROM sqlite_master
62
+ WHERE type='table' AND name NOT LIKE 'sqlite_%'
63
+ ORDER BY name
64
+ """
65
+ )
66
+ ]
67
+ tables = []
68
+ for table in table_names:
69
+ cols = [
70
+ {"name": row["name"], "type": row["type"] or "TEXT"}
71
+ for row in conn.execute(f"PRAGMA table_info({_quote_sqlite_ident(table)})")
72
+ ]
73
+ row_count = conn.execute(f"SELECT COUNT(*) FROM {_quote_sqlite_ident(table)}").fetchone()[0]
74
+ names = {c["name"] for c in cols}
75
+ rowid_available = _rowid_available(conn, table)
76
+ pk_columns = [
77
+ row["name"]
78
+ for row in sorted(
79
+ conn.execute(f"PRAGMA table_info({_quote_sqlite_ident(table)})"),
80
+ key=lambda item: int(item["pk"] or 0),
81
+ )
82
+ if int(row["pk"] or 0) > 0
83
+ ]
84
+ conflict_columns = (
85
+ ["id"]
86
+ if "id" in names
87
+ else pk_columns
88
+ if pk_columns
89
+ else ["__source_rowid"]
90
+ if rowid_available
91
+ else []
92
+ )
93
+ if not conflict_columns:
94
+ raise RuntimeError(
95
+ f"Cannot safely migrate rowid-less SQLite table without a primary key: {table}"
96
+ )
97
+ tables.append(
98
+ TablePlan(
99
+ name=table,
100
+ columns=cols,
101
+ rows=int(row_count),
102
+ conflict_key=conflict_columns[0],
103
+ conflict_columns=conflict_columns,
104
+ rowid_available=rowid_available,
105
+ )
106
+ )
107
+ return {
108
+ "source": str(self.sqlite_path),
109
+ "target_engine": self.target.name,
110
+ "target_schema": self.target.config.schema,
111
+ "tables": [table.__dict__ for table in tables],
112
+ "total_rows": sum(table.rows for table in tables),
113
+ }
114
+
115
+ def migrate(self, *, dry_run: bool = False) -> Dict[str, Any]:
116
+ plan = self.plan()
117
+ if dry_run:
118
+ return {"status": "planned", **plan}
119
+ schema = _quote_ident(self.target.config.schema)
120
+ copied: Dict[str, int] = {}
121
+ self.target.initialize()
122
+ with sqlite3.connect(str(self.sqlite_path)) as src, self.target.connect() as dst:
123
+ src.row_factory = sqlite3.Row
124
+ with dst.cursor() as cur:
125
+ for table in plan["tables"]:
126
+ name = str(table["name"])
127
+ cols = list(table["columns"])
128
+ conflict_columns = list(table.get("conflict_columns") or [table["conflict_key"]])
129
+ rowid_available = bool(table.get("rowid_available", True))
130
+ pg_table = f"{schema}.{_quote_ident(name)}"
131
+ defs = ["__source_rowid bigint NOT NULL"] if rowid_available else []
132
+ for col in cols:
133
+ defs.append(f"{_quote_ident(col['name'])} {_pg_type(col['type'])}")
134
+ pk = ", ".join(_quote_ident(c) for c in conflict_columns)
135
+ cur.execute(
136
+ f"CREATE TABLE IF NOT EXISTS {pg_table} ({', '.join(defs)}, PRIMARY KEY ({pk}))"
137
+ )
138
+ if rowid_available:
139
+ select_sql = (
140
+ f"SELECT rowid AS __source_rowid, * FROM {_quote_sqlite_ident(name)} ORDER BY rowid"
141
+ )
142
+ else:
143
+ order_by = ", ".join(_quote_sqlite_ident(c) for c in conflict_columns)
144
+ select_sql = f"SELECT * FROM {_quote_sqlite_ident(name)} ORDER BY {order_by}"
145
+ rows = src.execute(select_sql).fetchall()
146
+ if not rows:
147
+ copied[name] = 0
148
+ continue
149
+ columns = (["__source_rowid"] if rowid_available else []) + [c["name"] for c in cols]
150
+ placeholders = ", ".join(["%s"] * len(columns))
151
+ quoted_columns = ", ".join(_quote_ident(c) for c in columns)
152
+ updates = ", ".join(
153
+ f"{_quote_ident(c)} = EXCLUDED.{_quote_ident(c)}"
154
+ for c in columns
155
+ if c not in conflict_columns
156
+ )
157
+ conflict_action = f"DO UPDATE SET {updates}" if updates else "DO NOTHING"
158
+ sql = (
159
+ f"INSERT INTO {pg_table} ({quoted_columns}) VALUES ({placeholders}) "
160
+ f"ON CONFLICT ({pk}) {conflict_action}"
161
+ )
162
+ cur.executemany(
163
+ sql,
164
+ [
165
+ tuple(_adapt_value(row[col]) for col in columns)
166
+ for row in rows
167
+ ],
168
+ )
169
+ copied[name] = len(rows)
170
+ return {
171
+ "status": "migrated",
172
+ **plan,
173
+ "copied_rows": copied,
174
+ "total_copied_rows": sum(copied.values()),
175
+ }
176
+
177
+
178
+ def _quote_sqlite_ident(value: str) -> str:
179
+ return '"' + str(value).replace('"', '""') + '"'
180
+
181
+
182
+ def _rowid_available(conn: sqlite3.Connection, table: str) -> bool:
183
+ try:
184
+ conn.execute(f"SELECT rowid FROM {_quote_sqlite_ident(table)} LIMIT 1").fetchall()
185
+ return True
186
+ except sqlite3.OperationalError:
187
+ return False
188
+
189
+
190
+ __all__ = ["SQLiteToPostgresMigrator", "TablePlan"]
@@ -0,0 +1,123 @@
1
+ """Opt-in Postgres/pgvector storage engine for Brain Core scale mode."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Any, Dict
8
+
9
+ from .base import StorageCapabilities, StorageEngine, StorageUnavailable
10
+
11
+
12
+ def _quote_ident(value: str) -> str:
13
+ return '"' + str(value).replace('"', '""') + '"'
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class PostgresConfig:
18
+ dsn: str
19
+ schema: str = "lattice_brain"
20
+
21
+
22
+ class PostgresEngine(StorageEngine):
23
+ name = "postgres"
24
+
25
+ def __init__(self, dsn: str, *, schema: str = "lattice_brain") -> None:
26
+ self.config = PostgresConfig(dsn=dsn, schema=schema)
27
+
28
+ def _psycopg(self):
29
+ try:
30
+ import psycopg # type: ignore
31
+ except Exception as exc:
32
+ raise StorageUnavailable(
33
+ "Postgres storage requires optional dependency 'psycopg'. "
34
+ "Install the postgres extra before selecting LATTICEAI_STORAGE_ENGINE=postgres."
35
+ ) from exc
36
+ return psycopg
37
+
38
+ def connect(self) -> Any:
39
+ if not self.config.dsn:
40
+ raise StorageUnavailable(
41
+ "Postgres storage requires LATTICEAI_POSTGRES_DSN; no SQLite fallback is attempted."
42
+ )
43
+ psycopg = self._psycopg()
44
+ return psycopg.connect(self.config.dsn)
45
+
46
+ def initialize(self) -> Dict[str, Any]:
47
+ schema = _quote_ident(self.config.schema)
48
+ with self.connect() as conn:
49
+ with conn.cursor() as cur:
50
+ cur.execute("CREATE EXTENSION IF NOT EXISTS vector")
51
+ cur.execute(f"CREATE SCHEMA IF NOT EXISTS {schema}")
52
+ cur.execute(
53
+ f"""
54
+ CREATE TABLE IF NOT EXISTS {schema}.storage_meta (
55
+ key text PRIMARY KEY,
56
+ value text NOT NULL,
57
+ updated_at timestamptz NOT NULL DEFAULT now()
58
+ )
59
+ """
60
+ )
61
+ cur.execute(
62
+ f"""
63
+ CREATE TABLE IF NOT EXISTS {schema}.brain_vectors (
64
+ item_id text PRIMARY KEY,
65
+ item_type text NOT NULL,
66
+ source_node text NOT NULL,
67
+ text_hash text NOT NULL,
68
+ embedding vector,
69
+ embedding_dim integer NOT NULL,
70
+ embedding_model text NOT NULL,
71
+ metadata_json jsonb NOT NULL DEFAULT '{{}}'::jsonb,
72
+ indexed_at timestamptz NOT NULL DEFAULT now()
73
+ )
74
+ """
75
+ )
76
+ cur.execute(
77
+ f"""
78
+ INSERT INTO {schema}.storage_meta(key, value)
79
+ VALUES ('engine', 'postgres')
80
+ ON CONFLICT (key) DO UPDATE
81
+ SET value = EXCLUDED.value, updated_at = now()
82
+ """
83
+ )
84
+ return {"engine": self.name, "schema": self.config.schema}
85
+
86
+ def capabilities(self) -> StorageCapabilities:
87
+ try:
88
+ with self.connect() as conn:
89
+ with conn.cursor() as cur:
90
+ cur.execute("SELECT extname FROM pg_extension WHERE extname='vector'")
91
+ pgvector = cur.fetchone() is not None
92
+ except Exception as exc:
93
+ return StorageCapabilities(
94
+ engine=self.name,
95
+ available=False,
96
+ reason=str(exc),
97
+ vector_backend="pgvector",
98
+ metadata={"schema": self.config.schema},
99
+ )
100
+ return StorageCapabilities(
101
+ engine=self.name,
102
+ available=True,
103
+ reason=None if pgvector else "pgvector extension is not installed",
104
+ vector_backend="pgvector",
105
+ vector_available=pgvector,
106
+ backup_restore=False,
107
+ migrations=True,
108
+ encrypted_archives=False,
109
+ metadata={"schema": self.config.schema},
110
+ )
111
+
112
+ def backup(self, destination: Path) -> Dict[str, Any]:
113
+ raise StorageUnavailable(
114
+ "Postgres logical backup is not implemented inside the app; use pg_dump for this engine."
115
+ )
116
+
117
+ def restore(self, source: Path) -> Dict[str, Any]:
118
+ raise StorageUnavailable(
119
+ "Postgres restore is not implemented inside the app; use pg_restore/psql for this engine."
120
+ )
121
+
122
+
123
+ __all__ = ["PostgresConfig", "PostgresEngine", "_quote_ident"]
@@ -0,0 +1,128 @@
1
+ """SQLite storage engine for Brain Core.
2
+
3
+ SQLite is the default and remains fully local-first. sqlite-vec is detected and
4
+ loaded when present; otherwise the existing brute-force cosine path remains the
5
+ honest, real fallback and is surfaced in capability reports.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import shutil
11
+ import sqlite3
12
+ from pathlib import Path
13
+ from typing import Any, Dict, Optional
14
+
15
+ from .base import StorageCapabilities, StorageEngine
16
+
17
+
18
+ def _load_sqlite_vec(conn: sqlite3.Connection) -> tuple[bool, Optional[str]]:
19
+ try:
20
+ import sqlite_vec # type: ignore
21
+ except Exception as exc:
22
+ return False, f"sqlite-vec Python package not installed: {exc}"
23
+ try:
24
+ conn.enable_load_extension(True)
25
+ except Exception:
26
+ pass
27
+ try:
28
+ sqlite_vec.load(conn)
29
+ except Exception as exc:
30
+ return False, f"sqlite-vec extension failed to load: {exc}"
31
+ return True, None
32
+
33
+
34
+ class SQLiteEngine(StorageEngine):
35
+ name = "sqlite"
36
+
37
+ def __init__(self, db_path: Path, *, load_vec: bool = True) -> None:
38
+ self.db_path = Path(db_path)
39
+ self.load_vec = bool(load_vec)
40
+ self._sqlite_vec_loaded = False
41
+ self._sqlite_vec_reason: Optional[str] = None
42
+
43
+ def connect(self) -> sqlite3.Connection:
44
+ self.db_path.parent.mkdir(parents=True, exist_ok=True)
45
+ conn = sqlite3.connect(str(self.db_path))
46
+ conn.row_factory = sqlite3.Row
47
+ conn.execute("PRAGMA journal_mode=WAL")
48
+ conn.execute("PRAGMA foreign_keys=ON")
49
+ if self.load_vec:
50
+ loaded, reason = _load_sqlite_vec(conn)
51
+ self._sqlite_vec_loaded = loaded
52
+ self._sqlite_vec_reason = reason
53
+ return conn
54
+
55
+ def initialize(self) -> Dict[str, Any]:
56
+ with self.connect() as conn:
57
+ conn.execute(
58
+ """
59
+ CREATE TABLE IF NOT EXISTS storage_meta (
60
+ key TEXT PRIMARY KEY,
61
+ value TEXT NOT NULL
62
+ )
63
+ """
64
+ )
65
+ conn.execute(
66
+ "INSERT OR REPLACE INTO storage_meta(key, value) VALUES ('engine', 'sqlite')"
67
+ )
68
+ return {"engine": self.name, "db_path": str(self.db_path)}
69
+
70
+ def capabilities(self) -> StorageCapabilities:
71
+ if not self.db_path.parent.exists():
72
+ return StorageCapabilities(
73
+ engine=self.name,
74
+ available=True,
75
+ vector_backend="bruteforce-cosine",
76
+ vector_available=True,
77
+ backup_restore=True,
78
+ migrations=True,
79
+ encrypted_archives=True,
80
+ metadata={"db_path": str(self.db_path), "sqlite_vec_loaded": False},
81
+ )
82
+ # Probe on demand so status is accurate even before the graph opens.
83
+ try:
84
+ with self.connect():
85
+ pass
86
+ except Exception as exc:
87
+ return StorageCapabilities(
88
+ engine=self.name,
89
+ available=False,
90
+ reason=str(exc),
91
+ metadata={"db_path": str(self.db_path)},
92
+ )
93
+ vector_backend = "sqlite-vec" if self._sqlite_vec_loaded else "bruteforce-cosine"
94
+ return StorageCapabilities(
95
+ engine=self.name,
96
+ available=True,
97
+ reason=None if self._sqlite_vec_loaded else self._sqlite_vec_reason,
98
+ vector_backend=vector_backend,
99
+ vector_available=True,
100
+ backup_restore=True,
101
+ migrations=True,
102
+ encrypted_archives=True,
103
+ metadata={
104
+ "db_path": str(self.db_path),
105
+ "sqlite_vec_loaded": self._sqlite_vec_loaded,
106
+ },
107
+ )
108
+
109
+ def backup(self, destination: Path) -> Dict[str, Any]:
110
+ dest = Path(destination)
111
+ dest.parent.mkdir(parents=True, exist_ok=True)
112
+ with self.connect() as src, sqlite3.connect(str(dest)) as dst:
113
+ src.backup(dst)
114
+ return {"engine": self.name, "path": str(dest), "bytes": dest.stat().st_size}
115
+
116
+ def restore(self, source: Path) -> Dict[str, Any]:
117
+ src = Path(source)
118
+ if not src.exists():
119
+ raise FileNotFoundError(f"SQLite backup not found: {src}")
120
+ self.db_path.parent.mkdir(parents=True, exist_ok=True)
121
+ for sibling in (self.db_path, Path(str(self.db_path) + "-wal"), Path(str(self.db_path) + "-shm")):
122
+ if sibling.exists():
123
+ sibling.unlink()
124
+ shutil.copyfile(src, self.db_path)
125
+ return {"engine": self.name, "restored": True, "path": str(self.db_path)}
126
+
127
+
128
+ __all__ = ["SQLiteEngine"]
@@ -0,0 +1,3 @@
1
+ from latticeai.brain.store import KnowledgeGraphStore
2
+
3
+ __all__ = ["KnowledgeGraphStore"]
@@ -0,0 +1 @@
1
+ from latticeai.brain.write_master import * # noqa: F401,F403
@@ -1,3 +1,3 @@
1
1
  """Lattice AI - modular server package."""
2
2
 
3
- __version__ = "4.0.1"
3
+ __version__ = "4.2.0"