Compare commits

...

3 Commits

Author SHA1 Message Date
Devin AI
9a6ede9ae5 Fix type-checker errors in llm.py
Co-Authored-By: Joe Moura <joao@crewai.com>
2025-03-24 11:43:00 +00:00
Devin AI
6df3007190 Improve code with type hints, error handling, and additional tests
Co-Authored-By: Joe Moura <joao@crewai.com>
2025-03-24 11:38:32 +00:00
Devin AI
3060c6f919 Fix Gemini API additionalProperties error (issue #2457)
Co-Authored-By: Joe Moura <joao@crewai.com>
2025-03-24 11:32:29 +00:00
3 changed files with 105 additions and 5 deletions

View File

@@ -305,8 +305,6 @@ class LLM:
Args:
messages: Input messages for the LLM
tools: Optional list of tool schemas
callbacks: Optional list of callback functions
available_functions: Optional dict of available functions
Returns:
Dict[str, Any]: Parameters for the completion call
@@ -316,7 +314,10 @@ class LLM:
messages = [{"role": "user", "content": messages}]
formatted_messages = self._format_messages_for_provider(messages)
# --- 2) Prepare the parameters for the completion call
# --- 2) If using Gemini, ensure additionalProperties is not in tool schemas
self._clean_gemini_tool_parameters(tools)
# --- 3) Prepare the parameters for the completion call
params = {
"model": self.model,
"messages": formatted_messages,
@@ -345,6 +346,22 @@ class LLM:
# Remove None values from params
return {k: v for k, v in params.items() if v is not None}
def _clean_gemini_tool_parameters(
self, tools: Optional[List[dict]]
) -> None:
"""Remove additionalProperties from tool parameters for Gemini compatibility.
Args:
tools: List of tool dictionaries that may contain function schemas
"""
if not tools or "gemini" not in self.model.lower():
return
for tool in tools:
if isinstance(tool, dict) and "function" in tool:
params = tool["function"].get("parameters", {})
params.pop("additionalProperties", None)
def _handle_streaming_response(
self,

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
import inspect
import textwrap
from typing import Any, Callable, Optional, Union, get_type_hints
from typing import Any, Callable, Dict, List, Optional, Type, Union, get_type_hints
from pydantic import BaseModel, Field, create_model
@@ -239,6 +239,37 @@ class CrewStructuredTool:
def args(self) -> dict:
"""Get the tool's input arguments schema."""
return self.args_schema.model_json_schema()["properties"]
def to_openai_function(self) -> Dict[str, Any]:
"""Convert the tool to an OpenAI function format.
Returns:
Dict[str, Any]: A dictionary in the OpenAI function format.
Example:
```python
tool = CrewStructuredTool(...)
function_dict = tool.to_openai_function()
# Use with OpenAI or compatible APIs
```
Raises:
ValueError: If the schema conversion fails
"""
try:
schema = self.args_schema.model_json_schema()
schema.pop("additionalProperties", None)
return {
"type": "function",
"function": {
"name": self.name,
"description": self.description,
"parameters": schema
}
}
except Exception as e:
raise ValueError(f"Failed to convert tool to OpenAI function format: {str(e)}")
def __repr__(self) -> str:
return (

View File

@@ -25,7 +25,7 @@ def schema_class():
return TestSchema
class InternalCrewStructuredTool:
class TestInternalCrewStructuredTool:
def test_initialization(self, basic_function, schema_class):
"""Test basic initialization of CrewStructuredTool"""
tool = CrewStructuredTool(
@@ -144,3 +144,55 @@ class InternalCrewStructuredTool:
{"required_param": "test", "optional_param": "custom", "nullable_param": 42}
)
assert result == "test custom 42"
def test_to_openai_function_no_additional_properties(self):
"""Test that the to_openai_function method doesn't include additionalProperties."""
class TestSchema(BaseModel):
test_field: str = Field(..., description="A test field")
def test_func(test_field: str) -> str:
"""Test function that returns the input."""
return f"Test function received: {test_field}"
tool = CrewStructuredTool(
name="test_tool",
description="A test tool",
args_schema=TestSchema,
func=test_func
)
function_dict = tool.to_openai_function()
assert "additionalProperties" not in function_dict["function"]["parameters"]
# Verify other properties are correct
assert function_dict["type"] == "function"
assert function_dict["function"]["name"] == "test_tool"
assert function_dict["function"]["description"] == "A test tool"
assert "properties" in function_dict["function"]["parameters"]
assert "test_field" in function_dict["function"]["parameters"]["properties"]
def test_to_openai_function_edge_cases(self):
"""Test edge cases for to_openai_function conversion."""
class EmptySchema(BaseModel):
pass
def empty_func() -> None:
pass
tool = CrewStructuredTool(
name="empty_tool",
description="A tool with empty schema",
args_schema=EmptySchema,
func=empty_func
)
function_dict = tool.to_openai_function()
assert function_dict["type"] == "function"
assert function_dict["function"]["name"] == "empty_tool"
# Check that parameters contains the expected fields
params = function_dict["function"]["parameters"]
assert params["title"] == "EmptySchema"
assert params["type"] == "object"
assert "properties" in params # Empty schema still has a properties field