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
|
@@ -52,6 +52,7 @@ from pathlib import Path
|
|
|
52
52
|
from typing import Any, Dict, Optional
|
|
53
53
|
|
|
54
54
|
from ..quiet import quiet
|
|
55
|
+
from ..utils import now_iso as _stamp_now
|
|
55
56
|
|
|
56
57
|
# ── Schema version ──────────────────────────────────────────────────────────
|
|
57
58
|
KG_SCHEMA_V2_VERSION = 2
|
|
@@ -117,6 +118,16 @@ class NodeType(str, Enum):
|
|
|
117
118
|
ORGANIZATION = "ORGANIZATION" # 조직 / 회사 / 팀
|
|
118
119
|
WORKFLOW = "WORKFLOW" # 워크플로우 정의/실행
|
|
119
120
|
AGENT = "AGENT" # 에이전트(역할/실행 주체)
|
|
121
|
+
# v11.1.0 Personal Ontology (Track 4) — the Self-Model subgraph. These are
|
|
122
|
+
# not "another kind of document": they are what the Brain has been told
|
|
123
|
+
# about *its owner*, so they are read, edited and deleted as a unit
|
|
124
|
+
# (``lattice_brain.self_model``). DECISION already exists above and is
|
|
125
|
+
# reused rather than duplicated — a decision is a decision whether it came
|
|
126
|
+
# from a meeting note or from "결정: 로컬 모델을 기본값으로 쓴다".
|
|
127
|
+
SELF = "SELF" # 사용자 자신(Self-Model 루트)
|
|
128
|
+
PREFERENCE = "PREFERENCE" # 선호 (좋아함/싫어함/선호 방식)
|
|
129
|
+
HABIT = "HABIT" # 습관 (반복되는 행동)
|
|
130
|
+
RELATIONSHIP = "RELATIONSHIP" # 관계 (동료/친구/멘토 등)
|
|
120
131
|
|
|
121
132
|
@classmethod
|
|
122
133
|
def from_legacy(cls, label: str) -> "NodeType":
|
|
@@ -252,6 +263,19 @@ _LEGACY_NODE_MAP: Dict[str, NodeType] = {
|
|
|
252
263
|
"team": NodeType.ORGANIZATION,
|
|
253
264
|
"workflow": NodeType.WORKFLOW,
|
|
254
265
|
"agent": NodeType.AGENT,
|
|
266
|
+
# v11.1.0 Personal Ontology — the Self-Model vocabulary, including the
|
|
267
|
+
# Korean labels a person actually types.
|
|
268
|
+
"self": NodeType.SELF,
|
|
269
|
+
"selfmodel": NodeType.SELF,
|
|
270
|
+
"me": NodeType.SELF,
|
|
271
|
+
"나": NodeType.SELF,
|
|
272
|
+
"preference": NodeType.PREFERENCE,
|
|
273
|
+
"선호": NodeType.PREFERENCE,
|
|
274
|
+
"habit": NodeType.HABIT,
|
|
275
|
+
"습관": NodeType.HABIT,
|
|
276
|
+
"relationship": NodeType.RELATIONSHIP,
|
|
277
|
+
"관계": NodeType.RELATIONSHIP,
|
|
278
|
+
"결정": NodeType.DECISION,
|
|
255
279
|
}
|
|
256
280
|
|
|
257
281
|
_LEGACY_EDGE_MAP: Dict[str, EdgeType] = {
|
|
@@ -346,6 +370,13 @@ CREATE TABLE IF NOT EXISTS nodes_v2 (
|
|
|
346
370
|
visibility TEXT NOT NULL DEFAULT 'private',
|
|
347
371
|
-- Revision chain: a node replaced by a newer one points at its successor.
|
|
348
372
|
superseded_by TEXT,
|
|
373
|
+
-- Bitemporal validity (v11.1.0). NULL is the convention, not '':
|
|
374
|
+
-- valid_from IS NULL → the row has been valid since created_at
|
|
375
|
+
-- valid_to IS NULL → the row is still valid now
|
|
376
|
+
-- An empty string would read as "valid since the beginning of time" in a
|
|
377
|
+
-- string comparison, which is why the columns are nullable with no default.
|
|
378
|
+
valid_from TEXT,
|
|
379
|
+
valid_to TEXT,
|
|
349
380
|
created_at TEXT NOT NULL,
|
|
350
381
|
updated_at TEXT NOT NULL,
|
|
351
382
|
style TEXT,
|
|
@@ -366,6 +397,12 @@ CREATE TABLE IF NOT EXISTS edges_v2 (
|
|
|
366
397
|
metadata TEXT NOT NULL DEFAULT '{}',
|
|
367
398
|
created_by TEXT NOT NULL DEFAULT 'user',
|
|
368
399
|
created_at TEXT NOT NULL,
|
|
400
|
+
-- Bitemporal validity + revision chain (v11.1.0), same NULL convention as
|
|
401
|
+
-- nodes_v2: NULL valid_from falls back to created_at, NULL valid_to means
|
|
402
|
+
-- "still holds". Relationships go stale exactly like facts do.
|
|
403
|
+
valid_from TEXT,
|
|
404
|
+
valid_to TEXT,
|
|
405
|
+
superseded_by TEXT,
|
|
369
406
|
-- Edge identity (v4): the normalized type AND the raw legacy type.
|
|
370
407
|
-- Migrated rows keep their legacy_type discriminator, so two distinct
|
|
371
408
|
-- legacy strings between one pair (e.g. "mentions" / "관련됨") stay
|
|
@@ -403,6 +440,20 @@ CREATE INDEX IF NOT EXISTS idx_edges_v2_legacy ON edges_v2(legacy_type);
|
|
|
403
440
|
"""
|
|
404
441
|
|
|
405
442
|
|
|
443
|
+
# ── Temporal read convention (v11.1.0) ──────────────────────────────────────
|
|
444
|
+
# One predicate, shared by every ``as_of`` read so the fallback rule cannot
|
|
445
|
+
# drift between call sites:
|
|
446
|
+
# * ``valid_from IS NULL`` → the row has been valid since ``created_at``
|
|
447
|
+
# (plan §10 mitigation: existing Brains keep working with no backfill)
|
|
448
|
+
# * ``valid_to IS NULL`` → the row is still valid
|
|
449
|
+
# The window is half-open ``[valid_from, valid_to)``: the instant a fact is
|
|
450
|
+
# superseded belongs to its successor, so an ``as_of`` at exactly that instant
|
|
451
|
+
# returns exactly one of the two.
|
|
452
|
+
TEMPORAL_PREDICATE_SQL = (
|
|
453
|
+
"COALESCE(valid_from, created_at) <= ? AND (valid_to IS NULL OR valid_to > ?)"
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
|
|
406
457
|
def _exec_script(conn: sqlite3.Connection, script: str) -> None:
|
|
407
458
|
"""Run a multi-statement SQL script on ``conn`` statement-by-statement.
|
|
408
459
|
|
|
@@ -456,6 +507,9 @@ class KGStoreV2:
|
|
|
456
507
|
"metadata",
|
|
457
508
|
"created_by",
|
|
458
509
|
"created_at",
|
|
510
|
+
"valid_from",
|
|
511
|
+
"valid_to",
|
|
512
|
+
"superseded_by",
|
|
459
513
|
},
|
|
460
514
|
"nodes_v2": {
|
|
461
515
|
"id",
|
|
@@ -469,6 +523,8 @@ class KGStoreV2:
|
|
|
469
523
|
"workspace_id",
|
|
470
524
|
"visibility",
|
|
471
525
|
"superseded_by",
|
|
526
|
+
"valid_from",
|
|
527
|
+
"valid_to",
|
|
472
528
|
"created_at",
|
|
473
529
|
"updated_at",
|
|
474
530
|
"style",
|
|
@@ -481,8 +537,17 @@ class KGStoreV2:
|
|
|
481
537
|
# Columns added after a table's first release that can be healed in place
|
|
482
538
|
# with ALTER TABLE ADD COLUMN (nullable / defaulted only).
|
|
483
539
|
_V2_ADDABLE_COLUMNS = {
|
|
484
|
-
"nodes_v2": {
|
|
485
|
-
|
|
540
|
+
"nodes_v2": {
|
|
541
|
+
"workspace_id": "TEXT",
|
|
542
|
+
"superseded_by": "TEXT",
|
|
543
|
+
"valid_from": "TEXT",
|
|
544
|
+
"valid_to": "TEXT",
|
|
545
|
+
},
|
|
546
|
+
"edges_v2": {
|
|
547
|
+
"valid_from": "TEXT",
|
|
548
|
+
"valid_to": "TEXT",
|
|
549
|
+
"superseded_by": "TEXT",
|
|
550
|
+
},
|
|
486
551
|
}
|
|
487
552
|
|
|
488
553
|
def _drop_stale_empty_v2_tables(self, conn: sqlite3.Connection) -> None:
|
|
@@ -586,6 +651,64 @@ class KGStoreV2:
|
|
|
586
651
|
("embed_dim", str(EMBED_DIM)),
|
|
587
652
|
)
|
|
588
653
|
|
|
654
|
+
# ── Temporal stamping (v11.1.0) ──────────────────────────
|
|
655
|
+
#
|
|
656
|
+
# These are *primitives*, not a policy: the only caller in the product is
|
|
657
|
+
# the review-approval path in :mod:`lattice_brain.synthesis`. Background
|
|
658
|
+
# jobs and agents never call them directly — they raise a proposal and the
|
|
659
|
+
# user approves it. See ``ARCHITECTURE.md`` → "Temporal knowledge".
|
|
660
|
+
def stamp_node_validity(
|
|
661
|
+
self,
|
|
662
|
+
node_id: str,
|
|
663
|
+
*,
|
|
664
|
+
valid_from: Optional[str] = None,
|
|
665
|
+
valid_to: Optional[str] = None,
|
|
666
|
+
superseded_by: Optional[str] = None,
|
|
667
|
+
) -> bool:
|
|
668
|
+
"""Stamp a node's validity window. ``True`` when a row was updated.
|
|
669
|
+
|
|
670
|
+
Only the arguments actually supplied are written; ``None`` leaves the
|
|
671
|
+
stored value alone (it never clears a column back to NULL, so a second
|
|
672
|
+
resolution cannot silently un-supersede an earlier one).
|
|
673
|
+
"""
|
|
674
|
+
return self._stamp_row(
|
|
675
|
+
"nodes_v2",
|
|
676
|
+
node_id,
|
|
677
|
+
{
|
|
678
|
+
"valid_from": valid_from,
|
|
679
|
+
"valid_to": valid_to,
|
|
680
|
+
"superseded_by": superseded_by,
|
|
681
|
+
},
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
def touch_node(self, node_id: str, *, at: Optional[str] = None) -> bool:
|
|
685
|
+
"""Record one access: bump ``importance_score`` and set ``last_used``.
|
|
686
|
+
|
|
687
|
+
Read-path bookkeeping for the importance/decay report. Returns whether
|
|
688
|
+
a row existed; callers treat a ``False``/failure as harmless.
|
|
689
|
+
"""
|
|
690
|
+
with self._conn() as conn:
|
|
691
|
+
cursor = conn.execute(
|
|
692
|
+
"UPDATE nodes_v2 SET importance_score = importance_score + 1.0, "
|
|
693
|
+
"last_used = ? WHERE id = ?",
|
|
694
|
+
(at or _stamp_now(), str(node_id)),
|
|
695
|
+
)
|
|
696
|
+
return int(cursor.rowcount or 0) > 0
|
|
697
|
+
|
|
698
|
+
def _stamp_row(
|
|
699
|
+
self, table: str, row_id: str, updates: Dict[str, Optional[str]]
|
|
700
|
+
) -> bool:
|
|
701
|
+
fields = {key: value for key, value in updates.items() if value is not None}
|
|
702
|
+
if not fields:
|
|
703
|
+
return False
|
|
704
|
+
assignments = ", ".join(f"{key}=?" for key in fields)
|
|
705
|
+
with self._conn() as conn:
|
|
706
|
+
cursor = conn.execute(
|
|
707
|
+
f"UPDATE {table} SET {assignments} WHERE id=?",
|
|
708
|
+
(*fields.values(), str(row_id)),
|
|
709
|
+
)
|
|
710
|
+
return int(cursor.rowcount or 0) > 0
|
|
711
|
+
|
|
589
712
|
# ── Maintenance ──────────────────────────────────────────
|
|
590
713
|
def stats(self) -> Dict[str, Any]:
|
|
591
714
|
with self._conn() as conn:
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Pluggable vector-index backends + the durable pending-embed queue.
|
|
2
|
+
|
|
3
|
+
Public surface (v11.1.0, Track 1):
|
|
4
|
+
|
|
5
|
+
* :class:`VectorIndex` — the Protocol every backend satisfies.
|
|
6
|
+
* :class:`BruteForceIndex` — exact, exhaustive, the default.
|
|
7
|
+
* :class:`QuantizedIndex` — int8 storage, exhaustive, approximate scores.
|
|
8
|
+
* :class:`HnswIndex` — approximate nearest neighbour, optional ``hnswlib``.
|
|
9
|
+
* :func:`resolve_vector_index` / :func:`build_index` — ``LATTICEAI_VECTOR_INDEX``
|
|
10
|
+
resolution with an honest fallback reason.
|
|
11
|
+
* :class:`VectorEmbedQueue` — SQLite-backed background embedding backlog.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from .base import (
|
|
17
|
+
IndexItem,
|
|
18
|
+
IndexStats,
|
|
19
|
+
ScoredId,
|
|
20
|
+
Similarity,
|
|
21
|
+
VectorIndex,
|
|
22
|
+
dot_similarity,
|
|
23
|
+
score_floor,
|
|
24
|
+
take_top,
|
|
25
|
+
)
|
|
26
|
+
from .brute_force import BRUTE_FORCE_BACKEND, BruteForceIndex
|
|
27
|
+
from .hnsw import (
|
|
28
|
+
HNSW_BACKEND,
|
|
29
|
+
HNSW_META_SUFFIX,
|
|
30
|
+
HNSW_SUFFIX,
|
|
31
|
+
HnswIndex,
|
|
32
|
+
hnswlib_available,
|
|
33
|
+
load_hnswlib,
|
|
34
|
+
sidecar_paths,
|
|
35
|
+
)
|
|
36
|
+
from .jobs import (
|
|
37
|
+
DEFAULT_MAX_ATTEMPTS,
|
|
38
|
+
DEFAULT_TICK_LIMIT,
|
|
39
|
+
VECTOR_JOB_STATUSES,
|
|
40
|
+
VectorEmbedQueue,
|
|
41
|
+
VectorJobStore,
|
|
42
|
+
)
|
|
43
|
+
from .quantized import QUANTIZED_BACKEND, QuantizedIndex, quantize
|
|
44
|
+
from .selector import (
|
|
45
|
+
DEFAULT_VECTOR_INDEX,
|
|
46
|
+
VECTOR_INDEX_CHOICES,
|
|
47
|
+
VECTOR_INDEX_ENV,
|
|
48
|
+
BackendSelection,
|
|
49
|
+
build_index,
|
|
50
|
+
resolve_vector_index,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
__all__ = [
|
|
54
|
+
"BRUTE_FORCE_BACKEND",
|
|
55
|
+
"DEFAULT_MAX_ATTEMPTS",
|
|
56
|
+
"DEFAULT_TICK_LIMIT",
|
|
57
|
+
"DEFAULT_VECTOR_INDEX",
|
|
58
|
+
"HNSW_BACKEND",
|
|
59
|
+
"HNSW_META_SUFFIX",
|
|
60
|
+
"HNSW_SUFFIX",
|
|
61
|
+
"QUANTIZED_BACKEND",
|
|
62
|
+
"VECTOR_INDEX_CHOICES",
|
|
63
|
+
"VECTOR_INDEX_ENV",
|
|
64
|
+
"VECTOR_JOB_STATUSES",
|
|
65
|
+
"BackendSelection",
|
|
66
|
+
"BruteForceIndex",
|
|
67
|
+
"HnswIndex",
|
|
68
|
+
"IndexItem",
|
|
69
|
+
"IndexStats",
|
|
70
|
+
"QuantizedIndex",
|
|
71
|
+
"ScoredId",
|
|
72
|
+
"Similarity",
|
|
73
|
+
"VectorEmbedQueue",
|
|
74
|
+
"VectorIndex",
|
|
75
|
+
"VectorJobStore",
|
|
76
|
+
"build_index",
|
|
77
|
+
"dot_similarity",
|
|
78
|
+
"hnswlib_available",
|
|
79
|
+
"load_hnswlib",
|
|
80
|
+
"quantize",
|
|
81
|
+
"resolve_vector_index",
|
|
82
|
+
"score_floor",
|
|
83
|
+
"sidecar_paths",
|
|
84
|
+
"take_top",
|
|
85
|
+
]
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""The vector-index contract: what it means to score embeddings (v11.1.0).
|
|
2
|
+
|
|
3
|
+
Before this package the only vector "index" was the scan loop inside
|
|
4
|
+
``retrieval_vector.vector_search``: fetch every embedding row, decode it,
|
|
5
|
+
score it, sort. That is a perfectly good *exact* index and it stays the
|
|
6
|
+
default — but it was welded to SQL row handling, so there was nowhere to put
|
|
7
|
+
a second strategy without forking the search.
|
|
8
|
+
|
|
9
|
+
This module defines the seam. A backend owns exactly one question — *given a
|
|
10
|
+
query vector, which stored ids score highest* — and nothing about SQLite,
|
|
11
|
+
chunk metadata, workspaces, or citations. The vectors are ``Sequence[float]``
|
|
12
|
+
(the repo's embedders return ``list[float]``; numpy is not a dependency).
|
|
13
|
+
|
|
14
|
+
Honesty contract: every backend reports whether it is ``approx`` (scores are
|
|
15
|
+
estimates) and whether it is ``exhaustive`` (every stored vector was
|
|
16
|
+
compared). Those two bits travel with the search result so a caller never has
|
|
17
|
+
to guess whether "no match" means "not in your brain" or "the index did not
|
|
18
|
+
look".
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from typing import (
|
|
25
|
+
Any,
|
|
26
|
+
Callable,
|
|
27
|
+
Dict,
|
|
28
|
+
Iterable,
|
|
29
|
+
List,
|
|
30
|
+
Mapping,
|
|
31
|
+
Optional,
|
|
32
|
+
Protocol,
|
|
33
|
+
Sequence,
|
|
34
|
+
Tuple,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
#: ``(item_id, score)`` — the one shape every backend returns.
|
|
38
|
+
ScoredId = Tuple[str, float]
|
|
39
|
+
#: ``(item_id, vector, metadata)`` — one item as handed to :meth:`rebuild`.
|
|
40
|
+
IndexItem = Tuple[str, Sequence[float], Mapping[str, Any]]
|
|
41
|
+
#: Pluggable similarity so a backend can borrow the embedder's own function.
|
|
42
|
+
Similarity = Callable[[Sequence[float], Sequence[float]], float]
|
|
43
|
+
|
|
44
|
+
#: "no score floor" — ``0.0`` is a meaningful floor, so it cannot be the
|
|
45
|
+
#: sentinel (the score-0-is-falsy trap this codebase keeps re-learning).
|
|
46
|
+
NO_FLOOR = float("-inf")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def dot_similarity(left: Sequence[float], right: Sequence[float]) -> float:
|
|
50
|
+
"""Dot product, which is cosine similarity for unit-length vectors.
|
|
51
|
+
|
|
52
|
+
Mirrors :meth:`lattice_brain.embeddings.LocalEmbeddingModel.similarity`,
|
|
53
|
+
including its refusal to compare different dimensions: truncating to the
|
|
54
|
+
shorter vector produces a plausible-looking number that means nothing.
|
|
55
|
+
"""
|
|
56
|
+
left_v, right_v = list(left), list(right)
|
|
57
|
+
if len(left_v) != len(right_v):
|
|
58
|
+
raise ValueError(
|
|
59
|
+
f"embedding dimension mismatch: {len(left_v)} vs {len(right_v)}; "
|
|
60
|
+
"the vector index was built with a different model"
|
|
61
|
+
)
|
|
62
|
+
return float(sum(a * b for a, b in zip(left_v, right_v, strict=True)))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def score_floor(filter: Optional[Mapping[str, Any]]) -> float:
|
|
66
|
+
"""Resolve ``filter["min_score"]`` (absent/unparseable → no floor)."""
|
|
67
|
+
if not filter:
|
|
68
|
+
return NO_FLOOR
|
|
69
|
+
raw = filter.get("min_score")
|
|
70
|
+
if raw is None:
|
|
71
|
+
return NO_FLOOR
|
|
72
|
+
try:
|
|
73
|
+
return float(raw)
|
|
74
|
+
except (TypeError, ValueError):
|
|
75
|
+
return NO_FLOOR
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def take_top(scored: List[ScoredId], top_k: int) -> List[ScoredId]:
|
|
79
|
+
"""Sort by score descending (stable → insertion order breaks ties), cut."""
|
|
80
|
+
scored.sort(key=lambda pair: -pair[1])
|
|
81
|
+
return scored[: max(0, int(top_k))]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(frozen=True)
|
|
85
|
+
class IndexStats:
|
|
86
|
+
"""What a backend is willing to claim about itself."""
|
|
87
|
+
|
|
88
|
+
backend: str
|
|
89
|
+
size: int
|
|
90
|
+
dim: int
|
|
91
|
+
approx: bool
|
|
92
|
+
exhaustive: bool
|
|
93
|
+
available: bool = True
|
|
94
|
+
detail: Optional[str] = None
|
|
95
|
+
|
|
96
|
+
def as_dict(self) -> Dict[str, Any]:
|
|
97
|
+
return {
|
|
98
|
+
"backend": self.backend,
|
|
99
|
+
"size": self.size,
|
|
100
|
+
"dim": self.dim,
|
|
101
|
+
"approx": self.approx,
|
|
102
|
+
"exhaustive": self.exhaustive,
|
|
103
|
+
"available": self.available,
|
|
104
|
+
"detail": self.detail,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class VectorIndex(Protocol):
|
|
109
|
+
"""Structural contract implemented by every backend in this package.
|
|
110
|
+
|
|
111
|
+
Declared, never implemented — same convention as
|
|
112
|
+
:mod:`lattice_brain.graph._kg_contract`: the bodies raise so a stub can
|
|
113
|
+
never be mistaken for a working index.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def backend(self) -> str:
|
|
118
|
+
"""Stable id of the scoring strategy (goes into ``recall.backend``)."""
|
|
119
|
+
raise NotImplementedError
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def approx(self) -> bool:
|
|
123
|
+
"""True when scores are estimates rather than exact similarities."""
|
|
124
|
+
raise NotImplementedError
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def exhaustive(self) -> bool:
|
|
128
|
+
"""True when every stored vector is compared on every search."""
|
|
129
|
+
raise NotImplementedError
|
|
130
|
+
|
|
131
|
+
def add(
|
|
132
|
+
self,
|
|
133
|
+
id: str,
|
|
134
|
+
vector: Sequence[float],
|
|
135
|
+
metadata: Optional[Mapping[str, Any]] = None,
|
|
136
|
+
) -> None:
|
|
137
|
+
raise NotImplementedError
|
|
138
|
+
|
|
139
|
+
def remove(self, id: str) -> None:
|
|
140
|
+
raise NotImplementedError
|
|
141
|
+
|
|
142
|
+
def rebuild(self, items: Iterable[IndexItem]) -> None:
|
|
143
|
+
raise NotImplementedError
|
|
144
|
+
|
|
145
|
+
def metadata_for(self, id: str) -> Dict[str, Any]:
|
|
146
|
+
raise NotImplementedError
|
|
147
|
+
|
|
148
|
+
def search(
|
|
149
|
+
self,
|
|
150
|
+
query: Sequence[float],
|
|
151
|
+
top_k: int,
|
|
152
|
+
filter: Optional[Mapping[str, Any]] = None,
|
|
153
|
+
) -> List[ScoredId]:
|
|
154
|
+
raise NotImplementedError
|
|
155
|
+
|
|
156
|
+
def stats(self) -> IndexStats:
|
|
157
|
+
raise NotImplementedError
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
__all__ = [
|
|
161
|
+
"NO_FLOOR",
|
|
162
|
+
"IndexItem",
|
|
163
|
+
"IndexStats",
|
|
164
|
+
"ScoredId",
|
|
165
|
+
"Similarity",
|
|
166
|
+
"VectorIndex",
|
|
167
|
+
"dot_similarity",
|
|
168
|
+
"score_floor",
|
|
169
|
+
"take_top",
|
|
170
|
+
]
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Exact cosine scan — the historical ``vector_search`` scoring loop, moved.
|
|
2
|
+
|
|
3
|
+
This backend is deliberately boring and deliberately the default. It compares
|
|
4
|
+
the query against every vector it holds, so its answer is the ground truth the
|
|
5
|
+
other backends are measured against (``scripts/bench_vector_index.py`` reports
|
|
6
|
+
recall@k relative to this one).
|
|
7
|
+
|
|
8
|
+
Behaviour parity with the pre-11.1.0 inline loop is the whole point of the
|
|
9
|
+
move: same similarity function (injected, so it is the embedder's own), same
|
|
10
|
+
``score < min_score`` drop, same stable ordering. ``retrieval_vector`` feeds
|
|
11
|
+
it in bounded batches so a scan never holds every decoded vector at once.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence
|
|
17
|
+
|
|
18
|
+
from .base import (
|
|
19
|
+
IndexItem,
|
|
20
|
+
IndexStats,
|
|
21
|
+
ScoredId,
|
|
22
|
+
Similarity,
|
|
23
|
+
dot_similarity,
|
|
24
|
+
score_floor,
|
|
25
|
+
take_top,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
BRUTE_FORCE_BACKEND = "bruteforce-cosine"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class BruteForceIndex:
|
|
32
|
+
"""Exhaustive, exact, in-memory cosine index."""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
*,
|
|
37
|
+
dim: int = 0,
|
|
38
|
+
similarity: Optional[Similarity] = None,
|
|
39
|
+
) -> None:
|
|
40
|
+
self._dim = int(dim)
|
|
41
|
+
self._similarity: Similarity = (
|
|
42
|
+
dot_similarity if similarity is None else similarity
|
|
43
|
+
)
|
|
44
|
+
self._vectors: Dict[str, List[float]] = {}
|
|
45
|
+
self._metadata: Dict[str, Dict[str, Any]] = {}
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def backend(self) -> str:
|
|
49
|
+
return BRUTE_FORCE_BACKEND
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def approx(self) -> bool:
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def exhaustive(self) -> bool:
|
|
57
|
+
return True
|
|
58
|
+
|
|
59
|
+
def add(
|
|
60
|
+
self,
|
|
61
|
+
id: str,
|
|
62
|
+
vector: Sequence[float],
|
|
63
|
+
metadata: Optional[Mapping[str, Any]] = None,
|
|
64
|
+
) -> None:
|
|
65
|
+
values = [float(value) for value in vector]
|
|
66
|
+
if not self._dim:
|
|
67
|
+
self._dim = len(values)
|
|
68
|
+
key = str(id)
|
|
69
|
+
self._vectors[key] = values
|
|
70
|
+
self._metadata[key] = dict(metadata or {})
|
|
71
|
+
|
|
72
|
+
def remove(self, id: str) -> None:
|
|
73
|
+
key = str(id)
|
|
74
|
+
self._vectors.pop(key, None)
|
|
75
|
+
self._metadata.pop(key, None)
|
|
76
|
+
|
|
77
|
+
def rebuild(self, items: Iterable[IndexItem]) -> None:
|
|
78
|
+
self._vectors.clear()
|
|
79
|
+
self._metadata.clear()
|
|
80
|
+
for item_id, vector, metadata in items:
|
|
81
|
+
self.add(item_id, vector, metadata)
|
|
82
|
+
|
|
83
|
+
def metadata_for(self, id: str) -> Dict[str, Any]:
|
|
84
|
+
"""Metadata stored alongside ``id`` (empty dict when unknown)."""
|
|
85
|
+
return dict(self._metadata.get(str(id), {}))
|
|
86
|
+
|
|
87
|
+
def search(
|
|
88
|
+
self,
|
|
89
|
+
query: Sequence[float],
|
|
90
|
+
top_k: int,
|
|
91
|
+
filter: Optional[Mapping[str, Any]] = None,
|
|
92
|
+
) -> List[ScoredId]:
|
|
93
|
+
floor = score_floor(filter)
|
|
94
|
+
query_vector = list(query)
|
|
95
|
+
scored: List[ScoredId] = []
|
|
96
|
+
for item_id, vector in self._vectors.items():
|
|
97
|
+
score = float(self._similarity(query_vector, vector))
|
|
98
|
+
if score < floor:
|
|
99
|
+
continue
|
|
100
|
+
scored.append((item_id, score))
|
|
101
|
+
return take_top(scored, top_k)
|
|
102
|
+
|
|
103
|
+
def stats(self) -> IndexStats:
|
|
104
|
+
return IndexStats(
|
|
105
|
+
backend=self.backend,
|
|
106
|
+
size=len(self._vectors),
|
|
107
|
+
dim=self._dim,
|
|
108
|
+
approx=False,
|
|
109
|
+
exhaustive=True,
|
|
110
|
+
detail=None,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
__all__ = ["BRUTE_FORCE_BACKEND", "BruteForceIndex"]
|