seren-memory 2.2.2__tar.gz → 2.2.3__tar.gz
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.
- {seren_memory-2.2.2 → seren_memory-2.2.3}/PKG-INFO +1 -1
- {seren_memory-2.2.2 → seren_memory-2.2.3}/seren_memory/_version.py +3 -3
- {seren_memory-2.2.2 → seren_memory-2.2.3}/seren_memory/collections.py +70 -12
- {seren_memory-2.2.2 → seren_memory-2.2.3}/seren_memory/routes/search.py +1 -1
- {seren_memory-2.2.2 → seren_memory-2.2.3}/seren_memory.egg-info/PKG-INFO +1 -1
- {seren_memory-2.2.2 → seren_memory-2.2.3}/SerenMemory.pyproj +0 -0
- {seren_memory-2.2.2 → seren_memory-2.2.3}/pyproject.toml +0 -0
- {seren_memory-2.2.2 → seren_memory-2.2.3}/seren-memory.service.sample +0 -0
- {seren_memory-2.2.2 → seren_memory-2.2.3}/seren-memory.yaml.sample +0 -0
- {seren_memory-2.2.2 → seren_memory-2.2.3}/seren_memory/__init__.py +0 -0
- {seren_memory-2.2.2 → seren_memory-2.2.3}/seren_memory/__main__.py +0 -0
- {seren_memory-2.2.2 → seren_memory-2.2.3}/seren_memory/_supervised.py +0 -0
- {seren_memory-2.2.2 → seren_memory-2.2.3}/seren_memory/app.py +0 -0
- {seren_memory-2.2.2 → seren_memory-2.2.3}/seren_memory/config.py +0 -0
- {seren_memory-2.2.2 → seren_memory-2.2.3}/seren_memory/consolidator/__init__.py +0 -0
- {seren_memory-2.2.2 → seren_memory-2.2.3}/seren_memory/consolidator/service.py +0 -0
- {seren_memory-2.2.2 → seren_memory-2.2.3}/seren_memory/embedder.py +0 -0
- {seren_memory-2.2.2 → seren_memory-2.2.3}/seren_memory/mcp/__init__.py +0 -0
- {seren_memory-2.2.2 → seren_memory-2.2.3}/seren_memory/mcp/server.py +0 -0
- {seren_memory-2.2.2 → seren_memory-2.2.3}/seren_memory/mcp/tools.py +0 -0
- {seren_memory-2.2.2 → seren_memory-2.2.3}/seren_memory/models/__init__.py +0 -0
- {seren_memory-2.2.2 → seren_memory-2.2.3}/seren_memory/models/schemas.py +0 -0
- {seren_memory-2.2.2 → seren_memory-2.2.3}/seren_memory/routes/__init__.py +0 -0
- {seren_memory-2.2.2 → seren_memory-2.2.3}/seren_memory/routes/long.py +0 -0
- {seren_memory-2.2.2 → seren_memory-2.2.3}/seren_memory/routes/near.py +0 -0
- {seren_memory-2.2.2 → seren_memory-2.2.3}/seren_memory/routes/short.py +0 -0
- {seren_memory-2.2.2 → seren_memory-2.2.3}/seren_memory/viewer/halls.html +0 -0
- {seren_memory-2.2.2 → seren_memory-2.2.3}/seren_memory.egg-info/SOURCES.txt +0 -0
- {seren_memory-2.2.2 → seren_memory-2.2.3}/seren_memory.egg-info/dependency_links.txt +0 -0
- {seren_memory-2.2.2 → seren_memory-2.2.3}/seren_memory.egg-info/entry_points.txt +0 -0
- {seren_memory-2.2.2 → seren_memory-2.2.3}/seren_memory.egg-info/requires.txt +0 -0
- {seren_memory-2.2.2 → seren_memory-2.2.3}/seren_memory.egg-info/top_level.txt +0 -0
- {seren_memory-2.2.2 → seren_memory-2.2.3}/setup.cfg +0 -0
- {seren_memory-2.2.2 → seren_memory-2.2.3}/tests/__init__.py +0 -0
- {seren_memory-2.2.2 → seren_memory-2.2.3}/tests/conftest.py +0 -0
- {seren_memory-2.2.2 → seren_memory-2.2.3}/tests/test_auth.py +0 -0
- {seren_memory-2.2.2 → seren_memory-2.2.3}/tests/test_brief.py +0 -0
- {seren_memory-2.2.2 → seren_memory-2.2.3}/tests/test_drafts.py +0 -0
- {seren_memory-2.2.2 → seren_memory-2.2.3}/tests/test_embedder.py +0 -0
- {seren_memory-2.2.2 → seren_memory-2.2.3}/tests/test_mcp_endpoint.py +0 -0
- {seren_memory-2.2.2 → seren_memory-2.2.3}/tests/test_mcp_fallback.py +0 -0
- {seren_memory-2.2.2 → seren_memory-2.2.3}/tests/test_mcp_mount.py +0 -0
- {seren_memory-2.2.2 → seren_memory-2.2.3}/tests/test_mcp_tools.py +0 -0
- {seren_memory-2.2.2 → seren_memory-2.2.3}/tests/test_near_listing.py +0 -0
- {seren_memory-2.2.2 → seren_memory-2.2.3}/tests/test_search.py +0 -0
- {seren_memory-2.2.2 → seren_memory-2.2.3}/tests/test_smoke.py +0 -0
- {seren_memory-2.2.2 → seren_memory-2.2.3}/tests/test_tls.py +0 -0
- {seren_memory-2.2.2 → seren_memory-2.2.3}/tests/test_validation.py +0 -0
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '2.2.
|
|
22
|
-
__version_tuple__ = version_tuple = (2, 2,
|
|
21
|
+
__version__ = version = '2.2.3'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 2, 3)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'gfb84dcf2f'
|
|
@@ -23,6 +23,7 @@ METADATA FLATTENING:
|
|
|
23
23
|
from __future__ import annotations
|
|
24
24
|
|
|
25
25
|
import json
|
|
26
|
+
import math
|
|
26
27
|
import time
|
|
27
28
|
from typing import Any, Optional
|
|
28
29
|
from pathlib import Path
|
|
@@ -702,29 +703,86 @@ class MemoryStore:
|
|
|
702
703
|
# ------------------------------------------------------------------
|
|
703
704
|
def query(self, collection_name: str, query_text: str, n: int) -> list[dict[str, Any]]:
|
|
704
705
|
"""Similarity search against one collection. Returns hits with
|
|
705
|
-
distance. collection_name in {short, near, long}.
|
|
706
|
+
distance. collection_name in {short, near, long}.
|
|
707
|
+
|
|
708
|
+
ChromaDB's HNSW index is written asynchronously — count() reads from
|
|
709
|
+
SQLite (always current) but the .bin segment files may not exist yet
|
|
710
|
+
for entries added in the current session. When that happens we fall
|
|
711
|
+
back to a linear brute-force scan: fetch all docs via .get() (SQLite,
|
|
712
|
+
always available) then re-embed the query and rank by cosine distance.
|
|
713
|
+
For a personal memory store the counts are small enough this is fine.
|
|
714
|
+
"""
|
|
706
715
|
col = {"short": self.short, "near": self.near, "long": self.long}.get(collection_name)
|
|
707
716
|
if col is None:
|
|
708
717
|
raise ValueError(f"unknown collection '{collection_name}'")
|
|
709
718
|
if col.count() == 0:
|
|
710
719
|
return []
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
720
|
+
try:
|
|
721
|
+
res = col.query(
|
|
722
|
+
query_texts=[query_text],
|
|
723
|
+
n_results=min(n, col.count()),
|
|
724
|
+
include=["documents", "metadatas", "distances"],
|
|
725
|
+
)
|
|
726
|
+
hits: list[dict[str, Any]] = []
|
|
727
|
+
ids = res.get("ids", [[]])[0]
|
|
728
|
+
docs = res.get("documents", [[]])[0]
|
|
729
|
+
metas = res.get("metadatas", [[]])[0]
|
|
730
|
+
dists = res.get("distances", [[]])[0]
|
|
731
|
+
for i in range(len(ids)):
|
|
732
|
+
meta = {k: _maybe_json(v) for k, v in (metas[i] or {}).items()}
|
|
733
|
+
hits.append({
|
|
734
|
+
"id": ids[i],
|
|
735
|
+
"content": docs[i],
|
|
736
|
+
"metadata": meta,
|
|
737
|
+
"distance": dists[i],
|
|
738
|
+
})
|
|
739
|
+
return hits
|
|
740
|
+
except Exception: # noqa: BLE001
|
|
741
|
+
# HNSW segment not flushed to disk yet — fall back to brute-force.
|
|
742
|
+
return self._query_brute(col, query_text, n)
|
|
743
|
+
|
|
744
|
+
def _query_brute(self, col: Any, query_text: str, n: int) -> list[dict[str, Any]]:
|
|
745
|
+
"""Linear cosine-similarity fallback used when the HNSW index isn't
|
|
746
|
+
available (entries written but not yet persisted to disk). Re-embeds
|
|
747
|
+
the query using the collection's own embedding function so the vector
|
|
748
|
+
space is guaranteed to match."""
|
|
749
|
+
raw = col.get(include=["documents", "metadatas", "embeddings"])
|
|
750
|
+
ids = raw.get("ids") or []
|
|
751
|
+
docs = raw.get("documents") or []
|
|
752
|
+
metas = raw.get("metadatas") or []
|
|
753
|
+
embeddings = raw.get("embeddings") or []
|
|
754
|
+
if not ids or not embeddings:
|
|
755
|
+
return []
|
|
756
|
+
|
|
757
|
+
# Re-embed the query through the same EF the collection uses.
|
|
758
|
+
try:
|
|
759
|
+
q_vecs = col._embedding_function([query_text])
|
|
760
|
+
q_vec = q_vecs[0]
|
|
761
|
+
except Exception: # noqa: BLE001
|
|
762
|
+
return []
|
|
763
|
+
|
|
764
|
+
def _cosine_dist(a: list[float], b: list[float]) -> float:
|
|
765
|
+
dot = sum(x * y for x, y in zip(a, b))
|
|
766
|
+
na = math.sqrt(sum(x * x for x in a))
|
|
767
|
+
nb = math.sqrt(sum(x * x for x in b))
|
|
768
|
+
if na == 0.0 or nb == 0.0:
|
|
769
|
+
return 1.0
|
|
770
|
+
return 1.0 - dot / (na * nb)
|
|
771
|
+
|
|
772
|
+
scored: list[tuple[float, int]] = []
|
|
773
|
+
for i, emb in enumerate(embeddings):
|
|
774
|
+
if emb is not None:
|
|
775
|
+
scored.append((_cosine_dist(q_vec, emb), i))
|
|
776
|
+
scored.sort(key=lambda t: t[0])
|
|
777
|
+
|
|
716
778
|
hits: list[dict[str, Any]] = []
|
|
717
|
-
|
|
718
|
-
docs = res.get("documents", [[]])[0]
|
|
719
|
-
metas = res.get("metadatas", [[]])[0]
|
|
720
|
-
dists = res.get("distances", [[]])[0]
|
|
721
|
-
for i in range(len(ids)):
|
|
779
|
+
for dist, i in scored[:n]:
|
|
722
780
|
meta = {k: _maybe_json(v) for k, v in (metas[i] or {}).items()}
|
|
723
781
|
hits.append({
|
|
724
782
|
"id": ids[i],
|
|
725
783
|
"content": docs[i],
|
|
726
784
|
"metadata": meta,
|
|
727
|
-
"distance":
|
|
785
|
+
"distance": dist,
|
|
728
786
|
})
|
|
729
787
|
return hits
|
|
730
788
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|