diff --git a/mem0/memory/main.py b/mem0/memory/main.py index 1d81c0a28..8cac28ca0 100644 --- a/mem0/memory/main.py +++ b/mem0/memory/main.py @@ -22,6 +22,7 @@ from mem0.configs.prompts import ( PROCEDURAL_MEMORY_SYSTEM_PROMPT, generate_additive_extraction_prompt, ) +from mem0.exceptions import LLMError from mem0.exceptions import ValidationError as Mem0ValidationError from mem0.memory.base import MemoryBase from mem0.memory.setup import mem0_dir, setup_config @@ -912,8 +913,12 @@ class Memory(MemoryBase): response_format={"type": "json_object"}, ) except Exception as e: + # Re-raise so callers can implement provider fallback / retry. + # The original silent ``return []`` made upstream callers unable to + # distinguish "LLM unavailable" (429/5xx/timeout) from "LLM + # extracted no facts" -- both surfaced as an empty list. logger.error(f"LLM extraction failed: {e}") - return [] + raise LLMError(f"LLM extraction failed: {e}") from e # Parse response try: @@ -2536,8 +2541,10 @@ class AsyncMemory(MemoryBase): response_format={"type": "json_object"}, ) except Exception as e: + # Re-raise so callers can implement provider fallback / retry + # (see sync counterpart for rationale). logger.error(f"LLM extraction failed (async): {e}") - return [] + raise LLMError(f"LLM extraction failed: {e}") from e # Parse response try: diff --git a/tests/memory/test_main.py b/tests/memory/test_main.py index e8a490885..86acb8138 100644 --- a/tests/memory/test_main.py +++ b/tests/memory/test_main.py @@ -6,6 +6,7 @@ from unittest.mock import MagicMock, Mock import pytest +from mem0.exceptions import LLMError from mem0.memory.main import AsyncMemory, Memory @@ -79,6 +80,28 @@ class TestAddToVectorStoreErrors: assert mock_memory.llm.generate_response.call_count == 1 assert result == [] # Should return empty list when no memories processed + def test_llm_extraction_exception_is_reraised(self, mocker, mock_memory): + """A provider error during fact extraction must propagate, not be swallowed. + + Regression guard for the silent ``return []`` that made it impossible for + callers to tell "LLM unavailable" (429/5xx/timeout) from "no facts found". + Without the fix this raises AssertionError because the call returns []. + """ + + class _ProviderError(Exception): + pass + + mock_memory.llm.generate_response.side_effect = _ProviderError("429 rate limit") + mocker.patch("mem0.memory.main.capture_event") + + with pytest.raises(LLMError) as exc_info: + mock_memory._add_to_vector_store( + messages=[{"role": "user", "content": "test"}], metadata={}, filters={}, infer=True + ) + # The documented LLMError contract is honoured, and the original + # provider exception is preserved as the cause for debugging. + assert isinstance(exc_info.value.__cause__, _ProviderError) + class TestPromptOverridesCustomInstructions: @pytest.fixture @@ -245,6 +268,29 @@ class TestAsyncAddToVectorStoreErrors: assert result == [] assert mock_async_memory.llm.generate_response.call_count == 1 + @pytest.mark.asyncio + async def test_async_llm_extraction_exception_is_reraised(self, mock_async_memory, mocker): + """Async counterpart of the sync re-raise guard. + + A provider error during fact extraction must propagate as ``LLMError`` + (with the original exception preserved as the cause), not be swallowed + into ``return []``. Without the fix a future revert of the async + ``raise`` back to ``return []`` would pass the suite silently. + """ + mocker.patch("mem0.utils.factory.EmbedderFactory.create", return_value=MagicMock()) + + class _ProviderError(Exception): + pass + + mock_async_memory.llm.generate_response.side_effect = _ProviderError("429 rate limit") + mocker.patch("mem0.memory.main.capture_event") + + with pytest.raises(LLMError) as exc_info: + await mock_async_memory._add_to_vector_store( + messages=[{"role": "user", "content": "test"}], metadata={}, effective_filters={}, infer=True + ) + assert isinstance(exc_info.value.__cause__, _ProviderError) + def _build_memory_instance(mocker, memory_cls): _setup_mocks(mocker)