ltcai 4.3.1 → 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.
- package/README.md +191 -278
- package/docs/CHANGELOG.md +128 -0
- package/docs/V4_3_2_DEADCODE_AUDIT_REPORT.md +174 -0
- package/docs/V4_3_2_DOCUMENTATION_CLEANUP_REPORT.md +81 -0
- package/docs/V4_3_2_GITHUB_VERCEL_CHECK_REPORT.md +75 -0
- package/docs/V4_3_2_GRAPH_UX_REPORT.md +48 -0
- package/docs/V4_3_2_INDEPENDENT_AUDIT_PACKAGE.md +209 -0
- package/docs/V4_3_2_PRODUCT_POLISH_REPORT.md +57 -0
- package/docs/V4_3_2_SELF_AUDIT_REPORT.md +63 -0
- package/docs/V4_3_2_VALIDATION_REPORT.md +97 -0
- package/docs/V4_3_3_VALIDATION_REPORT.md +46 -0
- package/docs/V4_4_0_EXTRACTION_REPORT.md +239 -0
- package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +18 -19
- package/frontend/openapi.json +1 -1
- package/frontend/src/components/primitives.tsx +92 -10
- package/frontend/src/pages/Act.tsx +11 -9
- package/frontend/src/pages/Ask.tsx +2 -2
- package/frontend/src/pages/Brain.tsx +607 -65
- package/frontend/src/pages/Capture.tsx +11 -7
- package/frontend/src/pages/Library.tsx +3 -3
- package/frontend/src/pages/System.tsx +186 -23
- package/lattice_brain/__init__.py +38 -23
- package/lattice_brain/_kg_common.py +11 -1
- package/lattice_brain/context.py +212 -2
- package/lattice_brain/conversations.py +234 -1
- package/lattice_brain/discovery.py +11 -1
- package/lattice_brain/documents.py +11 -1
- package/lattice_brain/graph/__init__.py +28 -0
- package/lattice_brain/graph/_kg_common.py +1123 -0
- package/lattice_brain/graph/curator.py +473 -0
- package/lattice_brain/graph/discovery.py +1455 -0
- package/lattice_brain/graph/documents.py +218 -0
- package/lattice_brain/graph/identity.py +175 -0
- package/lattice_brain/graph/ingest.py +644 -0
- package/lattice_brain/graph/network.py +205 -0
- package/lattice_brain/graph/projection.py +571 -0
- package/lattice_brain/graph/provenance.py +401 -0
- package/lattice_brain/graph/retrieval.py +1341 -0
- package/lattice_brain/graph/schema.py +640 -0
- package/lattice_brain/graph/store.py +237 -0
- package/lattice_brain/graph/write_master.py +225 -0
- package/lattice_brain/identity.py +11 -13
- package/lattice_brain/ingest.py +11 -1
- package/lattice_brain/ingestion.py +318 -0
- package/lattice_brain/memory.py +100 -1
- package/lattice_brain/network.py +11 -1
- package/lattice_brain/portability.py +431 -0
- package/lattice_brain/projection.py +11 -1
- package/lattice_brain/provenance.py +11 -1
- package/lattice_brain/retrieval.py +11 -1
- package/lattice_brain/runtime/__init__.py +32 -0
- package/lattice_brain/runtime/agent_runtime.py +569 -0
- package/lattice_brain/runtime/hooks.py +754 -0
- package/lattice_brain/runtime/multi_agent.py +795 -0
- package/lattice_brain/schema.py +11 -1
- package/lattice_brain/store.py +10 -2
- package/lattice_brain/workflow.py +461 -0
- package/lattice_brain/write_master.py +11 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/agents.py +2 -2
- package/latticeai/api/browser.py +1 -1
- package/latticeai/api/chat.py +1 -1
- package/latticeai/api/computer_use.py +1 -1
- package/latticeai/api/hooks.py +2 -2
- package/latticeai/api/mcp.py +1 -1
- package/latticeai/api/tools.py +1 -1
- package/latticeai/api/workflow_designer.py +2 -2
- package/latticeai/app_factory.py +4 -4
- package/latticeai/brain/__init__.py +24 -6
- package/latticeai/brain/_kg_common.py +11 -1117
- package/latticeai/brain/context.py +12 -208
- package/latticeai/brain/conversations.py +12 -231
- package/latticeai/brain/discovery.py +13 -1451
- package/latticeai/brain/documents.py +13 -214
- package/latticeai/brain/identity.py +11 -169
- package/latticeai/brain/ingest.py +13 -640
- package/latticeai/brain/memory.py +12 -97
- package/latticeai/brain/network.py +12 -200
- package/latticeai/brain/projection.py +13 -567
- package/latticeai/brain/provenance.py +13 -397
- package/latticeai/brain/retrieval.py +13 -1337
- package/latticeai/brain/schema.py +12 -635
- package/latticeai/brain/store.py +13 -233
- package/latticeai/brain/write_master.py +13 -221
- package/latticeai/core/agent.py +1 -1
- package/latticeai/core/agent_registry.py +2 -2
- package/latticeai/core/builtin_hooks.py +2 -2
- package/latticeai/core/graph_curator.py +6 -468
- package/latticeai/core/hooks.py +6 -749
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/multi_agent.py +6 -790
- package/latticeai/core/workflow_engine.py +6 -456
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/services/agent_runtime.py +6 -564
- package/latticeai/services/ingestion.py +6 -313
- package/latticeai/services/kg_portability.py +6 -426
- package/latticeai/services/platform_runtime.py +3 -3
- package/latticeai/services/run_executor.py +1 -1
- package/latticeai/services/upload_service.py +1 -1
- package/p_reinforce.py +1 -1
- package/package.json +3 -6
- package/scripts/build_vercel_static.mjs +77 -0
- package/scripts/bump_version.py +1 -1
- package/scripts/check_markdown_links.mjs +75 -0
- package/scripts/wheel_smoke.py +7 -0
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/src/main.rs +12 -2
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +5 -5
- package/static/app/assets/index-CHHal8Zl.css +2 -0
- package/static/app/assets/index-pdzil9ac.js +333 -0
- package/static/app/assets/index-pdzil9ac.js.map +1 -0
- package/static/app/index.html +2 -2
- package/latticeai/api/deps.py +0 -15
- package/scripts/capture/README.md +0 -28
- package/scripts/capture/capture_enterprise.js +0 -8
- package/scripts/capture/capture_graph.js +0 -8
- package/scripts/capture/capture_onboarding.js +0 -8
- package/scripts/capture/capture_page.js +0 -43
- package/scripts/capture/capture_release_media.js +0 -125
- package/scripts/capture/capture_skills.js +0 -8
- package/scripts/capture/capture_v340.js +0 -88
- package/scripts/capture/capture_workspace.js +0 -8
- package/scripts/generate_diagrams.py +0 -512
- package/scripts/release-0.3.1.sh +0 -105
- package/scripts/take_screenshots.js +0 -69
- package/static/app/assets/index-BhPuj8rT.js +0 -333
- package/static/app/assets/index-BhPuj8rT.js.map +0 -1
- package/static/app/assets/index-yZswHE3d.css +0 -2
- package/static/css/tokens.3ba22e37.css +0 -260
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"""Unified ingestion pipeline — the single write-side seam into the Knowledge Graph.
|
|
2
|
+
|
|
3
|
+
v3.6.0 Knowledge Graph First principle: *no data source bypasses the Knowledge
|
|
4
|
+
Graph and no source creates an isolated silo*. Every source — local files,
|
|
5
|
+
connected folders, PDFs/Markdown/text/code, web URLs, browser tabs — is
|
|
6
|
+
normalized into one :class:`IngestionItem` and pushed through one
|
|
7
|
+
:meth:`IngestionPipeline.ingest` entrypoint:
|
|
8
|
+
|
|
9
|
+
Source → normalize → content hash → (file | text) ingest → provenance
|
|
10
|
+
|
|
11
|
+
The pipeline is deliberately thin. It owns normalization, idempotency reporting,
|
|
12
|
+
provenance capture, and — crucially — routing every ingest through the shared
|
|
13
|
+
``dispatch_tool`` lifecycle so ``pre_tool``/``post_tool`` hooks fire on data
|
|
14
|
+
ingestion exactly as they do on tool calls. The heavy graph construction lives in
|
|
15
|
+
:class:`knowledge_graph.KnowledgeGraphStore` (``ingest_document`` for files,
|
|
16
|
+
``ingest_source`` for text/web), which this module composes rather than
|
|
17
|
+
re-implements.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import hashlib
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from datetime import datetime, timezone
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Any, Dict, List, Optional
|
|
27
|
+
|
|
28
|
+
from .runtime.hooks import dispatch_tool
|
|
29
|
+
|
|
30
|
+
# Source types that arrive as a file on disk (read via ingest_document).
|
|
31
|
+
FILE_SOURCE_TYPES = frozenset({"file", "local_file", "upload", "pdf"})
|
|
32
|
+
# Source types that arrive as extracted text (read via ingest_source).
|
|
33
|
+
TEXT_SOURCE_TYPES = frozenset(
|
|
34
|
+
{"web_url", "browser_tab", "text", "markdown", "note", "code", "clipboard"}
|
|
35
|
+
)
|
|
36
|
+
# Conversational exchanges (read via ingest_message — role/content semantics,
|
|
37
|
+
# conversation chaining). v4: chat and MCP messages stop bypassing the
|
|
38
|
+
# pipeline, so they carry provenance and fire the hook lifecycle like every
|
|
39
|
+
# other source.
|
|
40
|
+
CHAT_SOURCE_TYPES = frozenset({"chat_message", "mcp_message"})
|
|
41
|
+
# Typed memory records (read via ingest_event → Decision/Experience/Event
|
|
42
|
+
# nodes). The Memory System writes through the same door as everything else.
|
|
43
|
+
MEMORY_SOURCE_TYPES = frozenset({"decision", "experience", "workspace_event"})
|
|
44
|
+
_MEMORY_NODE_TYPES = {"decision": "Decision", "experience": "Experience", "workspace_event": "Event"}
|
|
45
|
+
|
|
46
|
+
DEFAULT_MAX_TEXT_BYTES = 5 * 1024 * 1024 # 5 MB of extracted text per item
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _now_iso() -> str:
|
|
50
|
+
return datetime.now(timezone.utc).isoformat()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class IngestionItem:
|
|
55
|
+
"""A single thing to ingest, normalized across every source type."""
|
|
56
|
+
|
|
57
|
+
source_type: str
|
|
58
|
+
title: Optional[str] = None
|
|
59
|
+
text: Optional[str] = None # text/web sources
|
|
60
|
+
path: Optional[str] = None # file sources
|
|
61
|
+
source_uri: Optional[str] = None
|
|
62
|
+
mime_type: Optional[str] = None
|
|
63
|
+
owner: Optional[str] = None
|
|
64
|
+
workspace_id: Optional[str] = None
|
|
65
|
+
permissions: Optional[Dict[str, Any]] = None
|
|
66
|
+
captured_at: Optional[str] = None
|
|
67
|
+
modified_at: Optional[str] = None
|
|
68
|
+
conversation_id: Optional[str] = None
|
|
69
|
+
agent_used: Optional[str] = None
|
|
70
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class IngestionResult:
|
|
75
|
+
"""The outcome of one ingestion, including provenance and idempotency."""
|
|
76
|
+
|
|
77
|
+
status: str # ok | unavailable | blocked | failed
|
|
78
|
+
source_type: str
|
|
79
|
+
node_id: Optional[str] = None
|
|
80
|
+
source_node_id: Optional[str] = None
|
|
81
|
+
content_hash: Optional[str] = None
|
|
82
|
+
title: Optional[str] = None
|
|
83
|
+
chunk_ids: List[str] = field(default_factory=list)
|
|
84
|
+
chunk_count: int = 0
|
|
85
|
+
duplicate: bool = False
|
|
86
|
+
embedded: bool = False
|
|
87
|
+
indexing_status: str = "pending" # indexed | skipped | failed | pending
|
|
88
|
+
provenance_id: Optional[str] = None
|
|
89
|
+
detail: Optional[str] = None
|
|
90
|
+
|
|
91
|
+
def as_dict(self) -> Dict[str, Any]:
|
|
92
|
+
return {
|
|
93
|
+
"status": self.status,
|
|
94
|
+
"source_type": self.source_type,
|
|
95
|
+
"node_id": self.node_id,
|
|
96
|
+
"source_node_id": self.source_node_id,
|
|
97
|
+
"content_hash": self.content_hash,
|
|
98
|
+
"title": self.title,
|
|
99
|
+
"chunk_ids": self.chunk_ids,
|
|
100
|
+
"chunk_count": self.chunk_count,
|
|
101
|
+
"duplicate": self.duplicate,
|
|
102
|
+
"embedded": self.embedded,
|
|
103
|
+
"indexing_status": self.indexing_status,
|
|
104
|
+
"provenance_id": self.provenance_id,
|
|
105
|
+
"detail": self.detail,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class IngestionPipeline:
|
|
110
|
+
"""Single normalized entrypoint that feeds every source into the graph."""
|
|
111
|
+
|
|
112
|
+
def __init__(
|
|
113
|
+
self,
|
|
114
|
+
knowledge_graph: Any,
|
|
115
|
+
*,
|
|
116
|
+
hooks: Any = None,
|
|
117
|
+
enable_graph: bool = True,
|
|
118
|
+
audit: Optional[Any] = None,
|
|
119
|
+
max_text_bytes: int = DEFAULT_MAX_TEXT_BYTES,
|
|
120
|
+
pipeline_name: str = "unified-ingestion",
|
|
121
|
+
) -> None:
|
|
122
|
+
self._kg = knowledge_graph
|
|
123
|
+
self._hooks = hooks
|
|
124
|
+
self._enable = bool(enable_graph)
|
|
125
|
+
self._audit = audit
|
|
126
|
+
self._max_text_bytes = int(max_text_bytes)
|
|
127
|
+
self._pipeline_name = pipeline_name
|
|
128
|
+
|
|
129
|
+
def available(self) -> bool:
|
|
130
|
+
return self._enable and self._kg is not None
|
|
131
|
+
|
|
132
|
+
# ── public API ───────────────────────────────────────────────────────────
|
|
133
|
+
def ingest(self, item: IngestionItem, *, user_email: Optional[str] = None) -> IngestionResult:
|
|
134
|
+
"""Normalize, hash, route through dispatch_tool, and record provenance."""
|
|
135
|
+
source_type = str(item.source_type or "text").strip()
|
|
136
|
+
if not self.available():
|
|
137
|
+
return IngestionResult(
|
|
138
|
+
status="unavailable", source_type=source_type,
|
|
139
|
+
indexing_status="skipped",
|
|
140
|
+
detail="Knowledge Graph is disabled (LATTICEAI_ENABLE_GRAPH).",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
captured_at = item.captured_at or _now_iso()
|
|
144
|
+
owner = item.owner or user_email
|
|
145
|
+
tool_name = f"kg_ingest.{source_type}"
|
|
146
|
+
# Only the keys are read by the hook payload, so this dict is safe/cheap.
|
|
147
|
+
args = {
|
|
148
|
+
"source_type": source_type,
|
|
149
|
+
"source_uri": item.source_uri,
|
|
150
|
+
"owner": owner,
|
|
151
|
+
"workspace_id": item.workspace_id,
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
def _run() -> Dict[str, Any]:
|
|
155
|
+
if source_type in CHAT_SOURCE_TYPES:
|
|
156
|
+
return self._ingest_chat(item, source_type=source_type, owner=owner)
|
|
157
|
+
if source_type in MEMORY_SOURCE_TYPES:
|
|
158
|
+
return self._ingest_memory_record(item, source_type=source_type, owner=owner)
|
|
159
|
+
if source_type in FILE_SOURCE_TYPES or (item.path and not item.text):
|
|
160
|
+
return self._ingest_file(item, source_type=source_type, owner=owner, captured_at=captured_at)
|
|
161
|
+
return self._ingest_text(item, source_type=source_type, owner=owner, captured_at=captured_at)
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
raw = dispatch_tool(
|
|
165
|
+
self._hooks, tool_name, args, _run,
|
|
166
|
+
user_email=user_email, workspace_id=item.workspace_id, source="ingestion",
|
|
167
|
+
)
|
|
168
|
+
except PermissionError as exc:
|
|
169
|
+
return IngestionResult(
|
|
170
|
+
status="blocked", source_type=source_type,
|
|
171
|
+
indexing_status="skipped", detail=str(exc),
|
|
172
|
+
)
|
|
173
|
+
except FileNotFoundError as exc:
|
|
174
|
+
return IngestionResult(
|
|
175
|
+
status="failed", source_type=source_type,
|
|
176
|
+
indexing_status="failed", detail=str(exc),
|
|
177
|
+
)
|
|
178
|
+
except Exception as exc: # noqa: BLE001 — surface as a failed result, never crash the caller
|
|
179
|
+
return IngestionResult(
|
|
180
|
+
status="failed", source_type=source_type,
|
|
181
|
+
indexing_status="failed", detail=str(exc),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
node_id = raw.get("node_id")
|
|
185
|
+
content_hash = raw.get("content_hash") or raw.get("sha256")
|
|
186
|
+
chunk_ids = list(raw.get("chunk_ids") or [])
|
|
187
|
+
embedded = bool(self._kg.node_is_embedded(node_id)) if node_id else False
|
|
188
|
+
title = raw.get("title") or item.title
|
|
189
|
+
|
|
190
|
+
prov = self._kg.record_provenance(
|
|
191
|
+
node_id=node_id,
|
|
192
|
+
source_type=source_type,
|
|
193
|
+
pipeline=self._pipeline_name,
|
|
194
|
+
source_uri=item.source_uri,
|
|
195
|
+
content_hash=content_hash,
|
|
196
|
+
title=title,
|
|
197
|
+
owner=owner,
|
|
198
|
+
workspace_id=item.workspace_id,
|
|
199
|
+
captured_at=captured_at,
|
|
200
|
+
modified_at=item.modified_at,
|
|
201
|
+
embedded=embedded,
|
|
202
|
+
linked=bool(raw.get("source_node_id")),
|
|
203
|
+
duplicate=bool(raw.get("duplicate")),
|
|
204
|
+
agent_used=item.agent_used,
|
|
205
|
+
chunk_count=len(chunk_ids),
|
|
206
|
+
permissions=item.permissions,
|
|
207
|
+
metadata=item.metadata,
|
|
208
|
+
)
|
|
209
|
+
if self._audit is not None:
|
|
210
|
+
try:
|
|
211
|
+
self._audit(
|
|
212
|
+
"kg_ingest",
|
|
213
|
+
{
|
|
214
|
+
"source_type": source_type, "node_id": node_id,
|
|
215
|
+
"content_hash": content_hash, "duplicate": bool(raw.get("duplicate")),
|
|
216
|
+
},
|
|
217
|
+
user_email,
|
|
218
|
+
)
|
|
219
|
+
except Exception: # noqa: BLE001 — audit must never break ingestion
|
|
220
|
+
pass
|
|
221
|
+
|
|
222
|
+
return IngestionResult(
|
|
223
|
+
status="ok",
|
|
224
|
+
source_type=source_type,
|
|
225
|
+
node_id=node_id,
|
|
226
|
+
source_node_id=raw.get("source_node_id"),
|
|
227
|
+
content_hash=content_hash,
|
|
228
|
+
title=title,
|
|
229
|
+
chunk_ids=chunk_ids,
|
|
230
|
+
chunk_count=len(chunk_ids),
|
|
231
|
+
duplicate=bool(raw.get("duplicate")),
|
|
232
|
+
embedded=embedded,
|
|
233
|
+
indexing_status="indexed",
|
|
234
|
+
provenance_id=prov.get("id"),
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# ── routing helpers ──────────────────────────────────────────────────────
|
|
238
|
+
def _ingest_text(self, item, *, source_type, owner, captured_at) -> Dict[str, Any]:
|
|
239
|
+
text = item.text or ""
|
|
240
|
+
if len(text.encode("utf-8", "ignore")) > self._max_text_bytes:
|
|
241
|
+
raise ValueError(
|
|
242
|
+
f"Text payload exceeds the {self._max_text_bytes // (1024 * 1024)}MB ingestion limit."
|
|
243
|
+
)
|
|
244
|
+
title = item.title or item.source_uri or source_type
|
|
245
|
+
return self._kg.ingest_source(
|
|
246
|
+
source_type=source_type,
|
|
247
|
+
title=title,
|
|
248
|
+
text=text,
|
|
249
|
+
source_uri=item.source_uri,
|
|
250
|
+
owner=owner,
|
|
251
|
+
workspace_id=item.workspace_id,
|
|
252
|
+
permissions=item.permissions,
|
|
253
|
+
captured_at=captured_at,
|
|
254
|
+
modified_at=item.modified_at,
|
|
255
|
+
conversation_id=item.conversation_id,
|
|
256
|
+
metadata={"mime_type": item.mime_type, **(item.metadata or {})},
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
def _ingest_chat(self, item, *, source_type, owner) -> Dict[str, Any]:
|
|
260
|
+
text = item.text or ""
|
|
261
|
+
meta = item.metadata or {}
|
|
262
|
+
role = str(meta.get("role") or "user")
|
|
263
|
+
result = self._kg.ingest_message(
|
|
264
|
+
role,
|
|
265
|
+
text,
|
|
266
|
+
user_email=owner,
|
|
267
|
+
user_nickname=meta.get("user_nickname"),
|
|
268
|
+
source=meta.get("source") or source_type,
|
|
269
|
+
conversation_id=item.conversation_id,
|
|
270
|
+
raw=meta.get("raw"),
|
|
271
|
+
)
|
|
272
|
+
# ingest_message reports message/response node ids; normalize the keys
|
|
273
|
+
# the provenance step expects.
|
|
274
|
+
result.setdefault("node_id", result.get("node_id") or result.get("message_node_id") or result.get("id"))
|
|
275
|
+
result.setdefault("title", item.title or text[:80])
|
|
276
|
+
return result
|
|
277
|
+
|
|
278
|
+
def _ingest_memory_record(self, item, *, source_type, owner) -> Dict[str, Any]:
|
|
279
|
+
node_type = _MEMORY_NODE_TYPES[source_type]
|
|
280
|
+
meta = item.metadata or {}
|
|
281
|
+
result = self._kg.ingest_event(
|
|
282
|
+
node_type,
|
|
283
|
+
item.title or (item.text or node_type)[:120],
|
|
284
|
+
user_email=owner,
|
|
285
|
+
source=meta.get("source") or source_type,
|
|
286
|
+
conversation_id=item.conversation_id,
|
|
287
|
+
metadata={**meta, "detail": (item.text or "")[:2000]},
|
|
288
|
+
)
|
|
289
|
+
result.setdefault("node_id", result.get("node_id") or result.get("id"))
|
|
290
|
+
result.setdefault("title", item.title)
|
|
291
|
+
return result
|
|
292
|
+
|
|
293
|
+
def _ingest_file(self, item, *, source_type, owner, captured_at) -> Dict[str, Any]:
|
|
294
|
+
if not item.path:
|
|
295
|
+
raise ValueError("File ingestion requires a path.")
|
|
296
|
+
path = Path(item.path)
|
|
297
|
+
if not path.exists():
|
|
298
|
+
raise FileNotFoundError(f"File not found: {path}")
|
|
299
|
+
return self._kg.ingest_document(
|
|
300
|
+
path,
|
|
301
|
+
original_filename=item.title or path.name,
|
|
302
|
+
mime_type=item.mime_type,
|
|
303
|
+
uploader=owner,
|
|
304
|
+
conversation_id=item.conversation_id,
|
|
305
|
+
extracted=item.metadata.get("extracted") if item.metadata else None,
|
|
306
|
+
source_type=source_type,
|
|
307
|
+
source_uri=item.source_uri or str(path),
|
|
308
|
+
captured_at=captured_at,
|
|
309
|
+
modified_at=item.modified_at,
|
|
310
|
+
owner=owner,
|
|
311
|
+
workspace_id=item.workspace_id,
|
|
312
|
+
permissions=item.permissions,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def content_hash_text(text: str) -> str:
|
|
317
|
+
"""Canonical content hash for a text payload (matches store hashing scheme)."""
|
|
318
|
+
return hashlib.sha256((text or "").encode("utf-8", "ignore")).hexdigest()
|
package/lattice_brain/memory.py
CHANGED
|
@@ -1,3 +1,102 @@
|
|
|
1
|
-
|
|
1
|
+
"""Memory System — typed, durable memory records on the brain substrate.
|
|
2
|
+
|
|
3
|
+
Decision and Experience records become first-class graph nodes through the
|
|
4
|
+
unified ingestion pipeline (provenance + hooks), instead of markdown dumps
|
|
5
|
+
with swallowed errors. Episodic memory is the conversation store; semantic
|
|
6
|
+
memory is the workspace MEMORY_KINDS records — this module adds the typed
|
|
7
|
+
record kinds the schema always had but never populated.
|
|
8
|
+
|
|
9
|
+
Only REAL events become memories: simulation runs are rejected at this
|
|
10
|
+
boundary (the run record's own mode field is checked — fabricated artifacts
|
|
11
|
+
must never enter the brain as experience).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Any, Dict, Optional
|
|
17
|
+
|
|
18
|
+
from .ingestion import IngestionItem
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BrainMemory:
|
|
22
|
+
"""Writes Decision / Experience records through the ingestion pipeline."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, ingestion_pipeline: Any):
|
|
25
|
+
self._pipeline = ingestion_pipeline
|
|
26
|
+
|
|
27
|
+
def available(self) -> bool:
|
|
28
|
+
return self._pipeline is not None and self._pipeline.available()
|
|
29
|
+
|
|
30
|
+
def record_decision(
|
|
31
|
+
self,
|
|
32
|
+
title: str,
|
|
33
|
+
detail: str = "",
|
|
34
|
+
*,
|
|
35
|
+
user_email: Optional[str] = None,
|
|
36
|
+
workspace_id: Optional[str] = None,
|
|
37
|
+
conversation_id: Optional[str] = None,
|
|
38
|
+
decided_by: Optional[str] = None,
|
|
39
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
40
|
+
) -> Dict[str, Any]:
|
|
41
|
+
if not str(title or "").strip():
|
|
42
|
+
raise ValueError("a decision needs a title")
|
|
43
|
+
result = self._pipeline.ingest(
|
|
44
|
+
IngestionItem(
|
|
45
|
+
source_type="decision",
|
|
46
|
+
title=title.strip(),
|
|
47
|
+
text=detail,
|
|
48
|
+
owner=user_email,
|
|
49
|
+
workspace_id=workspace_id,
|
|
50
|
+
conversation_id=conversation_id,
|
|
51
|
+
metadata={"decided_by": decided_by or user_email, **(metadata or {})},
|
|
52
|
+
),
|
|
53
|
+
user_email=user_email,
|
|
54
|
+
)
|
|
55
|
+
return result.as_dict()
|
|
56
|
+
|
|
57
|
+
def record_experience(
|
|
58
|
+
self,
|
|
59
|
+
title: str,
|
|
60
|
+
detail: str = "",
|
|
61
|
+
*,
|
|
62
|
+
run: Optional[Dict[str, Any]] = None,
|
|
63
|
+
user_email: Optional[str] = None,
|
|
64
|
+
workspace_id: Optional[str] = None,
|
|
65
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
66
|
+
) -> Dict[str, Any]:
|
|
67
|
+
"""Persist a completed run/action as an Experience node.
|
|
68
|
+
|
|
69
|
+
``run`` is the persisted run record; simulated runs are refused —
|
|
70
|
+
a simulation is replay scaffolding, not something that happened.
|
|
71
|
+
"""
|
|
72
|
+
if run is not None and run.get("mode", "simulation") == "simulation":
|
|
73
|
+
return {
|
|
74
|
+
"status": "rejected",
|
|
75
|
+
"detail": "simulation runs are not experiences and never enter the brain",
|
|
76
|
+
}
|
|
77
|
+
if not str(title or "").strip():
|
|
78
|
+
raise ValueError("an experience needs a title")
|
|
79
|
+
run_meta = {}
|
|
80
|
+
if run is not None:
|
|
81
|
+
run_meta = {
|
|
82
|
+
"run_id": run.get("id"),
|
|
83
|
+
"agent_id": run.get("agent_id"),
|
|
84
|
+
"run_status": run.get("status"),
|
|
85
|
+
"mode": run.get("mode"),
|
|
86
|
+
"retries": run.get("retries"),
|
|
87
|
+
}
|
|
88
|
+
result = self._pipeline.ingest(
|
|
89
|
+
IngestionItem(
|
|
90
|
+
source_type="experience",
|
|
91
|
+
title=title.strip(),
|
|
92
|
+
text=detail,
|
|
93
|
+
owner=user_email,
|
|
94
|
+
workspace_id=workspace_id,
|
|
95
|
+
metadata={**run_meta, **(metadata or {})},
|
|
96
|
+
),
|
|
97
|
+
user_email=user_email,
|
|
98
|
+
)
|
|
99
|
+
return result.as_dict()
|
|
100
|
+
|
|
2
101
|
|
|
3
102
|
__all__ = ["BrainMemory"]
|
package/lattice_brain/network.py
CHANGED
|
@@ -1 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
"""Compatibility shim: implementation moved to lattice_brain.graph.network.
|
|
2
|
+
|
|
3
|
+
This module aliases itself to the physical module so identity, singletons,
|
|
4
|
+
and monkeypatching behave as if the old flat path were the real module.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from .graph import network as _impl
|
|
10
|
+
|
|
11
|
+
sys.modules[__name__] = _impl
|