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.

Files changed (82) hide show
  1. {memoryos-0.2.2.dist-info → memoryos-1.0.1.dist-info}/METADATA +7 -1
  2. {memoryos-0.2.2.dist-info → memoryos-1.0.1.dist-info}/RECORD +81 -66
  3. memos/__init__.py +1 -1
  4. memos/api/config.py +31 -8
  5. memos/api/context/context.py +1 -1
  6. memos/api/context/context_thread.py +96 -0
  7. memos/api/middleware/request_context.py +94 -0
  8. memos/api/product_api.py +5 -1
  9. memos/api/product_models.py +16 -0
  10. memos/api/routers/product_router.py +39 -3
  11. memos/api/start_api.py +3 -0
  12. memos/configs/internet_retriever.py +13 -0
  13. memos/configs/mem_scheduler.py +38 -16
  14. memos/configs/memory.py +13 -0
  15. memos/configs/reranker.py +18 -0
  16. memos/graph_dbs/base.py +33 -4
  17. memos/graph_dbs/nebular.py +631 -236
  18. memos/graph_dbs/neo4j.py +18 -7
  19. memos/graph_dbs/neo4j_community.py +6 -3
  20. memos/llms/vllm.py +2 -0
  21. memos/log.py +125 -8
  22. memos/mem_os/core.py +49 -11
  23. memos/mem_os/main.py +1 -1
  24. memos/mem_os/product.py +392 -215
  25. memos/mem_os/utils/default_config.py +1 -1
  26. memos/mem_os/utils/format_utils.py +11 -47
  27. memos/mem_os/utils/reference_utils.py +153 -0
  28. memos/mem_reader/simple_struct.py +112 -43
  29. memos/mem_scheduler/base_scheduler.py +58 -55
  30. memos/mem_scheduler/{modules → general_modules}/base.py +1 -2
  31. memos/mem_scheduler/{modules → general_modules}/dispatcher.py +54 -15
  32. memos/mem_scheduler/{modules → general_modules}/rabbitmq_service.py +4 -4
  33. memos/mem_scheduler/{modules → general_modules}/redis_service.py +1 -1
  34. memos/mem_scheduler/{modules → general_modules}/retriever.py +19 -5
  35. memos/mem_scheduler/{modules → general_modules}/scheduler_logger.py +10 -4
  36. memos/mem_scheduler/general_scheduler.py +110 -67
  37. memos/mem_scheduler/monitors/__init__.py +0 -0
  38. memos/mem_scheduler/monitors/dispatcher_monitor.py +305 -0
  39. memos/mem_scheduler/{modules/monitor.py → monitors/general_monitor.py} +57 -19
  40. memos/mem_scheduler/mos_for_test_scheduler.py +7 -1
  41. memos/mem_scheduler/schemas/general_schemas.py +3 -2
  42. memos/mem_scheduler/schemas/message_schemas.py +2 -1
  43. memos/mem_scheduler/schemas/monitor_schemas.py +10 -2
  44. memos/mem_scheduler/utils/misc_utils.py +43 -2
  45. memos/mem_user/mysql_user_manager.py +4 -2
  46. memos/memories/activation/item.py +1 -1
  47. memos/memories/activation/kv.py +20 -8
  48. memos/memories/textual/base.py +1 -1
  49. memos/memories/textual/general.py +1 -1
  50. memos/memories/textual/item.py +1 -1
  51. memos/memories/textual/tree.py +31 -1
  52. memos/memories/textual/tree_text_memory/organize/{conflict.py → handler.py} +30 -48
  53. memos/memories/textual/tree_text_memory/organize/manager.py +8 -96
  54. memos/memories/textual/tree_text_memory/organize/relation_reason_detector.py +2 -0
  55. memos/memories/textual/tree_text_memory/organize/reorganizer.py +102 -140
  56. memos/memories/textual/tree_text_memory/retrieve/bochasearch.py +231 -0
  57. memos/memories/textual/tree_text_memory/retrieve/internet_retriever_factory.py +9 -0
  58. memos/memories/textual/tree_text_memory/retrieve/recall.py +67 -10
  59. memos/memories/textual/tree_text_memory/retrieve/reranker.py +1 -1
  60. memos/memories/textual/tree_text_memory/retrieve/searcher.py +246 -134
  61. memos/memories/textual/tree_text_memory/retrieve/task_goal_parser.py +7 -2
  62. memos/memories/textual/tree_text_memory/retrieve/utils.py +7 -5
  63. memos/memos_tools/lockfree_dict.py +120 -0
  64. memos/memos_tools/notification_utils.py +46 -0
  65. memos/memos_tools/thread_safe_dict.py +288 -0
  66. memos/reranker/__init__.py +4 -0
  67. memos/reranker/base.py +24 -0
  68. memos/reranker/cosine_local.py +95 -0
  69. memos/reranker/factory.py +43 -0
  70. memos/reranker/http_bge.py +99 -0
  71. memos/reranker/noop.py +16 -0
  72. memos/templates/mem_reader_prompts.py +290 -39
  73. memos/templates/mem_scheduler_prompts.py +23 -10
  74. memos/templates/mos_prompts.py +133 -31
  75. memos/templates/tree_reorganize_prompts.py +24 -17
  76. memos/utils.py +19 -0
  77. memos/memories/textual/tree_text_memory/organize/redundancy.py +0 -193
  78. {memoryos-0.2.2.dist-info → memoryos-1.0.1.dist-info}/LICENSE +0 -0
  79. {memoryos-0.2.2.dist-info → memoryos-1.0.1.dist-info}/WHEEL +0 -0
  80. {memoryos-0.2.2.dist-info → memoryos-1.0.1.dist-info}/entry_points.txt +0 -0
  81. /memos/mem_scheduler/{modules → general_modules}/__init__.py +0 -0
  82. /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)
@@ -0,0 +1,4 @@
1
+ from .factory import RerankerFactory
2
+
3
+
4
+ __all__ = ["RerankerFactory"]
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]]