diff --git a/mem0/llms/gemini.py b/mem0/llms/gemini.py index 1e1c78721..b30e2de8c 100644 --- a/mem0/llms/gemini.py +++ b/mem0/llms/gemini.py @@ -32,22 +32,25 @@ class GeminiLLM(LLMBase): Returns: str or dict: The processed response. """ + # Get parts safely — content can be None when Gemini blocks the response + candidate = response.candidates[0] if response.candidates else None + parts = candidate.content.parts if candidate and candidate.content else None + if tools: processed_response = { "content": None, "tool_calls": [], } - # Extract content from the first candidate - if response.candidates and response.candidates[0].content.parts: - for part in response.candidates[0].content.parts: + if parts: + # Extract content from the first candidate + for part in parts: if hasattr(part, "text") and part.text: processed_response["content"] = part.text break - # Extract function calls - if response.candidates and response.candidates[0].content.parts: - for part in response.candidates[0].content.parts: + # Extract function calls + for part in parts: if hasattr(part, "function_call") and part.function_call: fn = part.function_call processed_response["tool_calls"].append( @@ -59,8 +62,8 @@ class GeminiLLM(LLMBase): return processed_response else: - if response.candidates and response.candidates[0].content.parts: - for part in response.candidates[0].content.parts: + if parts: + for part in parts: if hasattr(part, "text") and part.text: return part.text return "" diff --git a/tests/llms/test_gemini.py b/tests/llms/test_gemini.py index 19875d9db..47ec7c9d8 100644 --- a/tests/llms/test_gemini.py +++ b/tests/llms/test_gemini.py @@ -117,3 +117,73 @@ def test_generate_response_with_tools(mock_gemini_client: Mock): assert len(response["tool_calls"]) == 1 assert response["tool_calls"][0]["name"] == "add_memory" assert response["tool_calls"][0]["arguments"] == {"data": "Today is a sunny day."} + + +def test_parse_response_none_content_no_tools(mock_gemini_client: Mock): + """Gemini can return content=None when response is blocked by safety filters.""" + config = BaseLlmConfig(model="gemini-2.0-flash", temperature=0.7, max_tokens=100, top_p=1.0) + llm = GeminiLLM(config) + + mock_candidate = Mock(content=None) + mock_response = Mock(candidates=[mock_candidate]) + + result = llm._parse_response(mock_response, tools=None) + assert result == "" + + +def test_parse_response_none_content_with_tools(mock_gemini_client: Mock): + """Gemini can return content=None when response is blocked by safety filters (tools path).""" + config = BaseLlmConfig(model="gemini-2.0-flash", temperature=0.7, max_tokens=100, top_p=1.0) + llm = GeminiLLM(config) + + mock_candidate = Mock(content=None) + mock_response = Mock(candidates=[mock_candidate]) + + result = llm._parse_response(mock_response, tools=[{"function": {"name": "test"}}]) + assert result == {"content": None, "tool_calls": []} + + +def test_parse_response_empty_candidates(mock_gemini_client: Mock): + """Gemini can return an empty candidates list.""" + config = BaseLlmConfig(model="gemini-2.0-flash", temperature=0.7, max_tokens=100, top_p=1.0) + llm = GeminiLLM(config) + + mock_response = Mock(candidates=[]) + result = llm._parse_response(mock_response, tools=None) + assert result == "" + + +def test_parse_response_none_candidates(mock_gemini_client: Mock): + """Gemini can return candidates=None.""" + config = BaseLlmConfig(model="gemini-2.0-flash", temperature=0.7, max_tokens=100, top_p=1.0) + llm = GeminiLLM(config) + + mock_response = Mock(candidates=None) + result = llm._parse_response(mock_response, tools=None) + assert result == "" + + +def test_parse_response_empty_parts_no_tools(mock_gemini_client: Mock): + """Gemini can return content with an empty parts list.""" + config = BaseLlmConfig(model="gemini-2.0-flash", temperature=0.7, max_tokens=100, top_p=1.0) + llm = GeminiLLM(config) + + mock_content = Mock(parts=[]) + mock_candidate = Mock(content=mock_content) + mock_response = Mock(candidates=[mock_candidate]) + + result = llm._parse_response(mock_response, tools=None) + assert result == "" + + +def test_parse_response_empty_parts_with_tools(mock_gemini_client: Mock): + """Gemini can return content with an empty parts list (tools path).""" + config = BaseLlmConfig(model="gemini-2.0-flash", temperature=0.7, max_tokens=100, top_p=1.0) + llm = GeminiLLM(config) + + mock_content = Mock(parts=[]) + mock_candidate = Mock(content=mock_content) + mock_response = Mock(candidates=[mock_candidate]) + + result = llm._parse_response(mock_response, tools=[{"function": {"name": "test"}}]) + assert result == {"content": None, "tool_calls": []}