ltcai 4.3.3 → 4.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 (89) hide show
  1. package/README.md +21 -16
  2. package/docs/CHANGELOG.md +37 -0
  3. package/docs/V4_4_0_EXTRACTION_REPORT.md +239 -0
  4. package/lattice_brain/__init__.py +38 -23
  5. package/lattice_brain/_kg_common.py +11 -1
  6. package/lattice_brain/context.py +212 -2
  7. package/lattice_brain/conversations.py +234 -1
  8. package/lattice_brain/discovery.py +11 -1
  9. package/lattice_brain/documents.py +11 -1
  10. package/lattice_brain/graph/__init__.py +28 -0
  11. package/lattice_brain/graph/_kg_common.py +1123 -0
  12. package/lattice_brain/graph/curator.py +473 -0
  13. package/lattice_brain/graph/discovery.py +1455 -0
  14. package/lattice_brain/graph/documents.py +218 -0
  15. package/lattice_brain/graph/identity.py +175 -0
  16. package/lattice_brain/graph/ingest.py +644 -0
  17. package/lattice_brain/graph/network.py +205 -0
  18. package/lattice_brain/graph/projection.py +571 -0
  19. package/lattice_brain/graph/provenance.py +401 -0
  20. package/lattice_brain/graph/retrieval.py +1341 -0
  21. package/lattice_brain/graph/schema.py +640 -0
  22. package/lattice_brain/graph/store.py +237 -0
  23. package/lattice_brain/graph/write_master.py +225 -0
  24. package/lattice_brain/identity.py +11 -13
  25. package/lattice_brain/ingest.py +11 -1
  26. package/lattice_brain/ingestion.py +318 -0
  27. package/lattice_brain/memory.py +100 -1
  28. package/lattice_brain/network.py +11 -1
  29. package/lattice_brain/portability.py +431 -0
  30. package/lattice_brain/projection.py +11 -1
  31. package/lattice_brain/provenance.py +11 -1
  32. package/lattice_brain/retrieval.py +11 -1
  33. package/lattice_brain/runtime/__init__.py +32 -0
  34. package/lattice_brain/runtime/agent_runtime.py +569 -0
  35. package/lattice_brain/runtime/hooks.py +754 -0
  36. package/lattice_brain/runtime/multi_agent.py +795 -0
  37. package/lattice_brain/schema.py +11 -1
  38. package/lattice_brain/store.py +10 -2
  39. package/lattice_brain/workflow.py +461 -0
  40. package/lattice_brain/write_master.py +11 -1
  41. package/latticeai/__init__.py +1 -1
  42. package/latticeai/api/agents.py +2 -2
  43. package/latticeai/api/browser.py +1 -1
  44. package/latticeai/api/chat.py +1 -1
  45. package/latticeai/api/computer_use.py +1 -1
  46. package/latticeai/api/hooks.py +2 -2
  47. package/latticeai/api/mcp.py +1 -1
  48. package/latticeai/api/tools.py +1 -1
  49. package/latticeai/api/workflow_designer.py +2 -2
  50. package/latticeai/app_factory.py +4 -4
  51. package/latticeai/brain/__init__.py +24 -6
  52. package/latticeai/brain/_kg_common.py +11 -1117
  53. package/latticeai/brain/context.py +12 -208
  54. package/latticeai/brain/conversations.py +12 -231
  55. package/latticeai/brain/discovery.py +13 -1451
  56. package/latticeai/brain/documents.py +13 -214
  57. package/latticeai/brain/identity.py +11 -169
  58. package/latticeai/brain/ingest.py +13 -640
  59. package/latticeai/brain/memory.py +12 -97
  60. package/latticeai/brain/network.py +12 -200
  61. package/latticeai/brain/projection.py +13 -567
  62. package/latticeai/brain/provenance.py +13 -397
  63. package/latticeai/brain/retrieval.py +13 -1337
  64. package/latticeai/brain/schema.py +12 -635
  65. package/latticeai/brain/store.py +13 -233
  66. package/latticeai/brain/write_master.py +13 -221
  67. package/latticeai/core/agent.py +1 -1
  68. package/latticeai/core/agent_registry.py +2 -2
  69. package/latticeai/core/builtin_hooks.py +2 -2
  70. package/latticeai/core/graph_curator.py +6 -468
  71. package/latticeai/core/hooks.py +6 -749
  72. package/latticeai/core/marketplace.py +1 -1
  73. package/latticeai/core/multi_agent.py +6 -790
  74. package/latticeai/core/workflow_engine.py +6 -456
  75. package/latticeai/core/workspace_os.py +1 -1
  76. package/latticeai/services/agent_runtime.py +6 -564
  77. package/latticeai/services/ingestion.py +6 -313
  78. package/latticeai/services/kg_portability.py +6 -426
  79. package/latticeai/services/platform_runtime.py +3 -3
  80. package/latticeai/services/run_executor.py +1 -1
  81. package/latticeai/services/upload_service.py +1 -1
  82. package/p_reinforce.py +1 -1
  83. package/package.json +1 -1
  84. package/scripts/bump_version.py +1 -1
  85. package/scripts/wheel_smoke.py +7 -0
  86. package/src-tauri/Cargo.lock +1 -1
  87. package/src-tauri/Cargo.toml +1 -1
  88. package/src-tauri/tauri.conf.json +1 -1
  89. package/static/app/asset-manifest.json +1 -1
@@ -0,0 +1,218 @@
1
+ from __future__ import annotations
2
+
3
+ # ruff: noqa: F403,F405
4
+
5
+ from ._kg_common import * # noqa: F403,F401
6
+
7
+
8
+ class KnowledgeGraphDocumentsMixin:
9
+ def _ingest_structure_nodes(
10
+ self,
11
+ conn: sqlite3.Connection,
12
+ file_id: str,
13
+ filename: str,
14
+ structure: Dict[str, Any],
15
+ ) -> None:
16
+ for slide in structure.get("slides") or []:
17
+ index = slide.get("index")
18
+ slide_id = f"slide:{_sha256_text(f'{file_id}:slide:{index}')[:24]}"
19
+ title = f"{filename} slide {index}"
20
+ summary = "\n".join(slide.get("texts") or [])[:800]
21
+ self._upsert_node(
22
+ conn, slide_id, "Slide", title, summary=summary, metadata=slide
23
+ )
24
+ self._upsert_edge(conn, file_id, slide_id, "has_slide")
25
+ for text in slide.get("texts") or []:
26
+ for topic in _topic_candidates(text, limit=4):
27
+ topic_id = f"topic:{_slug(topic)}"
28
+ self._upsert_node(
29
+ conn,
30
+ topic_id,
31
+ "Topic",
32
+ topic,
33
+ metadata={"auto_extracted": True},
34
+ )
35
+ self._upsert_edge(conn, slide_id, topic_id, "discusses", weight=0.6)
36
+
37
+ for page in structure.get("pages") or []:
38
+ index = page.get("index")
39
+ page_id = f"page:{_sha256_text(f'{file_id}:page:{index}')[:24]}"
40
+ title = f"{filename} page {index}"
41
+ self._upsert_node(
42
+ conn,
43
+ page_id,
44
+ "Page",
45
+ title,
46
+ summary=page.get("preview") or "",
47
+ metadata=page,
48
+ )
49
+ self._upsert_edge(conn, file_id, page_id, "has_page")
50
+ for topic in _topic_candidates(page.get("preview") or "", limit=4):
51
+ topic_id = f"topic:{_slug(topic)}"
52
+ self._upsert_node(
53
+ conn, topic_id, "Topic", topic, metadata={"auto_extracted": True}
54
+ )
55
+ self._upsert_edge(conn, page_id, topic_id, "discusses", weight=0.6)
56
+
57
+ for sheet in structure.get("sheets") or []:
58
+ sheet_title = sheet.get("title")
59
+ sheet_id = f"sheet:{_sha256_text(f'{file_id}:sheet:{sheet_title}')[:24]}"
60
+ self._upsert_node(
61
+ conn, sheet_id, "Sheet", f"{filename} / {sheet_title}", metadata=sheet
62
+ )
63
+ self._upsert_edge(conn, file_id, sheet_id, "has_sheet")
64
+
65
+ for image in structure.get("images") or []:
66
+ image_key = image.get("sha256") or _sha256_text(
67
+ json.dumps(image, ensure_ascii=False, sort_keys=True)
68
+ )
69
+ image_id = f"image:{str(image_key)[:24]}"
70
+ title_parts = [filename, "image"]
71
+ if image.get("page"):
72
+ title_parts.append(f"page {image.get('page')}")
73
+ if image.get("name"):
74
+ title_parts.append(str(image.get("name")).split("/")[-1])
75
+ self._upsert_node(
76
+ conn, image_id, "Image", " / ".join(title_parts), metadata=image
77
+ )
78
+ self._upsert_edge(conn, file_id, image_id, "contains_image")
79
+
80
+ def _document_structure(self, path: Path, ext: str) -> Dict[str, Any]:
81
+ try:
82
+ if ext == ".pptx":
83
+ return self._pptx_structure(path)
84
+ if ext == ".pdf":
85
+ return self._pdf_structure(path)
86
+ if ext == ".docx":
87
+ return self._docx_structure(path)
88
+ if ext == ".xlsx":
89
+ return self._xlsx_structure(path)
90
+ except Exception as exc:
91
+ return {"error": str(exc)}
92
+ return {}
93
+
94
+ def _pptx_structure(self, path: Path) -> Dict[str, Any]:
95
+ result: Dict[str, Any] = {"slides": [], "images": []}
96
+ try:
97
+ from PIL import Image
98
+ from pptx import Presentation
99
+
100
+ prs = Presentation(str(path))
101
+ for slide_index, slide in enumerate(prs.slides, start=1):
102
+ slide_info = {"index": slide_index, "shapes": [], "texts": []}
103
+ for shape_index, shape in enumerate(slide.shapes, start=1):
104
+ shape_info = {
105
+ "index": shape_index,
106
+ "name": getattr(shape, "name", ""),
107
+ "shape_type": str(getattr(shape, "shape_type", "")),
108
+ "bbox": {
109
+ "left": int(getattr(shape, "left", 0) or 0),
110
+ "top": int(getattr(shape, "top", 0) or 0),
111
+ "width": int(getattr(shape, "width", 0) or 0),
112
+ "height": int(getattr(shape, "height", 0) or 0),
113
+ },
114
+ }
115
+ if getattr(shape, "has_text_frame", False):
116
+ text = shape.text_frame.text.strip()
117
+ if text:
118
+ shape_info["text"] = text[:1000]
119
+ slide_info["texts"].append(text)
120
+ slide_info["shapes"].append(shape_info)
121
+ result["slides"].append(slide_info)
122
+ with zipfile.ZipFile(path) as zf:
123
+ for name in zf.namelist():
124
+ if not name.startswith("ppt/media/"):
125
+ continue
126
+ data = zf.read(name)
127
+ image_info: Dict[str, Any] = {
128
+ "name": name,
129
+ "bytes": len(data),
130
+ "sha256": _sha256_bytes(data),
131
+ }
132
+ try:
133
+ from io import BytesIO
134
+
135
+ with Image.open(BytesIO(data)) as img:
136
+ image_info.update(
137
+ {
138
+ "width": img.width,
139
+ "height": img.height,
140
+ "format": img.format,
141
+ }
142
+ )
143
+ except Exception:
144
+ pass
145
+ result["images"].append(image_info)
146
+ except Exception as exc:
147
+ result["error"] = str(exc)
148
+ return result
149
+
150
+ def _pdf_structure(self, path: Path) -> Dict[str, Any]:
151
+ result: Dict[str, Any] = {"pages": [], "images": []}
152
+ try:
153
+ import pdfplumber
154
+
155
+ with pdfplumber.open(str(path)) as pdf:
156
+ metadata = dict(pdf.metadata or {})
157
+ result["metadata"] = {str(k): str(v) for k, v in metadata.items()}
158
+ for page_index, page in enumerate(pdf.pages, start=1):
159
+ text = page.extract_text() or ""
160
+ page_info = {
161
+ "index": page_index,
162
+ "width": float(page.width or 0),
163
+ "height": float(page.height or 0),
164
+ "chars": len(text),
165
+ "preview": _clean_text(text)[:500],
166
+ "image_count": len(page.images or []),
167
+ }
168
+ result["pages"].append(page_info)
169
+ for image_index, image in enumerate(page.images or [], start=1):
170
+ result["images"].append(
171
+ {
172
+ "page": page_index,
173
+ "index": image_index,
174
+ "name": image.get("name"),
175
+ "width": image.get("width"),
176
+ "height": image.get("height"),
177
+ "bbox": {
178
+ "x0": image.get("x0"),
179
+ "top": image.get("top"),
180
+ "x1": image.get("x1"),
181
+ "bottom": image.get("bottom"),
182
+ },
183
+ }
184
+ )
185
+ except Exception as exc:
186
+ result["error"] = str(exc)
187
+ return result
188
+
189
+ def _docx_structure(self, path: Path) -> Dict[str, Any]:
190
+ from docx import Document
191
+
192
+ doc = Document(str(path))
193
+ headings = []
194
+ paragraphs = 0
195
+ for p in doc.paragraphs:
196
+ text = p.text.strip()
197
+ if not text:
198
+ continue
199
+ paragraphs += 1
200
+ style = getattr(p.style, "name", "")
201
+ if style.lower().startswith("heading"):
202
+ headings.append({"style": style, "text": text[:240]})
203
+ return {
204
+ "paragraphs": paragraphs,
205
+ "headings": headings[:80],
206
+ "tables": len(doc.tables),
207
+ }
208
+
209
+ def _xlsx_structure(self, path: Path) -> Dict[str, Any]:
210
+ from openpyxl import load_workbook
211
+
212
+ wb = load_workbook(str(path), read_only=True, data_only=True)
213
+ sheets = []
214
+ for ws in wb.worksheets:
215
+ sheets.append(
216
+ {"title": ws.title, "max_row": ws.max_row, "max_column": ws.max_column}
217
+ )
218
+ return {"sheets": sheets}
@@ -0,0 +1,175 @@
1
+ """Device identity — the sovereignty primitive.
2
+
3
+ Every Lattice installation owns an Ed25519 keypair. Exports are signed by
4
+ it, peers pair against its public key, and imported knowledge records which
5
+ device it came from. The private key never leaves the machine: it lives in
6
+ the OS keyring when one is available, otherwise in a 0600 file under the
7
+ data directory (the storage backend is reported honestly).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import base64
13
+ import hashlib
14
+ import json
15
+ import logging
16
+ import os
17
+ from pathlib import Path
18
+ from typing import Any, Dict, Optional
19
+
20
+ from cryptography.hazmat.primitives import serialization
21
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import (
22
+ Ed25519PrivateKey,
23
+ Ed25519PublicKey,
24
+ )
25
+
26
+ _KEYRING_SERVICE = "lattice-ai-device-identity"
27
+ _KEYRING_ENTRY = "ed25519-private-key"
28
+
29
+
30
+ def _b64(data: bytes) -> str:
31
+ return base64.urlsafe_b64encode(data).decode("ascii").rstrip("=")
32
+
33
+
34
+ def _unb64(text: str) -> bytes:
35
+ padded = text + "=" * (-len(text) % 4)
36
+ return base64.urlsafe_b64decode(padded.encode("ascii"))
37
+
38
+
39
+ def _keyring_opt_in() -> bool:
40
+ """Keyring storage is opt-in (LATTICEAI_DEVICE_KEY_KEYRING=1).
41
+
42
+ OS keychain access can block or prompt during startup/tests; the default
43
+ is a 0600 file under the data dir, and ``describe()`` reports which
44
+ backend holds the key — no silent security theater either way.
45
+ """
46
+ return os.getenv("LATTICEAI_DEVICE_KEY_KEYRING", "").strip() in {"1", "true", "yes"}
47
+
48
+
49
+ class DeviceIdentity:
50
+ """Loads-or-creates the installation's Ed25519 keypair."""
51
+
52
+ def __init__(self, data_dir: Path, *, use_keyring: Optional[bool] = None):
53
+ if use_keyring is None:
54
+ use_keyring = _keyring_opt_in()
55
+ self._data_dir = Path(data_dir)
56
+ self._key_file = self._data_dir / "device_identity.key"
57
+ self._private: Ed25519PrivateKey
58
+ self.storage: str # "keyring" | "file"
59
+ self._load_or_create(use_keyring)
60
+
61
+ # ── key material ───────────────────────────────────────────────────────
62
+ def _load_or_create(self, use_keyring: bool) -> None:
63
+ raw: Optional[bytes] = None
64
+ backend = "file"
65
+ if use_keyring:
66
+ try:
67
+ import keyring
68
+
69
+ stored = keyring.get_password(_KEYRING_SERVICE, _KEYRING_ENTRY)
70
+ if stored:
71
+ raw = _unb64(stored)
72
+ backend = "keyring"
73
+ except Exception as exc:
74
+ logging.debug("device identity: keyring unavailable (%s)", exc)
75
+ if raw is None and self._key_file.exists():
76
+ raw = _unb64(self._key_file.read_text().strip())
77
+ backend = "file"
78
+ if raw is None:
79
+ key = Ed25519PrivateKey.generate()
80
+ raw = key.private_bytes(
81
+ serialization.Encoding.Raw,
82
+ serialization.PrivateFormat.Raw,
83
+ serialization.NoEncryption(),
84
+ )
85
+ backend = self._persist_new(raw, use_keyring)
86
+ self._private = Ed25519PrivateKey.from_private_bytes(raw)
87
+ self.storage = backend
88
+
89
+ def _persist_new(self, raw: bytes, use_keyring: bool) -> str:
90
+ if use_keyring:
91
+ try:
92
+ import keyring
93
+
94
+ keyring.set_password(_KEYRING_SERVICE, _KEYRING_ENTRY, _b64(raw))
95
+ return "keyring"
96
+ except Exception as exc:
97
+ logging.debug("device identity: keyring store failed (%s); using file", exc)
98
+ self._data_dir.mkdir(parents=True, exist_ok=True)
99
+ self._key_file.write_text(_b64(raw))
100
+ os.chmod(self._key_file, 0o600)
101
+ return "file"
102
+
103
+ # ── public surface ─────────────────────────────────────────────────────
104
+ @property
105
+ def public_key_b64(self) -> str:
106
+ return _b64(
107
+ self._private.public_key().public_bytes(
108
+ serialization.Encoding.Raw, serialization.PublicFormat.Raw
109
+ )
110
+ )
111
+
112
+ @property
113
+ def fingerprint(self) -> str:
114
+ """Short human-comparable id: sha256 of the raw public key."""
115
+ raw = self._private.public_key().public_bytes(
116
+ serialization.Encoding.Raw, serialization.PublicFormat.Raw
117
+ )
118
+ digest = hashlib.sha256(raw).hexdigest()
119
+ return ":".join(digest[i : i + 4] for i in range(0, 16, 4))
120
+
121
+ def describe(self) -> Dict[str, Any]:
122
+ return {
123
+ "fingerprint": self.fingerprint,
124
+ "public_key": self.public_key_b64,
125
+ "algorithm": "ed25519",
126
+ "storage": self.storage,
127
+ }
128
+
129
+ # ── signing ────────────────────────────────────────────────────────────
130
+ def sign(self, payload: bytes) -> str:
131
+ return _b64(self._private.sign(payload))
132
+
133
+ def sign_manifest(self, manifest: Dict[str, Any]) -> Dict[str, Any]:
134
+ """Detached signature over the canonical JSON of a manifest."""
135
+ canonical = json.dumps(manifest, sort_keys=True, ensure_ascii=False).encode("utf-8")
136
+ return {
137
+ "algorithm": "ed25519",
138
+ "public_key": self.public_key_b64,
139
+ "fingerprint": self.fingerprint,
140
+ "signature": self.sign(canonical),
141
+ }
142
+
143
+
144
+ def fingerprint_of(public_key_b64: str) -> str:
145
+ """Human-comparable fingerprint of an Ed25519 public key.
146
+
147
+ Raises ValueError when the input is not a valid key — the pairing flow
148
+ uses this as its validation gate.
149
+ """
150
+ raw = _unb64(public_key_b64)
151
+ Ed25519PublicKey.from_public_bytes(raw) # validates; raises on garbage
152
+ digest = hashlib.sha256(raw).hexdigest()
153
+ return ":".join(digest[i : i + 4] for i in range(0, 16, 4))
154
+
155
+
156
+ def verify_signature(public_key_b64: str, payload: bytes, signature_b64: str) -> bool:
157
+ """True iff ``signature`` is valid for ``payload`` under the given key."""
158
+ try:
159
+ key = Ed25519PublicKey.from_public_bytes(_unb64(public_key_b64))
160
+ key.verify(_unb64(signature_b64), payload)
161
+ return True
162
+ except Exception:
163
+ return False
164
+
165
+
166
+ def verify_manifest(manifest: Dict[str, Any], signature_block: Dict[str, Any]) -> bool:
167
+ canonical = json.dumps(manifest, sort_keys=True, ensure_ascii=False).encode("utf-8")
168
+ return verify_signature(
169
+ str(signature_block.get("public_key") or ""),
170
+ canonical,
171
+ str(signature_block.get("signature") or ""),
172
+ )
173
+
174
+
175
+ __all__ = ["DeviceIdentity", "verify_signature", "verify_manifest"]