From 2b0e1f8bc9da7acd22f86889ab56dc84f7772fc0 Mon Sep 17 00:00:00 2001 From: Travis Tang Date: Mon, 9 Feb 2026 16:16:59 +0800 Subject: [PATCH 01/12] fix: allow get_user_names_by_memory_ids supports all types of db (#1063) --- src/memos/api/routers/server_router.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/memos/api/routers/server_router.py b/src/memos/api/routers/server_router.py index 736c328ac..12e72c968 100644 --- a/src/memos/api/routers/server_router.py +++ b/src/memos/api/routers/server_router.py @@ -47,7 +47,6 @@ SuggestionResponse, TaskQueueResponse, ) -from memos.graph_dbs.polardb import PolarDBGraphDB from memos.log import get_logger from memos.mem_scheduler.base_scheduler import BaseScheduler from memos.mem_scheduler.utils.status_tracker import TaskStatusTracker @@ -364,15 +363,6 @@ def feedback_memories(feedback_req: APIFeedbackRequest): ) def get_user_names_by_memory_ids(request: GetUserNamesByMemoryIdsRequest): """Get user names by memory ids.""" - if not isinstance(graph_db, PolarDBGraphDB): - raise HTTPException( - status_code=400, - detail=( - "graph_db must be an instance of PolarDBGraphDB to use " - "get_user_names_by_memory_ids" - f"current graph_db is: {graph_db.__class__.__name__}" - ), - ) result = graph_db.get_user_names_by_memory_ids(memory_ids=request.memory_ids) if vector_db: prefs = [] From 1af1d7608e73fe474f0de4d05329a99a8b1b4892 Mon Sep 17 00:00:00 2001 From: Wang Daoji <75928131+Wang-Daoji@users.noreply.github.com> Date: Tue, 10 Feb 2026 17:08:01 +0800 Subject: [PATCH 02/12] feat: optimization v2.0.6 (#1071) * fix: fix bug in handle_get_memories_dashboard * feat: chat complete, stream, business add relativity * fix: fix skill mem llm call return None problem --------- Co-authored-by: yuan.wang --- src/memos/api/handlers/chat_handler.py | 3 ++ src/memos/api/handlers/memory_handler.py | 22 ++++++++-- src/memos/api/product_models.py | 18 ++++++++ .../read_skill_memory/process_skill_memory.py | 41 +++++++++++-------- 4 files changed, 62 insertions(+), 22 deletions(-) diff --git a/src/memos/api/handlers/chat_handler.py b/src/memos/api/handlers/chat_handler.py index cd33a7aeb..2e9032f11 100644 --- a/src/memos/api/handlers/chat_handler.py +++ b/src/memos/api/handlers/chat_handler.py @@ -130,6 +130,7 @@ def handle_chat_complete(self, chat_req: APIChatCompleteRequest) -> dict[str, An include_preference=chat_req.include_preference, pref_top_k=chat_req.pref_top_k, filter=chat_req.filter, + relativity=chat_req.relativity, ) search_response = self.search_handler.handle_search_memories(search_req) @@ -269,6 +270,7 @@ def generate_chat_response() -> Generator[str, None, None]: include_preference=chat_req.include_preference, pref_top_k=chat_req.pref_top_k, filter=chat_req.filter, + relativity=chat_req.relativity, ) search_response = self.search_handler.handle_search_memories(search_req) @@ -811,6 +813,7 @@ def generate_chat_response() -> Generator[str, None, None]: include_preference=chat_req.include_preference, pref_top_k=chat_req.pref_top_k, filter=chat_req.filter, + relativity=chat_req.relativity, ) search_response = self.search_handler.handle_search_memories(search_req) diff --git a/src/memos/api/handlers/memory_handler.py b/src/memos/api/handlers/memory_handler.py index a3430d475..df9f70e77 100644 --- a/src/memos/api/handlers/memory_handler.py +++ b/src/memos/api/handlers/memory_handler.py @@ -412,6 +412,12 @@ def handle_get_memories_dashboard( get_mem_req: GetMemoryDashboardRequest, naive_mem_cube: NaiveMemCube ) -> GetMemoryResponse: results: dict[str, Any] = {"text_mem": [], "pref_mem": [], "tool_mem": [], "skill_mem": []} + # for statistics + total_text_nodes, total_tool_nodes, total_skill_nodes, total_preference_nodes = 0, 0, 0, 0 + total_tool_nodes = 0 + total_skill_nodes = 0 + total_preference_nodes = 0 + text_memory_type = ["WorkingMemory", "LongTermMemory", "UserMemory", "OuterMemory"] text_memories_info = naive_mem_cube.text_mem.get_all( user_name=get_mem_req.mem_cube_id, @@ -421,7 +427,7 @@ def handle_get_memories_dashboard( filter=get_mem_req.filter, memory_type=text_memory_type, ) - text_memories, _ = text_memories_info["nodes"], text_memories_info["total_nodes"] + text_memories, total_text_nodes = text_memories_info["nodes"], text_memories_info["total_nodes"] # Group text memories by cube_id from metadata.user_name text_mem_by_cube: dict[str, list] = {} @@ -453,7 +459,7 @@ def handle_get_memories_dashboard( filter=get_mem_req.filter, memory_type=["ToolSchemaMemory", "ToolTrajectoryMemory"], ) - tool_memories, _ = ( + tool_memories, total_tool_nodes = ( tool_memories_info["nodes"], tool_memories_info["total_nodes"], ) @@ -488,7 +494,7 @@ def handle_get_memories_dashboard( filter=get_mem_req.filter, memory_type=["SkillMemory"], ) - skill_memories, _ = ( + skill_memories, total_skill_nodes = ( skill_memories_info["nodes"], skill_memories_info["total_nodes"], ) @@ -515,7 +521,6 @@ def handle_get_memories_dashboard( ] preferences: list[TextualMemoryItem] = [] - total_preference_nodes = 0 format_preferences = [] if get_mem_req.include_preference and naive_mem_cube.pref_mem is not None: @@ -578,4 +583,13 @@ def handle_get_memories_dashboard( "skill_mem": results.get("skill_mem", []), } + # statistics + statistics = { + "total_text_nodes": total_text_nodes, + "total_tool_nodes": total_tool_nodes, + "total_skill_nodes": total_skill_nodes, + "total_preference_nodes": total_preference_nodes, + } + filtered_results["statistics"] = statistics + return GetMemoryResponse(message="Memories retrieved successfully", data=filtered_results) diff --git a/src/memos/api/product_models.py b/src/memos/api/product_models.py index c41526e33..6fc03e735 100644 --- a/src/memos/api/product_models.py +++ b/src/memos/api/product_models.py @@ -98,6 +98,15 @@ class ChatRequest(BaseRequest): add_message_on_answer: bool = Field(True, description="Add dialogs to memory after chat") manager_user_id: str | None = Field(None, description="Manager User ID") project_id: str | None = Field(None, description="Project ID") + relativity: float = Field( + 0.0, + ge=0, + description=( + "Relevance threshold for recalled memories. " + "Only memories with metadata.relativity >= relativity will be returned. " + "Use 0 to disable threshold filtering. Default: 0.3." + ), + ) # ==== Filter conditions ==== filter: dict[str, Any] | None = Field( @@ -775,6 +784,15 @@ class APIChatCompleteRequest(BaseRequest): add_message_on_answer: bool = Field(True, description="Add dialogs to memory after chat") manager_user_id: str | None = Field(None, description="Manager User ID") project_id: str | None = Field(None, description="Project ID") + relativity: float = Field( + 0.0, + ge=0, + description=( + "Relevance threshold for recalled memories. " + "Only memories with metadata.relativity >= relativity will be returned. " + "Use 0 to disable threshold filtering. Default: 0.3." + ), + ) # ==== Filter conditions ==== filter: dict[str, Any] | None = Field( diff --git a/src/memos/mem_reader/read_skill_memory/process_skill_memory.py b/src/memos/mem_reader/read_skill_memory/process_skill_memory.py index fa799e759..d691e53a3 100644 --- a/src/memos/mem_reader/read_skill_memory/process_skill_memory.py +++ b/src/memos/mem_reader/read_skill_memory/process_skill_memory.py @@ -52,6 +52,9 @@ def _generate_content_by_llm(llm: BaseLLM, prompt_template: str, **kwargs) -> An try: prompt = prompt_template.format(**kwargs) response = llm.generate([{"role": "user", "content": prompt}]) + if not response: + logger.warning("[PROCESS_SKILLS] LLM returned empty or invalid response") + return {} if "json" in prompt_template.lower() else "" if "json" in prompt_template.lower(): response = response.replace("```json", "").replace("```", "").strip() return json.loads(response) @@ -436,6 +439,9 @@ def _extract_skill_memory_by_llm( skills_llm = os.getenv("SKILLS_LLM", None) llm_kwargs = {"model_name_or_path": skills_llm} if skills_llm else {} response_text = llm.generate(prompt, **llm_kwargs) + if not response_text: + logger.warning("[PROCESS_SKILLS] LLM returned empty or invalid response") + continue # Clean up response (remove Markdown code blocks if present) logger.info(f"[Skill Memory]: response_text {response_text}") response_text = response_text.strip() @@ -561,6 +567,9 @@ def _extract_skill_memory_by_llm_md( skills_llm = os.getenv("SKILLS_LLM", None) llm_kwargs = {"model_name_or_path": skills_llm} if skills_llm else {} response_text = llm.generate(prompt, **llm_kwargs) + if not response_text: + logger.warning("[PROCESS_SKILLS] LLM returned empty or invalid response") + continue # Clean up response (remove Markdown code blocks if present) logger.info(f"[Skill Memory]: response_text {response_text}") response_text = response_text.strip() @@ -641,26 +650,22 @@ def _rewrite_query(task_type: str, messages: MessageList, llm: BaseLLM, rewrite_ ) prompt = [{"role": "user", "content": prompt_content}] - # Call LLM to rewrite the query with retry logic - for attempt in range(3): - try: - response_text = llm.generate(prompt) - # Clean up response (remove any markdown formatting if present) - response_text = response_text.strip() - logger.info(f"[PROCESS_SKILLS] Rewritten query for task '{task_type}': {response_text}") - return response_text - except Exception as e: + # Call LLM to rewrite the query + try: + response_text = llm.generate(prompt) + # Clean up response (remove any markdown formatting if present) + if response_text and isinstance(response_text, str): + return response_text.strip() + else: logger.warning( - f"[PROCESS_SKILLS] LLM query rewrite failed (attempt {attempt + 1}): {e}" + "[PROCESS_SKILLS] LLM returned empty or invalid response, returning first message content" ) - if attempt == 2: - logger.warning( - "[PROCESS_SKILLS] LLM query rewrite failed after 3 retries, returning first message content" - ) - return messages[0]["content"] if messages else "" - - # Fallback (should not reach here due to return in exception handling) - return messages[0]["content"] if messages else "" + return messages[0]["content"] if messages else "" + except Exception as e: + logger.warning( + f"[PROCESS_SKILLS] LLM query rewrite failed: {e}, returning first message content" + ) + return messages[0]["content"] if messages else "" @require_python_package( From 88bdf9a279f1661d173b7a1f6e53c9737842fc11 Mon Sep 17 00:00:00 2001 From: Jiang <33757498+hijzy@users.noreply.github.com> Date: Wed, 11 Feb 2026 11:43:29 +0800 Subject: [PATCH 03/12] feat: add keyword search (#1073) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add keyword search * test: 按照 top_k 截断 keyword 路检索而不是 top6 * fix: 修复 cot embedding 顺序问题, 修复阈值筛选偏好记忆可能误删的问题 * feat: 强保语义和关键词路 top1 * fix: 使用 keyword_score 而不是 relativity 作为关键词路强保判断依据 * feat: 两路 top1 参与 mmr 计算但是不会被淘汰 * revert and 使用 http bge reranker * Revert "fix: 修复 cot embedding 顺序问题, 修复阈值筛选偏好记忆可能误删的问题" This reverts commit 27967fee9bb9f134aa7cedce746af86f4038bb29. * fix: 修复 query embedding 顺序问题和偏好记忆的相关的分数字段错误 * revert * reformat --------- Co-authored-by: CaralHsi --- src/memos/api/handlers/search_handler.py | 9 +- .../tree_text_memory/retrieve/searcher.py | 109 +++++++++++++++++- src/memos/multi_mem_cube/single_cube.py | 4 +- 3 files changed, 115 insertions(+), 7 deletions(-) diff --git a/src/memos/api/handlers/search_handler.py b/src/memos/api/handlers/search_handler.py index 91980bdeb..1b3d32bbc 100644 --- a/src/memos/api/handlers/search_handler.py +++ b/src/memos/api/handlers/search_handler.py @@ -120,11 +120,14 @@ def _apply_relativity_threshold(results: dict[str, Any], relativity: float) -> d if not isinstance(mem, dict): continue meta = mem.get("metadata", {}) - score = meta.get("relativity", 0.0) if isinstance(meta, dict) else 0.0 + if key == "text_mem": + score = meta.get("relativity", 1.0) if isinstance(meta, dict) else 1.0 + else: + score = meta.get("score", 1.0) if isinstance(meta, dict) else 1.0 try: - score_val = float(score) if score is not None else 0.0 + score_val = float(score) if score is not None else 1.0 except (TypeError, ValueError): - score_val = 0.0 + score_val = 1.0 if score_val >= relativity: filtered.append(mem) diff --git a/src/memos/memories/textual/tree_text_memory/retrieve/searcher.py b/src/memos/memories/textual/tree_text_memory/retrieve/searcher.py index bc8d76517..f00efccb6 100644 --- a/src/memos/memories/textual/tree_text_memory/retrieve/searcher.py +++ b/src/memos/memories/textual/tree_text_memory/retrieve/searcher.py @@ -1,3 +1,4 @@ +import copy import traceback from concurrent.futures import as_completed @@ -306,8 +307,8 @@ def _parse_task( query = parsed_goal.rephrased_query or query # if goal has extra memories, embed them too if parsed_goal.memories: - query_embedding = self.embedder.embed(list({query, *parsed_goal.memories})) - + embed_texts = list(dict.fromkeys([query, *parsed_goal.memories])) + query_embedding = self.embedder.embed(embed_texts) return parsed_goal, query_embedding, context, query @timed @@ -379,6 +380,20 @@ def _retrieve_paths( user_name, ) ) + tasks.append( + executor.submit( + self._retrieve_from_keyword, + query, + parsed_goal, + query_embedding, + top_k, + memory_type, + search_filter, + search_priority, + user_name, + id_filter, + ) + ) if search_tool_memory: tasks.append( executor.submit( @@ -456,6 +471,96 @@ def _retrieve_from_working_memory( search_filter=search_filter, ) + @timed + def _retrieve_from_keyword( + self, + query, + parsed_goal, + query_embedding, + top_k, + memory_type, + search_filter: dict | None = None, + search_priority: dict | None = None, + user_name: str | None = None, + id_filter: dict | None = None, + ) -> list[tuple[TextualMemoryItem, float]]: + """Keyword/fulltext path that directly calls graph DB fulltext search.""" + + if memory_type not in ["All", "LongTermMemory", "UserMemory"]: + return [] + if not query_embedding: + return [] + + query_words: list[str] = [] + if self.tokenizer: + query_words = self.tokenizer.tokenize_mixed(query) + else: + query_words = query.strip().split() + # Use unique tokens; avoid passing the raw query into `to_tsquery(...)` because it may contain + # spaces/operators that cause tsquery parsing errors. + query_words = list(dict.fromkeys(query_words)) + if len(query_words) > 64: + query_words = query_words[:64] + if not query_words: + return [] + tsquery_terms = ["'" + w.replace("'", "''") + "'" for w in query_words if w and w.strip()] + if not tsquery_terms: + return [] + + scopes = [memory_type] if memory_type != "All" else ["LongTermMemory", "UserMemory"] + + id_to_score: dict[str, float] = {} + for scope in scopes: + hits = self.graph_store.search_by_fulltext( + query_words=tsquery_terms, + top_k=top_k * 2, + status="activated", + scope=scope, + search_filter=None, + filter=search_filter, + user_name=user_name, + tsquery_config="jiebaqry", + ) + for h in hits or []: + hid = str(h.get("id") or "").strip().strip("'\"") + if not hid: + continue + score = h.get("score", 0.0) + if hid not in id_to_score or score > id_to_score[hid]: + id_to_score[hid] = score + if not id_to_score: + return [] + + sorted_ids = sorted(id_to_score.keys(), key=lambda x: id_to_score[x], reverse=True) + sorted_ids = sorted_ids[:top_k] + node_dicts = ( + self.graph_store.get_nodes(sorted_ids, include_embedding=True, user_name=user_name) + or [] + ) + id_to_node = {n.get("id"): n for n in node_dicts} + ordered_nodes = [] + + for rid in sorted_ids: + if rid in id_to_node: + node = copy.deepcopy(id_to_node[rid]) + meta = node.setdefault("metadata", {}) + meta_target = meta + if isinstance(meta, dict) and isinstance(meta.get("metadata"), dict): + meta_target = meta["metadata"] + if isinstance(meta_target, dict): + meta_target["keyword_score"] = id_to_score[rid] + ordered_nodes.append(node) + + results = [TextualMemoryItem.from_dict(n) for n in ordered_nodes] + return self.reranker.rerank( + query=query, + query_embedding=query_embedding[0], + graph_results=results, + top_k=top_k, + parsed_goal=parsed_goal, + search_filter=search_filter, + ) + # --- Path B @timed def _retrieve_from_long_term_and_user( diff --git a/src/memos/multi_mem_cube/single_cube.py b/src/memos/multi_mem_cube/single_cube.py index 6da55ce02..307bf05b2 100644 --- a/src/memos/multi_mem_cube/single_cube.py +++ b/src/memos/multi_mem_cube/single_cube.py @@ -468,13 +468,13 @@ def _fast_search( search_req=search_req, user_context=user_context, mode=SearchMode.FAST, - include_embedding=(search_req.dedup == "mmr"), + include_embedding=(search_req.dedup in ("mmr", "sim")), ) return self._postformat_memories( search_results, user_context.mem_cube_id, - include_embedding=search_req.dedup == "sim", + include_embedding=(search_req.dedup in ("mmr", "sim")), neighbor_discovery=search_req.neighbor_discovery, ) From ee771e71e40ffe15f3feb1362c1c1b826d96d235 Mon Sep 17 00:00:00 2001 From: Jiang <33757498+hijzy@users.noreply.github.com> Date: Wed, 11 Feb 2026 12:04:30 +0800 Subject: [PATCH 04/12] fix: replace relativity with score for pref_mem (#1077) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add keyword search * test: 按照 top_k 截断 keyword 路检索而不是 top6 * fix: 修复 cot embedding 顺序问题, 修复阈值筛选偏好记忆可能误删的问题 * feat: 强保语义和关键词路 top1 * fix: 使用 keyword_score 而不是 relativity 作为关键词路强保判断依据 * feat: 两路 top1 参与 mmr 计算但是不会被淘汰 * revert and 使用 http bge reranker * Revert "fix: 修复 cot embedding 顺序问题, 修复阈值筛选偏好记忆可能误删的问题" This reverts commit 27967fee9bb9f134aa7cedce746af86f4038bb29. * fix: 修复 query embedding 顺序问题和偏好记忆的相关的分数字段错误 * revert * reformat * fix: 修复偏好记忆 score 问题 --------- Co-authored-by: CaralHsi --- src/memos/api/handlers/search_handler.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/memos/api/handlers/search_handler.py b/src/memos/api/handlers/search_handler.py index 1b3d32bbc..267d1bb28 100644 --- a/src/memos/api/handlers/search_handler.py +++ b/src/memos/api/handlers/search_handler.py @@ -223,7 +223,11 @@ def _mmr_dedup_text_memories( # Flatten preference memories for bucket_idx, bucket in enumerate(pref_buckets): for mem in bucket.get("memories", []): - score = mem.get("metadata", {}).get("relativity", 0.0) + meta = mem.get("metadata", {}) + if isinstance(meta, dict): + score = meta.get("score", meta.get("relativity", 0.0)) + else: + score = 0.0 flat.append( ("preference", bucket_idx, mem, float(score) if score is not None else 0.0) ) From 76395969cd0e80a7cfb04bc63d1de32479390287 Mon Sep 17 00:00:00 2001 From: Wenqiang Wei <46308778+endxxxx@users.noreply.github.com> Date: Wed, 11 Feb 2026 14:30:22 +0800 Subject: [PATCH 05/12] fix: support cases when there is both field condition and logical operators (#1076) --- src/memos/vec_dbs/milvus.py | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/src/memos/vec_dbs/milvus.py b/src/memos/vec_dbs/milvus.py index cc8909d34..3963e6e4a 100644 --- a/src/memos/vec_dbs/milvus.py +++ b/src/memos/vec_dbs/milvus.py @@ -287,16 +287,33 @@ def _dict_to_expr(self, filter_dict: dict[str, Any]) -> str: def _build_expression(self, condition: Any) -> str: """Build expression from condition dict or value.""" if isinstance(condition, dict): + conditions = [] + # Handle logical operators if "and" in condition: - return self._handle_logical_and(condition["and"]) - elif "or" in condition: - return self._handle_logical_or(condition["or"]) - elif "not" in condition: - return self._handle_logical_not(condition["not"]) - else: - # Handle field conditions - return self._handle_field_conditions(condition) + and_expr = self._handle_logical_and(condition["and"]) + if and_expr: + conditions.append(and_expr) + if "or" in condition: + or_expr = self._handle_logical_or(condition["or"]) + if or_expr: + conditions.append(or_expr) + if "not" in condition: + not_expr = self._handle_logical_not(condition["not"]) + if not_expr: + conditions.append(not_expr) + + # Handle field conditions (keys that are not logical operators) + field_dict = {k: v for k, v in condition.items() if k not in ["and", "or", "not"]} + if field_dict: + field_expr = self._handle_field_conditions(field_dict) + if field_expr: + conditions.append(field_expr) + + # Combine all conditions with AND + if not conditions: + return "" + return " and ".join(conditions) else: # Simple value comparison return f"{condition}" From 18a301a8c2287ba5f35e57de2d4c0fc2fe47f7b4 Mon Sep 17 00:00:00 2001 From: Wang Daoji <75928131+Wang-Daoji@users.noreply.github.com> Date: Wed, 11 Feb 2026 15:25:58 +0800 Subject: [PATCH 06/12] fix: llm call enable thinking bug (#1080) fix: llm extra body bug Co-authored-by: yuan.wang --- src/memos/api/config.py | 4 ++-- src/memos/api/handlers/config_builders.py | 1 + src/memos/configs/llm.py | 1 + src/memos/llms/vllm.py | 16 ++-------------- 4 files changed, 6 insertions(+), 16 deletions(-) diff --git a/src/memos/api/config.py b/src/memos/api/config.py index 70d9366e3..27f77b6ab 100644 --- a/src/memos/api/config.py +++ b/src/memos/api/config.py @@ -335,7 +335,7 @@ def get_memreader_config() -> dict[str, Any]: # validation requirements during tests/import. "api_base": os.getenv("MEMRADER_API_BASE", "https://api.openai.com/v1"), "remove_think_prefix": True, - "extra_body": {"chat_template_kwargs": {"enable_thinking": False}}, + "extra_body": {"enable_thinking": False}, }, } @@ -531,7 +531,7 @@ def get_internet_config() -> dict[str, Any]: "api_key": os.getenv("MEMRADER_API_KEY", "EMPTY"), "api_base": os.getenv("MEMRADER_API_BASE"), "remove_think_prefix": True, - "extra_body": {"chat_template_kwargs": {"enable_thinking": False}}, + "extra_body": {"enable_thinking": False}, }, }, "embedder": APIConfig.get_embedder_config(), diff --git a/src/memos/api/handlers/config_builders.py b/src/memos/api/handlers/config_builders.py index 2b3fbdd35..7426a60ba 100644 --- a/src/memos/api/handlers/config_builders.py +++ b/src/memos/api/handlers/config_builders.py @@ -105,6 +105,7 @@ def build_chat_llm_config() -> list[dict[str, Any]]: } ), "support_models": cfg.get("support_models", None), + "extra_body": cfg.get("extra_body", None), } for cfg in configs ] diff --git a/src/memos/configs/llm.py b/src/memos/configs/llm.py index 70217b896..5487d117c 100644 --- a/src/memos/configs/llm.py +++ b/src/memos/configs/llm.py @@ -116,6 +116,7 @@ class VLLMLLMConfig(BaseLLMConfig): default=False, description="Enable reasoning outputs from vLLM", ) + extra_body: Any = Field(default=None, description="Extra options for API") class LLMConfigFactory(BaseConfig): diff --git a/src/memos/llms/vllm.py b/src/memos/llms/vllm.py index 362112f11..0efead97b 100644 --- a/src/memos/llms/vllm.py +++ b/src/memos/llms/vllm.py @@ -111,13 +111,7 @@ def _generate_with_api_client(self, messages: list[MessageDict], **kwargs) -> st "temperature": kwargs.get("temperature", self.config.temperature), "max_tokens": kwargs.get("max_tokens", self.config.max_tokens), "top_p": kwargs.get("top_p", self.config.top_p), - "extra_body": { - "chat_template_kwargs": { - "enable_thinking": kwargs.get( - "enable_thinking", self.config.enable_thinking - ) - } - }, + "extra_body": kwargs.get("extra_body", self.config.extra_body), } if kwargs.get("tools"): completion_kwargs["tools"] = kwargs.get("tools") @@ -175,13 +169,7 @@ def generate_stream(self, messages: list[MessageDict], **kwargs): "max_tokens": kwargs.get("max_tokens", self.config.max_tokens), "top_p": kwargs.get("top_p", self.config.top_p), "stream": True, - "extra_body": { - "chat_template_kwargs": { - "enable_thinking": kwargs.get( - "enable_thinking", self.config.enable_thinking - ) - } - }, + "extra_body": kwargs.get("extra_body", self.config.extra_body), } stream = self.client.chat.completions.create(**completion_kwargs) From c7e4cdad40547a38b384fb61c11b78580a0d66c0 Mon Sep 17 00:00:00 2001 From: Wenqiang Wei <46308778+endxxxx@users.noreply.github.com> Date: Wed, 11 Feb 2026 17:09:34 +0800 Subject: [PATCH 07/12] fix: support hours, minutes, and seconds to the filter field. (#1081) * fix: support cases when there is both field condition and logical operators * fix: support hours, minutes, and seconds to the filter field. --- src/memos/vec_dbs/milvus.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/memos/vec_dbs/milvus.py b/src/memos/vec_dbs/milvus.py index 3963e6e4a..e63cabf6f 100644 --- a/src/memos/vec_dbs/milvus.py +++ b/src/memos/vec_dbs/milvus.py @@ -363,6 +363,16 @@ def _handle_field_conditions(self, condition_dict: dict[str, Any]) -> str: def _build_field_expression(self, field: str, value: Any) -> str: """Build expression for a single field.""" + # Convert date-time format from 'YYYY-MM-DD HH:MM:SS' to 'YYYY-MM-DDTHH:MM:SS' for comparison + if (field == "created_at" or field == "updated_at") and isinstance(value, str): + # Replace space with 'T' to match ISO 8601 format + value = value.replace(" ", "T") + elif (field == "created_at" or field == "updated_at") and isinstance(value, dict): + # Handle dict case (e.g., {"gte": "2026-02-09 15:43:12"}) + for op, operand in value.items(): + if isinstance(operand, str): + value[op] = operand.replace(" ", "T") + # Handle comparison operators if isinstance(value, dict): if len(value) == 1: @@ -435,6 +445,11 @@ def _handle_comparison_operator(self, field: str, operator: str, value: Any) -> """Handle comparison operators (gte, lte, gt, lt, ne).""" milvus_op = {"gte": ">=", "lte": "<=", "gt": ">", "lt": "<", "ne": "!="}.get(operator, "==") + # Convert date-time format from 'YYYY-MM-DD HH:MM:SS' to 'YYYY-MM-DDTHH:MM:SS' for comparison + if (field == "created_at" or field == "updated_at") and isinstance(value, str): + # Replace space with 'T' to match ISO 8601 format + value = value.replace(" ", "T") + formatted_value = self._format_value(value) return f"payload['{field}'] {milvus_op} {formatted_value}" @@ -520,7 +535,9 @@ def get_by_filter( Returns: List of items including vectors and payload that match the filter """ + logger.info(f"filter for milvus: {filter}") expr = self._dict_to_expr(filter) if filter else "" + logger.info(f"filter expr for milvus: {expr}") all_items = [] # Use query_iterator for efficient pagination From 8a247a1cf5afe48a3ae3615b6ec7823a2b85b404 Mon Sep 17 00:00:00 2001 From: Wang Daoji <75928131+Wang-Daoji@users.noreply.github.com> Date: Thu, 12 Feb 2026 10:47:56 +0800 Subject: [PATCH 08/12] fix: llm call enable thinking bug (#1082) * fix: llm extra body bug * fix: fix bug --------- Co-authored-by: yuan.wang --- src/memos/api/config.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/memos/api/config.py b/src/memos/api/config.py index 27f77b6ab..d2dd19266 100644 --- a/src/memos/api/config.py +++ b/src/memos/api/config.py @@ -335,7 +335,6 @@ def get_memreader_config() -> dict[str, Any]: # validation requirements during tests/import. "api_base": os.getenv("MEMRADER_API_BASE", "https://api.openai.com/v1"), "remove_think_prefix": True, - "extra_body": {"enable_thinking": False}, }, } @@ -531,7 +530,6 @@ def get_internet_config() -> dict[str, Any]: "api_key": os.getenv("MEMRADER_API_KEY", "EMPTY"), "api_base": os.getenv("MEMRADER_API_BASE"), "remove_think_prefix": True, - "extra_body": {"enable_thinking": False}, }, }, "embedder": APIConfig.get_embedder_config(), From cd1984f5fca63750390b153f3eba35ad24f26e4c Mon Sep 17 00:00:00 2001 From: Wang Daoji <75928131+Wang-Daoji@users.noreply.github.com> Date: Thu, 12 Feb 2026 11:46:54 +0800 Subject: [PATCH 09/12] fix: llm call enable thinking bug (#1083) * fix: llm extra body bug * fix: fix bug * feat: add timed log to function --------- Co-authored-by: yuan.wang --- src/memos/mem_reader/multi_modal_struct.py | 2 ++ .../mem_reader/read_multi_modal/multi_modal_parser.py | 4 ++++ .../mem_reader/read_skill_memory/process_skill_memory.py | 7 +++++++ 3 files changed, 13 insertions(+) diff --git a/src/memos/mem_reader/multi_modal_struct.py b/src/memos/mem_reader/multi_modal_struct.py index 8b0968ca1..a27d64758 100644 --- a/src/memos/mem_reader/multi_modal_struct.py +++ b/src/memos/mem_reader/multi_modal_struct.py @@ -646,6 +646,7 @@ def _merge_memories_with_llm( return None + @timed def _process_string_fine( self, fast_memory_items: list[TextualMemoryItem], @@ -883,6 +884,7 @@ def _get_llm_tool_trajectory_response(self, mem_str: str) -> dict: logger.error(f"[MultiModalFine] Error calling LLM for tool trajectory: {e}") return [] + @timed def _process_tool_trajectory_fine( self, fast_memory_items: list[TextualMemoryItem], info: dict[str, Any], **kwargs ) -> list[TextualMemoryItem]: diff --git a/src/memos/mem_reader/read_multi_modal/multi_modal_parser.py b/src/memos/mem_reader/read_multi_modal/multi_modal_parser.py index 808410e65..81bd25902 100644 --- a/src/memos/mem_reader/read_multi_modal/multi_modal_parser.py +++ b/src/memos/mem_reader/read_multi_modal/multi_modal_parser.py @@ -11,6 +11,7 @@ from memos.log import get_logger from memos.memories.textual.item import SourceMessage, TextualMemoryItem from memos.types import MessagesType +from memos.utils import timed from .assistant_parser import AssistantParser from .base import BaseMessageParser @@ -120,6 +121,7 @@ def _get_parser(self, message: Any) -> BaseMessageParser | None: logger.warning(f"[MultiModalParser] Could not determine parser for message: {message}") return None + @timed def parse( self, message: MessagesType, @@ -157,6 +159,7 @@ def parse( logger.error(f"[MultiModalParser] Error parsing message: {e}") return [] + @timed def parse_batch( self, messages: list[MessagesType], @@ -182,6 +185,7 @@ def parse_batch( results.append(items) return results + @timed def process_transfer( self, source: SourceMessage, diff --git a/src/memos/mem_reader/read_skill_memory/process_skill_memory.py b/src/memos/mem_reader/read_skill_memory/process_skill_memory.py index d691e53a3..d39955ac2 100644 --- a/src/memos/mem_reader/read_skill_memory/process_skill_memory.py +++ b/src/memos/mem_reader/read_skill_memory/process_skill_memory.py @@ -36,6 +36,7 @@ TOOL_GENERATION_PROMPT, ) from memos.types import MessageList +from memos.utils import timed load_dotenv() @@ -64,6 +65,7 @@ def _generate_content_by_llm(llm: BaseLLM, prompt_template: str, **kwargs) -> An return {} if "json" in prompt_template.lower() else "" +@timed def _batch_extract_skills( task_chunks: dict[str, MessageList], related_memories_map: dict[str, list[TextualMemoryItem]], @@ -97,6 +99,7 @@ def _batch_extract_skills( return results +@timed def _batch_generate_skill_details( raw_skills_data: list[tuple[dict[str, Any], str, MessageList]], related_skill_memories_map: dict[str, list[TextualMemoryItem]], @@ -756,6 +759,7 @@ def _delete_skills( logger.warning(f"Error deleting local file: {e}") +@timed def _write_skills_to_file( skill_memory: dict[str, Any], info: dict[str, Any], skills_dir_config: dict[str, Any] ) -> str: @@ -1000,6 +1004,7 @@ def _get_skill_file_storage_location() -> str: return "LOCAL" +@timed def process_skill_memory_fine( fast_memory_items: list[TextualMemoryItem], info: dict[str, Any], @@ -1064,6 +1069,7 @@ def process_skill_memory_fine( ) related_skill_memories_by_task[task_name] = [] + @timed def _simple_extract(): # simple extract skill memory, only one stage memories = [] @@ -1096,6 +1102,7 @@ def _simple_extract(): ) return memories + @timed def _full_extract(): # full extract skill memory, include two stage raw_extraction_results = _batch_extract_skills( From 3f931bfed5cb55c81dfc79782c3602e0bf0c2046 Mon Sep 17 00:00:00 2001 From: CaralHsi Date: Thu, 12 Feb 2026 15:32:12 +0800 Subject: [PATCH 10/12] fix: preference return results (#1088) --- src/memos/multi_mem_cube/single_cube.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/memos/multi_mem_cube/single_cube.py b/src/memos/multi_mem_cube/single_cube.py index 307bf05b2..1678d9d15 100644 --- a/src/memos/multi_mem_cube/single_cube.py +++ b/src/memos/multi_mem_cube/single_cube.py @@ -443,7 +443,28 @@ def _search_pref( }, search_filter=search_req.filter, ) - return self._postformat_memories(results, user_context.mem_cube_id) + formatted_results = self._postformat_memories(results, user_context.mem_cube_id) + + # For each returned item, tackle with metadata.info project_id / + # operation / manager_user_id + for item in formatted_results: + if not isinstance(item, dict): + continue + metadata = item.get("metadata") + if not isinstance(metadata, dict): + continue + info = metadata.get("info") + if not isinstance(info, dict): + continue + + for key in ("project_id", "operation", "manager_user_id"): + if key not in info: + continue + value = info.pop(key) + if key not in metadata: + metadata[key] = value + + return formatted_results except Exception as e: self.logger.error("Error in _search_pref: %s; traceback: %s", e, traceback.format_exc()) return [] From a74fa1cc2173b0a3614b0f7536d93f0f86696ea4 Mon Sep 17 00:00:00 2001 From: Wang Daoji <75928131+Wang-Daoji@users.noreply.github.com> Date: Thu, 12 Feb 2026 16:55:03 +0800 Subject: [PATCH 11/12] feat: optimization v2.0.6 (#1090) * fix: fix bug in handle_get_memories_dashboard * feat: chat complete, stream, business add relativity * fix: fix skill mem llm call return None problem * fix: pref info bug in get memory * fix: info field problem --------- Co-authored-by: yuan.wang --- src/memos/api/handlers/memory_handler.py | 52 ++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/src/memos/api/handlers/memory_handler.py b/src/memos/api/handlers/memory_handler.py index df9f70e77..ef56c7489 100644 --- a/src/memos/api/handlers/memory_handler.py +++ b/src/memos/api/handlers/memory_handler.py @@ -202,6 +202,20 @@ def handle_get_memory(memory_id: str, naive_mem_cube: NaiveMemCube) -> GetMemory # Get the data from whichever memory source succeeded data = (memory or pref).model_dump() if (memory or pref) else None + if data is not None: + # For each returned item, tackle with metadata.info project_id / + # operation / manager_user_id + metadata = data.get("metadata", None) + if metadata is not None and isinstance(metadata, dict): + info = metadata.get("info", None) + if info is not None and isinstance(info, dict): + for key in ("project_id", "operation", "manager_user_id"): + if key not in info: + continue + value = info.pop(key) + if key not in metadata: + metadata[key] = value + return GetMemoryResponse( message="Memory retrieved successfully" if data @@ -241,6 +255,25 @@ def handle_get_memory_by_ids( except Exception: continue + # For each returned item, tackle with metadata.info project_id / + # operation / manager_user_id + for item in memories: + if not isinstance(item, dict): + continue + metadata = item.get("metadata") + if not isinstance(metadata, dict): + continue + info = metadata.get("info") + if not isinstance(info, dict): + continue + + for key in ("project_id", "operation", "manager_user_id"): + if key not in info: + continue + value = info.pop(key) + if key not in metadata: + metadata[key] = value + return GetMemoryResponse( message="Memories retrieved successfully", code=200, data={"memories": memories} ) @@ -345,6 +378,25 @@ def handle_get_memories( ) format_preferences = [format_memory_item(item, save_sources=False) for item in preferences] + # For each returned item, tackle with metadata.info project_id / + # operation / manager_user_id + for item in format_preferences: + if not isinstance(item, dict): + continue + metadata = item.get("metadata") + if not isinstance(metadata, dict): + continue + info = metadata.get("info") + if not isinstance(info, dict): + continue + + for key in ("project_id", "operation", "manager_user_id"): + if key not in info: + continue + value = info.pop(key) + if key not in metadata: + metadata[key] = value + results = post_process_pref_mem( results, format_preferences, get_mem_req.mem_cube_id, get_mem_req.include_preference ) From f19cce49c7eddc3bb60034c1636e52bc6c6f6024 Mon Sep 17 00:00:00 2001 From: "yuan.wang" Date: Thu, 12 Feb 2026 19:26:08 +0800 Subject: [PATCH 12/12] chore: change version number to v2.0.6 --- pyproject.toml | 2 +- src/memos/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index bd37b0c71..b4b01e0e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ ############################################################################## name = "MemoryOS" -version = "2.0.5" +version = "2.0.6" description = "Intelligence Begins with Memory" license = {text = "Apache-2.0"} readme = "README.md" diff --git a/src/memos/__init__.py b/src/memos/__init__.py index 28bcd2277..b568ae0c2 100644 --- a/src/memos/__init__.py +++ b/src/memos/__init__.py @@ -1,4 +1,4 @@ -__version__ = "2.0.5" +__version__ = "2.0.6" from memos.configs.mem_cube import GeneralMemCubeConfig from memos.configs.mem_os import MOSConfig