From 678b99880afc6760465fe510bd7f6e361ece3b8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=AD=E9=98=B3=E9=98=B3?= Date: Thu, 29 Jan 2026 15:16:00 +0800 Subject: [PATCH 01/14] feat: skill with history --- src/memos/mem_reader/base.py | 2 +- src/memos/mem_reader/multi_modal_struct.py | 9 +++++++-- .../mem_reader/read_skill_memory/process_skill_memory.py | 1 + src/memos/mem_reader/simple_struct.py | 5 ++++- src/memos/multi_mem_cube/single_cube.py | 3 +++ 5 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/memos/mem_reader/base.py b/src/memos/mem_reader/base.py index b034c9367..0bfd31fa8 100644 --- a/src/memos/mem_reader/base.py +++ b/src/memos/mem_reader/base.py @@ -42,7 +42,7 @@ def set_searcher(self, searcher: "Searcher | None") -> None: @abstractmethod def get_memory( - self, scene_data: list, type: str, info: dict[str, Any], mode: str = "fast" + self, scene_data: list, type: str, info: dict[str, Any], mode: str = "fast", **kwargs ) -> list[list[TextualMemoryItem]]: """Various types of memories extracted from scene_data""" diff --git a/src/memos/mem_reader/multi_modal_struct.py b/src/memos/mem_reader/multi_modal_struct.py index 236a8f180..3d3e51f82 100644 --- a/src/memos/mem_reader/multi_modal_struct.py +++ b/src/memos/mem_reader/multi_modal_struct.py @@ -879,7 +879,7 @@ def _process_multi_modal_data( embedder=self.embedder, oss_config=self.oss_config, skills_dir_config=self.skills_dir_config, - **kwargs, + history=kwargs.get("history"), ) # Collect results @@ -963,7 +963,12 @@ def _process_transfer_multi_modal_data( 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 + source, + context_items=[raw_node], + info=info, + custom_tags=custom_tags, + lang=lang, + history=kwargs.get("history"), ) fine_memory_items.extend(items) return fine_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 bb809e69d..70caa9930 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 @@ -524,6 +524,7 @@ def process_skill_memory_fine( rewrite_query: bool = True, oss_config: dict[str, Any] | None = None, skills_dir_config: dict[str, Any] | None = None, + history: list | None = None, **kwargs, ) -> list[TextualMemoryItem]: # Validate required configurations diff --git a/src/memos/mem_reader/simple_struct.py b/src/memos/mem_reader/simple_struct.py index f3ae98ccb..2c4fee853 100644 --- a/src/memos/mem_reader/simple_struct.py +++ b/src/memos/mem_reader/simple_struct.py @@ -428,6 +428,7 @@ def get_memory( info: dict[str, Any], mode: str = "fine", user_name: str | None = None, + **kwargs, ) -> list[list[TextualMemoryItem]]: """ Extract and classify memory content from scene_data. @@ -471,7 +472,9 @@ def get_memory( # Backward compatibility, after coercing scene_data, we only tackle # with standard scene_data type: MessagesType standard_scene_data = coerce_scene_data(scene_data, type) - return self._read_memory(standard_scene_data, type, info, mode, user_name=user_name) + return self._read_memory( + standard_scene_data, type, info, mode, user_name=user_name, **kwargs + ) def rewrite_memories( self, messages: list[dict], memory_list: list[TextualMemoryItem], user_only: bool = True diff --git a/src/memos/multi_mem_cube/single_cube.py b/src/memos/multi_mem_cube/single_cube.py index 2f7883548..3ca37150e 100644 --- a/src/memos/multi_mem_cube/single_cube.py +++ b/src/memos/multi_mem_cube/single_cube.py @@ -527,6 +527,7 @@ def _schedule_memory_tasks( user_context: UserContext, mem_ids: list[str], sync_mode: str, + **kwargs, ) -> None: """ Schedule memory processing tasks based on sync mode. @@ -807,6 +808,7 @@ def _process_text_mem( }, mode=extract_mode, user_name=user_context.mem_cube_id, + history=add_req.chat_history, ) self.logger.info( f"Time for get_memory in extract mode {extract_mode}: {time.time() - init_time}" @@ -837,6 +839,7 @@ def _process_text_mem( user_context=user_context, mem_ids=mem_ids_local, sync_mode=sync_mode, + history=add_req.chat_history, ) # Mark merged_from memories as archived when provided in add_req.info From 8869b897f6401b2870c648a2b19a4ed4b3847d21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=AD=E9=98=B3=E9=98=B3?= Date: Thu, 29 Jan 2026 15:47:06 +0800 Subject: [PATCH 02/14] feat: pass chat-history into skill --- src/memos/mem_reader/multi_modal_struct.py | 4 ++-- src/memos/mem_scheduler/general_scheduler.py | 4 ++++ src/memos/mem_scheduler/schemas/message_schemas.py | 4 ++++ src/memos/multi_mem_cube/single_cube.py | 5 ++--- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/memos/mem_reader/multi_modal_struct.py b/src/memos/mem_reader/multi_modal_struct.py index 3d3e51f82..a607b617f 100644 --- a/src/memos/mem_reader/multi_modal_struct.py +++ b/src/memos/mem_reader/multi_modal_struct.py @@ -879,7 +879,7 @@ def _process_multi_modal_data( embedder=self.embedder, oss_config=self.oss_config, skills_dir_config=self.skills_dir_config, - history=kwargs.get("history"), + history=kwargs.get("chat_history"), ) # Collect results @@ -946,7 +946,7 @@ def _process_transfer_multi_modal_data( graph_db=self.graph_db, oss_config=self.oss_config, skills_dir_config=self.skills_dir_config, - **kwargs, + history=kwargs.get("chat_history"), ) # Collect results diff --git a/src/memos/mem_scheduler/general_scheduler.py b/src/memos/mem_scheduler/general_scheduler.py index d4ac09cc3..74e50a514 100644 --- a/src/memos/mem_scheduler/general_scheduler.py +++ b/src/memos/mem_scheduler/general_scheduler.py @@ -764,6 +764,7 @@ def process_message(message: ScheduleMessageItem): content = message.content user_name = message.user_name info = message.info or {} + chat_history = message.chat_history # Parse the memory IDs from content mem_ids = json.loads(content) if isinstance(content, str) else content @@ -790,6 +791,7 @@ def process_message(message: ScheduleMessageItem): custom_tags=info.get("custom_tags", None), task_id=message.task_id, info=info, + chat_history=chat_history, ) logger.info( @@ -817,6 +819,7 @@ def _process_memories_with_reader( custom_tags: list[str] | None = None, task_id: str | None = None, info: dict | None = None, + chat_history: list | None = None, ) -> None: logger.info( f"[DIAGNOSTIC] general_scheduler._process_memories_with_reader called. mem_ids: {mem_ids}, user_id: {user_id}, mem_cube_id: {mem_cube_id}, task_id: {task_id}" @@ -878,6 +881,7 @@ def _process_memories_with_reader( type="chat", custom_tags=custom_tags, user_name=user_name, + chat_history=chat_history, ) except Exception as e: logger.warning(f"{e}: Fail to transfer mem: {memory_items}") diff --git a/src/memos/mem_scheduler/schemas/message_schemas.py b/src/memos/mem_scheduler/schemas/message_schemas.py index cf3019d5e..c7f270f19 100644 --- a/src/memos/mem_scheduler/schemas/message_schemas.py +++ b/src/memos/mem_scheduler/schemas/message_schemas.py @@ -54,6 +54,7 @@ class ScheduleMessageItem(BaseModel, DictConversionMixin): default=None, description="Optional business-level task ID. Multiple items can share the same task_id.", ) + chat_history: list | None = Field(default=None, description="user chat history") # Pydantic V2 model configuration model_config = ConfigDict( @@ -89,6 +90,7 @@ def to_dict(self) -> dict: "timestamp": self.timestamp.isoformat(), "user_name": self.user_name, "task_id": self.task_id if self.task_id is not None else "", + "chat_history": self.chat_history if self.chat_history is not None else [], } @classmethod @@ -104,6 +106,7 @@ def from_dict(cls, data: dict) -> "ScheduleMessageItem": timestamp=datetime.fromisoformat(data["timestamp"]), user_name=data.get("user_name"), task_id=data.get("task_id"), + chat_history=data.get("chat_history"), ) @@ -158,6 +161,7 @@ class ScheduleLogForWebItem(BaseModel, DictConversionMixin): default=None, description="Completion status of the task (e.g., 'completed', 'failed')" ) source_doc_id: str | None = Field(default=None, description="Source document ID") + chat_history: list | None = Field(default=None, description="user chat history") def debug_info(self) -> dict[str, Any]: """Return structured debug information for logging purposes.""" diff --git a/src/memos/multi_mem_cube/single_cube.py b/src/memos/multi_mem_cube/single_cube.py index 3ca37150e..bd026a51d 100644 --- a/src/memos/multi_mem_cube/single_cube.py +++ b/src/memos/multi_mem_cube/single_cube.py @@ -527,7 +527,6 @@ def _schedule_memory_tasks( user_context: UserContext, mem_ids: list[str], sync_mode: str, - **kwargs, ) -> None: """ Schedule memory processing tasks based on sync mode. @@ -554,6 +553,7 @@ def _schedule_memory_tasks( timestamp=datetime.utcnow(), user_name=self.cube_id, info=add_req.info, + chat_history=add_req.chat_history, ) self.mem_scheduler.submit_messages(messages=[message_item_read]) self.logger.info( @@ -808,7 +808,7 @@ def _process_text_mem( }, mode=extract_mode, user_name=user_context.mem_cube_id, - history=add_req.chat_history, + chat_history=add_req.chat_history, ) self.logger.info( f"Time for get_memory in extract mode {extract_mode}: {time.time() - init_time}" @@ -839,7 +839,6 @@ def _process_text_mem( user_context=user_context, mem_ids=mem_ids_local, sync_mode=sync_mode, - history=add_req.chat_history, ) # Mark merged_from memories as archived when provided in add_req.info From 1331b0b676f31d86222bf2912b92dd83fc5752c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=AD=E9=98=B3=E9=98=B3?= Date: Thu, 29 Jan 2026 15:58:11 +0800 Subject: [PATCH 03/14] feat: modify chat-history passing in skills --- src/memos/mem_reader/multi_modal_struct.py | 4 ++-- .../mem_reader/read_skill_memory/process_skill_memory.py | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/memos/mem_reader/multi_modal_struct.py b/src/memos/mem_reader/multi_modal_struct.py index a607b617f..491d33e5f 100644 --- a/src/memos/mem_reader/multi_modal_struct.py +++ b/src/memos/mem_reader/multi_modal_struct.py @@ -879,7 +879,7 @@ def _process_multi_modal_data( embedder=self.embedder, oss_config=self.oss_config, skills_dir_config=self.skills_dir_config, - history=kwargs.get("chat_history"), + **kwargs, ) # Collect results @@ -946,7 +946,7 @@ def _process_transfer_multi_modal_data( graph_db=self.graph_db, oss_config=self.oss_config, skills_dir_config=self.skills_dir_config, - history=kwargs.get("chat_history"), + **kwargs, ) # Collect results 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 70caa9930..a6ab71c6e 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 @@ -524,7 +524,6 @@ def process_skill_memory_fine( rewrite_query: bool = True, oss_config: dict[str, Any] | None = None, skills_dir_config: dict[str, Any] | None = None, - history: list | None = None, **kwargs, ) -> list[TextualMemoryItem]: # Validate required configurations @@ -538,6 +537,10 @@ def process_skill_memory_fine( ) return [] + chat_history = kwargs.get("chat_history") + if not chat_history: + logger.warning("[PROCESS_SKILLS] History is None in Skills") + # Validate skills_dir has required keys required_keys = ["skills_local_dir", "skills_oss_dir"] missing_keys = [key for key in required_keys if key not in skills_dir_config] From 21a0ce6b21aeab010eaa570513d0f88a58c44fd5 Mon Sep 17 00:00:00 2001 From: "yuan.wang" Date: Thu, 29 Jan 2026 16:09:11 +0800 Subject: [PATCH 04/14] feat: modify code --- .../read_skill_memory/process_skill_memory.py | 26 +++++++++++++++---- 1 file changed, 21 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 70caa9930..65293b336 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 @@ -105,6 +105,15 @@ def _reconstruct_messages_from_memory_items(memory_items: list[TextualMemoryItem return reconstructed_messages +def _preprocess_extract_messages(history: MessageList, messages: MessageList) -> MessageList: + """Process data and check whether to extract skill memory""" + history = history[-20:] + if (len(history) + len(messages)) < 10: + logger.warning("[PROCESS_SKILLS] Not enough messages to extract skill memory") + return [] + return history + messages + + def _add_index_to_message(messages: MessageList) -> MessageList: for i, message in enumerate(messages): message["idx"] = i @@ -524,7 +533,7 @@ def process_skill_memory_fine( rewrite_query: bool = True, oss_config: dict[str, Any] | None = None, skills_dir_config: dict[str, Any] | None = None, - history: list | None = None, + history: MessageList | None = None, **kwargs, ) -> list[TextualMemoryItem]: # Validate required configurations @@ -553,6 +562,11 @@ def process_skill_memory_fine( return [] messages = _reconstruct_messages_from_memory_items(fast_memory_items) + + messages = _preprocess_extract_messages(history, messages) + if not messages: + return [] + messages = _add_index_to_message(messages) task_chunks = _split_task_chunk_by_llm(llm, messages) @@ -714,9 +728,11 @@ def process_skill_memory_fine( 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", "")) - ) + for skill_memory, skill_memory_item in zip(skill_memories, skill_memory_items, strict=False): + if skill_memory.get("update", False) and skill_memory.get("old_memory_id", ""): + add_id_to_mysql( + memory_id=skill_memory_item.id, + mem_cube_id=kwargs.get("user_name", info.get("user_id", "")), + ) return skill_memory_items From 5a2c9ee094ae84be2e640306ce387e9c6a998908 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=AD=E9=98=B3=E9=98=B3?= Date: Thu, 29 Jan 2026 16:14:46 +0800 Subject: [PATCH 05/14] fix: we don't need to pass history in part B --- src/memos/mem_reader/multi_modal_struct.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/memos/mem_reader/multi_modal_struct.py b/src/memos/mem_reader/multi_modal_struct.py index 491d33e5f..236a8f180 100644 --- a/src/memos/mem_reader/multi_modal_struct.py +++ b/src/memos/mem_reader/multi_modal_struct.py @@ -963,12 +963,7 @@ def _process_transfer_multi_modal_data( 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, - history=kwargs.get("history"), + source, context_items=[raw_node], info=info, custom_tags=custom_tags, lang=lang ) fine_memory_items.extend(items) return fine_memory_items From 3e28da99cb53035dd3316964710eaf3c52e7b40a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=AD=E9=98=B3=E9=98=B3?= Date: Thu, 29 Jan 2026 16:33:52 +0800 Subject: [PATCH 06/14] fix: process skill memory --- .../read_skill_memory/process_skill_memory.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 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 9a9a16e65..a85f9c1ff 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 @@ -546,8 +546,8 @@ def process_skill_memory_fine( ) return [] - history = kwargs.get("chat_history") - if not history: + chat_history = kwargs.get("chat_history") + if not chat_history: logger.warning("[PROCESS_SKILLS] History is None in Skills") # Validate skills_dir has required keys @@ -566,7 +566,7 @@ def process_skill_memory_fine( messages = _reconstruct_messages_from_memory_items(fast_memory_items) - messages = _preprocess_extract_messages(history, messages) + messages = _preprocess_extract_messages(chat_history, messages) if not messages: return [] @@ -733,9 +733,10 @@ def process_skill_memory_fine( # TODO: deprecate this funtion and call for skill_memory, skill_memory_item in zip(skill_memories, skill_memory_items, strict=False): if skill_memory.get("update", False) and skill_memory.get("old_memory_id", ""): - add_id_to_mysql( - memory_id=skill_memory_item.id, - mem_cube_id=kwargs.get("user_name", info.get("user_id", "")), - ) + continue + add_id_to_mysql( + memory_id=skill_memory_item.id, + mem_cube_id=kwargs.get("user_name", info.get("user_id", "")), + ) return skill_memory_items From 30497ef5ac869e1bc2ec2e4bc0973868f194b9fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=AD=E9=98=B3=E9=98=B3?= Date: Thu, 29 Jan 2026 16:47:41 +0800 Subject: [PATCH 07/14] feat: we do not return None with few history now --- .../mem_reader/read_skill_memory/process_skill_memory.py | 5 +++-- 1 file changed, 3 insertions(+), 2 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 a85f9c1ff..059fad631 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 @@ -109,8 +109,8 @@ def _preprocess_extract_messages(history: MessageList, messages: MessageList) -> """Process data and check whether to extract skill memory""" history = history[-20:] if (len(history) + len(messages)) < 10: + # TODO: maybe directly return [] logger.warning("[PROCESS_SKILLS] Not enough messages to extract skill memory") - return [] return history + messages @@ -547,7 +547,8 @@ def process_skill_memory_fine( return [] chat_history = kwargs.get("chat_history") - if not chat_history: + if not chat_history or not isinstance(chat_history, list): + chat_history = [] logger.warning("[PROCESS_SKILLS] History is None in Skills") # Validate skills_dir has required keys From 3265a6d3c176586a1a04dc43d9070c5c474bd1f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=AD=E9=98=B3=E9=98=B3?= Date: Thu, 29 Jan 2026 18:06:15 +0800 Subject: [PATCH 08/14] feat: update skill --- src/memos/mem_reader/multi_modal_struct.py | 2 +- .../read_skill_memory/process_skill_memory.py | 35 +++++++++++++------ src/memos/templates/skill_mem_prompt.py | 34 ++++++++++++++++-- 3 files changed, 58 insertions(+), 13 deletions(-) diff --git a/src/memos/mem_reader/multi_modal_struct.py b/src/memos/mem_reader/multi_modal_struct.py index 236a8f180..f6a016556 100644 --- a/src/memos/mem_reader/multi_modal_struct.py +++ b/src/memos/mem_reader/multi_modal_struct.py @@ -862,7 +862,7 @@ def _process_multi_modal_data( # Part A: call llm in parallel using thread pool fine_memory_items = [] - with ContextThreadPoolExecutor(max_workers=2) as executor: + with ContextThreadPoolExecutor(max_workers=3) as executor: future_string = executor.submit( self._process_string_fine, fast_memory_items, info, custom_tags, **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 059fad631..724bdfee4 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 @@ -105,13 +105,15 @@ def _reconstruct_messages_from_memory_items(memory_items: list[TextualMemoryItem return reconstructed_messages -def _preprocess_extract_messages(history: MessageList, messages: MessageList) -> MessageList: +def _preprocess_extract_messages( + history: MessageList, messages: MessageList +) -> (MessageList, MessageList): """Process data and check whether to extract skill memory""" history = history[-20:] if (len(history) + len(messages)) < 10: # TODO: maybe directly return [] logger.warning("[PROCESS_SKILLS] Not enough messages to extract skill memory") - return history + messages + return history, messages def _add_index_to_message(messages: MessageList) -> MessageList: @@ -164,7 +166,11 @@ def _split_task_chunk_by_llm(llm: BaseLLM, messages: MessageList) -> dict[str, M def _extract_skill_memory_by_llm( - messages: MessageList, old_memories: list[TextualMemoryItem], llm: BaseLLM + messages: MessageList, + old_memories: list[TextualMemoryItem], + llm: BaseLLM, + chat_history: MessageList, + chat_history_max_length: int = 5000, ) -> dict[str, Any]: old_memories_dict = [skill_memory.model_dump() for skill_memory in old_memories] old_mem_references = [ @@ -178,7 +184,7 @@ def _extract_skill_memory_by_llm( "examples": mem["metadata"]["examples"], "tags": mem["metadata"]["tags"], "scripts": mem["metadata"].get("scripts"), - "others": mem["metadata"]["others"], + "others": mem["metadata"].get("others"), } for mem in old_memories_dict ] @@ -188,14 +194,22 @@ def _extract_skill_memory_by_llm( [f"{message['role']}: {message['content']}" for message in messages] ) + # Prepare history context + chat_history_context = "\n".join( + [f"{history['role']}: {history['content']}" for history in chat_history] + ) + chat_history_context = chat_history_context[-chat_history_max_length:] + # Prepare old memories context old_memories_context = json.dumps(old_mem_references, ensure_ascii=False, indent=2) # Prepare prompt 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_content = ( + template.replace("{old_memories}", old_memories_context) + .replace("{messages}", messages_context) + .replace("{chat_history}", chat_history_context) ) prompt = [{"role": "user", "content": prompt_content}] @@ -207,7 +221,7 @@ def _extract_skill_memory_by_llm( skills_llm = os.getenv("SKILLS_LLM", None) llm_kwargs = {"model_name_or_path": skills_llm} if skills_llm else {} response_text = llm.generate(prompt, **llm_kwargs) - # Clean up response (remove markdown code blocks if present) + # Clean up response (remove Markdown code blocks if present) response_text = response_text.strip() response_text = response_text.replace("```json", "").replace("```", "").strip() @@ -567,11 +581,12 @@ def process_skill_memory_fine( messages = _reconstruct_messages_from_memory_items(fast_memory_items) - messages = _preprocess_extract_messages(chat_history, messages) + chat_history, messages = _preprocess_extract_messages(chat_history, messages) if not messages: return [] messages = _add_index_to_message(messages) + chat_history = _add_index_to_message(chat_history) task_chunks = _split_task_chunk_by_llm(llm, messages) if not task_chunks: @@ -613,6 +628,7 @@ def process_skill_memory_fine( messages, related_skill_memories_by_task.get(task_type, []), llm, + chat_history, ): task_type for task_type, messages in task_chunks.items() } @@ -627,7 +643,7 @@ def process_skill_memory_fine( # write skills to file and get zip paths skill_memory_with_paths = [] - with ContextThreadPoolExecutor(max_workers=min(len(skill_memories), 5)) as executor: + with ContextThreadPoolExecutor(max_workers=5) as executor: futures = { executor.submit( _write_skills_to_file, skill_memory, info, skills_dir_config @@ -739,5 +755,4 @@ def process_skill_memory_fine( memory_id=skill_memory_item.id, mem_cube_id=kwargs.get("user_name", info.get("user_id", "")), ) - return skill_memory_items diff --git a/src/memos/templates/skill_mem_prompt.py b/src/memos/templates/skill_mem_prompt.py index 0bc0c1809..f1b44f341 100644 --- a/src/memos/templates/skill_mem_prompt.py +++ b/src/memos/templates/skill_mem_prompt.py @@ -78,6 +78,9 @@ # Existing Skill Memories {old_memories} +# Chat_history +{chat_history} + # Conversation Messages {messages} @@ -86,6 +89,11 @@ 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. +5. **History Usage Constraints**: + - `chat_history` serves only as auxiliary context to supplement stable preferences or methodologies that are not explicitly stated in `messages` but may affect skill abstraction. + - `chat_history` may be considered only when it provides information **missing from `messages`** and **relevant to the current task’s goals, execution approach, or constraints**. + - `chat_history` must not be the primary source of a skill, and may only be used to enrich auxiliary fields such as `preference` or `experience`. + - If `chat_history` does not provide any valid information beyond what already exists in `messages`, or contains only greetings or background content, it must be completely ignored. # Output Format ```json @@ -100,7 +108,9 @@ "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": "" + "old_memory_id": "", + "whether_use_chat_history": false, + "content_of_related_chat_history": "" } ``` @@ -119,6 +129,10 @@ - **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 +- **whether_use_chat_history**: Indicates whether information from chat_history that does not appear in messages was incorporated into the skill +- **content_of_related_chat_history**: + If whether_use_chat_history is true, provide a high-level summary of the type of historical information used (e.g., “long-term preference: prioritizes cultural attractions”); do not quote the original dialogue verbatim + If not used, leave this field as an empty string # Critical Guidelines - Keep all fields general except "examples" @@ -141,6 +155,9 @@ # 现有技能记忆 {old_memories} +# 对话消息的上下文chat_history +{chat_history} + # 对话消息 {messages} @@ -149,6 +166,11 @@ 2. **普适性**:除"examples"外,所有字段必须保持通用,与具体场景无关。 3. **相似性检查**:如存在相似技能,设置"update": true 及"old_memory_id"。否则设置"update": false 并将"old_memory_id"留空。 4. **语言一致性**:与对话语言保持一致。 +5. **历史使用约束**: + - chat_history 仅作为辅助上下文,用于补充 messages 中未明确出现的、但会影响技能抽象的稳定偏好或方法论。 + - 当 chat_history 能提供 messages 中缺失、且与当前任务目标、执行方式或约束相关的信息增量时,可以纳入考虑。 + - chat_history 不得作为技能的主要来源,仅可用于完善 preference、experience 等辅助字段。 + - 若 chat_history 未提供任何 messages 中不存在的有效信息,或仅包含寒暄、背景性内容,应完全忽略。 # 输出格式 ```json @@ -163,7 +185,9 @@ "scripts": {"script_name.py": "# Python 代码\nprint('Hello')", "another_script.py": "# 更多代码\nimport os"}, "others": {"章节标题": "这里的内容", "reference.md": "# 此技能的参考内容"}, "update": false, - "old_memory_id": "" + "old_memory_id": "", + "whether_use_chat_history": false, + "content_of_related_chat_history": "", } ``` @@ -182,12 +206,18 @@ - **examples**:展示最终任务成果的输出模板,包括格式、章节和内容组织结构。应展示应用此技能后任务结果的样子,包含所有必要的部分。内容可以省略但必须展示完整结构。使用 markdown 格式以提高可读性 - **update**:更新现有技能为true,新建为false - **old_memory_id**:被更新技能的ID,新建则为空字符串 +- **whether_use_chat_history**:是否从 chat_history 中引用了 messages 中没有的内容并提取到skill中 +- **content_of_related_chat_history**:若 whether_use_chat_history 为 true, + 仅需概括性说明所使用的历史信息类型(如“长期偏好:文化类景点优先”), + 不要求逐字引用原始对话内容; + 若未使用,则置为空字符串。 # 关键指导 - 除"examples"外保持所有字段通用 - "examples"应展示完整的最终输出格式和结构,包含所有必要章节 - "others"包含补充说明或扩展信息 - 无法提取技能时返回null +- 注意区分chat_history与对话消息 # 输出格式 仅输出JSON对象。 From bfaa98398b3d0f3e8e751e38529f297456226609 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=AD=E9=98=B3=E9=98=B3?= Date: Thu, 29 Jan 2026 19:36:09 +0800 Subject: [PATCH 09/14] feat: modify _split_task_chunk_by_llm --- .../read_skill_memory/process_skill_memory.py | 11 ++++++++--- 1 file changed, 8 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 724bdfee4..b5cbfa2c7 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 @@ -155,13 +155,18 @@ def _split_task_chunk_by_llm(llm: BaseLLM, messages: MessageList) -> dict[str, M message_indices = item["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: + if isinstance(indices, list) and len(indices) == 1: + start, end = indices[0], indices[0] + 1 + elif isinstance(indices, int): + start, end = indices, indices + 1 + elif isinstance(indices, list) and len(indices) == 2: + start, end = indices[0], indices[1] + 1 + else: logger.warning( f"[PROCESS_SKILLS] Invalid message indices format for task '{task_name}': {indices}, skipping" ) continue - start, end = indices - task_chunks.setdefault(task_name, []).extend(messages[start : end + 1]) + task_chunks.setdefault(task_name, []).extend(messages[start:end]) return task_chunks From 7e5b2a26c9393e01be46db4008eb199cd04268a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=AD=E9=98=B3=E9=98=B3?= Date: Thu, 29 Jan 2026 19:43:25 +0800 Subject: [PATCH 10/14] feat: filter by embedding --- src/memos/api/handlers/search_handler.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/memos/api/handlers/search_handler.py b/src/memos/api/handlers/search_handler.py index 93eff185b..6eda1e2aa 100644 --- a/src/memos/api/handlers/search_handler.py +++ b/src/memos/api/handlers/search_handler.py @@ -377,16 +377,13 @@ def _extract_embeddings(memories: list[dict[str, Any]]) -> list[list[float]] | N @staticmethod def _strip_embeddings(results: dict[str, Any]) -> None: - for bucket in results.get("text_mem", []): - for mem in bucket.get("memories", []): - metadata = mem.get("metadata", {}) - if "embedding" in metadata: - metadata["embedding"] = [] - for bucket in results.get("tool_mem", []): - for mem in bucket.get("memories", []): - metadata = mem.get("metadata", {}) - if "embedding" in metadata: - metadata["embedding"] = [] + for _mem_type, mem_results in results.items(): + if isinstance(mem_results, list): + for bucket in mem_results: + for mem in bucket.get("memories", []): + metadata = mem.get("metadata", {}) + if "embedding" in metadata: + metadata["embedding"] = [] @staticmethod def _dice_similarity(text1: str, text2: str) -> float: From 538b295b5b594dd74bf0a1423b8dabdbb4b166ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=AD=E9=98=B3=E9=98=B3?= Date: Thu, 29 Jan 2026 20:31:43 +0800 Subject: [PATCH 11/14] feat: modify skill --- .../mem_reader/read_skill_memory/process_skill_memory.py | 2 ++ src/memos/templates/skill_mem_prompt.py | 6 ++++-- 2 files changed, 6 insertions(+), 2 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 b5cbfa2c7..30ba2bf09 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 @@ -218,6 +218,7 @@ def _extract_skill_memory_by_llm( ) prompt = [{"role": "user", "content": prompt_content}] + logger.info(f"[Skill Memory]: Prompt {prompt_content}") # Call LLM to extract skill memory with retry logic for attempt in range(3): @@ -227,6 +228,7 @@ def _extract_skill_memory_by_llm( llm_kwargs = {"model_name_or_path": skills_llm} if skills_llm else {} response_text = llm.generate(prompt, **llm_kwargs) # Clean up response (remove Markdown code blocks if present) + logger.info(f"[Skill Memory]: response_text {response_text}") response_text = response_text.strip() response_text = response_text.replace("```json", "").replace("```", "").strip() diff --git a/src/memos/templates/skill_mem_prompt.py b/src/memos/templates/skill_mem_prompt.py index f1b44f341..f36cefcc1 100644 --- a/src/memos/templates/skill_mem_prompt.py +++ b/src/memos/templates/skill_mem_prompt.py @@ -158,7 +158,7 @@ # 对话消息的上下文chat_history {chat_history} -# 对话消息 +# 当前对话消息 {messages} # 核心原则 @@ -186,6 +186,7 @@ "others": {"章节标题": "这里的内容", "reference.md": "# 此技能的参考内容"}, "update": false, "old_memory_id": "", + "content_of_current_message": "", "whether_use_chat_history": false, "content_of_related_chat_history": "", } @@ -206,6 +207,7 @@ - **examples**:展示最终任务成果的输出模板,包括格式、章节和内容组织结构。应展示应用此技能后任务结果的样子,包含所有必要的部分。内容可以省略但必须展示完整结构。使用 markdown 格式以提高可读性 - **update**:更新现有技能为true,新建为false - **old_memory_id**:被更新技能的ID,新建则为空字符串 +- **content_of_current_message**: 从当前对话消息中提取的核心内容(简写但必填), - **whether_use_chat_history**:是否从 chat_history 中引用了 messages 中没有的内容并提取到skill中 - **content_of_related_chat_history**:若 whether_use_chat_history 为 true, 仅需概括性说明所使用的历史信息类型(如“长期偏好:文化类景点优先”), @@ -217,7 +219,7 @@ - "examples"应展示完整的最终输出格式和结构,包含所有必要章节 - "others"包含补充说明或扩展信息 - 无法提取技能时返回null -- 注意区分chat_history与对话消息 +- 注意区分chat_history与当前对话消息,如果能提出skill,必须有一部分来自于当前对话消息 # 输出格式 仅输出JSON对象。 From e6f1a8f4ea4d2d5eab4304f09cd519bdbe5ac24f Mon Sep 17 00:00:00 2001 From: CaralHsi Date: Thu, 29 Jan 2026 20:32:09 +0800 Subject: [PATCH 12/14] feat: skill with history (#986) * feat: skill with history * feat: pass chat-history into skill * feat: modify chat-history passing in skills * feat: modify code * fix: we don't need to pass history in part B * fix: process skill memory * feat: we do not return None with few history now * feat: update skill * feat: modify _split_task_chunk_by_llm * feat: filter by embedding * feat: modify skill --------- Co-authored-by: yuan.wang Co-authored-by: Wang Daoji <75928131+Wang-Daoji@users.noreply.github.com> --- src/memos/api/handlers/search_handler.py | 17 ++--- src/memos/mem_reader/base.py | 2 +- src/memos/mem_reader/multi_modal_struct.py | 2 +- .../read_skill_memory/process_skill_memory.py | 68 +++++++++++++++---- src/memos/mem_reader/simple_struct.py | 5 +- src/memos/mem_scheduler/general_scheduler.py | 4 ++ .../mem_scheduler/schemas/message_schemas.py | 4 ++ src/memos/multi_mem_cube/single_cube.py | 2 + src/memos/templates/skill_mem_prompt.py | 38 ++++++++++- 9 files changed, 114 insertions(+), 28 deletions(-) diff --git a/src/memos/api/handlers/search_handler.py b/src/memos/api/handlers/search_handler.py index 93eff185b..6eda1e2aa 100644 --- a/src/memos/api/handlers/search_handler.py +++ b/src/memos/api/handlers/search_handler.py @@ -377,16 +377,13 @@ def _extract_embeddings(memories: list[dict[str, Any]]) -> list[list[float]] | N @staticmethod def _strip_embeddings(results: dict[str, Any]) -> None: - for bucket in results.get("text_mem", []): - for mem in bucket.get("memories", []): - metadata = mem.get("metadata", {}) - if "embedding" in metadata: - metadata["embedding"] = [] - for bucket in results.get("tool_mem", []): - for mem in bucket.get("memories", []): - metadata = mem.get("metadata", {}) - if "embedding" in metadata: - metadata["embedding"] = [] + for _mem_type, mem_results in results.items(): + if isinstance(mem_results, list): + for bucket in mem_results: + for mem in bucket.get("memories", []): + metadata = mem.get("metadata", {}) + if "embedding" in metadata: + metadata["embedding"] = [] @staticmethod def _dice_similarity(text1: str, text2: str) -> float: diff --git a/src/memos/mem_reader/base.py b/src/memos/mem_reader/base.py index b034c9367..0bfd31fa8 100644 --- a/src/memos/mem_reader/base.py +++ b/src/memos/mem_reader/base.py @@ -42,7 +42,7 @@ def set_searcher(self, searcher: "Searcher | None") -> None: @abstractmethod def get_memory( - self, scene_data: list, type: str, info: dict[str, Any], mode: str = "fast" + self, scene_data: list, type: str, info: dict[str, Any], mode: str = "fast", **kwargs ) -> list[list[TextualMemoryItem]]: """Various types of memories extracted from scene_data""" diff --git a/src/memos/mem_reader/multi_modal_struct.py b/src/memos/mem_reader/multi_modal_struct.py index 236a8f180..f6a016556 100644 --- a/src/memos/mem_reader/multi_modal_struct.py +++ b/src/memos/mem_reader/multi_modal_struct.py @@ -862,7 +862,7 @@ def _process_multi_modal_data( # Part A: call llm in parallel using thread pool fine_memory_items = [] - with ContextThreadPoolExecutor(max_workers=2) as executor: + with ContextThreadPoolExecutor(max_workers=3) as executor: future_string = executor.submit( self._process_string_fine, fast_memory_items, info, custom_tags, **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 bb809e69d..30ba2bf09 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 @@ -105,6 +105,17 @@ def _reconstruct_messages_from_memory_items(memory_items: list[TextualMemoryItem return reconstructed_messages +def _preprocess_extract_messages( + history: MessageList, messages: MessageList +) -> (MessageList, MessageList): + """Process data and check whether to extract skill memory""" + history = history[-20:] + if (len(history) + len(messages)) < 10: + # TODO: maybe directly return [] + logger.warning("[PROCESS_SKILLS] Not enough messages to extract skill memory") + return history, messages + + def _add_index_to_message(messages: MessageList) -> MessageList: for i, message in enumerate(messages): message["idx"] = i @@ -144,18 +155,27 @@ def _split_task_chunk_by_llm(llm: BaseLLM, messages: MessageList) -> dict[str, M message_indices = item["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: + if isinstance(indices, list) and len(indices) == 1: + start, end = indices[0], indices[0] + 1 + elif isinstance(indices, int): + start, end = indices, indices + 1 + elif isinstance(indices, list) and len(indices) == 2: + start, end = indices[0], indices[1] + 1 + else: logger.warning( f"[PROCESS_SKILLS] Invalid message indices format for task '{task_name}': {indices}, skipping" ) continue - start, end = indices - task_chunks.setdefault(task_name, []).extend(messages[start : end + 1]) + task_chunks.setdefault(task_name, []).extend(messages[start:end]) return task_chunks def _extract_skill_memory_by_llm( - messages: MessageList, old_memories: list[TextualMemoryItem], llm: BaseLLM + messages: MessageList, + old_memories: list[TextualMemoryItem], + llm: BaseLLM, + chat_history: MessageList, + chat_history_max_length: int = 5000, ) -> dict[str, Any]: old_memories_dict = [skill_memory.model_dump() for skill_memory in old_memories] old_mem_references = [ @@ -169,7 +189,7 @@ def _extract_skill_memory_by_llm( "examples": mem["metadata"]["examples"], "tags": mem["metadata"]["tags"], "scripts": mem["metadata"].get("scripts"), - "others": mem["metadata"]["others"], + "others": mem["metadata"].get("others"), } for mem in old_memories_dict ] @@ -179,17 +199,26 @@ def _extract_skill_memory_by_llm( [f"{message['role']}: {message['content']}" for message in messages] ) + # Prepare history context + chat_history_context = "\n".join( + [f"{history['role']}: {history['content']}" for history in chat_history] + ) + chat_history_context = chat_history_context[-chat_history_max_length:] + # Prepare old memories context old_memories_context = json.dumps(old_mem_references, ensure_ascii=False, indent=2) # Prepare prompt 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_content = ( + template.replace("{old_memories}", old_memories_context) + .replace("{messages}", messages_context) + .replace("{chat_history}", chat_history_context) ) prompt = [{"role": "user", "content": prompt_content}] + logger.info(f"[Skill Memory]: Prompt {prompt_content}") # Call LLM to extract skill memory with retry logic for attempt in range(3): @@ -198,7 +227,8 @@ def _extract_skill_memory_by_llm( skills_llm = os.getenv("SKILLS_LLM", None) llm_kwargs = {"model_name_or_path": skills_llm} if skills_llm else {} response_text = llm.generate(prompt, **llm_kwargs) - # Clean up response (remove markdown code blocks if present) + # Clean up response (remove Markdown code blocks if present) + logger.info(f"[Skill Memory]: response_text {response_text}") response_text = response_text.strip() response_text = response_text.replace("```json", "").replace("```", "").strip() @@ -537,6 +567,11 @@ def process_skill_memory_fine( ) return [] + chat_history = kwargs.get("chat_history") + if not chat_history or not isinstance(chat_history, list): + chat_history = [] + logger.warning("[PROCESS_SKILLS] History is None in Skills") + # Validate skills_dir has required keys required_keys = ["skills_local_dir", "skills_oss_dir"] missing_keys = [key for key in required_keys if key not in skills_dir_config] @@ -552,7 +587,13 @@ def process_skill_memory_fine( return [] messages = _reconstruct_messages_from_memory_items(fast_memory_items) + + chat_history, messages = _preprocess_extract_messages(chat_history, messages) + if not messages: + return [] + messages = _add_index_to_message(messages) + chat_history = _add_index_to_message(chat_history) task_chunks = _split_task_chunk_by_llm(llm, messages) if not task_chunks: @@ -594,6 +635,7 @@ def process_skill_memory_fine( messages, related_skill_memories_by_task.get(task_type, []), llm, + chat_history, ): task_type for task_type, messages in task_chunks.items() } @@ -608,7 +650,7 @@ def process_skill_memory_fine( # write skills to file and get zip paths skill_memory_with_paths = [] - with ContextThreadPoolExecutor(max_workers=min(len(skill_memories), 5)) as executor: + with ContextThreadPoolExecutor(max_workers=5) as executor: futures = { executor.submit( _write_skills_to_file, skill_memory, info, skills_dir_config @@ -713,9 +755,11 @@ def process_skill_memory_fine( continue # TODO: deprecate this funtion and call - for skill_memory in skill_memory_items: + for skill_memory, skill_memory_item in zip(skill_memories, skill_memory_items, strict=False): + if skill_memory.get("update", False) and skill_memory.get("old_memory_id", ""): + continue add_id_to_mysql( - memory_id=skill_memory.id, mem_cube_id=kwargs.get("user_name", info.get("user_id", "")) + memory_id=skill_memory_item.id, + mem_cube_id=kwargs.get("user_name", info.get("user_id", "")), ) - return skill_memory_items diff --git a/src/memos/mem_reader/simple_struct.py b/src/memos/mem_reader/simple_struct.py index f3ae98ccb..2c4fee853 100644 --- a/src/memos/mem_reader/simple_struct.py +++ b/src/memos/mem_reader/simple_struct.py @@ -428,6 +428,7 @@ def get_memory( info: dict[str, Any], mode: str = "fine", user_name: str | None = None, + **kwargs, ) -> list[list[TextualMemoryItem]]: """ Extract and classify memory content from scene_data. @@ -471,7 +472,9 @@ def get_memory( # Backward compatibility, after coercing scene_data, we only tackle # with standard scene_data type: MessagesType standard_scene_data = coerce_scene_data(scene_data, type) - return self._read_memory(standard_scene_data, type, info, mode, user_name=user_name) + return self._read_memory( + standard_scene_data, type, info, mode, user_name=user_name, **kwargs + ) def rewrite_memories( self, messages: list[dict], memory_list: list[TextualMemoryItem], user_only: bool = True diff --git a/src/memos/mem_scheduler/general_scheduler.py b/src/memos/mem_scheduler/general_scheduler.py index d4ac09cc3..74e50a514 100644 --- a/src/memos/mem_scheduler/general_scheduler.py +++ b/src/memos/mem_scheduler/general_scheduler.py @@ -764,6 +764,7 @@ def process_message(message: ScheduleMessageItem): content = message.content user_name = message.user_name info = message.info or {} + chat_history = message.chat_history # Parse the memory IDs from content mem_ids = json.loads(content) if isinstance(content, str) else content @@ -790,6 +791,7 @@ def process_message(message: ScheduleMessageItem): custom_tags=info.get("custom_tags", None), task_id=message.task_id, info=info, + chat_history=chat_history, ) logger.info( @@ -817,6 +819,7 @@ def _process_memories_with_reader( custom_tags: list[str] | None = None, task_id: str | None = None, info: dict | None = None, + chat_history: list | None = None, ) -> None: logger.info( f"[DIAGNOSTIC] general_scheduler._process_memories_with_reader called. mem_ids: {mem_ids}, user_id: {user_id}, mem_cube_id: {mem_cube_id}, task_id: {task_id}" @@ -878,6 +881,7 @@ def _process_memories_with_reader( type="chat", custom_tags=custom_tags, user_name=user_name, + chat_history=chat_history, ) except Exception as e: logger.warning(f"{e}: Fail to transfer mem: {memory_items}") diff --git a/src/memos/mem_scheduler/schemas/message_schemas.py b/src/memos/mem_scheduler/schemas/message_schemas.py index cf3019d5e..c7f270f19 100644 --- a/src/memos/mem_scheduler/schemas/message_schemas.py +++ b/src/memos/mem_scheduler/schemas/message_schemas.py @@ -54,6 +54,7 @@ class ScheduleMessageItem(BaseModel, DictConversionMixin): default=None, description="Optional business-level task ID. Multiple items can share the same task_id.", ) + chat_history: list | None = Field(default=None, description="user chat history") # Pydantic V2 model configuration model_config = ConfigDict( @@ -89,6 +90,7 @@ def to_dict(self) -> dict: "timestamp": self.timestamp.isoformat(), "user_name": self.user_name, "task_id": self.task_id if self.task_id is not None else "", + "chat_history": self.chat_history if self.chat_history is not None else [], } @classmethod @@ -104,6 +106,7 @@ def from_dict(cls, data: dict) -> "ScheduleMessageItem": timestamp=datetime.fromisoformat(data["timestamp"]), user_name=data.get("user_name"), task_id=data.get("task_id"), + chat_history=data.get("chat_history"), ) @@ -158,6 +161,7 @@ class ScheduleLogForWebItem(BaseModel, DictConversionMixin): default=None, description="Completion status of the task (e.g., 'completed', 'failed')" ) source_doc_id: str | None = Field(default=None, description="Source document ID") + chat_history: list | None = Field(default=None, description="user chat history") def debug_info(self) -> dict[str, Any]: """Return structured debug information for logging purposes.""" diff --git a/src/memos/multi_mem_cube/single_cube.py b/src/memos/multi_mem_cube/single_cube.py index 2f7883548..bd026a51d 100644 --- a/src/memos/multi_mem_cube/single_cube.py +++ b/src/memos/multi_mem_cube/single_cube.py @@ -553,6 +553,7 @@ def _schedule_memory_tasks( timestamp=datetime.utcnow(), user_name=self.cube_id, info=add_req.info, + chat_history=add_req.chat_history, ) self.mem_scheduler.submit_messages(messages=[message_item_read]) self.logger.info( @@ -807,6 +808,7 @@ def _process_text_mem( }, mode=extract_mode, user_name=user_context.mem_cube_id, + chat_history=add_req.chat_history, ) self.logger.info( f"Time for get_memory in extract mode {extract_mode}: {time.time() - init_time}" diff --git a/src/memos/templates/skill_mem_prompt.py b/src/memos/templates/skill_mem_prompt.py index 0bc0c1809..f36cefcc1 100644 --- a/src/memos/templates/skill_mem_prompt.py +++ b/src/memos/templates/skill_mem_prompt.py @@ -78,6 +78,9 @@ # Existing Skill Memories {old_memories} +# Chat_history +{chat_history} + # Conversation Messages {messages} @@ -86,6 +89,11 @@ 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. +5. **History Usage Constraints**: + - `chat_history` serves only as auxiliary context to supplement stable preferences or methodologies that are not explicitly stated in `messages` but may affect skill abstraction. + - `chat_history` may be considered only when it provides information **missing from `messages`** and **relevant to the current task’s goals, execution approach, or constraints**. + - `chat_history` must not be the primary source of a skill, and may only be used to enrich auxiliary fields such as `preference` or `experience`. + - If `chat_history` does not provide any valid information beyond what already exists in `messages`, or contains only greetings or background content, it must be completely ignored. # Output Format ```json @@ -100,7 +108,9 @@ "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": "" + "old_memory_id": "", + "whether_use_chat_history": false, + "content_of_related_chat_history": "" } ``` @@ -119,6 +129,10 @@ - **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 +- **whether_use_chat_history**: Indicates whether information from chat_history that does not appear in messages was incorporated into the skill +- **content_of_related_chat_history**: + If whether_use_chat_history is true, provide a high-level summary of the type of historical information used (e.g., “long-term preference: prioritizes cultural attractions”); do not quote the original dialogue verbatim + If not used, leave this field as an empty string # Critical Guidelines - Keep all fields general except "examples" @@ -141,7 +155,10 @@ # 现有技能记忆 {old_memories} -# 对话消息 +# 对话消息的上下文chat_history +{chat_history} + +# 当前对话消息 {messages} # 核心原则 @@ -149,6 +166,11 @@ 2. **普适性**:除"examples"外,所有字段必须保持通用,与具体场景无关。 3. **相似性检查**:如存在相似技能,设置"update": true 及"old_memory_id"。否则设置"update": false 并将"old_memory_id"留空。 4. **语言一致性**:与对话语言保持一致。 +5. **历史使用约束**: + - chat_history 仅作为辅助上下文,用于补充 messages 中未明确出现的、但会影响技能抽象的稳定偏好或方法论。 + - 当 chat_history 能提供 messages 中缺失、且与当前任务目标、执行方式或约束相关的信息增量时,可以纳入考虑。 + - chat_history 不得作为技能的主要来源,仅可用于完善 preference、experience 等辅助字段。 + - 若 chat_history 未提供任何 messages 中不存在的有效信息,或仅包含寒暄、背景性内容,应完全忽略。 # 输出格式 ```json @@ -163,7 +185,10 @@ "scripts": {"script_name.py": "# Python 代码\nprint('Hello')", "another_script.py": "# 更多代码\nimport os"}, "others": {"章节标题": "这里的内容", "reference.md": "# 此技能的参考内容"}, "update": false, - "old_memory_id": "" + "old_memory_id": "", + "content_of_current_message": "", + "whether_use_chat_history": false, + "content_of_related_chat_history": "", } ``` @@ -182,12 +207,19 @@ - **examples**:展示最终任务成果的输出模板,包括格式、章节和内容组织结构。应展示应用此技能后任务结果的样子,包含所有必要的部分。内容可以省略但必须展示完整结构。使用 markdown 格式以提高可读性 - **update**:更新现有技能为true,新建为false - **old_memory_id**:被更新技能的ID,新建则为空字符串 +- **content_of_current_message**: 从当前对话消息中提取的核心内容(简写但必填), +- **whether_use_chat_history**:是否从 chat_history 中引用了 messages 中没有的内容并提取到skill中 +- **content_of_related_chat_history**:若 whether_use_chat_history 为 true, + 仅需概括性说明所使用的历史信息类型(如“长期偏好:文化类景点优先”), + 不要求逐字引用原始对话内容; + 若未使用,则置为空字符串。 # 关键指导 - 除"examples"外保持所有字段通用 - "examples"应展示完整的最终输出格式和结构,包含所有必要章节 - "others"包含补充说明或扩展信息 - 无法提取技能时返回null +- 注意区分chat_history与当前对话消息,如果能提出skill,必须有一部分来自于当前对话消息 # 输出格式 仅输出JSON对象。 From 0f4634ab901e106545613bc53046c12a973a3bc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=AD=E9=98=B3=E9=98=B3?= Date: Thu, 29 Jan 2026 21:01:07 +0800 Subject: [PATCH 13/14] feat: reinforce update rule --- src/memos/templates/skill_mem_prompt.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/memos/templates/skill_mem_prompt.py b/src/memos/templates/skill_mem_prompt.py index f36cefcc1..df64d736d 100644 --- a/src/memos/templates/skill_mem_prompt.py +++ b/src/memos/templates/skill_mem_prompt.py @@ -171,6 +171,7 @@ - 当 chat_history 能提供 messages 中缺失、且与当前任务目标、执行方式或约束相关的信息增量时,可以纳入考虑。 - chat_history 不得作为技能的主要来源,仅可用于完善 preference、experience 等辅助字段。 - 若 chat_history 未提供任何 messages 中不存在的有效信息,或仅包含寒暄、背景性内容,应完全忽略。 +6. 如果你提取的抽象方法论和已有的技能记忆描述的是同一个主题(比如同一个生活场景),请务必使用更新操作,不要新建一个方法论,注意合理的追加到已有的技能记忆上,保证通顺且不丢失已有方法论的信息。 # 输出格式 ```json @@ -220,6 +221,8 @@ - "others"包含补充说明或扩展信息 - 无法提取技能时返回null - 注意区分chat_history与当前对话消息,如果能提出skill,必须有一部分来自于当前对话消息 +- 一定仅在必要时才新建方法论,同样的场景尽量合并("update": true), +如饮食规划合并为一条,不要已有“饮食规划”的情况下,再新增一个“生酮饮食规划”。 # 输出格式 仅输出JSON对象。 From cb5194165cf9d5a5c965437884c408e0a5aadc7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=AD=E9=98=B3=E9=98=B3?= Date: Fri, 30 Jan 2026 11:05:29 +0800 Subject: [PATCH 14/14] fix: batch ContextThreadPoolExecutor bug --- .../mem_reader/read_skill_memory/process_skill_memory.py | 4 ++-- 1 file changed, 2 insertions(+), 2 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 30ba2bf09..6bd18808d 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 @@ -602,7 +602,7 @@ def process_skill_memory_fine( # 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: + with ContextThreadPoolExecutor(max_workers=5) as executor: recall_futures = { executor.submit( _recall_related_skill_memories, @@ -628,7 +628,7 @@ def process_skill_memory_fine( related_skill_memories_by_task[task_name] = [] skill_memories = [] - with ContextThreadPoolExecutor(max_workers=min(len(task_chunks), 5)) as executor: + with ContextThreadPoolExecutor(max_workers=5) as executor: futures = { executor.submit( _extract_skill_memory_by_llm,