From 8de27afa2c95776ebe53ca106c8450a2c3165d6b Mon Sep 17 00:00:00 2001 From: Dhravya Shah Date: Tue, 18 Aug 2026 08:14:00 -0700 Subject: [PATCH] feat(python-sdks): SDK-level cross-source memory deduplication Port the normalized, priority-ordered (static > dynamic > search) profile deduplication into the Python SDKs, injecting one owned memory block per request that replaces the prior block rather than accumulating. Dedup is request-local (no shared state), so it stays correct under concurrency. Covers OpenAI, Agent Framework (middleware + context provider), Cartesia, and Pipecat. Co-Authored-By: Claude Opus 4.8 --- .../context_provider.py | 4 +- .../supermemory_agent_framework/middleware.py | 41 ++++++++++---- .../src/supermemory_agent_framework/utils.py | 41 ++++++++++---- .../tests/test_context_provider.py | 23 ++++++++ .../tests/test_middleware.py | 54 +++++++++++++++++++ .../tests/test_utils.py | 8 +++ .../src/supermemory_cartesia/agent.py | 9 ++-- .../tests/test_empty_profile.py | 20 +++++++ .../src/supermemory_openai/middleware.py | 47 +++++++++++----- .../src/supermemory_openai/utils.py | 51 +++++++++++++++--- .../tests/test_middleware.py | 23 ++++++-- .../openai-sdk-python/tests/test_utils.py | 17 ++++++ .../src/supermemory_pipecat/service.py | 9 ++-- .../src/supermemory_pipecat/utils.py | 8 ++- .../tests/test_empty_profile.py | 35 +++++++++++- 15 files changed, 333 insertions(+), 57 deletions(-) create mode 100644 packages/openai-sdk-python/tests/test_utils.py diff --git a/packages/agent-framework-python/src/supermemory_agent_framework/context_provider.py b/packages/agent-framework-python/src/supermemory_agent_framework/context_provider.py index a1d7b161..5bee19ec 100644 --- a/packages/agent-framework-python/src/supermemory_agent_framework/context_provider.py +++ b/packages/agent-framework-python/src/supermemory_agent_framework/context_provider.py @@ -217,8 +217,8 @@ class SupermemoryContextProvider(BaseContextProvider): ) deduplicated = deduplicate_memories( - static=static, - dynamic=dynamic, + static=static if self._mode != "query" else [], + dynamic=dynamic if self._mode != "query" else [], search_results=search_results_raw, ) diff --git a/packages/agent-framework-python/src/supermemory_agent_framework/middleware.py b/packages/agent-framework-python/src/supermemory_agent_framework/middleware.py index 93536521..649f94e8 100644 --- a/packages/agent-framework-python/src/supermemory_agent_framework/middleware.py +++ b/packages/agent-framework-python/src/supermemory_agent_framework/middleware.py @@ -21,6 +21,8 @@ from .utils import ( convert_profile_to_markdown, create_logger, deduplicate_memories, + replace_memory_injection, + strip_memory_injection, wrap_memory_injection, ) @@ -152,8 +154,8 @@ async def _build_memories_text( ) deduplicated = deduplicate_memories( - static=static, - dynamic=dynamic, + static=static if mode != "query" else [], + dynamic=dynamic if mode != "query" else [], search_results=search_results_raw, ) @@ -393,10 +395,11 @@ def _inject_memories(context: Any, memories: str) -> None: different Agent Framework providers. """ messages = context.messages - memory_text = f"\n\n{wrap_memory_injection(memories)}" + memory_text = wrap_memory_injection(memories) - # Try to find and augment existing system message - for i, msg in enumerate(messages): + # Replace prior SDK blocks in every system message and inject once. + injected = False + for msg in messages: role = None if hasattr(msg, "role"): role = msg.role @@ -405,17 +408,35 @@ def _inject_memories(context: Any, memories: str) -> None: if role == "system": if hasattr(msg, "text"): - msg.text = (msg.text or "") + memory_text + existing = msg.text or "" + msg.text = ( + replace_memory_injection(existing, memories) + if not injected + else strip_memory_injection(existing) + ) elif hasattr(msg, "content"): - msg.content = (msg.content or "") + memory_text + existing = msg.content or "" + msg.content = ( + replace_memory_injection(existing, memories) + if not injected + else strip_memory_injection(existing) + ) elif isinstance(msg, dict): - msg["content"] = (msg.get("content", "") or "") + memory_text - return + existing = msg.get("content", "") or "" + msg["content"] = ( + replace_memory_injection(existing, memories) + if not injected + else strip_memory_injection(existing) + ) + injected = True + + if injected: + return # No system message found - prepend one try: if isinstance(messages, list): - messages.insert(0, Message("system", [memories])) + messages.insert(0, Message("system", [memory_text])) except Exception: # If messages is immutable, log a warning pass diff --git a/packages/agent-framework-python/src/supermemory_agent_framework/utils.py b/packages/agent-framework-python/src/supermemory_agent_framework/utils.py index 1e4ee56a..f7f64257 100644 --- a/packages/agent-framework-python/src/supermemory_agent_framework/utils.py +++ b/packages/agent-framework-python/src/supermemory_agent_framework/utils.py @@ -5,6 +5,10 @@ import re from typing import Any, Optional, Protocol DEFAULT_CONTEXT_PROMPT = "The following are retrieved memories about the user." +MEMORY_CONTEXT_PATTERN = re.compile( + r'[ \t]*.*?[ \t]*', + re.DOTALL, +) def wrap_memory_injection(memories: str, context_prompt: str = "") -> str: @@ -19,6 +23,21 @@ def wrap_memory_injection(memories: str, context_prompt: str = "") -> str: ) +def strip_memory_injection(content: str) -> str: + """Remove every context block previously owned by this middleware.""" + stripped = MEMORY_CONTEXT_PATTERN.sub("", content) + return re.sub(r"\n{3,}", "\n\n", stripped).strip() + + +def replace_memory_injection(content: str, memories: str) -> str: + """Replace middleware-owned context while preserving caller instructions.""" + preserved = strip_memory_injection(content) + memory_context = wrap_memory_injection(memories) if memories.strip() else "" + if not memory_context: + return preserved + return f"{preserved}\n\n{memory_context}" if preserved else memory_context + + class Logger(Protocol): """Logger protocol for type safety.""" @@ -110,36 +129,40 @@ def deduplicate_memories( return None def comparison_key(memory: str) -> str: - """Remove Mono's dynamic-profile date decoration for comparison only.""" - return re.sub( + """Normalize display-only profile decoration for duplicate comparison.""" + without_prefix = re.sub( r"^(?:\[Recent\]\s*)?\[\d{4}-\d{2}-\d{2}\]\s*", "", memory, count=1, - ).strip() + ) + return " ".join(without_prefix.strip().split()).casefold() static_memories: list[str] = [] seen_memories: set[str] = set() for item in static_items: memory = extract_memory_text(item) - if memory is not None: + key = comparison_key(memory) if memory is not None else None + if memory is not None and key is not None and key not in seen_memories: static_memories.append(memory) - seen_memories.add(comparison_key(memory)) + seen_memories.add(key) dynamic_memories: list[str] = [] for item in dynamic_items: memory = extract_memory_text(item) - if memory is not None and comparison_key(memory) not in seen_memories: + key = comparison_key(memory) if memory is not None else None + if memory is not None and key is not None and key not in seen_memories: dynamic_memories.append(memory) - seen_memories.add(comparison_key(memory)) + seen_memories.add(key) search_memories: list[str] = [] for item in search_items: memory = extract_memory_text(item) - if memory is not None and comparison_key(memory) not in seen_memories: + key = comparison_key(memory) if memory is not None else None + if memory is not None and key is not None and key not in seen_memories: search_memories.append(memory) - seen_memories.add(comparison_key(memory)) + seen_memories.add(key) return DeduplicatedMemories( static=static_memories, diff --git a/packages/agent-framework-python/tests/test_context_provider.py b/packages/agent-framework-python/tests/test_context_provider.py index c6c8c912..35e9639f 100644 --- a/packages/agent-framework-python/tests/test_context_provider.py +++ b/packages/agent-framework-python/tests/test_context_provider.py @@ -1,5 +1,8 @@ """Tests for Supermemory context provider.""" +from types import SimpleNamespace +from unittest.mock import AsyncMock + import pytest from supermemory_agent_framework import AgentSupermemory, SupermemoryContextProvider @@ -123,3 +126,23 @@ class TestExtractConversation: result = provider._extract_conversation_from_context(MockContext()) assert "User: Hello!" in result assert "Assistant: Hi there!" in result + + +class TestMemoryRetrieval: + @pytest.mark.asyncio + async def test_query_mode_keeps_search_fact_also_present_in_profile(self) -> None: + fact = "User likes machine learning projects" + conn = _make_conn() + conn.client.profile = AsyncMock( + return_value=SimpleNamespace( + profile=SimpleNamespace(static=[fact], dynamic=[]), + search_results=SimpleNamespace( + results=[SimpleNamespace(memory=fact)] + ), + ) + ) + provider = SupermemoryContextProvider(conn, mode="query") + + memories = await provider._fetch_memories("machine learning") + + assert fact in memories diff --git a/packages/agent-framework-python/tests/test_middleware.py b/packages/agent-framework-python/tests/test_middleware.py index b3ea23e0..c5c867fd 100644 --- a/packages/agent-framework-python/tests/test_middleware.py +++ b/packages/agent-framework-python/tests/test_middleware.py @@ -1,5 +1,8 @@ """Tests for Supermemory middleware.""" +from types import SimpleNamespace +from unittest.mock import AsyncMock, Mock + import pytest from supermemory_agent_framework import ( @@ -10,6 +13,8 @@ from supermemory_agent_framework import ( from supermemory_agent_framework.middleware import ( _get_last_user_message, _get_conversation_content, + _build_memories_text, + _inject_memories, ) @@ -111,3 +116,52 @@ class TestMiddlewareConfiguration: conn = _make_conn(entity_context="User is a Python developer") middleware = SupermemoryChatMiddleware(conn) assert middleware._connection.entity_context == "User is a Python developer" + + +class TestMemoryInjection: + def test_replaces_prior_sdk_context(self) -> None: + context = SimpleNamespace( + messages=[ + { + "role": "system", + "content": ( + "Be helpful.\n\n" + '\n' + "Stale profile fact\n" + "" + ), + }, + {"role": "user", "content": "What do you remember?"}, + ] + ) + + _inject_memories(context, "Fresh profile fact") + + content = context.messages[0]["content"] + assert "Be helpful." in content + assert "Fresh profile fact" in content + assert "Stale profile fact" not in content + assert content.count( + '' + ) == 1 + + @pytest.mark.asyncio + async def test_query_mode_keeps_search_fact_also_present_in_profile(self) -> None: + fact = "User likes machine learning projects" + client = SimpleNamespace( + profile=AsyncMock( + return_value=SimpleNamespace( + profile=SimpleNamespace(static=[fact], dynamic=[]), + search_results=SimpleNamespace( + results=[SimpleNamespace(memory=fact)] + ), + ) + ) + ) + logger = Mock() + + memories = await _build_memories_text( + "user-123", logger, "query", client, "machine learning" + ) + + assert fact in memories diff --git a/packages/agent-framework-python/tests/test_utils.py b/packages/agent-framework-python/tests/test_utils.py index 6b9362bb..6cc3de31 100644 --- a/packages/agent-framework-python/tests/test_utils.py +++ b/packages/agent-framework-python/tests/test_utils.py @@ -56,6 +56,14 @@ class TestDeduplicateMemories: ) assert result.static == ["valid"] + def test_normalized_fact_variants(self) -> None: + result = deduplicate_memories( + static=["User likes Python", " user likes python "], + dynamic=["[2026-08-10] USER LIKES PYTHON"], + ) + assert result.static == ["User likes Python"] + assert result.dynamic == [] + class TestConvertProfileToMarkdown: def test_empty_profile(self) -> None: diff --git a/packages/cartesia-sdk-python/src/supermemory_cartesia/agent.py b/packages/cartesia-sdk-python/src/supermemory_cartesia/agent.py index 27aba07b..ea575e6a 100644 --- a/packages/cartesia-sdk-python/src/supermemory_cartesia/agent.py +++ b/packages/cartesia-sdk-python/src/supermemory_cartesia/agent.py @@ -237,9 +237,11 @@ class SupermemoryCartesiaAgent: def _build_memory_message(self, memories_data: Dict[str, Any]) -> Optional[str]: """Build memory context from retrieved data.""" profile = memories_data["profile"] + include_profile = self.config.mode in ("profile", "full") + include_search = self.config.mode in ("query", "full") deduplicated = deduplicate_memories( - static=profile["static"], - dynamic=profile["dynamic"], + static=profile["static"] if include_profile else [], + dynamic=profile["dynamic"] if include_profile else [], search_results=memories_data["search_results"], ) @@ -252,9 +254,6 @@ class SupermemoryCartesiaAgent: if total == 0: return None - include_profile = self.config.mode in ("profile", "full") - include_search = self.config.mode in ("query", "full") - memory_text = format_memories_to_text( deduplicated, system_prompt=self.config.system_prompt, diff --git a/packages/cartesia-sdk-python/tests/test_empty_profile.py b/packages/cartesia-sdk-python/tests/test_empty_profile.py index 382e5e5f..f374ecfa 100644 --- a/packages/cartesia-sdk-python/tests/test_empty_profile.py +++ b/packages/cartesia-sdk-python/tests/test_empty_profile.py @@ -72,6 +72,26 @@ class TestSupermemoryCartesiaNullProfile(unittest.IsolatedAsyncioTestCase): }, ) + def test_query_mode_keeps_search_fact_also_present_in_profile(self) -> None: + fact = "User likes machine learning projects" + agent = SupermemoryCartesiaAgent( + agent=SimpleNamespace(), + api_key="mock_key", + container_tag="user-123", + custom_id="conversation-456", + config=SupermemoryCartesiaAgent.MemoryConfig(mode="query"), + ) + + context = agent._build_memory_message( + { + "profile": {"static": [fact], "dynamic": []}, + "search_results": [SimpleNamespace(memory=fact)], + } + ) + + self.assertIsNotNone(context) + self.assertIn(fact, context) + if __name__ == "__main__": unittest.main() diff --git a/packages/openai-sdk-python/src/supermemory_openai/middleware.py b/packages/openai-sdk-python/src/supermemory_openai/middleware.py index 5191a6c8..ca031bec 100644 --- a/packages/openai-sdk-python/src/supermemory_openai/middleware.py +++ b/packages/openai-sdk-python/src/supermemory_openai/middleware.py @@ -26,6 +26,9 @@ from .utils import ( deduplicate_memories, get_conversation_content, get_last_user_message, + replace_memory_context, + strip_memory_context, + wrap_memory_context, ) DEFAULT_SUPERMEMORY_BASE_URL = "https://api.supermemory.ai" @@ -155,8 +158,8 @@ async def add_system_prompt( ) deduplicated = deduplicate_memories( - static=profile.get("static", []), - dynamic=profile.get("dynamic", []), + static=profile.get("static", []) if mode != "query" else [], + dynamic=profile.get("dynamic", []) if mode != "query" else [], search_results=search_results_data.get("results", []), ) @@ -208,24 +211,40 @@ async def add_system_prompt( }, ) + if system_prompt_exists: + logger.debug("Replaced Supermemory context in existing system prompt") + enhanced: list[ChatCompletionMessageParam] = [] + injected = False + for msg in messages: + if msg.get("role") != "system": + enhanced.append(msg) + continue + content = msg.get("content", "") + existing = content if isinstance(content, str) else "" + if not injected: + enhanced.append( + cast( + ChatCompletionMessageParam, + {**msg, "content": replace_memory_context(existing, memories)}, + ) + ) + injected = True + else: + enhanced.append( + cast( + ChatCompletionMessageParam, + {**msg, "content": strip_memory_context(existing)}, + ) + ) + return enhanced + if not memories: return messages - if system_prompt_exists: - logger.debug("Added memories to existing system prompt") - return [ - ( - {**msg, "content": f"{msg.get('content', '')} \n {memories}"} - if msg.get("role") == "system" - else msg - ) - for msg in messages - ] - logger.debug("System prompt does not exist, created system prompt with memories") system_message: ChatCompletionSystemMessageParam = { "role": "system", - "content": memories, + "content": wrap_memory_context(memories), } return [system_message] + messages diff --git a/packages/openai-sdk-python/src/supermemory_openai/utils.py b/packages/openai-sdk-python/src/supermemory_openai/utils.py index e3cad8d9..38c50f97 100644 --- a/packages/openai-sdk-python/src/supermemory_openai/utils.py +++ b/packages/openai-sdk-python/src/supermemory_openai/utils.py @@ -1,11 +1,43 @@ """Utility functions for Supermemory OpenAI middleware.""" import json +import re from typing import Optional, Any, Protocol from openai.types.chat import ChatCompletionMessageParam +MEMORY_CONTEXT_START = '' +MEMORY_CONTEXT_END = "" +MEMORY_CONTEXT_PATTERN = re.compile( + r'[ \t]*.*?[ \t]*', + re.DOTALL, +) + + +def strip_memory_context(content: str) -> str: + """Remove every context block previously owned by this middleware.""" + stripped = MEMORY_CONTEXT_PATTERN.sub("", content) + return re.sub(r"\n{3,}", "\n\n", stripped).strip() + + +def wrap_memory_context(memories: str) -> str: + """Mark retrieved context so the next turn can replace it safely.""" + normalized = memories.strip() + if not normalized: + return "" + return f"{MEMORY_CONTEXT_START}\n{normalized}\n{MEMORY_CONTEXT_END}" + + +def replace_memory_context(content: str, memories: str) -> str: + """Replace middleware-owned context while preserving caller instructions.""" + preserved = strip_memory_context(content) + memory_context = wrap_memory_context(memories) + if not memory_context: + return preserved + return f"{preserved}\n\n{memory_context}" if preserved else memory_context + + class Logger(Protocol): """Logger protocol for type safety.""" @@ -231,25 +263,32 @@ def deduplicate_memories( static_memories: list[str] = [] seen_memories: set[str] = set() + def normalize_fact(memory: str) -> str: + without_date = re.sub(r"^\[\d{4}-\d{2}-\d{2}\]\s*", "", memory) + return " ".join(without_date.strip().split()).casefold() + for item in static_items: memory = extract_memory_text(item) - if memory is not None: + key = normalize_fact(memory) if memory is not None else None + if memory is not None and key is not None and key not in seen_memories: static_memories.append(memory) - seen_memories.add(memory) + seen_memories.add(key) dynamic_memories: list[str] = [] for item in dynamic_items: memory = extract_memory_text(item) - if memory is not None and memory not in seen_memories: + key = normalize_fact(memory) if memory is not None else None + if memory is not None and key is not None and key not in seen_memories: dynamic_memories.append(memory) - seen_memories.add(memory) + seen_memories.add(key) search_memories: list[str] = [] for item in search_items: memory = extract_memory_text(item) - if memory is not None and memory not in seen_memories: + key = normalize_fact(memory) if memory is not None else None + if memory is not None and key is not None and key not in seen_memories: search_memories.append(memory) - seen_memories.add(memory) + seen_memories.add(key) return DeduplicatedMemories( static=static_memories, diff --git a/packages/openai-sdk-python/tests/test_middleware.py b/packages/openai-sdk-python/tests/test_middleware.py index de4004ac..cec2cb6e 100644 --- a/packages/openai-sdk-python/tests/test_middleware.py +++ b/packages/openai-sdk-python/tests/test_middleware.py @@ -215,7 +215,10 @@ class TestMemoryInjection: with patch.dict(os.environ, {"SUPERMEMORY_API_KEY": "test-key"}): with patch("supermemory_openai.middleware.supermemory_profile_search") as mock_search: mock_search.return_value = Mock() - mock_search.return_value.profile = {"static": [], "dynamic": []} + mock_search.return_value.profile = { + "static": [{"memory": "User likes machine learning projects"}], + "dynamic": [], + } mock_search.return_value.search_results = mock_supermemory_response["searchResults"] wrapped_client = with_supermemory( @@ -236,6 +239,8 @@ class TestMemoryInjection: mock_search.assert_called_once() search_args = mock_search.call_args[0] assert search_args[1] == "What machine learning frameworks do I like?" + enhanced_messages = original_create.call_args[1]["messages"] + assert "User likes machine learning projects" in enhanced_messages[0]["content"] @pytest.mark.asyncio async def test_memory_injection_full_mode( @@ -295,7 +300,15 @@ class TestMemoryInjection: ) messages = [ - {"role": "system", "content": "You are a helpful assistant."}, + { + "role": "system", + "content": ( + "You are a helpful assistant.\n\n" + '\n' + "Stale profile fact\n" + "" + ), + }, {"role": "user", "content": "What do you know about me?"} ] @@ -316,6 +329,10 @@ class TestMemoryInjection: assert system_message["role"] == "system" assert "You are a helpful assistant." in system_message["content"] assert "User prefers Python" in system_message["content"] + assert "Stale profile fact" not in system_message["content"] + assert system_message["content"].count( + '' + ) == 1 @pytest.mark.asyncio @@ -794,4 +811,4 @@ class TestBackgroundTaskManagement: messages=[{"role": "user", "content": "Hello"}] ) - # Should complete without error \ No newline at end of file + # Should complete without error diff --git a/packages/openai-sdk-python/tests/test_utils.py b/packages/openai-sdk-python/tests/test_utils.py new file mode 100644 index 00000000..b1d71147 --- /dev/null +++ b/packages/openai-sdk-python/tests/test_utils.py @@ -0,0 +1,17 @@ +"""Tests for shared middleware utilities.""" + +from supermemory_openai.utils import deduplicate_memories + + +def test_deduplicates_normalized_fact_variants() -> None: + result = deduplicate_memories( + static=[ + {"memory": "User likes Python"}, + {"memory": " user likes python "}, + ], + dynamic=[{"memory": "[2026-08-10] USER LIKES PYTHON"}], + search_results=[], + ) + + assert result.static == ["User likes Python"] + assert result.dynamic == [] diff --git a/packages/pipecat-sdk-python/src/supermemory_pipecat/service.py b/packages/pipecat-sdk-python/src/supermemory_pipecat/service.py index d3b000b5..3ea28197 100644 --- a/packages/pipecat-sdk-python/src/supermemory_pipecat/service.py +++ b/packages/pipecat-sdk-python/src/supermemory_pipecat/service.py @@ -359,9 +359,11 @@ class SupermemoryPipecatService(FrameProcessor): memories_data: Memory data from Supermemory API. """ profile = memories_data["profile"] + include_profile = self.params.mode in ("profile", "full") + include_search = self.params.mode in ("query", "full") deduplicated = deduplicate_memories( - static=profile["static"], - dynamic=profile["dynamic"], + static=profile["static"] if include_profile else [], + dynamic=profile["dynamic"] if include_profile else [], search_results=memories_data["search_results"], ) @@ -374,9 +376,6 @@ class SupermemoryPipecatService(FrameProcessor): if total_memories == 0: return - include_profile = self.params.mode in ("profile", "full") - include_search = self.params.mode in ("query", "full") - memory_text = format_memories_to_text( deduplicated, system_prompt=self.params.system_prompt, diff --git a/packages/pipecat-sdk-python/src/supermemory_pipecat/utils.py b/packages/pipecat-sdk-python/src/supermemory_pipecat/utils.py index 743bb4c0..b9dfbe22 100644 --- a/packages/pipecat-sdk-python/src/supermemory_pipecat/utils.py +++ b/packages/pipecat-sdk-python/src/supermemory_pipecat/utils.py @@ -5,7 +5,10 @@ from datetime import datetime, timezone from typing import Any, Dict, List, Union -_DYNAMIC_DATE_PREFIX = re.compile(r"^\s*(?:\[Recent\]\s*)?\[\d{4}-\d{2}-\d{2}\]\s*") +_DYNAMIC_DATE_PREFIX = re.compile( + r"^\s*(?:\[Recent\]\s*)?\[\d{4}-\d{2}-\d{2}\]\s*", + re.IGNORECASE, +) def get_last_user_message(messages: List[Dict[str, Any]]) -> str | None: @@ -91,7 +94,8 @@ def deduplicate_memories( def comparison_key(memory: str) -> str: # Dynamic profile entries are date-labelled by the API while search # results contain the same memory without that presentation prefix. - return _DYNAMIC_DATE_PREFIX.sub("", memory.strip()) + without_prefix = _DYNAMIC_DATE_PREFIX.sub("", memory.strip()) + return " ".join(without_prefix.split()).casefold() def unique_strings(memories: List[str]) -> List[str]: out: List[str] = [] diff --git a/packages/pipecat-sdk-python/tests/test_empty_profile.py b/packages/pipecat-sdk-python/tests/test_empty_profile.py index ec3ccd26..184cff6c 100644 --- a/packages/pipecat-sdk-python/tests/test_empty_profile.py +++ b/packages/pipecat-sdk-python/tests/test_empty_profile.py @@ -120,4 +120,37 @@ class TestSupermemoryPipecatNullProfile(unittest.IsolatedAsyncioTestCase): "profile": {"static": [], "dynamic": []}, "search_results": [], }, - ) \ No newline at end of file + ) + + def test_query_mode_keeps_search_fact_also_present_in_profile(self) -> None: + fact = "User likes machine learning projects" + service = SupermemoryPipecatService( + api_key="mock_key", + user_id="user-123", + session_id="conversation-456", + params=SupermemoryPipecatService.InputParams(mode="query"), + ) + + class Context: + def __init__(self): + self.messages = [{"role": "user", "content": "What do I like?"}] + + def get_messages(self): + return self.messages + + def add_message(self, message): + self.messages.append(message) + + context = Context() + service._enhance_context_with_memories( + context, + "What do I like?", + { + "profile": {"static": [fact], "dynamic": []}, + "search_results": [SimpleNamespace(memory=fact)], + }, + ) + + self.assertTrue( + any(fact in message.get("content", "") for message in context.messages) + )