fix: handle None content and empty candidates in GeminiLLM parsing (#4462)

This commit is contained in:
Kartik
2026-03-21 13:52:13 +05:30
committed by GitHub
parent 7b26df728d
commit abdb07c204
2 changed files with 81 additions and 8 deletions
+11 -8
View File
@@ -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 ""
+70
View File
@@ -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": []}