From dbac0352128f6b7d5c7ebc10c62221a5ada6f933 Mon Sep 17 00:00:00 2001 From: "yuan.wang" Date: Fri, 23 Jan 2026 14:11:57 +0800 Subject: [PATCH 01/35] feat: skill memory --- src/memos/api/product_models.py | 12 +++ .../read_skill_memory/process_skill_memory.py | 102 ++++++++++++++++++ src/memos/memories/textual/tree.py | 4 + .../tree_text_memory/retrieve/searcher.py | 38 ++++++- src/memos/multi_mem_cube/single_cube.py | 2 + src/memos/templates/skill_mem_prompt.py | 12 +++ 6 files changed, 168 insertions(+), 2 deletions(-) create mode 100644 src/memos/mem_reader/read_skill_memory/process_skill_memory.py create mode 100644 src/memos/templates/skill_mem_prompt.py diff --git a/src/memos/api/product_models.py b/src/memos/api/product_models.py index b2f8a9fa3..1889ca7d5 100644 --- a/src/memos/api/product_models.py +++ b/src/memos/api/product_models.py @@ -358,6 +358,18 @@ class APISearchRequest(BaseRequest): description="Number of tool memories to retrieve (top-K). Default: 6.", ) + include_skill_memory: bool = Field( + True, + description="Whether to retrieve skill memories along with general memories. " + "If enabled, the system will automatically recall skill memories " + "relevant to the query. Default: True.", + ) + skill_mem_top_k: int = Field( + 3, + ge=0, + description="Number of skill memories to retrieve (top-K). Default: 3.", + ) + # ==== Filter conditions ==== # TODO: maybe add detailed description later 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 new file mode 100644 index 000000000..8065e3db6 --- /dev/null +++ b/src/memos/mem_reader/read_skill_memory/process_skill_memory.py @@ -0,0 +1,102 @@ +from concurrent.futures import as_completed +from typing import Any + +from memos.context import ContextThreadPoolExecutor +from memos.log import get_logger +from memos.memories.textual.item import TextualMemoryItem +from memos.types import MessageList + + +logger = get_logger(__name__) + + +OSS_DIR = "memos/skill_memory/" + + +def _reconstruct_messages_from_memory_items(memory_items: list[TextualMemoryItem]) -> MessageList: + pass + + +def _add_index_to_message(messages: MessageList) -> MessageList: + pass + + +def _split_task_chunk_by_llm(messages: MessageList) -> dict[str, MessageList]: + pass + + +def _extract_skill_memory_by_llm(task_type: str, messages: MessageList) -> dict[str, Any]: + pass + + +def _upload_skills_to_oss(file_path: str) -> str: + pass + + +def _delete_skills_from_oss(file_path: str) -> None: + pass + + +def _write_skills_to_file(skill_memory: dict[str, Any]) -> str: + pass + + +def create_skill_memory_item(skill_memory: dict[str, Any]) -> TextualMemoryItem: + pass + + +def process_skill_memory_fine( + self, fast_memory_items: list[TextualMemoryItem], info: dict[str, Any], **kwargs +) -> list[TextualMemoryItem]: + messages = _reconstruct_messages_from_memory_items(fast_memory_items) + messages = _add_index_to_message(messages) + + task_chunks = _split_task_chunk_by_llm(messages) + + skill_memories = [] + with ContextThreadPoolExecutor(max_workers=min(len(task_chunks), 5)) as executor: + futures = { + executor.submit(_extract_skill_memory_by_llm, task_type, messages): task_type + for task_type, messages in task_chunks.items() + } + for future in as_completed(futures): + try: + skill_memory = future.result() + skill_memories.append(skill_memory) + except Exception as e: + logger.error(f"Error extracting skill memory: {e}") + continue + + # write skills to file + file_paths = [] + with ContextThreadPoolExecutor(max_workers=min(len(skill_memories), 5)) as executor: + futures = { + executor.submit(_write_skills_to_file, skill_memory): skill_memory + for skill_memory in skill_memories + } + for future in as_completed(futures): + try: + file_path = future.result() + file_paths.append(file_path) + except Exception as e: + logger.error(f"Error writing skills to file: {e}") + continue + + for skill_memory in skill_memories: + if skill_memory.get("update", False): + _delete_skills_from_oss() + + urls = [] + for file_path in file_paths: + # upload skills to oss + _upload_skills_to_oss(file_path) + + # set urls to skill_memories + for skill_memory in skill_memories: + skill_memory["url"] = urls[skill_memory["id"]] + + skill_memory_items = [] + for skill_memory in skill_memories: + skill_memory_items.append(create_skill_memory_item(skill_memory)) + + return skill_memories diff --git a/src/memos/memories/textual/tree.py b/src/memos/memories/textual/tree.py index b963cfa9b..5b999cd6d 100644 --- a/src/memos/memories/textual/tree.py +++ b/src/memos/memories/textual/tree.py @@ -161,6 +161,8 @@ def search( user_name: str | None = None, search_tool_memory: bool = False, tool_mem_top_k: int = 6, + include_skill_memory: bool = False, + skill_mem_top_k: int = 3, dedup: str | None = None, **kwargs, ) -> list[TextualMemoryItem]: @@ -208,6 +210,8 @@ def search( user_name=user_name, search_tool_memory=search_tool_memory, tool_mem_top_k=tool_mem_top_k, + include_skill_memory=include_skill_memory, + skill_mem_top_k=skill_mem_top_k, dedup=dedup, **kwargs, ) 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 8c30d74f3..0c58dd19b 100644 --- a/src/memos/memories/textual/tree_text_memory/retrieve/searcher.py +++ b/src/memos/memories/textual/tree_text_memory/retrieve/searcher.py @@ -119,6 +119,8 @@ def post_retrieve( info=None, search_tool_memory: bool = False, tool_mem_top_k: int = 6, + include_skill_memory: bool = False, + skill_mem_top_k: int = 3, dedup: str | None = None, plugin=False, ): @@ -127,7 +129,13 @@ def post_retrieve( else: deduped = self._deduplicate_results(retrieved_results) final_results = self._sort_and_trim( - deduped, top_k, plugin, search_tool_memory, tool_mem_top_k + deduped, + top_k, + plugin, + search_tool_memory, + tool_mem_top_k, + include_skill_memory, + skill_mem_top_k, ) self._update_usage_history(final_results, info, user_name) return final_results @@ -145,6 +153,8 @@ def search( user_name: str | None = None, search_tool_memory: bool = False, tool_mem_top_k: int = 6, + include_skill_memory: bool = False, + skill_mem_top_k: int = 3, dedup: str | None = None, **kwargs, ) -> list[TextualMemoryItem]: @@ -207,6 +217,8 @@ def search( plugin=kwargs.get("plugin", False), search_tool_memory=search_tool_memory, tool_mem_top_k=tool_mem_top_k, + include_skill_memory=include_skill_memory, + skill_mem_top_k=skill_mem_top_k, dedup=dedup, ) @@ -642,6 +654,17 @@ def _retrieve_from_tool_memory( ) return schema_reranked + trajectory_reranked + # --- Path E + @timed + def _retrieve_from_skill_memory( + self, + query, + parsed_goal, + query_embedding, + top_k, + ): + """Retrieve and rerank from SkillMemory""" + @timed def _retrieve_simple( self, @@ -704,7 +727,14 @@ def _deduplicate_results(self, results): @timed def _sort_and_trim( - self, results, top_k, plugin=False, search_tool_memory=False, tool_mem_top_k=6 + self, + results, + top_k, + plugin=False, + search_tool_memory=False, + tool_mem_top_k=6, + include_skill_memory=False, + skill_mem_top_k=3, ): """Sort results by score and trim to top_k""" final_items = [] @@ -749,6 +779,10 @@ def _sort_and_trim( metadata=SearchedTreeNodeTextualMemoryMetadata(**meta_data), ) ) + + if include_skill_memory: + pass + # separate textual results results = [ (item, score) diff --git a/src/memos/multi_mem_cube/single_cube.py b/src/memos/multi_mem_cube/single_cube.py index 426cf32be..b387a8ee5 100644 --- a/src/memos/multi_mem_cube/single_cube.py +++ b/src/memos/multi_mem_cube/single_cube.py @@ -475,6 +475,8 @@ def _fast_search( plugin=plugin, search_tool_memory=search_req.search_tool_memory, tool_mem_top_k=search_req.tool_mem_top_k, + include_skill_memory=search_req.include_skill_memory, + skill_mem_top_k=search_req.skill_mem_top_k, dedup=search_req.dedup, ) diff --git a/src/memos/templates/skill_mem_prompt.py b/src/memos/templates/skill_mem_prompt.py new file mode 100644 index 000000000..6b90ab7bb --- /dev/null +++ b/src/memos/templates/skill_mem_prompt.py @@ -0,0 +1,12 @@ +TASK_CHUNKING_PROMPT = """ +""" + +SKILL_MEMORY_EXTRACTION_PROMPT = """ +""" + + +SKILLS_AUTHORING_PROMPT = """ +""" + +TASK_QUERY_REWRITE_PROMPT = """ +""" From 36f626a8204013744a08e20c32b2e855baaacade Mon Sep 17 00:00:00 2001 From: Wenqiang Wei Date: Fri, 23 Jan 2026 17:13:10 +0800 Subject: [PATCH 02/35] feat: split task chunks for skill memories --- .../read_skill_memory/process_skill_memory.py | 52 +++++++++-- src/memos/mem_reader/utils.py | 86 ++++++++++++++++++- src/memos/templates/skill_mem_prompt.py | 27 ++++++ 3 files changed, 157 insertions(+), 8 deletions(-) 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 8065e3db6..cf1a4fe22 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 @@ -2,8 +2,11 @@ from typing import Any from memos.context import ContextThreadPoolExecutor +from memos.llms.base import BaseLLM from memos.log import get_logger +from memos.mem_reader.utils import parse_json_string from memos.memories.textual.item import TextualMemoryItem +from memos.templates.skill_mem_prompt import TASK_CHUNKING_PROMPT from memos.types import MessageList @@ -14,15 +17,52 @@ def _reconstruct_messages_from_memory_items(memory_items: list[TextualMemoryItem]) -> MessageList: - pass + reconstructed_messages = [] + for memory_item in memory_items: + for source_message in memory_item.metadata.sources: + try: + role = source_message.role + content = source_message.content + reconstructed_messages.append({"role": role, "content": content}) + except Exception as e: + logger.error(f"Error reconstructing message: {e}") + continue + return reconstructed_messages def _add_index_to_message(messages: MessageList) -> MessageList: - pass - - -def _split_task_chunk_by_llm(messages: MessageList) -> dict[str, MessageList]: - pass + for i, message in enumerate(messages): + message["idx"] = i + return messages + + +def _split_task_chunk_by_llm(llm: BaseLLM, messages: MessageList) -> dict[str, MessageList]: + """Split messages into task chunks by LLM.""" + messages_context = "\n".join( + [ + f"{message.get('idx', i)}: {message['role']}: {message['content']}" + for i, message in enumerate(messages) + ] + ) + prompt = [ + {"role": "user", "content": TASK_CHUNKING_PROMPT.replace("{{messages}}", messages_context)} + ] + for attempt in range(3): + try: + response_text = llm.generate(prompt) + break + except Exception as e: + logger.warning(f"LLM generate failed (attempt {attempt + 1}): {e}") + if attempt == 2: + logger.error("LLM generate failed after 3 retries, returning default value") + return {"default": [messages[i] for i in range(len(messages))]} + response_json = parse_json_string(response_text) + task_chunks = {} + for item in response_json: + task_name = item["task_name"] + message_indices = item["message_indices"] + task_chunks[task_name] = [messages[idx] for idx in message_indices] + return task_chunks def _extract_skill_memory_by_llm(task_type: str, messages: MessageList) -> dict[str, Any]: diff --git a/src/memos/mem_reader/utils.py b/src/memos/mem_reader/utils.py index 4e5a78af2..731984fa2 100644 --- a/src/memos/mem_reader/utils.py +++ b/src/memos/mem_reader/utils.py @@ -71,8 +71,75 @@ def _cheap_close(t: str) -> str: s = s.replace("\\", "\\\\") return json.loads(s) logger.error( - f"[JSONParse] Failed to decode JSON: {e}\nTail: Raw {response_text} \ - json: {s}" + f"[JSONParse] Failed to decode JSON: {e}\nTail: Raw {response_text} \\ json: {s}" + ) + return {} + + +def parse_json_string(response_text: str) -> any: + """Parse JSON string that could be either an object or an array. + + Args: + response_text: The text containing JSON data + + Returns: + Parsed JSON object or array, or empty dict if parsing fails + """ + s = (response_text or "").strip() + + # Extract JSON from code blocks + m = re.search(r"```(?:json)?\s*([\s\S]*?)```", s, flags=re.I) + s = (m.group(1) if m else s.replace("```", "")).strip() + + # Find the start of JSON (either { or [) + brace_idx = s.find("{") + bracket_idx = s.find("[") + + # Determine which one comes first + if brace_idx == -1 and bracket_idx == -1: + return {} + + # Start from the first JSON delimiter + if brace_idx == -1: + # Only bracket found + start_idx = bracket_idx + elif bracket_idx == -1: + # Only brace found + start_idx = brace_idx + else: + # Both found, use the one that comes first + start_idx = min(brace_idx, bracket_idx) + + s = s[start_idx:].strip() + + try: + return json.loads(s) + except json.JSONDecodeError: + pass + + # Try to find the end of JSON + j = max(s.rfind("}"), s.rfind("]")) + if j != -1: + try: + return json.loads(s[: j + 1]) + except json.JSONDecodeError: + pass + + # Try to close the JSON structure + def _cheap_close(t: str) -> str: + t += "}" * max(0, t.count("{") - t.count("}")) + t += "]" * max(0, t.count("[") - t.count("]")) + return t + + t = _cheap_close(s) + try: + return json.loads(t) + except json.JSONDecodeError as e: + if "Invalid \\escape" in str(e): + s = s.replace("\\", "\\\\") + return json.loads(s) + logger.error( + f"[JSONParse] Failed to decode JSON: {e}\nTail: Raw {response_text} \\ json: {s}" ) return {} @@ -155,3 +222,18 @@ def parse_keep_filter_response(text: str) -> tuple[bool, dict[int, dict]]: "reason": reason, } return (len(result) > 0), result + + +if __name__ == "__main__": + json_str = """ + [ + { + "task_id": 1, + "task_name": "任务的简短描述(如:制定旅行计划)", + "message_indices": [0, 1, 2, 3, 4, 5], + "reasoning": "简述为什么将这些消息归为一类" + } + ] + """ + json_data = parse_json_string(json_str) + print(json_data) diff --git a/src/memos/templates/skill_mem_prompt.py b/src/memos/templates/skill_mem_prompt.py index 6b90ab7bb..27f773e99 100644 --- a/src/memos/templates/skill_mem_prompt.py +++ b/src/memos/templates/skill_mem_prompt.py @@ -1,4 +1,31 @@ TASK_CHUNKING_PROMPT = """ +# Role +You are an expert in natural language processing (NLP) and dialogue logic analysis. You excel at organizing logical threads from complex long conversations and accurately extracting users' core intentions. + +# Task +Please analyze the provided conversation records, identify all independent "tasks" that the user has asked the AI to perform, and assign the corresponding dialogue message numbers to each task. + +# Rules & Constraints +1. **Task Independence**: If multiple unrelated topics are discussed in the conversation, identify them as different tasks. +2. **Non-continuous Processing**: Pay attention to identifying "jumping" conversations. For example, if the user made travel plans in messages 8-11, switched to consulting about weather in messages 12-22, and then returned to making travel plans in messages 23-24, be sure to assign both 8-11 and 23-24 to the task "Making travel plans". +3. **Filter Chit-chat**: Only extract tasks with clear goals, instructions, or knowledge-based discussions. Ignore meaningless greetings (such as "Hello", "Are you there?") or closing remarks unless they are part of the task context. +4. **Output Format**: Please strictly follow the JSON format for output to facilitate my subsequent processing. +5. **Language Consistency**: The language used in the task_name field must match the language used in the conversation records. + +[ + { + "task_id": 1, + "task_name": "Brief description of the task (e.g., Making travel plans)", + "message_indices": [0, 1, 2, 3, 4, 5], + "reasoning": "Briefly explain why these messages are grouped together" + }, + ... +] + + + +# Context (Conversation Records) +{{messages}} """ SKILL_MEMORY_EXTRACTION_PROMPT = """ From ec9316db41fae1da8691a5a5da210761f62288f0 Mon Sep 17 00:00:00 2001 From: Wenqiang Wei Date: Fri, 23 Jan 2026 17:36:04 +0800 Subject: [PATCH 03/35] fix: refine the returned format from llm and parsing --- .../read_skill_memory/process_skill_memory.py | 8 +- src/memos/mem_reader/utils.py | 83 ------------------- src/memos/templates/skill_mem_prompt.py | 4 +- 3 files changed, 8 insertions(+), 87 deletions(-) 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 cf1a4fe22..2c55834d6 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 @@ -1,10 +1,11 @@ +import json + from concurrent.futures import as_completed from typing import Any from memos.context import ContextThreadPoolExecutor from memos.llms.base import BaseLLM from memos.log import get_logger -from memos.mem_reader.utils import parse_json_string from memos.memories.textual.item import TextualMemoryItem from memos.templates.skill_mem_prompt import TASK_CHUNKING_PROMPT from memos.types import MessageList @@ -56,12 +57,13 @@ def _split_task_chunk_by_llm(llm: BaseLLM, messages: MessageList) -> dict[str, M if attempt == 2: logger.error("LLM generate failed after 3 retries, returning default value") return {"default": [messages[i] for i in range(len(messages))]} - response_json = parse_json_string(response_text) + response_json = json.loads(response_text.replace("```json", "").replace("```", "")) task_chunks = {} for item in response_json: task_name = item["task_name"] message_indices = item["message_indices"] - task_chunks[task_name] = [messages[idx] for idx in message_indices] + for start, end in message_indices: + task_chunks.setdefault(task_name, []).extend(messages[start : end + 1]) return task_chunks diff --git a/src/memos/mem_reader/utils.py b/src/memos/mem_reader/utils.py index 731984fa2..99fd9347f 100644 --- a/src/memos/mem_reader/utils.py +++ b/src/memos/mem_reader/utils.py @@ -76,74 +76,6 @@ def _cheap_close(t: str) -> str: return {} -def parse_json_string(response_text: str) -> any: - """Parse JSON string that could be either an object or an array. - - Args: - response_text: The text containing JSON data - - Returns: - Parsed JSON object or array, or empty dict if parsing fails - """ - s = (response_text or "").strip() - - # Extract JSON from code blocks - m = re.search(r"```(?:json)?\s*([\s\S]*?)```", s, flags=re.I) - s = (m.group(1) if m else s.replace("```", "")).strip() - - # Find the start of JSON (either { or [) - brace_idx = s.find("{") - bracket_idx = s.find("[") - - # Determine which one comes first - if brace_idx == -1 and bracket_idx == -1: - return {} - - # Start from the first JSON delimiter - if brace_idx == -1: - # Only bracket found - start_idx = bracket_idx - elif bracket_idx == -1: - # Only brace found - start_idx = brace_idx - else: - # Both found, use the one that comes first - start_idx = min(brace_idx, bracket_idx) - - s = s[start_idx:].strip() - - try: - return json.loads(s) - except json.JSONDecodeError: - pass - - # Try to find the end of JSON - j = max(s.rfind("}"), s.rfind("]")) - if j != -1: - try: - return json.loads(s[: j + 1]) - except json.JSONDecodeError: - pass - - # Try to close the JSON structure - def _cheap_close(t: str) -> str: - t += "}" * max(0, t.count("{") - t.count("}")) - t += "]" * max(0, t.count("[") - t.count("]")) - return t - - t = _cheap_close(s) - try: - return json.loads(t) - except json.JSONDecodeError as e: - if "Invalid \\escape" in str(e): - s = s.replace("\\", "\\\\") - return json.loads(s) - logger.error( - f"[JSONParse] Failed to decode JSON: {e}\nTail: Raw {response_text} \\ json: {s}" - ) - return {} - - def parse_rewritten_response(text: str) -> tuple[bool, dict[int, dict]]: """Parse index-keyed JSON from hallucination filter response. Expected shape: { "0": {"need_rewrite": bool, "rewritten": str, "reason": str}, ... } @@ -222,18 +154,3 @@ def parse_keep_filter_response(text: str) -> tuple[bool, dict[int, dict]]: "reason": reason, } return (len(result) > 0), result - - -if __name__ == "__main__": - json_str = """ - [ - { - "task_id": 1, - "task_name": "任务的简短描述(如:制定旅行计划)", - "message_indices": [0, 1, 2, 3, 4, 5], - "reasoning": "简述为什么将这些消息归为一类" - } - ] - """ - json_data = parse_json_string(json_str) - print(json_data) diff --git a/src/memos/templates/skill_mem_prompt.py b/src/memos/templates/skill_mem_prompt.py index 27f773e99..eaa709bc7 100644 --- a/src/memos/templates/skill_mem_prompt.py +++ b/src/memos/templates/skill_mem_prompt.py @@ -12,15 +12,17 @@ 4. **Output Format**: Please strictly follow the JSON format for output to facilitate my subsequent processing. 5. **Language Consistency**: The language used in the task_name field must match the language used in the conversation records. +```json [ { "task_id": 1, "task_name": "Brief description of the task (e.g., Making travel plans)", - "message_indices": [0, 1, 2, 3, 4, 5], + "message_indices": [[0, 5],[16, 17]], # 0-5 and 16-17 are the message indices for this task "reasoning": "Briefly explain why these messages are grouped together" }, ... ] +``` From 0d33b1dc9425e063c953fc829bb306ed3581a7c2 Mon Sep 17 00:00:00 2001 From: "yuan.wang" Date: Sat, 24 Jan 2026 11:50:22 +0800 Subject: [PATCH 04/35] feat: add new pack oss --- docker/requirements.txt | 1 + poetry.lock | 133 +++++++++++++++++- pyproject.toml | 6 + .../read_skill_memory/process_skill_memory.py | 46 +++++- 4 files changed, 179 insertions(+), 7 deletions(-) diff --git a/docker/requirements.txt b/docker/requirements.txt index f89617c10..e8d77acb2 100644 --- a/docker/requirements.txt +++ b/docker/requirements.txt @@ -123,3 +123,4 @@ uvicorn==0.38.0 uvloop==0.22.1; sys_platform != 'win32' watchfiles==1.1.1 websockets==15.0.1 +alibabacloud-oss-v2==1.2.2 diff --git a/poetry.lock b/poetry.lock index fb818e665..d2ecf26b2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -12,6 +12,23 @@ files = [ {file = "absl_py-2.3.1.tar.gz", hash = "sha256:a97820526f7fbfd2ec1bce83f3f25e3a14840dac0d8e02a0b71cd75db3f77fc9"}, ] +[[package]] +name = "alibabacloud-oss-v2" +version = "1.2.2" +description = "Alibaba Cloud OSS (Object Storage Service) SDK V2 for Python" +optional = true +python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"skill-mem\" or extra == \"all\"" +files = [ + {file = "alibabacloud_oss_v2-1.2.2-py3-none-any.whl", hash = "sha256:d138d1bdb38da6cc20d96b96faaeb099062a710a7f3d50f4b4b39a8cfcbdc120"}, +] + +[package.dependencies] +crcmod-plus = ">=2.1.0" +pycryptodome = ">=3.4.7" +requests = ">=2.18.4" + [[package]] name = "annotated-types" version = "0.7.0" @@ -582,6 +599,65 @@ mypy = ["bokeh", "contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.15.0)", " test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] test-no-images = ["pytest", "pytest-cov", "pytest-rerunfailures", "pytest-xdist", "wurlitzer"] +[[package]] +name = "crcmod-plus" +version = "2.3.1" +description = "CRC generator - modernized" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"skill-mem\" or extra == \"all\"" +files = [ + {file = "crcmod_plus-2.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:466d5fb9a05549a401164a2ba46a560779f7240f43f0b864e9fd277c5c12133a"}, + {file = "crcmod_plus-2.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b31f039c440d59b808d1d90afbfd90ad901dc6e4a81d32a0fefa8d2c118064b9"}, + {file = "crcmod_plus-2.3.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:24088832717435fc94d948e3140518c5a19fea99d1f6180b3396320398aca4c1"}, + {file = "crcmod_plus-2.3.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e5632576426e78c51ad4ed0569650e397f282cec2751862f3fd8a88dd9d5019a"}, + {file = "crcmod_plus-2.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0313488db8e9048deee987f04859b9ad46c8e6fa26385fb1d3e481c771530961"}, + {file = "crcmod_plus-2.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c1d8ae3ed019e9c164f1effee61cbc509ca39695738f7556fc0685e4c9218c86"}, + {file = "crcmod_plus-2.3.1-cp310-cp310-win32.whl", hash = "sha256:bb54ac5623938726f4e92c18af0ccd9d119011e1821e949440bbfd24552ca539"}, + {file = "crcmod_plus-2.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:79c58a3118e0c95cedffb48745fa1071982f8ba84309267b6020c2fffdbfaea7"}, + {file = "crcmod_plus-2.3.1-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:b7e35e0f7d93d7571c2c9c3d6760e456999ea4c1eae5ead6acac247b5a79e469"}, + {file = "crcmod_plus-2.3.1-cp311-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6853243120db84677b94b625112116f0ef69cd581741d20de58dce4c34242654"}, + {file = "crcmod_plus-2.3.1-cp311-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:17735bc4e944d552ea18c8609fc6d08a5e64ee9b29cc216ba4d623754029cc3a"}, + {file = "crcmod_plus-2.3.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ac755040a2a35f43ab331978c48a9acb4ff64b425f282a296be467a410f00c3"}, + {file = "crcmod_plus-2.3.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bdcfb838ca093ca673a3bbb37f62d1e5ec7182e00cc5ee2d00759f9f9f8ab11"}, + {file = "crcmod_plus-2.3.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9166bc3c9b5e7b07b4e6854cac392b4a451b31d58d3950e48c140ab7b5d05394"}, + {file = "crcmod_plus-2.3.1-cp311-abi3-win32.whl", hash = "sha256:cb99b694cce5c862560cf332a8b5e793620e28f0de3726995608bbd6f9b6e09a"}, + {file = "crcmod_plus-2.3.1-cp311-abi3-win_amd64.whl", hash = "sha256:82b0f7e968c430c5a80fe0fc59e75cb54f2e84df2ed0cee5a3ff9cadfbf8a220"}, + {file = "crcmod_plus-2.3.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:fcb7a64648d70cac0a90c23bc6c58de6c13b28a0841c742039ba8528e23f51d1"}, + {file = "crcmod_plus-2.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:abcf3ac30e41a58dd8d2659930e357d2fd47ab4fabb52382698ed1003c9a2598"}, + {file = "crcmod_plus-2.3.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:693d2791af64aaf4467efe1473e02acd0ef1da229100262f29198f3ad59d42f8"}, + {file = "crcmod_plus-2.3.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab075292b41b33be4d2f349e1139ea897023c3ebffc28c0d4c2ed7f2b31f1bce"}, + {file = "crcmod_plus-2.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ccdc48e0af53c68304d60bbccfd5f51aed9979b5721016c3e097d51e0692b35e"}, + {file = "crcmod_plus-2.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:283d23e4f13629413e6c963ffcc49c6166c9829b1e4ec6488e0d3703bd218dce"}, + {file = "crcmod_plus-2.3.1-cp313-cp313t-win32.whl", hash = "sha256:53319d2e9697a8d68260709aa61987fb89c49dd02b7f585b82c578659c1922b6"}, + {file = "crcmod_plus-2.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:c9ebd256f792ef01a1d0335419f679e7501d4fdf132a5206168c5269fcea65d0"}, + {file = "crcmod_plus-2.3.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:52abc724f5232eddbe565c258878123337339bf9cfe9ac9c154e38557b8affc5"}, + {file = "crcmod_plus-2.3.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8b0e644395d68bbfb576ee28becb69d962b173fa648ce269aec260f538841fa9"}, + {file = "crcmod_plus-2.3.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:07962695c53eedf3c9f0bacb2d7d6c00064394d4c88c0eb7d5b082808812fe82"}, + {file = "crcmod_plus-2.3.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:43acb79630192f91e60ec5b979a0e1fc2a4734182ce8b37d657f11fcd27c1f86"}, + {file = "crcmod_plus-2.3.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:52aacdfc0f04510c9c0e6ecf7c09528543cb00f4d4edd0871be8c9b8e03f2c08"}, + {file = "crcmod_plus-2.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ac4ce5a423f3ccf143a42ce6af4661e2f806f09a6124c24996689b3457f1afcb"}, + {file = "crcmod_plus-2.3.1-cp314-cp314t-win32.whl", hash = "sha256:cf2df1058d6bf674c8b7b6f56c7ecdc0479707c81860f032abf69526f0111f70"}, + {file = "crcmod_plus-2.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:ba925ca53a1e00233a1b93380a46c0e821f6b797a19fc401aec85219cd85fd6f"}, + {file = "crcmod_plus-2.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:22600072de422632531e92d7675faf223a5b2548d45c5cd6f77ec4575339900f"}, + {file = "crcmod_plus-2.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f940704e359607b47b4a8e98c4d0f453f15bea039eb183cd0ffb14a8268fea78"}, + {file = "crcmod_plus-2.3.1-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f939fc1f7d143962a8fbed2305ce5931627fea1ea3a7f1865c04dbba9d41bf67"}, + {file = "crcmod_plus-2.3.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3c6e8c7cf7ef49bcae7d3293996f82edde98e5fa202752ae58bf37a0289d35d"}, + {file = "crcmod_plus-2.3.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:728f68d0e3049ba23978aaf277f3eb405dd21e78be6ba96382739ba09bba473c"}, + {file = "crcmod_plus-2.3.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b3829ed0dba48765f9b4139cb70b9bdf6553d2154302d9e3de6377556357892f"}, + {file = "crcmod_plus-2.3.1-cp39-cp39-win32.whl", hash = "sha256:855fcbd07c3eb9162c701c1c7ed1a8b5a5f7b1e8c2dd3fd8ed2273e2f141ecc9"}, + {file = "crcmod_plus-2.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:5422081be6403b6fba736c544e79c68410307f7a1a8ac1925b421a5c6f4591d3"}, + {file = "crcmod_plus-2.3.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9397324da1be2729f894744d9031a21ed97584c17fb0289e69e0c3c60916fc5f"}, + {file = "crcmod_plus-2.3.1-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:073c7a3b832652e66c41c8b8705eaecda704d1cbe850b9fa05fdee36cd50745a"}, + {file = "crcmod_plus-2.3.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e5f4c62553f772ea7ae12d9484801b752622c9c288e49ee7ea34a20b94e4920"}, + {file = "crcmod_plus-2.3.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:5e80a9860f66f339956f540d86a768f4fe8c8bfcb139811f14be864425c48d64"}, + {file = "crcmod_plus-2.3.1.tar.gz", hash = "sha256:732ffe3c3ce3ef9b272e1827d8fb894590c4d6ff553f2a2b41ae30f4f94b0f5d"}, +] + +[package.extras] +dev = ["pytest"] + [[package]] name = "cryptography" version = "45.0.5" @@ -3507,6 +3583,58 @@ files = [ ] markers = {main = "extra == \"mem-reader\" or extra == \"all\" or platform_python_implementation != \"PyPy\"", eval = "platform_python_implementation == \"PyPy\""} +[[package]] +name = "pycryptodome" +version = "3.23.0" +description = "Cryptographic library for Python" +optional = true +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main"] +markers = "extra == \"skill-mem\" or extra == \"all\"" +files = [ + {file = "pycryptodome-3.23.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a176b79c49af27d7f6c12e4b178b0824626f40a7b9fed08f712291b6d54bf566"}, + {file = "pycryptodome-3.23.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:573a0b3017e06f2cffd27d92ef22e46aa3be87a2d317a5abf7cc0e84e321bd75"}, + {file = "pycryptodome-3.23.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:63dad881b99ca653302b2c7191998dd677226222a3f2ea79999aa51ce695f720"}, + {file = "pycryptodome-3.23.0-cp27-cp27m-win32.whl", hash = "sha256:b34e8e11d97889df57166eda1e1ddd7676da5fcd4d71a0062a760e75060514b4"}, + {file = "pycryptodome-3.23.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:7ac1080a8da569bde76c0a104589c4f414b8ba296c0b3738cf39a466a9fb1818"}, + {file = "pycryptodome-3.23.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6fe8258e2039eceb74dfec66b3672552b6b7d2c235b2dfecc05d16b8921649a8"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39"}, + {file = "pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27"}, + {file = "pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843"}, + {file = "pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490"}, + {file = "pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575"}, + {file = "pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b"}, + {file = "pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a"}, + {file = "pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f"}, + {file = "pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa"}, + {file = "pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886"}, + {file = "pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2"}, + {file = "pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c"}, + {file = "pycryptodome-3.23.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:350ebc1eba1da729b35ab7627a833a1a355ee4e852d8ba0447fafe7b14504d56"}, + {file = "pycryptodome-3.23.0-pp27-pypy_73-win32.whl", hash = "sha256:93837e379a3e5fd2bb00302a47aee9fdf7940d83595be3915752c74033d17ca7"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddb95b49df036ddd264a0ad246d1be5b672000f12d6961ea2c267083a5e19379"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e95564beb8782abfd9e431c974e14563a794a4944c29d6d3b7b5ea042110b4"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e15c081e912c4b0d75632acd8382dfce45b258667aa3c67caf7a4d4c13f630"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7fc76bf273353dc7e5207d172b83f569540fc9a28d63171061c42e361d22353"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:45c69ad715ca1a94f778215a11e66b7ff989d792a4d63b68dc586a1da1392ff5"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:865d83c906b0fc6a59b510deceee656b6bc1c4fa0d82176e2b77e97a420a996a"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89d4d56153efc4d81defe8b65fd0821ef8b2d5ddf8ed19df31ba2f00872b8002"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3f2d0aaf8080bda0587d58fc9fe4766e012441e2eed4269a77de6aea981c8be"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64093fc334c1eccfd3933c134c4457c34eaca235eeae49d69449dc4728079339"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ce64e84a962b63a47a592690bdc16a7eaf709d2c2697ababf24a0def566899a6"}, + {file = "pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef"}, +] + [[package]] name = "pydantic" version = "2.11.7" @@ -6234,14 +6362,15 @@ cffi = {version = ">=1.11", markers = "platform_python_implementation == \"PyPy\ cffi = ["cffi (>=1.11)"] [extras] -all = ["cachetools", "chonkie", "datasketch", "jieba", "langchain-text-splitters", "markitdown", "neo4j", "nltk", "pika", "pymilvus", "pymysql", "qdrant-client", "rake-nltk", "rank-bm25", "redis", "schedule", "sentence-transformers", "torch", "volcengine-python-sdk"] +all = ["alibabacloud-oss-v2", "cachetools", "chonkie", "datasketch", "jieba", "langchain-text-splitters", "markitdown", "neo4j", "nltk", "pika", "pymilvus", "pymysql", "qdrant-client", "rake-nltk", "rank-bm25", "redis", "schedule", "sentence-transformers", "torch", "volcengine-python-sdk"] mem-reader = ["chonkie", "langchain-text-splitters", "markitdown"] mem-scheduler = ["pika", "redis"] mem-user = ["pymysql"] pref-mem = ["datasketch", "pymilvus"] +skill-mem = ["alibabacloud-oss-v2"] tree-mem = ["neo4j", "schedule"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "22bfcac5ed0be1e3aea294e3da96ff1a4bd9d7b62865ad827e1508f5ade6b708" +content-hash = "d4a267db0ac8b85f5bd995b34bfd7ebb8a678e478ddb3c3e45fb52cf58403b50" diff --git a/pyproject.toml b/pyproject.toml index 53fec3151..1a7a1ca73 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -97,6 +97,11 @@ pref-mem = [ "datasketch (>=1.6.5,<2.0.0)", # MinHash library ] +# SkillMemory +skill-mem = [ + "alibabacloud-oss-v2 (>=1.2.2,<1.2.3)", +] + # All optional dependencies # Allow users to install with `pip install MemoryOS[all]` all = [ @@ -123,6 +128,7 @@ all = [ "volcengine-python-sdk (>=4.0.4,<5.0.0)", "nltk (>=3.9.1,<4.0.0)", "rake-nltk (>=1.0.6,<1.1.0)", + "alibabacloud-oss-v2 (>=1.2.2,<1.2.3)", # Uncategorized dependencies ] 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 2c55834d6..b7526e610 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 @@ -1,8 +1,11 @@ import json +import os from concurrent.futures import as_completed from typing import Any +import alibabacloud_oss_v2 as oss + from memos.context import ContextThreadPoolExecutor from memos.llms.base import BaseLLM from memos.log import get_logger @@ -17,6 +20,22 @@ OSS_DIR = "memos/skill_memory/" +def create_oss_client() -> oss.Client: + credentials_provider = oss.credentials.EnvironmentVariableCredentialsProvider() + + # load SDK's default configuration, and set credential provider + cfg = oss.config.load_default() + cfg.credentials_provider = credentials_provider + cfg.region = os.getenv("OSS_REGION") + cfg.endpoint = os.getenv("OSS_ENDPOINT") + client = oss.Client(cfg) + + return client + + +OSS_CLIENT = create_oss_client() + + def _reconstruct_messages_from_memory_items(memory_items: list[TextualMemoryItem]) -> MessageList: reconstructed_messages = [] for memory_item in memory_items: @@ -67,16 +86,33 @@ def _split_task_chunk_by_llm(llm: BaseLLM, messages: MessageList) -> dict[str, M return task_chunks -def _extract_skill_memory_by_llm(task_type: str, messages: MessageList) -> dict[str, Any]: +def _extract_skill_memory_by_llm( + task_type: str, messages: MessageList, llm: BaseLLM +) -> dict[str, Any]: pass -def _upload_skills_to_oss(file_path: str) -> str: - pass +def _upload_skills_to_oss( + local_file_path: str, oss_file_path: str, client: oss.Client +) -> oss.PutObjectResult: + result = client.put_object_from_file( + request=oss.PutObjectRequest( + bucket=os.getenv("OSS_BUCKET_NAME"), + key=oss_file_path, + ), + filepath=local_file_path, + ) + return result -def _delete_skills_from_oss(file_path: str) -> None: - pass +def _delete_skills_from_oss(oss_file_path: str, client: oss.Client) -> oss.DeleteObjectResult: + result = client.delete_object( + oss.DeleteObjectRequest( + bucket=os.getenv("OSS_BUCKET_NAME"), + key=oss_file_path, + ) + ) + return result def _write_skills_to_file(skill_memory: dict[str, Any]) -> str: From 3a8841987c4d99b5cf08d543308d277c34622974 Mon Sep 17 00:00:00 2001 From: "yuan.wang" Date: Sun, 25 Jan 2026 12:09:34 +0800 Subject: [PATCH 05/35] feat: skill mem pipeline --- src/memos/api/handlers/formatters_handler.py | 13 +- src/memos/api/product_models.py | 2 +- src/memos/mem_reader/factory.py | 5 + src/memos/mem_reader/multi_modal_struct.py | 12 + .../read_skill_memory/process_skill_memory.py | 421 ++++++++++++++++-- src/memos/mem_reader/simple_struct.py | 4 + src/memos/memories/textual/item.py | 1 + .../tree_text_memory/organize/manager.py | 16 +- .../tree_text_memory/retrieve/recall.py | 1 + .../tree_text_memory/retrieve/searcher.py | 93 +++- src/memos/multi_mem_cube/composite_cube.py | 3 +- src/memos/multi_mem_cube/single_cube.py | 1 + src/memos/templates/skill_mem_prompt.py | 86 ++++ 13 files changed, 613 insertions(+), 45 deletions(-) diff --git a/src/memos/api/handlers/formatters_handler.py b/src/memos/api/handlers/formatters_handler.py index 29e376d33..4d9c6bdc2 100644 --- a/src/memos/api/handlers/formatters_handler.py +++ b/src/memos/api/handlers/formatters_handler.py @@ -112,13 +112,17 @@ def post_process_textual_mem( fact_mem = [ mem for mem in text_formatted_mem - if mem["metadata"]["memory_type"] not in ["ToolSchemaMemory", "ToolTrajectoryMemory"] + if mem["metadata"]["memory_type"] + in ["WorkingMemory", "LongTermMemory", "UserMemory", "OuterMemory"] ] tool_mem = [ mem for mem in text_formatted_mem if mem["metadata"]["memory_type"] in ["ToolSchemaMemory", "ToolTrajectoryMemory"] ] + skill_mem = [ + mem for mem in text_formatted_mem if mem["metadata"]["memory_type"] == "SkillMemory" + ] memories_result["text_mem"].append( { @@ -134,6 +138,13 @@ def post_process_textual_mem( "total_nodes": len(tool_mem), } ) + memories_result["skill_mem"].append( + { + "cube_id": mem_cube_id, + "memories": skill_mem, + "total_nodes": len(skill_mem), + } + ) return memories_result diff --git a/src/memos/api/product_models.py b/src/memos/api/product_models.py index 1889ca7d5..cc37474ac 100644 --- a/src/memos/api/product_models.py +++ b/src/memos/api/product_models.py @@ -405,7 +405,7 @@ class APISearchRequest(BaseRequest): # Internal field for search memory type search_memory_type: str = Field( "All", - description="Type of memory to search: All, WorkingMemory, LongTermMemory, UserMemory, OuterMemory, ToolSchemaMemory, ToolTrajectoryMemory", + description="Type of memory to search: All, WorkingMemory, LongTermMemory, UserMemory, OuterMemory, ToolSchemaMemory, ToolTrajectoryMemory, SkillMemory", ) # ==== Context ==== diff --git a/src/memos/mem_reader/factory.py b/src/memos/mem_reader/factory.py index 2749327bf..7bd551fb8 100644 --- a/src/memos/mem_reader/factory.py +++ b/src/memos/mem_reader/factory.py @@ -10,6 +10,7 @@ if TYPE_CHECKING: from memos.graph_dbs.base import BaseGraphDB + from memos.memories.textual.tree_text_memory.retrieve.searcher import Searcher class MemReaderFactory(BaseMemReader): @@ -27,6 +28,7 @@ def from_config( cls, config_factory: MemReaderConfigFactory, graph_db: Optional["BaseGraphDB | None"] = None, + searcher: Optional["Searcher | None"] = None, ) -> BaseMemReader: """ Create a MemReader instance from configuration. @@ -50,4 +52,7 @@ def from_config( if graph_db is not None: reader.set_graph_db(graph_db) + if searcher is not None: + reader.set_searcher(searcher) + return reader diff --git a/src/memos/mem_reader/multi_modal_struct.py b/src/memos/mem_reader/multi_modal_struct.py index 9edcd0a55..9779e98fe 100644 --- a/src/memos/mem_reader/multi_modal_struct.py +++ b/src/memos/mem_reader/multi_modal_struct.py @@ -10,6 +10,7 @@ from memos.context.context import ContextThreadPoolExecutor from memos.mem_reader.read_multi_modal import MultiModalParser, detect_lang from memos.mem_reader.read_multi_modal.base import _derive_key +from memos.mem_reader.read_skill_memory.process_skill_memory import process_skill_memory_fine from memos.mem_reader.simple_struct import PROMPT_DICT, SimpleStructMemReader from memos.mem_reader.utils import parse_json_result from memos.memories.textual.item import TextualMemoryItem, TreeNodeTextualMemoryMetadata @@ -819,13 +820,24 @@ def _process_multi_modal_data( future_tool = executor.submit( self._process_tool_trajectory_fine, fast_memory_items, info, **kwargs ) + future_skill = executor.submit( + process_skill_memory_fine, + fast_memory_items=fast_memory_items, + info=info, + searcher=self.searcher, + llm=self.llm, + rewrite_query=kwargs.get("rewrite_query", False), + **kwargs, + ) # Collect results fine_memory_items_string_parser = future_string.result() fine_memory_items_tool_trajectory_parser = future_tool.result() + fine_memory_items_skill_memory_parser = future_skill.result() fine_memory_items.extend(fine_memory_items_string_parser) fine_memory_items.extend(fine_memory_items_tool_trajectory_parser) + fine_memory_items.extend(fine_memory_items_skill_memory_parser) # Part B: get fine multimodal items for fast_item in fast_memory_items: 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 b7526e610..dfd4fb013 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 @@ -1,16 +1,25 @@ import json import os +import tempfile +import uuid +import zipfile from concurrent.futures import as_completed +from datetime import datetime from typing import Any import alibabacloud_oss_v2 as oss -from memos.context import ContextThreadPoolExecutor +from memos.context.context import ContextThreadPoolExecutor from memos.llms.base import BaseLLM from memos.log import get_logger -from memos.memories.textual.item import TextualMemoryItem -from memos.templates.skill_mem_prompt import TASK_CHUNKING_PROMPT +from memos.memories.textual.item import TextualMemoryItem, TreeNodeTextualMemoryMetadata +from memos.memories.textual.tree_text_memory.retrieve.searcher import Searcher +from memos.templates.skill_mem_prompt import ( + SKILL_MEMORY_EXTRACTION_PROMPT, + TASK_CHUNKING_PROMPT, + TASK_QUERY_REWRITE_PROMPT, +) from memos.types import MessageList @@ -87,22 +96,139 @@ def _split_task_chunk_by_llm(llm: BaseLLM, messages: MessageList) -> dict[str, M def _extract_skill_memory_by_llm( - task_type: str, messages: MessageList, llm: BaseLLM + messages: MessageList, old_memories: list[TextualMemoryItem], llm: BaseLLM ) -> dict[str, Any]: - pass + old_memories_dict = [skill_memory.model_dump() for skill_memory in old_memories] + old_mem_references = [ + { + "id": mem["id"], + "name": mem["metadata"]["name"], + "description": mem["metadata"]["description"], + "procedure": mem["metadata"]["procedure"], + "experience": mem["metadata"]["experience"], + "preference": mem["metadata"]["preference"], + "example": mem["metadata"]["example"], + "tags": mem["metadata"]["tags"], + "scripts": mem["metadata"].get("scripts"), + "others": mem["metadata"]["others"], + } + for mem in old_memories_dict + ] + + # Prepare conversation context + messages_context = "\n".join( + [f"{message['role']}: {message['content']}" for message in messages] + ) + + # Prepare old memories context + old_memories_context = json.dumps(old_mem_references, ensure_ascii=False, indent=2) + + # Prepare prompt + prompt_content = SKILL_MEMORY_EXTRACTION_PROMPT.replace( + "{old_memories}", old_memories_context + ).replace("{messages}", messages_context) + + prompt = [{"role": "user", "content": prompt_content}] + + # Call LLM to extract skill memory with retry logic + for attempt in range(3): + try: + response_text = llm.generate(prompt) + # Clean up response (remove markdown code blocks if present) + response_text = response_text.strip() + if response_text.startswith("```json"): + response_text = response_text.replace("```json", "").replace("```", "").strip() + elif response_text.startswith("```"): + response_text = response_text.replace("```", "").strip() + + # Parse JSON response + skill_memory = json.loads(response_text) + + # Validate response + if skill_memory is None: + logger.info("No skill memory extracted from conversation") + return None + + return skill_memory + + except json.JSONDecodeError as e: + logger.warning(f"JSON decode failed (attempt {attempt + 1}): {e}") + logger.debug(f"Response text: {response_text}") + if attempt == 2: + logger.error("Failed to parse skill memory after 3 retries") + return None + except Exception as e: + logger.warning(f"LLM skill memory extraction failed (attempt {attempt + 1}): {e}") + if attempt == 2: + logger.error("LLM skill memory extraction failed after 3 retries") + return None + + return None -def _upload_skills_to_oss( - local_file_path: str, oss_file_path: str, client: oss.Client -) -> oss.PutObjectResult: - result = client.put_object_from_file( +def _recall_related_skill_memories( + task_type: str, + messages: MessageList, + searcher: Searcher, + llm: BaseLLM, + rewrite_query: bool, +) -> list[TextualMemoryItem]: + query = _rewrite_query(task_type, messages, llm, rewrite_query) + related_skill_memories = searcher.search(query, top_k=10, memory_type="SkillMemory") + + return related_skill_memories + + +def _rewrite_query(task_type: str, messages: MessageList, llm: BaseLLM, rewrite_query: bool) -> str: + if not rewrite_query: + # Return the first user message content if rewrite is disabled + return messages[0]["content"] if messages else "" + + # Construct messages context for LLM + messages_context = "\n".join( + [f"{message['role']}: {message['content']}" for message in messages] + ) + + # Prepare prompt with task type and messages + prompt_content = TASK_QUERY_REWRITE_PROMPT.replace("{task_type}", task_type).replace( + "{messages}", messages_context + ) + 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"Rewritten query for task '{task_type}': {response_text}") + return response_text + except Exception as e: + logger.warning(f"LLM query rewrite failed (attempt {attempt + 1}): {e}") + if attempt == 2: + logger.error( + "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 "" + + +def _upload_skills_to_oss(local_file_path: str, oss_file_path: str, client: oss.Client) -> str: + client.put_object_from_file( request=oss.PutObjectRequest( bucket=os.getenv("OSS_BUCKET_NAME"), key=oss_file_path, ), filepath=local_file_path, ) - return result + + # Construct and return the URL + bucket_name = os.getenv("OSS_BUCKET_NAME") + endpoint = os.getenv("OSS_ENDPOINT") + url = f"https://{bucket_name}.{endpoint}/{oss_file_path}" + return url def _delete_skills_from_oss(oss_file_path: str, client: oss.Client) -> oss.DeleteObjectResult: @@ -115,66 +241,289 @@ def _delete_skills_from_oss(oss_file_path: str, client: oss.Client) -> oss.Delet return result -def _write_skills_to_file(skill_memory: dict[str, Any]) -> str: - pass +def _write_skills_to_file(skill_memory: dict[str, Any], info: dict[str, Any]) -> str: + user_id = info.get("user_id", "unknown") + skill_name = skill_memory.get("name", "unnamed_skill").replace(" ", "_").lower() + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + + # Create tmp directory for user if it doesn't exist + tmp_dir = os.path.join("/tmp", user_id) + os.makedirs(tmp_dir, exist_ok=True) + + # Create a temporary directory for the skill structure + with tempfile.TemporaryDirectory() as temp_skill_dir: + skill_dir = os.path.join(temp_skill_dir, skill_name) + os.makedirs(skill_dir, exist_ok=True) + + # Generate SKILL.md content with frontmatter + skill_md_content = f"""--- +name: {skill_name} +description: {skill_memory.get("description", "")} +tags: {", ".join(skill_memory.get("tags", []))} +--- +""" + + # Add Procedure section only if present + procedure = skill_memory.get("procedure", "") + if procedure and procedure.strip(): + skill_md_content += f"\n## Procedure\n{procedure}\n" + + # Add Experience section only if there are items + experiences = skill_memory.get("experience", []) + if experiences: + skill_md_content += "\n## Experience\n" + for idx, exp in enumerate(experiences, 1): + skill_md_content += f"{idx}. {exp}\n" + + # Add User Preferences section only if there are items + preferences = skill_memory.get("preference", []) + if preferences: + skill_md_content += "\n## User Preferences\n" + for pref in preferences: + skill_md_content += f"- {pref}\n" + + # Add Examples section only if there are items + examples = skill_memory.get("example", []) + if examples: + skill_md_content += "\n## Examples\n" + for idx, example in enumerate(examples, 1): + skill_md_content += f"\n### Example {idx}\n{example}\n" + + # Add scripts reference if present + scripts = skill_memory.get("scripts") + if scripts and isinstance(scripts, dict): + skill_md_content += "\n## Scripts\n" + skill_md_content += "This skill includes the following executable scripts:\n\n" + for script_name in scripts: + skill_md_content += f"- `./scripts/{script_name}`\n" + + # Add others - handle both inline content and separate markdown files + others = skill_memory.get("others") + if others and isinstance(others, dict): + # Separate markdown files from inline content + md_files = {} + inline_content = {} + + for key, value in others.items(): + if key.endswith(".md"): + md_files[key] = value + else: + inline_content[key] = value + + # Add inline content to SKILL.md + if inline_content: + skill_md_content += "\n## Additional Information\n" + for key, value in inline_content.items(): + skill_md_content += f"\n### {key}\n{value}\n" + + # Add references to separate markdown files + if md_files: + if not inline_content: + skill_md_content += "\n## Additional Information\n" + skill_md_content += "\nSee also:\n" + for md_filename in md_files: + skill_md_content += f"- [{md_filename}](./{md_filename})\n" + + # Write SKILL.md file + skill_md_path = os.path.join(skill_dir, "SKILL.md") + with open(skill_md_path, "w", encoding="utf-8") as f: + f.write(skill_md_content) + + # Write separate markdown files from others + if others and isinstance(others, dict): + for key, value in others.items(): + if key.endswith(".md"): + md_file_path = os.path.join(skill_dir, key) + with open(md_file_path, "w", encoding="utf-8") as f: + f.write(value) + + # If there are scripts, create a scripts directory with individual script files + if scripts and isinstance(scripts, dict): + scripts_dir = os.path.join(skill_dir, "scripts") + os.makedirs(scripts_dir, exist_ok=True) + + # Write each script to its own file + for script_filename, script_content in scripts.items(): + # Ensure filename ends with .py + if not script_filename.endswith(".py"): + script_filename = f"{script_filename}.py" + + script_path = os.path.join(scripts_dir, script_filename) + with open(script_path, "w", encoding="utf-8") as f: + f.write(script_content) + + # Create zip file + zip_filename = f"{skill_name}_{timestamp}.zip" + zip_path = os.path.join(tmp_dir, zip_filename) + + with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zipf: + # Walk through the skill directory and add all files + for root, _dirs, files in os.walk(skill_dir): + for file in files: + file_path = os.path.join(root, file) + arcname = os.path.relpath(file_path, temp_skill_dir) + zipf.write(file_path, arcname) + + logger.info(f"Created skill zip file: {zip_path}") + return zip_path + + +def create_skill_memory_item( + skill_memory: dict[str, Any], info: dict[str, Any], zip_path: str +) -> TextualMemoryItem: + info_ = info.copy() + user_id = info_.pop("user_id", "") + session_id = info_.pop("session_id", "") + + # Use description as the memory content + memory_content = skill_memory.get("description", "") + + # Create metadata with all skill-specific fields directly + metadata = TreeNodeTextualMemoryMetadata( + user_id=user_id, + session_id=session_id, + memory_type="SkillMemory", + status="activated", + tags=skill_memory.get("tags", []), + key=skill_memory.get("name", ""), + sources=[], + usage=[], + background="", + created_at=datetime.now().isoformat(), + updated_at=datetime.now().isoformat(), + info=info_, + # Skill-specific fields + name=skill_memory.get("name", ""), + description=skill_memory.get("description", ""), + procedure=skill_memory.get("procedure", ""), + experience=skill_memory.get("experience", []), + preference=skill_memory.get("preference", []), + example=skill_memory.get("example", []), + scripts=skill_memory.get("scripts"), + others=skill_memory.get("others"), + url=skill_memory.get("url", ""), + ) + # If this is an update, use the old memory ID + item_id = ( + skill_memory.get("old_memory_id", "") + if skill_memory.get("update", False) + else str(uuid.uuid4()) + ) + if not item_id: + item_id = str(uuid.uuid4()) -def create_skill_memory_item(skill_memory: dict[str, Any]) -> TextualMemoryItem: - pass + return TextualMemoryItem(id=item_id, memory=memory_content, metadata=metadata) def process_skill_memory_fine( - self, fast_memory_items: list[TextualMemoryItem], info: dict[str, Any], **kwargs + fast_memory_items: list[TextualMemoryItem], + info: dict[str, Any], + searcher: Searcher | None = None, + llm: BaseLLM | None = None, + rewrite_query: bool = False, + **kwargs, ) -> list[TextualMemoryItem]: messages = _reconstruct_messages_from_memory_items(fast_memory_items) messages = _add_index_to_message(messages) - task_chunks = _split_task_chunk_by_llm(messages) + task_chunks = _split_task_chunk_by_llm(llm, messages) + + # recall + related_skill_memories = [] + for task, msg in task_chunks.items(): + related_skill_memories.extend( + _recall_related_skill_memories( + task_type=task, + messages=msg, + searcher=searcher, + llm=llm, + rewrite_query=rewrite_query, + ) + ) skill_memories = [] with ContextThreadPoolExecutor(max_workers=min(len(task_chunks), 5)) as executor: futures = { - executor.submit(_extract_skill_memory_by_llm, task_type, messages): task_type + executor.submit( + _extract_skill_memory_by_llm, messages, related_skill_memories, llm + ): task_type for task_type, messages in task_chunks.items() } for future in as_completed(futures): try: skill_memory = future.result() - skill_memories.append(skill_memory) + if skill_memory: # Only add non-None results + skill_memories.append(skill_memory) except Exception as e: logger.error(f"Error extracting skill memory: {e}") continue - # write skills to file - file_paths = [] + # write skills to file and get zip paths + skill_memory_with_paths = [] with ContextThreadPoolExecutor(max_workers=min(len(skill_memories), 5)) as executor: futures = { - executor.submit(_write_skills_to_file, skill_memory): skill_memory + executor.submit(_write_skills_to_file, skill_memory, info): skill_memory for skill_memory in skill_memories } for future in as_completed(futures): try: - file_path = future.result() - file_paths.append(file_path) + zip_path = future.result() + skill_memory = futures[future] + skill_memory_with_paths.append((skill_memory, zip_path)) except Exception as e: logger.error(f"Error writing skills to file: {e}") continue - for skill_memory in skill_memories: - if skill_memory.get("update", False): - _delete_skills_from_oss() + # Create a mapping from old_memory_id to old memory for easy lookup + old_memories_map = {mem.id: mem for mem in related_skill_memories} - urls = [] - for file_path in file_paths: - # upload skills to oss - _upload_skills_to_oss(file_path) + # upload skills to oss and get urls + user_id = info.get("user_id", "unknown") + urls_map = {} - # set urls to skill_memories - for skill_memory in skill_memories: - skill_memory["url"] = urls[skill_memory["id"]] + for skill_memory, zip_path in skill_memory_with_paths: + try: + # Delete old skill from OSS if this is an update + if skill_memory.get("update", False) and skill_memory.get("old_memory_id"): + old_memory_id = skill_memory["old_memory_id"] + old_memory = old_memories_map.get(old_memory_id) + + if old_memory: + # Get old OSS path from the old memory's metadata + old_oss_path = getattr(old_memory.metadata, "url", None) + + if old_oss_path: + try: + _delete_skills_from_oss(old_oss_path, OSS_CLIENT) + logger.info(f"Deleted old skill from OSS: {old_oss_path}") + except Exception as e: + logger.warning(f"Failed to delete old skill from OSS: {e}") + + # Upload new skill to OSS + # Use the same filename as the local zip file + zip_filename = os.path.basename(zip_path) + oss_path = f"{OSS_DIR}{user_id}/{zip_filename}" + + # _upload_skills_to_oss returns the URL + url = _upload_skills_to_oss(zip_path, oss_path, OSS_CLIENT) + urls_map[id(skill_memory)] = url + + logger.info(f"Uploaded skill to OSS: {url}") + except Exception as e: + logger.error(f"Error uploading skill to OSS: {e}") + urls_map[id(skill_memory)] = zip_path # Fallback to local path + # Create TextualMemoryItem objects skill_memory_items = [] - for skill_memory in skill_memories: - skill_memory_items.append(create_skill_memory_item(skill_memory)) + for skill_memory, zip_path in skill_memory_with_paths: + try: + url = urls_map.get(id(skill_memory), zip_path) + skill_memory["url"] = url + memory_item = create_skill_memory_item(skill_memory, info, zip_path) + skill_memory_items.append(memory_item) + except Exception as e: + logger.error(f"Error creating skill memory item: {e}") + continue - return skill_memories + return skill_memory_items diff --git a/src/memos/mem_reader/simple_struct.py b/src/memos/mem_reader/simple_struct.py index 3e33538e0..b6a2f6f9b 100644 --- a/src/memos/mem_reader/simple_struct.py +++ b/src/memos/mem_reader/simple_struct.py @@ -20,6 +20,7 @@ if TYPE_CHECKING: from memos.graph_dbs.base import BaseGraphDB + from memos.memories.textual.tree_text_memory.retrieve.searcher import Searcher from memos.mem_reader.read_multi_modal import coerce_scene_data, detect_lang from memos.mem_reader.utils import ( count_tokens_text, @@ -187,6 +188,9 @@ def __init__(self, config: SimpleStructMemReaderConfig): def set_graph_db(self, graph_db: "BaseGraphDB | None") -> None: self.graph_db = graph_db + def set_searcher(self, searcher: "Searcher | None") -> None: + self.searcher = searcher + def _make_memory_item( self, value: str, diff --git a/src/memos/memories/textual/item.py b/src/memos/memories/textual/item.py index a1c85033b..46770758d 100644 --- a/src/memos/memories/textual/item.py +++ b/src/memos/memories/textual/item.py @@ -112,6 +112,7 @@ class TreeNodeTextualMemoryMetadata(TextualMemoryMetadata): "OuterMemory", "ToolSchemaMemory", "ToolTrajectoryMemory", + "SkillMemory", ] = Field(default="WorkingMemory", description="Memory lifecycle type.") sources: list[SourceMessage] | None = Field( default=None, description="Multiple origins of the memory (e.g., URLs, notes)." diff --git a/src/memos/memories/textual/tree_text_memory/organize/manager.py b/src/memos/memories/textual/tree_text_memory/organize/manager.py index c96d5a12a..59675bdc2 100644 --- a/src/memos/memories/textual/tree_text_memory/organize/manager.py +++ b/src/memos/memories/textual/tree_text_memory/organize/manager.py @@ -159,7 +159,12 @@ def _add_memories_batch( for memory in memories: working_id = str(uuid.uuid4()) - if memory.metadata.memory_type not in ("ToolSchemaMemory", "ToolTrajectoryMemory"): + if memory.metadata.memory_type in ( + "WorkingMemory", + "LongTermMemory", + "UserMemory", + "OuterMemory", + ): working_metadata = memory.metadata.model_copy( update={"memory_type": "WorkingMemory"} ).model_dump(exclude_none=True) @@ -176,6 +181,7 @@ def _add_memories_batch( "UserMemory", "ToolSchemaMemory", "ToolTrajectoryMemory", + "SkillMemory", ): graph_node_id = str(uuid.uuid4()) metadata_dict = memory.metadata.model_dump(exclude_none=True) @@ -310,7 +316,12 @@ def _process_memory(self, memory: TextualMemoryItem, user_name: str | None = Non working_id = str(uuid.uuid4()) with ContextThreadPoolExecutor(max_workers=2, thread_name_prefix="mem") as ex: - if memory.metadata.memory_type not in ("ToolSchemaMemory", "ToolTrajectoryMemory"): + if memory.metadata.memory_type in ( + "WorkingMemory", + "LongTermMemory", + "UserMemory", + "OuterMemory", + ): f_working = ex.submit( self._add_memory_to_db, memory, "WorkingMemory", user_name, working_id ) @@ -321,6 +332,7 @@ def _process_memory(self, memory: TextualMemoryItem, user_name: str | None = Non "UserMemory", "ToolSchemaMemory", "ToolTrajectoryMemory", + "SkillMemory", ): f_graph = ex.submit( self._add_to_graph_memory, diff --git a/src/memos/memories/textual/tree_text_memory/retrieve/recall.py b/src/memos/memories/textual/tree_text_memory/retrieve/recall.py index 4541b118b..c9f2ec156 100644 --- a/src/memos/memories/textual/tree_text_memory/retrieve/recall.py +++ b/src/memos/memories/textual/tree_text_memory/retrieve/recall.py @@ -67,6 +67,7 @@ def retrieve( "UserMemory", "ToolSchemaMemory", "ToolTrajectoryMemory", + "SkillMemory", ]: raise ValueError(f"Unsupported memory scope: {memory_scope}") 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 0c58dd19b..dcd4e1fba 100644 --- a/src/memos/memories/textual/tree_text_memory/retrieve/searcher.py +++ b/src/memos/memories/textual/tree_text_memory/retrieve/searcher.py @@ -81,6 +81,8 @@ def retrieve( user_name: str | None = None, search_tool_memory: bool = False, tool_mem_top_k: int = 6, + include_skill_memory: bool = False, + skill_mem_top_k: int = 3, **kwargs, ) -> list[tuple[TextualMemoryItem, float]]: logger.info( @@ -108,6 +110,8 @@ def retrieve( user_name, search_tool_memory, tool_mem_top_k, + include_skill_memory, + skill_mem_top_k, ) return results @@ -202,6 +206,8 @@ def search( user_name=user_name, search_tool_memory=search_tool_memory, tool_mem_top_k=tool_mem_top_k, + include_skill_memory=include_skill_memory, + skill_mem_top_k=skill_mem_top_k, **kwargs, ) @@ -317,8 +323,10 @@ def _retrieve_paths( user_name: str | None = None, search_tool_memory: bool = False, tool_mem_top_k: int = 6, + include_skill_memory: bool = False, + skill_mem_top_k: int = 3, ): - """Run A/B/C retrieval paths in parallel""" + """Run A/B/C/D/E retrieval paths in parallel""" tasks = [] id_filter = { "user_id": info.get("user_id", None), @@ -326,7 +334,7 @@ def _retrieve_paths( } id_filter = {k: v for k, v in id_filter.items() if v is not None} - with ContextThreadPoolExecutor(max_workers=3) as executor: + with ContextThreadPoolExecutor(max_workers=5) as executor: tasks.append( executor.submit( self._retrieve_from_working_memory, @@ -385,6 +393,22 @@ def _retrieve_paths( mode=mode, ) ) + if include_skill_memory: + tasks.append( + executor.submit( + self._retrieve_from_skill_memory, + query, + parsed_goal, + query_embedding, + skill_mem_top_k, + memory_type, + search_filter, + search_priority, + user_name, + id_filter, + mode=mode, + ) + ) results = [] for t in tasks: results.extend(t.result()) @@ -662,8 +686,49 @@ def _retrieve_from_skill_memory( 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, + mode: str = "fast", ): """Retrieve and rerank from SkillMemory""" + if memory_type not in ["All", "SkillMemory"]: + logger.info(f"[PATH-E] '{query}' Skipped (memory_type does not match)") + return [] + + # chain of thinking + cot_embeddings = [] + if self.vec_cot: + queries = self._cot_query(query, mode=mode, context=parsed_goal.context) + if len(queries) > 1: + cot_embeddings = self.embedder.embed(queries) + cot_embeddings.extend(query_embedding) + else: + cot_embeddings = query_embedding + + items = self.graph_retriever.retrieve( + query=query, + parsed_goal=parsed_goal, + query_embedding=cot_embeddings, + top_k=top_k * 2, + memory_scope="SkillMemory", + search_filter=search_filter, + search_priority=search_priority, + user_name=user_name, + id_filter=id_filter, + use_fast_graph=self.use_fast_graph, + ) + + return self.reranker.rerank( + query=query, + query_embedding=query_embedding[0], + graph_results=items, + top_k=top_k, + parsed_goal=parsed_goal, + search_filter=search_filter, + ) @timed def _retrieve_simple( @@ -781,13 +846,33 @@ def _sort_and_trim( ) if include_skill_memory: - pass + skill_results = [ + (item, score) + for item, score in results + if item.metadata.memory_type == "SkillMemory" + ] + sorted_skill_results = sorted(skill_results, key=lambda pair: pair[1], reverse=True)[ + :skill_mem_top_k + ] + for item, score in sorted_skill_results: + if plugin and round(score, 2) == 0.00: + continue + meta_data = item.metadata.model_dump() + meta_data["relativity"] = score + final_items.append( + TextualMemoryItem( + id=item.id, + memory=item.memory, + metadata=SearchedTreeNodeTextualMemoryMetadata(**meta_data), + ) + ) # separate textual results results = [ (item, score) for item, score in results - if item.metadata.memory_type not in ["ToolSchemaMemory", "ToolTrajectoryMemory"] + if item.metadata.memory_type + in ["WorkingMemory", "LongTermMemory", "UserMemory", "OuterMemory"] ] sorted_results = sorted(results, key=lambda pair: pair[1], reverse=True)[:top_k] diff --git a/src/memos/multi_mem_cube/composite_cube.py b/src/memos/multi_mem_cube/composite_cube.py index c1017bfae..0d2d460e9 100644 --- a/src/memos/multi_mem_cube/composite_cube.py +++ b/src/memos/multi_mem_cube/composite_cube.py @@ -46,6 +46,7 @@ def search_memories(self, search_req: APISearchRequest) -> dict[str, Any]: "pref_mem": [], "pref_note": "", "tool_mem": [], + "skill_mem": [], } def _search_single_cube(view: SingleCubeView) -> dict[str, Any]: @@ -65,7 +66,7 @@ def _search_single_cube(view: SingleCubeView) -> dict[str, Any]: merged_results["para_mem"].extend(cube_result.get("para_mem", [])) merged_results["pref_mem"].extend(cube_result.get("pref_mem", [])) merged_results["tool_mem"].extend(cube_result.get("tool_mem", [])) - + merged_results["skill_mem"].extend(cube_result.get("skill_mem", [])) note = cube_result.get("pref_note") if note: if merged_results["pref_note"]: diff --git a/src/memos/multi_mem_cube/single_cube.py b/src/memos/multi_mem_cube/single_cube.py index b387a8ee5..c75fc23c6 100644 --- a/src/memos/multi_mem_cube/single_cube.py +++ b/src/memos/multi_mem_cube/single_cube.py @@ -121,6 +121,7 @@ def search_memories(self, search_req: APISearchRequest) -> dict[str, Any]: "pref_mem": [], "pref_note": "", "tool_mem": [], + "skill_mem": [], } # Determine search mode diff --git a/src/memos/templates/skill_mem_prompt.py b/src/memos/templates/skill_mem_prompt.py index eaa709bc7..b2a37f6c0 100644 --- a/src/memos/templates/skill_mem_prompt.py +++ b/src/memos/templates/skill_mem_prompt.py @@ -31,6 +31,69 @@ """ SKILL_MEMORY_EXTRACTION_PROMPT = """ +# Role +You are an expert in knowledge extraction and skill memory management. You excel at analyzing conversations to extract actionable skills, procedures, experiences, and user preferences. + +# Task +Based on the provided conversation messages and existing skill memories, extract new skill memory or update existing ones. You need to determine whether the current conversation contains skills similar to existing memories. + +# Existing Skill Memories +{old_memories} + +# Conversation Messages +{messages} + +# Extraction Rules +1. **Similarity Check**: Compare the current conversation with existing skill memories. If a similar skill exists, set "update": true and provide the "old_memory_id". Otherwise, set "update": false and leave "old_memory_id" empty. +2. **Completeness**: Extract comprehensive information including procedures, experiences, preferences, and examples. +3. **Clarity**: Ensure procedures are step-by-step and easy to follow. +4. **Specificity**: Capture specific user preferences and lessons learned from experiences. +5. **Language Consistency**: Use the same language as the conversation. +6. **Accuracy**: Only extract information that is explicitly present or strongly implied in the conversation. + +# Output Format +Please output in strict JSON format: + +```json +{ + "name": "A concise name for this skill or task type", + "description": "A clear description of what this skill does or accomplishes (this will be stored as the memory field)", + "procedure": "Step-by-step procedure: 1. First step 2. Second step 3. Third step...", + "experience": ["Lesson 1: Specific experience or insight learned", "Lesson 2: Another valuable experience..."], + "preference": ["User preference 1", "User preference 2", "User preference 3..."], + "example": ["Example scenario 1 showing how to apply this skill", "Example scenario 2..."], + "tags": ["tag1", "tag2", "tag3"], + "scripts": {"script_name.py": "# Python code here\nprint('Hello')", "another_script.py": "# More code\nimport os"}, + "others": {"Section Title": "Content here", "reference.md": "# Reference content for this skill"}, + "update": false, + "old_memory_id": "" +} +``` + +# Field Descriptions +- **name**: Brief identifier for the skill (e.g., "Travel Planning", "Code Review Process") +- **description**: What this skill accomplishes or its purpose +- **procedure**: Sequential steps to complete the task +- **experience**: Lessons learned, best practices, things to avoid +- **preference**: User's specific preferences, likes, dislikes +- **example**: Concrete examples of applying this skill +- **tags**: Relevant keywords for categorization +- **scripts**: Dictionary of scripts where key is the .py filename and value is the executable code snippet. Use null if not applicable +- **others**: Flexible additional information in key-value format. Can be either: + - Simple key-value pairs where key is a title and value is content (displayed inline in SKILL.md) + - Separate markdown files where key is .md filename and value is the markdown content (creates separate file and links to it) + Use null if not applicable +- **update**: true if updating existing memory, false if creating new +- **old_memory_id**: The ID of the existing memory being updated, or empty string if new + +# Important Notes +- If no clear skill can be extracted from the conversation, return null +- Ensure all string values are properly formatted and contain meaningful information +- Arrays should contain at least one item if the field is populated +- Be thorough but avoid redundancy + +# Output +Please output only the JSON object, without any additional formatting, markdown code blocks, or explanation. """ @@ -38,4 +101,27 @@ """ TASK_QUERY_REWRITE_PROMPT = """ +# Role +You are an expert in understanding user intentions and task requirements. You excel at analyzing conversations and extracting the core task description. + +# Task +Based on the provided task type and conversation messages, analyze and determine what specific task the user wants to complete, then rewrite it into a clear, concise task query string. + +# Task Type +{task_type} + +# Conversation Messages +{messages} + +# Requirements +1. Analyze the conversation content to understand the user's core intention +2. Consider the task type as context +3. Extract and summarize the key task objective +4. Output a clear, concise task description string (one sentence) +5. Use the same language as the conversation +6. Focus on WHAT needs to be done, not HOW to do it +7. Do not include any explanations, just output the rewritten task string directly + +# Output +Please output only the rewritten task query string, without any additional formatting or explanation. """ From 2903152c85c5e5fb67a10fb00bc273a399fd41a4 Mon Sep 17 00:00:00 2001 From: "yuan.wang" Date: Mon, 26 Jan 2026 14:05:46 +0800 Subject: [PATCH 06/35] feat: fill code --- src/memos/mem_reader/multi_modal_struct.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/memos/mem_reader/multi_modal_struct.py b/src/memos/mem_reader/multi_modal_struct.py index 9779e98fe..3eea10b3e 100644 --- a/src/memos/mem_reader/multi_modal_struct.py +++ b/src/memos/mem_reader/multi_modal_struct.py @@ -826,7 +826,6 @@ def _process_multi_modal_data( info=info, searcher=self.searcher, llm=self.llm, - rewrite_query=kwargs.get("rewrite_query", False), **kwargs, ) @@ -885,13 +884,22 @@ def _process_transfer_multi_modal_data( future_tool = executor.submit( self._process_tool_trajectory_fine, [raw_node], info, **kwargs ) + future_skill = executor.submit( + process_skill_memory_fine, + [raw_node], + info, + searcher=self.searcher, + llm=self.llm, + **kwargs, + ) # Collect results fine_memory_items_string_parser = future_string.result() fine_memory_items_tool_trajectory_parser = future_tool.result() - + fine_memory_items_skill_memory_parser = future_skill.result() fine_memory_items.extend(fine_memory_items_string_parser) fine_memory_items.extend(fine_memory_items_tool_trajectory_parser) + fine_memory_items.extend(fine_memory_items_skill_memory_parser) # Part B: get fine multimodal items for source in sources: From bd119d615f311efb26742122293d3adf599b300a Mon Sep 17 00:00:00 2001 From: "yuan.wang" Date: Mon, 26 Jan 2026 19:37:31 +0800 Subject: [PATCH 07/35] feat: modify code --- src/memos/api/handlers/component_init.py | 3 + src/memos/mem_reader/base.py | 7 + src/memos/mem_reader/multi_modal_struct.py | 4 + .../read_skill_memory/process_skill_memory.py | 367 ++++++++++-------- src/memos/templates/skill_mem_prompt.py | 146 ++++++- 5 files changed, 360 insertions(+), 167 deletions(-) diff --git a/src/memos/api/handlers/component_init.py b/src/memos/api/handlers/component_init.py index 76af6decf..417f0acf2 100644 --- a/src/memos/api/handlers/component_init.py +++ b/src/memos/api/handlers/component_init.py @@ -304,6 +304,9 @@ def init_server() -> dict[str, Any]: ) logger.debug("Searcher created") + # Set searcher to mem_reader + mem_reader.set_searcher(searcher) + # Initialize feedback server feedback_server = SimpleMemFeedback( llm=llm, diff --git a/src/memos/mem_reader/base.py b/src/memos/mem_reader/base.py index 87bf43b0f..b034c9367 100644 --- a/src/memos/mem_reader/base.py +++ b/src/memos/mem_reader/base.py @@ -7,6 +7,7 @@ if TYPE_CHECKING: from memos.graph_dbs.base import BaseGraphDB + from memos.memories.textual.tree_text_memory.retrieve.searcher import Searcher class BaseMemReader(ABC): @@ -33,6 +34,12 @@ def set_graph_db(self, graph_db: "BaseGraphDB | None") -> None: graph_db: The graph database instance, or None to disable recall operations. """ + @abstractmethod + def set_searcher(self, searcher: "Searcher | None") -> None: + """ + Set the searcher instance for recall operations. + """ + @abstractmethod def get_memory( self, scene_data: list, type: str, info: dict[str, Any], mode: str = "fast" diff --git a/src/memos/mem_reader/multi_modal_struct.py b/src/memos/mem_reader/multi_modal_struct.py index 3eea10b3e..6589335f8 100644 --- a/src/memos/mem_reader/multi_modal_struct.py +++ b/src/memos/mem_reader/multi_modal_struct.py @@ -825,7 +825,9 @@ def _process_multi_modal_data( fast_memory_items=fast_memory_items, info=info, searcher=self.searcher, + graph_db=self.graph_db, llm=self.llm, + embedder=self.embedder, **kwargs, ) @@ -890,6 +892,8 @@ def _process_transfer_multi_modal_data( info, searcher=self.searcher, llm=self.llm, + embedder=self.embedder, + graph_db=self.graph_db, **kwargs, ) 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 dfd4fb013..f75a92b0f 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 @@ -1,24 +1,30 @@ import json import os -import tempfile import uuid import zipfile from concurrent.futures import as_completed from datetime import datetime +from pathlib import Path from typing import Any import alibabacloud_oss_v2 as oss from memos.context.context import ContextThreadPoolExecutor +from memos.embedders.base import BaseEmbedder +from memos.graph_dbs.base import BaseGraphDB from memos.llms.base import BaseLLM from memos.log import get_logger +from memos.mem_reader.read_multi_modal import detect_lang from memos.memories.textual.item import TextualMemoryItem, TreeNodeTextualMemoryMetadata from memos.memories.textual.tree_text_memory.retrieve.searcher import Searcher from memos.templates.skill_mem_prompt import ( SKILL_MEMORY_EXTRACTION_PROMPT, + SKILL_MEMORY_EXTRACTION_PROMPT_ZH, TASK_CHUNKING_PROMPT, + TASK_CHUNKING_PROMPT_ZH, TASK_QUERY_REWRITE_PROMPT, + TASK_QUERY_REWRITE_PROMPT_ZH, ) from memos.types import MessageList @@ -26,7 +32,8 @@ logger = get_logger(__name__) -OSS_DIR = "memos/skill_memory/" +OSS_DIR = "skill_memory/" +LOCAL_DIR = "tmp/skill_memory/" def create_oss_client() -> oss.Client: @@ -47,15 +54,25 @@ def create_oss_client() -> oss.Client: def _reconstruct_messages_from_memory_items(memory_items: list[TextualMemoryItem]) -> MessageList: reconstructed_messages = [] + seen = set() # Track (role, content) tuples to detect duplicates + for memory_item in memory_items: for source_message in memory_item.metadata.sources: try: role = source_message.role content = source_message.content - reconstructed_messages.append({"role": role, "content": content}) + + # Create a tuple for deduplication + message_key = (role, content) + + # Only add if not seen before (keep first occurrence) + if message_key not in seen: + reconstructed_messages.append({"role": role, "content": content}) + seen.add(message_key) except Exception as e: logger.error(f"Error reconstructing message: {e}") continue + return reconstructed_messages @@ -73,19 +90,21 @@ def _split_task_chunk_by_llm(llm: BaseLLM, messages: MessageList) -> dict[str, M for i, message in enumerate(messages) ] ) - prompt = [ - {"role": "user", "content": TASK_CHUNKING_PROMPT.replace("{{messages}}", messages_context)} - ] + lang = detect_lang(messages_context) + template = TASK_CHUNKING_PROMPT_ZH if lang == "zh" else TASK_CHUNKING_PROMPT + prompt = [{"role": "user", "content": template.replace("{{messages}}", messages_context)}] for attempt in range(3): try: response_text = llm.generate(prompt) + response_json = json.loads(response_text.replace("```json", "").replace("```", "")) break except Exception as e: logger.warning(f"LLM generate failed (attempt {attempt + 1}): {e}") if attempt == 2: - logger.error("LLM generate failed after 3 retries, returning default value") - return {"default": [messages[i] for i in range(len(messages))]} - response_json = json.loads(response_text.replace("```json", "").replace("```", "")) + logger.warning("LLM generate failed after 3 retries, returning empty dict") + response_json = [] + break + task_chunks = {} for item in response_json: task_name = item["task_name"] @@ -124,9 +143,11 @@ def _extract_skill_memory_by_llm( old_memories_context = json.dumps(old_mem_references, ensure_ascii=False, indent=2) # Prepare prompt - prompt_content = SKILL_MEMORY_EXTRACTION_PROMPT.replace( - "{old_memories}", old_memories_context - ).replace("{messages}", messages_context) + lang = detect_lang(messages_context) + template = SKILL_MEMORY_EXTRACTION_PROMPT_ZH if lang == "zh" else SKILL_MEMORY_EXTRACTION_PROMPT + prompt_content = template.replace("{old_memories}", old_memories_context).replace( + "{messages}", messages_context + ) prompt = [{"role": "user", "content": prompt_content}] @@ -136,17 +157,14 @@ def _extract_skill_memory_by_llm( response_text = llm.generate(prompt) # Clean up response (remove markdown code blocks if present) response_text = response_text.strip() - if response_text.startswith("```json"): - response_text = response_text.replace("```json", "").replace("```", "").strip() - elif response_text.startswith("```"): - response_text = response_text.replace("```", "").strip() + response_text = response_text.replace("```json", "").replace("```", "").strip() # Parse JSON response skill_memory = json.loads(response_text) - # Validate response + # If LLM returns null (parsed as None), log and return None if skill_memory is None: - logger.info("No skill memory extracted from conversation") + logger.info("No skill memory extracted from conversation (LLM returned null)") return None return skill_memory @@ -155,12 +173,12 @@ def _extract_skill_memory_by_llm( logger.warning(f"JSON decode failed (attempt {attempt + 1}): {e}") logger.debug(f"Response text: {response_text}") if attempt == 2: - logger.error("Failed to parse skill memory after 3 retries") + logger.warning("Failed to parse skill memory after 3 retries") return None except Exception as e: logger.warning(f"LLM skill memory extraction failed (attempt {attempt + 1}): {e}") if attempt == 2: - logger.error("LLM skill memory extraction failed after 3 retries") + logger.warning("LLM skill memory extraction failed after 3 retries") return None return None @@ -172,9 +190,10 @@ def _recall_related_skill_memories( searcher: Searcher, llm: BaseLLM, rewrite_query: bool, + info: dict[str, Any], ) -> list[TextualMemoryItem]: query = _rewrite_query(task_type, messages, llm, rewrite_query) - related_skill_memories = searcher.search(query, top_k=10, memory_type="SkillMemory") + related_skill_memories = searcher.search(query, top_k=10, memory_type="SkillMemory", info=info) return related_skill_memories @@ -190,7 +209,9 @@ def _rewrite_query(task_type: str, messages: MessageList, llm: BaseLLM, rewrite_ ) # Prepare prompt with task type and messages - prompt_content = TASK_QUERY_REWRITE_PROMPT.replace("{task_type}", task_type).replace( + lang = detect_lang(messages_context) + template = TASK_QUERY_REWRITE_PROMPT_ZH if lang == "zh" else TASK_QUERY_REWRITE_PROMPT + prompt_content = template.replace("{task_type}", task_type).replace( "{messages}", messages_context ) prompt = [{"role": "user", "content": prompt_content}] @@ -216,7 +237,7 @@ def _rewrite_query(task_type: str, messages: MessageList, llm: BaseLLM, rewrite_ def _upload_skills_to_oss(local_file_path: str, oss_file_path: str, client: oss.Client) -> str: - client.put_object_from_file( + result = client.put_object_from_file( request=oss.PutObjectRequest( bucket=os.getenv("OSS_BUCKET_NAME"), key=oss_file_path, @@ -224,10 +245,15 @@ def _upload_skills_to_oss(local_file_path: str, oss_file_path: str, client: oss. filepath=local_file_path, ) + if result.status_code != 200: + logger.error("Failed to upload skill to OSS") + return "" + # Construct and return the URL bucket_name = os.getenv("OSS_BUCKET_NAME") - endpoint = os.getenv("OSS_ENDPOINT") - url = f"https://{bucket_name}.{endpoint}/{oss_file_path}" + endpoint = os.getenv("OSS_ENDPOINT").replace("https://", "").replace("http://", "") + file_name = Path(local_file_path).name + url = f"https://{bucket_name}.{endpoint}/{file_name}" return url @@ -244,132 +270,129 @@ def _delete_skills_from_oss(oss_file_path: str, client: oss.Client) -> oss.Delet def _write_skills_to_file(skill_memory: dict[str, Any], info: dict[str, Any]) -> str: user_id = info.get("user_id", "unknown") skill_name = skill_memory.get("name", "unnamed_skill").replace(" ", "_").lower() - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") # Create tmp directory for user if it doesn't exist - tmp_dir = os.path.join("/tmp", user_id) - os.makedirs(tmp_dir, exist_ok=True) + tmp_dir = Path(LOCAL_DIR) / user_id + tmp_dir.mkdir(parents=True, exist_ok=True) - # Create a temporary directory for the skill structure - with tempfile.TemporaryDirectory() as temp_skill_dir: - skill_dir = os.path.join(temp_skill_dir, skill_name) - os.makedirs(skill_dir, exist_ok=True) + # Create skill directory directly in tmp_dir + skill_dir = tmp_dir / skill_name + skill_dir.mkdir(parents=True, exist_ok=True) - # Generate SKILL.md content with frontmatter - skill_md_content = f"""--- + # Generate SKILL.md content with frontmatter + skill_md_content = f"""--- name: {skill_name} description: {skill_memory.get("description", "")} -tags: {", ".join(skill_memory.get("tags", []))} --- """ - # Add Procedure section only if present - procedure = skill_memory.get("procedure", "") - if procedure and procedure.strip(): - skill_md_content += f"\n## Procedure\n{procedure}\n" - - # Add Experience section only if there are items - experiences = skill_memory.get("experience", []) - if experiences: - skill_md_content += "\n## Experience\n" - for idx, exp in enumerate(experiences, 1): - skill_md_content += f"{idx}. {exp}\n" - - # Add User Preferences section only if there are items - preferences = skill_memory.get("preference", []) - if preferences: - skill_md_content += "\n## User Preferences\n" - for pref in preferences: - skill_md_content += f"- {pref}\n" - - # Add Examples section only if there are items - examples = skill_memory.get("example", []) - if examples: - skill_md_content += "\n## Examples\n" - for idx, example in enumerate(examples, 1): - skill_md_content += f"\n### Example {idx}\n{example}\n" - - # Add scripts reference if present - scripts = skill_memory.get("scripts") - if scripts and isinstance(scripts, dict): - skill_md_content += "\n## Scripts\n" - skill_md_content += "This skill includes the following executable scripts:\n\n" - for script_name in scripts: - skill_md_content += f"- `./scripts/{script_name}`\n" - - # Add others - handle both inline content and separate markdown files - others = skill_memory.get("others") - if others and isinstance(others, dict): - # Separate markdown files from inline content - md_files = {} - inline_content = {} - - for key, value in others.items(): - if key.endswith(".md"): - md_files[key] = value - else: - inline_content[key] = value - - # Add inline content to SKILL.md - if inline_content: + # Add Procedure section only if present + procedure = skill_memory.get("procedure", "") + if procedure and procedure.strip(): + skill_md_content += f"\n## Procedure\n{procedure}\n" + + # Add Experience section only if there are items + experiences = skill_memory.get("experience", []) + if experiences: + skill_md_content += "\n## Experience\n" + for idx, exp in enumerate(experiences, 1): + skill_md_content += f"{idx}. {exp}\n" + + # Add User Preferences section only if there are items + preferences = skill_memory.get("preference", []) + if preferences: + skill_md_content += "\n## User Preferences\n" + for pref in preferences: + skill_md_content += f"- {pref}\n" + + # Add Examples section only if there are items + examples = skill_memory.get("example", []) + if examples: + skill_md_content += "\n## Examples\n" + for idx, example in enumerate(examples, 1): + skill_md_content += f"\n### Example {idx}\n{example}\n" + + # Add scripts reference if present + scripts = skill_memory.get("scripts") + if scripts and isinstance(scripts, dict): + skill_md_content += "\n## Scripts\n" + skill_md_content += "This skill includes the following executable scripts:\n\n" + for script_name in scripts: + skill_md_content += f"- `./scripts/{script_name}`\n" + + # Add others - handle both inline content and separate markdown files + others = skill_memory.get("others") + if others and isinstance(others, dict): + # Separate markdown files from inline content + md_files = {} + inline_content = {} + + for key, value in others.items(): + if key.endswith(".md"): + md_files[key] = value + else: + inline_content[key] = value + + # Add inline content to SKILL.md + if inline_content: + skill_md_content += "\n## Additional Information\n" + for key, value in inline_content.items(): + skill_md_content += f"\n### {key}\n{value}\n" + + # Add references to separate markdown files + if md_files: + if not inline_content: skill_md_content += "\n## Additional Information\n" - for key, value in inline_content.items(): - skill_md_content += f"\n### {key}\n{value}\n" - - # Add references to separate markdown files - if md_files: - if not inline_content: - skill_md_content += "\n## Additional Information\n" - skill_md_content += "\nSee also:\n" - for md_filename in md_files: - skill_md_content += f"- [{md_filename}](./{md_filename})\n" - - # Write SKILL.md file - skill_md_path = os.path.join(skill_dir, "SKILL.md") - with open(skill_md_path, "w", encoding="utf-8") as f: - f.write(skill_md_content) - - # Write separate markdown files from others - if others and isinstance(others, dict): - for key, value in others.items(): - if key.endswith(".md"): - md_file_path = os.path.join(skill_dir, key) - with open(md_file_path, "w", encoding="utf-8") as f: - f.write(value) - - # If there are scripts, create a scripts directory with individual script files - if scripts and isinstance(scripts, dict): - scripts_dir = os.path.join(skill_dir, "scripts") - os.makedirs(scripts_dir, exist_ok=True) - - # Write each script to its own file - for script_filename, script_content in scripts.items(): - # Ensure filename ends with .py - if not script_filename.endswith(".py"): - script_filename = f"{script_filename}.py" - - script_path = os.path.join(scripts_dir, script_filename) - with open(script_path, "w", encoding="utf-8") as f: - f.write(script_content) - - # Create zip file - zip_filename = f"{skill_name}_{timestamp}.zip" - zip_path = os.path.join(tmp_dir, zip_filename) - - with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zipf: - # Walk through the skill directory and add all files - for root, _dirs, files in os.walk(skill_dir): - for file in files: - file_path = os.path.join(root, file) - arcname = os.path.relpath(file_path, temp_skill_dir) - zipf.write(file_path, arcname) - - logger.info(f"Created skill zip file: {zip_path}") - return zip_path + skill_md_content += "\nSee also:\n" + for md_filename in md_files: + skill_md_content += f"- [{md_filename}](./{md_filename})\n" + + # Write SKILL.md file + skill_md_path = skill_dir / "SKILL.md" + with open(skill_md_path, "w", encoding="utf-8") as f: + f.write(skill_md_content) + + # Write separate markdown files from others + if others and isinstance(others, dict): + for key, value in others.items(): + if key.endswith(".md"): + md_file_path = skill_dir / key + with open(md_file_path, "w", encoding="utf-8") as f: + f.write(value) + + # If there are scripts, create a scripts directory with individual script files + if scripts and isinstance(scripts, dict): + scripts_dir = skill_dir / "scripts" + scripts_dir.mkdir(parents=True, exist_ok=True) + + # Write each script to its own file + for script_filename, script_content in scripts.items(): + # Ensure filename ends with .py + if not script_filename.endswith(".py"): + script_filename = f"{script_filename}.py" + + script_path = scripts_dir / script_filename + with open(script_path, "w", encoding="utf-8") as f: + f.write(script_content) + + # Create zip file in tmp_dir + zip_filename = f"{skill_name}.zip" + zip_path = tmp_dir / zip_filename + + with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zipf: + # Walk through the skill directory and add all files + for file_path in skill_dir.rglob("*"): + if file_path.is_file(): + # Use relative path from skill_dir for archive + arcname = Path(skill_dir.name) / file_path.relative_to(skill_dir) + zipf.write(str(file_path), str(arcname)) + + logger.info(f"Created skill zip file: {zip_path}") + return str(zip_path) def create_skill_memory_item( - skill_memory: dict[str, Any], info: dict[str, Any], zip_path: str + skill_memory: dict[str, Any], info: dict[str, Any], embedder: BaseEmbedder | None = None ) -> TextualMemoryItem: info_ = info.copy() user_id = info_.pop("user_id", "") @@ -389,9 +412,12 @@ def create_skill_memory_item( sources=[], usage=[], background="", + confidence=0.99, created_at=datetime.now().isoformat(), updated_at=datetime.now().isoformat(), + type="skills", info=info_, + embedding=embedder.embed([memory_content])[0] if embedder else None, # Skill-specific fields name=skill_memory.get("name", ""), description=skill_memory.get("description", ""), @@ -420,7 +446,9 @@ def process_skill_memory_fine( fast_memory_items: list[TextualMemoryItem], info: dict[str, Any], searcher: Searcher | None = None, + graph_db: BaseGraphDB | None = None, llm: BaseLLM | None = None, + embedder: BaseEmbedder | None = None, rewrite_query: bool = False, **kwargs, ) -> list[TextualMemoryItem]: @@ -429,24 +457,38 @@ def process_skill_memory_fine( task_chunks = _split_task_chunk_by_llm(llm, messages) - # recall - related_skill_memories = [] - for task, msg in task_chunks.items(): - related_skill_memories.extend( - _recall_related_skill_memories( + # recall - get related skill memories for each task separately (parallel) + related_skill_memories_by_task = {} + with ContextThreadPoolExecutor(max_workers=min(len(task_chunks), 5)) as executor: + recall_futures = { + executor.submit( + _recall_related_skill_memories, task_type=task, messages=msg, searcher=searcher, llm=llm, rewrite_query=rewrite_query, - ) - ) + info=info, + ): task + for task, msg in task_chunks.items() + } + for future in as_completed(recall_futures): + task_name = recall_futures[future] + try: + related_memories = future.result() + related_skill_memories_by_task[task_name] = related_memories + except Exception as e: + logger.error(f"Error recalling skill memories for task '{task_name}': {e}") + related_skill_memories_by_task[task_name] = [] skill_memories = [] with ContextThreadPoolExecutor(max_workers=min(len(task_chunks), 5)) as executor: futures = { executor.submit( - _extract_skill_memory_by_llm, messages, related_skill_memories, llm + _extract_skill_memory_by_llm, + messages, + related_skill_memories_by_task.get(task_type, []), + llm, ): task_type for task_type, messages in task_chunks.items() } @@ -476,11 +518,14 @@ def process_skill_memory_fine( continue # Create a mapping from old_memory_id to old memory for easy lookup - old_memories_map = {mem.id: mem for mem in related_skill_memories} + # Collect all related memories from all tasks + all_related_memories = [] + for memories in related_skill_memories_by_task.values(): + all_related_memories.extend(memories) + old_memories_map = {mem.id: mem for mem in all_related_memories} - # upload skills to oss and get urls + # upload skills to oss and set urls directly to skill_memory user_id = info.get("user_id", "unknown") - urls_map = {} for skill_memory, zip_path in skill_memory_with_paths: try: @@ -495,32 +540,38 @@ def process_skill_memory_fine( if old_oss_path: try: + # delete old skill from OSS _delete_skills_from_oss(old_oss_path, OSS_CLIENT) logger.info(f"Deleted old skill from OSS: {old_oss_path}") except Exception as e: logger.warning(f"Failed to delete old skill from OSS: {e}") + # delete old skill from graph db + if graph_db: + graph_db.delete_node_by_prams(memory_ids=[old_memory_id]) + logger.info(f"Deleted old skill from graph db: {old_memory_id}") + # Upload new skill to OSS # Use the same filename as the local zip file - zip_filename = os.path.basename(zip_path) - oss_path = f"{OSS_DIR}{user_id}/{zip_filename}" + zip_filename = Path(zip_path).name + oss_path = (Path(OSS_DIR) / user_id / zip_filename).as_posix() # _upload_skills_to_oss returns the URL - url = _upload_skills_to_oss(zip_path, oss_path, OSS_CLIENT) - urls_map[id(skill_memory)] = url + url = _upload_skills_to_oss(str(zip_path), oss_path, OSS_CLIENT) + + # Set URL directly to skill_memory + skill_memory["url"] = url logger.info(f"Uploaded skill to OSS: {url}") except Exception as e: logger.error(f"Error uploading skill to OSS: {e}") - urls_map[id(skill_memory)] = zip_path # Fallback to local path + skill_memory["url"] = "" # Set to empty string if upload fails # Create TextualMemoryItem objects skill_memory_items = [] - for skill_memory, zip_path in skill_memory_with_paths: + for skill_memory in skill_memories: try: - url = urls_map.get(id(skill_memory), zip_path) - skill_memory["url"] = url - memory_item = create_skill_memory_item(skill_memory, info, zip_path) + memory_item = create_skill_memory_item(skill_memory, info, embedder) skill_memory_items.append(memory_item) except Exception as e: logger.error(f"Error creating skill memory item: {e}") diff --git a/src/memos/templates/skill_mem_prompt.py b/src/memos/templates/skill_mem_prompt.py index b2a37f6c0..abfc11ef2 100644 --- a/src/memos/templates/skill_mem_prompt.py +++ b/src/memos/templates/skill_mem_prompt.py @@ -5,9 +5,11 @@ # Task Please analyze the provided conversation records, identify all independent "tasks" that the user has asked the AI to perform, and assign the corresponding dialogue message numbers to each task. +**Note**: Tasks should be high-level and general, typically divided by theme or topic. For example: "Travel Planning", "PDF Operations", "Code Review", "Data Analysis", etc. Avoid being too specific or granular. + # Rules & Constraints 1. **Task Independence**: If multiple unrelated topics are discussed in the conversation, identify them as different tasks. -2. **Non-continuous Processing**: Pay attention to identifying "jumping" conversations. For example, if the user made travel plans in messages 8-11, switched to consulting about weather in messages 12-22, and then returned to making travel plans in messages 23-24, be sure to assign both 8-11 and 23-24 to the task "Making travel plans". +2. **Non-continuous Processing**: Pay attention to identifying "jumping" conversations. For example, if the user made travel plans in messages 8-11, switched to consulting about weather in messages 12-22, and then returned to making travel plans in messages 23-24, be sure to assign both 8-11 and 23-24 to the task "Making travel plans". However, if messages are continuous and belong to the same task, do not split them apart. 3. **Filter Chit-chat**: Only extract tasks with clear goals, instructions, or knowledge-based discussions. Ignore meaningless greetings (such as "Hello", "Are you there?") or closing remarks unless they are part of the task context. 4. **Output Format**: Please strictly follow the JSON format for output to facilitate my subsequent processing. 5. **Language Consistency**: The language used in the task_name field must match the language used in the conversation records. @@ -24,15 +26,47 @@ ] ``` +# Context (Conversation Records) +{{messages}} +""" + +TASK_CHUNKING_PROMPT_ZH = """ +# 角色 +你是自然语言处理(NLP)和对话逻辑分析的专家。你擅长从复杂的长对话中整理逻辑线索,准确提取用户的核心意图。 -# Context (Conversation Records) +# 任务 +请分析提供的对话记录,识别所有用户要求 AI 执行的独立"任务",并为每个任务分配相应的对话消息编号。 + +**注意**:任务应该是高层次和通用的,通常按主题或话题划分。例如:"旅行计划"、"PDF操作"、"代码审查"、"数据分析"等。避免过于具体或细化。 + +# 规则与约束 +1. **任务独立性**:如果对话中讨论了多个不相关的话题,请将它们识别为不同的任务。 +2. **非连续处理**:注意识别"跳跃式"对话。例如,如果用户在消息 8-11 中制定旅行计划,在消息 12-22 中切换到咨询天气,然后在消息 23-24 中返回到制定旅行计划,请务必将 8-11 和 23-24 都分配给"制定旅行计划"任务。但是,如果消息是连续的且属于同一任务,不能将其分开。 +3. **过滤闲聊**:仅提取具有明确目标、指令或基于知识的讨论的任务。忽略无意义的问候(例如"你好"、"在吗?")或结束语,除非它们是任务上下文的一部分。 +4. **输出格式**:请严格遵循 JSON 格式输出,以便我后续处理。 +5. **语言一致性**:task_name 字段使用的语言必须与对话记录中使用的语言相匹配。 + +```json +[ + { + "task_id": 1, + "task_name": "任务的简要描述(例如:制定旅行计划)", + "message_indices": [[0, 5],[16, 17]], # 0-5 和 16-17 是此任务的消息索引 + "reasoning": "简要解释为什么这些消息被分组在一起" + }, + ... +] +``` + +# 上下文(对话记录) {{messages}} """ + SKILL_MEMORY_EXTRACTION_PROMPT = """ # Role -You are an expert in knowledge extraction and skill memory management. You excel at analyzing conversations to extract actionable skills, procedures, experiences, and user preferences. +You are an expert in general skill extraction and skill memory management. You excel at analyzing conversations to extract actionable, transferable, and reusable skills, procedures, experiences, and user preferences. The skills you extract should be general and applicable across similar scenarios, not overly specific to a single instance. # Task Based on the provided conversation messages and existing skill memories, extract new skill memory or update existing ones. You need to determine whether the current conversation contains skills similar to existing memories. @@ -57,11 +91,11 @@ ```json { "name": "A concise name for this skill or task type", - "description": "A clear description of what this skill does or accomplishes (this will be stored as the memory field)", + "description": "A clear description of what this skill does or accomplishes", "procedure": "Step-by-step procedure: 1. First step 2. Second step 3. Third step...", "experience": ["Lesson 1: Specific experience or insight learned", "Lesson 2: Another valuable experience..."], "preference": ["User preference 1", "User preference 2", "User preference 3..."], - "example": ["Example scenario 1 showing how to apply this skill", "Example scenario 2..."], + "example": ["Example case 1 demonstrating how to complete the task following this skill's guidance", "Example case 2..."], "tags": ["tag1", "tag2", "tag3"], "scripts": {"script_name.py": "# Python code here\nprint('Hello')", "another_script.py": "# More code\nimport os"}, "others": {"Section Title": "Content here", "reference.md": "# Reference content for this skill"}, @@ -76,12 +110,12 @@ - **procedure**: Sequential steps to complete the task - **experience**: Lessons learned, best practices, things to avoid - **preference**: User's specific preferences, likes, dislikes -- **example**: Concrete examples of applying this skill +- **example**: Concrete example cases demonstrating how to complete the task by following this skill's guidance - **tags**: Relevant keywords for categorization - **scripts**: Dictionary of scripts where key is the .py filename and value is the executable code snippet. Use null if not applicable - **others**: Flexible additional information in key-value format. Can be either: - - Simple key-value pairs where key is a title and value is content (displayed inline in SKILL.md) - - Separate markdown files where key is .md filename and value is the markdown content (creates separate file and links to it) + - Simple key-value pairs where key is a title and value is content + - Separate markdown files where key is .md filename and value is the markdown content Use null if not applicable - **update**: true if updating existing memory, false if creating new - **old_memory_id**: The ID of the existing memory being updated, or empty string if new @@ -97,9 +131,73 @@ """ -SKILLS_AUTHORING_PROMPT = """ +SKILL_MEMORY_EXTRACTION_PROMPT_ZH = """ +# 角色 +你是通用技能提取和技能记忆管理的专家。你擅长分析对话,提取可操作的、可迁移的、可复用的技能、流程、经验和用户偏好。你提取的技能应该是通用的,能够应用于类似场景,而不是过于针对单一实例。 + +# 任务 +基于提供的对话消息和现有的技能记忆,提取新的技能记忆或更新现有的技能记忆。你需要判断当前对话中是否包含与现有记忆相似的技能。 + +# 现有技能记忆 +{old_memories} + +# 对话消息 +{messages} + +# 提取规则 +1. **相似性检查**:将当前对话与现有技能记忆进行比较。如果存在相似的技能,设置 "update": true 并提供 "old_memory_id"。否则,设置 "update": false 并将 "old_memory_id" 留空。 +2. **完整性**:提取全面的信息,包括流程、经验、偏好和示例。 +3. **清晰性**:确保流程是逐步的,易于遵循。 +4. **具体性**:捕获具体的用户偏好和从经验中学到的教训。 +5. **语言一致性**:使用与对话相同的语言。 +6. **准确性**:仅提取对话中明确存在或强烈暗示的信息。 + +# 输出格式 +请以严格的 JSON 格式输出: + +```json +{ + "name": "技能或任务类型的简洁名称", + "description": "对该技能的作用或目的的清晰描述", + "procedure": "逐步流程:1. 第一步 2. 第二步 3. 第三步...", + "experience": ["经验教训 1:学到的具体经验或见解", "经验教训 2:另一个有价值的经验..."], + "preference": ["用户偏好 1", "用户偏好 2", "用户偏好 3..."], + "example": ["示例案例 1:展示按照此技能的指引完成任务的过程", "示例案例 2..."], + "tags": ["标签1", "标签2", "标签3"], + "scripts": {"script_name.py": "# Python 代码\nprint('Hello')", "another_script.py": "# 更多代码\nimport os"}, + "others": {"章节标题": "这里的内容", "reference.md": "# 此技能的参考内容"}, + "update": false, + "old_memory_id": "" +} +``` + +# 字段说明 +- **name**:技能的简短标识符(例如:"旅行计划"、"代码审查流程") +- **description**:该技能完成什么或其目的 +- **procedure**:完成任务的顺序步骤 +- **experience**:学到的经验教训、最佳实践、要避免的事项 +- **preference**:用户的具体偏好、喜好、厌恶 +- **example**:具体的示例案例,展示如何按照此技能的指引完成任务 +- **tags**:用于分类的相关关键词 +- **scripts**:脚本字典,其中 key 是 .py 文件名,value 是可执行代码片段。如果不适用则使用 null +- **others**:灵活的附加信息,采用键值对格式。可以是: + - 简单的键值对,其中 key 是标题,value 是内容 + - 独立的 markdown 文件,其中 key 是 .md 文件名,value 是 markdown 内容 + 如果不适用则使用 null +- **update**:如果更新现有记忆则为 true,如果创建新记忆则为 false +- **old_memory_id**:正在更新的现有记忆的 ID,如果是新记忆则为空字符串 + +# 重要说明 +- 如果无法从对话中提取清晰的技能,返回 null +- 确保所有字符串值格式正确且包含有意义的信息 +- 如果填充数组,则数组应至少包含一项 +- 要全面但避免冗余 + +# 输出 +请仅输出 JSON 对象,不要添加任何额外的格式、markdown 代码块或解释。 """ + TASK_QUERY_REWRITE_PROMPT = """ # Role You are an expert in understanding user intentions and task requirements. You excel at analyzing conversations and extracting the core task description. @@ -125,3 +223,33 @@ # Output Please output only the rewritten task query string, without any additional formatting or explanation. """ + + +TASK_QUERY_REWRITE_PROMPT_ZH = """ +# 角色 +你是理解用户意图和任务需求的专家。你擅长分析对话并提取核心任务描述。 + +# 任务 +基于提供的任务类型和对话消息,分析并确定用户想要完成的具体任务,然后将其重写为清晰、简洁的任务查询字符串。 + +# 任务类型 +{task_type} + +# 对话消息 +{messages} + +# 要求 +1. 分析对话内容以理解用户的核心意图 +2. 将任务类型作为上下文考虑 +3. 提取并总结关键任务目标 +4. 输出清晰、简洁的任务描述字符串(一句话) +5. 使用与对话相同的语言 +6. 关注需要做什么(WHAT),而不是如何做(HOW) +7. 不要包含任何解释,直接输出重写后的任务字符串 + +# 输出 +请仅输出重写后的任务查询字符串,不要添加任何额外的格式或解释。 +""" + +SKILLS_AUTHORING_PROMPT = """ +""" From 4173f7b8a6848deee10890dbefd6ec46ee17faea Mon Sep 17 00:00:00 2001 From: "yuan.wang" Date: Mon, 26 Jan 2026 20:51:17 +0800 Subject: [PATCH 08/35] feat: modify code --- .../read_skill_memory/process_skill_memory.py | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) 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 f75a92b0f..55c753ce5 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 @@ -191,9 +191,17 @@ def _recall_related_skill_memories( llm: BaseLLM, rewrite_query: bool, info: dict[str, Any], + mem_cube_id: str, ) -> list[TextualMemoryItem]: query = _rewrite_query(task_type, messages, llm, rewrite_query) - related_skill_memories = searcher.search(query, top_k=10, memory_type="SkillMemory", info=info) + related_skill_memories = searcher.search( + query, + top_k=10, + memory_type="SkillMemory", + info=info, + include_skill_memory=True, + user_name=mem_cube_id, + ) return related_skill_memories @@ -252,8 +260,7 @@ def _upload_skills_to_oss(local_file_path: str, oss_file_path: str, client: oss. # Construct and return the URL bucket_name = os.getenv("OSS_BUCKET_NAME") endpoint = os.getenv("OSS_ENDPOINT").replace("https://", "").replace("http://", "") - file_name = Path(local_file_path).name - url = f"https://{bucket_name}.{endpoint}/{file_name}" + url = f"https://{bucket_name}.{endpoint}/{oss_file_path}" return url @@ -449,7 +456,7 @@ def process_skill_memory_fine( graph_db: BaseGraphDB | None = None, llm: BaseLLM | None = None, embedder: BaseEmbedder | None = None, - rewrite_query: bool = False, + rewrite_query: bool = True, **kwargs, ) -> list[TextualMemoryItem]: messages = _reconstruct_messages_from_memory_items(fast_memory_items) @@ -469,6 +476,7 @@ def process_skill_memory_fine( llm=llm, rewrite_query=rewrite_query, info=info, + mem_cube_id=kwargs.get("user_name", info.get("user_id", "")), ): task for task, msg in task_chunks.items() } @@ -541,6 +549,8 @@ def process_skill_memory_fine( if old_oss_path: try: # delete old skill from OSS + zip_filename = Path(old_oss_path).name + old_oss_path = (Path(OSS_DIR) / user_id / zip_filename).as_posix() _delete_skills_from_oss(old_oss_path, OSS_CLIENT) logger.info(f"Deleted old skill from OSS: {old_oss_path}") except Exception as e: @@ -557,7 +567,9 @@ def process_skill_memory_fine( oss_path = (Path(OSS_DIR) / user_id / zip_filename).as_posix() # _upload_skills_to_oss returns the URL - url = _upload_skills_to_oss(str(zip_path), oss_path, OSS_CLIENT) + url = _upload_skills_to_oss( + local_file_path=str(zip_path), oss_file_path=oss_path, client=OSS_CLIENT + ) # Set URL directly to skill_memory skill_memory["url"] = url From bccba713e42bc00db6ff0d414ee5c063e179fad3 Mon Sep 17 00:00:00 2001 From: "yuan.wang" Date: Tue, 27 Jan 2026 11:30:38 +0800 Subject: [PATCH 09/35] feat: async add skill memory --- src/memos/mem_reader/multi_modal_struct.py | 56 ++++++++-------------- 1 file changed, 21 insertions(+), 35 deletions(-) diff --git a/src/memos/mem_reader/multi_modal_struct.py b/src/memos/mem_reader/multi_modal_struct.py index 6589335f8..8d895a42d 100644 --- a/src/memos/mem_reader/multi_modal_struct.py +++ b/src/memos/mem_reader/multi_modal_struct.py @@ -857,7 +857,7 @@ def _process_multi_modal_data( @timed def _process_transfer_multi_modal_data( - self, raw_node: TextualMemoryItem, custom_tags: list[str] | None = None, **kwargs + self, raw_nodes: list[TextualMemoryItem], custom_tags: list[str] | None = None, **kwargs ) -> list[TextualMemoryItem]: """ Process transfer for multimodal data. @@ -865,30 +865,29 @@ def _process_transfer_multi_modal_data( Each source is processed independently by its corresponding parser, which knows how to rebuild the original message and parse it in fine mode. """ - sources = raw_node.metadata.sources or [] - if not sources: - logger.warning("[MultiModalStruct] No sources found in raw_node") + if not raw_nodes: + logger.warning("[MultiModalStruct] No raw nodes found.") return [] - # Extract info from raw_node (same as simple_struct.py) + # Extract info from raw_nodes (same as simple_struct.py) info = { - "user_id": raw_node.metadata.user_id, - "session_id": raw_node.metadata.session_id, - **(raw_node.metadata.info or {}), + "user_id": raw_nodes[0].metadata.user_id, + "session_id": raw_nodes[0].metadata.session_id, + **(raw_nodes[0].metadata.info or {}), } fine_memory_items = [] # Part A: call llm in parallel using thread pool with ContextThreadPoolExecutor(max_workers=2) as executor: future_string = executor.submit( - self._process_string_fine, [raw_node], info, custom_tags, **kwargs + self._process_string_fine, raw_nodes, info, custom_tags, **kwargs ) future_tool = executor.submit( - self._process_tool_trajectory_fine, [raw_node], info, **kwargs + self._process_tool_trajectory_fine, raw_nodes, info, **kwargs ) future_skill = executor.submit( process_skill_memory_fine, - [raw_node], + raw_nodes, info, searcher=self.searcher, llm=self.llm, @@ -906,12 +905,14 @@ def _process_transfer_multi_modal_data( fine_memory_items.extend(fine_memory_items_skill_memory_parser) # Part B: get fine multimodal items - for source in sources: - lang = getattr(source, "lang", "en") - items = self.multi_modal_parser.process_transfer( - source, context_items=[raw_node], info=info, custom_tags=custom_tags, lang=lang - ) - fine_memory_items.extend(items) + for raw_node in raw_nodes: + sources = raw_node.metadata.sources + for source in sources: + lang = getattr(source, "lang", "en") + items = self.multi_modal_parser.process_transfer( + source, context_items=[raw_node], info=info, custom_tags=custom_tags, lang=lang + ) + fine_memory_items.extend(items) return fine_memory_items def get_scene_data_info(self, scene_data: list, type: str) -> list[list[Any]]: @@ -968,22 +969,7 @@ def fine_transfer_simple_mem( if not input_memories: return [] - memory_list = [] - # Process Q&A pairs concurrently with context propagation - with ContextThreadPoolExecutor() as executor: - futures = [ - executor.submit( - self._process_transfer_multi_modal_data, scene_data_info, custom_tags, **kwargs - ) - for scene_data_info in input_memories - ] - for future in concurrent.futures.as_completed(futures): - try: - res_memory = future.result() - if res_memory is not None: - memory_list.append(res_memory) - except Exception as e: - logger.error(f"Task failed with exception: {e}") - logger.error(traceback.format_exc()) - return memory_list + memory_list = self._process_transfer_multi_modal_data(input_memories, custom_tags, **kwargs) + + return [memory_list] From 14f85e0886dd496b9158f01031d2a9b56c984fa5 Mon Sep 17 00:00:00 2001 From: "yuan.wang" Date: Tue, 27 Jan 2026 12:02:30 +0800 Subject: [PATCH 10/35] feat: update ollama version --- docker/requirements-full.txt | 3 ++- docker/requirements.txt | 2 +- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/docker/requirements-full.txt b/docker/requirements-full.txt index be9ed2068..a14257a76 100644 --- a/docker/requirements-full.txt +++ b/docker/requirements-full.txt @@ -89,7 +89,7 @@ nvidia-cusparselt-cu12==0.6.3 nvidia-nccl-cu12==2.26.2 nvidia-nvjitlink-cu12==12.6.85 nvidia-nvtx-cu12==12.6.77 -ollama==0.4.9 +ollama==0.5.0 onnxruntime==1.22.1 openai==1.97.0 openapi-pydantic==0.5.1 @@ -184,3 +184,4 @@ py-key-value-aio==0.2.8 py-key-value-shared==0.2.8 PyJWT==2.10.1 pytest==9.0.2 +alibabacloud-oss-v2==1.2.2 diff --git a/docker/requirements.txt b/docker/requirements.txt index e8d77acb2..340f4e140 100644 --- a/docker/requirements.txt +++ b/docker/requirements.txt @@ -54,7 +54,7 @@ mdurl==0.1.2 more-itertools==10.8.0 neo4j==5.28.1 numpy==2.3.4 -ollama==0.4.9 +ollama==0.5.0 openai==1.109.1 openapi-pydantic==0.5.1 orjson==3.11.4 diff --git a/poetry.lock b/poetry.lock index d2ecf26b2..ba31d1a31 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2929,14 +2929,14 @@ markers = {main = "platform_system == \"Linux\" and platform_machine == \"x86_64 [[package]] name = "ollama" -version = "0.4.9" +version = "0.5.0" description = "The official Python client for Ollama." optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "ollama-0.4.9-py3-none-any.whl", hash = "sha256:18c8c85358c54d7f73d6a66cda495b0e3ba99fdb88f824ae470d740fbb211a50"}, - {file = "ollama-0.4.9.tar.gz", hash = "sha256:5266d4d29b5089a01489872b8e8f980f018bccbdd1082b3903448af1d5615ce7"}, + {file = "ollama-0.5.0-py3-none-any.whl", hash = "sha256:625371de663ccb48f14faa49bd85ae409da5e40d84cab42366371234b4dbaf68"}, + {file = "ollama-0.5.0.tar.gz", hash = "sha256:ed6a343b64de22f69309ac930d8ac12b46775aebe21cbb91b859b99f59c53fa7"}, ] [package.dependencies] @@ -6373,4 +6373,4 @@ tree-mem = ["neo4j", "schedule"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "d4a267db0ac8b85f5bd995b34bfd7ebb8a678e478ddb3c3e45fb52cf58403b50" +content-hash = "faff240c05a74263a404e8d9324ffd2f342cb4f0a4c1f5455b87349f6ccc61a5" diff --git a/pyproject.toml b/pyproject.toml index 1a7a1ca73..ef2527e09 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ classifiers = [ ] dependencies = [ "openai (>=1.77.0,<2.0.0)", - "ollama (>=0.4.8,<0.5.0)", + "ollama (>=0.5.0,<0.5.1)", "transformers (>=4.51.3,<5.0.0)", "tenacity (>=9.1.2,<10.0.0)", # Error handling and retrying library "fastapi[all] (>=0.115.12,<0.116.0)", # Web framework for building APIs From b3c79acbb2772485d44e29831b9826ab7b835f19 Mon Sep 17 00:00:00 2001 From: "yuan.wang" Date: Tue, 27 Jan 2026 14:21:38 +0800 Subject: [PATCH 11/35] feat: get memory return skill memory --- src/memos/api/handlers/memory_handler.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/memos/api/handlers/memory_handler.py b/src/memos/api/handlers/memory_handler.py index d2aa2b204..c9ade8972 100644 --- a/src/memos/api/handlers/memory_handler.py +++ b/src/memos/api/handlers/memory_handler.py @@ -213,7 +213,7 @@ def handle_get_memory(memory_id: str, naive_mem_cube: NaiveMemCube) -> GetMemory def handle_get_memories( get_mem_req: GetMemoryRequest, naive_mem_cube: NaiveMemCube ) -> GetMemoryResponse: - results: dict[str, Any] = {"text_mem": [], "pref_mem": [], "tool_mem": []} + results: dict[str, Any] = {"text_mem": [], "pref_mem": [], "tool_mem": [], "skill_mem": []} memories = naive_mem_cube.text_mem.get_all( user_name=get_mem_req.mem_cube_id, user_id=get_mem_req.user_id, @@ -270,6 +270,7 @@ def handle_get_memories( "text_mem": results.get("text_mem", []), "pref_mem": results.get("pref_mem", []), "tool_mem": results.get("tool_mem", []), + "skill_mem": results.get("skill_mem", []), } return GetMemoryResponse(message="Memories retrieved successfully", data=filtered_results) From 76f197515553ae92b63ddb23ff03da997cfa8a8e Mon Sep 17 00:00:00 2001 From: "yuan.wang" Date: Tue, 27 Jan 2026 14:41:56 +0800 Subject: [PATCH 12/35] feat: get api add skill mem --- src/memos/api/handlers/memory_handler.py | 3 +++ src/memos/api/product_models.py | 1 + 2 files changed, 4 insertions(+) diff --git a/src/memos/api/handlers/memory_handler.py b/src/memos/api/handlers/memory_handler.py index c9ade8972..21332977a 100644 --- a/src/memos/api/handlers/memory_handler.py +++ b/src/memos/api/handlers/memory_handler.py @@ -227,6 +227,9 @@ def handle_get_memories( if not get_mem_req.include_tool_memory: results["tool_mem"] = [] + if not get_mem_req.include_skill_memory: + results["skill_mem"] = [] + preferences: list[TextualMemoryItem] = [] format_preferences = [] diff --git a/src/memos/api/product_models.py b/src/memos/api/product_models.py index cc37474ac..67928c520 100644 --- a/src/memos/api/product_models.py +++ b/src/memos/api/product_models.py @@ -785,6 +785,7 @@ class GetMemoryRequest(BaseRequest): user_id: str | None = Field(None, description="User ID") include_preference: bool = Field(True, description="Whether to return preference memory") include_tool_memory: bool = Field(False, description="Whether to return tool memory") + include_skill_memory: bool = Field(False, description="Whether to return skill memory") filter: dict[str, Any] | None = Field(None, description="Filter for the memory") page: int | None = Field( None, From 687cf9d1ae90916ccf75ceb311106ba5d6fd0b3e Mon Sep 17 00:00:00 2001 From: "yuan.wang" Date: Tue, 27 Jan 2026 14:57:39 +0800 Subject: [PATCH 13/35] feat: get api add skill mem --- src/memos/api/handlers/memory_handler.py | 1 - src/memos/api/product_models.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/memos/api/handlers/memory_handler.py b/src/memos/api/handlers/memory_handler.py index 21332977a..978f5acdd 100644 --- a/src/memos/api/handlers/memory_handler.py +++ b/src/memos/api/handlers/memory_handler.py @@ -226,7 +226,6 @@ def handle_get_memories( if not get_mem_req.include_tool_memory: results["tool_mem"] = [] - if not get_mem_req.include_skill_memory: results["skill_mem"] = [] diff --git a/src/memos/api/product_models.py b/src/memos/api/product_models.py index 67928c520..e6c4ae23d 100644 --- a/src/memos/api/product_models.py +++ b/src/memos/api/product_models.py @@ -784,8 +784,8 @@ class GetMemoryRequest(BaseRequest): mem_cube_id: str = Field(..., description="Cube ID") user_id: str | None = Field(None, description="User ID") include_preference: bool = Field(True, description="Whether to return preference memory") - include_tool_memory: bool = Field(False, description="Whether to return tool memory") - include_skill_memory: bool = Field(False, description="Whether to return skill memory") + include_tool_memory: bool = Field(True, description="Whether to return tool memory") + include_skill_memory: bool = Field(True, description="Whether to return skill memory") filter: dict[str, Any] | None = Field(None, description="Filter for the memory") page: int | None = Field( None, From 8555b1d2a243bfdecbc7642a2d3a95a4cd05ac54 Mon Sep 17 00:00:00 2001 From: "yuan.wang" Date: Tue, 27 Jan 2026 15:51:34 +0800 Subject: [PATCH 14/35] feat: modify env config --- .../read_skill_memory/process_skill_memory.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) 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 55c753ce5..ea578057f 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 @@ -32,8 +32,8 @@ logger = get_logger(__name__) -OSS_DIR = "skill_memory/" -LOCAL_DIR = "tmp/skill_memory/" +SKILLS_OSS_DIR = os.getenv("SKILLS_OSS_DIR") +SKILLS_LOCAL_DIR = os.getenv("SKILLS_LOCAL_DIR") def create_oss_client() -> oss.Client: @@ -279,7 +279,7 @@ def _write_skills_to_file(skill_memory: dict[str, Any], info: dict[str, Any]) -> skill_name = skill_memory.get("name", "unnamed_skill").replace(" ", "_").lower() # Create tmp directory for user if it doesn't exist - tmp_dir = Path(LOCAL_DIR) / user_id + tmp_dir = Path(SKILLS_LOCAL_DIR) / user_id tmp_dir.mkdir(parents=True, exist_ok=True) # Create skill directory directly in tmp_dir @@ -550,7 +550,9 @@ def process_skill_memory_fine( try: # delete old skill from OSS zip_filename = Path(old_oss_path).name - old_oss_path = (Path(OSS_DIR) / user_id / zip_filename).as_posix() + old_oss_path = ( + Path(SKILLS_OSS_DIR) / user_id / zip_filename + ).as_posix() _delete_skills_from_oss(old_oss_path, OSS_CLIENT) logger.info(f"Deleted old skill from OSS: {old_oss_path}") except Exception as e: @@ -564,7 +566,7 @@ def process_skill_memory_fine( # Upload new skill to OSS # Use the same filename as the local zip file zip_filename = Path(zip_path).name - oss_path = (Path(OSS_DIR) / user_id / zip_filename).as_posix() + oss_path = (Path(SKILLS_OSS_DIR) / user_id / zip_filename).as_posix() # _upload_skills_to_oss returns the URL url = _upload_skills_to_oss( From ae67378822cf76fc0271f397ad346d08ddc7ac1b Mon Sep 17 00:00:00 2001 From: "yuan.wang" Date: Tue, 27 Jan 2026 17:26:02 +0800 Subject: [PATCH 15/35] feat: back set oss client --- .../mem_reader/read_skill_memory/process_skill_memory.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) 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 ea578057f..86920ec92 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 @@ -49,9 +49,6 @@ def create_oss_client() -> oss.Client: return client -OSS_CLIENT = create_oss_client() - - def _reconstruct_messages_from_memory_items(memory_items: list[TextualMemoryItem]) -> MessageList: reconstructed_messages = [] seen = set() # Track (role, content) tuples to detect duplicates @@ -459,6 +456,7 @@ def process_skill_memory_fine( rewrite_query: bool = True, **kwargs, ) -> list[TextualMemoryItem]: + oss_client = create_oss_client() messages = _reconstruct_messages_from_memory_items(fast_memory_items) messages = _add_index_to_message(messages) @@ -553,7 +551,7 @@ def process_skill_memory_fine( old_oss_path = ( Path(SKILLS_OSS_DIR) / user_id / zip_filename ).as_posix() - _delete_skills_from_oss(old_oss_path, OSS_CLIENT) + _delete_skills_from_oss(old_oss_path, oss_client) logger.info(f"Deleted old skill from OSS: {old_oss_path}") except Exception as e: logger.warning(f"Failed to delete old skill from OSS: {e}") @@ -570,7 +568,7 @@ def process_skill_memory_fine( # _upload_skills_to_oss returns the URL url = _upload_skills_to_oss( - local_file_path=str(zip_path), oss_file_path=oss_path, client=OSS_CLIENT + local_file_path=str(zip_path), oss_file_path=oss_path, client=oss_client ) # Set URL directly to skill_memory From 793b5081d0e4830dee990abf40828d7a9aa8652e Mon Sep 17 00:00:00 2001 From: "yuan.wang" Date: Tue, 27 Jan 2026 17:39:31 +0800 Subject: [PATCH 16/35] feat: delete tmp skill code --- .../read_skill_memory/process_skill_memory.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) 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 86920ec92..79dec020c 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 @@ -1,5 +1,6 @@ import json import os +import shutil import uuid import zipfile @@ -578,6 +579,20 @@ def process_skill_memory_fine( except Exception as e: logger.error(f"Error uploading skill to OSS: {e}") skill_memory["url"] = "" # Set to empty string if upload fails + finally: + # Clean up local files after upload + try: + zip_file = Path(zip_path) + skill_dir = zip_file.parent / zip_file.stem + # Delete zip file + if zip_file.exists(): + zip_file.unlink() + # Delete skill directory + if skill_dir.exists(): + shutil.rmtree(skill_dir) + logger.info(f"Cleaned up local files: {zip_path} and {skill_dir}") + except Exception as cleanup_error: + logger.warning(f"Error cleaning up local files: {cleanup_error}") # Create TextualMemoryItem objects skill_memory_items = [] From e3ef4ccc2ad1a504bc483cb428e194ba7a536b52 Mon Sep 17 00:00:00 2001 From: "yuan.wang" Date: Tue, 27 Jan 2026 19:09:24 +0800 Subject: [PATCH 17/35] feat: process new package import error --- .../read_skill_memory/process_skill_memory.py | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) 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 79dec020c..82c997cc1 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 @@ -9,9 +9,8 @@ from pathlib import Path from typing import Any -import alibabacloud_oss_v2 as oss - from memos.context.context import ContextThreadPoolExecutor +from memos.dependency import require_python_package from memos.embedders.base import BaseEmbedder from memos.graph_dbs.base import BaseGraphDB from memos.llms.base import BaseLLM @@ -37,7 +36,13 @@ SKILLS_LOCAL_DIR = os.getenv("SKILLS_LOCAL_DIR") -def create_oss_client() -> oss.Client: +@require_python_package( + import_name="alibabacloud_oss_v2", + install_command="pip install alibabacloud-oss-v2", +) +def create_oss_client() -> Any: + import alibabacloud_oss_v2 as oss + credentials_provider = oss.credentials.EnvironmentVariableCredentialsProvider() # load SDK's default configuration, and set credential provider @@ -242,7 +247,13 @@ def _rewrite_query(task_type: str, messages: MessageList, llm: BaseLLM, rewrite_ return messages[0]["content"] if messages else "" -def _upload_skills_to_oss(local_file_path: str, oss_file_path: str, client: oss.Client) -> str: +@require_python_package( + import_name="alibabacloud_oss_v2", + install_command="pip install alibabacloud-oss-v2", +) +def _upload_skills_to_oss(local_file_path: str, oss_file_path: str, client: Any) -> str: + import alibabacloud_oss_v2 as oss + result = client.put_object_from_file( request=oss.PutObjectRequest( bucket=os.getenv("OSS_BUCKET_NAME"), @@ -262,7 +273,13 @@ def _upload_skills_to_oss(local_file_path: str, oss_file_path: str, client: oss. return url -def _delete_skills_from_oss(oss_file_path: str, client: oss.Client) -> oss.DeleteObjectResult: +@require_python_package( + import_name="alibabacloud_oss_v2", + install_command="pip install alibabacloud-oss-v2", +) +def _delete_skills_from_oss(oss_file_path: str, client: Any) -> Any: + import alibabacloud_oss_v2 as oss + result = client.delete_object( oss.DeleteObjectRequest( bucket=os.getenv("OSS_BUCKET_NAME"), From 6ba55d3454a19a695ea99750465df0a8f6f03bae Mon Sep 17 00:00:00 2001 From: "yuan.wang" Date: Tue, 27 Jan 2026 21:47:15 +0800 Subject: [PATCH 18/35] feat: modify oss config --- src/memos/api/config.py | 34 ++++++++++ src/memos/configs/mem_reader.py | 9 +++ src/memos/mem_reader/multi_modal_struct.py | 10 +++ .../read_skill_memory/process_skill_memory.py | 68 +++++++++++++------ 4 files changed, 100 insertions(+), 21 deletions(-) diff --git a/src/memos/api/config.py b/src/memos/api/config.py index a3bf25be0..fb6e5e35e 100644 --- a/src/memos/api/config.py +++ b/src/memos/api/config.py @@ -467,6 +467,35 @@ def get_reader_config() -> dict[str, Any]: } @staticmethod + def get_oss_config() -> dict[str, Any] | None: + """Get OSS configuration and validate connection.""" + + config = { + "endpoint": os.getenv("OSS_ENDPOINT", "http://oss-cn-shanghai.aliyuncs.com"), + "access_key_id": os.getenv("OSS_ACCESS_KEY_ID", ""), + "access_key_secret": os.getenv("OSS_ACCESS_KEY_SECRET", ""), + "region": os.getenv("OSS_REGION", ""), + "bucket_name": os.getenv("OSS_BUCKET_NAME", ""), + } + + # Validate that all required fields have values + required_fields = [ + "endpoint", + "access_key_id", + "access_key_secret", + "region", + "bucket_name", + ] + missing_fields = [field for field in required_fields if not config.get(field)] + + if missing_fields: + logger.warning( + f"OSS configuration incomplete. Missing fields: {', '.join(missing_fields)}" + ) + return None + + return config + def get_internet_config() -> dict[str, Any]: """Get embedder configuration.""" reader_config = APIConfig.get_reader_config() @@ -746,6 +775,11 @@ def get_product_default_config() -> dict[str, Any]: ).split(",") if h.strip() ], + "oss_config": APIConfig.get_oss_config(), + "skills_dir_config": { + "skills_oss_dir": os.getenv("SKILLS_OSS_DIR", "skill_memory/"), + "skills_local_dir": os.getenv("SKILLS_LOCAL_DIR", "/tmp/skill_memory/"), + }, }, }, "enable_textual_memory": True, diff --git a/src/memos/configs/mem_reader.py b/src/memos/configs/mem_reader.py index eaaa71461..4bd7953c0 100644 --- a/src/memos/configs/mem_reader.py +++ b/src/memos/configs/mem_reader.py @@ -57,6 +57,15 @@ class MultiModalStructMemReaderConfig(BaseMemReaderConfig): "If None, reads from FILE_PARSER_DIRECT_MARKDOWN_HOSTNAMES environment variable.", ) + oss_config: dict[str, Any] | None = Field( + default=None, + description="OSS configuration for the MemReader", + ) + skills_dir_config: dict[str, Any] | None = Field( + default=None, + description="Skills directory for the MemReader", + ) + class StrategyStructMemReaderConfig(BaseMemReaderConfig): """StrategyStruct MemReader configuration class.""" diff --git a/src/memos/mem_reader/multi_modal_struct.py b/src/memos/mem_reader/multi_modal_struct.py index 8d895a42d..352f25561 100644 --- a/src/memos/mem_reader/multi_modal_struct.py +++ b/src/memos/mem_reader/multi_modal_struct.py @@ -39,6 +39,12 @@ def __init__(self, config: MultiModalStructMemReaderConfig): # Extract direct_markdown_hostnames before converting to SimpleStructMemReaderConfig direct_markdown_hostnames = getattr(config, "direct_markdown_hostnames", None) + # oss + self.oss_config = getattr(config, "oss_config", None) + + # skills_dir + self.skills_dir_config = getattr(config, "skills_dir_config", None) + # Create config_dict excluding direct_markdown_hostnames for SimpleStructMemReaderConfig config_dict = config.model_dump(exclude_none=True) config_dict.pop("direct_markdown_hostnames", None) @@ -828,6 +834,8 @@ def _process_multi_modal_data( graph_db=self.graph_db, llm=self.llm, embedder=self.embedder, + oss_config=self.oss_config, + skills_dir_config=self.skills_dir_config, **kwargs, ) @@ -893,6 +901,8 @@ def _process_transfer_multi_modal_data( llm=self.llm, embedder=self.embedder, graph_db=self.graph_db, + oss_config=self.oss_config, + skills_dir_config=self.skills_dir_config, **kwargs, ) 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 82c997cc1..f341abc1c 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 @@ -32,15 +32,11 @@ logger = get_logger(__name__) -SKILLS_OSS_DIR = os.getenv("SKILLS_OSS_DIR") -SKILLS_LOCAL_DIR = os.getenv("SKILLS_LOCAL_DIR") - - @require_python_package( import_name="alibabacloud_oss_v2", install_command="pip install alibabacloud-oss-v2", ) -def create_oss_client() -> Any: +def create_oss_client(oss_config: dict[str, Any] | None = None) -> Any: import alibabacloud_oss_v2 as oss credentials_provider = oss.credentials.EnvironmentVariableCredentialsProvider() @@ -48,8 +44,8 @@ def create_oss_client() -> Any: # load SDK's default configuration, and set credential provider cfg = oss.config.load_default() cfg.credentials_provider = credentials_provider - cfg.region = os.getenv("OSS_REGION") - cfg.endpoint = os.getenv("OSS_ENDPOINT") + cfg.region = oss_config.get("region", os.getenv("OSS_REGION")) + cfg.endpoint = oss_config.get("endpoint", os.getenv("OSS_ENDPOINT")) client = oss.Client(cfg) return client @@ -73,7 +69,7 @@ def _reconstruct_messages_from_memory_items(memory_items: list[TextualMemoryItem reconstructed_messages.append({"role": role, "content": content}) seen.add(message_key) except Exception as e: - logger.error(f"Error reconstructing message: {e}") + logger.warning(f"Error reconstructing message: {e}") continue return reconstructed_messages @@ -238,7 +234,7 @@ def _rewrite_query(task_type: str, messages: MessageList, llm: BaseLLM, rewrite_ except Exception as e: logger.warning(f"LLM query rewrite failed (attempt {attempt + 1}): {e}") if attempt == 2: - logger.error( + logger.warning( "LLM query rewrite failed after 3 retries, returning first message content" ) return messages[0]["content"] if messages else "" @@ -263,7 +259,7 @@ def _upload_skills_to_oss(local_file_path: str, oss_file_path: str, client: Any) ) if result.status_code != 200: - logger.error("Failed to upload skill to OSS") + logger.warning("Failed to upload skill to OSS") return "" # Construct and return the URL @@ -289,12 +285,14 @@ def _delete_skills_from_oss(oss_file_path: str, client: Any) -> Any: return result -def _write_skills_to_file(skill_memory: dict[str, Any], info: dict[str, Any]) -> str: +def _write_skills_to_file( + skill_memory: dict[str, Any], info: dict[str, Any], skills_dir_config: dict[str, Any] +) -> str: user_id = info.get("user_id", "unknown") skill_name = skill_memory.get("name", "unnamed_skill").replace(" ", "_").lower() # Create tmp directory for user if it doesn't exist - tmp_dir = Path(SKILLS_LOCAL_DIR) / user_id + tmp_dir = Path(skills_dir_config["skills_local_dir"]) / user_id tmp_dir.mkdir(parents=True, exist_ok=True) # Create skill directory directly in tmp_dir @@ -472,9 +470,33 @@ def process_skill_memory_fine( llm: BaseLLM | None = None, embedder: BaseEmbedder | None = None, rewrite_query: bool = True, + oss_config: dict[str, Any] | None = None, + skills_dir_config: dict[str, Any] | None = None, **kwargs, ) -> list[TextualMemoryItem]: - oss_client = create_oss_client() + # Validate required configurations + if not oss_config: + logger.warning("OSS configuration is required for skill memory processing") + return [] + + if not skills_dir_config: + logger.warning("Skills directory configuration is required for skill memory processing") + return [] + + # Validate skills_dir has required keys + required_keys = ["skills_local_dir", "skills_oss_dir"] + missing_keys = [key for key in required_keys if key not in skills_dir_config] + if missing_keys: + logger.warning( + f"Skills directory configuration missing required keys: {', '.join(missing_keys)}" + ) + return [] + + oss_client = create_oss_client(oss_config) + if not oss_client: + logger.warning("Failed to create OSS client") + return [] + messages = _reconstruct_messages_from_memory_items(fast_memory_items) messages = _add_index_to_message(messages) @@ -502,7 +524,7 @@ def process_skill_memory_fine( related_memories = future.result() related_skill_memories_by_task[task_name] = related_memories except Exception as e: - logger.error(f"Error recalling skill memories for task '{task_name}': {e}") + logger.warning(f"Error recalling skill memories for task '{task_name}': {e}") related_skill_memories_by_task[task_name] = [] skill_memories = [] @@ -522,14 +544,16 @@ def process_skill_memory_fine( if skill_memory: # Only add non-None results skill_memories.append(skill_memory) except Exception as e: - logger.error(f"Error extracting skill memory: {e}") + logger.warning(f"Error extracting skill memory: {e}") continue # write skills to file and get zip paths skill_memory_with_paths = [] with ContextThreadPoolExecutor(max_workers=min(len(skill_memories), 5)) as executor: futures = { - executor.submit(_write_skills_to_file, skill_memory, info): skill_memory + executor.submit( + _write_skills_to_file, skill_memory, info, skills_dir_config + ): skill_memory for skill_memory in skill_memories } for future in as_completed(futures): @@ -538,7 +562,7 @@ def process_skill_memory_fine( skill_memory = futures[future] skill_memory_with_paths.append((skill_memory, zip_path)) except Exception as e: - logger.error(f"Error writing skills to file: {e}") + logger.warning(f"Error writing skills to file: {e}") continue # Create a mapping from old_memory_id to old memory for easy lookup @@ -567,7 +591,7 @@ def process_skill_memory_fine( # delete old skill from OSS zip_filename = Path(old_oss_path).name old_oss_path = ( - Path(SKILLS_OSS_DIR) / user_id / zip_filename + Path(skills_dir_config["skills_oss_dir"]) / user_id / zip_filename ).as_posix() _delete_skills_from_oss(old_oss_path, oss_client) logger.info(f"Deleted old skill from OSS: {old_oss_path}") @@ -582,7 +606,9 @@ def process_skill_memory_fine( # Upload new skill to OSS # Use the same filename as the local zip file zip_filename = Path(zip_path).name - oss_path = (Path(SKILLS_OSS_DIR) / user_id / zip_filename).as_posix() + oss_path = ( + Path(skills_dir_config["skills_oss_dir"]) / user_id / zip_filename + ).as_posix() # _upload_skills_to_oss returns the URL url = _upload_skills_to_oss( @@ -594,7 +620,7 @@ def process_skill_memory_fine( logger.info(f"Uploaded skill to OSS: {url}") except Exception as e: - logger.error(f"Error uploading skill to OSS: {e}") + logger.warning(f"Error uploading skill to OSS: {e}") skill_memory["url"] = "" # Set to empty string if upload fails finally: # Clean up local files after upload @@ -618,7 +644,7 @@ def process_skill_memory_fine( memory_item = create_skill_memory_item(skill_memory, info, embedder) skill_memory_items.append(memory_item) except Exception as e: - logger.error(f"Error creating skill memory item: {e}") + logger.warning(f"Error creating skill memory item: {e}") continue return skill_memory_items From 85e42d9b987476fbec657e7c834f3388c5c633c9 Mon Sep 17 00:00:00 2001 From: "yuan.wang" Date: Wed, 28 Jan 2026 19:11:47 +0800 Subject: [PATCH 19/35] feat: modiy prompt and add two api --- src/memos/api/handlers/memory_handler.py | 38 +++++ src/memos/api/routers/server_router.py | 8 + .../read_skill_memory/process_skill_memory.py | 54 ++++++- src/memos/memories/textual/base.py | 4 +- src/memos/memories/textual/tree.py | 3 +- src/memos/templates/skill_mem_prompt.py | 150 +++++++++--------- 6 files changed, 170 insertions(+), 87 deletions(-) diff --git a/src/memos/api/handlers/memory_handler.py b/src/memos/api/handlers/memory_handler.py index 978f5acdd..dfde51961 100644 --- a/src/memos/api/handlers/memory_handler.py +++ b/src/memos/api/handlers/memory_handler.py @@ -210,6 +210,44 @@ def handle_get_memory(memory_id: str, naive_mem_cube: NaiveMemCube) -> GetMemory ) +def handle_get_memory_by_ids( + memory_ids: list[str], naive_mem_cube: NaiveMemCube +) -> GetMemoryResponse: + """ + Handler for getting multiple memories by their IDs. + + Retrieves multiple memories and formats them as a list of dictionaries. + """ + try: + memories = naive_mem_cube.text_mem.get_by_ids(memory_ids=memory_ids) + except Exception: + memories = [] + + # Ensure memories is not None + if memories is None: + memories = [] + + if naive_mem_cube.pref_mem is not None: + collection_names = ["explicit_preference", "implicit_preference"] + for collection_name in collection_names: + try: + result = naive_mem_cube.pref_mem.get_by_ids_with_collection_name( + collection_name, memory_ids + ) + if result is not None: + memories.extend(result) + except Exception: + continue + + memories = [ + format_memory_item(item, save_sources=False) for item in memories if item is not None + ] + + return GetMemoryResponse( + message="Memories retrieved successfully", code=200, data={"memories": memories} + ) + + def handle_get_memories( get_mem_req: GetMemoryRequest, naive_mem_cube: NaiveMemCube ) -> GetMemoryResponse: diff --git a/src/memos/api/routers/server_router.py b/src/memos/api/routers/server_router.py index 86b75d73e..d28ca4a08 100644 --- a/src/memos/api/routers/server_router.py +++ b/src/memos/api/routers/server_router.py @@ -320,6 +320,14 @@ def get_memory_by_id(memory_id: str): ) +@router.get("/get_memory_by_ids", summary="Get memory by ids", response_model=GetMemoryResponse) +def get_memory_by_ids(memory_ids: list[str]): + return handlers.memory_handler.handle_get_memory_by_ids( + memory_ids=memory_ids, + naive_mem_cube=naive_mem_cube, + ) + + @router.post( "/delete_memory", summary="Delete memories for user", response_model=DeleteMemoryResponse ) 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 f341abc1c..9fc26cd8b 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 @@ -32,6 +32,27 @@ logger = get_logger(__name__) +def add_id_to_mysql(memory_id: str, mem_cube_id: str): + """Add id to mysql, will deprecate this function in the future""" + # TODO: tmp function, deprecate soon + import requests + + skill_mysql_url = os.getenv("SKILLS_MYSQL_URL", "") + skill_mysql_bearer = os.getenv("SKILLS_MYSQL_BEARER", "") + + if not skill_mysql_url or not skill_mysql_bearer: + logger.warning("SKILLS_MYSQL_URL or SKILLS_MYSQL_BEARER is not set") + return None + headers = {"Authorization": skill_mysql_bearer, "Content-Type": "application/json"} + data = {"memCubeId": mem_cube_id, "skillId": memory_id} + try: + response = requests.post(skill_mysql_url, headers=headers, json=data) + return response.json() + except Exception as e: + logger.warning(f"Error adding id to mysql: {e}") + return None + + @require_python_package( import_name="alibabacloud_oss_v2", install_command="pip install alibabacloud-oss-v2", @@ -108,7 +129,14 @@ def _split_task_chunk_by_llm(llm: BaseLLM, messages: MessageList) -> dict[str, M for item in response_json: task_name = item["task_name"] message_indices = item["message_indices"] - for start, end in message_indices: + for indices in message_indices: + # Validate that indices is a list/tuple with exactly 2 elements + if not isinstance(indices, list | tuple) or len(indices) != 2: + logger.warning( + f"Invalid message indices format for task '{task_name}': {indices}, skipping" + ) + continue + start, end = indices task_chunks.setdefault(task_name, []).extend(messages[start : end + 1]) return task_chunks @@ -125,7 +153,7 @@ def _extract_skill_memory_by_llm( "procedure": mem["metadata"]["procedure"], "experience": mem["metadata"]["experience"], "preference": mem["metadata"]["preference"], - "example": mem["metadata"]["example"], + "examples": mem["metadata"]["examples"], "tags": mem["metadata"]["tags"], "scripts": mem["metadata"].get("scripts"), "others": mem["metadata"]["others"], @@ -153,7 +181,10 @@ def _extract_skill_memory_by_llm( # Call LLM to extract skill memory with retry logic for attempt in range(3): try: - response_text = llm.generate(prompt) + # Only pass model_name_or_path if SKILLS_LLM is set + 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) # Clean up response (remove markdown code blocks if present) response_text = response_text.strip() response_text = response_text.replace("```json", "").replace("```", "").strip() @@ -195,7 +226,7 @@ def _recall_related_skill_memories( query = _rewrite_query(task_type, messages, llm, rewrite_query) related_skill_memories = searcher.search( query, - top_k=10, + top_k=5, memory_type="SkillMemory", info=info, include_skill_memory=True, @@ -326,11 +357,11 @@ def _write_skills_to_file( skill_md_content += f"- {pref}\n" # Add Examples section only if there are items - examples = skill_memory.get("example", []) + examples = skill_memory.get("examples", []) if examples: skill_md_content += "\n## Examples\n" for idx, example in enumerate(examples, 1): - skill_md_content += f"\n### Example {idx}\n{example}\n" + skill_md_content += f"\n### Example {idx}\n```markdown\n{example}\n```\n" # Add scripts reference if present scripts = skill_memory.get("scripts") @@ -444,7 +475,7 @@ def create_skill_memory_item( procedure=skill_memory.get("procedure", ""), experience=skill_memory.get("experience", []), preference=skill_memory.get("preference", []), - example=skill_memory.get("example", []), + examples=skill_memory.get("examples", []), scripts=skill_memory.get("scripts"), others=skill_memory.get("others"), url=skill_memory.get("url", ""), @@ -501,6 +532,9 @@ def process_skill_memory_fine( messages = _add_index_to_message(messages) task_chunks = _split_task_chunk_by_llm(llm, messages) + if not task_chunks: + logger.warning("No task chunks found") + return [] # recall - get related skill memories for each task separately (parallel) related_skill_memories_by_task = {} @@ -647,4 +681,10 @@ def process_skill_memory_fine( logger.warning(f"Error creating skill memory item: {e}") continue + # TODO: deprecate this funtion and call + for skill_memory in skill_memory_items: + add_id_to_mysql( + memory_id=skill_memory.id, mem_cube_id=kwargs.get("user_name", info.get("user_id", "")) + ) + return skill_memory_items diff --git a/src/memos/memories/textual/base.py b/src/memos/memories/textual/base.py index 6b0b7e8a6..cbf1a97b3 100644 --- a/src/memos/memories/textual/base.py +++ b/src/memos/memories/textual/base.py @@ -59,7 +59,9 @@ def get(self, memory_id: str, user_name: str | None = None) -> TextualMemoryItem """ @abstractmethod - def get_by_ids(self, memory_ids: list[str]) -> list[TextualMemoryItem]: + def get_by_ids( + self, memory_ids: list[str], user_name: str | None = None + ) -> list[TextualMemoryItem]: """Get memories by their IDs. Args: memory_ids (list[str]): List of memory IDs to retrieve. diff --git a/src/memos/memories/textual/tree.py b/src/memos/memories/textual/tree.py index 5b999cd6d..b556db5d7 100644 --- a/src/memos/memories/textual/tree.py +++ b/src/memos/memories/textual/tree.py @@ -323,7 +323,8 @@ def get(self, memory_id: str, user_name: str | None = None) -> TextualMemoryItem def get_by_ids( self, memory_ids: list[str], user_name: str | None = None ) -> list[TextualMemoryItem]: - raise NotImplementedError + graph_output = self.graph_store.get_nodes(ids=memory_ids, user_name=user_name) + return graph_output def get_all( self, diff --git a/src/memos/templates/skill_mem_prompt.py b/src/memos/templates/skill_mem_prompt.py index abfc11ef2..870c25e1a 100644 --- a/src/memos/templates/skill_mem_prompt.py +++ b/src/memos/templates/skill_mem_prompt.py @@ -13,12 +13,13 @@ 3. **Filter Chit-chat**: Only extract tasks with clear goals, instructions, or knowledge-based discussions. Ignore meaningless greetings (such as "Hello", "Are you there?") or closing remarks unless they are part of the task context. 4. **Output Format**: Please strictly follow the JSON format for output to facilitate my subsequent processing. 5. **Language Consistency**: The language used in the task_name field must match the language used in the conversation records. +6. **Generic Task Names**: Use generic, reusable task names, not specific descriptions. For example, use "Travel Planning" instead of "Planning a 5-day trip to Chengdu". ```json [ { "task_id": 1, - "task_name": "Brief description of the task (e.g., Making travel plans)", + "task_name": "Generic task name (e.g., Travel Planning, Code Review, Data Analysis)", "message_indices": [[0, 5],[16, 17]], # 0-5 and 16-17 are the message indices for this task "reasoning": "Briefly explain why these messages are grouped together" }, @@ -46,12 +47,13 @@ 3. **过滤闲聊**:仅提取具有明确目标、指令或基于知识的讨论的任务。忽略无意义的问候(例如"你好"、"在吗?")或结束语,除非它们是任务上下文的一部分。 4. **输出格式**:请严格遵循 JSON 格式输出,以便我后续处理。 5. **语言一致性**:task_name 字段使用的语言必须与对话记录中使用的语言相匹配。 +6. **通用任务名称**:使用通用的、可复用的任务名称,而不是具体的描述。例如,使用"旅行规划"而不是"规划成都5日游"。 ```json [ { "task_id": 1, - "task_name": "任务的简要描述(例如:制定旅行计划)", + "task_name": "通用任务名称(例如:旅行规划、代码审查、数据分析)", "message_indices": [[0, 5],[16, 17]], # 0-5 和 16-17 是此任务的消息索引 "reasoning": "简要解释为什么这些消息被分组在一起" }, @@ -66,10 +68,10 @@ SKILL_MEMORY_EXTRACTION_PROMPT = """ # Role -You are an expert in general skill extraction and skill memory management. You excel at analyzing conversations to extract actionable, transferable, and reusable skills, procedures, experiences, and user preferences. The skills you extract should be general and applicable across similar scenarios, not overly specific to a single instance. +You are an expert in skill abstraction and knowledge extraction. You excel at distilling general, reusable methodologies from specific conversations. # Task -Based on the provided conversation messages and existing skill memories, extract new skill memory or update existing ones. You need to determine whether the current conversation contains skills similar to existing memories. +Extract a universal skill template from the conversation that can be applied to similar scenarios. Compare with existing skills to determine if this is new or an update. # Existing Skill Memories {old_memories} @@ -77,26 +79,22 @@ # Conversation Messages {messages} -# Extraction Rules -1. **Similarity Check**: Compare the current conversation with existing skill memories. If a similar skill exists, set "update": true and provide the "old_memory_id". Otherwise, set "update": false and leave "old_memory_id" empty. -2. **Completeness**: Extract comprehensive information including procedures, experiences, preferences, and examples. -3. **Clarity**: Ensure procedures are step-by-step and easy to follow. -4. **Specificity**: Capture specific user preferences and lessons learned from experiences. -5. **Language Consistency**: Use the same language as the conversation. -6. **Accuracy**: Only extract information that is explicitly present or strongly implied in the conversation. +# Core Principles +1. **Generalization**: Extract abstract methodologies applicable across scenarios. Avoid specific details (e.g., "Travel Planning" not "Beijing Travel Planning"). +2. **Universality**: All fields except "example" must remain general and scenario-independent. +3. **Similarity Check**: If similar skill exists, set "update": true with "old_memory_id". Otherwise, set "update": false and leave "old_memory_id" empty. +4. **Language Consistency**: Match the conversation language. # Output Format -Please output in strict JSON format: - ```json { - "name": "A concise name for this skill or task type", - "description": "A clear description of what this skill does or accomplishes", - "procedure": "Step-by-step procedure: 1. First step 2. Second step 3. Third step...", - "experience": ["Lesson 1: Specific experience or insight learned", "Lesson 2: Another valuable experience..."], - "preference": ["User preference 1", "User preference 2", "User preference 3..."], - "example": ["Example case 1 demonstrating how to complete the task following this skill's guidance", "Example case 2..."], - "tags": ["tag1", "tag2", "tag3"], + "name": "General skill name (e.g., 'Travel Itinerary Planning', 'Code Review Workflow')", + "description": "Universal description of what this skill accomplishes", + "procedure": "Generic step-by-step process: 1. Step one 2. Step two...", + "experience": ["General principle or lesson learned", "Best practice applicable to similar cases..."], + "preference": ["User's general preference pattern", "Preferred approach or constraint..."], + "examples": ["Complete formatted output example in markdown format showing the final deliverable structure, content can be abbreviated with '...' but should demonstrate the format and structure", "Another complete output template..."], + "tags": ["keyword1", "keyword2"], "scripts": {"script_name.py": "# Python code here\nprint('Hello')", "another_script.py": "# More code\nimport os"}, "others": {"Section Title": "Content here", "reference.md": "# Reference content for this skill"}, "update": false, @@ -104,39 +102,39 @@ } ``` -# Field Descriptions -- **name**: Brief identifier for the skill (e.g., "Travel Planning", "Code Review Process") -- **description**: What this skill accomplishes or its purpose -- **procedure**: Sequential steps to complete the task -- **experience**: Lessons learned, best practices, things to avoid -- **preference**: User's specific preferences, likes, dislikes -- **example**: Concrete example cases demonstrating how to complete the task by following this skill's guidance -- **tags**: Relevant keywords for categorization -- **scripts**: Dictionary of scripts where key is the .py filename and value is the executable code snippet. Use null if not applicable -- **others**: Flexible additional information in key-value format. Can be either: +# Field Specifications +- **name**: Generic skill identifier without specific instances +- **description**: Universal purpose and applicability +- **procedure**: Abstract, reusable process steps without specific details. Should be generalizable to similar tasks +- **experience**: General lessons, principles, or insights +- **preference**: User's overarching preference patterns +- **examples**: Complete output templates showing the final deliverable format and structure. Should demonstrate how the task result looks when this skill is applied, including format, sections, and content organization. Content can be abbreviated but must show the complete structure. Use markdown format for better readability +- **tags**: Generic keywords for categorization +- **scripts**: Dictionary of scripts where key is the .py filename and value is the executable code snippet. Only applicable for code-related tasks (e.g., data processing, automation). Use null for non-coding tasks +- **others**: Supplementary information beyond standard fields or lengthy content unsuitable for other fields. Can be either: - Simple key-value pairs where key is a title and value is content - Separate markdown files where key is .md filename and value is the markdown content - Use null if not applicable -- **update**: true if updating existing memory, false if creating new -- **old_memory_id**: The ID of the existing memory being updated, or empty string if new + - Use null if not applicable +- **update**: true if updating existing skill, false if new +- **old_memory_id**: ID of skill being updated, or empty string if new -# Important Notes -- If no clear skill can be extracted from the conversation, return null -- Ensure all string values are properly formatted and contain meaningful information -- Arrays should contain at least one item if the field is populated -- Be thorough but avoid redundancy +# Critical Guidelines +- Keep all fields general except "examples" +- "examples" should demonstrate complete final output format and structure with all necessary sections +- "others" contains supplementary context or extended information +- Return null if no extractable skill exists -# Output -Please output only the JSON object, without any additional formatting, markdown code blocks, or explanation. +# Output Format +Output the JSON object only. """ SKILL_MEMORY_EXTRACTION_PROMPT_ZH = """ # 角色 -你是通用技能提取和技能记忆管理的专家。你擅长分析对话,提取可操作的、可迁移的、可复用的技能、流程、经验和用户偏好。你提取的技能应该是通用的,能够应用于类似场景,而不是过于针对单一实例。 +你是技能抽象和知识提取的专家。你擅长从具体对话中提炼通用的、可复用的方法论。 # 任务 -基于提供的对话消息和现有的技能记忆,提取新的技能记忆或更新现有的技能记忆。你需要判断当前对话中是否包含与现有记忆相似的技能。 +从对话中提取可应用于类似场景的通用技能模板。对比现有技能判断是新建还是更新。 # 现有技能记忆 {old_memories} @@ -144,26 +142,22 @@ # 对话消息 {messages} -# 提取规则 -1. **相似性检查**:将当前对话与现有技能记忆进行比较。如果存在相似的技能,设置 "update": true 并提供 "old_memory_id"。否则,设置 "update": false 并将 "old_memory_id" 留空。 -2. **完整性**:提取全面的信息,包括流程、经验、偏好和示例。 -3. **清晰性**:确保流程是逐步的,易于遵循。 -4. **具体性**:捕获具体的用户偏好和从经验中学到的教训。 -5. **语言一致性**:使用与对话相同的语言。 -6. **准确性**:仅提取对话中明确存在或强烈暗示的信息。 +# 核心原则 +1. **通用化**:提取可跨场景应用的抽象方法论。避免具体细节(如"旅行规划"而非"北京旅行规划")。 +2. **普适性**:除"examples"外,所有字段必须保持通用,与具体场景无关。 +3. **相似性检查**:如存在相似技能,设置"update": true 及"old_memory_id"。否则设置"update": false 并将"old_memory_id"留空。 +4. **语言一致性**:与对话语言保持一致。 # 输出格式 -请以严格的 JSON 格式输出: - ```json { - "name": "技能或任务类型的简洁名称", - "description": "对该技能的作用或目的的清晰描述", - "procedure": "逐步流程:1. 第一步 2. 第二步 3. 第三步...", - "experience": ["经验教训 1:学到的具体经验或见解", "经验教训 2:另一个有价值的经验..."], - "preference": ["用户偏好 1", "用户偏好 2", "用户偏好 3..."], - "example": ["示例案例 1:展示按照此技能的指引完成任务的过程", "示例案例 2..."], - "tags": ["标签1", "标签2", "标签3"], + "name": "通用技能名称(如:'旅行行程规划'、'代码审查流程')", + "description": "技能作用的通用描述", + "procedure": "通用的分步流程:1. 步骤一 2. 步骤二...", + "experience": ["通用原则或经验教训", "可应用于类似场景的最佳实践..."], + "preference": ["用户的通用偏好模式", "偏好的方法或约束..."], + "examples": ["展示最终交付成果的完整格式范本(使用 markdown 格式), 内容可用'...'省略,但需展示完整格式和结构", "另一个完整输出模板..."], + "tags": ["关键词1", "关键词2"], "scripts": {"script_name.py": "# Python 代码\nprint('Hello')", "another_script.py": "# 更多代码\nimport os"}, "others": {"章节标题": "这里的内容", "reference.md": "# 此技能的参考内容"}, "update": false, @@ -171,30 +165,30 @@ } ``` -# 字段说明 -- **name**:技能的简短标识符(例如:"旅行计划"、"代码审查流程") -- **description**:该技能完成什么或其目的 -- **procedure**:完成任务的顺序步骤 -- **experience**:学到的经验教训、最佳实践、要避免的事项 -- **preference**:用户的具体偏好、喜好、厌恶 -- **example**:具体的示例案例,展示如何按照此技能的指引完成任务 -- **tags**:用于分类的相关关键词 -- **scripts**:脚本字典,其中 key 是 .py 文件名,value 是可执行代码片段。如果不适用则使用 null -- **others**:灵活的附加信息,采用键值对格式。可以是: +# 字段规范 +- **name**:通用技能标识符,不含具体实例 +- **description**:通用用途和适用范围 +- **procedure**:抽象的、可复用的流程步骤,不含具体细节。应当能够推广到类似任务 +- **experience**:通用经验、原则或见解 +- **preference**:用户的整体偏好模式 +- **examples**:展示最终任务成果的输出模板,包括格式、章节和内容组织结构。应展示应用此技能后任务结果的样子,包含所有必要的部分。内容可以省略但必须展示完整结构。使用 markdown 格式以提高可读性 +- **tags**:通用分类关键词 +- **scripts**:脚本字典,其中 key 是 .py 文件名,value 是可执行代码片段。仅适用于代码相关任务(如数据处理、自动化脚本等)。非编程任务直接使用 null +- **others**:标准字段之外的补充信息或不适合放在其他字段的较长内容。可以是: - 简单的键值对,其中 key 是标题,value 是内容 - 独立的 markdown 文件,其中 key 是 .md 文件名,value 是 markdown 内容 - 如果不适用则使用 null -- **update**:如果更新现有记忆则为 true,如果创建新记忆则为 false -- **old_memory_id**:正在更新的现有记忆的 ID,如果是新记忆则为空字符串 + - 如果不适用则使用 null +- **update**:更新现有技能为true,新建为false +- **old_memory_id**:被更新技能的ID,新建则为空字符串 -# 重要说明 -- 如果无法从对话中提取清晰的技能,返回 null -- 确保所有字符串值格式正确且包含有意义的信息 -- 如果填充数组,则数组应至少包含一项 -- 要全面但避免冗余 +# 关键指导 +- 除"examples"外保持所有字段通用 +- "examples"应展示完整的最终输出格式和结构,包含所有必要章节 +- "others"包含补充说明或扩展信息 +- 无法提取技能时返回null -# 输出 -请仅输出 JSON 对象,不要添加任何额外的格式、markdown 代码块或解释。 +# 输出格式 +仅输出JSON对象。 """ From 962f80499d400abb1f55bb561d312a27dbefd3d7 Mon Sep 17 00:00:00 2001 From: "yuan.wang" Date: Wed, 28 Jan 2026 19:48:29 +0800 Subject: [PATCH 20/35] feat: modify prompt --- .../mem_reader/read_skill_memory/process_skill_memory.py | 4 +++- src/memos/templates/skill_mem_prompt.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) 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 9fc26cd8b..bfa9ea2b1 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 @@ -115,7 +115,9 @@ def _split_task_chunk_by_llm(llm: BaseLLM, messages: MessageList) -> dict[str, M prompt = [{"role": "user", "content": template.replace("{{messages}}", messages_context)}] for attempt in range(3): try: - response_text = llm.generate(prompt) + 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) response_json = json.loads(response_text.replace("```json", "").replace("```", "")) break except Exception as e: diff --git a/src/memos/templates/skill_mem_prompt.py b/src/memos/templates/skill_mem_prompt.py index 870c25e1a..65aba175c 100644 --- a/src/memos/templates/skill_mem_prompt.py +++ b/src/memos/templates/skill_mem_prompt.py @@ -108,13 +108,13 @@ - **procedure**: Abstract, reusable process steps without specific details. Should be generalizable to similar tasks - **experience**: General lessons, principles, or insights - **preference**: User's overarching preference patterns -- **examples**: Complete output templates showing the final deliverable format and structure. Should demonstrate how the task result looks when this skill is applied, including format, sections, and content organization. Content can be abbreviated but must show the complete structure. Use markdown format for better readability - **tags**: Generic keywords for categorization - **scripts**: Dictionary of scripts where key is the .py filename and value is the executable code snippet. Only applicable for code-related tasks (e.g., data processing, automation). Use null for non-coding tasks - **others**: Supplementary information beyond standard fields or lengthy content unsuitable for other fields. Can be either: - Simple key-value pairs where key is a title and value is content - Separate markdown files where key is .md filename and value is the markdown content - Use null if not applicable +- **examples**: Complete output templates showing the final deliverable format and structure. Should demonstrate how the task result looks when this skill is applied, including format, sections, and content organization. Content can be abbreviated but must show the complete structure. Use markdown format for better readability - **update**: true if updating existing skill, false if new - **old_memory_id**: ID of skill being updated, or empty string if new @@ -171,13 +171,13 @@ - **procedure**:抽象的、可复用的流程步骤,不含具体细节。应当能够推广到类似任务 - **experience**:通用经验、原则或见解 - **preference**:用户的整体偏好模式 -- **examples**:展示最终任务成果的输出模板,包括格式、章节和内容组织结构。应展示应用此技能后任务结果的样子,包含所有必要的部分。内容可以省略但必须展示完整结构。使用 markdown 格式以提高可读性 - **tags**:通用分类关键词 - **scripts**:脚本字典,其中 key 是 .py 文件名,value 是可执行代码片段。仅适用于代码相关任务(如数据处理、自动化脚本等)。非编程任务直接使用 null - **others**:标准字段之外的补充信息或不适合放在其他字段的较长内容。可以是: - 简单的键值对,其中 key 是标题,value 是内容 - 独立的 markdown 文件,其中 key 是 .md 文件名,value 是 markdown 内容 - 如果不适用则使用 null +- **examples**:展示最终任务成果的输出模板,包括格式、章节和内容组织结构。应展示应用此技能后任务结果的样子,包含所有必要的部分。内容可以省略但必须展示完整结构。使用 markdown 格式以提高可读性 - **update**:更新现有技能为true,新建为false - **old_memory_id**:被更新技能的ID,新建则为空字符串 From bbb6e79c9c8ec4cbbc902072bcfbb7a9cf8274d0 Mon Sep 17 00:00:00 2001 From: "yuan.wang" Date: Wed, 28 Jan 2026 21:20:20 +0800 Subject: [PATCH 21/35] feat: modify code --- src/memos/api/handlers/memory_handler.py | 5 +---- src/memos/api/routers/server_router.py | 2 +- src/memos/templates/skill_mem_prompt.py | 26 +++++++++++++----------- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/memos/api/handlers/memory_handler.py b/src/memos/api/handlers/memory_handler.py index dfde51961..e8bc5b640 100644 --- a/src/memos/api/handlers/memory_handler.py +++ b/src/memos/api/handlers/memory_handler.py @@ -235,14 +235,11 @@ def handle_get_memory_by_ids( collection_name, memory_ids ) if result is not None: + result = [format_memory_item(item, save_sources=False) for item in result] memories.extend(result) except Exception: continue - memories = [ - format_memory_item(item, save_sources=False) for item in memories if item is not None - ] - return GetMemoryResponse( message="Memories retrieved successfully", code=200, data={"memories": memories} ) diff --git a/src/memos/api/routers/server_router.py b/src/memos/api/routers/server_router.py index d28ca4a08..736c328ac 100644 --- a/src/memos/api/routers/server_router.py +++ b/src/memos/api/routers/server_router.py @@ -320,7 +320,7 @@ def get_memory_by_id(memory_id: str): ) -@router.get("/get_memory_by_ids", summary="Get memory by ids", response_model=GetMemoryResponse) +@router.post("/get_memory_by_ids", summary="Get memory by ids", response_model=GetMemoryResponse) def get_memory_by_ids(memory_ids: list[str]): return handlers.memory_handler.handle_get_memory_by_ids( memory_ids=memory_ids, diff --git a/src/memos/templates/skill_mem_prompt.py b/src/memos/templates/skill_mem_prompt.py index 65aba175c..0bc0c1809 100644 --- a/src/memos/templates/skill_mem_prompt.py +++ b/src/memos/templates/skill_mem_prompt.py @@ -1,4 +1,7 @@ TASK_CHUNKING_PROMPT = """ +# Context (Conversation Records) +{{messages}} + # Role You are an expert in natural language processing (NLP) and dialogue logic analysis. You excel at organizing logical threads from complex long conversations and accurately extracting users' core intentions. @@ -11,9 +14,10 @@ 1. **Task Independence**: If multiple unrelated topics are discussed in the conversation, identify them as different tasks. 2. **Non-continuous Processing**: Pay attention to identifying "jumping" conversations. For example, if the user made travel plans in messages 8-11, switched to consulting about weather in messages 12-22, and then returned to making travel plans in messages 23-24, be sure to assign both 8-11 and 23-24 to the task "Making travel plans". However, if messages are continuous and belong to the same task, do not split them apart. 3. **Filter Chit-chat**: Only extract tasks with clear goals, instructions, or knowledge-based discussions. Ignore meaningless greetings (such as "Hello", "Are you there?") or closing remarks unless they are part of the task context. -4. **Output Format**: Please strictly follow the JSON format for output to facilitate my subsequent processing. -5. **Language Consistency**: The language used in the task_name field must match the language used in the conversation records. -6. **Generic Task Names**: Use generic, reusable task names, not specific descriptions. For example, use "Travel Planning" instead of "Planning a 5-day trip to Chengdu". +4. **Main Task and Subtasks**: Carefully identify whether subtasks serve a main task. If a subtask supports the main task (e.g., "checking weather" serves "travel planning"), do NOT separate it as an independent task. Instead, include all related conversations in the main task. Only split tasks when they are truly independent and unrelated. +5. **Output Format**: Please strictly follow the JSON format for output to facilitate my subsequent processing. +6. **Language Consistency**: The language used in the task_name field must match the language used in the conversation records. +7. **Generic Task Names**: Use generic, reusable task names, not specific descriptions. For example, use "Travel Planning" instead of "Planning a 5-day trip to Chengdu". ```json [ @@ -26,13 +30,13 @@ ... ] ``` - -# Context (Conversation Records) -{{messages}} """ TASK_CHUNKING_PROMPT_ZH = """ +# 上下文(对话记录) +{{messages}} + # 角色 你是自然语言处理(NLP)和对话逻辑分析的专家。你擅长从复杂的长对话中整理逻辑线索,准确提取用户的核心意图。 @@ -45,9 +49,10 @@ 1. **任务独立性**:如果对话中讨论了多个不相关的话题,请将它们识别为不同的任务。 2. **非连续处理**:注意识别"跳跃式"对话。例如,如果用户在消息 8-11 中制定旅行计划,在消息 12-22 中切换到咨询天气,然后在消息 23-24 中返回到制定旅行计划,请务必将 8-11 和 23-24 都分配给"制定旅行计划"任务。但是,如果消息是连续的且属于同一任务,不能将其分开。 3. **过滤闲聊**:仅提取具有明确目标、指令或基于知识的讨论的任务。忽略无意义的问候(例如"你好"、"在吗?")或结束语,除非它们是任务上下文的一部分。 -4. **输出格式**:请严格遵循 JSON 格式输出,以便我后续处理。 -5. **语言一致性**:task_name 字段使用的语言必须与对话记录中使用的语言相匹配。 -6. **通用任务名称**:使用通用的、可复用的任务名称,而不是具体的描述。例如,使用"旅行规划"而不是"规划成都5日游"。 +4. **主任务与子任务识别**:仔细识别子任务是否服务于主任务。如果子任务是为主任务服务的(例如"查天气"服务于"旅行规划"),不要将其作为独立任务分离出来,而是将所有相关对话都划分到主任务中。只有真正独立且无关联的任务才需要分开。 +5. **输出格式**:请严格遵循 JSON 格式输出,以便我后续处理。 +6. **语言一致性**:task_name 字段使用的语言必须与对话记录中使用的语言相匹配。 +7. **通用任务名称**:使用通用的、可复用的任务名称,而不是具体的描述。例如,使用"旅行规划"而不是"规划成都5日游"。 ```json [ @@ -60,9 +65,6 @@ ... ] ``` - -# 上下文(对话记录) -{{messages}} """ From dcfb772c4a15cbbe4c867d626a71af87139b9b20 Mon Sep 17 00:00:00 2001 From: "yuan.wang" Date: Thu, 29 Jan 2026 10:19:25 +0800 Subject: [PATCH 22/35] feat: add logger --- .../read_skill_memory/process_skill_memory.py | 95 ++++++++++++------- 1 file changed, 62 insertions(+), 33 deletions(-) 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 bfa9ea2b1..bb809e69d 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 @@ -41,15 +41,24 @@ def add_id_to_mysql(memory_id: str, mem_cube_id: str): skill_mysql_bearer = os.getenv("SKILLS_MYSQL_BEARER", "") if not skill_mysql_url or not skill_mysql_bearer: - logger.warning("SKILLS_MYSQL_URL or SKILLS_MYSQL_BEARER is not set") + logger.warning("[PROCESS_SKILLS] SKILLS_MYSQL_URL or SKILLS_MYSQL_BEARER is not set") return None headers = {"Authorization": skill_mysql_bearer, "Content-Type": "application/json"} data = {"memCubeId": mem_cube_id, "skillId": memory_id} try: response = requests.post(skill_mysql_url, headers=headers, json=data) + + logger.info(f"[PROCESS_SKILLS] response: \n\n{response.json()}") + logger.info(f"[PROCESS_SKILLS] memory_id: \n\n{memory_id}") + logger.info(f"[PROCESS_SKILLS] mem_cube_id: \n\n{mem_cube_id}") + logger.info(f"[PROCESS_SKILLS] skill_mysql_url: \n\n{skill_mysql_url}") + logger.info(f"[PROCESS_SKILLS] skill_mysql_bearer: \n\n{skill_mysql_bearer}") + logger.info(f"[PROCESS_SKILLS] headers: \n\n{headers}") + logger.info(f"[PROCESS_SKILLS] data: \n\n{data}") + return response.json() except Exception as e: - logger.warning(f"Error adding id to mysql: {e}") + logger.warning(f"[PROCESS_SKILLS] Error adding id to mysql: {e}") return None @@ -90,7 +99,7 @@ def _reconstruct_messages_from_memory_items(memory_items: list[TextualMemoryItem reconstructed_messages.append({"role": role, "content": content}) seen.add(message_key) except Exception as e: - logger.warning(f"Error reconstructing message: {e}") + logger.warning(f"[PROCESS_SKILLS] Error reconstructing message: {e}") continue return reconstructed_messages @@ -121,9 +130,11 @@ def _split_task_chunk_by_llm(llm: BaseLLM, messages: MessageList) -> dict[str, M response_json = json.loads(response_text.replace("```json", "").replace("```", "")) break except Exception as e: - logger.warning(f"LLM generate failed (attempt {attempt + 1}): {e}") + logger.warning(f"[PROCESS_SKILLS] LLM generate failed (attempt {attempt + 1}): {e}") if attempt == 2: - logger.warning("LLM generate failed after 3 retries, returning empty dict") + logger.warning( + "[PROCESS_SKILLS] LLM generate failed after 3 retries, returning empty dict" + ) response_json = [] break @@ -135,7 +146,7 @@ def _split_task_chunk_by_llm(llm: BaseLLM, messages: MessageList) -> dict[str, M # Validate that indices is a list/tuple with exactly 2 elements if not isinstance(indices, list | tuple) or len(indices) != 2: logger.warning( - f"Invalid message indices format for task '{task_name}': {indices}, skipping" + f"[PROCESS_SKILLS] Invalid message indices format for task '{task_name}': {indices}, skipping" ) continue start, end = indices @@ -196,21 +207,27 @@ def _extract_skill_memory_by_llm( # If LLM returns null (parsed as None), log and return None if skill_memory is None: - logger.info("No skill memory extracted from conversation (LLM returned null)") + logger.info( + "[PROCESS_SKILLS] No skill memory extracted from conversation (LLM returned null)" + ) return None return skill_memory except json.JSONDecodeError as e: - logger.warning(f"JSON decode failed (attempt {attempt + 1}): {e}") - logger.debug(f"Response text: {response_text}") + logger.warning(f"[PROCESS_SKILLS] JSON decode failed (attempt {attempt + 1}): {e}") + logger.debug(f"[PROCESS_SKILLS] Response text: {response_text}") if attempt == 2: - logger.warning("Failed to parse skill memory after 3 retries") + logger.warning("[PROCESS_SKILLS] Failed to parse skill memory after 3 retries") return None except Exception as e: - logger.warning(f"LLM skill memory extraction failed (attempt {attempt + 1}): {e}") + logger.warning( + f"[PROCESS_SKILLS] LLM skill memory extraction failed (attempt {attempt + 1}): {e}" + ) if attempt == 2: - logger.warning("LLM skill memory extraction failed after 3 retries") + logger.warning( + "[PROCESS_SKILLS] LLM skill memory extraction failed after 3 retries" + ) return None return None @@ -262,13 +279,15 @@ def _rewrite_query(task_type: str, messages: MessageList, llm: BaseLLM, rewrite_ response_text = llm.generate(prompt) # Clean up response (remove any markdown formatting if present) response_text = response_text.strip() - logger.info(f"Rewritten query for task '{task_type}': {response_text}") + logger.info(f"[PROCESS_SKILLS] Rewritten query for task '{task_type}': {response_text}") return response_text except Exception as e: - logger.warning(f"LLM query rewrite failed (attempt {attempt + 1}): {e}") + logger.warning( + f"[PROCESS_SKILLS] LLM query rewrite failed (attempt {attempt + 1}): {e}" + ) if attempt == 2: logger.warning( - "LLM query rewrite failed after 3 retries, returning first message content" + "[PROCESS_SKILLS] LLM query rewrite failed after 3 retries, returning first message content" ) return messages[0]["content"] if messages else "" @@ -292,7 +311,7 @@ def _upload_skills_to_oss(local_file_path: str, oss_file_path: str, client: Any) ) if result.status_code != 200: - logger.warning("Failed to upload skill to OSS") + logger.warning("[PROCESS_SKILLS] Failed to upload skill to OSS") return "" # Construct and return the URL @@ -440,7 +459,7 @@ def _write_skills_to_file( arcname = Path(skill_dir.name) / file_path.relative_to(skill_dir) zipf.write(str(file_path), str(arcname)) - logger.info(f"Created skill zip file: {zip_path}") + logger.info(f"[PROCESS_SKILLS] Created skill zip file: {zip_path}") return str(zip_path) @@ -509,11 +528,13 @@ def process_skill_memory_fine( ) -> list[TextualMemoryItem]: # Validate required configurations if not oss_config: - logger.warning("OSS configuration is required for skill memory processing") + logger.warning("[PROCESS_SKILLS] OSS configuration is required for skill memory processing") return [] if not skills_dir_config: - logger.warning("Skills directory configuration is required for skill memory processing") + logger.warning( + "[PROCESS_SKILLS] Skills directory configuration is required for skill memory processing" + ) return [] # Validate skills_dir has required keys @@ -521,13 +542,13 @@ def process_skill_memory_fine( missing_keys = [key for key in required_keys if key not in skills_dir_config] if missing_keys: logger.warning( - f"Skills directory configuration missing required keys: {', '.join(missing_keys)}" + f"[PROCESS_SKILLS] Skills directory configuration missing required keys: {', '.join(missing_keys)}" ) return [] oss_client = create_oss_client(oss_config) if not oss_client: - logger.warning("Failed to create OSS client") + logger.warning("[PROCESS_SKILLS] Failed to create OSS client") return [] messages = _reconstruct_messages_from_memory_items(fast_memory_items) @@ -535,7 +556,7 @@ def process_skill_memory_fine( task_chunks = _split_task_chunk_by_llm(llm, messages) if not task_chunks: - logger.warning("No task chunks found") + logger.warning("[PROCESS_SKILLS] No task chunks found") return [] # recall - get related skill memories for each task separately (parallel) @@ -560,7 +581,9 @@ def process_skill_memory_fine( related_memories = future.result() related_skill_memories_by_task[task_name] = related_memories except Exception as e: - logger.warning(f"Error recalling skill memories for task '{task_name}': {e}") + logger.warning( + f"[PROCESS_SKILLS] Error recalling skill memories for task '{task_name}': {e}" + ) related_skill_memories_by_task[task_name] = [] skill_memories = [] @@ -580,7 +603,7 @@ def process_skill_memory_fine( if skill_memory: # Only add non-None results skill_memories.append(skill_memory) except Exception as e: - logger.warning(f"Error extracting skill memory: {e}") + logger.warning(f"[PROCESS_SKILLS] Error extracting skill memory: {e}") continue # write skills to file and get zip paths @@ -598,7 +621,7 @@ def process_skill_memory_fine( skill_memory = futures[future] skill_memory_with_paths.append((skill_memory, zip_path)) except Exception as e: - logger.warning(f"Error writing skills to file: {e}") + logger.warning(f"[PROCESS_SKILLS] Error writing skills to file: {e}") continue # Create a mapping from old_memory_id to old memory for easy lookup @@ -630,14 +653,20 @@ def process_skill_memory_fine( Path(skills_dir_config["skills_oss_dir"]) / user_id / zip_filename ).as_posix() _delete_skills_from_oss(old_oss_path, oss_client) - logger.info(f"Deleted old skill from OSS: {old_oss_path}") + logger.info( + f"[PROCESS_SKILLS] Deleted old skill from OSS: {old_oss_path}" + ) except Exception as e: - logger.warning(f"Failed to delete old skill from OSS: {e}") + logger.warning( + f"[PROCESS_SKILLS] Failed to delete old skill from OSS: {e}" + ) # delete old skill from graph db if graph_db: graph_db.delete_node_by_prams(memory_ids=[old_memory_id]) - logger.info(f"Deleted old skill from graph db: {old_memory_id}") + logger.info( + f"[PROCESS_SKILLS] Deleted old skill from graph db: {old_memory_id}" + ) # Upload new skill to OSS # Use the same filename as the local zip file @@ -654,9 +683,9 @@ def process_skill_memory_fine( # Set URL directly to skill_memory skill_memory["url"] = url - logger.info(f"Uploaded skill to OSS: {url}") + logger.info(f"[PROCESS_SKILLS] Uploaded skill to OSS: {url}") except Exception as e: - logger.warning(f"Error uploading skill to OSS: {e}") + logger.warning(f"[PROCESS_SKILLS] Error uploading skill to OSS: {e}") skill_memory["url"] = "" # Set to empty string if upload fails finally: # Clean up local files after upload @@ -669,9 +698,9 @@ def process_skill_memory_fine( # Delete skill directory if skill_dir.exists(): shutil.rmtree(skill_dir) - logger.info(f"Cleaned up local files: {zip_path} and {skill_dir}") + logger.info(f"[PROCESS_SKILLS] Cleaned up local files: {zip_path} and {skill_dir}") except Exception as cleanup_error: - logger.warning(f"Error cleaning up local files: {cleanup_error}") + logger.warning(f"[PROCESS_SKILLS] Error cleaning up local files: {cleanup_error}") # Create TextualMemoryItem objects skill_memory_items = [] @@ -680,7 +709,7 @@ def process_skill_memory_fine( memory_item = create_skill_memory_item(skill_memory, info, embedder) skill_memory_items.append(memory_item) except Exception as e: - logger.warning(f"Error creating skill memory item: {e}") + logger.warning(f"[PROCESS_SKILLS] Error creating skill memory item: {e}") continue # TODO: deprecate this funtion and call From b0946f10497758370cdfb48bea375ef19340be9c Mon Sep 17 00:00:00 2001 From: "yuan.wang" Date: Thu, 29 Jan 2026 11:55:50 +0800 Subject: [PATCH 23/35] feat: fix bug in memory id --- .../memories/textual/tree_text_memory/organize/manager.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/memos/memories/textual/tree_text_memory/organize/manager.py b/src/memos/memories/textual/tree_text_memory/organize/manager.py index 59675bdc2..5e9c74f61 100644 --- a/src/memos/memories/textual/tree_text_memory/organize/manager.py +++ b/src/memos/memories/textual/tree_text_memory/organize/manager.py @@ -183,7 +183,9 @@ def _add_memories_batch( "ToolTrajectoryMemory", "SkillMemory", ): - graph_node_id = str(uuid.uuid4()) + if not memory.id: + logger.error("Memory ID is not set, generating a new one") + graph_node_id = memory.id or str(uuid.uuid4()) metadata_dict = memory.metadata.model_dump(exclude_none=True) metadata_dict["updated_at"] = datetime.now().isoformat() @@ -384,7 +386,9 @@ def _add_to_graph_memory( """ Generalized method to add memory to a graph-based memory type (e.g., LongTermMemory, UserMemory). """ - node_id = str(uuid.uuid4()) + if not memory.id: + logger.error("Memory ID is not set, generating a new one") + node_id = memory.id or str(uuid.uuid4()) # Step 2: Add new node to graph metadata_dict = memory.metadata.model_dump(exclude_none=True) tags = metadata_dict.get("tags") or [] From 16613630a17b3d9ee70143c05b7a66e5aba6c203 Mon Sep 17 00:00:00 2001 From: CCC <15764764+triple-c-individual@user.noreply.gitee.com> Date: Wed, 4 Feb 2026 19:23:01 +0800 Subject: [PATCH 24/35] =?UTF-8?q?fix:skill=20OSS=20+=20LOCAL=E5=AD=98=20zi?= =?UTF-8?q?p?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/memos/api/server_api.py | 6 + .../read_skill_memory/process_skill_memory.py | 206 +++++++++++------- 2 files changed, 137 insertions(+), 75 deletions(-) diff --git a/src/memos/api/server_api.py b/src/memos/api/server_api.py index ac9ed8d88..ea9c07c12 100644 --- a/src/memos/api/server_api.py +++ b/src/memos/api/server_api.py @@ -1,4 +1,5 @@ import logging +import os from fastapi import FastAPI, HTTPException from fastapi.exceptions import RequestValidationError @@ -6,7 +7,10 @@ from memos.api.exceptions import APIExceptionHandler from memos.api.middleware.request_context import RequestContextMiddleware from memos.api.routers.server_router import router as server_router +from starlette.staticfiles import StaticFiles +from dotenv import load_dotenv +load_dotenv() # Configure logging logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") @@ -18,6 +22,8 @@ version="1.0.1", ) +app.mount("/download", StaticFiles(directory=os.getenv("FILE_LOCAL_PATH")), name="static_mapping") + app.add_middleware(RequestContextMiddleware, source="server_api") # Include routers app.include_router(server_router) 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 6bd18808d..242cb0179 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 @@ -1,4 +1,5 @@ import json +import logging import os import shutil import uuid @@ -27,7 +28,9 @@ TASK_QUERY_REWRITE_PROMPT_ZH, ) from memos.types import MessageList - +import warnings +from dotenv import load_dotenv +load_dotenv() logger = get_logger(__name__) @@ -329,43 +332,74 @@ def _rewrite_query(task_type: str, messages: MessageList, llm: BaseLLM, rewrite_ import_name="alibabacloud_oss_v2", install_command="pip install alibabacloud-oss-v2", ) -def _upload_skills_to_oss(local_file_path: str, oss_file_path: str, client: Any) -> str: - import alibabacloud_oss_v2 as oss - - result = client.put_object_from_file( - request=oss.PutObjectRequest( - bucket=os.getenv("OSS_BUCKET_NAME"), - key=oss_file_path, - ), - filepath=local_file_path, - ) - - if result.status_code != 200: - logger.warning("[PROCESS_SKILLS] Failed to upload skill to OSS") - return "" - - # Construct and return the URL - bucket_name = os.getenv("OSS_BUCKET_NAME") - endpoint = os.getenv("OSS_ENDPOINT").replace("https://", "").replace("http://", "") - url = f"https://{bucket_name}.{endpoint}/{oss_file_path}" - return url +def _upload_skills(skills_repo_backend:str, skills_oss_dir: dict[str, Any] | None, local_file_path: str, client: Any, user_id:str) -> str: + if skills_repo_backend == "OSS": + zip_filename = Path(local_file_path).name + oss_path = ( + Path(skills_oss_dir) / user_id / zip_filename + ).as_posix() + + import alibabacloud_oss_v2 as oss + + result = client.put_object_from_file( + request=oss.PutObjectRequest( + bucket=os.getenv("OSS_BUCKET_NAME"), + key=oss_path, + ), + filepath=local_file_path, + ) + if result.status_code != 200: + logger.warning("[PROCESS_SKILLS] Failed to upload skill to OSS") + return "" + + # Construct and return the URL + bucket_name = os.getenv("OSS_BUCKET_NAME") + endpoint = os.getenv("OSS_ENDPOINT").replace("https://", "").replace("http://", "") + url = f"https://{bucket_name}.{endpoint}/{oss_path}" + return url + else: + import sys + args = sys.argv + PORT = int(args[args.index('--port') + 1]) if '--port' in args and args.index('--port') + 1 < len( + args) else "8000" + logging.warning(f"PORT:{PORT}") + + zip_path = str(local_file_path) + local_save_path = os.getenv("FILE_LOCAL_PATH") + os.makedirs(local_save_path, exist_ok=True) + file_name = os.path.basename(zip_path) + target_full_path = os.path.join(local_save_path, file_name) + shutil.copy2(zip_path, target_full_path) + return f"http://localhost:{PORT}/download/{file_name}" @require_python_package( import_name="alibabacloud_oss_v2", install_command="pip install alibabacloud-oss-v2", ) -def _delete_skills_from_oss(oss_file_path: str, client: Any) -> Any: - import alibabacloud_oss_v2 as oss - - result = client.delete_object( - oss.DeleteObjectRequest( - bucket=os.getenv("OSS_BUCKET_NAME"), - key=oss_file_path, +def _delete_skills(skills_repo_backend:str, zip_filename:str, client: Any, skills_oss_dir: dict[str, Any] | None, user_id:str) -> Any: + if skills_repo_backend == "OSS": + old_path = ( + Path(skills_oss_dir) / user_id / zip_filename + ).as_posix() + import alibabacloud_oss_v2 as oss + return client.delete_object( + oss.DeleteObjectRequest( + bucket=os.getenv("OSS_BUCKET_NAME"), + key=old_path, + ) ) - ) - return result - + else: + target_full_path = os.path.join(os.getenv("FILE_LOCAL_PATH"), zip_filename) + target_path = Path(target_full_path) + try: + if target_path.is_file(): + target_path.unlink() + logger.info(f"本地文件 {target_path} 已成功删除") + else: + print(f"本地文件 {target_path} 不存在,无需删除") + except Exception as e: + print(f"删除本地文件时出错:{e}") def _write_skills_to_file( skill_memory: dict[str, Any], info: dict[str, Any], skills_dir_config: dict[str, Any] @@ -543,6 +577,47 @@ def create_skill_memory_item( return TextualMemoryItem(id=item_id, memory=memory_content, metadata=metadata) +def _skill_init(skills_repo_backend, oss_config, skills_dir_config): + if skills_repo_backend == "OSS": + # Validate required configurations + if not oss_config: + logger.warning("[PROCESS_SKILLS] OSS configuration is required for skill memory processing") + return None, None, False + + if not skills_dir_config: + logger.warning( + "[PROCESS_SKILLS] Skills directory configuration is required for skill memory processing" + ) + return None, None, False + + # Validate skills_dir has required keys + required_keys = ["skills_local_dir", "skills_oss_dir"] + missing_keys = [key for key in required_keys if key not in skills_dir_config] + if missing_keys: + logger.warning( + f"[PROCESS_SKILLS] Skills directory configuration missing required keys: {', '.join(missing_keys)}" + ) + return None, None, False + + oss_client = create_oss_client(oss_config) + if not oss_client: + logger.warning("[PROCESS_SKILLS] Failed to create OSS client") + return None, None, False + return oss_client, missing_keys, True + else: + return None, None, True + + +def _get_skill_file_storage_location() -> str: + # SKILLS_REPO_BACKEND: Skill 文件保存地址 OSS/LOCAL + ALLOWED_BACKENDS = {"OSS", "LOCAL"} + raw_backend = os.getenv("SKILLS_REPO_BACKEND") + if raw_backend in ALLOWED_BACKENDS: + return raw_backend + else: + warnings.warn("环境变量【SKILLS_REPO_BACKEND】赋值错误,本次使用 LOCAL 存储 skill", UserWarning) + return "LOCAL" + def process_skill_memory_fine( fast_memory_items: list[TextualMemoryItem], @@ -556,15 +631,9 @@ def process_skill_memory_fine( skills_dir_config: dict[str, Any] | None = None, **kwargs, ) -> list[TextualMemoryItem]: - # Validate required configurations - if not oss_config: - logger.warning("[PROCESS_SKILLS] OSS configuration is required for skill memory processing") - return [] - - if not skills_dir_config: - logger.warning( - "[PROCESS_SKILLS] Skills directory configuration is required for skill memory processing" - ) + skills_repo_backend = _get_skill_file_storage_location() + oss_client, missing_keys, flag = _skill_init(skills_repo_backend, oss_config, skills_dir_config) + if not flag: return [] chat_history = kwargs.get("chat_history") @@ -572,20 +641,6 @@ def process_skill_memory_fine( chat_history = [] logger.warning("[PROCESS_SKILLS] History is None in Skills") - # Validate skills_dir has required keys - required_keys = ["skills_local_dir", "skills_oss_dir"] - missing_keys = [key for key in required_keys if key not in skills_dir_config] - if missing_keys: - logger.warning( - f"[PROCESS_SKILLS] Skills directory configuration missing required keys: {', '.join(missing_keys)}" - ) - return [] - - oss_client = create_oss_client(oss_config) - if not oss_client: - logger.warning("[PROCESS_SKILLS] Failed to create OSS client") - return [] - messages = _reconstruct_messages_from_memory_items(fast_memory_items) chat_history, messages = _preprocess_extract_messages(chat_history, messages) @@ -684,23 +739,26 @@ def process_skill_memory_fine( old_memory = old_memories_map.get(old_memory_id) if old_memory: - # Get old OSS path from the old memory's metadata - old_oss_path = getattr(old_memory.metadata, "url", None) + # Get old path from the old memory's metadata + old_path = getattr(old_memory.metadata, "url", None) - if old_oss_path: + if old_path: try: # delete old skill from OSS - zip_filename = Path(old_oss_path).name - old_oss_path = ( - Path(skills_dir_config["skills_oss_dir"]) / user_id / zip_filename - ).as_posix() - _delete_skills_from_oss(old_oss_path, oss_client) + zip_filename = Path(old_path).name + _delete_skills( + skills_repo_backend=skills_repo_backend, + zip_filename=zip_filename, + client=oss_client, + skills_oss_dir=skills_dir_config["skills_oss_dir"], + user_id=user_id + ) logger.info( - f"[PROCESS_SKILLS] Deleted old skill from OSS: {old_oss_path}" + f"[PROCESS_SKILLS] Deleted old skill from {skills_repo_backend}: {old_path}" ) except Exception as e: logger.warning( - f"[PROCESS_SKILLS] Failed to delete old skill from OSS: {e}" + f"[PROCESS_SKILLS] Failed to delete old skill from {skills_repo_backend}: {e}" ) # delete old skill from graph db @@ -710,24 +768,22 @@ def process_skill_memory_fine( f"[PROCESS_SKILLS] Deleted old skill from graph db: {old_memory_id}" ) - # Upload new skill to OSS + # Upload new skill # Use the same filename as the local zip file - zip_filename = Path(zip_path).name - oss_path = ( - Path(skills_dir_config["skills_oss_dir"]) / user_id / zip_filename - ).as_posix() - - # _upload_skills_to_oss returns the URL - url = _upload_skills_to_oss( - local_file_path=str(zip_path), oss_file_path=oss_path, client=oss_client + url = _upload_skills( + skills_repo_backend=skills_repo_backend, + skills_oss_dir=skills_dir_config["skills_oss_dir"], + local_file_path=zip_path, + client=oss_client, + user_id=user_id ) # Set URL directly to skill_memory skill_memory["url"] = url - logger.info(f"[PROCESS_SKILLS] Uploaded skill to OSS: {url}") + logger.info(f"[PROCESS_SKILLS] Uploaded skill to {skills_repo_backend}: {url}") except Exception as e: - logger.warning(f"[PROCESS_SKILLS] Error uploading skill to OSS: {e}") + logger.warning(f"[PROCESS_SKILLS] Error uploading skill to {skills_repo_backend}: {e}") skill_memory["url"] = "" # Set to empty string if upload fails finally: # Clean up local files after upload From b7df634ac909cb7fb468be7d86898f5b1894d017 Mon Sep 17 00:00:00 2001 From: CCC <15764764+triple-c-individual@user.noreply.gitee.com> Date: Wed, 4 Feb 2026 19:23:47 +0800 Subject: [PATCH 25/35] =?UTF-8?q?fix:skill=20OSS=20+=20LOCAL=E5=AD=98=20zi?= =?UTF-8?q?p?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/memos/mem_reader/read_skill_memory/process_skill_memory.py | 1 - 1 file changed, 1 deletion(-) 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 242cb0179..84cfb40ce 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 @@ -363,7 +363,6 @@ def _upload_skills(skills_repo_backend:str, skills_oss_dir: dict[str, Any] | Non args = sys.argv PORT = int(args[args.index('--port') + 1]) if '--port' in args and args.index('--port') + 1 < len( args) else "8000" - logging.warning(f"PORT:{PORT}") zip_path = str(local_file_path) local_save_path = os.getenv("FILE_LOCAL_PATH") From 55611b4643142abc49eba75d9b3f4cb439bd3228 Mon Sep 17 00:00:00 2001 From: CCC <15764764+triple-c-individual@user.noreply.gitee.com> Date: Wed, 4 Feb 2026 19:49:43 +0800 Subject: [PATCH 26/35] =?UTF-8?q?fix:skill=20OSS=20+=20LOCAL=E5=AD=98=20zi?= =?UTF-8?q?p?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/memos/api/server_api.py | 5 +- .../read_skill_memory/process_skill_memory.py | 64 +++++++++++++------ 2 files changed, 47 insertions(+), 22 deletions(-) diff --git a/src/memos/api/server_api.py b/src/memos/api/server_api.py index ea9c07c12..529a709a4 100644 --- a/src/memos/api/server_api.py +++ b/src/memos/api/server_api.py @@ -1,14 +1,15 @@ import logging import os +from dotenv import load_dotenv from fastapi import FastAPI, HTTPException from fastapi.exceptions import RequestValidationError +from starlette.staticfiles import StaticFiles from memos.api.exceptions import APIExceptionHandler from memos.api.middleware.request_context import RequestContextMiddleware from memos.api.routers.server_router import router as server_router -from starlette.staticfiles import StaticFiles -from dotenv import load_dotenv + load_dotenv() 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 84cfb40ce..d9d9d1644 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 @@ -1,8 +1,8 @@ import json -import logging import os import shutil import uuid +import warnings import zipfile from concurrent.futures import as_completed @@ -10,6 +10,8 @@ from pathlib import Path from typing import Any +from dotenv import load_dotenv + from memos.context.context import ContextThreadPoolExecutor from memos.dependency import require_python_package from memos.embedders.base import BaseEmbedder @@ -28,8 +30,8 @@ TASK_QUERY_REWRITE_PROMPT_ZH, ) from memos.types import MessageList -import warnings -from dotenv import load_dotenv + + load_dotenv() logger = get_logger(__name__) @@ -332,12 +334,16 @@ def _rewrite_query(task_type: str, messages: MessageList, llm: BaseLLM, rewrite_ import_name="alibabacloud_oss_v2", install_command="pip install alibabacloud-oss-v2", ) -def _upload_skills(skills_repo_backend:str, skills_oss_dir: dict[str, Any] | None, local_file_path: str, client: Any, user_id:str) -> str: +def _upload_skills( + skills_repo_backend: str, + skills_oss_dir: dict[str, Any] | None, + local_file_path: str, + client: Any, + user_id: str, +) -> str: if skills_repo_backend == "OSS": zip_filename = Path(local_file_path).name - oss_path = ( - Path(skills_oss_dir) / user_id / zip_filename - ).as_posix() + oss_path = (Path(skills_oss_dir) / user_id / zip_filename).as_posix() import alibabacloud_oss_v2 as oss @@ -360,9 +366,13 @@ def _upload_skills(skills_repo_backend:str, skills_oss_dir: dict[str, Any] | Non return url else: import sys + args = sys.argv - PORT = int(args[args.index('--port') + 1]) if '--port' in args and args.index('--port') + 1 < len( - args) else "8000" + port = ( + int(args[args.index("--port") + 1]) + if "--port" in args and args.index("--port") + 1 < len(args) + else "8000" + ) zip_path = str(local_file_path) local_save_path = os.getenv("FILE_LOCAL_PATH") @@ -370,18 +380,24 @@ def _upload_skills(skills_repo_backend:str, skills_oss_dir: dict[str, Any] | Non file_name = os.path.basename(zip_path) target_full_path = os.path.join(local_save_path, file_name) shutil.copy2(zip_path, target_full_path) - return f"http://localhost:{PORT}/download/{file_name}" + return f"http://localhost:{port}/download/{file_name}" + @require_python_package( import_name="alibabacloud_oss_v2", install_command="pip install alibabacloud-oss-v2", ) -def _delete_skills(skills_repo_backend:str, zip_filename:str, client: Any, skills_oss_dir: dict[str, Any] | None, user_id:str) -> Any: +def _delete_skills( + skills_repo_backend: str, + zip_filename: str, + client: Any, + skills_oss_dir: dict[str, Any] | None, + user_id: str, +) -> Any: if skills_repo_backend == "OSS": - old_path = ( - Path(skills_oss_dir) / user_id / zip_filename - ).as_posix() + old_path = (Path(skills_oss_dir) / user_id / zip_filename).as_posix() import alibabacloud_oss_v2 as oss + return client.delete_object( oss.DeleteObjectRequest( bucket=os.getenv("OSS_BUCKET_NAME"), @@ -400,6 +416,7 @@ def _delete_skills(skills_repo_backend:str, zip_filename:str, client: Any, skill except Exception as e: print(f"删除本地文件时出错:{e}") + def _write_skills_to_file( skill_memory: dict[str, Any], info: dict[str, Any], skills_dir_config: dict[str, Any] ) -> str: @@ -576,11 +593,14 @@ def create_skill_memory_item( return TextualMemoryItem(id=item_id, memory=memory_content, metadata=metadata) + def _skill_init(skills_repo_backend, oss_config, skills_dir_config): if skills_repo_backend == "OSS": # Validate required configurations if not oss_config: - logger.warning("[PROCESS_SKILLS] OSS configuration is required for skill memory processing") + logger.warning( + "[PROCESS_SKILLS] OSS configuration is required for skill memory processing" + ) return None, None, False if not skills_dir_config: @@ -609,12 +629,16 @@ def _skill_init(skills_repo_backend, oss_config, skills_dir_config): def _get_skill_file_storage_location() -> str: # SKILLS_REPO_BACKEND: Skill 文件保存地址 OSS/LOCAL - ALLOWED_BACKENDS = {"OSS", "LOCAL"} + allowed_backends = {"OSS", "LOCAL"} raw_backend = os.getenv("SKILLS_REPO_BACKEND") - if raw_backend in ALLOWED_BACKENDS: + if raw_backend in allowed_backends: return raw_backend else: - warnings.warn("环境变量【SKILLS_REPO_BACKEND】赋值错误,本次使用 LOCAL 存储 skill", UserWarning) + warnings.warn( + "环境变量【SKILLS_REPO_BACKEND】赋值错误,本次使用 LOCAL 存储 skill", + UserWarning, + stacklevel=1, + ) return "LOCAL" @@ -750,7 +774,7 @@ def process_skill_memory_fine( zip_filename=zip_filename, client=oss_client, skills_oss_dir=skills_dir_config["skills_oss_dir"], - user_id=user_id + user_id=user_id, ) logger.info( f"[PROCESS_SKILLS] Deleted old skill from {skills_repo_backend}: {old_path}" @@ -774,7 +798,7 @@ def process_skill_memory_fine( skills_oss_dir=skills_dir_config["skills_oss_dir"], local_file_path=zip_path, client=oss_client, - user_id=user_id + user_id=user_id, ) # Set URL directly to skill_memory From a4d8a4341a4e3ba1658fe708c440dfc34cc99763 Mon Sep 17 00:00:00 2001 From: "yuan.wang" Date: Thu, 5 Feb 2026 21:04:18 +0800 Subject: [PATCH 27/35] feat: new code --- src/memos/api/handlers/memory_handler.py | 74 +++++++++++++++++++ src/memos/api/product_models.py | 8 ++ src/memos/api/routers/server_router.py | 11 +++ src/memos/graph_dbs/polardb.py | 2 + src/memos/mem_reader/multi_modal_struct.py | 19 ++++- .../read_multi_modal/assistant_parser.py | 13 +++- .../read_multi_modal/file_content_parser.py | 23 +++++- .../read_multi_modal/image_parser.py | 17 ++++- .../read_multi_modal/string_parser.py | 13 +++- .../read_multi_modal/system_parser.py | 20 ++++- .../read_multi_modal/text_content_parser.py | 13 +++- .../read_multi_modal/tool_parser.py | 13 +++- .../read_multi_modal/user_parser.py | 13 +++- .../read_skill_memory/process_skill_memory.py | 20 ++++- src/memos/mem_scheduler/general_scheduler.py | 5 ++ .../mem_scheduler/schemas/message_schemas.py | 8 ++ src/memos/memories/textual/tree.py | 2 +- src/memos/multi_mem_cube/single_cube.py | 4 + src/memos/types/general_types.py | 6 +- 19 files changed, 270 insertions(+), 14 deletions(-) diff --git a/src/memos/api/handlers/memory_handler.py b/src/memos/api/handlers/memory_handler.py index 2b9c137ca..a35caf4d9 100644 --- a/src/memos/api/handlers/memory_handler.py +++ b/src/memos/api/handlers/memory_handler.py @@ -14,6 +14,7 @@ from memos.api.product_models import ( DeleteMemoryRequest, DeleteMemoryResponse, + GetMemoryDashboardRequest, GetMemoryRequest, GetMemoryResponse, MemoryResponse, @@ -353,3 +354,76 @@ def handle_delete_memories(delete_mem_req: DeleteMemoryRequest, naive_mem_cube: message="Memories deleted successfully", data={"status": "success"}, ) + + +# ============================================================================= +# Other handler functions Endpoints (for internal use) +# ============================================================================= + + +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": []} + memories = naive_mem_cube.text_mem.get_all( + user_name=get_mem_req.mem_cube_id, + user_id=get_mem_req.user_id, + page=get_mem_req.page, + page_size=get_mem_req.page_size, + filter=get_mem_req.filter, + )["nodes"] + + results = post_process_textual_mem(results, memories, get_mem_req.mem_cube_id) + + if not get_mem_req.include_tool_memory: + results["tool_mem"] = [] + if not get_mem_req.include_skill_memory: + results["skill_mem"] = [] + + preferences: list[TextualMemoryItem] = [] + + format_preferences = [] + if get_mem_req.include_preference and naive_mem_cube.pref_mem is not None: + filter_params: dict[str, Any] = {} + if get_mem_req.user_id is not None: + filter_params["user_id"] = get_mem_req.user_id + if get_mem_req.mem_cube_id is not None: + filter_params["mem_cube_id"] = get_mem_req.mem_cube_id + if get_mem_req.filter is not None: + # Check and remove user_id/mem_cube_id from filter if present + filter_copy = get_mem_req.filter.copy() + removed_fields = [] + + if "user_id" in filter_copy: + filter_copy.pop("user_id") + removed_fields.append("user_id") + if "mem_cube_id" in filter_copy: + filter_copy.pop("mem_cube_id") + removed_fields.append("mem_cube_id") + + if removed_fields: + logger.warning( + f"Fields {removed_fields} found in filter will be ignored. " + f"Use request-level user_id/mem_cube_id parameters instead." + ) + + filter_params.update(filter_copy) + + preferences, _ = naive_mem_cube.pref_mem.get_memory_by_filter( + filter_params, page=get_mem_req.page, page_size=get_mem_req.page_size + ) + format_preferences = [format_memory_item(item, save_sources=False) for item in preferences] + + results = post_process_pref_mem( + results, format_preferences, get_mem_req.mem_cube_id, get_mem_req.include_preference + ) + + # Filter to only keep text_mem, pref_mem, tool_mem + filtered_results = { + "text_mem": results.get("text_mem", []), + "pref_mem": results.get("pref_mem", []), + "tool_mem": results.get("tool_mem", []), + "skill_mem": results.get("skill_mem", []), + } + + 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 d11573610..ff9d772ae 100644 --- a/src/memos/api/product_models.py +++ b/src/memos/api/product_models.py @@ -497,6 +497,8 @@ class APIADDRequest(BaseRequest): description="Session ID. If not provided, a default session will be used.", ) task_id: str | None = Field(None, description="Task ID for monitering async tasks") + manager_user_id: str | None = Field(None, description="Manager User ID") + project_id: str | None = Field(None, description="Project ID") # ==== Multi-cube writing ==== writable_cube_ids: list[str] | None = Field( @@ -804,6 +806,12 @@ class GetMemoryRequest(BaseRequest): ) +class GetMemoryDashboardRequest(GetMemoryRequest): + """Request model for getting memories for dashboard.""" + + mem_cube_id: str | None = Field(None, description="Cube ID") + + class DeleteMemoryRequest(BaseRequest): """Request model for deleting memories.""" diff --git a/src/memos/api/routers/server_router.py b/src/memos/api/routers/server_router.py index 243fb36cd..83079239f 100644 --- a/src/memos/api/routers/server_router.py +++ b/src/memos/api/routers/server_router.py @@ -38,6 +38,7 @@ DeleteMemoryResponse, ExistMemCubeIdRequest, ExistMemCubeIdResponse, + GetMemoryDashboardRequest, GetMemoryPlaygroundRequest, GetMemoryRequest, GetMemoryResponse, @@ -456,3 +457,13 @@ def recover_memory_by_record_id(memory_req: RecoverMemoryByRecordIdRequest): message="Called Successfully", data={"status": "success"}, ) + + +@router.post( + "/get_memory_dashboard", summary="Get memories for dashboard", response_model=GetMemoryResponse +) +def get_memories_dashboard(memory_req: GetMemoryDashboardRequest): + return handlers.memory_handler.handle_get_memories_dashboard( + get_mem_req=memory_req, + naive_mem_cube=naive_mem_cube, + ) diff --git a/src/memos/graph_dbs/polardb.py b/src/memos/graph_dbs/polardb.py index 18778532f..094235831 100644 --- a/src/memos/graph_dbs/polardb.py +++ b/src/memos/graph_dbs/polardb.py @@ -2167,6 +2167,8 @@ def search_by_embedding( oldid = row[3] # old_id score = row[4] # scope id_val = str(oldid) + if id_val.startswith('"') and id_val.endswith('"'): + id_val = id_val[1:-1] score_val = float(score) score_val = (score_val + 1) / 2 # align to neo4j, Normalized Cosine Score if threshold is None or score_val >= threshold: diff --git a/src/memos/mem_reader/multi_modal_struct.py b/src/memos/mem_reader/multi_modal_struct.py index ce75f6dc5..1a312868a 100644 --- a/src/memos/mem_reader/multi_modal_struct.py +++ b/src/memos/mem_reader/multi_modal_struct.py @@ -3,7 +3,7 @@ import re import traceback -from typing import Any +from typing import TYPE_CHECKING, Any from memos import log from memos.configs.mem_reader import MultiModalStructMemReaderConfig @@ -20,6 +20,10 @@ from memos.utils import timed +if TYPE_CHECKING: + from memos.types.general_types import UserContext + + logger = log.get_logger(__name__) @@ -667,6 +671,12 @@ def _process_one_item(fast_item: TextualMemoryItem) -> list[TextualMemoryItem]: if file_ids: extra_kwargs["file_ids"] = file_ids + # Extract manager_user_id and project_id from user_context + user_context: UserContext | None = kwargs.get("user_context") + if user_context: + extra_kwargs["manager_user_id"] = user_context.manager_user_id + extra_kwargs["project_id"] = user_context.project_id + # Determine prompt type based on sources prompt_type = self._determine_prompt_type(sources) @@ -782,6 +792,11 @@ def _process_tool_trajectory_fine( fine_memory_items = [] + # Extract manager_user_id and project_id from user_context + user_context: UserContext | None = kwargs.get("user_context") + manager_user_id = user_context.manager_user_id if user_context else None + project_id = user_context.project_id if user_context else None + for fast_item in fast_memory_items: # Extract memory text (string content) mem_str = fast_item.memory or "" @@ -808,6 +823,8 @@ def _process_tool_trajectory_fine( correctness=m.get("correctness", ""), experience=m.get("experience", ""), tool_used_status=m.get("tool_used_status", []), + manager_user_id=manager_user_id, + project_id=project_id, ) fine_memory_items.append(node) except Exception as e: diff --git a/src/memos/mem_reader/read_multi_modal/assistant_parser.py b/src/memos/mem_reader/read_multi_modal/assistant_parser.py index 89d4fec7f..bac9deaad 100644 --- a/src/memos/mem_reader/read_multi_modal/assistant_parser.py +++ b/src/memos/mem_reader/read_multi_modal/assistant_parser.py @@ -2,7 +2,7 @@ import json -from typing import Any +from typing import TYPE_CHECKING, Any from memos.embedders.base import BaseEmbedder from memos.llms.base import BaseLLM @@ -18,6 +18,10 @@ from .utils import detect_lang +if TYPE_CHECKING: + from memos.types.general_types import UserContext + + logger = get_logger(__name__) @@ -281,6 +285,11 @@ def parse_fast( user_id = info_.pop("user_id", "") session_id = info_.pop("session_id", "") + # Extract manager_user_id and project_id from user_context + user_context: UserContext | None = kwargs.get("user_context") + manager_user_id = user_context.manager_user_id if user_context else None + project_id = user_context.project_id if user_context else None + # Create memory item (equivalent to _make_memory_item) memory_item = TextualMemoryItem( memory=line, @@ -298,6 +307,8 @@ def parse_fast( confidence=0.99, type="fact", info=info_, + manager_user_id=manager_user_id, + project_id=project_id, ), ) diff --git a/src/memos/mem_reader/read_multi_modal/file_content_parser.py b/src/memos/mem_reader/read_multi_modal/file_content_parser.py index 9f4ab94c2..0f3f3ef01 100644 --- a/src/memos/mem_reader/read_multi_modal/file_content_parser.py +++ b/src/memos/mem_reader/read_multi_modal/file_content_parser.py @@ -5,7 +5,7 @@ import re import tempfile -from typing import Any +from typing import TYPE_CHECKING, Any from tqdm import tqdm @@ -34,6 +34,10 @@ from memos.types.openai_chat_completion_types import File +if TYPE_CHECKING: + from memos.types.general_types import UserContext + + logger = get_logger(__name__) # Prompt dictionary for doc processing (shared by simple_struct and file_content_parser) @@ -451,6 +455,11 @@ def parse_fast( user_id = info_.pop("user_id", "") session_id = info_.pop("session_id", "") + # Extract manager_user_id and project_id from user_context + user_context: UserContext | None = kwargs.get("user_context") + manager_user_id = user_context.manager_user_id if user_context else None + project_id = user_context.project_id if user_context else None + # For file content parts, default to LongTermMemory # (since we don't have role information at this level) memory_type = "LongTermMemory" @@ -495,6 +504,8 @@ def parse_fast( type="fact", info=info_, file_ids=file_ids, + manager_user_id=manager_user_id, + project_id=project_id, ), ) memory_items.append(memory_item) @@ -527,6 +538,8 @@ def parse_fast( type="fact", info=info_, file_ids=file_ids, + manager_user_id=manager_user_id, + project_id=project_id, ), ) memory_items.append(memory_item) @@ -644,6 +657,12 @@ def parse_fine( info_ = info.copy() user_id = info_.pop("user_id", "") session_id = info_.pop("session_id", "") + + # Extract manager_user_id and project_id from user_context + user_context: UserContext | None = kwargs.get("user_context") + manager_user_id = user_context.manager_user_id if user_context else None + project_id = user_context.project_id if user_context else None + if file_id: info_["file_id"] = file_id file_ids = [file_id] if file_id else [] @@ -702,6 +721,8 @@ def _make_memory_item( type="fact", info=info_, file_ids=file_ids, + manager_user_id=manager_user_id, + project_id=project_id, ), ) diff --git a/src/memos/mem_reader/read_multi_modal/image_parser.py b/src/memos/mem_reader/read_multi_modal/image_parser.py index 9322b9bc9..97400ca26 100644 --- a/src/memos/mem_reader/read_multi_modal/image_parser.py +++ b/src/memos/mem_reader/read_multi_modal/image_parser.py @@ -3,7 +3,7 @@ import json import re -from typing import Any +from typing import TYPE_CHECKING, Any from memos.embedders.base import BaseEmbedder from memos.llms.base import BaseLLM @@ -20,6 +20,10 @@ from .utils import detect_lang +if TYPE_CHECKING: + from memos.types.general_types import UserContext + + logger = get_logger(__name__) @@ -212,6 +216,7 @@ def parse_fine( key=_derive_key(summary), sources=[source], background=summary, + **kwargs, ) ) return memory_items @@ -252,6 +257,7 @@ def parse_fine( key=key if key else _derive_key(value), sources=[source], background=background, + **kwargs, ) memory_items.append(memory_item) except Exception as e: @@ -273,6 +279,7 @@ def parse_fine( key=_derive_key(fallback_value), sources=[source], background="Image processing encountered an error.", + **kwargs, ) ] @@ -333,12 +340,18 @@ def _create_memory_item( key: str, sources: list[SourceMessage], background: str = "", + **kwargs, ) -> TextualMemoryItem: """Create a TextualMemoryItem with the given parameters.""" info_ = info.copy() user_id = info_.pop("user_id", "") session_id = info_.pop("session_id", "") + # Extract manager_user_id and project_id from user_context + user_context: UserContext | None = kwargs.get("user_context") + manager_user_id = user_context.manager_user_id if user_context else None + project_id = user_context.project_id if user_context else None + return TextualMemoryItem( memory=value, metadata=TreeNodeTextualMemoryMetadata( @@ -355,5 +368,7 @@ def _create_memory_item( confidence=0.99, type="fact", info=info_, + manager_user_id=manager_user_id, + project_id=project_id, ), ) diff --git a/src/memos/mem_reader/read_multi_modal/string_parser.py b/src/memos/mem_reader/read_multi_modal/string_parser.py index b6e18fda3..220cf6e58 100644 --- a/src/memos/mem_reader/read_multi_modal/string_parser.py +++ b/src/memos/mem_reader/read_multi_modal/string_parser.py @@ -3,7 +3,7 @@ Handles simple string messages that need to be converted to memory items. """ -from typing import Any +from typing import TYPE_CHECKING, Any from memos.embedders.base import BaseEmbedder from memos.llms.base import BaseLLM @@ -17,6 +17,10 @@ from .base import BaseMessageParser, _add_lang_to_source, _derive_key +if TYPE_CHECKING: + from memos.types.general_types import UserContext + + logger = get_logger(__name__) @@ -92,6 +96,11 @@ def parse_fast( user_id = info_.pop("user_id", "") session_id = info_.pop("session_id", "") + # Extract manager_user_id and project_id from user_context + user_context: UserContext | None = kwargs.get("user_context") + manager_user_id = user_context.manager_user_id if user_context else None + project_id = user_context.project_id if user_context else None + # For string messages, default to LongTermMemory memory_type = "LongTermMemory" @@ -120,6 +129,8 @@ def parse_fast( confidence=0.99, type="fact", info=info_, + manager_user_id=manager_user_id, + project_id=project_id, ), ) memory_items.append(memory_item) diff --git a/src/memos/mem_reader/read_multi_modal/system_parser.py b/src/memos/mem_reader/read_multi_modal/system_parser.py index 03a49afd8..74545ceee 100644 --- a/src/memos/mem_reader/read_multi_modal/system_parser.py +++ b/src/memos/mem_reader/read_multi_modal/system_parser.py @@ -6,7 +6,7 @@ import re import uuid -from typing import Any +from typing import TYPE_CHECKING, Any from memos.embedders.base import BaseEmbedder from memos.llms.base import BaseLLM @@ -21,6 +21,10 @@ from .base import BaseMessageParser, _add_lang_to_source +if TYPE_CHECKING: + from memos.types.general_types import UserContext + + logger = get_logger(__name__) @@ -242,6 +246,11 @@ def format_tool_schema_readable(tool_schema): user_id = info_.pop("user_id", "") session_id = info_.pop("session_id", "") + # Extract manager_user_id and project_id from user_context + user_context: UserContext | None = kwargs.get("user_context") + manager_user_id = user_context.manager_user_id if user_context else None + project_id = user_context.project_id if user_context else None + # Split parsed text into chunks content_chunks = self._split_text(msg_line) @@ -260,6 +269,8 @@ def format_tool_schema_readable(tool_schema): tags=["mode:fast"], sources=[source], info=info_, + manager_user_id=manager_user_id, + project_id=project_id, ), ) memory_items.append(memory_item) @@ -294,6 +305,11 @@ def parse_fine( user_id = info_.pop("user_id", "") session_id = info_.pop("session_id", "") + # Extract manager_user_id and project_id from user_context + user_context: UserContext | None = kwargs.get("user_context") + manager_user_id = user_context.manager_user_id if user_context else None + project_id = user_context.project_id if user_context else None + # Deduplicate tool schemas based on memory content # Use hash as key for efficiency, but store original string to handle collisions seen_memories = {} # hash -> memory_str mapping @@ -321,6 +337,8 @@ def parse_fine( status="activated", embedding=self.embedder.embed([json.dumps(schema, ensure_ascii=False)])[0], info=info_, + manager_user_id=manager_user_id, + project_id=project_id, ), ) for schema in unique_schemas diff --git a/src/memos/mem_reader/read_multi_modal/text_content_parser.py b/src/memos/mem_reader/read_multi_modal/text_content_parser.py index 549f74852..9fdcf8c58 100644 --- a/src/memos/mem_reader/read_multi_modal/text_content_parser.py +++ b/src/memos/mem_reader/read_multi_modal/text_content_parser.py @@ -4,7 +4,7 @@ Text content parts are typically used in user/assistant messages with multimodal content. """ -from typing import Any +from typing import TYPE_CHECKING, Any from memos.embedders.base import BaseEmbedder from memos.llms.base import BaseLLM @@ -19,6 +19,10 @@ from .base import BaseMessageParser, _add_lang_to_source, _derive_key +if TYPE_CHECKING: + from memos.types.general_types import UserContext + + logger = get_logger(__name__) @@ -92,6 +96,11 @@ def parse_fast( user_id = info_.pop("user_id", "") session_id = info_.pop("session_id", "") + # Extract manager_user_id and project_id from user_context + user_context: UserContext | None = kwargs.get("user_context") + manager_user_id = user_context.manager_user_id if user_context else None + project_id = user_context.project_id if user_context else None + # For text content parts, default to LongTermMemory # (since we don't have role information at this level) memory_type = "LongTermMemory" @@ -113,6 +122,8 @@ def parse_fast( confidence=0.99, type="fact", info=info_, + manager_user_id=manager_user_id, + project_id=project_id, ), ) diff --git a/src/memos/mem_reader/read_multi_modal/tool_parser.py b/src/memos/mem_reader/read_multi_modal/tool_parser.py index caf5ffaa6..4718f87ba 100644 --- a/src/memos/mem_reader/read_multi_modal/tool_parser.py +++ b/src/memos/mem_reader/read_multi_modal/tool_parser.py @@ -2,7 +2,7 @@ import json -from typing import Any +from typing import TYPE_CHECKING, Any from memos.embedders.base import BaseEmbedder from memos.llms.base import BaseLLM @@ -18,6 +18,10 @@ from .utils import detect_lang +if TYPE_CHECKING: + from memos.types.general_types import UserContext + + logger = get_logger(__name__) @@ -179,6 +183,11 @@ def parse_fast( user_id = info_.pop("user_id", "") session_id = info_.pop("session_id", "") + # Extract manager_user_id and project_id from user_context + user_context: UserContext | None = kwargs.get("user_context") + manager_user_id = user_context.manager_user_id if user_context else None + project_id = user_context.project_id if user_context else None + content_chunks = self._split_text(line) memory_items = [] for _chunk_idx, chunk_text in enumerate(content_chunks): @@ -195,6 +204,8 @@ def parse_fast( tags=["mode:fast"], sources=sources, info=info_, + manager_user_id=manager_user_id, + project_id=project_id, ), ) memory_items.append(memory_item) diff --git a/src/memos/mem_reader/read_multi_modal/user_parser.py b/src/memos/mem_reader/read_multi_modal/user_parser.py index 1ab48c82e..abfebc5db 100644 --- a/src/memos/mem_reader/read_multi_modal/user_parser.py +++ b/src/memos/mem_reader/read_multi_modal/user_parser.py @@ -1,6 +1,6 @@ """Parser for user messages.""" -from typing import Any +from typing import TYPE_CHECKING, Any from memos.embedders.base import BaseEmbedder from memos.llms.base import BaseLLM @@ -16,6 +16,10 @@ from .utils import detect_lang +if TYPE_CHECKING: + from memos.types.general_types import UserContext + + logger = get_logger(__name__) @@ -183,6 +187,11 @@ def parse_fast( user_id = info_.pop("user_id", "") session_id = info_.pop("session_id", "") + # Extract manager_user_id and project_id from user_context + user_context: UserContext | None = kwargs.get("user_context") + manager_user_id = user_context.manager_user_id if user_context else None + project_id = user_context.project_id if user_context else None + # Create memory item (equivalent to _make_memory_item) memory_item = TextualMemoryItem( memory=line, @@ -200,6 +209,8 @@ def parse_fast( confidence=0.99, type="fact", info=info_, + manager_user_id=manager_user_id, + project_id=project_id, ), ) 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 6bd18808d..7bd3f3ebb 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 @@ -7,7 +7,7 @@ from concurrent.futures import as_completed from datetime import datetime from pathlib import Path -from typing import Any +from typing import TYPE_CHECKING, Any from memos.context.context import ContextThreadPoolExecutor from memos.dependency import require_python_package @@ -29,6 +29,10 @@ from memos.types import MessageList +if TYPE_CHECKING: + from memos.types.general_types import UserContext + + logger = get_logger(__name__) @@ -494,12 +498,20 @@ def _write_skills_to_file( def create_skill_memory_item( - skill_memory: dict[str, Any], info: dict[str, Any], embedder: BaseEmbedder | None = None + skill_memory: dict[str, Any], + info: dict[str, Any], + embedder: BaseEmbedder | None = None, + **kwargs: Any, ) -> TextualMemoryItem: info_ = info.copy() user_id = info_.pop("user_id", "") session_id = info_.pop("session_id", "") + # Extract manager_user_id and project_id from user_context + user_context: UserContext | None = kwargs.get("user_context") + manager_user_id = user_context.manager_user_id if user_context else None + project_id = user_context.project_id if user_context else None + # Use description as the memory content memory_content = skill_memory.get("description", "") @@ -530,6 +542,8 @@ def create_skill_memory_item( scripts=skill_memory.get("scripts"), others=skill_memory.get("others"), url=skill_memory.get("url", ""), + manager_user_id=manager_user_id, + project_id=project_id, ) # If this is an update, use the old memory ID @@ -748,7 +762,7 @@ def process_skill_memory_fine( skill_memory_items = [] for skill_memory in skill_memories: try: - memory_item = create_skill_memory_item(skill_memory, info, embedder) + memory_item = create_skill_memory_item(skill_memory, info, embedder, **kwargs) skill_memory_items.append(memory_item) except Exception as e: logger.warning(f"[PROCESS_SKILLS] Error creating skill memory item: {e}") diff --git a/src/memos/mem_scheduler/general_scheduler.py b/src/memos/mem_scheduler/general_scheduler.py index 74e50a514..9893a02e2 100644 --- a/src/memos/mem_scheduler/general_scheduler.py +++ b/src/memos/mem_scheduler/general_scheduler.py @@ -41,6 +41,7 @@ MemCubeID, UserID, ) +from memos.types.general_types import UserContext logger = get_logger(__name__) @@ -765,6 +766,7 @@ def process_message(message: ScheduleMessageItem): user_name = message.user_name info = message.info or {} chat_history = message.chat_history + user_context = message.user_context # Parse the memory IDs from content mem_ids = json.loads(content) if isinstance(content, str) else content @@ -792,6 +794,7 @@ def process_message(message: ScheduleMessageItem): task_id=message.task_id, info=info, chat_history=chat_history, + user_context=user_context, ) logger.info( @@ -820,6 +823,7 @@ def _process_memories_with_reader( task_id: str | None = None, info: dict | None = None, chat_history: list | None = None, + user_context: UserContext | None = None, ) -> None: logger.info( f"[DIAGNOSTIC] general_scheduler._process_memories_with_reader called. mem_ids: {mem_ids}, user_id: {user_id}, mem_cube_id: {mem_cube_id}, task_id: {task_id}" @@ -882,6 +886,7 @@ def _process_memories_with_reader( custom_tags=custom_tags, user_name=user_name, chat_history=chat_history, + user_context=user_context, ) except Exception as e: logger.warning(f"{e}: Fail to transfer mem: {memory_items}") diff --git a/src/memos/mem_scheduler/schemas/message_schemas.py b/src/memos/mem_scheduler/schemas/message_schemas.py index c7f270f19..d7ef0ea24 100644 --- a/src/memos/mem_scheduler/schemas/message_schemas.py +++ b/src/memos/mem_scheduler/schemas/message_schemas.py @@ -9,6 +9,7 @@ from memos.log import get_logger from memos.mem_scheduler.general_modules.misc import DictConversionMixin from memos.mem_scheduler.utils.db_utils import get_utc_now +from memos.types.general_types import UserContext from .general_schemas import NOT_INITIALIZED @@ -55,6 +56,7 @@ class ScheduleMessageItem(BaseModel, DictConversionMixin): description="Optional business-level task ID. Multiple items can share the same task_id.", ) chat_history: list | None = Field(default=None, description="user chat history") + user_context: UserContext | None = Field(default=None, description="user context") # Pydantic V2 model configuration model_config = ConfigDict( @@ -91,6 +93,9 @@ def to_dict(self) -> dict: "user_name": self.user_name, "task_id": self.task_id if self.task_id is not None else "", "chat_history": self.chat_history if self.chat_history is not None else [], + "user_context": self.user_context.model_dump(exclude_none=True) + if self.user_context + else None, } @classmethod @@ -107,6 +112,9 @@ def from_dict(cls, data: dict) -> "ScheduleMessageItem": user_name=data.get("user_name"), task_id=data.get("task_id"), chat_history=data.get("chat_history"), + user_context=UserContext.model_validate(data.get("user_context")) + if data.get("user_context") + else None, ) diff --git a/src/memos/memories/textual/tree.py b/src/memos/memories/textual/tree.py index ea3d536c4..0042613d5 100644 --- a/src/memos/memories/textual/tree.py +++ b/src/memos/memories/textual/tree.py @@ -359,7 +359,7 @@ def get_by_ids( def get_all( self, - user_name: str, + user_name: str | None = None, user_id: str | None = None, page: int | None = None, page_size: int | None = None, diff --git a/src/memos/multi_mem_cube/single_cube.py b/src/memos/multi_mem_cube/single_cube.py index bd026a51d..6fbf64947 100644 --- a/src/memos/multi_mem_cube/single_cube.py +++ b/src/memos/multi_mem_cube/single_cube.py @@ -72,6 +72,8 @@ def add_memories(self, add_req: APIADDRequest) -> list[dict[str, Any]]: user_id=add_req.user_id, mem_cube_id=self.cube_id, session_id=add_req.session_id or "default_session", + manager_user_id=add_req.manager_user_id, + project_id=add_req.project_id, ) target_session_id = add_req.session_id or "default_session" @@ -554,6 +556,7 @@ def _schedule_memory_tasks( user_name=self.cube_id, info=add_req.info, chat_history=add_req.chat_history, + user_context=user_context, ) self.mem_scheduler.submit_messages(messages=[message_item_read]) self.logger.info( @@ -809,6 +812,7 @@ def _process_text_mem( mode=extract_mode, user_name=user_context.mem_cube_id, chat_history=add_req.chat_history, + user_context=user_context, ) self.logger.info( f"Time for get_memory in extract mode {extract_mode}: {time.time() - init_time}" diff --git a/src/memos/types/general_types.py b/src/memos/types/general_types.py index 44c75ec02..8234caf8b 100644 --- a/src/memos/types/general_types.py +++ b/src/memos/types/general_types.py @@ -10,7 +10,7 @@ from enum import Enum from typing import Literal, NewType, TypeAlias -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict from typing_extensions import TypedDict from memos.memories.activation.item import ActivationMemoryItem @@ -149,3 +149,7 @@ class UserContext(BaseModel): mem_cube_id: str | None = None session_id: str | None = None operation: list[PermissionDict] | None = None + manager_user_id: str | None = None + project_id: str | None = None + + model_config = ConfigDict(extra="allow") From f97b7e2201e5fd27c238b6c284f3ad8e3527ef03 Mon Sep 17 00:00:00 2001 From: "yuan.wang" Date: Fri, 6 Feb 2026 10:29:11 +0800 Subject: [PATCH 28/35] fix: fix name error in polardb and related code --- src/memos/graph_dbs/polardb.py | 16 ++++++++++------ src/memos/mem_feedback/feedback.py | 4 ++-- .../tree_text_memory/retrieve/pre_update.py | 8 ++++---- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/memos/graph_dbs/polardb.py b/src/memos/graph_dbs/polardb.py index 094235831..faa36a321 100644 --- a/src/memos/graph_dbs/polardb.py +++ b/src/memos/graph_dbs/polardb.py @@ -1691,7 +1691,7 @@ def get_context_chain(self, id: str, type: str = "FOLLOWS") -> list[str]: raise NotImplementedError @timed - def seach_by_keywords_like( + def search_by_keywords_like( self, query_word: str, scope: str | None = None, @@ -1761,7 +1761,7 @@ def seach_by_keywords_like( params = (query_word,) logger.info( - f"[seach_by_keywords_LIKE start:] user_name: {user_name}, query: {query}, params: {params}" + f"[search_by_keywords_LIKE start:] user_name: {user_name}, query: {query}, params: {params}" ) conn = None try: @@ -1775,14 +1775,14 @@ def seach_by_keywords_like( id_val = str(oldid) output.append({"id": id_val}) logger.info( - f"[seach_by_keywords_LIKE end:] user_name: {user_name}, query: {query}, params: {params} recalled: {output}" + f"[search_by_keywords_LIKE end:] user_name: {user_name}, query: {query}, params: {params} recalled: {output}" ) return output finally: self._return_connection(conn) @timed - def seach_by_keywords_tfidf( + def search_by_keywords_tfidf( self, query_words: list[str], scope: str | None = None, @@ -1858,7 +1858,7 @@ def seach_by_keywords_tfidf( params = (tsquery_string,) logger.info( - f"[seach_by_keywords_TFIDF start:] user_name: {user_name}, query: {query}, params: {params}" + f"[search_by_keywords_TFIDF start:] user_name: {user_name}, query: {query}, params: {params}" ) conn = None try: @@ -1870,10 +1870,12 @@ def seach_by_keywords_tfidf( for row in results: oldid = row[0] id_val = str(oldid) + if id_val.startswith('"') and id_val.endswith('"'): + id_val = id_val[1:-1] output.append({"id": id_val}) logger.info( - f"[seach_by_keywords_TFIDF end:] user_name: {user_name}, query: {query}, params: {params} recalled: {output}" + f"[search_by_keywords_TFIDF end:] user_name: {user_name}, query: {query}, params: {params} recalled: {output}" ) return output finally: @@ -2003,6 +2005,8 @@ def search_by_fulltext( rank = row[2] # rank score id_val = str(oldid) + if id_val.startswith('"') and id_val.endswith('"'): + id_val = id_val[1:-1] score_val = float(rank) # Apply threshold filter if specified diff --git a/src/memos/mem_feedback/feedback.py b/src/memos/mem_feedback/feedback.py index e38318a64..6e24ca7a5 100644 --- a/src/memos/mem_feedback/feedback.py +++ b/src/memos/mem_feedback/feedback.py @@ -924,7 +924,7 @@ def process_keyword_replace( ) must_part = f"{' & '.join(queries)}" if len(queries) > 1 else queries[0] - retrieved_ids = self.graph_store.seach_by_keywords_tfidf( + retrieved_ids = self.graph_store.search_by_keywords_tfidf( [must_part], user_name=user_name, filter=filter_dict ) if len(retrieved_ids) < 1: @@ -932,7 +932,7 @@ def process_keyword_replace( queries, top_k=100, user_name=user_name, filter=filter_dict ) else: - retrieved_ids = self.graph_store.seach_by_keywords_like( + retrieved_ids = self.graph_store.search_by_keywords_like( f"%{original_word}%", user_name=user_name, filter=filter_dict ) diff --git a/src/memos/memories/textual/tree_text_memory/retrieve/pre_update.py b/src/memos/memories/textual/tree_text_memory/retrieve/pre_update.py index a5fc7e049..cb77d2243 100644 --- a/src/memos/memories/textual/tree_text_memory/retrieve/pre_update.py +++ b/src/memos/memories/textual/tree_text_memory/retrieve/pre_update.py @@ -163,14 +163,14 @@ def keyword_search( results = [] - # 2. Try seach_by_keywords_tfidf (PolarDB specific) - if hasattr(self.graph_db, "seach_by_keywords_tfidf"): + # 2. Try search_by_keywords_tfidf (PolarDB specific) + if hasattr(self.graph_db, "search_by_keywords_tfidf"): try: - results = self.graph_db.seach_by_keywords_tfidf( + results = self.graph_db.search_by_keywords_tfidf( query_words=keywords, user_name=user_name, filter=search_filter ) except Exception as e: - logger.warning(f"[PreUpdateRetriever] seach_by_keywords_tfidf failed: {e}") + logger.warning(f"[PreUpdateRetriever] search_by_keywords_tfidf failed: {e}") # 3. Fallback to search_by_fulltext if not results and hasattr(self.graph_db, "search_by_fulltext"): From fc105472cb4824447104fac1fad3399e50a6db41 Mon Sep 17 00:00:00 2001 From: "yuan.wang" Date: Fri, 6 Feb 2026 11:08:19 +0800 Subject: [PATCH 29/35] fix: bug in polardb --- src/memos/graph_dbs/polardb.py | 2 ++ src/memos/mem_scheduler/general_scheduler.py | 2 ++ .../memories/textual/prefer_text_memory/extractor.py | 11 ++++++++++- src/memos/memories/textual/preference.py | 4 ++-- src/memos/memories/textual/simple_preference.py | 5 +++-- src/memos/multi_mem_cube/single_cube.py | 2 ++ 6 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/memos/graph_dbs/polardb.py b/src/memos/graph_dbs/polardb.py index faa36a321..98fbc6e66 100644 --- a/src/memos/graph_dbs/polardb.py +++ b/src/memos/graph_dbs/polardb.py @@ -1773,6 +1773,8 @@ def search_by_keywords_like( for row in results: oldid = row[0] id_val = str(oldid) + if id_val.startswith('"') and id_val.endswith('"'): + id_val = id_val[1:-1] output.append({"id": id_val}) logger.info( f"[search_by_keywords_LIKE end:] user_name: {user_name}, query: {query}, params: {params} recalled: {output}" diff --git a/src/memos/mem_scheduler/general_scheduler.py b/src/memos/mem_scheduler/general_scheduler.py index 9893a02e2..44bea3dec 100644 --- a/src/memos/mem_scheduler/general_scheduler.py +++ b/src/memos/mem_scheduler/general_scheduler.py @@ -1345,6 +1345,7 @@ def process_message(message: ScheduleMessageItem): mem_cube_id = message.mem_cube_id content = message.content messages_list = json.loads(content) + user_context = message.user_context info = message.info or {} logger.info(f"Processing pref_add for user_id={user_id}, mem_cube_id={mem_cube_id}") @@ -1374,6 +1375,7 @@ def process_message(message: ScheduleMessageItem): "session_id": session_id, "mem_cube_id": mem_cube_id, }, + user_context=user_context, ) # Add pref_mem to vector db pref_ids = pref_mem.add(pref_memories) diff --git a/src/memos/memories/textual/prefer_text_memory/extractor.py b/src/memos/memories/textual/prefer_text_memory/extractor.py index aa4f3cb44..e696e82d4 100644 --- a/src/memos/memories/textual/prefer_text_memory/extractor.py +++ b/src/memos/memories/textual/prefer_text_memory/extractor.py @@ -4,7 +4,7 @@ from abc import ABC, abstractmethod from concurrent.futures import as_completed from datetime import datetime -from typing import Any +from typing import TYPE_CHECKING, Any from memos.context.context import ContextThreadPoolExecutor from memos.log import get_logger @@ -25,6 +25,10 @@ from memos.types import MessageList +if TYPE_CHECKING: + from memos.types.general_types import UserContext + + logger = get_logger(__name__) @@ -177,6 +181,7 @@ def extract( msg_type: str, info: dict[str, Any], max_workers: int = 10, + **kwargs, ) -> list[TextualMemoryItem]: """Extract preference memories based on the messages using thread pool for acceleration.""" chunks: list[MessageList] = [] @@ -186,6 +191,10 @@ def extract( if not chunks: return [] + user_context: UserContext | None = kwargs.get("user_context") + user_context_dict = user_context.model_dump() if user_context else {} + info = {**info, **user_context_dict} + memories = [] with ContextThreadPoolExecutor(max_workers=min(max_workers, len(chunks))) as executor: futures = { diff --git a/src/memos/memories/textual/preference.py b/src/memos/memories/textual/preference.py index 78f4d6e28..dba321f55 100644 --- a/src/memos/memories/textual/preference.py +++ b/src/memos/memories/textual/preference.py @@ -67,7 +67,7 @@ def __init__(self, config: PreferenceTextMemoryConfig): ) def get_memory( - self, messages: list[MessageList], type: str, info: dict[str, Any] + self, messages: list[MessageList], type: str, info: dict[str, Any], **kwargs ) -> list[TextualMemoryItem]: """Get memory based on the messages. Args: @@ -75,7 +75,7 @@ def get_memory( type (str): The type of memory to get. info (dict[str, Any]): The info to get memory. """ - return self.extractor.extract(messages, type, info) + return self.extractor.extract(messages, type, info, **kwargs) def search( self, query: str, top_k: int, info=None, search_filter=None, **kwargs diff --git a/src/memos/memories/textual/simple_preference.py b/src/memos/memories/textual/simple_preference.py index cc1781f06..db7101744 100644 --- a/src/memos/memories/textual/simple_preference.py +++ b/src/memos/memories/textual/simple_preference.py @@ -40,15 +40,16 @@ def __init__( self.retriever = retriever def get_memory( - self, messages: list[MessageList], type: str, info: dict[str, Any] + self, messages: list[MessageList], type: str, info: dict[str, Any], **kwargs ) -> list[TextualMemoryItem]: """Get memory based on the messages. Args: messages (MessageList): The messages to get memory from. type (str): The type of memory to get. info (dict[str, Any]): The info to get memory. + **kwargs: Additional keyword arguments to pass to the extractor. """ - return self.extractor.extract(messages, type, info) + return self.extractor.extract(messages, type, info, **kwargs) def search( self, query: str, top_k: int, info=None, search_filter=None, **kwargs diff --git a/src/memos/multi_mem_cube/single_cube.py b/src/memos/multi_mem_cube/single_cube.py index 6fbf64947..9e9bf72a0 100644 --- a/src/memos/multi_mem_cube/single_cube.py +++ b/src/memos/multi_mem_cube/single_cube.py @@ -627,6 +627,7 @@ def _process_pref_mem( info=add_req.info, user_name=self.cube_id, task_id=add_req.task_id, + user_context=user_context, ) self.mem_scheduler.submit_messages(messages=[message_item_pref]) self.logger.info(f"[SingleCubeView] cube={self.cube_id} Submitted PREF_ADD async") @@ -646,6 +647,7 @@ def _process_pref_mem( "session_id": target_session_id, "mem_cube_id": user_context.mem_cube_id, }, + user_context=user_context, ) pref_ids_local: list[str] = self.naive_mem_cube.pref_mem.add(pref_memories_local) self.logger.info( From 74a8f9f226bdb85407d5cb4673bf5c8c7f4e3974 Mon Sep 17 00:00:00 2001 From: "yuan.wang" Date: Fri, 6 Feb 2026 16:47:23 +0800 Subject: [PATCH 30/35] feat: optimize skill --- .../read_skill_memory/process_skill_memory.py | 410 +++++++++++++++++- src/memos/templates/skill_mem_prompt.py | 298 +++++++++++-- 2 files changed, 659 insertions(+), 49 deletions(-) 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 7bd3f3ebb..3ac45c99a 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 @@ -1,3 +1,4 @@ +import copy import json import os import shutil @@ -19,12 +20,18 @@ from memos.memories.textual.item import TextualMemoryItem, TreeNodeTextualMemoryMetadata from memos.memories.textual.tree_text_memory.retrieve.searcher import Searcher from memos.templates.skill_mem_prompt import ( + OTHERS_GENERATION_PROMPT, + OTHERS_GENERATION_PROMPT_ZH, + SCRIPT_GENERATION_PROMPT, SKILL_MEMORY_EXTRACTION_PROMPT, + SKILL_MEMORY_EXTRACTION_PROMPT_MD, + SKILL_MEMORY_EXTRACTION_PROMPT_MD_ZH, SKILL_MEMORY_EXTRACTION_PROMPT_ZH, TASK_CHUNKING_PROMPT, TASK_CHUNKING_PROMPT_ZH, TASK_QUERY_REWRITE_PROMPT, TASK_QUERY_REWRITE_PROMPT_ZH, + TOOL_GENERATION_PROMPT, ) from memos.types import MessageList @@ -36,6 +43,200 @@ logger = get_logger(__name__) +def _generate_content_by_llm(llm: BaseLLM, prompt_template: str, **kwargs) -> Any: + """Generate content using LLM.""" + try: + prompt = prompt_template.format(**kwargs) + response = llm.generate([{"role": "user", "content": prompt}]) + if "json" in prompt_template.lower(): + response = response.replace("```json", "").replace("```", "").strip() + return json.loads(response) + return response.strip() + except Exception as e: + logger.warning(f"[PROCESS_SKILLS] LLM generation failed: {e}") + return {} if "json" in prompt_template.lower() else "" + + +def _batch_extract_skills( + task_chunks: dict[str, MessageList], + related_memories_map: dict[str, list[TextualMemoryItem]], + llm: BaseLLM, + chat_history: MessageList, +) -> list[tuple[dict[str, Any], str, MessageList]]: + """Phase 1: Batch extract base skill structures from all task chunks in parallel.""" + results = [] + with ContextThreadPoolExecutor(max_workers=min(5, len(task_chunks))) as executor: + futures = { + executor.submit( + _extract_skill_memory_by_llm_md, + messages=messages, + old_memories=related_memories_map.get(task_type, []), + llm=llm, + chat_history=chat_history, + ): task_type + for task_type, messages in task_chunks.items() + } + + for future in as_completed(futures): + task_type = futures[future] + try: + skill_memory = future.result() + if skill_memory: + results.append((skill_memory, task_type, task_chunks.get(task_type, []))) + except Exception as e: + logger.warning( + f"[PROCESS_SKILLS] Error extracting skill memory for task '{task_type}': {e}" + ) + return results + + +def _batch_generate_skill_details( + raw_skills_data: list[tuple[dict[str, Any], str, MessageList]], + related_skill_memories_map: dict[str, list[TextualMemoryItem]], + llm: BaseLLM, +) -> list[dict[str, Any]]: + """Phase 2: Batch generate details (scripts, tools, others, examples) for all skills in parallel.""" + generation_tasks = [] + + # Helper to create task objects + def create_task(skill_mem, gen_type, prompt, requirements, context, **kwargs): + return { + "type": gen_type, + "skill_memory": skill_mem, + "func": _generate_content_by_llm, + "args": (llm, prompt), + "kwargs": {"requirements": requirements, "context": context, **kwargs}, + } + + # 1. Collect all generation tasks from all skills + for skill_memory, task_type, messages in raw_skills_data: + messages_context = "\n".join([f"{msg['role']}: {msg['content']}" for msg in messages]) + + # Script + script_req = copy.deepcopy(skill_memory.get("scripts")) + if script_req: + generation_tasks.append( + create_task( + skill_memory, "scripts", SCRIPT_GENERATION_PROMPT, script_req, messages_context + ) + ) + # TODO Add loop verification after code completion to ensure the generated script meets requirements + else: + skill_memory["scripts"] = {} + + # Tool + tool_req = skill_memory.get("tool") + if tool_req: + # Extract available tool schemas from related memories + tool_memories = [ + memory + for memory in related_skill_memories_map.get(task_type, []) + if memory.metadata.memory_type == "ToolSchemaMemory" + ] + tool_schemas_list = [memory.memory for memory in tool_memories] + + tool_schemas_str = ( + "\n\n".join( + [ + f"Tool Schema {i + 1}:\n{schema}" + for i, schema in enumerate(tool_schemas_list) + ] + ) + if tool_schemas_list + else "No specific tool schemas available." + ) + + generation_tasks.append( + create_task( + skill_memory, + "tool", + TOOL_GENERATION_PROMPT, + tool_req, + messages_context, + tool_schemas=tool_schemas_str, + ) + ) + else: + skill_memory["tool"] = {} + + lang = detect_lang(messages_context) + others_req = skill_memory.get("others") + if others_req and isinstance(others_req, dict): + for filename, summary in others_req.items(): + generation_tasks.append( + { + "type": "others", + "skill_memory": skill_memory, + "key": filename, + "func": _generate_content_by_llm, + "args": ( + llm, + OTHERS_GENERATION_PROMPT_ZH + if lang == "zh" + else OTHERS_GENERATION_PROMPT, + ), + "kwargs": { + "filename": filename, + "summary": summary, + "context": messages_context, + }, + } + ) + else: + skill_memory["others"] = {} + + if not generation_tasks: + return [item[0] for item in raw_skills_data] + + # 2. Execute all tasks in parallel + with ContextThreadPoolExecutor(max_workers=min(len(generation_tasks), 5)) as executor: + futures = { + executor.submit(t["func"], *t["args"], **t["kwargs"]): t for t in generation_tasks + } + + for future in as_completed(futures): + task_info = futures[future] + try: + result = future.result() + if not result: + continue + + skill_mem = task_info["skill_memory"] + + if task_info["type"] == "scripts": + if isinstance(result, dict): + # Combine code with script_req + try: + skill_mem["scripts"] = { + filename: f"# {abstract}:\n{code}" + for abstract, (filename, code) in zip( + script_req, result.items(), strict=False + ) + } + except ValueError: + logger.warning( + f"[PROCESS_SKILLS] Invalid script generation result: {result}" + ) + skill_mem["scripts"] = {} + + elif task_info["type"] == "tool": + skill_mem["tool"] = result + + elif task_info["type"] == "others": + if "others" not in skill_mem or not isinstance(skill_mem["others"], dict): + skill_mem["others"] = {} + skill_mem["others"][task_info["key"]] = ( + f"# {task_info['kwargs']['summary']}\n{result}" + ) + + except Exception as e: + logger.warning( + f"[PROCESS_SKILLS] Error in generation task {task_info['type']}: {e}" + ) + + return [item[0] for item in raw_skills_data] + + def add_id_to_mysql(memory_id: str, mem_cube_id: str): """Add id to mysql, will deprecate this function in the future""" # TODO: tmp function, deprecate soon @@ -267,6 +468,125 @@ def _extract_skill_memory_by_llm( return None +def _extract_skill_memory_by_llm_md( + messages: MessageList, + old_memories: list[TextualMemoryItem], + llm: BaseLLM, + chat_history: MessageList, + chat_history_max_length: int = 5000, +) -> dict[str, Any]: + old_memories_dict = [memory.model_dump() for memory in old_memories] + old_memories_context = {} + old_skill_content = [] + seen_messages = set() + + for mem in old_memories_dict: + if mem["metadata"]["memory_type"] == "SkillMemory" and mem["metadata"]["relativity"] > 0.02: + old_skill_content.append( + { + "id": mem["id"], + "name": mem["metadata"]["name"], + "description": mem["metadata"]["description"], + "procedure": mem["metadata"]["procedure"], + "experience": mem["metadata"]["experience"], + "preference": mem["metadata"]["preference"], + "examples": mem["metadata"]["examples"], + "others": mem["metadata"].get("others"), # TODO: maybe remove, too long + } + ) + else: + # Filter and deduplicate messages + unique_messages = [] + for item in mem["metadata"]["sources"]: + message_content = f"{item['role']}: {item['content']}" + if message_content not in seen_messages: + seen_messages.add(message_content) + unique_messages.append(message_content) + + if unique_messages: + old_memories_context.setdefault(mem["metadata"]["memory_type"], []).extend( + unique_messages + ) + + # Prepare current conversation context + messages_context = "\n".join( + [f"{message['role']}: {message['content']}" for message in messages] + ) + + # Prepare history context + chat_history_context = "\n".join( + [f"{history['role']}: {history['content']}" for history in chat_history] + ) + chat_history_context = chat_history_context[-chat_history_max_length:] + + # Prepare old memories context + old_skill_content = ( + ("Exsit Skill Schemas: \n" + json.dumps(old_skill_content, ensure_ascii=False, indent=2)) + if old_skill_content + else "" + ) + + old_memories_context = "Relavant Context:\n" + "\n".join( + [f"{k}:\n{v}" for k, v in old_memories_context.items()] + ) + + # Prepare prompt + lang = detect_lang(messages_context) + template = ( + SKILL_MEMORY_EXTRACTION_PROMPT_MD_ZH if lang == "zh" else SKILL_MEMORY_EXTRACTION_PROMPT_MD + ) + prompt_content = ( + template.replace("{old_memories}", old_memories_context + old_skill_content) + .replace("{messages}", messages_context) + .replace("{chat_history}", chat_history_context) + ) + + prompt = [{"role": "user", "content": prompt_content}] + logger.info(f"[Skill Memory]: _extract_skill_memory_by_llm_md: Prompt {prompt_content}") + + # Call LLM to extract skill memory with retry logic + for attempt in range(3): + try: + # Only pass model_name_or_path if SKILLS_LLM is set + 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) + # Clean up response (remove Markdown code blocks if present) + logger.info(f"[Skill Memory]: response_text {response_text}") + response_text = response_text.strip() + response_text = response_text.replace("```json", "").replace("```", "").strip() + + # Parse JSON response + skill_memory = json.loads(response_text) + + # If LLM returns null (parsed as None), log and return None + if skill_memory is None: + logger.info( + "[PROCESS_SKILLS] No skill memory extracted from conversation (LLM returned null)" + ) + return None + + return skill_memory + + except json.JSONDecodeError as e: + logger.warning(f"[PROCESS_SKILLS] JSON decode failed (attempt {attempt + 1}): {e}") + logger.debug(f"[PROCESS_SKILLS] Response text: {response_text}") + if attempt == 2: + logger.warning("[PROCESS_SKILLS] Failed to parse skill memory after 3 retries") + return None + except Exception as e: + logger.warning( + f"[PROCESS_SKILLS] LLM skill memory extraction failed (attempt {attempt + 1}): {e}" + ) + if attempt == 2: + logger.warning( + "[PROCESS_SKILLS] LLM skill memory extraction failed after 3 retries" + ) + return None + + return None + + def _recall_related_skill_memories( task_type: str, messages: MessageList, @@ -280,7 +600,7 @@ def _recall_related_skill_memories( related_skill_memories = searcher.search( query, top_k=5, - memory_type="SkillMemory", + memory_type="All", info=info, include_skill_memory=True, user_name=mem_cube_id, @@ -392,6 +712,11 @@ def _write_skills_to_file( --- """ + # 加入trigger + trigger = skill_memory.get("trigger", "") + if trigger: + skill_md_content += f"\n## Trigger\n{trigger}\n" + # Add Procedure section only if present procedure = skill_memory.get("procedure", "") if procedure and procedure.strip(): @@ -426,6 +751,10 @@ def _write_skills_to_file( for script_name in scripts: skill_md_content += f"- `./scripts/{script_name}`\n" + tool_usage = skill_memory.get("tool", "") + if tool_usage: + skill_md_content += f"\n## Tool Usage\n{tool_usage}\n" + # Add others - handle both inline content and separate markdown files others = skill_memory.get("others") if others and isinstance(others, dict): @@ -451,7 +780,7 @@ def _write_skills_to_file( skill_md_content += "\n## Additional Information\n" skill_md_content += "\nSee also:\n" for md_filename in md_files: - skill_md_content += f"- [{md_filename}](./{md_filename})\n" + skill_md_content += f"- [{md_filename}](./reference/{md_filename})\n" # Write SKILL.md file skill_md_path = skill_dir / "SKILL.md" @@ -462,7 +791,9 @@ def _write_skills_to_file( if others and isinstance(others, dict): for key, value in others.items(): if key.endswith(".md"): - md_file_path = skill_dir / key + md_file_dir = skill_dir / "reference" + md_file_dir.mkdir(parents=True, exist_ok=True) + md_file_path = md_file_dir / key with open(md_file_path, "w", encoding="utf-8") as f: f.write(value) @@ -521,7 +852,7 @@ def create_skill_memory_item( session_id=session_id, memory_type="SkillMemory", status="activated", - tags=skill_memory.get("tags", []), + tags=skill_memory.get("tags") or skill_memory.get("trigger", []), key=skill_memory.get("name", ""), sources=[], usage=[], @@ -568,6 +899,7 @@ def process_skill_memory_fine( rewrite_query: bool = True, oss_config: dict[str, Any] | None = None, skills_dir_config: dict[str, Any] | None = None, + complete_skill_memory: bool = True, **kwargs, ) -> list[TextualMemoryItem]: # Validate required configurations @@ -641,26 +973,56 @@ def process_skill_memory_fine( ) related_skill_memories_by_task[task_name] = [] - skill_memories = [] - with ContextThreadPoolExecutor(max_workers=5) as executor: - futures = { - executor.submit( - _extract_skill_memory_by_llm, - messages, - related_skill_memories_by_task.get(task_type, []), - llm, - chat_history, - ): task_type - for task_type, messages in task_chunks.items() - } - for future in as_completed(futures): - try: - skill_memory = future.result() - if skill_memory: # Only add non-None results - skill_memories.append(skill_memory) - except Exception as e: - logger.warning(f"[PROCESS_SKILLS] Error extracting skill memory: {e}") - continue + def _simple_extract(): + # simple extract skill memory, only one stage + memories = [] + with ContextThreadPoolExecutor(max_workers=min(5, len(task_chunks))) as executor: + futures = { + executor.submit( + _extract_skill_memory_by_llm, + messages=chunk_messages, + # Filter only SkillMemory types + old_memories=[ + item + for item in related_skill_memories_by_task.get(task_type, []) + if item and getattr(item.metadata, "memory_type", "") == "SkillMemory" + ], + llm=llm, + chat_history=chat_history, + ): task_type + for task_type, chunk_messages in task_chunks.items() + } + + for future in as_completed(futures): + task_type = futures[future] + try: + skill_memory = future.result() + if skill_memory: + memories.append(skill_memory) + except Exception as e: + logger.warning( + f"[PROCESS_SKILLS] _simple_extract: Error processing task '{task_type}': {e}" + ) + return memories + + def _full_extract(): + # full extract skill memory, include two stage + raw_extraction_results = _batch_extract_skills( + task_chunks=task_chunks, + related_memories_map=related_skill_memories_by_task, + llm=llm, + chat_history=chat_history, + ) + if not raw_extraction_results: + return [] + return _batch_generate_skill_details( + raw_skills_data=raw_extraction_results, + related_skill_memories_map=related_skill_memories_by_task, + llm=llm, + ) + + # Execute both parts in parallel + skill_memories = _simple_extract() if not complete_skill_memory else _full_extract() # write skills to file and get zip paths skill_memory_with_paths = [] diff --git a/src/memos/templates/skill_mem_prompt.py b/src/memos/templates/skill_mem_prompt.py index df64d736d..7c60e2147 100644 --- a/src/memos/templates/skill_mem_prompt.py +++ b/src/memos/templates/skill_mem_prompt.py @@ -3,29 +3,29 @@ {{messages}} # Role -You are an expert in natural language processing (NLP) and dialogue logic analysis. You excel at organizing logical threads from complex long conversations and accurately extracting users' core intentions. +You are an expert in natural language processing (NLP) and dialogue logic analysis. You excel at organizing logical threads from complex long conversations and accurately extracting users' core intentions to segment the dialogue into distinct tasks. # Task -Please analyze the provided conversation records, identify all independent "tasks" that the user has asked the AI to perform, and assign the corresponding dialogue message numbers to each task. +Please analyze the provided conversation records, identify all independent "tasks" that the user has asked the AI to perform, and assign the corresponding dialogue message indices to each task. -**Note**: Tasks should be high-level and general, typically divided by theme or topic. For example: "Travel Planning", "PDF Operations", "Code Review", "Data Analysis", etc. Avoid being too specific or granular. +**Note**: Tasks should be high-level and general. Group similar activities under broad themes such as "Travel Planning", "Project Engineering & Implementation", "Code Review", "Data Analysis", etc. Avoid being overly specific or granular. # Rules & Constraints -1. **Task Independence**: If multiple unrelated topics are discussed in the conversation, identify them as different tasks. -2. **Non-continuous Processing**: Pay attention to identifying "jumping" conversations. For example, if the user made travel plans in messages 8-11, switched to consulting about weather in messages 12-22, and then returned to making travel plans in messages 23-24, be sure to assign both 8-11 and 23-24 to the task "Making travel plans". However, if messages are continuous and belong to the same task, do not split them apart. -3. **Filter Chit-chat**: Only extract tasks with clear goals, instructions, or knowledge-based discussions. Ignore meaningless greetings (such as "Hello", "Are you there?") or closing remarks unless they are part of the task context. -4. **Main Task and Subtasks**: Carefully identify whether subtasks serve a main task. If a subtask supports the main task (e.g., "checking weather" serves "travel planning"), do NOT separate it as an independent task. Instead, include all related conversations in the main task. Only split tasks when they are truly independent and unrelated. -5. **Output Format**: Please strictly follow the JSON format for output to facilitate my subsequent processing. -6. **Language Consistency**: The language used in the task_name field must match the language used in the conversation records. -7. **Generic Task Names**: Use generic, reusable task names, not specific descriptions. For example, use "Travel Planning" instead of "Planning a 5-day trip to Chengdu". +1. **Task Independence**: If multiple completely unrelated topics are discussed, identify them as different tasks. +2. **Main Task and Subtasks**: Carefully identify whether a subtask serves a primary objective. If a specific request supports a larger goal (e.g., "checking weather" within a "Travel Planning" thread), do NOT separate it. Include all supporting conversations within the main task. **Only split tasks when they are truly independent and unrelated.** +3. **Non-continuous Processing**: Identify "jumping" or "interleaved" conversations. For example, if the user works on Travel Planning in messages 8-11, switches topics in 12-22, and returns to Travel Planning in 23-24, assign both [8, 11] and [23, 24] to the same "Travel Planning" task. Conversely, if messages are continuous and belong to the same task, keep them as a single range. +4. **Filter Chit-chat**: Only extract tasks with clear goals, instructions, or knowledge-based discussions. Ignore meaningless greetings (e.g., "Hello", "Are you there?") or polite closings unless they contain necessary context for the task. +5. **Output Format**: Strictly follow the JSON format below for automated processing. +6. **Language Consistency**: The language used in the `task_name` field must match the primary language used in the conversation records. +7. **Generic Task Names**: Use broad, reusable task categories. For example, use "Travel Planning" instead of "Planning a 5-day trip to Chengdu". ```json [ { "task_id": 1, - "task_name": "Generic task name (e.g., Travel Planning, Code Review, Data Analysis)", - "message_indices": [[0, 5],[16, 17]], # 0-5 and 16-17 are the message indices for this task - "reasoning": "Briefly explain why these messages are grouped together" + "task_name": "Generic task name (e.g., Travel Planning, Code Review)", + "message_indices": [[0, 5], [16, 17]], + "reasoning": "Briefly explain the logic behind grouping these indices and how they relate to the core intent." }, ... ] @@ -34,31 +34,30 @@ TASK_CHUNKING_PROMPT_ZH = """ -# 上下文(对话记录) +# 上下文(历史对话记录) {{messages}} # 角色 -你是自然语言处理(NLP)和对话逻辑分析的专家。你擅长从复杂的长对话中整理逻辑线索,准确提取用户的核心意图。 +你是自然语言处理(NLP)和对话逻辑分析的专家。你擅长从复杂的长对话中整理逻辑线索,准确提取用户的不同意图,从而按照不同的意图对上述对话进行任务划分。 -# 任务 +# 目标 请分析提供的对话记录,识别所有用户要求 AI 执行的独立"任务",并为每个任务分配相应的对话消息编号。 -**注意**:任务应该是高层次和通用的,通常按主题或话题划分。例如:"旅行计划"、"PDF操作"、"代码审查"、"数据分析"等。避免过于具体或细化。 +**注意**:上述划分"任务"应该是高层次且通用的,通常按主题或任务类型划分,对同目标或相似的任务进行合并,例如:"旅行计划"、"项目工程设计与实现"、"代码审查" 等,避免过于具体或细化。 # 规则与约束 -1. **任务独立性**:如果对话中讨论了多个不相关的话题,请将它们识别为不同的任务。 -2. **非连续处理**:注意识别"跳跃式"对话。例如,如果用户在消息 8-11 中制定旅行计划,在消息 12-22 中切换到咨询天气,然后在消息 23-24 中返回到制定旅行计划,请务必将 8-11 和 23-24 都分配给"制定旅行计划"任务。但是,如果消息是连续的且属于同一任务,不能将其分开。 -3. **过滤闲聊**:仅提取具有明确目标、指令或基于知识的讨论的任务。忽略无意义的问候(例如"你好"、"在吗?")或结束语,除非它们是任务上下文的一部分。 -4. **主任务与子任务识别**:仔细识别子任务是否服务于主任务。如果子任务是为主任务服务的(例如"查天气"服务于"旅行规划"),不要将其作为独立任务分离出来,而是将所有相关对话都划分到主任务中。只有真正独立且无关联的任务才需要分开。 +1. **任务独立性**:如果对话中讨论了多个完全不相关的话题,请将它们识别为不同的任务。 +2. **主任务与子任务识别**:仔细识别划分的任务是否服务于主任务。如果某一个任务是为了完成主任务而服务的(例如"旅行规划"的对话中出现了"查天气"),不要将其作为独立任务分离出来,而是将所有相关对话都划分到主任务中。**只有真正独立且无关联的任务才需要分开。** +3. **非连续处理**:注意识别"跳跃式"对话。例如,如果用户在消息 8-11 中制定旅行计划,在消息 12-22 中切换到其他任务,然后在消息 23-24 中返回到制定旅行计划,请务必将 8-11 和 23-24 都分配给"制定旅行计划"任务。按照规则2的描述,如果消息是连续的且属于同一任务,不能将其分开。 +4. **过滤闲聊**:仅提取具有明确目标、指令或基于知识的讨论的任务。忽略无意义的问候(例如"你好"、"在吗?")或结束语,除非它们是任务上下文的一部分。 5. **输出格式**:请严格遵循 JSON 格式输出,以便我后续处理。 -6. **语言一致性**:task_name 字段使用的语言必须与对话记录中使用的语言相匹配。 -7. **通用任务名称**:使用通用的、可复用的任务名称,而不是具体的描述。例如,使用"旅行规划"而不是"规划成都5日游"。 +6. **通用任务名称**:使用通用的、可复用的任务名称,而不是具体的描述。例如,使用"旅行规划"而不是"规划成都5日游"。 ```json [ { "task_id": 1, - "task_name": "通用任务名称(例如:旅行规划、代码审查、数据分析)", + "task_name": "通用任务名称", "message_indices": [[0, 5],[16, 17]], # 0-5 和 16-17 是此任务的消息索引 "reasoning": "简要解释为什么这些消息被分组在一起" }, @@ -67,7 +66,6 @@ ``` """ - SKILL_MEMORY_EXTRACTION_PROMPT = """ # Role You are an expert in skill abstraction and knowledge extraction. You excel at distilling general, reusable methodologies from specific conversations. @@ -229,6 +227,152 @@ """ +SKILL_MEMORY_EXTRACTION_PROMPT_MD = """ +# Role +You are an expert in skill abstraction and knowledge extraction. You excel at distilling general, reusable methodologies and executable workflows from specific conversations to enable direct application in future similar scenarios. + +# Task +Analyze the current messages and chat history to extract a universal, effective skill template. Compare the extracted methodology with existing skill memories (checking descriptions and triggers) to determine if this should be a new entry or an update to an existing one. + +# Prerequisites +## Long Term Relevant Memories +{old_memories} + +## Short Term Conversation +{chat_history} + +## Conversation Messages +{messages} + +# Skill Extraction Principles +To define the content of a skill, comprehensively analyze the dialogue content to create a list of reusable resources, including scripts, reference materials, and resources. Please generate the skill according to the following principles: +1. **Generalization**: Extract abstract methodologies that can be applied across scenarios. Avoid specific details (e.g., 'travel planning' rather than 'Beijing travel planning'). Moreover, the skills acquired should be durable and effective, rather than tied to a specific time. +2. **Similarity Check**: If a similar skill exists, set "update": true and provide the "old_memory_id". Otherwise, set "update": false and leave "old_memory_id" blank. +3. **Language Consistency**: Keep consistent with the language of the dialogue. +4. **Historical Usage Constraint**: Use 'historically related dialogues' as auxiliary context. If the current historical messages are insufficient to form a complete skill, and the historically related dialogue can provide missing information in the messages that is related to the current task objectives, execution methods, or constraints, it may be considered. +5. If the abstract methodology you extract and an existing skill memory describe the same topic (such as the same life scenario), be sure to use the update operation rather than creating a new methodology. Properly append it to the existing skill memory to ensure fluency and retain the information of the existing methodology. + +# Output Format and Field Specifications +## Output Format +```json +{ + "name": "General skill name (e.g., 'Travel Itinerary Planning', 'Code Review Workflow')", + "description": "Universal description of what this skill accomplishes and its scope", + "trigger": ["keyword1", "keyword2"], + "procedure": "Generic step-by-step process: 1. Step one 2. Step two...", + "experience": ["General principles or lessons learned", "Error handling strategies", "Best practices..."], + "preference": ["User's general preference patterns", "Preferred approaches or constraints..."], + "update": false, + "old_memory_id": "", + "content_of_current_message": "Summary of core content from current messages", + "whether_use_chat_history": false, + "content_of_related_chat_history": "", + "examples": ["Complete formatted output example in markdown format showing the final deliverable structure, content can be abbreviated with '...' but should demonstrate the format and structure"], + "scripts": a TODO list of code and requirements. Use null if no specific code are required. + "tool": List of specific external tools required (for example, if links or API information appear in the context, a websearch or external API may be needed), not product names or system tools (e.g., Python, Redis, or MySQL). If no specific tools are needed, please use null. + "others": {"reference.md": "A concise summary of other reference need to be provided (e.g., examples, tutorials, or best practices) "}. Only need to give the writing requirements, no need to provide the full documentation content. +} +``` + +## Field Specifications +- **name**: Generic skill identifier without specific instances. +- **description**: Universal purpose and applicability. +- **trigger**: List of keywords that should activate this skill. +- **procedure**: Abstract, reusable process steps without specific details. Should be generalizable to similar tasks. +- **experience**: General lessons, principles, or insights. +- **preference**: User's overarching preference patterns. +- **update**: true if updating existing skill, false if new. +- **old_memory_id**: ID of skill being updated, or empty string if new. +- **whether_use_chat_history**: Indicates whether information from chat_history that does not appear in messages was incorporated into the skill. +- **content_of_related_chat_history**: If whether_use_chat_history is true, provide a high-level summary of the type of historical information used (e.g., “long-term preference: prioritizes cultural attractions”); do not quote the original dialogue verbatim. If not used, leave this field as an empty string. +- **examples**: Complete output templates showing the final deliverable format and structure. Should demonstrate how the task result looks when this skill is applied, including format, sections, and content organization. Content can be abbreviated but must show the complete structure. Use markdown format for better readability +- **scripts**: If the skill examples requires an implementation involving code, you must provide a TODO list that clearly enumerates: (1) The components or steps that need to be implemented, (2) The expected inputs, (3)The expected outputs. Detailed code or full implementations are not required. Use null if no specific code is required. +- **tool**: If links or interface information appear in the context, it indicates that the skill needs to rely on specific tools (such as websearch, external APIs, or system tools) during the answering process. Please list the tool names. If no specific tools are detected, please use null. +- **others**: If must have additional supporting sections for the skill or other dependencies, structured as key–value pairs. For example: {"reference.md": "A concise summary of the reference content"}. Only need to give the writing requirements, no need to provide the full documentation content. + +# Key Guidelines +- Return null if a skill cannot be extracted. +- Only create a new methodology when necessary. In the same scenario, try to merge them ("update": true). +For example, merge dietary planning into one entry. Do not add a new "Keto Diet Planning" if "Dietary Planning" already exists, because skills are a universal template. You can choose to add preferences and triggers to update "Dietary Planning". + +# Output Format +Output the JSON object only. +""" + + +SKILL_MEMORY_EXTRACTION_PROMPT_MD_ZH = """ +# 角色 +你是技能抽象和知识提取的专家。你擅长从上下文的具体对话中提炼通用的、可复用的方法流程,从而可以在后续遇到相似任务中允许直接执行该工作流程及脚本。 + +# 任务 +通过分析历史相关对话和**给定当前对话消息**中提取可应用于类似场景的**有效且通用**的技能模板,同时还需要分析现有的技能的描述和触发关键字(trigger),判断与当前对话是否相关,从而决定技能是需要新建还是更新。 + +# 先决条件 +## 长期相关记忆 +{old_memories} + +## 短期对话 +{chat_history} + +## 当前对话消息 +{messages} + +# 技能提取原则 +为了确定技能的内容,综合分析对话内容以创建可重复使用资源的清单,包括脚本、参考资料和资源,请你按照下面的原则来生成技能: +1. **通用化**:提取可跨场景应用的抽象方法论。避免具体细节(如"旅行规划"而非"北京旅行规划")。 而且提取的技能应该是持久有效的,而非与特定时间绑定。 +2. **相似性检查**:如存在相似技能,设置"update": true 及"old_memory_id"。否则设置"update": false 并将"old_memory_id"留空。 +3. **语言一致性**:与对话语言保持一致。 +4. **历史使用约束**:“历史相关对话”作为辅助上下文,若当前历史消息不足以形成完整的技能,且历史相关对话能提供 messages 中缺失、且与当前任务目标、执行方式或约束相关的信息增量时,可以纳入考虑。 +5. 如果你提取的抽象方法论和已有的技能记忆描述的是同一个主题(比如同一个生活场景),请务必使用更新操作,不要新建一个方法论,注意合理的追加到已有的技能记忆上,保证通顺且不丢失已有方法论的信息。 + +# 输出格式的模版和字段规范描述 +## 输出格式 +```json +{ + "name": "通用技能名称(如:'旅行行程规划'、'代码审查流程')", + "description": "技能作用的通用描述", + "trigger": ["关键词1", "关键词2"], + "procedure": "通用的分步流程:1. 步骤一 2. 步骤二...", + "experience": ["通用原则或经验教训", "对于可能出现错误的处理情况", "可应用于类似场景的最佳实践..."], + "preference": ["用户的通用偏好模式", "偏好的方法或约束..."], + "update": false, + "old_memory_id": "", + "content_of_current_message": "", + "whether_use_chat_history": false, + "content_of_related_chat_history": "", + "examples": ["展示最终交付成果的完整格式范本(使用 markdown 格式), 内容可用'...'省略,但需展示完整格式和结构"], + "scripts": "一个代码待办列表和需求说明。如果不需要特定代码,请使用 null.", + "tool": "所需特定外部工具列表(例如,如果上下文中出现了链接或接口信息,则需要使用websearch或外部 API)。", + "others": {"reference.md": "其他对于执行技能必须的参考内容(例如,示例、教程或最佳实践)"}。只需要给出撰写要求,无需完整的文档内容。 +} +``` + +## 字段规范 +- **name**:通用技能标识符,不含具体实例 +- **description**:通用用途和适用范围 +- **trigger**:触发技能执行的关键字列表,用于自动识别任务场景 +- **procedure**:抽象的、可复用的流程步骤,不含具体细节。应当能够推广到类似任务 +- **experience**:通用经验、原则或见解 +- **preference**:用户的整体偏好模式 +- **update**:更新现有技能为true,新建为false +- **old_memory_id**:被更新技能的ID,新建则为空字符串 +- **content_of_current_message**: 从当前对话消息中提取的核心内容(简写但必填), +- **whether_use_chat_history**:是否从 chat_history 中引用了 messages 中没有的内容并提取到skill中 +- **content_of_related_chat_history**:若 whether_use_chat_history 为 true,仅需概括性说明所使用的历史信息类型(如“长期偏好:文化类景点优先”),不要求逐字引用原始对话内容;若未使用,则置为空字符串。 +- **examples**:展示最终任务成果的输出模板,包括格式、章节和内容组织结构。应展示应用此技能后任务结果的样子,包含所有必要的部分。内容可以省略但必须展示完整结构。使用 markdown 格式以提高可读性 +- **scripts**:如果技能examples需要实现代码,必须提供一个待办列表,清晰枚举:(1) 需实现的组件或步骤,(2) 预期输入,(3) 预期输出。详细代码或完整实现不是必须的。如果不需要特定代码,请使用 null. +- **tool**:如果上下文中出现了链接或接口信息,则表明在回答过程中技能需要依赖特定工具(如websearch或外部 API),请列出工具名称。 +- **others**:如果必须要其他支持性章节或其他依赖项,格式为键值对,例如:{"reference.md": "参考内容的简要总结"}。只需要给出撰写要求,无需完整的文档内容。 + +# 关键指导 +- 无法提取技能时返回null +- 一定仅在必要时才新建方法论,同样的场景尽量合并("update": true), +如饮食规划合并为一条,不要已有“饮食规划”的情况下,再新增一个“生酮饮食规划”,因为技能是一个通用的模版,可以选择添加preference和trigger来更新“饮食规划”。 + +请生成技能模版,返回上述JSON对象 +""" + + TASK_QUERY_REWRITE_PROMPT = """ # Role You are an expert in understanding user intentions and task requirements. You excel at analyzing conversations and extracting the core task description. @@ -284,3 +428,107 @@ SKILLS_AUTHORING_PROMPT = """ """ + + +SCRIPT_GENERATION_PROMPT = """ +# Role +You are a Senior Python Developer and Architect. + +# Task +Generate production-ready, executable Python scripts based on the provided requirements and context. +The scripts will be part of a skill package used by an AI agent or a developer. + +# Requirements +{requirements} + +# Context +{context} + +# Instructions +1. **Completeness**: The code must be fully functional and self-contained. DO NOT use placeholders like `# ...`, `pass` (unless necessary), or `TODO`. +2. **Robustness**: Include comprehensive error handling (try-except blocks) and input validation. +3. **Style**: Follow PEP 8 guidelines. Use type hints for all function signatures. +4. **Dependencies**: Use standard libraries whenever possible. If external libraries are needed, list them in a comment at the top. +5. **Main Guard**: Include `if __name__ == "__main__":` blocks with example usage or test cases. + +# Output Format +Return ONLY a valid JSON object where keys are filenames (e.g., "utils.py", "main_task.py") and values are the raw code strings. +```json +{{ + "filename.py": "import os\\n\\ndef func():\\n ..." +}} +``` +""" + +TOOL_GENERATION_PROMPT = """ +# Task +Analyze the `Requirements` and `Context` to identify the relevant tools from the provided `Available Tools`. Return a list of the **names** of the matching tools. + +# Constraints +1. **Selection Criteria**: Include a tool name only if the tool's schema directly addresses the user's requirements. +2. **Empty Set Logic**: If `Available Tools` is empty or no relevant tools are found, you **must** return an empty JSON array: `[]`. +3. **Format Purity**: Return ONLY the JSON array of strings. Do not provide commentary, justifications, or any text outside the JSON block. + +# Available Tools +{tool_schemas} + +# Requirements +{requirements} + +# Context +{context} + +# Output +```json +[ + "tool_name_1", + "tool_name_2" +] +``` +""" + +OTHERS_GENERATION_PROMPT = """ +# Task +Create detailed, well-structured documentation for the file '{filename}' based on the provided summary and context. + +# Summary +{summary} + +# Context +{context} + +# Instructions +1. **Structure**: + - **Introduction**: Brief overview of the topic. + - **Detailed Content**: The main body of the documentation, organized with headers (##, ###). + - **Key Concepts/Reference**: Definitions or reference tables if applicable. + - **Conclusion/Next Steps**: Wrap up or point to related resources. +2. **Formatting**: Use Markdown effectively (lists, tables, code blocks, bold text) to enhance readability. +3. **Language Consistency**: Keep consistent with **the language of the context**. + +# Output Format +Return the content directly in Markdown format. +""" + +OTHERS_GENERATION_PROMPT_ZH = """ +# 任务 +根据提供的摘要和上下文,为文件 '{filename}' 创建详细且结构良好的文档。 + +# 摘要 +{summary} + +# 上下文 +{context} + +# 指南 +1. **结构**: +- **简介**:对主题进行简要概述。 +- **详细内容**:文档的主体内容,使用标题(##, ###)进行组织。 +- **关键概念/参考**:如果适用,提供定义或参考表格。 +- **结论/下一步**:总结或指向相关资源。 +2. **格式**:有效使用 Markdown(列表、表格、代码块、加粗文本)以增强可读性。 +3. **语言一致性**:保持与**上下文语言**一致。 + +# 输出格式 +以 Markdown 格式直接返回内容。 +""" From 42b66c87146fbe14e49ca099e99c13de8296ab3f Mon Sep 17 00:00:00 2001 From: "yuan.wang" Date: Mon, 9 Feb 2026 11:18:14 +0800 Subject: [PATCH 31/35] feat: local deploy --- src/memos/api/config.py | 7 +++++- .../read_skill_memory/process_skill_memory.py | 25 +++++++++++-------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/memos/api/config.py b/src/memos/api/config.py index 3c1ad959b..adbd04e3c 100644 --- a/src/memos/api/config.py +++ b/src/memos/api/config.py @@ -821,7 +821,12 @@ def get_product_default_config() -> dict[str, Any]: "oss_config": APIConfig.get_oss_config(), "skills_dir_config": { "skills_oss_dir": os.getenv("SKILLS_OSS_DIR", "skill_memory/"), - "skills_local_dir": os.getenv("SKILLS_LOCAL_DIR", "/tmp/skill_memory/"), + "skills_local_tmp_dir": os.getenv( + "SKILLS_LOCAL_TMP_DIR", "/tmp/skill_memory/" + ), + "skills_local_dir": os.getenv( + "SKILLS_LOCAL_DIR", "/tmp/upload_skill_memory/" + ), }, }, }, 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 33e225f3f..791f0ca4b 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 @@ -661,12 +661,13 @@ def _rewrite_query(task_type: str, messages: MessageList, llm: BaseLLM, rewrite_ def _upload_skills( skills_repo_backend: str, skills_oss_dir: dict[str, Any] | None, - local_file_path: str, + local_tmp_file_path: str, + local_save_file_path: str, client: Any, user_id: str, ) -> str: if skills_repo_backend == "OSS": - zip_filename = Path(local_file_path).name + zip_filename = Path(local_tmp_file_path).name oss_path = (Path(skills_oss_dir) / user_id / zip_filename).as_posix() import alibabacloud_oss_v2 as oss @@ -676,7 +677,7 @@ def _upload_skills( bucket=os.getenv("OSS_BUCKET_NAME"), key=oss_path, ), - filepath=local_file_path, + filepath=local_tmp_file_path, ) if result.status_code != 200: @@ -698,11 +699,10 @@ def _upload_skills( else "8000" ) - zip_path = str(local_file_path) - local_save_path = os.getenv("FILE_LOCAL_PATH") - os.makedirs(local_save_path, exist_ok=True) + zip_path = str(local_tmp_file_path) + os.makedirs(local_save_file_path, exist_ok=True) file_name = os.path.basename(zip_path) - target_full_path = os.path.join(local_save_path, file_name) + target_full_path = os.path.join(local_save_file_path, file_name) shutil.copy2(zip_path, target_full_path) return f"http://localhost:{port}/download/{file_name}" @@ -716,6 +716,7 @@ def _delete_skills( zip_filename: str, client: Any, skills_oss_dir: dict[str, Any] | None, + local_save_file_path: str, user_id: str, ) -> Any: if skills_repo_backend == "OSS": @@ -729,7 +730,7 @@ def _delete_skills( ) ) else: - target_full_path = os.path.join(os.getenv("FILE_LOCAL_PATH"), zip_filename) + target_full_path = os.path.join(local_save_file_path, zip_filename) target_path = Path(target_full_path) try: if target_path.is_file(): @@ -748,7 +749,7 @@ def _write_skills_to_file( skill_name = skill_memory.get("name", "unnamed_skill").replace(" ", "_").lower() # Create tmp directory for user if it doesn't exist - tmp_dir = Path(skills_dir_config["skills_local_dir"]) / user_id + tmp_dir = Path(skills_dir_config["skills_local_tmp_dir"]) / user_id tmp_dir.mkdir(parents=True, exist_ok=True) # Create skill directory directly in tmp_dir @@ -955,7 +956,7 @@ def _skill_init(skills_repo_backend, oss_config, skills_dir_config): return None, None, False # Validate skills_dir has required keys - required_keys = ["skills_local_dir", "skills_oss_dir"] + required_keys = ["skills_local_tmp_dir", "skills_local_dir", "skills_oss_dir"] missing_keys = [key for key in required_keys if key not in skills_dir_config] if missing_keys: logger.warning( @@ -1150,6 +1151,7 @@ def _full_extract(): zip_filename=zip_filename, client=oss_client, skills_oss_dir=skills_dir_config["skills_oss_dir"], + local_save_file_path=skills_dir_config["skills_local_dir"], user_id=user_id, ) logger.info( @@ -1172,7 +1174,8 @@ def _full_extract(): url = _upload_skills( skills_repo_backend=skills_repo_backend, skills_oss_dir=skills_dir_config["skills_oss_dir"], - local_file_path=zip_path, + local_tmp_file_path=zip_path, + local_save_file_path=skills_dir_config["skills_local_dir"], client=oss_client, user_id=user_id, ) From 42c1764cc2431cb9c1c7775afe86544e1307335a Mon Sep 17 00:00:00 2001 From: "yuan.wang" Date: Mon, 9 Feb 2026 14:35:15 +0800 Subject: [PATCH 32/35] fix: modify code --- .../read_skill_memory/process_skill_memory.py | 22 ++++++++++++++----- src/memos/templates/skill_mem_prompt.py | 8 +++---- 2 files changed, 20 insertions(+), 10 deletions(-) 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 791f0ca4b..e244ac1b4 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 @@ -486,7 +486,7 @@ def _extract_skill_memory_by_llm_md( seen_messages = set() for mem in old_memories_dict: - if mem["metadata"]["memory_type"] == "SkillMemory" and mem["metadata"]["relativity"] > 0.02: + if mem["metadata"]["memory_type"] == "SkillMemory": old_skill_content.append( { "id": mem["id"], @@ -524,19 +524,25 @@ def _extract_skill_memory_by_llm_md( ) chat_history_context = chat_history_context[-chat_history_max_length:] + # Prepare prompt + lang = detect_lang(messages_context) + # Prepare old memories context old_skill_content = ( - ("Exsit Skill Schemas: \n" + json.dumps(old_skill_content, ensure_ascii=False, indent=2)) + "已有技能列表: \n" + if lang == "zh" + else "Exsit Skill Schemas: \n" + json.dumps(old_skill_content, ensure_ascii=False, indent=2) if old_skill_content else "" ) - old_memories_context = "Relavant Context:\n" + "\n".join( - [f"{k}:\n{v}" for k, v in old_memories_context.items()] + old_memories_context = ( + "相关历史对话:\n" + if lang == "zh" + else "Relavant Context:\n" + + "\n".join([f"{k}:\n{v}" for k, v in old_memories_context.items()]) ) - # Prepare prompt - lang = detect_lang(messages_context) template = ( SKILL_MEMORY_EXTRACTION_PROMPT_MD_ZH if lang == "zh" else SKILL_MEMORY_EXTRACTION_PROMPT_MD ) @@ -570,6 +576,10 @@ def _extract_skill_memory_by_llm_md( "[PROCESS_SKILLS] No skill memory extracted from conversation (LLM returned null)" ) return None + # If no old skill content, set update to False (for llm hallucination) + if not old_skill_content: + skill_memory["old_memory_id"] = "" + skill_memory["update"] = False return skill_memory diff --git a/src/memos/templates/skill_mem_prompt.py b/src/memos/templates/skill_mem_prompt.py index 7c60e2147..200f27c52 100644 --- a/src/memos/templates/skill_mem_prompt.py +++ b/src/memos/templates/skill_mem_prompt.py @@ -247,10 +247,10 @@ # Skill Extraction Principles To define the content of a skill, comprehensively analyze the dialogue content to create a list of reusable resources, including scripts, reference materials, and resources. Please generate the skill according to the following principles: 1. **Generalization**: Extract abstract methodologies that can be applied across scenarios. Avoid specific details (e.g., 'travel planning' rather than 'Beijing travel planning'). Moreover, the skills acquired should be durable and effective, rather than tied to a specific time. -2. **Similarity Check**: If a similar skill exists, set "update": true and provide the "old_memory_id". Otherwise, set "update": false and leave "old_memory_id" blank. +2. **Similarity Check**: If the skill list in 'existing skill memory' is not empty and there are skills with the **same topic**, you need to set "update": true and "old_memory_id". Otherwise, set "update": false and leave "old_memory_id" empty. 3. **Language Consistency**: Keep consistent with the language of the dialogue. 4. **Historical Usage Constraint**: Use 'historically related dialogues' as auxiliary context. If the current historical messages are insufficient to form a complete skill, and the historically related dialogue can provide missing information in the messages that is related to the current task objectives, execution methods, or constraints, it may be considered. -5. If the abstract methodology you extract and an existing skill memory describe the same topic (such as the same life scenario), be sure to use the update operation rather than creating a new methodology. Properly append it to the existing skill memory to ensure fluency and retain the information of the existing methodology. +Note: If the similarity check result shows that an existing **skill** description covers the same topic, be sure to use the update operation and set old_memory_id to the ID of the existing skill. Do not create a new methodology; make sure to reasonably add it to the existing skill memory, ensuring smoothness while preserving the information of the existing methodology. # Output Format and Field Specifications ## Output Format @@ -320,10 +320,10 @@ # 技能提取原则 为了确定技能的内容,综合分析对话内容以创建可重复使用资源的清单,包括脚本、参考资料和资源,请你按照下面的原则来生成技能: 1. **通用化**:提取可跨场景应用的抽象方法论。避免具体细节(如"旅行规划"而非"北京旅行规划")。 而且提取的技能应该是持久有效的,而非与特定时间绑定。 -2. **相似性检查**:如存在相似技能,设置"update": true 及"old_memory_id"。否则设置"update": false 并将"old_memory_id"留空。 +2. **相似性检查**:如果‘现有技能记忆’中的技能列表不为空,且存在**相同主题**的技能,则需要设置"update": true 及"old_memory_id"。否则设置"update": false 并将"old_memory_id"留空。 3. **语言一致性**:与对话语言保持一致。 4. **历史使用约束**:“历史相关对话”作为辅助上下文,若当前历史消息不足以形成完整的技能,且历史相关对话能提供 messages 中缺失、且与当前任务目标、执行方式或约束相关的信息增量时,可以纳入考虑。 -5. 如果你提取的抽象方法论和已有的技能记忆描述的是同一个主题(比如同一个生活场景),请务必使用更新操作,不要新建一个方法论,注意合理的追加到已有的技能记忆上,保证通顺且不丢失已有方法论的信息。 +注意:如果相似性检查结果是存在已有的**一个**技能描述的是同一个主题,请务必使用更新操作,并将old_memory_id设置为该历史技能的id,不要新建一个方法论,注意合理的追加到已有的技能记忆上,保证通顺的同时不丢失已有方法论的信息。 # 输出格式的模版和字段规范描述 ## 输出格式 From 870e2990072e22d40762f452a08dd58147e014bc Mon Sep 17 00:00:00 2001 From: "yuan.wang" Date: Mon, 9 Feb 2026 16:10:02 +0800 Subject: [PATCH 33/35] fix: remove chinese comment --- .../read_skill_memory/process_skill_memory.py | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) 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 e244ac1b4..fa799e759 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 @@ -3,7 +3,6 @@ import os import shutil import uuid -import warnings import zipfile from concurrent.futures import as_completed @@ -531,7 +530,7 @@ def _extract_skill_memory_by_llm_md( old_skill_content = ( "已有技能列表: \n" if lang == "zh" - else "Exsit Skill Schemas: \n" + json.dumps(old_skill_content, ensure_ascii=False, indent=2) + else "Exist Skill Schemas: \n" + json.dumps(old_skill_content, ensure_ascii=False, indent=2) if old_skill_content else "" ) @@ -539,7 +538,7 @@ def _extract_skill_memory_by_llm_md( old_memories_context = ( "相关历史对话:\n" if lang == "zh" - else "Relavant Context:\n" + else "Relevant Context:\n" + "\n".join([f"{k}:\n{v}" for k, v in old_memories_context.items()]) ) @@ -745,11 +744,11 @@ def _delete_skills( try: if target_path.is_file(): target_path.unlink() - logger.info(f"本地文件 {target_path} 已成功删除") + logger.info(f"Local file {target_path} successfully deleted") else: - print(f"本地文件 {target_path} 不存在,无需删除") + logger.info(f"Local file {target_path} does not exist, no need to delete") except Exception as e: - print(f"删除本地文件时出错:{e}") + logger.warning(f"Error deleting local file: {e}") def _write_skills_to_file( @@ -773,7 +772,7 @@ def _write_skills_to_file( --- """ - # 加入trigger + # Add trigger trigger = skill_memory.get("trigger", "") if trigger: skill_md_content += f"\n## Trigger\n{trigger}\n" @@ -984,16 +983,14 @@ def _skill_init(skills_repo_backend, oss_config, skills_dir_config): def _get_skill_file_storage_location() -> str: - # SKILLS_REPO_BACKEND: Skill 文件保存地址 OSS/LOCAL + # SKILLS_REPO_BACKEND: Skill file storage location OSS/LOCAL allowed_backends = {"OSS", "LOCAL"} raw_backend = os.getenv("SKILLS_REPO_BACKEND") if raw_backend in allowed_backends: return raw_backend else: - warnings.warn( - "环境变量【SKILLS_REPO_BACKEND】赋值错误,本次使用 LOCAL 存储 skill", - UserWarning, - stacklevel=1, + logger.warning( + "Environment variable [SKILLS_REPO_BACKEND] is invalid, using LOCAL to store skill", ) return "LOCAL" From 15facaf718b0c3aa6586829326f4f4bb0823af12 Mon Sep 17 00:00:00 2001 From: "yuan.wang" Date: Mon, 9 Feb 2026 16:53:27 +0800 Subject: [PATCH 34/35] fix: add logger to chat --- src/memos/api/handlers/chat_handler.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/memos/api/handlers/chat_handler.py b/src/memos/api/handlers/chat_handler.py index fa57dd9eb..cd33a7aeb 100644 --- a/src/memos/api/handlers/chat_handler.py +++ b/src/memos/api/handlers/chat_handler.py @@ -1557,6 +1557,10 @@ def _start_add_to_memory( manager_user_id: str | None = None, project_id: str | None = None, ) -> None: + self.logger.info( + f"Start add to memory for user {user_id}, writable_cube_ids: {writable_cube_ids}, session_id: {session_id}, query: {query}, full_response: {full_response}, async_mode: {async_mode}, manager_user_id: {manager_user_id}, project_id: {project_id}" + ) + def run_async_in_thread(): try: loop = asyncio.new_event_loop() From ebe69b2ddcddcd7ad89c47265a1aa7fd521bc686 Mon Sep 17 00:00:00 2001 From: "yuan.wang" Date: Mon, 9 Feb 2026 19:20:16 +0800 Subject: [PATCH 35/35] fix: ban RawFileMemory --- src/memos/api/product_models.py | 2 +- .../memories/textual/tree_text_memory/retrieve/searcher.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/memos/api/product_models.py b/src/memos/api/product_models.py index d056bca6a..c41526e33 100644 --- a/src/memos/api/product_models.py +++ b/src/memos/api/product_models.py @@ -424,7 +424,7 @@ class APISearchRequest(BaseRequest): ) # Internal field for search memory type search_memory_type: str = Field( - "AllSummaryMemory", + "All", description="Type of memory to search: All, WorkingMemory, LongTermMemory, UserMemory, OuterMemory, ToolSchemaMemory, ToolTrajectoryMemory, RawFileMemory, AllSummaryMemory, SkillMemory", ) 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 356402c90..bc8d76517 100644 --- a/src/memos/memories/textual/tree_text_memory/retrieve/searcher.py +++ b/src/memos/memories/textual/tree_text_memory/retrieve/searcher.py @@ -518,7 +518,7 @@ def _retrieve_from_long_term_and_user( use_fast_graph=self.use_fast_graph, ) ) - if memory_type in ["All", "RawFileMemory"]: + if memory_type in ["RawFileMemory"]: tasks.append( executor.submit( self.graph_retriever.retrieve,