ltcai 10.9.0 → 11.0.1
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 +46 -64
- package/docs/CHANGELOG.md +72 -237
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +1 -1
- package/docs/PERFORMANCE.md +78 -7
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/kg-schema.md +1 -1
- package/docs/v11.1.0_PRODUCT_INTELLIGENCE_PLAN.md +313 -0
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/graph/_kg_contract.py +11 -0
- package/lattice_brain/graph/curator.py +1 -1
- package/lattice_brain/graph/fusion.py +184 -3
- package/lattice_brain/graph/proactive.py +138 -1
- package/lattice_brain/graph/projection.py +10 -2
- package/lattice_brain/graph/retrieval.py +137 -7
- package/lattice_brain/graph/retrieval_docgen.py +20 -20
- package/lattice_brain/graph/retrieval_policy.py +6 -0
- package/lattice_brain/graph/retrieval_reads.py +188 -1
- package/lattice_brain/graph/retrieval_vector.py +474 -110
- package/lattice_brain/graph/schema.py +125 -2
- package/lattice_brain/graph/vector_index/__init__.py +85 -0
- package/lattice_brain/graph/vector_index/base.py +170 -0
- package/lattice_brain/graph/vector_index/brute_force.py +114 -0
- package/lattice_brain/graph/vector_index/hnsw.py +293 -0
- package/lattice_brain/graph/vector_index/jobs.py +287 -0
- package/lattice_brain/graph/vector_index/quantized.py +151 -0
- package/lattice_brain/graph/vector_index/selector.py +131 -0
- package/lattice_brain/ingestion.py +50 -3
- package/lattice_brain/portability.py +654 -2
- package/lattice_brain/runtime/agent_runtime.py +1 -1
- package/lattice_brain/runtime/contracts.py +1 -1
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/lattice_brain/self_model.py +620 -0
- package/lattice_brain/synthesis.py +801 -0
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/brain_intelligence.py +83 -1
- package/latticeai/api/chat_stream.py +4 -1
- package/latticeai/api/local_files.py +62 -0
- package/latticeai/api/models.py +1 -1
- package/latticeai/api/portability.py +130 -1
- package/latticeai/api/security_dashboard.py +48 -13
- package/latticeai/api/voice_capture.py +4 -1
- package/latticeai/api/workspace.py +11 -5
- package/latticeai/core/embedding_providers.py +20 -1
- package/latticeai/core/legacy_compatibility.py +1 -1
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/messages.py +14 -0
- package/latticeai/core/model_compat.py +2 -2
- package/latticeai/core/tool_registry.py +0 -7
- package/latticeai/core/workspace_os_constants.py +1 -1
- package/latticeai/core/workspace_os_utils.py +4 -50
- package/latticeai/core/workspace_review_items.py +12 -1
- package/latticeai/integrations/telegram_bot.py +28 -10
- package/latticeai/models/router.py +1 -1
- package/latticeai/runtime/access_runtime.py +1 -1
- package/latticeai/runtime/network_boundary_wiring.py +9 -5
- package/latticeai/runtime/permission_mode_wiring.py +9 -6
- package/latticeai/runtime/router_registration.py +3 -0
- package/latticeai/services/architecture_readiness.py +1 -1
- package/latticeai/services/brain_intelligence.py +253 -0
- package/latticeai/services/memory_service.py +1 -1
- package/latticeai/services/model_catalog.py +4 -3
- package/latticeai/services/model_engines.py +28 -14
- package/latticeai/services/obsidian_bridge.py +618 -0
- package/latticeai/services/product_readiness.py +5 -3
- package/latticeai/tools/filesystem.py +4 -1
- package/package.json +1 -1
- package/scripts/bench_vector_index.py +295 -0
- package/scripts/check_current_release_docs.mjs +4 -2
- package/scripts/release_screen_claims.json +31 -0
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +37 -37
- package/static/app/assets/{Act-CS9IeqUX.js → Act-D4zSxFR-.js} +1 -1
- package/static/app/assets/{AdminConsole-3UkIEWGA.js → AdminConsole-w5jBfPt2.js} +1 -1
- package/static/app/assets/{Brain-B22EmNqS.js → Brain-C2EqQg74.js} +2 -2
- package/static/app/assets/BrainHome-CvXS6XiQ.js +2 -0
- package/static/app/assets/BrainSignals-DOE_KhOU.js +1 -0
- package/static/app/assets/Capture-DPqpGK8d.js +1 -0
- package/static/app/assets/{CommandPalette-86m4FCcN.js → CommandPalette-CNf7h5fp.js} +1 -1
- package/static/app/assets/Library-BN0HYOfc.js +1 -0
- package/static/app/assets/LivingBrain-Dfq_wEDI.js +1 -0
- package/static/app/assets/ProductFlow-B-3O0rNV.js +1 -0
- package/static/app/assets/{ReviewCard-BepjSDpN.js → ReviewCard-gZ-tdqFM.js} +1 -1
- package/static/app/assets/System-BElUcSSw.js +1 -0
- package/static/app/assets/arrow-left-CFNIMjhv.js +1 -0
- package/static/app/assets/{bot-DQj0-LkM.js → bot--qYHMtkP.js} +1 -1
- package/static/app/assets/brain-DDCLjRqO.js +1 -0
- package/static/app/assets/{button-CmTknyAP.js → button-51Z3rsuv.js} +1 -1
- package/static/app/assets/{circle-pause-yTCWRziJ.js → circle-pause-CMIiMaQl.js} +1 -1
- package/static/app/assets/{circle-play-Ccrva84R.js → circle-play-DZoO_cfG.js} +1 -1
- package/static/app/assets/{cpu-CbJqWTlS.js → cpu-Bs6uc9W9.js} +1 -1
- package/static/app/assets/{download-CkSzbzU-.js → download-G-2olkWz.js} +1 -1
- package/static/app/assets/{folder-open-CKyjQ4PU.js → folder-open-CTOspnmb.js} +1 -1
- package/static/app/assets/{hard-drive-DAzk9um0.js → hard-drive-CewHWJhn.js} +1 -1
- package/static/app/assets/index-CkzokZAj.css +2 -0
- package/static/app/assets/{index-CxOcwsHV.js → index-D7Rr-J2Y.js} +3 -3
- package/static/app/assets/{input-DcMETmZ7.js → input-D4w_BZWl.js} +1 -1
- package/static/app/assets/{permissionCopy-BVf13_25.js → permissionCopy-CosBEXAZ.js} +1 -1
- package/static/app/assets/primitives-d0g9pvzS.js +1 -0
- package/static/app/assets/search-BLCYt75v.js +1 -0
- package/static/app/assets/{share-2-COWCHNZm.js → share-2-NmD7e_oV.js} +1 -1
- package/static/app/assets/{shield-alert-BDrvilyK.js → shield-alert-CcQeMuju.js} +1 -1
- package/static/app/assets/{textarea-rUmsc8cP.js → textarea-BPAJDc-0.js} +1 -1
- package/static/app/assets/{useFocusTrap-Bi5UY_8v.js → useFocusTrap-C7YLdTBC.js} +1 -1
- package/static/app/assets/{useQuery-C-AicB-3.js → useQuery-DRyD9opW.js} +1 -1
- package/static/app/assets/{utils-BMwWg78e.js → utils-DG1_ExrP.js} +3 -3
- package/static/app/assets/{workspace-Y93tls8P.js → workspace-CWVf3gsI.js} +1 -1
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/static/app/assets/BrainHome-CMDqgJF4.js +0 -2
- package/static/app/assets/BrainSignals-BeE8RJo3.js +0 -1
- package/static/app/assets/Capture-ZX9bQh68.js +0 -1
- package/static/app/assets/Library-Bhz5LUca.js +0 -1
- package/static/app/assets/LivingBrain-BSa0wpFG.js +0 -1
- package/static/app/assets/ProductFlow-IP4Q-5aQ.js +0 -1
- package/static/app/assets/System-Bwx1h_jT.js +0 -1
- package/static/app/assets/arrow-left-ig7AZU8B.js +0 -1
- package/static/app/assets/brain-D8OEwmVj.js +0 -1
- package/static/app/assets/index-BfD-jhA9.css +0 -2
- package/static/app/assets/primitives-CQV9Q2YM.js +0 -1
- package/static/app/assets/search-CXGASMMH.js +0 -1
|
@@ -13,8 +13,11 @@ from __future__ import annotations
|
|
|
13
13
|
|
|
14
14
|
from typing import TYPE_CHECKING
|
|
15
15
|
|
|
16
|
+
from ..quiet import quiet
|
|
17
|
+
|
|
16
18
|
# ruff: noqa: F403,F405
|
|
17
19
|
from ._kg_common import * # noqa: F403,F401
|
|
20
|
+
from .schema import TEMPORAL_PREDICATE_SQL
|
|
18
21
|
|
|
19
22
|
# The cross-mixin surface (`_connect`, `_upsert_node`, …) is declared in
|
|
20
23
|
# `_kg_contract.KnowledgeGraphCore`. It is a typing-only base: at runtime this
|
|
@@ -25,6 +28,39 @@ else:
|
|
|
25
28
|
_Core = object
|
|
26
29
|
|
|
27
30
|
|
|
31
|
+
def _as_of_stamp(value: Any) -> str:
|
|
32
|
+
"""Normalize an ``as_of`` argument into the store's own timestamp format.
|
|
33
|
+
|
|
34
|
+
The store writes naive local ISO-8601 seconds (``lattice_brain.utils.now_iso``),
|
|
35
|
+
so validity comparisons are plain lexicographic string comparisons against
|
|
36
|
+
that one format. An aware datetime is converted to local time first rather
|
|
37
|
+
than compared with an offset suffix the stored rows never carry.
|
|
38
|
+
"""
|
|
39
|
+
if isinstance(value, datetime):
|
|
40
|
+
moment = value.astimezone().replace(tzinfo=None) if value.tzinfo else value
|
|
41
|
+
return moment.isoformat(timespec="seconds")
|
|
42
|
+
text = str(value or "").strip()
|
|
43
|
+
if not text:
|
|
44
|
+
raise ValueError("as_of requires a timestamp")
|
|
45
|
+
return text
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _record_access(db_path: Any, node_id: str) -> None:
|
|
49
|
+
"""Record one access on a node (the importance/decay input).
|
|
50
|
+
|
|
51
|
+
Runs on its own short transaction, deliberately *outside* the read that
|
|
52
|
+
triggered it: promoting every node read to a writer would let a concurrent
|
|
53
|
+
ingest's write lock fail an otherwise perfectly good read. The counter is a
|
|
54
|
+
nice-to-have, so every sqlite failure — a missing row, a missing column, a
|
|
55
|
+
busy database — is swallowed.
|
|
56
|
+
"""
|
|
57
|
+
if KGStoreV2 is None:
|
|
58
|
+
return
|
|
59
|
+
try:
|
|
60
|
+
KGStoreV2(db_path).touch_node(node_id)
|
|
61
|
+
except sqlite3.Error:
|
|
62
|
+
quiet()
|
|
63
|
+
|
|
28
64
|
|
|
29
65
|
class KnowledgeGraphReadsMixin(_Core):
|
|
30
66
|
def list_documents(self, limit: int = 200) -> Dict[str, Any]:
|
|
@@ -173,14 +209,22 @@ class KnowledgeGraphReadsMixin(_Core):
|
|
|
173
209
|
*,
|
|
174
210
|
allowed_workspaces=None,
|
|
175
211
|
include_legacy_global: bool = False,
|
|
212
|
+
as_of=None,
|
|
176
213
|
) -> Dict[str, Any]:
|
|
177
|
-
"""Return direct neighbors (1-hop) of a node.
|
|
214
|
+
"""Return direct neighbors (1-hop) of a node.
|
|
215
|
+
|
|
216
|
+
``as_of`` is additive and defaults to ``None`` — the historical call
|
|
217
|
+
keeps its exact behaviour. With a timestamp, neighbors whose validity
|
|
218
|
+
window does not cover that instant are dropped (and with them the edges
|
|
219
|
+
that would dangle).
|
|
220
|
+
"""
|
|
178
221
|
if allowed_workspaces is not None and not self.filter_scoped_nodes(
|
|
179
222
|
[{"id": node_id}],
|
|
180
223
|
allowed_workspaces,
|
|
181
224
|
include_legacy_global=include_legacy_global,
|
|
182
225
|
):
|
|
183
226
|
raise ValueError(f"graph node not found: {node_id}")
|
|
227
|
+
stamp = None if as_of is None else _as_of_stamp(as_of)
|
|
184
228
|
nt, et = self._read_tables()
|
|
185
229
|
with self._connect() as conn:
|
|
186
230
|
edge_rows = conn.execute(
|
|
@@ -217,6 +261,14 @@ class KnowledgeGraphReadsMixin(_Core):
|
|
|
217
261
|
list(neighbor_ids),
|
|
218
262
|
)
|
|
219
263
|
]
|
|
264
|
+
if stamp is not None:
|
|
265
|
+
valid = self._valid_node_ids_at(conn, stamp)
|
|
266
|
+
nodes = [node for node in nodes if str(node.get("id")) in valid]
|
|
267
|
+
kept = {str(node.get("id")) for node in nodes} | {node_id}
|
|
268
|
+
edges = [
|
|
269
|
+
edge for edge in edges
|
|
270
|
+
if edge.get("from") in kept and edge.get("to") in kept
|
|
271
|
+
]
|
|
220
272
|
if allowed_workspaces is not None:
|
|
221
273
|
nodes = self.filter_scoped_nodes(
|
|
222
274
|
nodes,
|
|
@@ -257,6 +309,10 @@ class KnowledgeGraphReadsMixin(_Core):
|
|
|
257
309
|
f"SELECT COUNT(*) AS c FROM {et} WHERE from_node=? OR to_node=?",
|
|
258
310
|
(node_id, node_id),
|
|
259
311
|
).fetchone()["c"]
|
|
312
|
+
# Fetching a node by id is the one unambiguous "this memory was used"
|
|
313
|
+
# signal the read path has; it feeds importance_report(). Recorded
|
|
314
|
+
# after the read transaction closes so it can never fail the read.
|
|
315
|
+
_record_access(self.db_path, node_id)
|
|
260
316
|
node = {
|
|
261
317
|
"id": row["id"],
|
|
262
318
|
"type": row["type"],
|
|
@@ -274,6 +330,137 @@ class KnowledgeGraphReadsMixin(_Core):
|
|
|
274
330
|
raise ValueError(f"graph node not found: {node_id}")
|
|
275
331
|
return node
|
|
276
332
|
|
|
333
|
+
# ── temporal reads (v11.1.0) ─────────────────────────────────────────
|
|
334
|
+
#
|
|
335
|
+
# Validity lives on ``nodes_v2``/``edges_v2``, the authoritative v2
|
|
336
|
+
# projection — the same table ``workspaces_of`` already reads for scoping,
|
|
337
|
+
# and the only one that carries the columns whichever read mode
|
|
338
|
+
# ``_read_tables()`` is in. The legacy compatibility tables have no
|
|
339
|
+
# temporal dimension, so slicing them would silently answer "everything".
|
|
340
|
+
|
|
341
|
+
def _valid_node_ids_at(self, conn: sqlite3.Connection, stamp: str) -> set:
|
|
342
|
+
"""Ids of nodes whose validity window covers ``stamp``."""
|
|
343
|
+
return {
|
|
344
|
+
row["id"]
|
|
345
|
+
for row in conn.execute(
|
|
346
|
+
f"SELECT id FROM nodes_v2 WHERE {TEMPORAL_PREDICATE_SQL}",
|
|
347
|
+
(stamp, stamp),
|
|
348
|
+
).fetchall()
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
def access_stats(self, node_ids=None) -> Dict[str, Dict[str, Any]]:
|
|
352
|
+
"""Per-node access bookkeeping read from the authoritative projection.
|
|
353
|
+
|
|
354
|
+
``{node_id: {"accesses": float, "last_used": str | None}}`` for the ids
|
|
355
|
+
requested (all nodes when ``node_ids`` is ``None``). Counts come from
|
|
356
|
+
``_touch_node``; a node never opened simply reports ``0.0``.
|
|
357
|
+
"""
|
|
358
|
+
query = "SELECT id, importance_score, last_used FROM nodes_v2"
|
|
359
|
+
params: List[Any] = []
|
|
360
|
+
if node_ids is not None:
|
|
361
|
+
ids = sorted({str(node_id) for node_id in node_ids if node_id})
|
|
362
|
+
if not ids:
|
|
363
|
+
return {}
|
|
364
|
+
query += f" WHERE id IN ({','.join('?' * len(ids))})"
|
|
365
|
+
params = list(ids)
|
|
366
|
+
with self._connect() as conn:
|
|
367
|
+
return {
|
|
368
|
+
row["id"]: {
|
|
369
|
+
"accesses": float(row["importance_score"] or 0.0),
|
|
370
|
+
"last_used": row["last_used"],
|
|
371
|
+
}
|
|
372
|
+
for row in conn.execute(query, params).fetchall()
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
def as_of(
|
|
376
|
+
self,
|
|
377
|
+
timestamp,
|
|
378
|
+
*,
|
|
379
|
+
limit: int = 300,
|
|
380
|
+
allowed_workspaces=None,
|
|
381
|
+
include_legacy_global: bool = False,
|
|
382
|
+
) -> Dict[str, Any]:
|
|
383
|
+
"""The graph slice that was valid at ``timestamp``.
|
|
384
|
+
|
|
385
|
+
Answers "what did I know in June 2025?": nodes and edges whose
|
|
386
|
+
``[valid_from, valid_to)`` window covers the instant, with
|
|
387
|
+
``valid_from`` falling back to ``created_at`` so a Brain that predates
|
|
388
|
+
the temporal columns reads as "always was true" rather than as empty.
|
|
389
|
+
Edges are returned only when *both* endpoints are in the slice.
|
|
390
|
+
"""
|
|
391
|
+
stamp = _as_of_stamp(timestamp)
|
|
392
|
+
# An explicit 0 clamps to 1 rather than re-expanding to the default:
|
|
393
|
+
# `limit or 300` would quietly return 300 rows to a caller who asked
|
|
394
|
+
# for none.
|
|
395
|
+
try:
|
|
396
|
+
limit = max(1, min(int(limit), 2000))
|
|
397
|
+
except (TypeError, ValueError):
|
|
398
|
+
limit = 300
|
|
399
|
+
scope_sql, scope_params = self._workspace_scope_sql(
|
|
400
|
+
allowed_workspaces, include_legacy_global
|
|
401
|
+
)
|
|
402
|
+
where = [TEMPORAL_PREDICATE_SQL]
|
|
403
|
+
params: List[Any] = [stamp, stamp]
|
|
404
|
+
if scope_sql is not None:
|
|
405
|
+
where.append(f"({scope_sql})")
|
|
406
|
+
params.extend(scope_params)
|
|
407
|
+
with self._connect() as conn:
|
|
408
|
+
node_rows = conn.execute(
|
|
409
|
+
"SELECT id, COALESCE(legacy_type, type) AS type, label AS title, "
|
|
410
|
+
"summary, attrs AS metadata_json, updated_at, "
|
|
411
|
+
"valid_from, valid_to, superseded_by "
|
|
412
|
+
f"FROM nodes_v2 WHERE {' AND '.join(where)} "
|
|
413
|
+
"ORDER BY updated_at DESC, id ASC LIMIT ?",
|
|
414
|
+
(*params, limit),
|
|
415
|
+
).fetchall()
|
|
416
|
+
nodes = [
|
|
417
|
+
{
|
|
418
|
+
"id": row["id"],
|
|
419
|
+
"type": row["type"],
|
|
420
|
+
"title": row["title"],
|
|
421
|
+
"summary": row["summary"],
|
|
422
|
+
"metadata": _safe_loads(row["metadata_json"]),
|
|
423
|
+
"updated_at": row["updated_at"],
|
|
424
|
+
"valid_from": row["valid_from"],
|
|
425
|
+
"valid_to": row["valid_to"],
|
|
426
|
+
"superseded_by": row["superseded_by"],
|
|
427
|
+
}
|
|
428
|
+
for row in node_rows
|
|
429
|
+
]
|
|
430
|
+
edges: List[Dict[str, Any]] = []
|
|
431
|
+
visible = sorted(str(node["id"]) for node in nodes)
|
|
432
|
+
if visible:
|
|
433
|
+
placeholders = ",".join("?" * len(visible))
|
|
434
|
+
edges = [
|
|
435
|
+
{
|
|
436
|
+
"id": row["id"],
|
|
437
|
+
"from": row["from_node"],
|
|
438
|
+
"to": row["to_node"],
|
|
439
|
+
"type": row["type"],
|
|
440
|
+
"weight": row["weight"],
|
|
441
|
+
"metadata": _safe_loads(row["metadata_json"]),
|
|
442
|
+
"valid_from": row["valid_from"],
|
|
443
|
+
"valid_to": row["valid_to"],
|
|
444
|
+
"superseded_by": row["superseded_by"],
|
|
445
|
+
}
|
|
446
|
+
for row in conn.execute(
|
|
447
|
+
"SELECT id, source AS from_node, target AS to_node, "
|
|
448
|
+
"COALESCE(legacy_type, type) AS type, weight, "
|
|
449
|
+
"metadata AS metadata_json, valid_from, valid_to, superseded_by "
|
|
450
|
+
f"FROM edges_v2 WHERE {TEMPORAL_PREDICATE_SQL} "
|
|
451
|
+
f"AND source IN ({placeholders}) AND target IN ({placeholders}) "
|
|
452
|
+
"ORDER BY weight DESC, created_at DESC, id ASC",
|
|
453
|
+
(stamp, stamp, *visible, *visible),
|
|
454
|
+
).fetchall()
|
|
455
|
+
]
|
|
456
|
+
return {
|
|
457
|
+
"as_of": stamp,
|
|
458
|
+
"nodes": nodes,
|
|
459
|
+
"edges": edges,
|
|
460
|
+
"node_count": len(nodes),
|
|
461
|
+
"edge_count": len(edges),
|
|
462
|
+
}
|
|
463
|
+
|
|
277
464
|
def relationship_search(
|
|
278
465
|
self,
|
|
279
466
|
*,
|