MemoryOS 1.0.1__py3-none-any.whl → 1.1.2__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.1.dist-info → memoryos-1.1.2.dist-info}/METADATA +7 -2
- {memoryos-1.0.1.dist-info → memoryos-1.1.2.dist-info}/RECORD +79 -65
- {memoryos-1.0.1.dist-info → memoryos-1.1.2.dist-info}/WHEEL +1 -1
- memos/__init__.py +1 -1
- memos/api/client.py +109 -0
- memos/api/config.py +11 -9
- memos/api/context/dependencies.py +15 -55
- memos/api/middleware/request_context.py +9 -40
- memos/api/product_api.py +2 -3
- memos/api/product_models.py +91 -16
- memos/api/routers/product_router.py +23 -16
- memos/api/start_api.py +10 -0
- memos/configs/graph_db.py +4 -0
- memos/configs/mem_scheduler.py +38 -3
- memos/context/context.py +255 -0
- memos/embedders/factory.py +2 -0
- memos/graph_dbs/nebular.py +230 -232
- memos/graph_dbs/neo4j.py +35 -1
- memos/graph_dbs/neo4j_community.py +7 -0
- memos/llms/factory.py +2 -0
- memos/llms/openai.py +74 -2
- memos/log.py +27 -15
- memos/mem_cube/general.py +3 -1
- memos/mem_os/core.py +60 -22
- memos/mem_os/main.py +3 -6
- memos/mem_os/product.py +35 -11
- memos/mem_reader/factory.py +2 -0
- memos/mem_reader/simple_struct.py +127 -74
- memos/mem_scheduler/analyzer/__init__.py +0 -0
- memos/mem_scheduler/analyzer/mos_for_test_scheduler.py +569 -0
- memos/mem_scheduler/analyzer/scheduler_for_eval.py +280 -0
- memos/mem_scheduler/base_scheduler.py +126 -56
- memos/mem_scheduler/general_modules/dispatcher.py +2 -2
- memos/mem_scheduler/general_modules/misc.py +99 -1
- memos/mem_scheduler/general_modules/scheduler_logger.py +17 -11
- memos/mem_scheduler/general_scheduler.py +40 -88
- memos/mem_scheduler/memory_manage_modules/__init__.py +5 -0
- memos/mem_scheduler/memory_manage_modules/memory_filter.py +308 -0
- memos/mem_scheduler/{general_modules → memory_manage_modules}/retriever.py +34 -7
- memos/mem_scheduler/monitors/dispatcher_monitor.py +9 -8
- memos/mem_scheduler/monitors/general_monitor.py +119 -39
- memos/mem_scheduler/optimized_scheduler.py +124 -0
- memos/mem_scheduler/orm_modules/__init__.py +0 -0
- memos/mem_scheduler/orm_modules/base_model.py +635 -0
- memos/mem_scheduler/orm_modules/monitor_models.py +261 -0
- memos/mem_scheduler/scheduler_factory.py +2 -0
- memos/mem_scheduler/schemas/monitor_schemas.py +96 -29
- memos/mem_scheduler/utils/config_utils.py +100 -0
- memos/mem_scheduler/utils/db_utils.py +33 -0
- memos/mem_scheduler/utils/filter_utils.py +1 -1
- memos/mem_scheduler/webservice_modules/__init__.py +0 -0
- memos/memories/activation/kv.py +2 -1
- memos/memories/textual/item.py +95 -16
- memos/memories/textual/naive.py +1 -1
- memos/memories/textual/tree.py +27 -3
- memos/memories/textual/tree_text_memory/organize/handler.py +4 -2
- memos/memories/textual/tree_text_memory/organize/manager.py +28 -14
- memos/memories/textual/tree_text_memory/organize/relation_reason_detector.py +1 -2
- memos/memories/textual/tree_text_memory/organize/reorganizer.py +75 -23
- memos/memories/textual/tree_text_memory/retrieve/bochasearch.py +7 -5
- memos/memories/textual/tree_text_memory/retrieve/internet_retriever.py +6 -2
- memos/memories/textual/tree_text_memory/retrieve/internet_retriever_factory.py +2 -0
- memos/memories/textual/tree_text_memory/retrieve/recall.py +70 -22
- memos/memories/textual/tree_text_memory/retrieve/searcher.py +101 -33
- memos/memories/textual/tree_text_memory/retrieve/xinyusearch.py +5 -4
- memos/memos_tools/singleton.py +174 -0
- memos/memos_tools/thread_safe_dict.py +22 -0
- memos/memos_tools/thread_safe_dict_segment.py +382 -0
- memos/parsers/factory.py +2 -0
- memos/reranker/concat.py +59 -0
- memos/reranker/cosine_local.py +1 -0
- memos/reranker/factory.py +5 -0
- memos/reranker/http_bge.py +225 -12
- memos/templates/mem_scheduler_prompts.py +242 -0
- memos/types.py +4 -1
- memos/api/context/context.py +0 -147
- memos/api/context/context_thread.py +0 -96
- memos/mem_scheduler/mos_for_test_scheduler.py +0 -146
- {memoryos-1.0.1.dist-info → memoryos-1.1.2.dist-info}/entry_points.txt +0 -0
- {memoryos-1.0.1.dist-info → memoryos-1.1.2.dist-info/licenses}/LICENSE +0 -0
- /memos/mem_scheduler/{general_modules → webservice_modules}/rabbitmq_service.py +0 -0
- /memos/mem_scheduler/{general_modules → webservice_modules}/redis_service.py +0 -0
memos/parsers/factory.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from typing import Any, ClassVar
|
|
2
2
|
|
|
3
3
|
from memos.configs.parser import ParserConfigFactory
|
|
4
|
+
from memos.memos_tools.singleton import singleton_factory
|
|
4
5
|
from memos.parsers.base import BaseParser
|
|
5
6
|
from memos.parsers.markitdown import MarkItDownParser
|
|
6
7
|
|
|
@@ -11,6 +12,7 @@ class ParserFactory(BaseParser):
|
|
|
11
12
|
backend_to_class: ClassVar[dict[str, Any]] = {"markitdown": MarkItDownParser}
|
|
12
13
|
|
|
13
14
|
@classmethod
|
|
15
|
+
@singleton_factory()
|
|
14
16
|
def from_config(cls, config_factory: ParserConfigFactory) -> BaseParser:
|
|
15
17
|
backend = config_factory.backend
|
|
16
18
|
if backend not in cls.backend_to_class:
|
memos/reranker/concat.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
_TAG1 = re.compile(r"^\s*\[[^\]]*\]\s*")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def process_source(
|
|
10
|
+
items: list[tuple[Any, str | dict[str, Any] | list[Any]]] | None = None, recent_num: int = 3
|
|
11
|
+
) -> str:
|
|
12
|
+
"""
|
|
13
|
+
Args:
|
|
14
|
+
items: List of tuples where each tuple contains (memory, source).
|
|
15
|
+
source can be str, Dict, or List.
|
|
16
|
+
recent_num: Number of recent items to concatenate.
|
|
17
|
+
Returns:
|
|
18
|
+
str: Concatenated source.
|
|
19
|
+
"""
|
|
20
|
+
if items is None:
|
|
21
|
+
items = []
|
|
22
|
+
concat_data = []
|
|
23
|
+
memory = None
|
|
24
|
+
for item in items:
|
|
25
|
+
memory, source = item
|
|
26
|
+
for content in source:
|
|
27
|
+
if isinstance(content, str):
|
|
28
|
+
if "assistant:" in content:
|
|
29
|
+
continue
|
|
30
|
+
concat_data.append(content)
|
|
31
|
+
if memory is not None:
|
|
32
|
+
concat_data = [memory, *concat_data]
|
|
33
|
+
return "\n".join(concat_data)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def concat_original_source(
|
|
37
|
+
graph_results: list,
|
|
38
|
+
merge_field: list[str] | None = None,
|
|
39
|
+
) -> list[str]:
|
|
40
|
+
"""
|
|
41
|
+
Merge memory items with original dialogue.
|
|
42
|
+
Args:
|
|
43
|
+
graph_results (list[TextualMemoryItem]): List of memory items with embeddings.
|
|
44
|
+
merge_field (List[str]): List of fields to merge.
|
|
45
|
+
Returns:
|
|
46
|
+
list[str]: List of memory and concat orginal memory.
|
|
47
|
+
"""
|
|
48
|
+
if merge_field is None:
|
|
49
|
+
merge_field = ["sources"]
|
|
50
|
+
documents = []
|
|
51
|
+
for item in graph_results:
|
|
52
|
+
memory = _TAG1.sub("", m) if isinstance((m := getattr(item, "memory", None)), str) else m
|
|
53
|
+
sources = []
|
|
54
|
+
for field in merge_field:
|
|
55
|
+
source = getattr(item.metadata, field, "")
|
|
56
|
+
sources.append((memory, source))
|
|
57
|
+
concat_string = process_source(sources)
|
|
58
|
+
documents.append(concat_string)
|
|
59
|
+
return documents
|
memos/reranker/cosine_local.py
CHANGED
|
@@ -49,6 +49,7 @@ class CosineLocalReranker(BaseReranker):
|
|
|
49
49
|
self,
|
|
50
50
|
level_weights: dict[str, float] | None = None,
|
|
51
51
|
level_field: str = "background",
|
|
52
|
+
**kwargs,
|
|
52
53
|
):
|
|
53
54
|
self.level_weights = level_weights or {"topic": 1.0, "concept": 1.0, "fact": 1.0}
|
|
54
55
|
self.level_field = level_field
|
memos/reranker/factory.py
CHANGED
|
@@ -3,6 +3,9 @@ from __future__ import annotations
|
|
|
3
3
|
|
|
4
4
|
from typing import TYPE_CHECKING, Any
|
|
5
5
|
|
|
6
|
+
# Import singleton decorator
|
|
7
|
+
from memos.memos_tools.singleton import singleton_factory
|
|
8
|
+
|
|
6
9
|
from .cosine_local import CosineLocalReranker
|
|
7
10
|
from .http_bge import HTTPBGEReranker
|
|
8
11
|
from .noop import NoopReranker
|
|
@@ -16,6 +19,7 @@ if TYPE_CHECKING:
|
|
|
16
19
|
|
|
17
20
|
class RerankerFactory:
|
|
18
21
|
@staticmethod
|
|
22
|
+
@singleton_factory("RerankerFactory")
|
|
19
23
|
def from_config(cfg: RerankerConfigFactory | None) -> BaseReranker | None:
|
|
20
24
|
if not cfg:
|
|
21
25
|
return None
|
|
@@ -29,6 +33,7 @@ class RerankerFactory:
|
|
|
29
33
|
model=c.get("model", "bge-reranker-v2-m3"),
|
|
30
34
|
timeout=int(c.get("timeout", 10)),
|
|
31
35
|
headers_extra=c.get("headers_extra"),
|
|
36
|
+
rerank_source=c.get("rerank_source"),
|
|
32
37
|
)
|
|
33
38
|
|
|
34
39
|
if backend in {"cosine_local", "cosine"}:
|
memos/reranker/http_bge.py
CHANGED
|
@@ -3,22 +3,74 @@ from __future__ import annotations
|
|
|
3
3
|
|
|
4
4
|
import re
|
|
5
5
|
|
|
6
|
-
from
|
|
6
|
+
from collections.abc import Iterable
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
7
8
|
|
|
8
9
|
import requests
|
|
9
10
|
|
|
11
|
+
from memos.log import get_logger
|
|
12
|
+
|
|
10
13
|
from .base import BaseReranker
|
|
14
|
+
from .concat import concat_original_source
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
logger = get_logger(__name__)
|
|
11
18
|
|
|
12
19
|
|
|
13
20
|
if TYPE_CHECKING:
|
|
14
21
|
from memos.memories.textual.item import TextualMemoryItem
|
|
15
22
|
|
|
23
|
+
# Strip a leading "[...]" tag (e.g., "[2025-09-01] ..." or "[meta] ...")
|
|
24
|
+
# before sending text to the reranker. This keeps inputs clean and
|
|
25
|
+
# avoids misleading the model with bracketed prefixes.
|
|
16
26
|
_TAG1 = re.compile(r"^\s*\[[^\]]*\]\s*")
|
|
27
|
+
DEFAULT_BOOST_WEIGHTS = {"user_id": 0.5, "tags": 0.2, "session_id": 0.3}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _value_matches(item_value: Any, wanted: Any) -> bool:
|
|
31
|
+
"""
|
|
32
|
+
Generic matching:
|
|
33
|
+
- if item_value is list/tuple/set: check membership (any match if wanted is iterable)
|
|
34
|
+
- else: equality (any match if wanted is iterable)
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def _iterable(x):
|
|
38
|
+
# exclude strings from "iterable"
|
|
39
|
+
return isinstance(x, Iterable) and not isinstance(x, str | bytes)
|
|
40
|
+
|
|
41
|
+
if _iterable(item_value):
|
|
42
|
+
if _iterable(wanted):
|
|
43
|
+
return any(w in item_value for w in wanted)
|
|
44
|
+
return wanted in item_value
|
|
45
|
+
else:
|
|
46
|
+
if _iterable(wanted):
|
|
47
|
+
return any(item_value == w for w in wanted)
|
|
48
|
+
return item_value == wanted
|
|
17
49
|
|
|
18
50
|
|
|
19
51
|
class HTTPBGEReranker(BaseReranker):
|
|
20
52
|
"""
|
|
21
|
-
HTTP-based BGE reranker.
|
|
53
|
+
HTTP-based BGE reranker.
|
|
54
|
+
|
|
55
|
+
This class sends (query, documents[]) to a remote HTTP endpoint that
|
|
56
|
+
performs cross-encoder-style re-ranking (e.g., BGE reranker) and returns
|
|
57
|
+
relevance scores. It then maps those scores back onto the original
|
|
58
|
+
TextualMemoryItem list and returns (item, score) pairs sorted by score.
|
|
59
|
+
|
|
60
|
+
Notes
|
|
61
|
+
-----
|
|
62
|
+
- The endpoint is expected to accept JSON:
|
|
63
|
+
{
|
|
64
|
+
"model": "<model-name>",
|
|
65
|
+
"query": "<query text>",
|
|
66
|
+
"documents": ["doc1", "doc2", ...]
|
|
67
|
+
}
|
|
68
|
+
- Two response shapes are supported:
|
|
69
|
+
1) {"results": [{"index": <int>, "relevance_score": <float>}, ...]}
|
|
70
|
+
where "index" refers to the *position in the documents array*.
|
|
71
|
+
2) {"data": [{"score": <float>}, ...]} (aligned by list order)
|
|
72
|
+
- If the service fails or responds unexpectedly, this falls back to
|
|
73
|
+
returning the original items with 0.0 scores (best-effort).
|
|
22
74
|
"""
|
|
23
75
|
|
|
24
76
|
def __init__(
|
|
@@ -28,7 +80,26 @@ class HTTPBGEReranker(BaseReranker):
|
|
|
28
80
|
model: str = "bge-reranker-v2-m3",
|
|
29
81
|
timeout: int = 10,
|
|
30
82
|
headers_extra: dict | None = None,
|
|
83
|
+
rerank_source: list[str] | None = None,
|
|
84
|
+
boost_weights: dict[str, float] | None = None,
|
|
85
|
+
boost_default: float = 0.0,
|
|
86
|
+
warn_unknown_filter_keys: bool = True,
|
|
87
|
+
**kwargs,
|
|
31
88
|
):
|
|
89
|
+
"""
|
|
90
|
+
Parameters
|
|
91
|
+
----------
|
|
92
|
+
reranker_url : str
|
|
93
|
+
HTTP endpoint for the reranker service.
|
|
94
|
+
token : str, optional
|
|
95
|
+
Bearer token for auth. If non-empty, added to the Authorization header.
|
|
96
|
+
model : str, optional
|
|
97
|
+
Model identifier understood by the server.
|
|
98
|
+
timeout : int, optional
|
|
99
|
+
Request timeout (seconds).
|
|
100
|
+
headers_extra : dict | None, optional
|
|
101
|
+
Additional headers to merge into the request headers.
|
|
102
|
+
"""
|
|
32
103
|
if not reranker_url:
|
|
33
104
|
raise ValueError("reranker_url must not be empty")
|
|
34
105
|
self.reranker_url = reranker_url
|
|
@@ -36,22 +107,62 @@ class HTTPBGEReranker(BaseReranker):
|
|
|
36
107
|
self.model = model
|
|
37
108
|
self.timeout = timeout
|
|
38
109
|
self.headers_extra = headers_extra or {}
|
|
110
|
+
self.concat_source = rerank_source
|
|
111
|
+
|
|
112
|
+
self.boost_weights = (
|
|
113
|
+
DEFAULT_BOOST_WEIGHTS.copy()
|
|
114
|
+
if boost_weights is None
|
|
115
|
+
else {k: float(v) for k, v in boost_weights.items()}
|
|
116
|
+
)
|
|
117
|
+
self.boost_default = float(boost_default)
|
|
118
|
+
self.warn_unknown_filter_keys = bool(warn_unknown_filter_keys)
|
|
119
|
+
self._warned_missing_keys: set[str] = set()
|
|
39
120
|
|
|
40
121
|
def rerank(
|
|
41
122
|
self,
|
|
42
123
|
query: str,
|
|
43
|
-
graph_results: list,
|
|
124
|
+
graph_results: list[TextualMemoryItem],
|
|
44
125
|
top_k: int,
|
|
126
|
+
search_filter: dict | None = None,
|
|
45
127
|
**kwargs,
|
|
46
128
|
) -> list[tuple[TextualMemoryItem, float]]:
|
|
129
|
+
"""
|
|
130
|
+
Rank candidate memories by relevance to the query.
|
|
131
|
+
|
|
132
|
+
Parameters
|
|
133
|
+
----------
|
|
134
|
+
query : str
|
|
135
|
+
The search query.
|
|
136
|
+
graph_results : list[TextualMemoryItem]
|
|
137
|
+
Candidate items to re-rank. Each item is expected to have a
|
|
138
|
+
`.memory` str field; non-strings are ignored.
|
|
139
|
+
top_k : int
|
|
140
|
+
Return at most this many items.
|
|
141
|
+
search_filter : dict | None
|
|
142
|
+
Currently unused. Present to keep signature compatible.
|
|
143
|
+
|
|
144
|
+
Returns
|
|
145
|
+
-------
|
|
146
|
+
list[tuple[TextualMemoryItem, float]]
|
|
147
|
+
Re-ranked items with scores, sorted descending by score.
|
|
148
|
+
"""
|
|
47
149
|
if not graph_results:
|
|
48
150
|
return []
|
|
49
151
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
152
|
+
# Build a mapping from "payload docs index" -> "original graph_results index"
|
|
153
|
+
# Only include items that have a non-empty string memory. This ensures that
|
|
154
|
+
# any index returned by the server can be mapped back correctly.
|
|
155
|
+
if self.concat_source:
|
|
156
|
+
documents = concat_original_source(graph_results, self.concat_source)
|
|
157
|
+
else:
|
|
158
|
+
documents = [
|
|
159
|
+
(_TAG1.sub("", m) if isinstance((m := getattr(item, "memory", None)), str) else m)
|
|
160
|
+
for item in graph_results
|
|
161
|
+
]
|
|
162
|
+
documents = [d for d in documents if isinstance(d, str) and d]
|
|
163
|
+
|
|
164
|
+
logger.info(f"[HTTPBGERerankerSample] query: {query} , documents: {documents[:5]}...")
|
|
165
|
+
|
|
55
166
|
if not documents:
|
|
56
167
|
return []
|
|
57
168
|
|
|
@@ -59,6 +170,7 @@ class HTTPBGEReranker(BaseReranker):
|
|
|
59
170
|
payload = {"model": self.model, "query": query, "documents": documents}
|
|
60
171
|
|
|
61
172
|
try:
|
|
173
|
+
# Make the HTTP request to the reranker service
|
|
62
174
|
resp = requests.post(
|
|
63
175
|
self.reranker_url, headers=headers, json=payload, timeout=self.timeout
|
|
64
176
|
)
|
|
@@ -68,18 +180,28 @@ class HTTPBGEReranker(BaseReranker):
|
|
|
68
180
|
scored_items: list[tuple[TextualMemoryItem, float]] = []
|
|
69
181
|
|
|
70
182
|
if "results" in data:
|
|
183
|
+
# Format:
|
|
184
|
+
# dict("results": [{"index": int, "relevance_score": float},
|
|
185
|
+
# ...])
|
|
71
186
|
rows = data.get("results", [])
|
|
72
187
|
for r in rows:
|
|
73
188
|
idx = r.get("index")
|
|
189
|
+
# The returned index refers to 'documents' (i.e., our 'pairs' order),
|
|
190
|
+
# so we must map it back to the original graph_results index.
|
|
74
191
|
if isinstance(idx, int) and 0 <= idx < len(graph_results):
|
|
75
|
-
|
|
76
|
-
|
|
192
|
+
raw_score = float(r.get("relevance_score", r.get("score", 0.0)))
|
|
193
|
+
item = graph_results[idx]
|
|
194
|
+
# generic boost
|
|
195
|
+
score = self._apply_boost_generic(item, raw_score, search_filter)
|
|
196
|
+
scored_items.append((item, score))
|
|
77
197
|
|
|
78
198
|
scored_items.sort(key=lambda x: x[1], reverse=True)
|
|
79
199
|
return scored_items[: min(top_k, len(scored_items))]
|
|
80
200
|
|
|
81
201
|
elif "data" in data:
|
|
202
|
+
# Format: {"data": [{"score": float}, ...]} aligned by list order
|
|
82
203
|
rows = data.get("data", [])
|
|
204
|
+
# Build a list of scores aligned with our 'documents' (pairs)
|
|
83
205
|
score_list = [float(r.get("score", 0.0)) for r in rows]
|
|
84
206
|
|
|
85
207
|
if len(score_list) < len(graph_results):
|
|
@@ -87,13 +209,104 @@ class HTTPBGEReranker(BaseReranker):
|
|
|
87
209
|
elif len(score_list) > len(graph_results):
|
|
88
210
|
score_list = score_list[: len(graph_results)]
|
|
89
211
|
|
|
90
|
-
scored_items =
|
|
212
|
+
scored_items = []
|
|
213
|
+
for item, raw_score in zip(graph_results, score_list, strict=False):
|
|
214
|
+
score = self._apply_boost_generic(item, raw_score, search_filter)
|
|
215
|
+
scored_items.append((item, score))
|
|
216
|
+
|
|
91
217
|
scored_items.sort(key=lambda x: x[1], reverse=True)
|
|
92
218
|
return scored_items[: min(top_k, len(scored_items))]
|
|
93
219
|
|
|
94
220
|
else:
|
|
221
|
+
# Unexpected response schema: return a 0.0-scored fallback of the first top_k valid docs
|
|
222
|
+
# Note: we use 'pairs' to keep alignment with valid (string) docs.
|
|
95
223
|
return [(item, 0.0) for item in graph_results[:top_k]]
|
|
96
224
|
|
|
97
225
|
except Exception as e:
|
|
98
|
-
|
|
226
|
+
# Network error, timeout, JSON decode error, etc.
|
|
227
|
+
# Degrade gracefully by returning first top_k valid docs with 0.0 score.
|
|
228
|
+
logger.error(f"[HTTPBGEReranker] request failed: {e}")
|
|
99
229
|
return [(item, 0.0) for item in graph_results[:top_k]]
|
|
230
|
+
|
|
231
|
+
def _get_attr_or_key(self, obj: Any, key: str) -> Any:
|
|
232
|
+
"""
|
|
233
|
+
Resolve `key` on `obj` with one-level fallback into `obj.metadata`.
|
|
234
|
+
|
|
235
|
+
Priority:
|
|
236
|
+
1) obj.<key>
|
|
237
|
+
2) obj[key]
|
|
238
|
+
3) obj.metadata.<key>
|
|
239
|
+
4) obj.metadata[key]
|
|
240
|
+
"""
|
|
241
|
+
if obj is None:
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
# support input like "metadata.user_id"
|
|
245
|
+
if "." in key:
|
|
246
|
+
head, tail = key.split(".", 1)
|
|
247
|
+
base = self._get_attr_or_key(obj, head)
|
|
248
|
+
return self._get_attr_or_key(base, tail)
|
|
249
|
+
|
|
250
|
+
def _resolve(o: Any, k: str):
|
|
251
|
+
if o is None:
|
|
252
|
+
return None
|
|
253
|
+
v = getattr(o, k, None)
|
|
254
|
+
if v is not None:
|
|
255
|
+
return v
|
|
256
|
+
if hasattr(o, "get"):
|
|
257
|
+
try:
|
|
258
|
+
return o.get(k)
|
|
259
|
+
except Exception:
|
|
260
|
+
return None
|
|
261
|
+
return None
|
|
262
|
+
|
|
263
|
+
# 1) find in obj
|
|
264
|
+
v = _resolve(obj, key)
|
|
265
|
+
if v is not None:
|
|
266
|
+
return v
|
|
267
|
+
|
|
268
|
+
# 2) find in obj.metadata
|
|
269
|
+
meta = _resolve(obj, "metadata")
|
|
270
|
+
if meta is not None:
|
|
271
|
+
return _resolve(meta, key)
|
|
272
|
+
|
|
273
|
+
return None
|
|
274
|
+
|
|
275
|
+
def _apply_boost_generic(
|
|
276
|
+
self,
|
|
277
|
+
item: TextualMemoryItem,
|
|
278
|
+
base_score: float,
|
|
279
|
+
search_filter: dict | None,
|
|
280
|
+
) -> float:
|
|
281
|
+
"""
|
|
282
|
+
Multiply base_score by (1 + weight) for each matching key in search_filter.
|
|
283
|
+
- key resolution: self._get_attr_or_key(item, key)
|
|
284
|
+
- weight = boost_weights.get(key, self.boost_default)
|
|
285
|
+
- unknown key -> one-time warning
|
|
286
|
+
"""
|
|
287
|
+
if not search_filter:
|
|
288
|
+
return base_score
|
|
289
|
+
|
|
290
|
+
score = float(base_score)
|
|
291
|
+
|
|
292
|
+
for key, wanted in search_filter.items():
|
|
293
|
+
# _get_attr_or_key automatically find key in item and
|
|
294
|
+
# item.metadata ("metadata.user_id" supported)
|
|
295
|
+
resolved = self._get_attr_or_key(item, key)
|
|
296
|
+
|
|
297
|
+
if resolved is None:
|
|
298
|
+
if self.warn_unknown_filter_keys and key not in self._warned_missing_keys:
|
|
299
|
+
logger.warning(
|
|
300
|
+
"[HTTPBGEReranker] search_filter key '%s' not found on TextualMemoryItem or metadata",
|
|
301
|
+
key,
|
|
302
|
+
)
|
|
303
|
+
self._warned_missing_keys.add(key)
|
|
304
|
+
continue
|
|
305
|
+
|
|
306
|
+
if _value_matches(resolved, wanted):
|
|
307
|
+
w = float(self.boost_weights.get(key, self.boost_default))
|
|
308
|
+
if w != 0.0:
|
|
309
|
+
score *= 1.0 + w
|
|
310
|
+
score = min(max(0.0, score), 1.0)
|
|
311
|
+
|
|
312
|
+
return score
|
|
@@ -151,11 +151,253 @@ You are an intelligent keyword extraction system. Your task is to identify and e
|
|
|
151
151
|
Answer:
|
|
152
152
|
"""
|
|
153
153
|
|
|
154
|
+
MEMORY_FILTERING_PROMPT = """
|
|
155
|
+
# Memory Relevance Filtering Task
|
|
156
|
+
|
|
157
|
+
## Role
|
|
158
|
+
You are an intelligent memory filtering system. Your primary function is to analyze memory relevance and filter out memories that are completely unrelated to the user's query history.
|
|
159
|
+
|
|
160
|
+
## Task Description
|
|
161
|
+
Analyze the provided memories and determine which ones are relevant to the user's query history:
|
|
162
|
+
1. Evaluate semantic relationship between each memory and the query history
|
|
163
|
+
2. Identify memories that are completely unrelated or irrelevant
|
|
164
|
+
3. Filter out memories that don't contribute to answering the queries
|
|
165
|
+
4. Preserve memories that provide context, evidence, or relevant information
|
|
166
|
+
|
|
167
|
+
## Relevance Criteria
|
|
168
|
+
A memory is considered RELEVANT if it:
|
|
169
|
+
- Directly answers questions from the query history
|
|
170
|
+
- Provides context or background information related to the queries
|
|
171
|
+
- Contains information that could be useful for understanding the queries
|
|
172
|
+
- Shares semantic similarity with query topics or themes
|
|
173
|
+
- Contains keywords or concepts mentioned in the queries
|
|
174
|
+
|
|
175
|
+
A memory is considered IRRELEVANT if it:
|
|
176
|
+
- Has no semantic connection to any query in the history
|
|
177
|
+
- Discusses completely unrelated topics
|
|
178
|
+
- Contains information that cannot help answer any query
|
|
179
|
+
- Is too generic or vague to be useful
|
|
180
|
+
|
|
181
|
+
## Input Format
|
|
182
|
+
- Query History: List of user queries (chronological order)
|
|
183
|
+
- Memories: List of memory texts to be evaluated
|
|
184
|
+
|
|
185
|
+
## Output Format Requirements
|
|
186
|
+
You MUST output a valid JSON object with EXACTLY the following structure:
|
|
187
|
+
{{
|
|
188
|
+
"relevant_memories": [array_of_memory_indices],
|
|
189
|
+
"filtered_count": <number_of_filtered_memories>,
|
|
190
|
+
"reasoning": "string_explanation"
|
|
191
|
+
}}
|
|
192
|
+
|
|
193
|
+
## Important Notes:
|
|
194
|
+
- Only output the JSON object, nothing else
|
|
195
|
+
- Do not include any markdown formatting or code block notation
|
|
196
|
+
- Ensure all brackets and quotes are properly closed
|
|
197
|
+
- The output must be parseable by a JSON parser
|
|
198
|
+
- Memory indices should correspond to the input order (0-based)
|
|
199
|
+
|
|
200
|
+
## Processing Guidelines
|
|
201
|
+
1. Be conservative in filtering - when in doubt, keep the memory
|
|
202
|
+
2. Consider both direct and indirect relevance
|
|
203
|
+
3. Look for thematic connections, not just exact keyword matches
|
|
204
|
+
4. Preserve memories that provide valuable context
|
|
205
|
+
|
|
206
|
+
## Current Task
|
|
207
|
+
Query History: {query_history}
|
|
208
|
+
Memories to Filter: {memories}
|
|
209
|
+
|
|
210
|
+
Please provide your filtering analysis:
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
MEMORY_REDUNDANCY_FILTERING_PROMPT = """
|
|
214
|
+
# Memory Redundancy Filtering Task
|
|
215
|
+
|
|
216
|
+
## Role
|
|
217
|
+
You are an intelligent memory optimization system. Your primary function is to analyze memories and remove redundancy to improve memory quality and relevance.
|
|
218
|
+
|
|
219
|
+
## Task Description
|
|
220
|
+
Analyze the provided memories and identify redundant ones:
|
|
221
|
+
1. **Redundancy Detection**: Find memories that contain the same core facts relevant to queries
|
|
222
|
+
2. **Best Memory Selection**: Keep only the most concise and focused version of redundant information
|
|
223
|
+
3. **Quality Preservation**: Ensure the final set covers all necessary information without redundancy
|
|
224
|
+
|
|
225
|
+
## Redundancy Detection Criteria
|
|
226
|
+
A memory is considered REDUNDANT if it:
|
|
227
|
+
- Contains the same core fact as another memory that's relevant to the queries
|
|
228
|
+
- Provides the same information but with additional irrelevant details
|
|
229
|
+
- Repeats information that's already covered by a more concise memory
|
|
230
|
+
- Has overlapping content with another memory that serves the same purpose
|
|
231
|
+
|
|
232
|
+
When redundancy is found, KEEP the memory that:
|
|
233
|
+
- Is more concise and focused
|
|
234
|
+
- Contains less irrelevant information
|
|
235
|
+
- Is more directly relevant to the queries
|
|
236
|
+
- Has higher information density
|
|
237
|
+
|
|
238
|
+
## Input Format
|
|
239
|
+
- Query History: List of user queries (chronological order)
|
|
240
|
+
- Memories: List of memory texts to be evaluated
|
|
241
|
+
|
|
242
|
+
## Output Format Requirements
|
|
243
|
+
You MUST output a valid JSON object with EXACTLY the following structure:
|
|
244
|
+
{{
|
|
245
|
+
"kept_memories": [array_of_memory_indices_to_keep],
|
|
246
|
+
"redundant_groups": [
|
|
247
|
+
{{
|
|
248
|
+
"group_id": <number>,
|
|
249
|
+
"memories": [array_of_redundant_memory_indices],
|
|
250
|
+
"kept_memory": <index_of_best_memory_in_group>,
|
|
251
|
+
"reason": "explanation_of_why_this_memory_was_kept"
|
|
252
|
+
}}
|
|
253
|
+
],
|
|
254
|
+
"reasoning": "string_explanation_of_filtering_decisions"
|
|
255
|
+
}}
|
|
256
|
+
|
|
257
|
+
## Important Notes:
|
|
258
|
+
- Only output the JSON object, nothing else
|
|
259
|
+
- Do not include any markdown formatting or code block notation
|
|
260
|
+
- Ensure all brackets and quotes are properly closed
|
|
261
|
+
- The output must be parseable by a JSON parser
|
|
262
|
+
- Memory indices should correspond to the input order (0-based)
|
|
263
|
+
- Be conservative in filtering - when in doubt, keep the memory
|
|
264
|
+
- Focus on semantic similarity, not just exact text matches
|
|
265
|
+
|
|
266
|
+
## Processing Guidelines
|
|
267
|
+
1. First identify which memories are relevant to the queries
|
|
268
|
+
2. Group relevant memories by semantic similarity and core facts
|
|
269
|
+
3. Within each group, select the best memory (most concise, least noise)
|
|
270
|
+
4. Ensure the final set covers all necessary information without redundancy
|
|
271
|
+
|
|
272
|
+
## Current Task
|
|
273
|
+
Query History: {query_history}
|
|
274
|
+
Memories to Filter: {memories}
|
|
275
|
+
|
|
276
|
+
Please provide your redundancy filtering analysis:
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
MEMORY_COMBINED_FILTERING_PROMPT = """
|
|
280
|
+
# Memory Combined Filtering Task
|
|
281
|
+
|
|
282
|
+
## Role
|
|
283
|
+
You are an intelligent memory optimization system. Your primary function is to analyze memories and perform two types of filtering in sequence:
|
|
284
|
+
1. **Unrelated Memory Removal**: Remove memories that are completely unrelated to the user's query history
|
|
285
|
+
2. **Redundancy Removal**: Remove redundant memories by keeping only the most informative version
|
|
286
|
+
|
|
287
|
+
## Task Description
|
|
288
|
+
Analyze the provided memories and perform comprehensive filtering:
|
|
289
|
+
1. **First Step - Unrelated Filtering**: Identify and remove memories that have no semantic connection to any query
|
|
290
|
+
2. **Second Step - Redundancy Filtering**: Group similar memories and keep only the best version from each group
|
|
291
|
+
|
|
292
|
+
## Unrelated Memory Detection Criteria
|
|
293
|
+
A memory is considered UNRELATED if it:
|
|
294
|
+
- Has no semantic connection to any query in the history
|
|
295
|
+
- Discusses completely unrelated topics
|
|
296
|
+
- Contains information that cannot help answer any query
|
|
297
|
+
- Is too generic or vague to be useful
|
|
298
|
+
|
|
299
|
+
## Redundancy Detection Criteria
|
|
300
|
+
A memory is considered REDUNDANT if it:
|
|
301
|
+
- Contains the same core fact as another memory that's relevant to the queries
|
|
302
|
+
- Provides the same information but with additional irrelevant details
|
|
303
|
+
- Repeats information that's already covered by a more concise memory
|
|
304
|
+
- Has overlapping content with another memory that serves the same purpose
|
|
305
|
+
|
|
306
|
+
When redundancy is found, KEEP the memory that:
|
|
307
|
+
- Is more concise and focused
|
|
308
|
+
- Contains less irrelevant information
|
|
309
|
+
- Is more directly relevant to the queries
|
|
310
|
+
- Has higher information density
|
|
311
|
+
|
|
312
|
+
## Input Format
|
|
313
|
+
- Query History: List of user queries (chronological order)
|
|
314
|
+
- Memories: List of memory texts to be evaluated
|
|
315
|
+
|
|
316
|
+
## Output Format Requirements
|
|
317
|
+
You MUST output a valid JSON object with EXACTLY the following structure:
|
|
318
|
+
{{
|
|
319
|
+
"kept_memories": [array_of_memory_indices_to_keep],
|
|
320
|
+
"unrelated_removed_count": <number_of_unrelated_memories_removed>,
|
|
321
|
+
"redundant_removed_count": <number_of_redundant_memories_removed>,
|
|
322
|
+
"redundant_groups": [
|
|
323
|
+
{{
|
|
324
|
+
"group_id": <number>,
|
|
325
|
+
"memories": [array_of_redundant_memory_indices],
|
|
326
|
+
"kept_memory": <index_of_best_memory_in_group>,
|
|
327
|
+
"reason": "explanation_of_why_this_memory_was_kept"
|
|
328
|
+
}}
|
|
329
|
+
],
|
|
330
|
+
"reasoning": "string_explanation_of_filtering_decisions"
|
|
331
|
+
}}
|
|
332
|
+
|
|
333
|
+
## Important Notes:
|
|
334
|
+
- Only output the JSON object, nothing else
|
|
335
|
+
- Do not include any markdown formatting or code block notation
|
|
336
|
+
- Ensure all brackets and quotes are properly closed
|
|
337
|
+
- The output must be parseable by a JSON parser
|
|
338
|
+
- Memory indices should correspond to the input order (0-based)
|
|
339
|
+
- Be conservative in filtering - when in doubt, keep the memory
|
|
340
|
+
- Focus on semantic similarity, not just exact text matches
|
|
341
|
+
|
|
342
|
+
## Processing Guidelines
|
|
343
|
+
1. **First, identify unrelated memories** and mark them for removal
|
|
344
|
+
2. **Then, group remaining memories** by semantic similarity and core facts
|
|
345
|
+
3. **Within each group, select the best memory** (most concise, least noise)
|
|
346
|
+
4. **Ensure the final set covers all necessary information** without redundancy
|
|
347
|
+
5. **Count how many memories were removed** for each reason
|
|
348
|
+
|
|
349
|
+
## Current Task
|
|
350
|
+
Query History: {query_history}
|
|
351
|
+
Memories to Filter: {memories}
|
|
352
|
+
|
|
353
|
+
Please provide your combined filtering analysis:
|
|
354
|
+
"""
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
MEMORY_ANSWER_ABILITY_EVALUATION_PROMPT = """
|
|
358
|
+
# Memory Answer Ability Evaluation Task
|
|
359
|
+
|
|
360
|
+
## Task
|
|
361
|
+
Evaluate whether the provided memories contain sufficient information to answer the user's query.
|
|
362
|
+
|
|
363
|
+
## Evaluation Criteria
|
|
364
|
+
Consider these factors:
|
|
365
|
+
1. **Answer completeness**: Do the memories cover all aspects of the query?
|
|
366
|
+
2. **Evidence relevance**: Do the memories directly support answering the query?
|
|
367
|
+
3. **Detail specificity**: Do the memories contain necessary granularity?
|
|
368
|
+
4. **Information gaps**: Are there obvious missing pieces of information?
|
|
369
|
+
|
|
370
|
+
## Decision Rules
|
|
371
|
+
- Return `True` for "result" ONLY when memories provide complete, relevant answers
|
|
372
|
+
- Return `False` for "result" if memories are insufficient, irrelevant, or incomplete
|
|
373
|
+
|
|
374
|
+
## User Query
|
|
375
|
+
{query}
|
|
376
|
+
|
|
377
|
+
## Available Memories
|
|
378
|
+
{memory_list}
|
|
379
|
+
|
|
380
|
+
## Required Output
|
|
381
|
+
Return a JSON object with this exact structure:
|
|
382
|
+
{{
|
|
383
|
+
"result": <boolean>,
|
|
384
|
+
"reason": "<brief explanation of your decision>"
|
|
385
|
+
}}
|
|
386
|
+
|
|
387
|
+
## Instructions
|
|
388
|
+
- Only output the JSON object, nothing else
|
|
389
|
+
- Be conservative: if there's any doubt about completeness, return true
|
|
390
|
+
- Focus on whether the memories can fully answer the query without additional information
|
|
391
|
+
"""
|
|
154
392
|
|
|
155
393
|
PROMPT_MAPPING = {
|
|
156
394
|
"intent_recognizing": INTENT_RECOGNIZING_PROMPT,
|
|
157
395
|
"memory_reranking": MEMORY_RERANKING_PROMPT,
|
|
158
396
|
"query_keywords_extraction": QUERY_KEYWORDS_EXTRACTION_PROMPT,
|
|
397
|
+
"memory_filtering": MEMORY_FILTERING_PROMPT,
|
|
398
|
+
"memory_redundancy_filtering": MEMORY_REDUNDANCY_FILTERING_PROMPT,
|
|
399
|
+
"memory_combined_filtering": MEMORY_COMBINED_FILTERING_PROMPT,
|
|
400
|
+
"memory_answer_ability_evaluation": MEMORY_ANSWER_ABILITY_EVALUATION_PROMPT,
|
|
159
401
|
}
|
|
160
402
|
|
|
161
403
|
MEMORY_ASSEMBLY_TEMPLATE = """The retrieved memories are listed as follows:\n\n {memory_text}"""
|