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 5359426e..3b0fc257 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 dac80c3c..aa9f4418 100644 --- a/packages/agent-framework-python/src/supermemory_agent_framework/utils.py +++ b/packages/agent-framework-python/src/supermemory_agent_framework/utils.py @@ -1,9 +1,14 @@ """Utility functions for Supermemory Agent Framework integration.""" import json +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: @@ -18,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.""" @@ -111,25 +131,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/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 5d0d9b8c..5cfbed2c 100644 --- a/packages/agent-framework-python/tests/test_utils.py +++ b/packages/agent-framework-python/tests/test_utils.py @@ -70,6 +70,14 @@ class TestDeduplicateMemories: assert result.static == ["User likes Python"] assert result.search_results == ["User prefers async"] + 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 d369af8e..74f04710 100644 --- a/packages/cartesia-sdk-python/src/supermemory_cartesia/agent.py +++ b/packages/cartesia-sdk-python/src/supermemory_cartesia/agent.py @@ -240,9 +240,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"], ) @@ -255,9 +257,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/src/supermemory_cartesia/utils.py b/packages/cartesia-sdk-python/src/supermemory_cartesia/utils.py index 5cda9298..5cf56161 100644 --- a/packages/cartesia-sdk-python/src/supermemory_cartesia/utils.py +++ b/packages/cartesia-sdk-python/src/supermemory_cartesia/utils.py @@ -1,6 +1,7 @@ """Utility functions for Supermemory Cartesia integration.""" from datetime import datetime, timezone +import re from typing import Any, Dict, List, Union @@ -83,12 +84,18 @@ def deduplicate_memories( """ seen = set() + def fact_key(memory: str) -> str: + without_date = re.sub(r"^\[\d{4}-\d{2}-\d{2}\]\s*", "", memory) + return " ".join(without_date.strip().split()).casefold() + def unique_strings(memories: List[str]) -> List[str]: out = [] for m in memories: - if m not in seen: - seen.add(m) - out.append(m) + memory = m.strip() + key = fact_key(memory) + if memory and key not in seen: + seen.add(key) + out.append(memory) return out def unique_search(results: List[Any]) -> List[Any]: @@ -99,8 +106,9 @@ def deduplicate_memories( if not isinstance(memory, str): memory = "" memory = memory.strip() - if memory and memory not in seen: - seen.add(memory) + key = fact_key(memory) + if memory and key not in seen: + seen.add(key) out.append(r) return out diff --git a/packages/cartesia-sdk-python/tests/test_dedupe_utils.py b/packages/cartesia-sdk-python/tests/test_dedupe_utils.py index 81931fe1..9dcdb38b 100644 --- a/packages/cartesia-sdk-python/tests/test_dedupe_utils.py +++ b/packages/cartesia-sdk-python/tests/test_dedupe_utils.py @@ -81,6 +81,15 @@ class TestDeduplicateMemories(unittest.TestCase): ) self.assertEqual(result["search_results"], []) + def test_dedupes_normalized_fact_variants(self) -> None: + result = deduplicate_memories( + static=["User likes Python", " user likes python "], + dynamic=["[2026-08-10] USER LIKES PYTHON"], + search_results=[], + ) + self.assertEqual(result["static"], ["User likes Python"]) + self.assertEqual(result["dynamic"], []) + class TestFormatMemoriesToText(unittest.TestCase): def test_formats_pydantic_like_search_results(self) -> None: diff --git a/packages/cartesia-sdk-python/tests/test_empty_profile.py b/packages/cartesia-sdk-python/tests/test_empty_profile.py index a430007e..8fb301e2 100644 --- a/packages/cartesia-sdk-python/tests/test_empty_profile.py +++ b/packages/cartesia-sdk-python/tests/test_empty_profile.py @@ -76,6 +76,26 @@ class TestSupermemoryCartesiaNullProfile(unittest.IsolatedAsyncioTestCase): self.assertEqual(kwargs["container_tag"], "user-123") self.assertEqual(kwargs["q"], "Hello world") + 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 1a079fa2..9b7d72c5 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, ) @@ -140,8 +143,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", []), ) @@ -193,22 +196,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 04696076..d2821099 100644 --- a/packages/pipecat-sdk-python/src/supermemory_pipecat/service.py +++ b/packages/pipecat-sdk-python/src/supermemory_pipecat/service.py @@ -207,9 +207,11 @@ class SupermemoryPipecatService(FrameProcessor): self._last_query = query 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"], ) @@ -222,9 +224,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 3b74509f..b5af8a29 100644 --- a/packages/pipecat-sdk-python/src/supermemory_pipecat/utils.py +++ b/packages/pipecat-sdk-python/src/supermemory_pipecat/utils.py @@ -1,6 +1,7 @@ """Utility functions for Supermemory Pipecat integration.""" from datetime import datetime, timezone +import re from typing import Any, Dict, List, Union @@ -83,12 +84,18 @@ def deduplicate_memories( """ seen = set() + def fact_key(memory: str) -> str: + without_date = re.sub(r"^\[\d{4}-\d{2}-\d{2}\]\s*", "", memory) + return " ".join(without_date.strip().split()).casefold() + def unique_strings(memories: List[str]) -> List[str]: out = [] for m in memories: - if m not in seen: - seen.add(m) - out.append(m) + memory = m.strip() + key = fact_key(memory) + if memory and key not in seen: + seen.add(key) + out.append(memory) return out def unique_search(results: List[Any]) -> List[Any]: @@ -99,8 +106,9 @@ def deduplicate_memories( if not isinstance(memory, str): memory = "" memory = memory.strip() - if memory and memory not in seen: - seen.add(memory) + key = fact_key(memory) + if memory and key not in seen: + seen.add(key) out.append(r) return out diff --git a/packages/pipecat-sdk-python/tests/test_dedupe_utils.py b/packages/pipecat-sdk-python/tests/test_dedupe_utils.py index 94abeea2..4c12d4ce 100644 --- a/packages/pipecat-sdk-python/tests/test_dedupe_utils.py +++ b/packages/pipecat-sdk-python/tests/test_dedupe_utils.py @@ -142,6 +142,15 @@ class TestDeduplicateMemories(unittest.TestCase): ) self.assertEqual(result["search_results"], []) + def test_dedupes_normalized_fact_variants(self) -> None: + result = deduplicate_memories( + static=["User likes Python", " user likes python "], + dynamic=["[2026-08-10] USER LIKES PYTHON"], + search_results=[], + ) + self.assertEqual(result["static"], ["User likes Python"]) + self.assertEqual(result["dynamic"], []) + class TestFormatMemoriesToText(unittest.TestCase): def test_formats_pydantic_like_search_results(self) -> None: diff --git a/packages/pipecat-sdk-python/tests/test_empty_profile.py b/packages/pipecat-sdk-python/tests/test_empty_profile.py index aea9011c..04ea4079 100644 --- a/packages/pipecat-sdk-python/tests/test_empty_profile.py +++ b/packages/pipecat-sdk-python/tests/test_empty_profile.py @@ -124,4 +124,37 @@ class TestSupermemoryPipecatNullProfile(unittest.IsolatedAsyncioTestCase): service._supermemory_client.profile.assert_awaited_once() kwargs = service._supermemory_client.profile.await_args.kwargs self.assertEqual(kwargs["container_tag"], "new_user_123") - self.assertEqual(kwargs["q"], "Hello world") \ No newline at end of file + self.assertEqual(kwargs["q"], "Hello world") + + 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) + )