mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-01-04 13:48:31 +00:00
Compare commits
2 Commits
devin/1765
...
devin/1765
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dac1010356 | ||
|
|
4938654ede |
@@ -583,6 +583,47 @@ class GeminiCompletion(BaseLLM):
|
||||
messages_for_event, content, from_agent
|
||||
)
|
||||
|
||||
def _format_function_call_as_action(
|
||||
self, function_name: str, function_args: dict[str, Any]
|
||||
) -> str:
|
||||
"""Format a function call as Action/Action Input text for CrewAI's parser.
|
||||
|
||||
When Gemini returns a function call but native tool execution is not available
|
||||
(i.e., available_functions is empty), this method formats the function call
|
||||
in the text format that CrewAI's agent parser expects.
|
||||
|
||||
Args:
|
||||
function_name: Name of the function to call
|
||||
function_args: Arguments for the function call
|
||||
|
||||
Returns:
|
||||
Formatted string in "Action: <name>\nAction Input: <args>" format
|
||||
"""
|
||||
import json
|
||||
|
||||
try:
|
||||
args_str = json.dumps(function_args, default=str)
|
||||
except (TypeError, ValueError):
|
||||
args_str = str(function_args)
|
||||
|
||||
return f"Action: {function_name}\nAction Input: {args_str}"
|
||||
|
||||
def _extract_text_from_parts(
|
||||
self, parts: list[Any]
|
||||
) -> str:
|
||||
"""Extract text content from response parts.
|
||||
|
||||
Args:
|
||||
parts: List of response parts from Gemini
|
||||
|
||||
Returns:
|
||||
Concatenated text content from all text parts
|
||||
"""
|
||||
text_parts = [
|
||||
part.text for part in parts if hasattr(part, "text") and part.text
|
||||
]
|
||||
return " ".join(text_parts).strip()
|
||||
|
||||
def _process_response_with_tools(
|
||||
self,
|
||||
response: GenerateContentResponse,
|
||||
@@ -605,6 +646,8 @@ class GeminiCompletion(BaseLLM):
|
||||
Returns:
|
||||
Final response content or function call result
|
||||
"""
|
||||
function_call_fallback = ""
|
||||
|
||||
if response.candidates and (self.tools or available_functions):
|
||||
candidate = response.candidates[0]
|
||||
if candidate.content and candidate.content.parts:
|
||||
@@ -619,18 +662,35 @@ class GeminiCompletion(BaseLLM):
|
||||
else {}
|
||||
)
|
||||
|
||||
result = self._handle_tool_execution(
|
||||
function_name=function_name,
|
||||
function_args=function_args,
|
||||
available_functions=available_functions or {},
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
)
|
||||
if available_functions:
|
||||
result = self._handle_tool_execution(
|
||||
function_name=function_name,
|
||||
function_args=function_args,
|
||||
available_functions=available_functions,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
)
|
||||
|
||||
if result is not None:
|
||||
return result
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
if not function_call_fallback:
|
||||
function_call_fallback = self._format_function_call_as_action(
|
||||
function_name, function_args
|
||||
)
|
||||
|
||||
content = ""
|
||||
if response.candidates:
|
||||
candidate = response.candidates[0]
|
||||
if candidate.content and candidate.content.parts:
|
||||
content = self._extract_text_from_parts(candidate.content.parts)
|
||||
|
||||
if not content:
|
||||
content = response.text or ""
|
||||
|
||||
if not content and function_call_fallback:
|
||||
content = function_call_fallback
|
||||
|
||||
content = response.text or ""
|
||||
content = self._apply_stop_words(content)
|
||||
|
||||
return self._finalize_completion_response(
|
||||
@@ -718,8 +778,10 @@ class GeminiCompletion(BaseLLM):
|
||||
"""
|
||||
self._track_token_usage_internal(usage_data)
|
||||
|
||||
function_call_fallback = ""
|
||||
|
||||
# Handle completed function calls
|
||||
if function_calls and available_functions:
|
||||
if function_calls:
|
||||
for call_data in function_calls.values():
|
||||
function_name = call_data["name"]
|
||||
function_args = call_data["args"]
|
||||
@@ -732,20 +794,32 @@ class GeminiCompletion(BaseLLM):
|
||||
if not isinstance(function_args, dict):
|
||||
function_args = {}
|
||||
|
||||
# Execute tool
|
||||
result = self._handle_tool_execution(
|
||||
function_name=function_name,
|
||||
function_args=function_args,
|
||||
available_functions=available_functions,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
)
|
||||
# Try to execute tool if available_functions provided
|
||||
if available_functions:
|
||||
result = self._handle_tool_execution(
|
||||
function_name=function_name,
|
||||
function_args=function_args,
|
||||
available_functions=available_functions,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
)
|
||||
|
||||
if result is not None:
|
||||
return result
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
# Store first function call as fallback if no text response
|
||||
if not function_call_fallback:
|
||||
function_call_fallback = self._format_function_call_as_action(
|
||||
function_name, function_args
|
||||
)
|
||||
|
||||
# Use function call fallback if no text response
|
||||
content = full_response
|
||||
if not content and function_call_fallback:
|
||||
content = function_call_fallback
|
||||
|
||||
return self._finalize_completion_response(
|
||||
content=full_response,
|
||||
content=content,
|
||||
contents=contents,
|
||||
response_model=response_model,
|
||||
from_task=from_task,
|
||||
|
||||
@@ -728,3 +728,283 @@ def test_google_streaming_returns_usage_metrics():
|
||||
assert result.token_usage.prompt_tokens > 0
|
||||
assert result.token_usage.completion_tokens > 0
|
||||
assert result.token_usage.successful_requests >= 1
|
||||
|
||||
|
||||
def test_gemini_function_call_without_text_returns_action_format():
|
||||
"""
|
||||
Test that when Gemini returns a function call without text content,
|
||||
the response is formatted as Action/Action Input for CrewAI's parser.
|
||||
|
||||
This addresses GitHub issue #4093 where Gemini models would return
|
||||
"None or empty response" errors when the model returned only a function call.
|
||||
"""
|
||||
from unittest.mock import Mock, patch
|
||||
from crewai.llms.providers.gemini.completion import GeminiCompletion
|
||||
|
||||
completion = GeminiCompletion(model="gemini-2.0-flash-001")
|
||||
|
||||
with patch.object(completion.client.models, 'generate_content') as mock_generate:
|
||||
mock_function_call = Mock()
|
||||
mock_function_call.name = "search_tool"
|
||||
mock_function_call.args = {"query": "weather in Tokyo"}
|
||||
|
||||
mock_part = Mock()
|
||||
mock_part.function_call = mock_function_call
|
||||
mock_part.text = None
|
||||
|
||||
mock_content = Mock()
|
||||
mock_content.parts = [mock_part]
|
||||
|
||||
mock_candidate = Mock()
|
||||
mock_candidate.content = mock_content
|
||||
|
||||
mock_response = Mock()
|
||||
mock_response.candidates = [mock_candidate]
|
||||
mock_response.text = None
|
||||
mock_response.usage_metadata = Mock()
|
||||
mock_response.usage_metadata.prompt_token_count = 100
|
||||
mock_response.usage_metadata.candidates_token_count = 50
|
||||
mock_response.usage_metadata.total_token_count = 150
|
||||
|
||||
mock_generate.return_value = mock_response
|
||||
|
||||
# Pass tools to simulate real scenario where tools are configured
|
||||
# This sets self.tools in the completion object
|
||||
tools = [{"name": "search_tool", "description": "Search for information"}]
|
||||
messages = [{"role": "user", "content": "What's the weather in Tokyo?"}]
|
||||
result = completion.call(messages=messages, tools=tools)
|
||||
|
||||
assert result is not None
|
||||
assert result != ""
|
||||
assert "Action: search_tool" in result
|
||||
assert "Action Input:" in result
|
||||
assert "weather in Tokyo" in result
|
||||
|
||||
|
||||
def test_gemini_function_call_with_text_returns_text():
|
||||
"""
|
||||
Test that when Gemini returns both a function call and text content,
|
||||
the text content is returned (not the Action/Action Input format).
|
||||
"""
|
||||
from unittest.mock import Mock, patch
|
||||
from crewai.llms.providers.gemini.completion import GeminiCompletion
|
||||
|
||||
completion = GeminiCompletion(model="gemini-2.0-flash-001")
|
||||
|
||||
with patch.object(completion.client.models, 'generate_content') as mock_generate:
|
||||
mock_function_call = Mock()
|
||||
mock_function_call.name = "search_tool"
|
||||
mock_function_call.args = {"query": "weather"}
|
||||
|
||||
mock_function_part = Mock()
|
||||
mock_function_part.function_call = mock_function_call
|
||||
mock_function_part.text = None
|
||||
|
||||
mock_text_part = Mock()
|
||||
mock_text_part.function_call = None
|
||||
mock_text_part.text = "Let me search for the weather information."
|
||||
|
||||
mock_content = Mock()
|
||||
mock_content.parts = [mock_text_part, mock_function_part]
|
||||
|
||||
mock_candidate = Mock()
|
||||
mock_candidate.content = mock_content
|
||||
|
||||
mock_response = Mock()
|
||||
mock_response.candidates = [mock_candidate]
|
||||
mock_response.text = "Let me search for the weather information."
|
||||
mock_response.usage_metadata = Mock()
|
||||
mock_response.usage_metadata.prompt_token_count = 100
|
||||
mock_response.usage_metadata.candidates_token_count = 50
|
||||
mock_response.usage_metadata.total_token_count = 150
|
||||
|
||||
mock_generate.return_value = mock_response
|
||||
|
||||
# Pass tools to simulate real scenario where tools are configured
|
||||
tools = [{"name": "search_tool", "description": "Search for information"}]
|
||||
messages = [{"role": "user", "content": "What's the weather?"}]
|
||||
result = completion.call(messages=messages, tools=tools)
|
||||
|
||||
assert result is not None
|
||||
assert result != ""
|
||||
assert "Let me search for the weather information" in result
|
||||
assert "Action:" not in result
|
||||
|
||||
|
||||
def test_gemini_function_call_with_available_functions_executes_tool():
|
||||
"""
|
||||
Test that when available_functions is provided, the function call is executed
|
||||
instead of being formatted as Action/Action Input.
|
||||
"""
|
||||
from unittest.mock import Mock, patch
|
||||
from crewai.llms.providers.gemini.completion import GeminiCompletion
|
||||
|
||||
completion = GeminiCompletion(model="gemini-2.0-flash-001")
|
||||
|
||||
def mock_search_tool(query: str) -> str:
|
||||
return f"Search results for: {query}"
|
||||
|
||||
available_functions = {"search_tool": mock_search_tool}
|
||||
|
||||
with patch.object(completion.client.models, 'generate_content') as mock_generate:
|
||||
mock_function_call = Mock()
|
||||
mock_function_call.name = "search_tool"
|
||||
mock_function_call.args = {"query": "weather in Tokyo"}
|
||||
|
||||
mock_part = Mock()
|
||||
mock_part.function_call = mock_function_call
|
||||
mock_part.text = None
|
||||
|
||||
mock_content = Mock()
|
||||
mock_content.parts = [mock_part]
|
||||
|
||||
mock_candidate = Mock()
|
||||
mock_candidate.content = mock_content
|
||||
|
||||
mock_response = Mock()
|
||||
mock_response.candidates = [mock_candidate]
|
||||
mock_response.text = None
|
||||
mock_response.usage_metadata = Mock()
|
||||
mock_response.usage_metadata.prompt_token_count = 100
|
||||
mock_response.usage_metadata.candidates_token_count = 50
|
||||
mock_response.usage_metadata.total_token_count = 150
|
||||
|
||||
mock_generate.return_value = mock_response
|
||||
|
||||
# Pass tools to simulate real scenario where tools are configured
|
||||
tools = [{"name": "search_tool", "description": "Search for information"}]
|
||||
messages = [{"role": "user", "content": "What's the weather in Tokyo?"}]
|
||||
result = completion.call(
|
||||
messages=messages,
|
||||
tools=tools,
|
||||
available_functions=available_functions
|
||||
)
|
||||
|
||||
assert result == "Search results for: weather in Tokyo"
|
||||
|
||||
|
||||
def test_gemini_streaming_function_call_without_text_returns_action_format():
|
||||
"""
|
||||
Test that streaming responses with function calls but no text content
|
||||
return Action/Action Input format.
|
||||
"""
|
||||
from unittest.mock import Mock, patch
|
||||
from crewai.llms.providers.gemini.completion import GeminiCompletion
|
||||
|
||||
completion = GeminiCompletion(model="gemini-2.0-flash-001", stream=True)
|
||||
|
||||
with patch.object(completion.client.models, 'generate_content_stream') as mock_stream:
|
||||
mock_function_call = Mock()
|
||||
mock_function_call.name = "calculator"
|
||||
mock_function_call.args = {"expression": "2 + 2"}
|
||||
|
||||
mock_part = Mock()
|
||||
mock_part.function_call = mock_function_call
|
||||
mock_part.text = None
|
||||
|
||||
mock_content = Mock()
|
||||
mock_content.parts = [mock_part]
|
||||
|
||||
mock_candidate = Mock()
|
||||
mock_candidate.content = mock_content
|
||||
|
||||
mock_chunk = Mock()
|
||||
mock_chunk.candidates = [mock_candidate]
|
||||
mock_chunk.text = None
|
||||
mock_chunk.usage_metadata = Mock()
|
||||
mock_chunk.usage_metadata.prompt_token_count = 50
|
||||
mock_chunk.usage_metadata.candidates_token_count = 25
|
||||
mock_chunk.usage_metadata.total_token_count = 75
|
||||
|
||||
mock_stream.return_value = [mock_chunk]
|
||||
|
||||
# Pass tools to simulate real scenario where tools are configured
|
||||
tools = [{"name": "calculator", "description": "Calculate expressions"}]
|
||||
messages = [{"role": "user", "content": "Calculate 2 + 2"}]
|
||||
result = completion.call(messages=messages, tools=tools)
|
||||
|
||||
assert result is not None
|
||||
assert result != ""
|
||||
assert "Action: calculator" in result
|
||||
assert "Action Input:" in result
|
||||
assert "2 + 2" in result
|
||||
|
||||
|
||||
def test_gemini_format_function_call_as_action():
|
||||
"""
|
||||
Test the _format_function_call_as_action helper method directly.
|
||||
"""
|
||||
from crewai.llms.providers.gemini.completion import GeminiCompletion
|
||||
|
||||
completion = GeminiCompletion(model="gemini-2.0-flash-001")
|
||||
|
||||
result = completion._format_function_call_as_action(
|
||||
function_name="search_tool",
|
||||
function_args={"query": "test query", "limit": 10}
|
||||
)
|
||||
|
||||
assert "Action: search_tool" in result
|
||||
assert "Action Input:" in result
|
||||
assert "test query" in result
|
||||
assert "10" in result
|
||||
|
||||
|
||||
def test_gemini_format_function_call_as_action_handles_special_types():
|
||||
"""
|
||||
Test that _format_function_call_as_action handles non-JSON-serializable types.
|
||||
"""
|
||||
from crewai.llms.providers.gemini.completion import GeminiCompletion
|
||||
from datetime import datetime
|
||||
|
||||
completion = GeminiCompletion(model="gemini-2.0-flash-001")
|
||||
|
||||
result = completion._format_function_call_as_action(
|
||||
function_name="date_tool",
|
||||
function_args={"date": datetime(2024, 1, 1), "value": 42}
|
||||
)
|
||||
|
||||
assert "Action: date_tool" in result
|
||||
assert "Action Input:" in result
|
||||
assert "42" in result
|
||||
|
||||
|
||||
def test_gemini_extract_text_from_parts():
|
||||
"""
|
||||
Test the _extract_text_from_parts helper method.
|
||||
"""
|
||||
from unittest.mock import Mock
|
||||
from crewai.llms.providers.gemini.completion import GeminiCompletion
|
||||
|
||||
completion = GeminiCompletion(model="gemini-2.0-flash-001")
|
||||
|
||||
mock_part1 = Mock()
|
||||
mock_part1.text = "Hello"
|
||||
|
||||
mock_part2 = Mock()
|
||||
mock_part2.text = "World"
|
||||
|
||||
mock_part3 = Mock()
|
||||
mock_part3.text = None
|
||||
|
||||
parts = [mock_part1, mock_part2, mock_part3]
|
||||
result = completion._extract_text_from_parts(parts)
|
||||
|
||||
assert result == "Hello World"
|
||||
|
||||
|
||||
def test_gemini_extract_text_from_parts_empty():
|
||||
"""
|
||||
Test _extract_text_from_parts with no text parts.
|
||||
"""
|
||||
from unittest.mock import Mock
|
||||
from crewai.llms.providers.gemini.completion import GeminiCompletion
|
||||
|
||||
completion = GeminiCompletion(model="gemini-2.0-flash-001")
|
||||
|
||||
mock_part = Mock()
|
||||
mock_part.text = None
|
||||
|
||||
parts = [mock_part]
|
||||
result = completion._extract_text_from_parts(parts)
|
||||
|
||||
assert result == ""
|
||||
|
||||
Reference in New Issue
Block a user