MemoryOS 0.2.2__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-0.2.2.dist-info → memoryos-1.0.1.dist-info}/METADATA +7 -1
- {memoryos-0.2.2.dist-info → memoryos-1.0.1.dist-info}/RECORD +81 -66
- memos/__init__.py +1 -1
- memos/api/config.py +31 -8
- memos/api/context/context.py +1 -1
- memos/api/context/context_thread.py +96 -0
- 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/internet_retriever.py +13 -0
- memos/configs/mem_scheduler.py +38 -16
- memos/configs/memory.py +13 -0
- memos/configs/reranker.py +18 -0
- memos/graph_dbs/base.py +33 -4
- memos/graph_dbs/nebular.py +631 -236
- memos/graph_dbs/neo4j.py +18 -7
- memos/graph_dbs/neo4j_community.py +6 -3
- memos/llms/vllm.py +2 -0
- memos/log.py +125 -8
- memos/mem_os/core.py +49 -11
- memos/mem_os/main.py +1 -1
- memos/mem_os/product.py +392 -215
- memos/mem_os/utils/default_config.py +1 -1
- memos/mem_os/utils/format_utils.py +11 -47
- memos/mem_os/utils/reference_utils.py +153 -0
- memos/mem_reader/simple_struct.py +112 -43
- memos/mem_scheduler/base_scheduler.py +58 -55
- memos/mem_scheduler/{modules → general_modules}/base.py +1 -2
- memos/mem_scheduler/{modules → general_modules}/dispatcher.py +54 -15
- memos/mem_scheduler/{modules → general_modules}/rabbitmq_service.py +4 -4
- memos/mem_scheduler/{modules → general_modules}/redis_service.py +1 -1
- memos/mem_scheduler/{modules → general_modules}/retriever.py +19 -5
- memos/mem_scheduler/{modules → general_modules}/scheduler_logger.py +10 -4
- memos/mem_scheduler/general_scheduler.py +110 -67
- memos/mem_scheduler/monitors/__init__.py +0 -0
- memos/mem_scheduler/monitors/dispatcher_monitor.py +305 -0
- memos/mem_scheduler/{modules/monitor.py → monitors/general_monitor.py} +57 -19
- memos/mem_scheduler/mos_for_test_scheduler.py +7 -1
- memos/mem_scheduler/schemas/general_schemas.py +3 -2
- memos/mem_scheduler/schemas/message_schemas.py +2 -1
- memos/mem_scheduler/schemas/monitor_schemas.py +10 -2
- memos/mem_scheduler/utils/misc_utils.py +43 -2
- memos/mem_user/mysql_user_manager.py +4 -2
- memos/memories/activation/item.py +1 -1
- memos/memories/activation/kv.py +20 -8
- memos/memories/textual/base.py +1 -1
- memos/memories/textual/general.py +1 -1
- memos/memories/textual/item.py +1 -1
- memos/memories/textual/tree.py +31 -1
- memos/memories/textual/tree_text_memory/organize/{conflict.py → handler.py} +30 -48
- memos/memories/textual/tree_text_memory/organize/manager.py +8 -96
- memos/memories/textual/tree_text_memory/organize/relation_reason_detector.py +2 -0
- memos/memories/textual/tree_text_memory/organize/reorganizer.py +102 -140
- memos/memories/textual/tree_text_memory/retrieve/bochasearch.py +231 -0
- memos/memories/textual/tree_text_memory/retrieve/internet_retriever_factory.py +9 -0
- memos/memories/textual/tree_text_memory/retrieve/recall.py +67 -10
- memos/memories/textual/tree_text_memory/retrieve/reranker.py +1 -1
- memos/memories/textual/tree_text_memory/retrieve/searcher.py +246 -134
- memos/memories/textual/tree_text_memory/retrieve/task_goal_parser.py +7 -2
- memos/memories/textual/tree_text_memory/retrieve/utils.py +7 -5
- memos/memos_tools/lockfree_dict.py +120 -0
- memos/memos_tools/notification_utils.py +46 -0
- memos/memos_tools/thread_safe_dict.py +288 -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 +290 -39
- memos/templates/mem_scheduler_prompts.py +23 -10
- memos/templates/mos_prompts.py +133 -31
- memos/templates/tree_reorganize_prompts.py +24 -17
- memos/utils.py +19 -0
- memos/memories/textual/tree_text_memory/organize/redundancy.py +0 -193
- {memoryos-0.2.2.dist-info → memoryos-1.0.1.dist-info}/LICENSE +0 -0
- {memoryos-0.2.2.dist-info → memoryos-1.0.1.dist-info}/WHEEL +0 -0
- {memoryos-0.2.2.dist-info → memoryos-1.0.1.dist-info}/entry_points.txt +0 -0
- /memos/mem_scheduler/{modules → general_modules}/__init__.py +0 -0
- /memos/mem_scheduler/{modules → general_modules}/misc.py +0 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Notification utilities for MemOS product.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
import asyncio
|
|
5
6
|
import logging
|
|
6
7
|
|
|
7
8
|
from collections.abc import Callable
|
|
@@ -51,6 +52,51 @@ def send_online_bot_notification(
|
|
|
51
52
|
logger.warning(f"Failed to send online bot notification: {e}")
|
|
52
53
|
|
|
53
54
|
|
|
55
|
+
async def send_online_bot_notification_async(
|
|
56
|
+
online_bot: Callable | None,
|
|
57
|
+
header_name: str,
|
|
58
|
+
sub_title_name: str,
|
|
59
|
+
title_color: str,
|
|
60
|
+
other_data1: dict[str, Any],
|
|
61
|
+
other_data2: dict[str, Any],
|
|
62
|
+
emoji: dict[str, str],
|
|
63
|
+
) -> None:
|
|
64
|
+
"""
|
|
65
|
+
Send notification via online_bot asynchronously if available.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
online_bot: The online_bot function or None
|
|
69
|
+
header_name: Header name for the report
|
|
70
|
+
sub_title_name: Subtitle for the report
|
|
71
|
+
title_color: Title color
|
|
72
|
+
other_data1: First data dict
|
|
73
|
+
other_data2: Second data dict
|
|
74
|
+
emoji: Emoji configuration dict
|
|
75
|
+
"""
|
|
76
|
+
if online_bot is None:
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
# Run the potentially blocking notification in a thread pool
|
|
81
|
+
loop = asyncio.get_event_loop()
|
|
82
|
+
await loop.run_in_executor(
|
|
83
|
+
None,
|
|
84
|
+
lambda: online_bot(
|
|
85
|
+
header_name=header_name,
|
|
86
|
+
sub_title_name=sub_title_name,
|
|
87
|
+
title_color=title_color,
|
|
88
|
+
other_data1=other_data1,
|
|
89
|
+
other_data2=other_data2,
|
|
90
|
+
emoji=emoji,
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
logger.info(f"Online bot notification sent successfully (async): {header_name}")
|
|
95
|
+
|
|
96
|
+
except Exception as e:
|
|
97
|
+
logger.warning(f"Failed to send online bot notification (async): {e}")
|
|
98
|
+
|
|
99
|
+
|
|
54
100
|
def send_error_bot_notification(
|
|
55
101
|
error_bot: Callable | None,
|
|
56
102
|
err: str,
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Thread-safe dictionary wrapper for concurrent access with optimized read-write locks.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import threading
|
|
6
|
+
|
|
7
|
+
from collections.abc import ItemsView, Iterator, KeysView, ValuesView
|
|
8
|
+
from typing import Generic, TypeVar
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
K = TypeVar("K")
|
|
12
|
+
V = TypeVar("V")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ReadWriteLock:
|
|
16
|
+
"""A simple read-write lock implementation. use for product-server scenario"""
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
self._read_ready = threading.Condition(threading.RLock())
|
|
20
|
+
self._readers = 0
|
|
21
|
+
|
|
22
|
+
def acquire_read(self):
|
|
23
|
+
"""Acquire a read lock. Multiple readers can hold the lock simultaneously."""
|
|
24
|
+
self._read_ready.acquire()
|
|
25
|
+
try:
|
|
26
|
+
self._readers += 1
|
|
27
|
+
finally:
|
|
28
|
+
self._read_ready.release()
|
|
29
|
+
|
|
30
|
+
def release_read(self):
|
|
31
|
+
"""Release a read lock."""
|
|
32
|
+
self._read_ready.acquire()
|
|
33
|
+
try:
|
|
34
|
+
self._readers -= 1
|
|
35
|
+
if self._readers == 0:
|
|
36
|
+
self._read_ready.notify_all()
|
|
37
|
+
finally:
|
|
38
|
+
self._read_ready.release()
|
|
39
|
+
|
|
40
|
+
def acquire_write(self):
|
|
41
|
+
"""Acquire a write lock. Only one writer can hold the lock."""
|
|
42
|
+
self._read_ready.acquire()
|
|
43
|
+
while self._readers > 0:
|
|
44
|
+
self._read_ready.wait()
|
|
45
|
+
|
|
46
|
+
def release_write(self):
|
|
47
|
+
"""Release a write lock."""
|
|
48
|
+
self._read_ready.release()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ThreadSafeDict(Generic[K, V]):
|
|
52
|
+
"""
|
|
53
|
+
A thread-safe dictionary wrapper with optimized read-write locks.
|
|
54
|
+
|
|
55
|
+
This class allows multiple concurrent readers while ensuring exclusive access for writers.
|
|
56
|
+
Read operations (get, contains, iteration) can happen concurrently.
|
|
57
|
+
Write operations (set, delete, update) are exclusive.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(self, initial_dict: dict[K, V] | None = None):
|
|
61
|
+
"""
|
|
62
|
+
Initialize the thread-safe dictionary.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
initial_dict: Optional initial dictionary to copy from
|
|
66
|
+
"""
|
|
67
|
+
self._dict: dict[K, V] = initial_dict.copy() if initial_dict else {}
|
|
68
|
+
self._lock = ReadWriteLock()
|
|
69
|
+
|
|
70
|
+
def __getitem__(self, key: K) -> V:
|
|
71
|
+
"""Get item by key."""
|
|
72
|
+
self._lock.acquire_read()
|
|
73
|
+
try:
|
|
74
|
+
return self._dict[key]
|
|
75
|
+
finally:
|
|
76
|
+
self._lock.release_read()
|
|
77
|
+
|
|
78
|
+
def __setitem__(self, key: K, value: V) -> None:
|
|
79
|
+
"""Set item by key."""
|
|
80
|
+
self._lock.acquire_write()
|
|
81
|
+
try:
|
|
82
|
+
self._dict[key] = value
|
|
83
|
+
finally:
|
|
84
|
+
self._lock.release_write()
|
|
85
|
+
|
|
86
|
+
def __delitem__(self, key: K) -> None:
|
|
87
|
+
"""Delete item by key."""
|
|
88
|
+
self._lock.acquire_write()
|
|
89
|
+
try:
|
|
90
|
+
del self._dict[key]
|
|
91
|
+
finally:
|
|
92
|
+
self._lock.release_write()
|
|
93
|
+
|
|
94
|
+
def __contains__(self, key: K) -> bool:
|
|
95
|
+
"""Check if key exists in dictionary."""
|
|
96
|
+
self._lock.acquire_read()
|
|
97
|
+
try:
|
|
98
|
+
return key in self._dict
|
|
99
|
+
finally:
|
|
100
|
+
self._lock.release_read()
|
|
101
|
+
|
|
102
|
+
def __len__(self) -> int:
|
|
103
|
+
"""Get length of dictionary."""
|
|
104
|
+
self._lock.acquire_read()
|
|
105
|
+
try:
|
|
106
|
+
return len(self._dict)
|
|
107
|
+
finally:
|
|
108
|
+
self._lock.release_read()
|
|
109
|
+
|
|
110
|
+
def __bool__(self) -> bool:
|
|
111
|
+
"""Check if dictionary is not empty."""
|
|
112
|
+
self._lock.acquire_read()
|
|
113
|
+
try:
|
|
114
|
+
return bool(self._dict)
|
|
115
|
+
finally:
|
|
116
|
+
self._lock.release_read()
|
|
117
|
+
|
|
118
|
+
def __iter__(self) -> Iterator[K]:
|
|
119
|
+
"""Iterate over keys. Returns a snapshot to avoid iteration issues."""
|
|
120
|
+
self._lock.acquire_read()
|
|
121
|
+
try:
|
|
122
|
+
# Return a snapshot of keys to avoid iteration issues
|
|
123
|
+
return iter(list(self._dict.keys()))
|
|
124
|
+
finally:
|
|
125
|
+
self._lock.release_read()
|
|
126
|
+
|
|
127
|
+
def get(self, key: K, default: V | None = None) -> V:
|
|
128
|
+
"""Get item by key with optional default."""
|
|
129
|
+
self._lock.acquire_read()
|
|
130
|
+
try:
|
|
131
|
+
return self._dict.get(key, default)
|
|
132
|
+
finally:
|
|
133
|
+
self._lock.release_read()
|
|
134
|
+
|
|
135
|
+
def pop(self, key: K, *args) -> V:
|
|
136
|
+
"""Pop item by key."""
|
|
137
|
+
self._lock.acquire_write()
|
|
138
|
+
try:
|
|
139
|
+
return self._dict.pop(key, *args)
|
|
140
|
+
finally:
|
|
141
|
+
self._lock.release_write()
|
|
142
|
+
|
|
143
|
+
def update(self, *args, **kwargs) -> None:
|
|
144
|
+
"""Update dictionary."""
|
|
145
|
+
self._lock.acquire_write()
|
|
146
|
+
try:
|
|
147
|
+
self._dict.update(*args, **kwargs)
|
|
148
|
+
finally:
|
|
149
|
+
self._lock.release_write()
|
|
150
|
+
|
|
151
|
+
def clear(self) -> None:
|
|
152
|
+
"""Clear all items."""
|
|
153
|
+
self._lock.acquire_write()
|
|
154
|
+
try:
|
|
155
|
+
self._dict.clear()
|
|
156
|
+
finally:
|
|
157
|
+
self._lock.release_write()
|
|
158
|
+
|
|
159
|
+
def keys(self) -> KeysView[K]:
|
|
160
|
+
"""Get dictionary keys view (snapshot)."""
|
|
161
|
+
self._lock.acquire_read()
|
|
162
|
+
try:
|
|
163
|
+
return list(self._dict.keys())
|
|
164
|
+
finally:
|
|
165
|
+
self._lock.release_read()
|
|
166
|
+
|
|
167
|
+
def values(self) -> ValuesView[V]:
|
|
168
|
+
"""Get dictionary values view (snapshot)."""
|
|
169
|
+
self._lock.acquire_read()
|
|
170
|
+
try:
|
|
171
|
+
return list(self._dict.values())
|
|
172
|
+
finally:
|
|
173
|
+
self._lock.release_read()
|
|
174
|
+
|
|
175
|
+
def items(self) -> ItemsView[K, V]:
|
|
176
|
+
"""Get dictionary items view (snapshot)."""
|
|
177
|
+
self._lock.acquire_read()
|
|
178
|
+
try:
|
|
179
|
+
return list(self._dict.items())
|
|
180
|
+
finally:
|
|
181
|
+
self._lock.release_read()
|
|
182
|
+
|
|
183
|
+
def copy(self) -> dict[K, V]:
|
|
184
|
+
"""Create a copy of the dictionary."""
|
|
185
|
+
self._lock.acquire_read()
|
|
186
|
+
try:
|
|
187
|
+
return self._dict.copy()
|
|
188
|
+
finally:
|
|
189
|
+
self._lock.release_read()
|
|
190
|
+
|
|
191
|
+
def setdefault(self, key: K, default: V | None = None) -> V:
|
|
192
|
+
"""Set default value for key if not exists."""
|
|
193
|
+
self._lock.acquire_write()
|
|
194
|
+
try:
|
|
195
|
+
return self._dict.setdefault(key, default)
|
|
196
|
+
finally:
|
|
197
|
+
self._lock.release_write()
|
|
198
|
+
|
|
199
|
+
def __repr__(self) -> str:
|
|
200
|
+
"""String representation."""
|
|
201
|
+
self._lock.acquire_read()
|
|
202
|
+
try:
|
|
203
|
+
return f"ThreadSafeDict({self._dict})"
|
|
204
|
+
finally:
|
|
205
|
+
self._lock.release_read()
|
|
206
|
+
|
|
207
|
+
def __str__(self) -> str:
|
|
208
|
+
"""String representation."""
|
|
209
|
+
self._lock.acquire_read()
|
|
210
|
+
try:
|
|
211
|
+
return str(self._dict)
|
|
212
|
+
finally:
|
|
213
|
+
self._lock.release_read()
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class SimpleThreadSafeDict(Generic[K, V]):
|
|
217
|
+
"""
|
|
218
|
+
Simple thread-safe dictionary with exclusive locks for all operations.
|
|
219
|
+
Use this if you prefer simplicity over performance.
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
def __init__(self, initial_dict: dict[K, V] | None = None):
|
|
223
|
+
self._dict: dict[K, V] = initial_dict.copy() if initial_dict else {}
|
|
224
|
+
self._lock = threading.RLock()
|
|
225
|
+
|
|
226
|
+
def __getitem__(self, key: K) -> V:
|
|
227
|
+
with self._lock:
|
|
228
|
+
return self._dict[key]
|
|
229
|
+
|
|
230
|
+
def __setitem__(self, key: K, value: V) -> None:
|
|
231
|
+
with self._lock:
|
|
232
|
+
self._dict[key] = value
|
|
233
|
+
|
|
234
|
+
def __delitem__(self, key: K) -> None:
|
|
235
|
+
with self._lock:
|
|
236
|
+
del self._dict[key]
|
|
237
|
+
|
|
238
|
+
def __contains__(self, key: K) -> bool:
|
|
239
|
+
with self._lock:
|
|
240
|
+
return key in self._dict
|
|
241
|
+
|
|
242
|
+
def __len__(self) -> int:
|
|
243
|
+
with self._lock:
|
|
244
|
+
return len(self._dict)
|
|
245
|
+
|
|
246
|
+
def __bool__(self) -> bool:
|
|
247
|
+
with self._lock:
|
|
248
|
+
return bool(self._dict)
|
|
249
|
+
|
|
250
|
+
def __iter__(self) -> Iterator[K]:
|
|
251
|
+
with self._lock:
|
|
252
|
+
return iter(list(self._dict.keys()))
|
|
253
|
+
|
|
254
|
+
def get(self, key: K, default: V | None = None) -> V:
|
|
255
|
+
with self._lock:
|
|
256
|
+
return self._dict.get(key, default)
|
|
257
|
+
|
|
258
|
+
def pop(self, key: K, *args) -> V:
|
|
259
|
+
with self._lock:
|
|
260
|
+
return self._dict.pop(key, *args)
|
|
261
|
+
|
|
262
|
+
def update(self, *args, **kwargs) -> None:
|
|
263
|
+
with self._lock:
|
|
264
|
+
self._dict.update(*args, **kwargs)
|
|
265
|
+
|
|
266
|
+
def clear(self) -> None:
|
|
267
|
+
with self._lock:
|
|
268
|
+
self._dict.clear()
|
|
269
|
+
|
|
270
|
+
def keys(self):
|
|
271
|
+
with self._lock:
|
|
272
|
+
return list(self._dict.keys())
|
|
273
|
+
|
|
274
|
+
def values(self):
|
|
275
|
+
with self._lock:
|
|
276
|
+
return list(self._dict.values())
|
|
277
|
+
|
|
278
|
+
def items(self):
|
|
279
|
+
with self._lock:
|
|
280
|
+
return list(self._dict.items())
|
|
281
|
+
|
|
282
|
+
def copy(self) -> dict[K, V]:
|
|
283
|
+
with self._lock:
|
|
284
|
+
return self._dict.copy()
|
|
285
|
+
|
|
286
|
+
def setdefault(self, key: K, default: V | None = None) -> V:
|
|
287
|
+
with self._lock:
|
|
288
|
+
return self._dict.setdefault(key, default)
|
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]]
|