From 0fbb2dfd07b7967184109879a121279610cfec95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=AD=E9=98=B3=E9=98=B3?= Date: Thu, 5 Mar 2026 15:22:05 +0800 Subject: [PATCH 1/5] feat: add auto_cleanup_working --- src/memos/api/product_models.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/memos/api/product_models.py b/src/memos/api/product_models.py index 6f112b9a7..59f7f74c8 100644 --- a/src/memos/api/product_models.py +++ b/src/memos/api/product_models.py @@ -858,6 +858,13 @@ class DeleteMemoryRequest(BaseRequest): memory_ids: list[str] | None = Field(None, description="Memory IDs") file_ids: list[str] | None = Field(None, description="File IDs") filter: dict[str, Any] | None = Field(None, description="Filter for the memory") + auto_cleanup_working: bool | None = Field( + False, + description=( + "(Internal) Whether to automatically delete related WorkingMemory nodes " + "based on working_binding metadata when deleting by memory_ids." + ), + ) class SuggestionRequest(BaseRequest): From 7ae54f0d8a97177d938ffdf573eab843d04f2b6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=AD=E9=98=B3=E9=98=B3?= Date: Thu, 5 Mar 2026 15:34:44 +0800 Subject: [PATCH 2/5] feat: add delete by working_binding id in handle_delete_memories --- src/memos/api/handlers/memory_handler.py | 41 +++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/memos/api/handlers/memory_handler.py b/src/memos/api/handlers/memory_handler.py index 2ab8f50c7..72c6a3da8 100644 --- a/src/memos/api/handlers/memory_handler.py +++ b/src/memos/api/handlers/memory_handler.py @@ -320,7 +320,10 @@ def handle_delete_memories(delete_mem_req: DeleteMemoryRequest, naive_mem_cube: Now unified to delete from text_mem only (includes preferences). """ logger.info( - f"[Delete memory request] writable_cube_ids: {delete_mem_req.writable_cube_ids}, memory_ids: {delete_mem_req.memory_ids}" + "[Delete memory request] writable_cube_ids: %s, memory_ids: %s, auto_cleanup_working: %s", + delete_mem_req.writable_cube_ids, + delete_mem_req.memory_ids, + getattr(delete_mem_req, "auto_cleanup_working", False), ) # Validate that only one of memory_ids, file_ids, or filter is provided provided_params = [ @@ -335,6 +338,31 @@ def handle_delete_memories(delete_mem_req: DeleteMemoryRequest, naive_mem_cube: ) try: + working_ids_to_delete: set[str] = set() + # When deleting by explicit memory_ids and auto_cleanup_working is enabled, + # collect related WorkingMemory ids from working_binding + if delete_mem_req.memory_ids is not None and getattr( + delete_mem_req, "auto_cleanup_working", False + ): + try: + memories = naive_mem_cube.text_mem.get_by_ids(memory_ids=delete_mem_req.memory_ids) + except Exception as e: + logger.warning("Failed to fetch memories before delete for working cleanup: %s", e) + memories = [] + + if memories: + import re + + pattern = re.compile(r"\[working_binding:([0-9a-fA-F-]{36})\]") + for mem in memories: + metadata = mem.get("metadata") or {} + bg = metadata.get("background") or "" + if not isinstance(bg, str): + continue + match = pattern.search(bg) + if match: + working_ids_to_delete.add(match.group(1)) + if delete_mem_req.memory_ids is not None: # Unified deletion from text_mem (includes preferences) naive_mem_cube.text_mem.delete_by_memory_ids(delete_mem_req.memory_ids) @@ -344,6 +372,17 @@ def handle_delete_memories(delete_mem_req: DeleteMemoryRequest, naive_mem_cube: ) elif delete_mem_req.filter is not None: naive_mem_cube.text_mem.delete_by_filter(filter=delete_mem_req.filter) + + # After main deletion, optionally clean up related WorkingMemory nodes. + if working_ids_to_delete: + try: + logger.info( + "Auto-cleanup WorkingMemory nodes after delete, count=%d", + len(working_ids_to_delete), + ) + naive_mem_cube.text_mem.delete_by_memory_ids(list(working_ids_to_delete)) + except Exception as e: + logger.warning("Failed to auto-cleanup WorkingMemory nodes: %s, Pass", e) except Exception as e: logger.error(f"Failed to delete memories: {e}", exc_info=True) return DeleteMemoryResponse( From f9f56154ee765af3ad9b0e0c0d66064072c68801 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=AD=E9=98=B3=E9=98=B3?= Date: Thu, 5 Mar 2026 17:56:16 +0800 Subject: [PATCH 3/5] feat: working memory ids --- src/memos/memories/textual/tree_text_memory/organize/manager.py | 1 - 1 file changed, 1 deletion(-) 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 df419f0c1..96453f5a0 100644 --- a/src/memos/memories/textual/tree_text_memory/organize/manager.py +++ b/src/memos/memories/textual/tree_text_memory/organize/manager.py @@ -235,7 +235,6 @@ def _submit_batches(nodes: list[dict], node_kind: str) -> None: exc_info=e, ) - _submit_batches(working_nodes, "WorkingMemory") _submit_batches(graph_nodes, "graph memory") if graph_node_ids and self.is_reorganize: From d2b980d1b12fbd7c22ba5549fde73fd4a0935cc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=AD=E9=98=B3=E9=98=B3?= Date: Thu, 5 Mar 2026 22:12:41 +0800 Subject: [PATCH 4/5] feat: add image_id in image_parser --- .../read_multi_modal/file_content_parser.py | 7 +++++- .../read_multi_modal/image_parser.py | 25 ++++++++++++++----- .../read_multi_modal/multi_modal_parser.py | 7 +++++- .../read_multi_modal/user_parser.py | 1 + src/memos/memories/textual/item.py | 1 + ...hat_completion_content_part_image_param.py | 3 +++ 6 files changed, 36 insertions(+), 8 deletions(-) diff --git a/src/memos/mem_reader/read_multi_modal/file_content_parser.py b/src/memos/mem_reader/read_multi_modal/file_content_parser.py index 00e02abda..c8f944e41 100644 --- a/src/memos/mem_reader/read_multi_modal/file_content_parser.py +++ b/src/memos/mem_reader/read_multi_modal/file_content_parser.py @@ -470,7 +470,7 @@ def parse_fast( file_data = file_info.get("file_data", "") file_id = file_info.get("file_id", "") filename = file_info.get("filename", "") - file_url_flag = False + file_url_flag = bool(file_info) # Build content string based on available information content_parts = [] @@ -651,6 +651,9 @@ def parse_fine( file_id = file_info.get("file_id", "") filename = file_info.get("filename", "") + # Whether to keep full file_info in sources + file_url_flag = bool(file_info) + # Extract custom_tags from kwargs (for LLM extraction) custom_tags = kwargs.get("custom_tags") @@ -683,6 +686,7 @@ def parse_fine( url_str = file_data[1:] if file_data.startswith("@") else file_data if url_str.startswith(("http://", "https://")): + file_url_flag = True parsed_text, temp_file_path, is_markdown = self._handle_url( url_str, filename ) @@ -793,6 +797,7 @@ def _make_memory_item( chunk_index=chunk_idx, chunk_total=total_chunks, chunk_content=chunk_content, + file_url_flag=file_url_flag, ) return TextualMemoryItem( memory=value, diff --git a/src/memos/mem_reader/read_multi_modal/image_parser.py b/src/memos/mem_reader/read_multi_modal/image_parser.py index 0d5e8bcc2..43674a9cc 100644 --- a/src/memos/mem_reader/read_multi_modal/image_parser.py +++ b/src/memos/mem_reader/read_multi_modal/image_parser.py @@ -51,15 +51,23 @@ def create_source( if isinstance(image_url, dict): url = image_url.get("url", "") detail = image_url.get("detail", "auto") + image_info = image_url + return SourceMessage( + type="image", + content=url, + url=url, + detail=detail, + image_info=image_info, + ) else: url = str(image_url) detail = "auto" - return SourceMessage( - type="image", - content=url, - url=url, - detail=detail, - ) + return SourceMessage( + type="image", + content=url, + url=url, + detail=detail, + ) return SourceMessage(type="image", content=str(message)) def rebuild_from_source( @@ -74,11 +82,16 @@ def rebuild_from_source( or (source.content or "").replace("[image_url]: ", "") ) detail = getattr(source, "detail", "auto") + image_id = "" + image_info = source.image_info + if image_info and isinstance(image_info, dict): + image_id = image_info.get("image_id") return { "type": "image_url", "image_url": { "url": url, "detail": detail, + "image_id": str(image_id), }, } diff --git a/src/memos/mem_reader/read_multi_modal/multi_modal_parser.py b/src/memos/mem_reader/read_multi_modal/multi_modal_parser.py index 81bd25902..4750d181b 100644 --- a/src/memos/mem_reader/read_multi_modal/multi_modal_parser.py +++ b/src/memos/mem_reader/read_multi_modal/multi_modal_parser.py @@ -4,6 +4,8 @@ in both fast and fine modes. """ +import traceback + from typing import Any from memos.embedders.base import BaseEmbedder @@ -242,7 +244,10 @@ def process_transfer( try: message = parser.rebuild_from_source(source) except Exception as e: - logger.error(f"[MultiModalParser] Error rebuilding message from source: {e}") + logger.error( + f"[MultiModalParser] Error rebuilding message from " + f"source: {e} {traceback.format_exc()}" + ) return [] # Parse in fine mode (pass context_items and custom_tags to parse_fine) diff --git a/src/memos/mem_reader/read_multi_modal/user_parser.py b/src/memos/mem_reader/read_multi_modal/user_parser.py index 2e5ea6eae..a5a248e9d 100644 --- a/src/memos/mem_reader/read_multi_modal/user_parser.py +++ b/src/memos/mem_reader/read_multi_modal/user_parser.py @@ -114,6 +114,7 @@ def create_source( chat_time=chat_time, message_id=message_id, image_path=image_info.get("url"), + image_info=image_info, ) source.lang = overall_lang sources.append(source) diff --git a/src/memos/memories/textual/item.py b/src/memos/memories/textual/item.py index 60af67830..a9b2c43a4 100644 --- a/src/memos/memories/textual/item.py +++ b/src/memos/memories/textual/item.py @@ -42,6 +42,7 @@ class SourceMessage(BaseModel): content: str | None = None doc_path: str | None = None file_info: dict | None = None + image_info: dict | None = None model_config = ConfigDict(extra="allow") diff --git a/src/memos/types/openai_chat_completion_types/chat_completion_content_part_image_param.py b/src/memos/types/openai_chat_completion_types/chat_completion_content_part_image_param.py index 6718bd91e..a5469cdc3 100644 --- a/src/memos/types/openai_chat_completion_types/chat_completion_content_part_image_param.py +++ b/src/memos/types/openai_chat_completion_types/chat_completion_content_part_image_param.py @@ -19,6 +19,9 @@ class ImageURL(TypedDict, total=False): [Vision guide](https://platform.openai.com/docs/guides/vision#low-or-high-fidelity-image-understanding). """ + image_id: str + """Optional custom image id for tracking image sources.""" + class ChatCompletionContentPartImageParam(TypedDict, total=False): image_url: Required[ImageURL] From 1786d443c28e29d7b7353b1adc937406a6c1f475 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=AD=E9=98=B3=E9=98=B3?= Date: Fri, 6 Mar 2026 11:17:23 +0800 Subject: [PATCH 5/5] feat: modify detect lang; fix file_content_parser --- src/memos/mem_reader/multi_modal_struct.py | 2 +- .../read_multi_modal/file_content_parser.py | 2 +- .../mem_reader/read_multi_modal/utils.py | 22 ++++++++++++++++--- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/memos/mem_reader/multi_modal_struct.py b/src/memos/mem_reader/multi_modal_struct.py index 0b3e19208..679cb711d 100644 --- a/src/memos/mem_reader/multi_modal_struct.py +++ b/src/memos/mem_reader/multi_modal_struct.py @@ -807,7 +807,7 @@ def _process_one_item( if result: fine_memory_items.extend(result) except Exception as e: - logger.error(f"[MultiModalFine] worker error: {e}") + logger.error(f"[MultiModalFine] worker error: {e} {traceback.format_exc()}") # related preceding and following rawfilememories fine_memory_items = self._relate_preceding_following_rawfile_memories(fine_memory_items) diff --git a/src/memos/mem_reader/read_multi_modal/file_content_parser.py b/src/memos/mem_reader/read_multi_modal/file_content_parser.py index c8f944e41..b440a4aef 100644 --- a/src/memos/mem_reader/read_multi_modal/file_content_parser.py +++ b/src/memos/mem_reader/read_multi_modal/file_content_parser.py @@ -367,7 +367,7 @@ def create_source( ) -> SourceMessage: """Create SourceMessage from file content part.""" if isinstance(message, dict): - file_info = message.get("file", {}) + file_info = message.get("file", {}) or {} source_dict = { "type": "file", "doc_path": file_info.get("filename") or file_info.get("file_id", ""), diff --git a/src/memos/mem_reader/read_multi_modal/utils.py b/src/memos/mem_reader/read_multi_modal/utils.py index 96918589b..2e6ab1d6e 100644 --- a/src/memos/mem_reader/read_multi_modal/utils.py +++ b/src/memos/mem_reader/read_multi_modal/utils.py @@ -45,6 +45,10 @@ ) +KEYS_DROP_LABEL = r"(text|type|image_url|imageurl|url|file|file_id|image_id|file_data)" +ID_KEYS_DROP_VALUE = r"(file_id|image_id)" + + def parse_json_result(response_text: str) -> dict: """ Parse JSON result from LLM response. @@ -356,13 +360,25 @@ def detect_lang(text): cleaned_text = re.sub(r"\[[\d\-:\s]+\]", "", cleaned_text) # remove URLs to prevent the dilution of Chinese characters cleaned_text = re.sub(r'https?://[^\s<>"{}|\\^`\[\]]+', "", cleaned_text) - # remove MessageType schema keywords (multimodal JSON noise) + # remove common id-like tokens (uuid-ish / file_id / image_id / + # my_id_01 etc.) + # uuid + cleaned_text = re.sub( + r"\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b", + " ", + cleaned_text, + flags=re.IGNORECASE, + ) + # key:value where key ends with _id or is id, and value is quoted or bare token + cleaned_text = re.sub( + r'(?i)\b[a-z_]*id\b\s*[:=]\s*(".*?"|\'.*?\'|[a-z0-9_\-]+)', " ", cleaned_text + ) cleaned_text = re.sub( - r"\b(text|type|image_url|imageurl|url)\b", "", cleaned_text, flags=re.IGNORECASE + r'(?i)\b[a-z_]*_id\b\s*[:=]\s*(".*?"|\'.*?\'|[a-z0-9_\-]+)', " ", cleaned_text ) # remove schema keywords like text / type / image_url / url cleaned_text = re.sub( - r"\b(text|type|image_url|imageurl|url|file|file_id)\b", + r"\b(text|type|image_url|imageurl|url|file|file_id|image_id|file_data)\b", "", cleaned_text, flags=re.IGNORECASE,