diff --git a/pyproject.toml b/pyproject.toml index ff7c9699a..ba2f03de9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ ############################################################################## name = "MemoryOS" -version = "2.0.8" +version = "2.0.9" description = "Intelligence Begins with Memory" license = {text = "Apache-2.0"} readme = "README.md" diff --git a/src/memos/__init__.py b/src/memos/__init__.py index 36cc0b5b5..783c1d7dc 100644 --- a/src/memos/__init__.py +++ b/src/memos/__init__.py @@ -1,4 +1,4 @@ -__version__ = "2.0.8" +__version__ = "2.0.9" from memos.configs.mem_cube import GeneralMemCubeConfig from memos.configs.mem_os import MOSConfig diff --git a/src/memos/llms/ollama.py b/src/memos/llms/ollama.py index bd92f9625..e87734bd5 100644 --- a/src/memos/llms/ollama.py +++ b/src/memos/llms/ollama.py @@ -88,7 +88,7 @@ def generate(self, messages: MessageList, **kwargs) -> Any: ) str_response = response.message.content if self.config.remove_think_prefix: - return remove_thinking_tags(str_response) + return remove_thinking_tags(str_response or "") else: return str_thinking + str_response diff --git a/src/memos/llms/openai.py b/src/memos/llms/openai.py index f6bb4efc1..9a29ea68a 100644 --- a/src/memos/llms/openai.py +++ b/src/memos/llms/openai.py @@ -56,7 +56,7 @@ def _parse_response(self, response) -> str: if isinstance(reasoning_content, str) and reasoning_content: reasoning_content = f"{reasoning_content}" if self.config.remove_think_prefix: - return remove_thinking_tags(response_content) + return remove_thinking_tags(response_content or "") if reasoning_content: return reasoning_content + (response_content or "") return response_content or "" @@ -202,7 +202,7 @@ def generate(self, messages: MessageList, **kwargs) -> str: return self.tool_call_parser(response.choices[0].message.tool_calls) response_content = response.choices[0].message.content if self.config.remove_think_prefix: - return remove_thinking_tags(response_content) + return remove_thinking_tags(response_content or "") else: return response_content or "" diff --git a/src/memos/mem_reader/multi_modal_struct.py b/src/memos/mem_reader/multi_modal_struct.py index 2745a1bee..7aa8b1c5e 100644 --- a/src/memos/mem_reader/multi_modal_struct.py +++ b/src/memos/mem_reader/multi_modal_struct.py @@ -927,6 +927,10 @@ def _process_tool_trajectory_fine( project_id = user_context.project_id if user_context else None for fast_item in fast_memory_items: + sources = fast_item.metadata.sources or [] + if not isinstance(sources, list): + sources = [sources] + # Extract memory text (string content) mem_str = fast_item.memory or "" if not mem_str.strip() or ( @@ -954,6 +958,7 @@ def _process_tool_trajectory_fine( tool_used_status=m.get("tool_used_status", []), manager_user_id=manager_user_id, project_id=project_id, + sources=sources, ) fine_memory_items.append(node) except Exception as e: 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 a9a727b08..0b0c04252 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 @@ -19,7 +19,11 @@ 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.item import ( + SourceMessage, + TextualMemoryItem, + TreeNodeTextualMemoryMetadata, +) from memos.memories.textual.tree_text_memory.retrieve.searcher import Searcher from memos.templates.skill_mem_prompt import ( OTHERS_GENERATION_PROMPT, @@ -91,6 +95,7 @@ def _batch_extract_skills( try: skill_memory = future.result() if skill_memory: + skill_memory["_task_type"] = task_type results.append((skill_memory, task_type, task_chunks.get(task_type, []))) except Exception as e: logger.warning( @@ -901,6 +906,7 @@ def create_skill_memory_item( skill_memory: dict[str, Any], info: dict[str, Any], embedder: BaseEmbedder | None = None, + sources: list[SourceMessage] | None = None, **kwargs: Any, ) -> TextualMemoryItem: info_ = info.copy() @@ -923,7 +929,7 @@ def create_skill_memory_item( status="activated", tags=skill_memory.get("tags") or skill_memory.get("trigger", []), key=skill_memory.get("name", ""), - sources=[], + sources=sources or [], usage=[], background="", confidence=0.99, @@ -1097,6 +1103,7 @@ def _simple_extract(): try: skill_memory = future.result() if skill_memory: + skill_memory["_task_type"] = task_type memories.append(skill_memory) except Exception as e: logger.warning( @@ -1223,11 +1230,32 @@ def _full_extract(): except Exception as cleanup_error: logger.warning(f"[PROCESS_SKILLS] Error cleaning up local files: {cleanup_error}") + # Build source lookup: (role, content) → SourceMessage from fast_memory_items + source_lookup: dict[tuple[str, str], SourceMessage] = {} + for fast_item in fast_memory_items: + for source in getattr(fast_item.metadata, "sources", []) or []: + source_lookup.setdefault((source.role, source.content), source) + # Create TextualMemoryItem objects skill_memory_items = [] for skill_memory in skill_memories: try: - memory_item = create_skill_memory_item(skill_memory, info, embedder, **kwargs) + # Match sources precisely via the task chunk messages that produced this skill + task_type = skill_memory.pop("_task_type", None) + chunk_messages = task_chunks.get(task_type, []) if task_type else [] + skill_sources = [] + seen = set() + for msg in chunk_messages: + key = (msg.get("role"), msg.get("content")) + if key not in seen: + seen.add(key) + source = source_lookup.get(key) + if source: + skill_sources.append(source) + + memory_item = create_skill_memory_item( + skill_memory, info, embedder, sources=skill_sources, **kwargs + ) skill_memory_items.append(memory_item) except Exception as e: logger.warning(f"[PROCESS_SKILLS] Error creating skill memory item: {e}")