From dbac0352128f6b7d5c7ebc10c62221a5ada6f933 Mon Sep 17 00:00:00 2001 From: "yuan.wang" Date: Fri, 23 Jan 2026 14:11:57 +0800 Subject: [PATCH 01/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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}} """