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
@@ -1,213 +1,17 @@
1
- """Context System one budgeted, provenance-carrying assembly pipeline.
1
+ """Deprecated shim: physically moved to lattice_brain.context.
2
2
 
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).
3
+ Kept only for the compatibility window. The module aliases itself to the
4
+ physical module so identity, singletons, and monkeypatching are preserved.
12
5
  """
13
6
 
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
7
+ import sys
8
+ import warnings
211
9
 
10
+ import lattice_brain.context as _impl
212
11
 
213
- __all__ = ["ContextAssembler", "ContextSection", "AssembledContext", "approx_tokens"]
12
+ warnings.warn(
13
+ "latticeai.brain.context is deprecated; import lattice_brain.context instead",
14
+ DeprecationWarning,
15
+ stacklevel=2,
16
+ )
17
+ sys.modules[__name__] = _impl
@@ -1,236 +1,17 @@
1
- """Durable conversation store kills the 50-message chat_history.json cap.
1
+ """Deprecated shim: physically moved to lattice_brain.conversations.
2
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).
3
+ Kept only for the compatibility window. The module aliases itself to the
4
+ physical module so identity, singletons, and monkeypatching are preserved.
13
5
  """
14
6
 
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
- }
7
+ import sys
8
+ import warnings
234
9
 
10
+ import lattice_brain.conversations as _impl
235
11
 
236
- __all__ = ["ConversationStore"]
12
+ warnings.warn(
13
+ "latticeai.brain.conversations is deprecated; import lattice_brain.conversations instead",
14
+ DeprecationWarning,
15
+ stacklevel=2,
16
+ )
17
+ sys.modules[__name__] = _impl