MemoryOS 1.0.0__py3-none-any.whl → 1.0.1__py3-none-any.whl
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.
Potentially problematic release.
This version of MemoryOS might be problematic. Click here for more details.
- {memoryos-1.0.0.dist-info → memoryos-1.0.1.dist-info}/METADATA +2 -1
- {memoryos-1.0.0.dist-info → memoryos-1.0.1.dist-info}/RECORD +42 -33
- memos/__init__.py +1 -1
- memos/api/config.py +25 -0
- memos/api/context/context_thread.py +96 -0
- memos/api/context/dependencies.py +0 -11
- memos/api/middleware/request_context.py +94 -0
- memos/api/product_api.py +5 -1
- memos/api/product_models.py +16 -0
- memos/api/routers/product_router.py +39 -3
- memos/api/start_api.py +3 -0
- memos/configs/memory.py +13 -0
- memos/configs/reranker.py +18 -0
- memos/graph_dbs/base.py +4 -2
- memos/graph_dbs/nebular.py +215 -68
- memos/graph_dbs/neo4j.py +14 -12
- memos/graph_dbs/neo4j_community.py +6 -3
- memos/llms/vllm.py +2 -0
- memos/log.py +120 -8
- memos/mem_os/core.py +30 -2
- memos/mem_os/product.py +386 -146
- memos/mem_os/utils/reference_utils.py +20 -0
- memos/mem_reader/simple_struct.py +112 -43
- memos/mem_user/mysql_user_manager.py +4 -2
- memos/memories/textual/item.py +1 -1
- memos/memories/textual/tree.py +31 -1
- memos/memories/textual/tree_text_memory/retrieve/bochasearch.py +3 -1
- memos/memories/textual/tree_text_memory/retrieve/recall.py +53 -3
- memos/memories/textual/tree_text_memory/retrieve/searcher.py +74 -14
- memos/memories/textual/tree_text_memory/retrieve/utils.py +6 -4
- memos/memos_tools/notification_utils.py +46 -0
- memos/reranker/__init__.py +4 -0
- memos/reranker/base.py +24 -0
- memos/reranker/cosine_local.py +95 -0
- memos/reranker/factory.py +43 -0
- memos/reranker/http_bge.py +99 -0
- memos/reranker/noop.py +16 -0
- memos/templates/mem_reader_prompts.py +289 -40
- memos/templates/mos_prompts.py +133 -60
- {memoryos-1.0.0.dist-info → memoryos-1.0.1.dist-info}/LICENSE +0 -0
- {memoryos-1.0.0.dist-info → memoryos-1.0.1.dist-info}/WHEEL +0 -0
- {memoryos-1.0.0.dist-info → memoryos-1.0.1.dist-info}/entry_points.txt +0 -0
memos/reranker/base.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# memos/reranker/base.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from memos.memories.textual.item import TextualMemoryItem
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BaseReranker(ABC):
|
|
13
|
+
"""Abstract interface for memory rerankers."""
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def rerank(
|
|
17
|
+
self,
|
|
18
|
+
query: str,
|
|
19
|
+
graph_results: list,
|
|
20
|
+
top_k: int,
|
|
21
|
+
**kwargs,
|
|
22
|
+
) -> list[tuple[TextualMemoryItem, float]]:
|
|
23
|
+
"""Return top_k (item, score) sorted by score desc."""
|
|
24
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# memos/reranker/cosine_local.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from .base import BaseReranker
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from memos.memories.textual.item import TextualMemoryItem
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
import numpy as _np
|
|
14
|
+
|
|
15
|
+
_HAS_NUMPY = True
|
|
16
|
+
except Exception:
|
|
17
|
+
_HAS_NUMPY = False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _cosine_one_to_many(q: list[float], m: list[list[float]]) -> list[float]:
|
|
21
|
+
"""
|
|
22
|
+
Compute cosine similarities between a single vector q and a matrix m (rows are candidates).
|
|
23
|
+
"""
|
|
24
|
+
if not _HAS_NUMPY:
|
|
25
|
+
|
|
26
|
+
def dot(a, b): # lowercase per N806
|
|
27
|
+
return sum(x * y for x, y in zip(a, b, strict=False))
|
|
28
|
+
|
|
29
|
+
def norm(a): # lowercase per N806
|
|
30
|
+
return sum(x * x for x in a) ** 0.5
|
|
31
|
+
|
|
32
|
+
qn = norm(q) or 1e-10
|
|
33
|
+
sims = []
|
|
34
|
+
for v in m:
|
|
35
|
+
vn = norm(v) or 1e-10
|
|
36
|
+
sims.append(dot(q, v) / (qn * vn))
|
|
37
|
+
return sims
|
|
38
|
+
|
|
39
|
+
qv = _np.asarray(q, dtype=float) # lowercase
|
|
40
|
+
mv = _np.asarray(m, dtype=float) # lowercase
|
|
41
|
+
qn = _np.linalg.norm(qv) or 1e-10
|
|
42
|
+
mn = _np.linalg.norm(mv, axis=1) # lowercase
|
|
43
|
+
dots = mv @ qv
|
|
44
|
+
return (dots / (mn * qn + 1e-10)).tolist()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class CosineLocalReranker(BaseReranker):
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
level_weights: dict[str, float] | None = None,
|
|
51
|
+
level_field: str = "background",
|
|
52
|
+
):
|
|
53
|
+
self.level_weights = level_weights or {"topic": 1.0, "concept": 1.0, "fact": 1.0}
|
|
54
|
+
self.level_field = level_field
|
|
55
|
+
|
|
56
|
+
def rerank(
|
|
57
|
+
self,
|
|
58
|
+
query: str,
|
|
59
|
+
graph_results: list,
|
|
60
|
+
top_k: int,
|
|
61
|
+
**kwargs,
|
|
62
|
+
) -> list[tuple[TextualMemoryItem, float]]:
|
|
63
|
+
if not graph_results:
|
|
64
|
+
return []
|
|
65
|
+
|
|
66
|
+
query_embedding: list[float] | None = kwargs.get("query_embedding")
|
|
67
|
+
if not query_embedding:
|
|
68
|
+
return [(item, 0.0) for item in graph_results[:top_k]]
|
|
69
|
+
|
|
70
|
+
items_with_emb = [
|
|
71
|
+
it
|
|
72
|
+
for it in graph_results
|
|
73
|
+
if getattr(it, "metadata", None) and getattr(it.metadata, "embedding", None)
|
|
74
|
+
]
|
|
75
|
+
if not items_with_emb:
|
|
76
|
+
return [(item, 0.5) for item in graph_results[:top_k]]
|
|
77
|
+
|
|
78
|
+
cand_vecs = [it.metadata.embedding for it in items_with_emb]
|
|
79
|
+
sims = _cosine_one_to_many(query_embedding, cand_vecs)
|
|
80
|
+
|
|
81
|
+
def get_weight(it: TextualMemoryItem) -> float:
|
|
82
|
+
level = getattr(it.metadata, self.level_field, None)
|
|
83
|
+
return self.level_weights.get(level, 1.0)
|
|
84
|
+
|
|
85
|
+
weighted = [sim * get_weight(it) for sim, it in zip(sims, items_with_emb, strict=False)]
|
|
86
|
+
scored_pairs = list(zip(items_with_emb, weighted, strict=False))
|
|
87
|
+
scored_pairs.sort(key=lambda x: x[1], reverse=True)
|
|
88
|
+
|
|
89
|
+
top_items = scored_pairs[:top_k]
|
|
90
|
+
if len(top_items) < top_k:
|
|
91
|
+
chosen = {it.id for it, _ in top_items}
|
|
92
|
+
remain = [(it, -1.0) for it in graph_results if it.id not in chosen]
|
|
93
|
+
top_items.extend(remain[: top_k - len(top_items)])
|
|
94
|
+
|
|
95
|
+
return top_items
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# memos/reranker/factory.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
from .cosine_local import CosineLocalReranker
|
|
7
|
+
from .http_bge import HTTPBGEReranker
|
|
8
|
+
from .noop import NoopReranker
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from memos.configs.reranker import RerankerConfigFactory
|
|
13
|
+
|
|
14
|
+
from .base import BaseReranker
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class RerankerFactory:
|
|
18
|
+
@staticmethod
|
|
19
|
+
def from_config(cfg: RerankerConfigFactory | None) -> BaseReranker | None:
|
|
20
|
+
if not cfg:
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
backend = (cfg.backend or "").lower()
|
|
24
|
+
c: dict[str, Any] = cfg.config or {}
|
|
25
|
+
|
|
26
|
+
if backend in {"http_bge", "bge"}:
|
|
27
|
+
return HTTPBGEReranker(
|
|
28
|
+
reranker_url=c.get("url") or c.get("endpoint") or c.get("reranker_url"),
|
|
29
|
+
model=c.get("model", "bge-reranker-v2-m3"),
|
|
30
|
+
timeout=int(c.get("timeout", 10)),
|
|
31
|
+
headers_extra=c.get("headers_extra"),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
if backend in {"cosine_local", "cosine"}:
|
|
35
|
+
return CosineLocalReranker(
|
|
36
|
+
level_weights=c.get("level_weights"),
|
|
37
|
+
level_field=c.get("level_field", "background"),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
if backend in {"noop", "none", "disabled"}:
|
|
41
|
+
return NoopReranker()
|
|
42
|
+
|
|
43
|
+
raise ValueError(f"Unknown reranker backend: {cfg.backend}")
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# memos/reranker/http_bge.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
from .base import BaseReranker
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from memos.memories.textual.item import TextualMemoryItem
|
|
15
|
+
|
|
16
|
+
_TAG1 = re.compile(r"^\s*\[[^\]]*\]\s*")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class HTTPBGEReranker(BaseReranker):
|
|
20
|
+
"""
|
|
21
|
+
HTTP-based BGE reranker. Mirrors your old MemoryReranker, but configurable.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
reranker_url: str,
|
|
27
|
+
token: str = "",
|
|
28
|
+
model: str = "bge-reranker-v2-m3",
|
|
29
|
+
timeout: int = 10,
|
|
30
|
+
headers_extra: dict | None = None,
|
|
31
|
+
):
|
|
32
|
+
if not reranker_url:
|
|
33
|
+
raise ValueError("reranker_url must not be empty")
|
|
34
|
+
self.reranker_url = reranker_url
|
|
35
|
+
self.token = token or ""
|
|
36
|
+
self.model = model
|
|
37
|
+
self.timeout = timeout
|
|
38
|
+
self.headers_extra = headers_extra or {}
|
|
39
|
+
|
|
40
|
+
def rerank(
|
|
41
|
+
self,
|
|
42
|
+
query: str,
|
|
43
|
+
graph_results: list,
|
|
44
|
+
top_k: int,
|
|
45
|
+
**kwargs,
|
|
46
|
+
) -> list[tuple[TextualMemoryItem, float]]:
|
|
47
|
+
if not graph_results:
|
|
48
|
+
return []
|
|
49
|
+
|
|
50
|
+
documents = [
|
|
51
|
+
(_TAG1.sub("", m) if isinstance((m := getattr(item, "memory", None)), str) else m)
|
|
52
|
+
for item in graph_results
|
|
53
|
+
]
|
|
54
|
+
documents = [d for d in documents if isinstance(d, str) and d]
|
|
55
|
+
if not documents:
|
|
56
|
+
return []
|
|
57
|
+
|
|
58
|
+
headers = {"Content-Type": "application/json", **self.headers_extra}
|
|
59
|
+
payload = {"model": self.model, "query": query, "documents": documents}
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
resp = requests.post(
|
|
63
|
+
self.reranker_url, headers=headers, json=payload, timeout=self.timeout
|
|
64
|
+
)
|
|
65
|
+
resp.raise_for_status()
|
|
66
|
+
data = resp.json()
|
|
67
|
+
|
|
68
|
+
scored_items: list[tuple[TextualMemoryItem, float]] = []
|
|
69
|
+
|
|
70
|
+
if "results" in data:
|
|
71
|
+
rows = data.get("results", [])
|
|
72
|
+
for r in rows:
|
|
73
|
+
idx = r.get("index")
|
|
74
|
+
if isinstance(idx, int) and 0 <= idx < len(graph_results):
|
|
75
|
+
score = float(r.get("relevance_score", r.get("score", 0.0)))
|
|
76
|
+
scored_items.append((graph_results[idx], score))
|
|
77
|
+
|
|
78
|
+
scored_items.sort(key=lambda x: x[1], reverse=True)
|
|
79
|
+
return scored_items[: min(top_k, len(scored_items))]
|
|
80
|
+
|
|
81
|
+
elif "data" in data:
|
|
82
|
+
rows = data.get("data", [])
|
|
83
|
+
score_list = [float(r.get("score", 0.0)) for r in rows]
|
|
84
|
+
|
|
85
|
+
if len(score_list) < len(graph_results):
|
|
86
|
+
score_list += [0.0] * (len(graph_results) - len(score_list))
|
|
87
|
+
elif len(score_list) > len(graph_results):
|
|
88
|
+
score_list = score_list[: len(graph_results)]
|
|
89
|
+
|
|
90
|
+
scored_items = list(zip(graph_results, score_list, strict=False))
|
|
91
|
+
scored_items.sort(key=lambda x: x[1], reverse=True)
|
|
92
|
+
return scored_items[: min(top_k, len(scored_items))]
|
|
93
|
+
|
|
94
|
+
else:
|
|
95
|
+
return [(item, 0.0) for item in graph_results[:top_k]]
|
|
96
|
+
|
|
97
|
+
except Exception as e:
|
|
98
|
+
print(f"[HTTPBGEReranker] request failed: {e}")
|
|
99
|
+
return [(item, 0.0) for item in graph_results[:top_k]]
|
memos/reranker/noop.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from .base import BaseReranker
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from memos.memories.textual.item import TextualMemoryItem
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class NoopReranker(BaseReranker):
|
|
13
|
+
def rerank(
|
|
14
|
+
self, query: str, graph_results: list, top_k: int, **kwargs
|
|
15
|
+
) -> list[tuple[TextualMemoryItem, float]]:
|
|
16
|
+
return [(item, 0.0) for item in graph_results[:top_k]]
|