MemoryOS 0.1.12__py3-none-any.whl → 0.1.13__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.1.12.dist-info → memoryos-0.1.13.dist-info}/METADATA +42 -11
- {memoryos-0.1.12.dist-info → memoryos-0.1.13.dist-info}/RECORD +18 -13
- memos/__init__.py +1 -1
- memos/configs/internet_retriever.py +81 -0
- memos/configs/mem_os.py +4 -0
- memos/configs/memory.py +6 -1
- memos/mem_os/main.py +491 -0
- memos/mem_user/user_manager.py +10 -0
- memos/memories/textual/tree.py +34 -2
- memos/memories/textual/tree_text_memory/retrieve/internet_retriever.py +263 -0
- memos/memories/textual/tree_text_memory/retrieve/internet_retriever_factory.py +89 -0
- memos/memories/textual/tree_text_memory/retrieve/reasoner.py +1 -4
- memos/memories/textual/tree_text_memory/retrieve/searcher.py +46 -4
- memos/memories/textual/tree_text_memory/retrieve/task_goal_parser.py +3 -3
- memos/memories/textual/tree_text_memory/retrieve/xinyusearch.py +335 -0
- memos/templates/mos_prompts.py +63 -0
- {memoryos-0.1.12.dist-info → memoryos-0.1.13.dist-info}/LICENSE +0 -0
- {memoryos-0.1.12.dist-info → memoryos-0.1.13.dist-info}/WHEEL +0 -0
memos/mem_os/main.py
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
|
+
import concurrent.futures
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
1
6
|
from memos.configs.mem_os import MOSConfig
|
|
7
|
+
from memos.llms.factory import LLMFactory
|
|
8
|
+
from memos.log import get_logger
|
|
2
9
|
from memos.mem_os.core import MOSCore
|
|
10
|
+
from memos.memories.textual.base import BaseTextMemory
|
|
11
|
+
from memos.templates.mos_prompts import (
|
|
12
|
+
COT_DECOMPOSE_PROMPT,
|
|
13
|
+
PRO_MODE_WELCOME_MESSAGE,
|
|
14
|
+
SYNTHESIS_PROMPT,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
logger = get_logger(__name__)
|
|
3
19
|
|
|
4
20
|
|
|
5
21
|
class MOS(MOSCore):
|
|
@@ -9,4 +25,479 @@ class MOS(MOSCore):
|
|
|
9
25
|
"""
|
|
10
26
|
|
|
11
27
|
def __init__(self, config: MOSConfig):
|
|
28
|
+
self.enable_cot = config.PRO_MODE
|
|
29
|
+
if config.PRO_MODE:
|
|
30
|
+
print(PRO_MODE_WELCOME_MESSAGE)
|
|
31
|
+
logger.info(PRO_MODE_WELCOME_MESSAGE)
|
|
12
32
|
super().__init__(config)
|
|
33
|
+
|
|
34
|
+
def chat(self, query: str, user_id: str | None = None) -> str:
|
|
35
|
+
"""
|
|
36
|
+
Enhanced chat method with optional CoT (Chain of Thought) enhancement.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
query (str): The user's query.
|
|
40
|
+
user_id (str, optional): User ID for context.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
str: The response from the MOS.
|
|
44
|
+
"""
|
|
45
|
+
# Check if CoT enhancement is enabled (either explicitly or via PRO mode)
|
|
46
|
+
|
|
47
|
+
if not self.enable_cot:
|
|
48
|
+
# Use the original chat method from core
|
|
49
|
+
return super().chat(query, user_id)
|
|
50
|
+
|
|
51
|
+
# Enhanced chat with CoT decomposition
|
|
52
|
+
return self._chat_with_cot_enhancement(query, user_id)
|
|
53
|
+
|
|
54
|
+
def _chat_with_cot_enhancement(self, query: str, user_id: str | None = None) -> str:
|
|
55
|
+
"""
|
|
56
|
+
Chat with CoT enhancement for complex query decomposition.
|
|
57
|
+
This method includes all the same validation and processing logic as the core chat method.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
query (str): The user's query.
|
|
61
|
+
user_id (str, optional): User ID for context.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
str: The enhanced response.
|
|
65
|
+
"""
|
|
66
|
+
# Step 1: Perform all the same validation and setup as core chat method
|
|
67
|
+
target_user_id = user_id if user_id is not None else self.user_id
|
|
68
|
+
accessible_cubes = self.user_manager.get_user_cubes(target_user_id)
|
|
69
|
+
user_cube_ids = [cube.cube_id for cube in accessible_cubes]
|
|
70
|
+
|
|
71
|
+
# Register chat history if needed
|
|
72
|
+
if target_user_id not in self.chat_history_manager:
|
|
73
|
+
self._register_chat_history(target_user_id)
|
|
74
|
+
|
|
75
|
+
chat_history = self.chat_history_manager[target_user_id]
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
# Step 2: Decompose the query using CoT
|
|
79
|
+
logger.info(f"🔍 [CoT] Decomposing query: {query}")
|
|
80
|
+
decomposition_result = self.cot_decompose(
|
|
81
|
+
query, self.config.chat_model, target_user_id, self.chat_llm
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Check if the query is complex and needs decomposition
|
|
85
|
+
if not decomposition_result.get("is_complex", False):
|
|
86
|
+
logger.info("🔍 [CoT] Query is not complex, using standard chat")
|
|
87
|
+
return super().chat(query, user_id)
|
|
88
|
+
|
|
89
|
+
sub_questions = decomposition_result.get("sub_questions", [])
|
|
90
|
+
logger.info(f"🔍 [CoT] Decomposed into {len(sub_questions)} sub-questions")
|
|
91
|
+
|
|
92
|
+
# Step 3: Get search engine for sub-questions (with proper validation)
|
|
93
|
+
search_engine = self._get_search_engine_for_cot_with_validation(user_cube_ids)
|
|
94
|
+
if not search_engine:
|
|
95
|
+
logger.warning("🔍 [CoT] No search engine available, using standard chat")
|
|
96
|
+
return super().chat(query, user_id)
|
|
97
|
+
|
|
98
|
+
# Step 4: Get answers for sub-questions
|
|
99
|
+
logger.info("🔍 [CoT] Getting answers for sub-questions...")
|
|
100
|
+
sub_questions, sub_answers = self.get_sub_answers(
|
|
101
|
+
sub_questions=sub_questions,
|
|
102
|
+
search_engine=search_engine,
|
|
103
|
+
llm_config=self.config.chat_model,
|
|
104
|
+
user_id=target_user_id,
|
|
105
|
+
top_k=getattr(self.config, "cot_top_k", 3),
|
|
106
|
+
llm=self.chat_llm,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Step 5: Generate enhanced response using sub-answers
|
|
110
|
+
logger.info("🔍 [CoT] Generating enhanced response...")
|
|
111
|
+
enhanced_response = self._generate_enhanced_response_with_context(
|
|
112
|
+
original_query=query,
|
|
113
|
+
sub_questions=sub_questions,
|
|
114
|
+
sub_answers=sub_answers,
|
|
115
|
+
chat_history=chat_history,
|
|
116
|
+
user_id=target_user_id,
|
|
117
|
+
search_engine=search_engine,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Step 6: Update chat history (same as core method)
|
|
121
|
+
chat_history.chat_history.append({"role": "user", "content": query})
|
|
122
|
+
chat_history.chat_history.append({"role": "assistant", "content": enhanced_response})
|
|
123
|
+
self.chat_history_manager[target_user_id] = chat_history
|
|
124
|
+
|
|
125
|
+
# Step 7: Submit message to scheduler (same as core method)
|
|
126
|
+
if len(accessible_cubes) == 1:
|
|
127
|
+
mem_cube_id = accessible_cubes[0].cube_id
|
|
128
|
+
mem_cube = self.mem_cubes[mem_cube_id]
|
|
129
|
+
if self.enable_mem_scheduler and self.mem_scheduler is not None:
|
|
130
|
+
from datetime import datetime
|
|
131
|
+
|
|
132
|
+
from memos.mem_scheduler.modules.schemas import (
|
|
133
|
+
ANSWER_LABEL,
|
|
134
|
+
ScheduleMessageItem,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
message_item = ScheduleMessageItem(
|
|
138
|
+
user_id=target_user_id,
|
|
139
|
+
mem_cube_id=mem_cube_id,
|
|
140
|
+
mem_cube=mem_cube,
|
|
141
|
+
label=ANSWER_LABEL,
|
|
142
|
+
content=enhanced_response,
|
|
143
|
+
timestamp=datetime.now(),
|
|
144
|
+
)
|
|
145
|
+
self.mem_scheduler.submit_messages(messages=[message_item])
|
|
146
|
+
|
|
147
|
+
return enhanced_response
|
|
148
|
+
|
|
149
|
+
except Exception as e:
|
|
150
|
+
logger.error(f"🔍 [CoT] Error in CoT enhancement: {e}")
|
|
151
|
+
logger.info("🔍 [CoT] Falling back to standard chat")
|
|
152
|
+
return super().chat(query, user_id)
|
|
153
|
+
|
|
154
|
+
def _get_search_engine_for_cot_with_validation(
|
|
155
|
+
self, user_cube_ids: list[str]
|
|
156
|
+
) -> BaseTextMemory | None:
|
|
157
|
+
"""
|
|
158
|
+
Get the best available search engine for CoT operations with proper validation.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
user_cube_ids (list[str]): List of cube IDs the user has access to.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
BaseTextMemory or None: The search engine to use for CoT.
|
|
165
|
+
"""
|
|
166
|
+
if not self.mem_cubes:
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
# Get the first available text memory from user's accessible cubes
|
|
170
|
+
for mem_cube_id, mem_cube in self.mem_cubes.items():
|
|
171
|
+
if mem_cube_id not in user_cube_ids:
|
|
172
|
+
continue
|
|
173
|
+
if mem_cube.text_mem:
|
|
174
|
+
return mem_cube.text_mem
|
|
175
|
+
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
def _generate_enhanced_response_with_context(
|
|
179
|
+
self,
|
|
180
|
+
original_query: str,
|
|
181
|
+
sub_questions: list[str],
|
|
182
|
+
sub_answers: list[str],
|
|
183
|
+
chat_history: Any,
|
|
184
|
+
user_id: str | None = None,
|
|
185
|
+
search_engine: BaseTextMemory | None = None,
|
|
186
|
+
) -> str:
|
|
187
|
+
"""
|
|
188
|
+
Generate an enhanced response using sub-questions and their answers, with chat context.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
original_query (str): The original user query.
|
|
192
|
+
sub_questions (list[str]): List of sub-questions.
|
|
193
|
+
sub_answers (list[str]): List of answers to sub-questions.
|
|
194
|
+
chat_history: The user's chat history.
|
|
195
|
+
user_id (str, optional): User ID for context.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
str: The enhanced response.
|
|
199
|
+
"""
|
|
200
|
+
# Build the synthesis prompt
|
|
201
|
+
qa_text = ""
|
|
202
|
+
for i, (question, answer) in enumerate(zip(sub_questions, sub_answers, strict=False), 1):
|
|
203
|
+
qa_text += f"Q{i}: {question}\nA{i}: {answer}\n\n"
|
|
204
|
+
|
|
205
|
+
# Build messages with chat history context (similar to core method)
|
|
206
|
+
if (search_engine is not None) and self.config.enable_textual_memory:
|
|
207
|
+
if self.enable_cot:
|
|
208
|
+
search_memories = search_engine.search(
|
|
209
|
+
original_query, top_k=self.config.top_k, mode="fine"
|
|
210
|
+
)
|
|
211
|
+
else:
|
|
212
|
+
search_memories = search_engine.search(
|
|
213
|
+
original_query, top_k=self.config.top_k, mode="fast"
|
|
214
|
+
)
|
|
215
|
+
system_prompt = self._build_system_prompt(
|
|
216
|
+
search_memories
|
|
217
|
+
) # Use the same system prompt builder
|
|
218
|
+
else:
|
|
219
|
+
system_prompt = self._build_system_prompt()
|
|
220
|
+
current_messages = [
|
|
221
|
+
{"role": "system", "content": system_prompt + SYNTHESIS_PROMPT.format(qa_text=qa_text)},
|
|
222
|
+
*chat_history.chat_history,
|
|
223
|
+
{
|
|
224
|
+
"role": "user",
|
|
225
|
+
"content": original_query,
|
|
226
|
+
},
|
|
227
|
+
]
|
|
228
|
+
|
|
229
|
+
# Handle activation memory if enabled (same as core method)
|
|
230
|
+
past_key_values = None
|
|
231
|
+
if self.config.enable_activation_memory:
|
|
232
|
+
assert self.config.chat_model.backend == "huggingface", (
|
|
233
|
+
"Activation memory only used for huggingface backend."
|
|
234
|
+
)
|
|
235
|
+
# Get accessible cubes for the user
|
|
236
|
+
target_user_id = user_id if user_id is not None else self.user_id
|
|
237
|
+
accessible_cubes = self.user_manager.get_user_cubes(target_user_id)
|
|
238
|
+
user_cube_ids = [cube.cube_id for cube in accessible_cubes]
|
|
239
|
+
|
|
240
|
+
for mem_cube_id, mem_cube in self.mem_cubes.items():
|
|
241
|
+
if mem_cube_id not in user_cube_ids:
|
|
242
|
+
continue
|
|
243
|
+
if mem_cube.act_mem:
|
|
244
|
+
kv_cache = next(iter(mem_cube.act_mem.get_all()), None)
|
|
245
|
+
past_key_values = (
|
|
246
|
+
kv_cache.memory if (kv_cache and hasattr(kv_cache, "memory")) else None
|
|
247
|
+
)
|
|
248
|
+
break
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
# Generate the enhanced response using the chat LLM with same parameters as core
|
|
252
|
+
if past_key_values is not None:
|
|
253
|
+
enhanced_response = self.chat_llm.generate(
|
|
254
|
+
current_messages, past_key_values=past_key_values
|
|
255
|
+
)
|
|
256
|
+
else:
|
|
257
|
+
enhanced_response = self.chat_llm.generate(current_messages)
|
|
258
|
+
|
|
259
|
+
logger.info("🔍 [CoT] Generated enhanced response")
|
|
260
|
+
return enhanced_response
|
|
261
|
+
except Exception as e:
|
|
262
|
+
logger.error(f"🔍 [CoT] Error generating enhanced response: {e}")
|
|
263
|
+
# Fallback to standard chat
|
|
264
|
+
return super().chat(original_query, user_id)
|
|
265
|
+
|
|
266
|
+
@classmethod
|
|
267
|
+
def cot_decompose(
|
|
268
|
+
cls, query: str, llm_config: Any, user_id: str | None = None, llm: LLMFactory | None = None
|
|
269
|
+
) -> list[str] | dict[str, Any]:
|
|
270
|
+
"""
|
|
271
|
+
Decompose a complex query into sub-questions using Chain of Thought reasoning.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
query (str): The complex query to decompose
|
|
275
|
+
llm_config: LLM configuration for decomposition
|
|
276
|
+
user_id (str, optional): User ID for context
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
Union[List[str], Dict[str, Any]]: List of decomposed sub-questions or dict with complexity analysis
|
|
280
|
+
"""
|
|
281
|
+
# Create a temporary LLM instance for decomposition
|
|
282
|
+
if llm is None:
|
|
283
|
+
llm = LLMFactory.from_config(llm_config)
|
|
284
|
+
|
|
285
|
+
# System prompt for CoT decomposition with complexity analysis
|
|
286
|
+
system_prompt = COT_DECOMPOSE_PROMPT.format(query=query)
|
|
287
|
+
|
|
288
|
+
messages = [{"role": "system", "content": system_prompt}]
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
response = llm.generate(messages)
|
|
292
|
+
# Try to parse JSON response
|
|
293
|
+
result = json.loads(response)
|
|
294
|
+
return result
|
|
295
|
+
except json.JSONDecodeError as e:
|
|
296
|
+
logger.warning(f"Failed to parse JSON response from LLM: {e}")
|
|
297
|
+
logger.warning(f"Raw response: {response}")
|
|
298
|
+
|
|
299
|
+
# Try to extract JSON-like content from the response
|
|
300
|
+
try:
|
|
301
|
+
# Look for JSON-like content between curly braces
|
|
302
|
+
import re
|
|
303
|
+
|
|
304
|
+
json_match = re.search(r"\{.*\}", response, re.DOTALL)
|
|
305
|
+
if json_match:
|
|
306
|
+
json_str = json_match.group(0)
|
|
307
|
+
result = json.loads(json_str)
|
|
308
|
+
return result
|
|
309
|
+
except Exception:
|
|
310
|
+
pass
|
|
311
|
+
|
|
312
|
+
# If all parsing attempts fail, return default
|
|
313
|
+
return {"is_complex": False, "sub_questions": []}
|
|
314
|
+
except Exception as e:
|
|
315
|
+
logger.error(f"Unexpected error in cot_decompose: {e}")
|
|
316
|
+
return {"is_complex": False, "sub_questions": []}
|
|
317
|
+
|
|
318
|
+
@classmethod
|
|
319
|
+
def get_sub_answers(
|
|
320
|
+
cls,
|
|
321
|
+
sub_questions: list[str] | dict[str, Any],
|
|
322
|
+
search_results: dict[str, Any] | None = None,
|
|
323
|
+
search_engine: BaseTextMemory | None = None,
|
|
324
|
+
llm_config: LLMFactory | None = None,
|
|
325
|
+
user_id: str | None = None,
|
|
326
|
+
top_k: int = 5,
|
|
327
|
+
llm: LLMFactory | None = None,
|
|
328
|
+
) -> tuple[list[str], list[str]]:
|
|
329
|
+
"""
|
|
330
|
+
Get answers for sub-questions using either search results or a search engine.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
sub_questions (Union[List[str], Dict[str, Any]]): List of sub-questions from cot_decompose or dict with analysis
|
|
334
|
+
search_results (Dict[str, Any], optional): Search results containing relevant information
|
|
335
|
+
search_engine (BaseTextMemory, optional): Text memory engine for searching
|
|
336
|
+
llm_config (Any, optional): LLM configuration for processing (required if search_engine is provided)
|
|
337
|
+
user_id (str, optional): User ID for context
|
|
338
|
+
top_k (int): Number of top results to retrieve from search engine
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
Tuple[List[str], List[str]]: (sub_questions, sub_answers)
|
|
342
|
+
"""
|
|
343
|
+
# Extract sub-questions from decomposition result if needed
|
|
344
|
+
if isinstance(sub_questions, dict):
|
|
345
|
+
if not sub_questions.get("is_complex", False):
|
|
346
|
+
return [], []
|
|
347
|
+
sub_questions = sub_questions.get("sub_questions", [])
|
|
348
|
+
|
|
349
|
+
if not sub_questions:
|
|
350
|
+
return [], []
|
|
351
|
+
|
|
352
|
+
# Validate inputs
|
|
353
|
+
if search_results is None and search_engine is None:
|
|
354
|
+
raise ValueError("Either search_results or search_engine must be provided")
|
|
355
|
+
if llm is None:
|
|
356
|
+
llm = LLMFactory.from_config(llm_config)
|
|
357
|
+
|
|
358
|
+
# Step 1: Get search results if search_engine is provided
|
|
359
|
+
if search_engine is not None:
|
|
360
|
+
search_results = cls._search_with_engine(sub_questions, search_engine, top_k)
|
|
361
|
+
|
|
362
|
+
# Step 2: Generate answers for each sub-question using LLM in parallel
|
|
363
|
+
def generate_answer_for_question(question_index: int, sub_question: str) -> tuple[int, str]:
|
|
364
|
+
"""Generate answer for a single sub-question."""
|
|
365
|
+
# Extract relevant information from search results
|
|
366
|
+
relevant_info = []
|
|
367
|
+
if search_results and search_results.get("text_mem"):
|
|
368
|
+
for cube_result in search_results["text_mem"]:
|
|
369
|
+
for memory in cube_result.get("memories", []):
|
|
370
|
+
relevant_info.append(memory.memory)
|
|
371
|
+
|
|
372
|
+
# Build system prompt with memories (similar to MOSCore._build_system_prompt)
|
|
373
|
+
base_prompt = (
|
|
374
|
+
"You are a knowledgeable and helpful AI assistant. "
|
|
375
|
+
"You have access to relevant information that helps you provide accurate answers. "
|
|
376
|
+
"Use the provided information to answer the question comprehensively. "
|
|
377
|
+
"If the information is not sufficient, acknowledge the limitations."
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
# Add memory context if available
|
|
381
|
+
if relevant_info:
|
|
382
|
+
memory_context = "\n\n## Relevant Information:\n"
|
|
383
|
+
for j, info in enumerate(relevant_info[:top_k], 1): # Take top 3 most relevant
|
|
384
|
+
memory_context += f"{j}. {info}\n"
|
|
385
|
+
system_prompt = base_prompt + memory_context
|
|
386
|
+
else:
|
|
387
|
+
system_prompt = (
|
|
388
|
+
base_prompt
|
|
389
|
+
+ "\n\n## Relevant Information:\nNo specific information found in memory."
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
# Create messages for LLM
|
|
393
|
+
messages = [
|
|
394
|
+
{"role": "system", "content": system_prompt},
|
|
395
|
+
{"role": "user", "content": sub_question},
|
|
396
|
+
]
|
|
397
|
+
|
|
398
|
+
try:
|
|
399
|
+
# Generate answer using LLM
|
|
400
|
+
response = llm.generate(messages)
|
|
401
|
+
return question_index, response
|
|
402
|
+
except Exception as e:
|
|
403
|
+
logger.error(f"Failed to generate answer for sub-question '{sub_question}': {e}")
|
|
404
|
+
return question_index, f"Unable to generate answer for: {sub_question}"
|
|
405
|
+
|
|
406
|
+
# Generate answers in parallel while maintaining order
|
|
407
|
+
sub_answers = [None] * len(sub_questions)
|
|
408
|
+
with concurrent.futures.ThreadPoolExecutor(
|
|
409
|
+
max_workers=min(len(sub_questions), 10)
|
|
410
|
+
) as executor:
|
|
411
|
+
# Submit all answer generation tasks
|
|
412
|
+
future_to_index = {
|
|
413
|
+
executor.submit(generate_answer_for_question, i, question): i
|
|
414
|
+
for i, question in enumerate(sub_questions)
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
# Collect results as they complete, but store them in the correct position
|
|
418
|
+
for future in concurrent.futures.as_completed(future_to_index):
|
|
419
|
+
try:
|
|
420
|
+
question_index, answer = future.result()
|
|
421
|
+
sub_answers[question_index] = answer
|
|
422
|
+
except Exception as e:
|
|
423
|
+
question_index = future_to_index[future]
|
|
424
|
+
logger.error(
|
|
425
|
+
f"Exception occurred while generating answer for question at index {question_index}: {e}"
|
|
426
|
+
)
|
|
427
|
+
sub_answers[question_index] = (
|
|
428
|
+
f"Error generating answer for question {question_index + 1}"
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
return sub_questions, sub_answers
|
|
432
|
+
|
|
433
|
+
@classmethod
|
|
434
|
+
def _search_with_engine(
|
|
435
|
+
cls, sub_questions: list[str], search_engine: BaseTextMemory, top_k: int
|
|
436
|
+
) -> dict[str, Any]:
|
|
437
|
+
"""
|
|
438
|
+
Search for sub-questions using the provided search engine in parallel.
|
|
439
|
+
|
|
440
|
+
Args:
|
|
441
|
+
sub_questions (List[str]): List of sub-questions to search for
|
|
442
|
+
search_engine (BaseTextMemory): Text memory engine for searching
|
|
443
|
+
top_k (int): Number of top results to retrieve
|
|
444
|
+
|
|
445
|
+
Returns:
|
|
446
|
+
Dict[str, Any]: Search results in the expected format
|
|
447
|
+
"""
|
|
448
|
+
|
|
449
|
+
def search_single_question(question: str) -> list[Any]:
|
|
450
|
+
"""Search for a single question using the search engine."""
|
|
451
|
+
try:
|
|
452
|
+
# Handle different search method signatures
|
|
453
|
+
if hasattr(search_engine, "search"):
|
|
454
|
+
# Try different parameter combinations based on the engine type
|
|
455
|
+
try:
|
|
456
|
+
# For tree_text memory
|
|
457
|
+
return search_engine.search(question, top_k, mode="fast")
|
|
458
|
+
except TypeError:
|
|
459
|
+
try:
|
|
460
|
+
# For general_text memory
|
|
461
|
+
return search_engine.search(question, top_k)
|
|
462
|
+
except TypeError:
|
|
463
|
+
# For naive_text memory
|
|
464
|
+
return search_engine.search(question, top_k)
|
|
465
|
+
else:
|
|
466
|
+
return []
|
|
467
|
+
except Exception as e:
|
|
468
|
+
logger.error(f"Search failed for question '{question}': {e}")
|
|
469
|
+
return []
|
|
470
|
+
|
|
471
|
+
# Search in parallel while maintaining order
|
|
472
|
+
all_memories = []
|
|
473
|
+
with concurrent.futures.ThreadPoolExecutor(
|
|
474
|
+
max_workers=min(len(sub_questions), 10)
|
|
475
|
+
) as executor:
|
|
476
|
+
# Submit all search tasks and keep track of their order
|
|
477
|
+
future_to_index = {
|
|
478
|
+
executor.submit(search_single_question, question): i
|
|
479
|
+
for i, question in enumerate(sub_questions)
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
# Initialize results list with None values to maintain order
|
|
483
|
+
results = [None] * len(sub_questions)
|
|
484
|
+
|
|
485
|
+
# Collect results as they complete, but store them in the correct position
|
|
486
|
+
for future in concurrent.futures.as_completed(future_to_index):
|
|
487
|
+
index = future_to_index[future]
|
|
488
|
+
try:
|
|
489
|
+
memories = future.result()
|
|
490
|
+
results[index] = memories
|
|
491
|
+
except Exception as e:
|
|
492
|
+
logger.error(
|
|
493
|
+
f"Exception occurred while searching for question at index {index}: {e}"
|
|
494
|
+
)
|
|
495
|
+
results[index] = []
|
|
496
|
+
|
|
497
|
+
# Combine all results in the correct order
|
|
498
|
+
for result in results:
|
|
499
|
+
if result is not None:
|
|
500
|
+
all_memories.extend(result)
|
|
501
|
+
|
|
502
|
+
# Format results in the expected structure
|
|
503
|
+
return {"text_mem": [{"cube_id": "search_engine", "memories": all_memories}]}
|
memos/mem_user/user_manager.py
CHANGED
|
@@ -476,3 +476,13 @@ class UserManager:
|
|
|
476
476
|
return False
|
|
477
477
|
finally:
|
|
478
478
|
session.close()
|
|
479
|
+
|
|
480
|
+
def close(self) -> None:
|
|
481
|
+
"""Close the database engine and dispose of all connections.
|
|
482
|
+
|
|
483
|
+
This method should be called when the UserManager is no longer needed
|
|
484
|
+
to ensure proper cleanup of database connections.
|
|
485
|
+
"""
|
|
486
|
+
if hasattr(self, "engine"):
|
|
487
|
+
self.engine.dispose()
|
|
488
|
+
logger.info("UserManager database connections closed")
|
memos/memories/textual/tree.py
CHANGED
|
@@ -15,6 +15,9 @@ from memos.log import get_logger
|
|
|
15
15
|
from memos.memories.textual.base import BaseTextMemory
|
|
16
16
|
from memos.memories.textual.item import TextualMemoryItem, TreeNodeTextualMemoryMetadata
|
|
17
17
|
from memos.memories.textual.tree_text_memory.organize.manager import MemoryManager
|
|
18
|
+
from memos.memories.textual.tree_text_memory.retrieve.internet_retriever_factory import (
|
|
19
|
+
InternetRetrieverFactory,
|
|
20
|
+
)
|
|
18
21
|
from memos.memories.textual.tree_text_memory.retrieve.searcher import Searcher
|
|
19
22
|
from memos.types import MessageList
|
|
20
23
|
|
|
@@ -34,6 +37,18 @@ class TreeTextMemory(BaseTextMemory):
|
|
|
34
37
|
self.graph_store: Neo4jGraphDB = GraphStoreFactory.from_config(config.graph_db)
|
|
35
38
|
self.memory_manager: MemoryManager = MemoryManager(self.graph_store, self.embedder)
|
|
36
39
|
|
|
40
|
+
# Create internet retriever if configured
|
|
41
|
+
self.internet_retriever = None
|
|
42
|
+
if config.internet_retriever is not None:
|
|
43
|
+
self.internet_retriever = InternetRetrieverFactory.from_config(
|
|
44
|
+
config.internet_retriever, self.embedder
|
|
45
|
+
)
|
|
46
|
+
logger.info(
|
|
47
|
+
f"Internet retriever initialized with backend: {config.internet_retriever.backend}"
|
|
48
|
+
)
|
|
49
|
+
else:
|
|
50
|
+
logger.info("No internet retriever configured")
|
|
51
|
+
|
|
37
52
|
def add(self, memories: list[TextualMemoryItem | dict[str, Any]]) -> None:
|
|
38
53
|
"""Add memories.
|
|
39
54
|
Args:
|
|
@@ -66,7 +81,13 @@ class TreeTextMemory(BaseTextMemory):
|
|
|
66
81
|
return self.memory_manager.get_current_memory_size()
|
|
67
82
|
|
|
68
83
|
def search(
|
|
69
|
-
self,
|
|
84
|
+
self,
|
|
85
|
+
query: str,
|
|
86
|
+
top_k: int,
|
|
87
|
+
info=None,
|
|
88
|
+
mode: str = "fast",
|
|
89
|
+
memory_type: str = "All",
|
|
90
|
+
manual_close_internet: bool = False,
|
|
70
91
|
) -> list[TextualMemoryItem]:
|
|
71
92
|
"""Search for memories based on a query.
|
|
72
93
|
User query -> TaskGoalParser -> MemoryPathResolver ->
|
|
@@ -80,10 +101,21 @@ class TreeTextMemory(BaseTextMemory):
|
|
|
80
101
|
- 'fine': Uses a more detailed search process, invoking large models for higher precision, but slower performance.
|
|
81
102
|
memory_type (str): Type restriction for search.
|
|
82
103
|
['All', 'WorkingMemory', 'LongTermMemory', 'UserMemory']
|
|
104
|
+
manual_close_internet (bool): If True, the internet retriever will be closed by this search, it high priority than config.
|
|
83
105
|
Returns:
|
|
84
106
|
list[TextualMemoryItem]: List of matching memories.
|
|
85
107
|
"""
|
|
86
|
-
|
|
108
|
+
if (self.internet_retriever is not None) and manual_close_internet:
|
|
109
|
+
logger.warning(
|
|
110
|
+
"Internet retriever is init by config , but this search set manual_close_internet is True and will close it"
|
|
111
|
+
)
|
|
112
|
+
self.internet_retriever = None
|
|
113
|
+
searcher = Searcher(
|
|
114
|
+
self.dispatcher_llm,
|
|
115
|
+
self.graph_store,
|
|
116
|
+
self.embedder,
|
|
117
|
+
internet_retriever=self.internet_retriever,
|
|
118
|
+
)
|
|
87
119
|
return searcher.search(query, top_k, info, mode, memory_type)
|
|
88
120
|
|
|
89
121
|
def get_relevant_subgraph(
|