Description
Description
Hi CrewAI Team,
We are encountering a persistent issue where our Agent, configured with a Google Gemini LLM (via crewai.LLM
), fails to make tool calls. The Agent gets stuck in the "Thinking..." phase, and our LLMCallStartedEvent
listener consistently shows that the tools
parameter in the API request to Gemini is null
.
We have conducted extensive debugging and believe the issue lies in how CrewAI (v0.121.0) and LiteLLM (v1.70.4) handle the transmission of structured tool definitions to the Gemini API.
Below is a detailed breakdown of the bug, our environment, and our findings:
1. Describe the Bug
When using CrewAI v0.121.0
with Google Gemini LLM (e.g., gemini/gemini-2.0-flash
via crewai.LLM
), custom tools configured for an Agent are not being correctly passed to the underlying Gemini API. Specifically, when inspecting the LLMCallStartedEvent
, the event.__dict__['tools']
field (which should contain the structured tool definitions for the LLM API call) is consistently null
. This prevents the Gemini LLM from recognizing or utilizing its native function calling capabilities, causing the Agent to get stuck in the "Thinking..." phase without generating any Action:
or Action Input:
.
This issue persists despite extensive Prompt engineering, including:
- Ensuring all Pydantic
args_schema
fields for custom tools have non-nulldescription
and nodefault
values. - Adding
model_config = ConfigDict(extra='forbid')
to Pydanticargs_schema
models. - Manually embedding a standard JSON Schema representation of
args_schema
within thetool.description
attribute passed to the LLM in the text prompt. - Providing clear, ReAct-style tool invocation examples within the
Task
description.
2. Environment and Library Versions
- CrewAI:
0.121.0
- crewai-tools: (Please specify your version, e.g.,
0.x.y
) - LiteLLM:
1.70.4
- google-generativeai (SDK):
0.8.5
- Pydantic:
v2.11.5
- Python: (Please specify your Python version, e.g.,
3.10.x
or3.12.x
) - Operating System: WSL2 (Ubuntu)
- Network: Accessing Gemini API via a SOCKS5 proxy (
socks5h://127.0.0.1:10808
), withHTTP_PROXY
,HTTPS_PROXY
, andALL_PROXY
environment variables correctly set. Necessary SOCKS support libraries (pysocks
,httpx[socks]
) are installed. - LLM Configuration in CrewAI:
from crewai import LLM gemini_llm = LLM( model="gemini/gemini-2.0-flash", api_key=os.getenv("GEMINI_API_KEY"), temperature=0.1, max_tokens=2048 )
3. Steps to Reproduce
- Define a custom tool inheriting from
crewai.tools.BaseTool
with a Pydanticargs_schema
. - Configure an
Agent
with this custom tool and the Gemini LLM instance. - Define a
Task
that explicitly guides the Agent to use this custom tool. - Instantiate a
Crew
andkickoff()
the task. - Implement
LLMCallStartedEvent
andLLMCallCompletedEvent
listeners to inspect the data sent to and received from the LLM.
4. Expected Behavior
The LLMCallStartedEvent.event.__dict__['tools']
field (or equivalent attribute holding the structured tool definitions passed to litellm.completion
) should contain a list of tool definitions formatted兼容 Gemini API (e.g., a list of dictionaries, each with a function_declarations
key). The Gemini LLM should then be able to recognize these tools and, when appropriate, return a response containing tool_calls
to invoke the custom tool.
5. Actual Behavior & Logs
- The Agent gets stuck in the "Thinking..." phase indefinitely.
- The
LLMCallStartedEvent.event.__dict__['tools']
field is consistentlynull
. - The
system
message in the prompt sent to the LLM (observed viaLLMCallStartedEvent.messages
) does correctly show the tool's name and a JSON Schema representation of its arguments (due to manual embedding in the tool'sdescription
attribute), but this text-based schema is not a substitute for the API-leveltools
parameter. - Crucially, a direct test using
litellm.completion()
(outside of CrewAI) with the same Gemini model, API key, and a manually constructedtools
parameter containing the Pydantic-derived JSON schema for the custom tool, successfully passes the tool definition to the Gemini API. LiteLLM's debug logs (Final returned optional params: {'tools': [{'function_declarations': [...]}]}
) confirm this. The Gemini API, in this direct LiteLLM test, recognizes the tool (though it may still opt to respond with text asking for more clarity before calling it, which is acceptable LLM behavior). This indicates LiteLLM itself is capable of handling and transmitting the tool schema correctly when provided with it.
Log Snippet from LLMCallStartedEvent
in CrewAI:
// event.__dict__
{
"timestamp": "2025-05-24 01:32:09.566129",
"type": "llm_call_started",
// ... other fields ...
"messages": [
{
"role": "system",
"content": "You are 信息检索专家...\nTool Name: HybridRAGQueryTool\nTool Arguments (JSON Schema):\n```json\n{\n \"additionalProperties\": false,\n \"properties\": {\n \"query\": {\n \"description\": \"用户提出的原始查询文本。\",\n \"title\": \"Query\",\n \"type\": \"string\"\n },\n // ... other params ...\n },\n \"required\": [\"query\", ...],\n \"title\": \"QueryRequest\",\n \"type\": \"object\"\n}\n```\nTool Description: 【核心RAG工具】...\n\nIMPORTANT: Use the following format in your response..."
},
{
"role": "user",
"content": "\nCurrent Task: 用户查询是:'公司2024年第一季度的销售额是多少?'..."
}
],
"tools": null, // <--- THIS IS THE CORE PROBLEM
"callbacks": [
"<crewai.utilities.token_counter_callback.TokenCalcHandler object at 0x...>"
],
"available_functions": null
}
6. Suspected Cause
The issue appears to stem from how CrewAI v0.121.0
(possibly in conjunction with LiteLLM v1.70.4
) processes the Agent
's tools
list and prepares the tools
parameter for the litellm.completion
call when the target LLM is Gemini. Despite the google-generativeai
package being installed and Pydantic schemas being well-defined (without default
values and with ConfigDict(extra='forbid')
), the structured tool definitions are not making it into the final API request's tools
field.
This could be due to:
- An internal bug in CrewAI's
LLM
class or its tool handling logic specific to Gemini. - An incorrect or missing step in the conversion of CrewAI
BaseTool
instances (and their Pydanticargs_schema
) into the format LiteLLM expects for Gemini'sfunction_declarations
. - A misconfiguration or missing parameter in the
crewai.LLM
instantiation for Gemini that is necessary to enable full tool/function calling support via the API.
7. Request for Assistance
- Could you please investigate why the
tools
parameter isnull
in theLLMCallStartedEvent
when using Gemini with CrewAI v0.121.0 and LiteLLM v1.70.4, even when a direct LiteLLM call with a manually constructedtools
parameter works? - Is there a known issue or a specific configuration required in CrewAI or its
LLM
class to ensure Pydanticargs_schema
fromBaseTool
instances are correctly translated into thetools
(containingfunction_declarations
) parameter for Gemini API calls via LiteLLM? - Are there any working examples specifico to CrewAI
v0.121.0+
and Gemini (gemini/gemini-2.0-flash
orgemini-pro
) that demonstrate successful native tool/function calling where the API request'stools
field is correctly populated?
Thank you for your time and assistance with this critical issue.
Steps to Reproduce
Prerequisites:
- Python environment with the following packages installed (versions reflect our testing environment):
crewai==0.121.0
crewai-tools==0.45.0
(请将 0.45.0 替换为您实际使用的crewai-tools
版本,可以通过pip show crewai-tools
查看)litellm==1.70.4
google-generativeai==0.8.5
pydantic==2.11.5
python-dotenv
httpx[socks]
pysocks
- A valid
GEMINI_API_KEY
environment variable set. - (Optional, for full reproduction of our setup) A SOCKS5 proxy running locally on
127.0.0.1:10808
that can access Google Gemini APIs. Environment variablesHTTP_PROXY
,HTTPS_PROXY
,ALL_PROXY
set tosocks5h://127.0.0.1:10808
. Note: The coretools: null
issue seems independent of the proxy, as direct LiteLLM calls with tools work through the proxy. - An MCP proxy server (like
mcpo
) and a simple RAG MCP service. For a truly minimal example to demonstrate thetools: null
issue, this MCP dependency can be mocked out in the tool's_run
method to just return a string, as the problem occurs before tool execution.
Code to Reproduce (Minimal Example Focus):
-
pydantic_models.py
:from pydantic import BaseModel, Field, ConfigDict from typing import List, Dict, Any, Optional class QueryRequest(BaseModel): model_config = ConfigDict(extra='forbid') query: str = Field(description="用户提出的原始查询文本。") top_k_vector: int = Field(description="期望检索的向量搜索结果数量。") top_k_kg: int = Field(description="期望检索的知识图谱结果数量。") top_k_bm25: int = Field(description="期望检索的 BM25 关键词搜索结果数量。")
-
custom_crewai_tools.py
(Simplified for MRE):import json from typing import Type, ClassVar from pydantic import BaseModel from crewai.tools import BaseTool from pydantic_models import QueryRequest # Assuming pydantic_models.py is in the same directory class GeminiJSONSchemaBaseTool(BaseTool): _original_description: str = "" def __init__(self, **kwargs): if 'description' in kwargs: self._original_description = kwargs['description'] super().__init__(**kwargs) if hasattr(self, 'args_schema') and self.args_schema: self._reformat_description_with_json_schema() elif self._original_description: self.description = f"Tool Name: {self.name}\nTool Arguments (JSON Schema):\n```json\nNo arguments required.\n```\nTool Description: {self._original_description}" def _reformat_description_with_json_schema(self): schema_str = "No arguments required." if self.args_schema: try: if isinstance(self.args_schema, type) and issubclass(self.args_schema, BaseModel): schema_dict = self.args_schema.model_json_schema(); schema_str = json.dumps(schema_dict, indent=2, ensure_ascii=False) else: schema_str = str(self.args_schema) except Exception as e: print(f"Error generating JSON schema for {self.name}: {e}"); schema_str = str(self.args_schema) self.description = f"Tool Name: {self.name}\nTool Arguments (JSON Schema):\n```json\n{schema_str}\n```\nTool Description: {self._original_description}" class MinimalRAGTool(GeminiJSONSchemaBaseTool): # Changed from BaseMCPTool for MRE name: str = "MinimalRAGQueryTool" args_schema: Type[BaseModel] = QueryRequest def __init__(self, **kwargs): original_tool_description = "A minimal RAG tool for testing." super().__init__(name=self.name, description=original_tool_description, args_schema=self.args_schema, **kwargs) def _run(self, query: str, top_k_vector: int, top_k_kg: int, top_k_bm25: int) -> str: print(f"--- MinimalRAGTool Executed with query: {query} ---") return json.dumps({"status": "success", "final_answer": f"Mocked RAG result for '{query}'"})
-
run_agent_mre.py
(Minimal Reproducible Example):import os import json from crewai import Agent, Task, Crew, Process, LLM from crewai.utilities.events import base_event_listener, CrewAIEventsBus from crewai.utilities.events.llm_events import LLMCallStartedEvent, LLMCallCompletedEvent from custom_crewai_tools import MinimalRAGTool # Assuming custom_crewai_tools.py is in the same directory from dotenv import load_dotenv load_dotenv() GEMINI_MODEL_NAME = "gemini/gemini-2.0-flash" GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") if not GEMINI_API_KEY: print("CRITICAL ERROR: GEMINI_API_KEY not set.") exit(1) try: llm_for_agent = LLM(model=GEMINI_MODEL_NAME, api_key=GEMINI_API_KEY, temperature=0.1, max_tokens=2048) print(f"Agent LLM configured: {GEMINI_MODEL_NAME}") except Exception as e: print(f"CRITICAL ERROR: Failed to configure LLM: {e}"); exit(1) minimal_rag_tool = MinimalRAGTool() researcher_agent = Agent( role='信息检索专家 (MRE)', goal='使用 MinimalRAGQueryTool 回答查询。', backstory='一个用于复现工具调用问题的AI助手。', llm=llm_for_agent, tools=[minimal_rag_tool], verbose=True, allow_delegation=False ) research_task = Task( description="用户查询是:'{query}'。请使用 `MinimalRAGQueryTool` 工具进行搜索。严格按以下格式调用:\n```\nThought: 我需要用 MinimalRAGQueryTool 搜索 '{query}'。\nAction: MinimalRAGQueryTool\nAction Input: {{\"query\": \"{query}\", \"top_k_vector\": 3, \"top_k_kg\": 2, \"top_k_bm25\": 3}}\n```", expected_output="工具的JSON字符串输出。", agent=researcher_agent, ) test_crew = Crew(agents=[researcher_agent], tasks=[research_task], process=Process.sequential, verbose=True) event_bus = CrewAIEventsBus() @event_bus.on(LLMCallStartedEvent) def on_llm_call_started(source: any, event: LLMCallStartedEvent): print("\n===== LLM Call Started (MRE) =====") print(f"Source: {type(source).__name__}") model_name = getattr(event, 'model', 'Unknown') if hasattr(event, '__dict__') and model_name == 'Unknown': event_dict = event.__dict__; model_name = event_dict.get('model_name', event_dict.get('model', 'Unknown')) print(f"Model: {model_name}") print("Messages: ", json.dumps(event.messages, indent=2, ensure_ascii=False) if isinstance(event.messages, list) else event.messages) print("Tools in API Call: ", json.dumps(event.tools, indent=2, ensure_ascii=False) if hasattr(event, 'tools') and event.tools else "None or Not Present") print("Event Dict Tools: ", json.dumps(event.__dict__.get('tools'), indent=2, default=str) if hasattr(event, '__dict__') else "No __dict__ or no tools key") print("================================\n") @event_bus.on(LLMCallCompletedEvent) def on_llm_call_completed(source: any, event: LLMCallCompletedEvent): print("\n===== LLM Call Completed (MRE) =====") # ... (similar logging as in your original run_agent.py) ... print("==================================\n") print("--- Starting MRE Crew ---") result = test_crew.kickoff(inputs={'query': 'Test query for MRE'}) print("\n--- MRE Crew Finished ---") print("Final Result (MRE):", result.raw if hasattr(result, 'raw') else result)
Steps to run the MRE:
- Save the above code blocks into
pydantic_models.py
,custom_crewai_tools.py
(usingMinimalRAGTool
), andrun_agent_mre.py
respectively in the same directory. - Create a
.env
file withGEMINI_API_KEY="YOUR_KEY"
. - Ensure SOCKS5 proxy is set up in the terminal if needed.
- Run
python run_agent_mre.py
. - Observe the console output.
Expected Result for MRE:
The LLMCallStartedEvent
log should show the Tools in API Call
(or Event Dict Tools
) field populated with the schema for MinimalRAGQueryTool
.
Actual Result for MRE:
The Tools in API Call
(or Event Dict Tools
) field in LLMCallStartedEvent
is null
. The agent gets stuck in "Thinking...".
Expected behavior
The LLMCallStartedEvent.event.__dict__['tools']
field (or an equivalent attribute holding the structured tool definitions passed to litellm.completion
) should contain a list of tool definitions formatted for the Gemini API (e.g., a list of dictionaries, each with a function_declarations
key).
The Gemini LLM should then be able to recognize these tools and, when appropriate, return a response containing tool_calls
to invoke the custom tool, or at least a ReAct formatted text output indicating a tool call. The Agent should proceed to execute the tool.
Screenshots/Code snippets
{
"role": "user",
"content": "\nCurrent Task:
\u7528\u6237\u67e5\u8be2\u662f\uff1a'\u516c\u53f82024\u5e74\u7b2c\u4e00\u5b63\u5ea6\u7684\u9500\u552e\u989d
\u662f\u591a\u5c11\uff1f'\u3002\u4f60\u7684\u4efb\u52a1\u662f\u4f7f\u7528HybridRAGQueryTool
\u5de5\u5177\u
5bf9\u7528\u6237\u67e5\u8be2\u8fdb\u884c\u6df7\u5408RAG\u641c\u7d22\u3002
\u4f60\u5fc5\u987b\u8c03\u7528\u8fd9\u4e2a\u5de5\u5177\u6765\u5b8c\u6210\u4efb\u52a1\u3002\u4ee5\u4e0b\u662
f\u8c03\u7528\u5de5\u5177\u7684\u7cbe\u786e\u793a\u4f8b\uff0c\u8bf7\u4e25\u683c\u9075\u5faa\u6b64\u683c\u5f
0f\uff1a\n\n\nThought: \u6211\u9700\u8981\u5bf9\u7528\u6237\u67e5\u8be2 '\u516c\u53f82024\u5e74\u7b2c\u4e00\u5b63\u5ea6\u7684\u9500\u552e\u989d\u662f\u591a\u5c11\uff1f' \u8fdb\u884c\u6df7\u5408RAG\u641c\u7d22\uff0c\u4ee5\u83b7\u53d6\u76f8\u5173\u4fe1\u606f\u3002\nAction: HybridRAGQueryTool\nAction Input: {{\"query\": \"\u516c\u53f82024\u5e74\u7b2c\u4e00\u5b63\u5ea6\u7684\u9500\u552e\u989d\u662f\u591a\u5c11\uff1f\", \"top_k_vector\": 5, \"top_k_kg\": 3, \"top_k_bm25\": 5}}\n
\n\n\u8bf7\u52a1\u5fc5\u5c06 Action Input
\u4e25\u683c\u683c\u5f0f\u5316\u4e3a JSON
\u5b57\u7b26\u4e32\uff0c\u5e76\u786e\u4fdd\u5176\u4e2d\u7684\u53c2\u6570\u4e0e\u5de5\u5177\u7684
args_schema
\u5339\u914d\u3002\n\u4f60\u7684\u6700\u7ec8\u8f93\u51fa\uff08\u6307\u4f60\u8c03\u7528\u5de5\u5177\u540e\u7
684\u771f\u5b9e
Observation\uff09\u5c06\u662fRAG\u5de5\u5177\u7684\u539f\u59cb\u8f93\u51faJSON\u5b57\u7b26\u4e32\uff0c\u4e0
d\u8fdb\u884c\u4efb\u4f55\u4fee\u6539\u6216\u89e3\u91ca\u3002\u6ce8\u610f\uff1aHybridRAGQueryTool
\u7684\u8f93\u51fa\uff08\u771f\u5b9e\u7684
Observation\uff09\u4f1a\u662f\u4e00\u4e2aJSON\u5b57\u7b26\u4e32\uff0c\u5305\u542b 'status'
\u5b57\u6bb5\u3002\u5982\u679c 'status' \u662f 'error' \u6216
'clarification_needed'\uff0c\u8bf7\u5c06\u9519\u8bef\u4fe1\u606f\u6216\u6f84\u6e05\u8bf7\u6c42\u4f5c\u4e3a
u4f60\u7684\u601d\u8003\u7ed3\u679c\u7684\u4e00\u90e8\u5206\uff0c\u5e76\u505c\u6b62\u8fdb\u4e00\u6b65\u7684
\u56de\u7b54\u751f\u6210\uff0c\u4f8b\u5982\uff1a'Thought:
\u5de5\u5177\u8fd4\u56de\u4e86\u6f84\u6e05\u8bf7\u6c42\uff0c\u6211\u9700\u8981\u62a5\u544a\u7ed9\u7528\u623
7\u3002\nFinal Answer: \u7cfb\u7edf\u9700\u8981\u6f84\u6e05\uff1a[\u6f84\u6e05\u95ee\u9898]'\u3002\n\nThis
is the expected criteria for your final answer:
\u6df7\u5408RAG\u5de5\u5177\u7684\u539f\u59cb\u8f93\u51faJSON\u5b57\u7b26\u4e32\uff0c\u53ef\u80fd\u5305\u54
2b 'status', 'final_answer', 'clarification_question', \u6216 'error_message' \u7b49\u5b57\u6bb5\u3002\nyou
MUST return the actual complete content as the final answer, not a summary.\n\nBegin! This is VERY
important to you, use the tools available and give your best Final Answer, your job depends on
it!\n\nThought:"
}
],
"tools": null,
"callbacks": [
"<crewai.utilities.token_counter_callback.TokenCalcHandler object at 0x7ff336a374a0>"
],
"available_functions": null
}
============================================
🚀 Crew: crew
└── 📋 Task: 50be96fc-ef6f-4afc-94e9-b738123b35df
Status: Executing Task...
└── 🧠 Thinking...
Operating System
Ubuntu 22.04
Python Version
3.12
crewAI Version
0.121.0
crewAI Tools Version
0.45.0
Virtual Environment
Venv
Evidence
Key evidence is the LLMCallStartedEvent log showing event.dict['tools']
as null
, consistently across multiple tests, even after ensuring Pydantic schemas are correctly defined without default
values and with ConfigDict(extra='forbid')
.
A direct LiteLLM call (outside CrewAI, using litellm.completion
) with a manually constructed tools
parameter (containing function_declarations
for the same tool schema) does show LiteLLM attempting to pass these tool definitions to the Gemini API (visible in LiteLLM's Final returned optional params
). This strongly suggests the issue lies within CrewAI's preparation or passing of tool definitions to LiteLLM when targeting Gemini.
Please see the "Description" and "Steps to Reproduce" sections for detailed logs and MRE code.
Possible Solution
None
Additional context
We have been through extensive debugging, including:
- Verifying API key validity and network connectivity (SOCKS5 proxy is in use and confirmed working for direct LiteLLM calls to Gemini for text generation, and also for LiteLLM calls with tools when tools are manually constructed and passed to litellm.completion).
- Ensuring all Pydantic
args_schema
fields have non-nulldescription
, nodefault
values, and useConfigDict(extra='forbid')
. - Manually embedding a standard JSON Schema representation of
args_schema
within thetool.description
attribute (this successfully appears in the text prompt but does not solve thetools: null
API parameter issue). - Providing very explicit ReAct examples in
Task
descriptions. - Updating all relevant libraries (
crewai
,litellm
,google-generativeai
,pydantic
,httpx[socks]
,pysocks
) to their latest or recent stable versions.
The core issue remains that the structured tool definitions are not being passed at the API call level (tools: null
), preventing Gemini from utilizing its native function calling.