ltcai 4.1.0 → 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 (66) hide show
  1. package/README.md +28 -24
  2. package/docs/CHANGELOG.md +42 -0
  3. package/docs/V4_2_BRAIN_CORE_ARCHITECTURE.md +97 -0
  4. package/docs/V4_2_STORAGE_MIGRATION_REPORT.md +91 -0
  5. package/docs/V4_2_VALIDATION_REPORT.md +89 -0
  6. package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +31 -33
  7. package/frontend/openapi.json +247 -1
  8. package/frontend/src/api/client.ts +3 -0
  9. package/frontend/src/api/openapi.ts +284 -0
  10. package/frontend/src/pages/System.tsx +34 -0
  11. package/kg_schema.py +1 -1
  12. package/knowledge_graph.py +4 -4
  13. package/lattice_brain/__init__.py +70 -0
  14. package/lattice_brain/_kg_common.py +1 -0
  15. package/lattice_brain/archive.py +133 -0
  16. package/lattice_brain/context.py +3 -0
  17. package/lattice_brain/conversations.py +3 -0
  18. package/lattice_brain/core.py +82 -0
  19. package/lattice_brain/discovery.py +1 -0
  20. package/lattice_brain/documents.py +1 -0
  21. package/lattice_brain/embeddings.py +82 -0
  22. package/lattice_brain/identity.py +13 -0
  23. package/lattice_brain/ingest.py +1 -0
  24. package/lattice_brain/memory.py +3 -0
  25. package/lattice_brain/network.py +1 -0
  26. package/lattice_brain/projection.py +1 -0
  27. package/lattice_brain/provenance.py +1 -0
  28. package/lattice_brain/retrieval.py +1 -0
  29. package/lattice_brain/schema.py +1 -0
  30. package/lattice_brain/storage/__init__.py +22 -0
  31. package/lattice_brain/storage/base.py +72 -0
  32. package/lattice_brain/storage/docker.py +105 -0
  33. package/lattice_brain/storage/factory.py +31 -0
  34. package/lattice_brain/storage/migration.py +190 -0
  35. package/lattice_brain/storage/postgres.py +123 -0
  36. package/lattice_brain/storage/sqlite.py +128 -0
  37. package/lattice_brain/store.py +3 -0
  38. package/lattice_brain/write_master.py +1 -0
  39. package/latticeai/__init__.py +1 -1
  40. package/latticeai/api/portability.py +69 -0
  41. package/latticeai/app_factory.py +17 -10
  42. package/latticeai/brain/__init__.py +6 -6
  43. package/latticeai/brain/_kg_common.py +1 -1
  44. package/latticeai/brain/network.py +1 -1
  45. package/latticeai/brain/retrieval.py +15 -0
  46. package/latticeai/brain/store.py +22 -6
  47. package/latticeai/core/config.py +8 -0
  48. package/latticeai/core/marketplace.py +1 -1
  49. package/latticeai/core/multi_agent.py +1 -1
  50. package/latticeai/core/workspace_os.py +1 -1
  51. package/latticeai/services/kg_portability.py +82 -1
  52. package/package.json +2 -1
  53. package/scripts/bump_version.py +3 -0
  54. package/scripts/lint_frontend.mjs +5 -0
  55. package/scripts/migrate_brain_storage.py +53 -0
  56. package/scripts/wheel_smoke.py +3 -0
  57. package/src-tauri/Cargo.lock +1 -1
  58. package/src-tauri/Cargo.toml +1 -1
  59. package/src-tauri/tauri.conf.json +5 -2
  60. package/static/app/asset-manifest.json +5 -5
  61. package/static/app/assets/index-CDjiH_se.css +2 -0
  62. package/static/app/assets/{index-CJRAzNnf.js → index-C_HAkbAg.js} +3 -3
  63. package/static/app/assets/index-C_HAkbAg.js.map +1 -0
  64. package/static/app/index.html +2 -2
  65. package/static/app/assets/index-CJRAzNnf.js.map +0 -1
  66. package/static/app/assets/index-CSwBBgf4.css +0 -2
@@ -0,0 +1,70 @@
1
+ """lattice-brain — independent Brain Core package for Lattice AI.
2
+
3
+ Heavy graph modules are lazy-loaded so storage and archive utilities remain
4
+ usable without importing the FastAPI application or creating runtime globals.
5
+ """
6
+
7
+ from .archive import BrainArchivePaths, EncryptedBrainArchive
8
+ from .core import BrainCore, BrainCoreConfig
9
+ from .storage import (
10
+ DockerPostgresPlan,
11
+ DockerPostgresWizard,
12
+ PostgresConfig,
13
+ PostgresEngine,
14
+ SQLiteEngine,
15
+ SQLiteToPostgresMigrator,
16
+ StorageCapabilities,
17
+ StorageEngine,
18
+ StorageUnavailable,
19
+ storage_from_env,
20
+ )
21
+
22
+ __version__ = "4.2.0"
23
+
24
+ __all__ = [
25
+ "AssembledContext",
26
+ "BrainArchivePaths",
27
+ "BrainCore",
28
+ "BrainCoreConfig",
29
+ "BrainMemory",
30
+ "ContextAssembler",
31
+ "ContextSection",
32
+ "ConversationStore",
33
+ "DockerPostgresPlan",
34
+ "DockerPostgresWizard",
35
+ "EncryptedBrainArchive",
36
+ "KnowledgeGraphStore",
37
+ "PostgresConfig",
38
+ "PostgresEngine",
39
+ "SQLiteEngine",
40
+ "SQLiteToPostgresMigrator",
41
+ "StorageCapabilities",
42
+ "StorageEngine",
43
+ "StorageUnavailable",
44
+ "storage_from_env",
45
+ "__version__",
46
+ ]
47
+
48
+
49
+ def __getattr__(name: str):
50
+ if name in {"AssembledContext", "ContextAssembler", "ContextSection"}:
51
+ from .context import AssembledContext, ContextAssembler, ContextSection
52
+
53
+ return {
54
+ "AssembledContext": AssembledContext,
55
+ "ContextAssembler": ContextAssembler,
56
+ "ContextSection": ContextSection,
57
+ }[name]
58
+ if name == "ConversationStore":
59
+ from .conversations import ConversationStore
60
+
61
+ return ConversationStore
62
+ if name == "BrainMemory":
63
+ from .memory import BrainMemory
64
+
65
+ return BrainMemory
66
+ if name == "KnowledgeGraphStore":
67
+ from .store import KnowledgeGraphStore
68
+
69
+ return KnowledgeGraphStore
70
+ raise AttributeError(name)
@@ -0,0 +1 @@
1
+ from latticeai.brain._kg_common import * # noqa: F401,F403
@@ -0,0 +1,133 @@
1
+ """Encrypted .latticebrain archive support."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import json
7
+ import os
8
+ import shutil
9
+ import tempfile
10
+ import zipfile
11
+ from dataclasses import dataclass
12
+ from datetime import datetime, timezone
13
+ from pathlib import Path
14
+ from typing import Dict, Optional
15
+
16
+ from cryptography.hazmat.primitives import hashes
17
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
18
+ from cryptography.exceptions import InvalidTag
19
+ from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
20
+
21
+
22
+ ARCHIVE_FORMAT = "latticebrain.encrypted"
23
+ ARCHIVE_VERSION = 1
24
+ KDF_ITERATIONS = 390_000
25
+
26
+
27
+ def _now() -> str:
28
+ return datetime.now(timezone.utc).isoformat()
29
+
30
+
31
+ def _derive_key(passphrase: str, salt: bytes) -> bytes:
32
+ if not passphrase:
33
+ raise ValueError("A passphrase is required for encrypted .latticebrain archives.")
34
+ kdf = PBKDF2HMAC(
35
+ algorithm=hashes.SHA256(),
36
+ length=32,
37
+ salt=salt,
38
+ iterations=KDF_ITERATIONS,
39
+ )
40
+ return kdf.derive(passphrase.encode("utf-8"))
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class BrainArchivePaths:
45
+ db_path: Path
46
+ blob_dir: Optional[Path] = None
47
+
48
+
49
+ class EncryptedBrainArchive:
50
+ """Create and restore encrypted local Brain Core archives."""
51
+
52
+ def __init__(self, paths: BrainArchivePaths) -> None:
53
+ self.paths = paths
54
+
55
+ def create(self, destination: Path, *, passphrase: str) -> Dict[str, object]:
56
+ dest = Path(destination)
57
+ if dest.suffix != ".latticebrain":
58
+ dest = dest.with_suffix(".latticebrain")
59
+ if not self.paths.db_path.exists():
60
+ raise FileNotFoundError(f"Brain database not found: {self.paths.db_path}")
61
+ dest.parent.mkdir(parents=True, exist_ok=True)
62
+ with tempfile.TemporaryDirectory() as tmp_s:
63
+ tmp = Path(tmp_s)
64
+ payload = tmp / "payload.zip"
65
+ with zipfile.ZipFile(payload, "w", zipfile.ZIP_DEFLATED) as zf:
66
+ zf.write(self.paths.db_path, "knowledge_graph.sqlite")
67
+ if self.paths.blob_dir and self.paths.blob_dir.exists():
68
+ for file in self.paths.blob_dir.rglob("*"):
69
+ if file.is_file():
70
+ zf.write(file, f"blobs/{file.relative_to(self.paths.blob_dir)}")
71
+ salt = os.urandom(16)
72
+ nonce = os.urandom(12)
73
+ key = _derive_key(passphrase, salt)
74
+ ciphertext = AESGCM(key).encrypt(nonce, payload.read_bytes(), None)
75
+ envelope = {
76
+ "format": ARCHIVE_FORMAT,
77
+ "format_version": ARCHIVE_VERSION,
78
+ "created_at": _now(),
79
+ "kdf": {
80
+ "name": "PBKDF2HMAC-SHA256",
81
+ "iterations": KDF_ITERATIONS,
82
+ "salt": base64.b64encode(salt).decode("ascii"),
83
+ },
84
+ "cipher": {
85
+ "name": "AES-256-GCM",
86
+ "nonce": base64.b64encode(nonce).decode("ascii"),
87
+ },
88
+ "payload": base64.b64encode(ciphertext).decode("ascii"),
89
+ }
90
+ dest.write_text(json.dumps(envelope, indent=2), encoding="utf-8")
91
+ return {"path": str(dest), "bytes": dest.stat().st_size, "encrypted": True}
92
+
93
+ def restore(self, source: Path, *, passphrase: str, target: BrainArchivePaths) -> Dict[str, object]:
94
+ src = Path(source)
95
+ if not src.exists():
96
+ raise FileNotFoundError(f"Brain archive not found: {src}")
97
+ envelope = json.loads(src.read_text(encoding="utf-8"))
98
+ if envelope.get("format") != ARCHIVE_FORMAT:
99
+ raise ValueError("Not a .latticebrain encrypted archive.")
100
+ salt = base64.b64decode(envelope["kdf"]["salt"])
101
+ nonce = base64.b64decode(envelope["cipher"]["nonce"])
102
+ ciphertext = base64.b64decode(envelope["payload"])
103
+ key = _derive_key(passphrase, salt)
104
+ try:
105
+ plaintext = AESGCM(key).decrypt(nonce, ciphertext, None)
106
+ except InvalidTag as exc:
107
+ raise ValueError("Archive decryption failed; passphrase or archive data is invalid.") from exc
108
+ with tempfile.TemporaryDirectory() as tmp_s:
109
+ tmp = Path(tmp_s)
110
+ payload = tmp / "payload.zip"
111
+ payload.write_bytes(plaintext)
112
+ with zipfile.ZipFile(payload) as zf:
113
+ zf.extractall(tmp / "out")
114
+ db_src = tmp / "out" / "knowledge_graph.sqlite"
115
+ if not db_src.exists():
116
+ raise ValueError("Archive payload is missing knowledge_graph.sqlite.")
117
+ target.db_path.parent.mkdir(parents=True, exist_ok=True)
118
+ for sibling in (target.db_path, Path(str(target.db_path) + "-wal"), Path(str(target.db_path) + "-shm")):
119
+ if sibling.exists():
120
+ sibling.unlink()
121
+ shutil.copyfile(db_src, target.db_path)
122
+ blobs_src = tmp / "out" / "blobs"
123
+ if target.blob_dir:
124
+ if target.blob_dir.exists():
125
+ shutil.rmtree(target.blob_dir)
126
+ if blobs_src.exists():
127
+ shutil.copytree(blobs_src, target.blob_dir)
128
+ else:
129
+ target.blob_dir.mkdir(parents=True, exist_ok=True)
130
+ return {"restored": True, "path": str(target.db_path), "encrypted": True}
131
+
132
+
133
+ __all__ = ["BrainArchivePaths", "EncryptedBrainArchive"]
@@ -0,0 +1,3 @@
1
+ from latticeai.brain.context import AssembledContext, ContextAssembler, ContextSection, approx_tokens
2
+
3
+ __all__ = ["AssembledContext", "ContextAssembler", "ContextSection", "approx_tokens"]
@@ -0,0 +1,3 @@
1
+ from latticeai.brain.conversations import ConversationStore
2
+
3
+ __all__ = ["ConversationStore"]
@@ -0,0 +1,82 @@
1
+ """Independent Brain Core package facade."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Any, Optional
8
+
9
+ from .archive import BrainArchivePaths, EncryptedBrainArchive
10
+ from .storage import SQLiteEngine, StorageEngine, StorageUnavailable
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class BrainCoreConfig:
15
+ data_dir: Path
16
+ blob_dir: Optional[Path] = None
17
+ storage_engine: Optional[StorageEngine] = None
18
+
19
+
20
+ class BrainCore:
21
+ """Stable application boundary for the local Digital Brain.
22
+
23
+ FastAPI, CLI, tests, and future tools should depend on this package-level
24
+ facade instead of constructing scattered storage objects directly.
25
+ """
26
+
27
+ def __init__(self, config: BrainCoreConfig, *, embedder: Any = None) -> None:
28
+ self.config = config
29
+ self.data_dir = Path(config.data_dir)
30
+ self.db_path = self.data_dir / "knowledge_graph.sqlite"
31
+ self.blob_dir = Path(config.blob_dir) if config.blob_dir else self.data_dir / "knowledge_graph_blobs"
32
+ self.storage_engine = config.storage_engine or SQLiteEngine(self.db_path)
33
+ caps = self.storage_engine.capabilities()
34
+ if not caps.available:
35
+ raise StorageUnavailable(caps.reason or f"{caps.engine} storage is unavailable")
36
+ if caps.engine != "sqlite":
37
+ raise StorageUnavailable(
38
+ "The active FastAPI Brain Core runtime currently requires SQLiteEngine. "
39
+ "Use PostgresEngine through the explicit migration/scale tooling; no SQLite fallback was attempted."
40
+ )
41
+
42
+ from .conversations import ConversationStore
43
+ from .store import KnowledgeGraphStore
44
+
45
+ self.knowledge = KnowledgeGraphStore(
46
+ self.db_path,
47
+ self.blob_dir,
48
+ embedder=embedder,
49
+ storage_engine=self.storage_engine,
50
+ )
51
+ self.conversations = ConversationStore(self.db_path)
52
+ self.archive = EncryptedBrainArchive(
53
+ BrainArchivePaths(db_path=self.db_path, blob_dir=self.blob_dir)
54
+ )
55
+
56
+ @classmethod
57
+ def from_paths(
58
+ cls,
59
+ data_dir: Path,
60
+ *,
61
+ blob_dir: Optional[Path] = None,
62
+ embedder: Any = None,
63
+ storage_engine: Optional[StorageEngine] = None,
64
+ ) -> "BrainCore":
65
+ return cls(
66
+ BrainCoreConfig(
67
+ data_dir=Path(data_dir),
68
+ blob_dir=blob_dir,
69
+ storage_engine=storage_engine,
70
+ ),
71
+ embedder=embedder,
72
+ )
73
+
74
+ def status(self) -> dict:
75
+ return {
76
+ "storage": self.storage_engine.capabilities().as_dict(),
77
+ "db_path": str(self.db_path),
78
+ "blob_dir": str(self.blob_dir),
79
+ }
80
+
81
+
82
+ __all__ = ["BrainCore", "BrainCoreConfig"]
@@ -0,0 +1 @@
1
+ from latticeai.brain.discovery import * # noqa: F401,F403
@@ -0,0 +1 @@
1
+ from latticeai.brain.documents import * # noqa: F401,F403
@@ -0,0 +1,82 @@
1
+ """Local deterministic embeddings used by the standalone Brain Core package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import math
7
+ import os
8
+ import re
9
+ import struct
10
+ from dataclasses import dataclass
11
+ from typing import Iterable, List
12
+
13
+
14
+ DEFAULT_EMBEDDING_DIM = int(os.getenv("LATTICEAI_VECTOR_DIM", "384"))
15
+ EMBEDDING_MODEL_ID = f"lattice-local-hash-v1:{DEFAULT_EMBEDDING_DIM}"
16
+
17
+
18
+ def _tokenize(text: str) -> List[str]:
19
+ raw = str(text or "").lower()
20
+ tokens = re.findall(r"[a-z0-9][a-z0-9_.:/+-]{1,}|[가-힣]{2,}", raw)
21
+ features: List[str] = []
22
+ for token in tokens:
23
+ features.append(f"tok:{token}")
24
+ if len(token) >= 5 and re.search(r"[a-z]", token):
25
+ for i in range(0, len(token) - 2):
26
+ features.append(f"tri:{token[i:i+3]}")
27
+ if re.search(r"[가-힣]", token) and len(token) >= 3:
28
+ for i in range(0, len(token) - 1):
29
+ features.append(f"ko:{token[i:i+2]}")
30
+ return features
31
+
32
+
33
+ def _hash_to_index(feature: str, dim: int) -> tuple[int, float]:
34
+ digest = hashlib.blake2b(feature.encode("utf-8"), digest_size=8).digest()
35
+ value = int.from_bytes(digest, "big", signed=False)
36
+ sign = 1.0 if (value & 1) == 0 else -1.0
37
+ return value % dim, sign
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class LocalEmbeddingModel:
42
+ """Deterministic local embedder.
43
+
44
+ This is intentionally not presented as a production semantic model. It is
45
+ a real, offline cosine signal for local-first operation and tests; setup
46
+ wizard provisioning can replace it with a user-consented model/provider.
47
+ """
48
+
49
+ dim: int = DEFAULT_EMBEDDING_DIM
50
+ model_id: str = EMBEDDING_MODEL_ID
51
+
52
+ def embed(self, text: str) -> List[float]:
53
+ vector = [0.0] * self.dim
54
+ features = _tokenize(text)
55
+ if not features:
56
+ return vector
57
+ for feature in features:
58
+ index, sign = _hash_to_index(feature, self.dim)
59
+ vector[index] += sign
60
+ norm = math.sqrt(sum(value * value for value in vector))
61
+ if norm <= 0:
62
+ return vector
63
+ return [value / norm for value in vector]
64
+
65
+ def similarity(self, left: Iterable[float], right: Iterable[float]) -> float:
66
+ return float(sum(a * b for a, b in zip(left, right)))
67
+
68
+ def encode(self, vector: Iterable[float]) -> bytes:
69
+ values = list(vector)
70
+ return struct.pack(f"<{len(values)}f", *values)
71
+
72
+ def decode(self, payload: bytes, dim: int | None = None) -> List[float]:
73
+ if not payload:
74
+ return []
75
+ count = int(dim or self.dim)
76
+ expected = count * 4
77
+ if len(payload) != expected:
78
+ count = len(payload) // 4
79
+ return list(struct.unpack(f"<{count}f", payload[: count * 4]))
80
+
81
+
82
+ __all__ = ["DEFAULT_EMBEDDING_DIM", "EMBEDDING_MODEL_ID", "LocalEmbeddingModel"]
@@ -0,0 +1,13 @@
1
+ from latticeai.brain.identity import (
2
+ DeviceIdentity,
3
+ fingerprint_of,
4
+ verify_manifest,
5
+ verify_signature,
6
+ )
7
+
8
+ __all__ = [
9
+ "DeviceIdentity",
10
+ "fingerprint_of",
11
+ "verify_manifest",
12
+ "verify_signature",
13
+ ]
@@ -0,0 +1 @@
1
+ from latticeai.brain.ingest import * # noqa: F401,F403
@@ -0,0 +1,3 @@
1
+ from latticeai.brain.memory import BrainMemory
2
+
3
+ __all__ = ["BrainMemory"]
@@ -0,0 +1 @@
1
+ from latticeai.brain.network import * # noqa: F401,F403
@@ -0,0 +1 @@
1
+ from latticeai.brain.projection import * # noqa: F401,F403
@@ -0,0 +1 @@
1
+ from latticeai.brain.provenance import * # noqa: F401,F403
@@ -0,0 +1 @@
1
+ from latticeai.brain.retrieval import * # noqa: F401,F403
@@ -0,0 +1 @@
1
+ from latticeai.brain.schema import * # noqa: F401,F403
@@ -0,0 +1,22 @@
1
+ """Pluggable storage layer for lattice-brain."""
2
+
3
+ from .base import StorageCapabilities, StorageEngine, StorageUnavailable
4
+ from .docker import DockerPostgresPlan, DockerPostgresWizard
5
+ from .factory import storage_from_env
6
+ from .migration import SQLiteToPostgresMigrator, TablePlan
7
+ from .postgres import PostgresConfig, PostgresEngine
8
+ from .sqlite import SQLiteEngine
9
+
10
+ __all__ = [
11
+ "DockerPostgresPlan",
12
+ "DockerPostgresWizard",
13
+ "PostgresConfig",
14
+ "PostgresEngine",
15
+ "SQLiteEngine",
16
+ "SQLiteToPostgresMigrator",
17
+ "StorageCapabilities",
18
+ "StorageEngine",
19
+ "StorageUnavailable",
20
+ "TablePlan",
21
+ "storage_from_env",
22
+ ]
@@ -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"]