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.
Files changed (131) hide show
  1. package/README.md +191 -278
  2. package/docs/CHANGELOG.md +128 -0
  3. package/docs/V4_3_2_DEADCODE_AUDIT_REPORT.md +174 -0
  4. package/docs/V4_3_2_DOCUMENTATION_CLEANUP_REPORT.md +81 -0
  5. package/docs/V4_3_2_GITHUB_VERCEL_CHECK_REPORT.md +75 -0
  6. package/docs/V4_3_2_GRAPH_UX_REPORT.md +48 -0
  7. package/docs/V4_3_2_INDEPENDENT_AUDIT_PACKAGE.md +209 -0
  8. package/docs/V4_3_2_PRODUCT_POLISH_REPORT.md +57 -0
  9. package/docs/V4_3_2_SELF_AUDIT_REPORT.md +63 -0
  10. package/docs/V4_3_2_VALIDATION_REPORT.md +97 -0
  11. package/docs/V4_3_3_VALIDATION_REPORT.md +46 -0
  12. package/docs/V4_4_0_EXTRACTION_REPORT.md +239 -0
  13. package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +18 -19
  14. package/frontend/openapi.json +1 -1
  15. package/frontend/src/components/primitives.tsx +92 -10
  16. package/frontend/src/pages/Act.tsx +11 -9
  17. package/frontend/src/pages/Ask.tsx +2 -2
  18. package/frontend/src/pages/Brain.tsx +607 -65
  19. package/frontend/src/pages/Capture.tsx +11 -7
  20. package/frontend/src/pages/Library.tsx +3 -3
  21. package/frontend/src/pages/System.tsx +186 -23
  22. package/lattice_brain/__init__.py +38 -23
  23. package/lattice_brain/_kg_common.py +11 -1
  24. package/lattice_brain/context.py +212 -2
  25. package/lattice_brain/conversations.py +234 -1
  26. package/lattice_brain/discovery.py +11 -1
  27. package/lattice_brain/documents.py +11 -1
  28. package/lattice_brain/graph/__init__.py +28 -0
  29. package/lattice_brain/graph/_kg_common.py +1123 -0
  30. package/lattice_brain/graph/curator.py +473 -0
  31. package/lattice_brain/graph/discovery.py +1455 -0
  32. package/lattice_brain/graph/documents.py +218 -0
  33. package/lattice_brain/graph/identity.py +175 -0
  34. package/lattice_brain/graph/ingest.py +644 -0
  35. package/lattice_brain/graph/network.py +205 -0
  36. package/lattice_brain/graph/projection.py +571 -0
  37. package/lattice_brain/graph/provenance.py +401 -0
  38. package/lattice_brain/graph/retrieval.py +1341 -0
  39. package/lattice_brain/graph/schema.py +640 -0
  40. package/lattice_brain/graph/store.py +237 -0
  41. package/lattice_brain/graph/write_master.py +225 -0
  42. package/lattice_brain/identity.py +11 -13
  43. package/lattice_brain/ingest.py +11 -1
  44. package/lattice_brain/ingestion.py +318 -0
  45. package/lattice_brain/memory.py +100 -1
  46. package/lattice_brain/network.py +11 -1
  47. package/lattice_brain/portability.py +431 -0
  48. package/lattice_brain/projection.py +11 -1
  49. package/lattice_brain/provenance.py +11 -1
  50. package/lattice_brain/retrieval.py +11 -1
  51. package/lattice_brain/runtime/__init__.py +32 -0
  52. package/lattice_brain/runtime/agent_runtime.py +569 -0
  53. package/lattice_brain/runtime/hooks.py +754 -0
  54. package/lattice_brain/runtime/multi_agent.py +795 -0
  55. package/lattice_brain/schema.py +11 -1
  56. package/lattice_brain/store.py +10 -2
  57. package/lattice_brain/workflow.py +461 -0
  58. package/lattice_brain/write_master.py +11 -1
  59. package/latticeai/__init__.py +1 -1
  60. package/latticeai/api/agents.py +2 -2
  61. package/latticeai/api/browser.py +1 -1
  62. package/latticeai/api/chat.py +1 -1
  63. package/latticeai/api/computer_use.py +1 -1
  64. package/latticeai/api/hooks.py +2 -2
  65. package/latticeai/api/mcp.py +1 -1
  66. package/latticeai/api/tools.py +1 -1
  67. package/latticeai/api/workflow_designer.py +2 -2
  68. package/latticeai/app_factory.py +4 -4
  69. package/latticeai/brain/__init__.py +24 -6
  70. package/latticeai/brain/_kg_common.py +11 -1117
  71. package/latticeai/brain/context.py +12 -208
  72. package/latticeai/brain/conversations.py +12 -231
  73. package/latticeai/brain/discovery.py +13 -1451
  74. package/latticeai/brain/documents.py +13 -214
  75. package/latticeai/brain/identity.py +11 -169
  76. package/latticeai/brain/ingest.py +13 -640
  77. package/latticeai/brain/memory.py +12 -97
  78. package/latticeai/brain/network.py +12 -200
  79. package/latticeai/brain/projection.py +13 -567
  80. package/latticeai/brain/provenance.py +13 -397
  81. package/latticeai/brain/retrieval.py +13 -1337
  82. package/latticeai/brain/schema.py +12 -635
  83. package/latticeai/brain/store.py +13 -233
  84. package/latticeai/brain/write_master.py +13 -221
  85. package/latticeai/core/agent.py +1 -1
  86. package/latticeai/core/agent_registry.py +2 -2
  87. package/latticeai/core/builtin_hooks.py +2 -2
  88. package/latticeai/core/graph_curator.py +6 -468
  89. package/latticeai/core/hooks.py +6 -749
  90. package/latticeai/core/marketplace.py +1 -1
  91. package/latticeai/core/multi_agent.py +6 -790
  92. package/latticeai/core/workflow_engine.py +6 -456
  93. package/latticeai/core/workspace_os.py +1 -1
  94. package/latticeai/services/agent_runtime.py +6 -564
  95. package/latticeai/services/ingestion.py +6 -313
  96. package/latticeai/services/kg_portability.py +6 -426
  97. package/latticeai/services/platform_runtime.py +3 -3
  98. package/latticeai/services/run_executor.py +1 -1
  99. package/latticeai/services/upload_service.py +1 -1
  100. package/p_reinforce.py +1 -1
  101. package/package.json +3 -6
  102. package/scripts/build_vercel_static.mjs +77 -0
  103. package/scripts/bump_version.py +1 -1
  104. package/scripts/check_markdown_links.mjs +75 -0
  105. package/scripts/wheel_smoke.py +7 -0
  106. package/src-tauri/Cargo.lock +1 -1
  107. package/src-tauri/Cargo.toml +1 -1
  108. package/src-tauri/src/main.rs +12 -2
  109. package/src-tauri/tauri.conf.json +1 -1
  110. package/static/app/asset-manifest.json +5 -5
  111. package/static/app/assets/index-CHHal8Zl.css +2 -0
  112. package/static/app/assets/index-pdzil9ac.js +333 -0
  113. package/static/app/assets/index-pdzil9ac.js.map +1 -0
  114. package/static/app/index.html +2 -2
  115. package/latticeai/api/deps.py +0 -15
  116. package/scripts/capture/README.md +0 -28
  117. package/scripts/capture/capture_enterprise.js +0 -8
  118. package/scripts/capture/capture_graph.js +0 -8
  119. package/scripts/capture/capture_onboarding.js +0 -8
  120. package/scripts/capture/capture_page.js +0 -43
  121. package/scripts/capture/capture_release_media.js +0 -125
  122. package/scripts/capture/capture_skills.js +0 -8
  123. package/scripts/capture/capture_v340.js +0 -88
  124. package/scripts/capture/capture_workspace.js +0 -8
  125. package/scripts/generate_diagrams.py +0 -512
  126. package/scripts/release-0.3.1.sh +0 -105
  127. package/scripts/take_screenshots.js +0 -69
  128. package/static/app/assets/index-BhPuj8rT.js +0 -333
  129. package/static/app/assets/index-BhPuj8rT.js.map +0 -1
  130. package/static/app/assets/index-yZswHE3d.css +0 -2
  131. package/static/css/tokens.3ba22e37.css +0 -260
@@ -1,3 +1,213 @@
1
- from latticeai.brain.context import AssembledContext, ContextAssembler, ContextSection, approx_tokens
1
+ """Context System one budgeted, provenance-carrying assembly pipeline.
2
2
 
3
- __all__ = ["AssembledContext", "ContextAssembler", "ContextSection", "approx_tokens"]
3
+ Replaces the ad-hoc string concatenation that built chat context (language
4
+ hint + vault scan + KG LIKE search + recent chat appended in fixed order
5
+ with no size control). Every section the assembler emits records WHY it is
6
+ in the prompt (source, ids, scores), so "why is this in my context?" is
7
+ answerable, and the whole assembly respects a token budget.
8
+
9
+ Token counts are an explicit approximation: ``approx_tokens = ceil(len/4)``
10
+ (the stack ships no model-agnostic tokenizer; the field name says so —
11
+ design-review amendment T5).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import logging
17
+ from dataclasses import dataclass, field
18
+ from typing import Any, Callable, Dict, List, Optional
19
+
20
+
21
+ def approx_tokens(text: str) -> int:
22
+ """Documented chars/4 approximation — NOT a real tokenizer count."""
23
+ return max(0, (len(text or "") + 3) // 4)
24
+
25
+
26
+ @dataclass
27
+ class ContextSection:
28
+ name: str
29
+ content: str
30
+ source: str # memory | knowledge | notes | recent_chat | system
31
+ provenance: List[Dict[str, Any]] = field(default_factory=list)
32
+ truncated: bool = False
33
+
34
+ @property
35
+ def approx_tokens(self) -> int:
36
+ return approx_tokens(self.content)
37
+
38
+ def as_trace(self) -> Dict[str, Any]:
39
+ return {
40
+ "name": self.name,
41
+ "source": self.source,
42
+ "approx_tokens": self.approx_tokens,
43
+ "truncated": self.truncated,
44
+ "provenance": self.provenance,
45
+ }
46
+
47
+
48
+ @dataclass
49
+ class AssembledContext:
50
+ sections: List[ContextSection]
51
+ budget_approx_tokens: int
52
+
53
+ @property
54
+ def text(self) -> str:
55
+ parts = []
56
+ for section in self.sections:
57
+ if section.content.strip():
58
+ parts.append(f"[{section.name}]\n{section.content.strip()}")
59
+ return "\n\n".join(parts)
60
+
61
+ @property
62
+ def approx_tokens(self) -> int:
63
+ return sum(s.approx_tokens for s in self.sections)
64
+
65
+ def trace(self) -> Dict[str, Any]:
66
+ return {
67
+ "budget_approx_tokens": self.budget_approx_tokens,
68
+ "used_approx_tokens": self.approx_tokens,
69
+ "sections": [s.as_trace() for s in self.sections],
70
+ }
71
+
72
+
73
+ class ContextAssembler:
74
+ """Ordered, budgeted context assembly over injected retrieval seams.
75
+
76
+ Section priority (kept under budget in this order — semantic memories
77
+ first because they are cheap and durable; recency last):
78
+ 1. memories — workspace semantic memories (preferences/decisions/…)
79
+ 2. knowledge — hybrid search over the brain (the product's own engine)
80
+ 3. notes — garden-note context
81
+ 4. recent — the user's recent exchange
82
+ Every seam is optional; an absent seam contributes nothing (honest
83
+ absence), never a fabricated section.
84
+ """
85
+
86
+ def __init__(
87
+ self,
88
+ *,
89
+ memory_recall: Optional[Callable[..., Dict[str, Any]]] = None,
90
+ hybrid_search: Optional[Callable[..., Dict[str, Any]]] = None,
91
+ notes_context: Optional[Callable[..., str]] = None,
92
+ recent_chat: Optional[Callable[..., str]] = None,
93
+ ) -> None:
94
+ self._memory_recall = memory_recall
95
+ self._hybrid_search = hybrid_search
96
+ self._notes_context = notes_context
97
+ self._recent_chat = recent_chat
98
+
99
+ def assemble(
100
+ self,
101
+ query: str,
102
+ *,
103
+ user_email: Optional[str] = None,
104
+ workspace_id: Optional[str] = None,
105
+ conversation_id: Optional[str] = None,
106
+ budget: int = 2000,
107
+ memory_limit: int = 5,
108
+ knowledge_limit: int = 5,
109
+ ) -> AssembledContext:
110
+ sections: List[ContextSection] = []
111
+
112
+ if self._memory_recall is not None:
113
+ sections.append(self._memories_section(query, user_email, workspace_id, memory_limit))
114
+ if self._hybrid_search is not None:
115
+ sections.append(self._knowledge_section(query, knowledge_limit, user_email))
116
+ if self._notes_context is not None:
117
+ sections.append(self._notes_section(query))
118
+ if self._recent_chat is not None:
119
+ sections.append(self._recent_section(user_email, conversation_id))
120
+
121
+ sections = [s for s in sections if s.content.strip()]
122
+ self._apply_budget(sections, budget)
123
+ return AssembledContext(sections=sections, budget_approx_tokens=budget)
124
+
125
+ # ── section builders (each failure-isolated and honest) ───────────────
126
+ def _memories_section(self, query, user_email, workspace_id, limit) -> ContextSection:
127
+ try:
128
+ recall = self._memory_recall(query, user_email=user_email, workspace_id=workspace_id, limit=limit)
129
+ results = [r for r in recall.get("results", []) if r.get("source") == "workspace"][:limit]
130
+ except Exception as exc:
131
+ logging.debug("context: memory recall failed: %s", exc)
132
+ results = []
133
+ lines = [f"- ({r.get('kind') or 'memory'}) {r.get('snippet') or ''}" for r in results]
134
+ return ContextSection(
135
+ name="User memories",
136
+ content="\n".join(lines),
137
+ source="memory",
138
+ provenance=[
139
+ {"id": r.get("id"), "kind": r.get("kind"), "score": r.get("score")}
140
+ for r in results
141
+ ],
142
+ )
143
+
144
+ def _knowledge_section(self, query, limit, user_email=None) -> ContextSection:
145
+ try:
146
+ hybrid = self._hybrid_search(query, limit=limit, user_email=user_email)
147
+ matches = hybrid.get("matches", [])[:limit]
148
+ except Exception as exc:
149
+ logging.debug("context: hybrid search failed: %s", exc)
150
+ matches = []
151
+ lines = []
152
+ provenance = []
153
+ for m in matches:
154
+ title = m.get("title") or m.get("id") or "item"
155
+ body = (m.get("summary") or m.get("snippet") or "")[:400]
156
+ lines.append(f"- {title}: {body}" if body else f"- {title}")
157
+ provenance.append({
158
+ "id": m.get("id"),
159
+ "score": m.get("score"),
160
+ "sources": m.get("sources"),
161
+ })
162
+ return ContextSection(
163
+ name="Knowledge",
164
+ content="\n".join(lines),
165
+ source="knowledge",
166
+ provenance=provenance,
167
+ )
168
+
169
+ def _notes_section(self, query) -> ContextSection:
170
+ try:
171
+ content = self._notes_context(query) or ""
172
+ except Exception as exc:
173
+ logging.debug("context: notes context failed: %s", exc)
174
+ content = ""
175
+ return ContextSection(
176
+ name="Garden notes",
177
+ content=content,
178
+ source="notes",
179
+ provenance=[{"source": "garden", "included": bool(content)}],
180
+ )
181
+
182
+ def _recent_section(self, user_email, conversation_id) -> ContextSection:
183
+ try:
184
+ content = self._recent_chat(user_email=user_email, conversation_id=conversation_id) or ""
185
+ except Exception as exc:
186
+ logging.debug("context: recent chat failed: %s", exc)
187
+ content = ""
188
+ return ContextSection(
189
+ name="Recent conversation",
190
+ content=content,
191
+ source="recent_chat",
192
+ provenance=[{"conversation_id": conversation_id, "user_email": user_email}],
193
+ )
194
+
195
+ # ── budget ─────────────────────────────────────────────────────────────
196
+ @staticmethod
197
+ def _apply_budget(sections: List[ContextSection], budget: int) -> None:
198
+ """Trim from the END (lowest priority) until under budget."""
199
+ budget = max(1, int(budget))
200
+ used = 0
201
+ for section in sections:
202
+ remaining = budget - used
203
+ if remaining <= 0:
204
+ section.content = ""
205
+ section.truncated = True
206
+ continue
207
+ if section.approx_tokens > remaining:
208
+ section.content = section.content[: remaining * 4]
209
+ section.truncated = True
210
+ used += section.approx_tokens
211
+
212
+
213
+ __all__ = ["ContextAssembler", "ContextSection", "AssembledContext", "approx_tokens"]
@@ -1,3 +1,236 @@
1
- from latticeai.brain.conversations import ConversationStore
1
+ """Durable conversation store — kills the 50-message chat_history.json cap.
2
+
3
+ Conversations are episodic memory, the most valuable raw input a Digital
4
+ Brain has; truncating them contradicted "knowledge is durable". This store
5
+ keeps every message in SQLite — by default in the same database file as the
6
+ knowledge graph, so the existing kg_portability backup/restore covers
7
+ conversations with no manifest changes.
8
+
9
+ The public item shape is exactly the legacy chat_history.json entry
10
+ (role/content/timestamp + optional user_email/user_nickname/source/
11
+ conversation_id), so every existing consumer of ``get_history()`` keeps
12
+ working. Legacy history is imported once, idempotently (content-hash dedup).
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import hashlib
18
+ import json
19
+ import logging
20
+ import sqlite3
21
+ import threading
22
+ from pathlib import Path
23
+ from typing import Any, Dict, List, Optional
24
+
25
+
26
+ def _message_hash(item: Dict[str, Any]) -> str:
27
+ basis = "|".join(
28
+ str(item.get(key) or "")
29
+ for key in ("role", "content", "timestamp", "user_email", "conversation_id", "source")
30
+ )
31
+ return hashlib.sha256(basis.encode("utf-8", "ignore")).hexdigest()
32
+
33
+
34
+ class ConversationStore:
35
+ """Unbounded, per-conversation chat history in SQLite."""
36
+
37
+ def __init__(self, db_path: Path):
38
+ self.db_path = Path(db_path)
39
+ self.db_path.parent.mkdir(parents=True, exist_ok=True)
40
+ self._lock = threading.RLock()
41
+ self._init_db()
42
+
43
+ def _connect(self) -> sqlite3.Connection:
44
+ conn = sqlite3.connect(str(self.db_path))
45
+ conn.row_factory = sqlite3.Row
46
+ conn.execute("PRAGMA journal_mode=WAL")
47
+ return conn
48
+
49
+ def _init_db(self) -> None:
50
+ with self._lock, self._connect() as conn:
51
+ conn.executescript(
52
+ """
53
+ CREATE TABLE IF NOT EXISTS conversation_messages (
54
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
55
+ message_hash TEXT NOT NULL UNIQUE,
56
+ conversation_id TEXT,
57
+ role TEXT NOT NULL,
58
+ content TEXT NOT NULL,
59
+ user_email TEXT,
60
+ user_nickname TEXT,
61
+ source TEXT,
62
+ timestamp TEXT NOT NULL,
63
+ metadata_json TEXT NOT NULL DEFAULT '{}'
64
+ );
65
+ CREATE INDEX IF NOT EXISTS idx_conv_messages_conv
66
+ ON conversation_messages(conversation_id);
67
+ CREATE INDEX IF NOT EXISTS idx_conv_messages_time
68
+ ON conversation_messages(timestamp);
69
+ """
70
+ )
71
+
72
+ # ── writes ────────────────────────────────────────────────────────────
73
+ def append(self, item: Dict[str, Any]) -> Dict[str, Any]:
74
+ """Persist one chat item (the legacy chat_history.json entry shape)."""
75
+ known = {"role", "content", "timestamp", "user_email", "user_nickname", "source", "conversation_id"}
76
+ extra = {k: v for k, v in item.items() if k not in known}
77
+ with self._lock, self._connect() as conn:
78
+ conn.execute(
79
+ """
80
+ INSERT OR IGNORE INTO conversation_messages
81
+ (message_hash, conversation_id, role, content, user_email,
82
+ user_nickname, source, timestamp, metadata_json)
83
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
84
+ """,
85
+ (
86
+ _message_hash(item),
87
+ item.get("conversation_id"),
88
+ str(item.get("role") or "user"),
89
+ str(item.get("content") or ""),
90
+ item.get("user_email"),
91
+ item.get("user_nickname"),
92
+ item.get("source"),
93
+ str(item.get("timestamp") or ""),
94
+ json.dumps(extra, ensure_ascii=False) if extra else "{}",
95
+ ),
96
+ )
97
+ return item
98
+
99
+ def import_legacy_json(self, history_file: Path) -> int:
100
+ """One-time, idempotent import of a chat_history.json file.
101
+
102
+ Re-running never duplicates (message_hash UNIQUE + INSERT OR IGNORE);
103
+ the source file is left untouched on disk.
104
+ """
105
+ path = Path(history_file)
106
+ if not path.exists():
107
+ return 0
108
+ try:
109
+ with open(path, "r", encoding="utf-8") as fh:
110
+ items = json.load(fh)
111
+ except Exception as exc:
112
+ logging.warning("conversation store: legacy import failed to read %s: %s", path, exc)
113
+ return 0
114
+ if not isinstance(items, list):
115
+ return 0
116
+ imported = 0
117
+ with self._lock, self._connect() as conn:
118
+ for item in items:
119
+ if not isinstance(item, dict):
120
+ continue
121
+ cur = conn.execute(
122
+ """
123
+ INSERT OR IGNORE INTO conversation_messages
124
+ (message_hash, conversation_id, role, content, user_email,
125
+ user_nickname, source, timestamp, metadata_json)
126
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, '{}')
127
+ """,
128
+ (
129
+ _message_hash(item),
130
+ item.get("conversation_id"),
131
+ str(item.get("role") or "user"),
132
+ str(item.get("content") or ""),
133
+ item.get("user_email"),
134
+ item.get("user_nickname"),
135
+ item.get("source"),
136
+ str(item.get("timestamp") or ""),
137
+ ),
138
+ )
139
+ imported += cur.rowcount if cur.rowcount > 0 else 0
140
+ if imported:
141
+ logging.info("conversation store: imported %d legacy chat messages from %s", imported, path)
142
+ return imported
143
+
144
+ # ── reads (legacy item shape) ─────────────────────────────────────────
145
+ @staticmethod
146
+ def _row_to_item(row: sqlite3.Row) -> Dict[str, Any]:
147
+ item: Dict[str, Any] = {
148
+ "role": row["role"],
149
+ "content": row["content"],
150
+ "timestamp": row["timestamp"],
151
+ }
152
+ for key in ("user_email", "user_nickname", "source", "conversation_id"):
153
+ if row[key]:
154
+ item[key] = row[key]
155
+ try:
156
+ extra = json.loads(row["metadata_json"] or "{}")
157
+ except Exception:
158
+ extra = {}
159
+ item.update(extra)
160
+ return item
161
+
162
+ def history(self, *, conversation_id: Optional[str] = None, limit: Optional[int] = None) -> List[Dict[str, Any]]:
163
+ """Chronological items; the unbounded successor of get_history()."""
164
+ query = "SELECT * FROM conversation_messages"
165
+ params: List[Any] = []
166
+ if conversation_id is not None:
167
+ query += " WHERE conversation_id IS ?" if conversation_id == "" else " WHERE conversation_id = ?"
168
+ params.append(None if conversation_id == "" else conversation_id)
169
+ query += " ORDER BY id ASC"
170
+ if limit is not None:
171
+ query += " LIMIT ?"
172
+ params.append(max(1, int(limit)))
173
+ with self._connect() as conn:
174
+ rows = conn.execute(query, params).fetchall()
175
+ return [self._row_to_item(row) for row in rows]
176
+
177
+ def count(self) -> int:
178
+ with self._connect() as conn:
179
+ return conn.execute("SELECT COUNT(*) FROM conversation_messages").fetchone()[0]
180
+
181
+ def size_bytes(self) -> int:
182
+ try:
183
+ return self.db_path.stat().st_size if self.db_path.exists() else 0
184
+ except OSError:
185
+ return 0
186
+
187
+ # ── clears (legacy semantics preserved) ───────────────────────────────
188
+ def clear_all(self, keep_last: int = 0) -> Dict[str, Any]:
189
+ keep_last = max(0, min(int(keep_last or 0), 20))
190
+ with self._lock, self._connect() as conn:
191
+ total = conn.execute("SELECT COUNT(*) FROM conversation_messages").fetchone()[0]
192
+ if keep_last:
193
+ conn.execute(
194
+ """
195
+ DELETE FROM conversation_messages WHERE id NOT IN (
196
+ SELECT id FROM conversation_messages ORDER BY id DESC LIMIT ?
197
+ )
198
+ """,
199
+ (keep_last,),
200
+ )
201
+ else:
202
+ conn.execute("DELETE FROM conversation_messages")
203
+ kept = conn.execute("SELECT COUNT(*) FROM conversation_messages").fetchone()[0]
204
+ return {"status": "cleared", "removed": max(0, total - kept), "kept": kept}
205
+
206
+ def clear_conversation(self, conversation_id: str, started_at: Optional[str] = None) -> Dict[str, Any]:
207
+ """Remove one conversation.
208
+
209
+ ``legacy-previous-history`` targets unattributed messages; when
210
+ ``started_at`` is given, unattributed messages from that point on are
211
+ removed too (mirrors the pre-v4 JSON behaviour exactly).
212
+ """
213
+ with self._lock, self._connect() as conn:
214
+ total = conn.execute("SELECT COUNT(*) FROM conversation_messages").fetchone()[0]
215
+ if conversation_id == "legacy-previous-history":
216
+ conn.execute("DELETE FROM conversation_messages WHERE conversation_id IS NULL")
217
+ else:
218
+ conn.execute(
219
+ "DELETE FROM conversation_messages WHERE conversation_id = ?",
220
+ (conversation_id,),
221
+ )
222
+ if started_at:
223
+ conn.execute(
224
+ "DELETE FROM conversation_messages WHERE conversation_id IS NULL AND timestamp >= ?",
225
+ (str(started_at),),
226
+ )
227
+ kept = conn.execute("SELECT COUNT(*) FROM conversation_messages").fetchone()[0]
228
+ return {
229
+ "status": "cleared",
230
+ "conversation_id": conversation_id,
231
+ "removed": max(0, total - kept),
232
+ "kept": kept,
233
+ }
234
+
2
235
 
3
236
  __all__ = ["ConversationStore"]
@@ -1 +1,11 @@
1
- from latticeai.brain.discovery import * # noqa: F401,F403
1
+ """Compatibility shim: implementation moved to lattice_brain.graph.discovery.
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 discovery as _impl
10
+
11
+ sys.modules[__name__] = _impl
@@ -1 +1,11 @@
1
- from latticeai.brain.documents import * # noqa: F401,F403
1
+ """Compatibility shim: implementation moved to lattice_brain.graph.documents.
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 documents as _impl
10
+
11
+ sys.modules[__name__] = _impl
@@ -0,0 +1,28 @@
1
+ """Knowledge graph subsystem of the Brain Core.
2
+
3
+ Physically hosts the graph schema, store, mixins (write/retrieval/discovery/
4
+ documents/ingest/projection/provenance), device identity, brain network, and
5
+ the graph curator. Heavy modules are lazy-loaded so importing
6
+ ``lattice_brain.graph`` stays cheap.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ __all__ = [
12
+ "KnowledgeGraphStore",
13
+ "KGStoreV2",
14
+ "NodeType",
15
+ "EdgeType",
16
+ ]
17
+
18
+
19
+ def __getattr__(name: str):
20
+ if name == "KnowledgeGraphStore":
21
+ from .store import KnowledgeGraphStore
22
+
23
+ return KnowledgeGraphStore
24
+ if name in {"KGStoreV2", "NodeType", "EdgeType"}:
25
+ from . import schema
26
+
27
+ return getattr(schema, name)
28
+ raise AttributeError(name)