mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-05-08 02:29:00 +00:00
feat: implement strip_openai_specific_schema_fields utility
* Added a new utility function to remove OpenAI-specific fields from schemas, ensuring compatibility with Gemini and Bedrock APIs. * Updated BedrockCompletion and GeminiCompletion classes to utilize this function for cleaning parameters before API calls. * Introduced tests to validate the functionality of the new utility, ensuring it correctly strips incompatible fields while preserving required properties.
This commit is contained in:
@@ -1948,6 +1948,9 @@ class BedrockCompletion(BaseLLM):
|
|||||||
) -> list[ConverseToolTypeDef]:
|
) -> list[ConverseToolTypeDef]:
|
||||||
"""Convert CrewAI tools to Converse API format following AWS specification."""
|
"""Convert CrewAI tools to Converse API format following AWS specification."""
|
||||||
from crewai.llms.providers.utils.common import safe_tool_conversion
|
from crewai.llms.providers.utils.common import safe_tool_conversion
|
||||||
|
from crewai.utilities.pydantic_schema_utils import (
|
||||||
|
strip_openai_specific_schema_fields,
|
||||||
|
)
|
||||||
|
|
||||||
converse_tools: list[ConverseToolTypeDef] = []
|
converse_tools: list[ConverseToolTypeDef] = []
|
||||||
|
|
||||||
@@ -1961,6 +1964,7 @@ class BedrockCompletion(BaseLLM):
|
|||||||
}
|
}
|
||||||
|
|
||||||
if parameters and isinstance(parameters, dict):
|
if parameters and isinstance(parameters, dict):
|
||||||
|
parameters = strip_openai_specific_schema_fields(parameters)
|
||||||
input_schema: ToolInputSchema = {"json": parameters}
|
input_schema: ToolInputSchema = {"json": parameters}
|
||||||
tool_spec["inputSchema"] = input_schema
|
tool_spec["inputSchema"] = input_schema
|
||||||
|
|
||||||
|
|||||||
@@ -529,10 +529,16 @@ class GeminiCompletion(BaseLLM):
|
|||||||
gemini_tools = []
|
gemini_tools = []
|
||||||
|
|
||||||
from crewai.llms.providers.utils.common import safe_tool_conversion
|
from crewai.llms.providers.utils.common import safe_tool_conversion
|
||||||
|
from crewai.utilities.pydantic_schema_utils import (
|
||||||
|
strip_openai_specific_schema_fields,
|
||||||
|
)
|
||||||
|
|
||||||
for tool in tools:
|
for tool in tools:
|
||||||
name, description, parameters = safe_tool_conversion(tool, "Gemini")
|
name, description, parameters = safe_tool_conversion(tool, "Gemini")
|
||||||
|
|
||||||
|
if parameters:
|
||||||
|
parameters = strip_openai_specific_schema_fields(parameters)
|
||||||
|
|
||||||
function_declaration = types.FunctionDeclaration(
|
function_declaration = types.FunctionDeclaration(
|
||||||
name=name,
|
name=name,
|
||||||
description=description,
|
description=description,
|
||||||
|
|||||||
@@ -417,6 +417,35 @@ def strip_null_from_types(schema: dict[str, Any]) -> dict[str, Any]:
|
|||||||
return schema
|
return schema
|
||||||
|
|
||||||
|
|
||||||
|
def strip_openai_specific_schema_fields(schema: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Remove OpenAI strict-mode fields that are incompatible with other providers.
|
||||||
|
|
||||||
|
OpenAI strict mode requires ``additionalProperties: false`` on every object
|
||||||
|
and all properties listed in ``required``. Gemini and Bedrock reject these
|
||||||
|
fields with schema validation errors, so we strip them before handing off to
|
||||||
|
non-OpenAI providers.
|
||||||
|
|
||||||
|
Specifically removes (recursively):
|
||||||
|
- ``additionalProperties`` — not supported by Gemini / Bedrock tool schemas
|
||||||
|
|
||||||
|
Args:
|
||||||
|
schema: JSON schema dictionary (mutated in place and returned).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The modified schema.
|
||||||
|
"""
|
||||||
|
if isinstance(schema, dict):
|
||||||
|
schema.pop("additionalProperties", None)
|
||||||
|
for value in schema.values():
|
||||||
|
if isinstance(value, dict):
|
||||||
|
strip_openai_specific_schema_fields(value)
|
||||||
|
elif isinstance(value, list):
|
||||||
|
for item in value:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
strip_openai_specific_schema_fields(item)
|
||||||
|
return schema
|
||||||
|
|
||||||
|
|
||||||
def generate_model_description(
|
def generate_model_description(
|
||||||
model: type[BaseModel],
|
model: type[BaseModel],
|
||||||
*,
|
*,
|
||||||
|
|||||||
@@ -559,6 +559,39 @@ class TestAnthropicNativeToolCalling:
|
|||||||
_assert_tools_overlapped()
|
_assert_tools_overlapped()
|
||||||
|
|
||||||
|
|
||||||
|
_MCP_OPTIONAL_FIELDS_TOOL_DEFS = [
|
||||||
|
{
|
||||||
|
"name": "filtered_search",
|
||||||
|
"description": (
|
||||||
|
"Search a knowledge base. Pass null for any filter you don't need."
|
||||||
|
),
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"query": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Search query string",
|
||||||
|
},
|
||||||
|
"filter_type": {
|
||||||
|
"anyOf": [
|
||||||
|
{"type": "string", "enum": ["news", "blog", "docs"]},
|
||||||
|
{"type": "null"},
|
||||||
|
],
|
||||||
|
"default": None,
|
||||||
|
"description": "Optional content-type filter",
|
||||||
|
},
|
||||||
|
"page_id": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"default": None,
|
||||||
|
"description": "Optional page UUID",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["query"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Google/Gemini Provider Tests
|
# Google/Gemini Provider Tests
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -796,6 +829,9 @@ class TestAzureNativeToolCalling:
|
|||||||
_assert_tools_overlapped()
|
_assert_tools_overlapped()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Bedrock Provider Tests
|
# Bedrock Provider Tests
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -929,6 +965,135 @@ class TestNativeToolCallingBehavior:
|
|||||||
assert hasattr(llm, "supports_function_calling")
|
assert hasattr(llm, "supports_function_calling")
|
||||||
assert llm.supports_function_calling() is True
|
assert llm.supports_function_calling() is True
|
||||||
|
|
||||||
|
@pytest.mark.vcr()
|
||||||
|
def test_gemini_mcp_tool_with_optional_fields_no_schema_error(self) -> None:
|
||||||
|
"""MCP tools with Optional fields must not cause Gemini schema validation errors.
|
||||||
|
|
||||||
|
Regression test for https://github.com/crewAIInc/crewAI/issues/4472.
|
||||||
|
|
||||||
|
Before the fix, generate_model_description() added additionalProperties:false
|
||||||
|
to the tool schema. Gemini rejects this field, raising:
|
||||||
|
"value at properties.<field> must be a list"
|
||||||
|
|
||||||
|
With the fix, strip_openai_specific_schema_fields() removes it before the
|
||||||
|
schema is sent to the Gemini API.
|
||||||
|
|
||||||
|
To record the cassette:
|
||||||
|
RUN_LIVE_GEMINI_TESTS=true GOOGLE_API_KEY=<key> \\
|
||||||
|
uv run pytest tests/agents/test_native_tool_calling.py \\
|
||||||
|
::TestGeminiNativeToolCalling::test_gemini_mcp_tool_with_optional_fields_no_schema_error \\
|
||||||
|
--record-mode=once -v
|
||||||
|
"""
|
||||||
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
from crewai.mcp.config import MCPServerStdio
|
||||||
|
|
||||||
|
stdio_config = MCPServerStdio(command="python", args=["fake_mcp_server.py"])
|
||||||
|
|
||||||
|
agent = Agent(
|
||||||
|
role="Knowledge Researcher",
|
||||||
|
goal="Search a knowledge base and return relevant results",
|
||||||
|
backstory="You are a helpful researcher who uses the filtered_search tool.",
|
||||||
|
mcps=[stdio_config],
|
||||||
|
llm=LLM(model="gemini/gemini-2.5-flash"),
|
||||||
|
verbose=False,
|
||||||
|
max_iter=3,
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch("crewai.agent.core.MCPClient") as mock_client_class:
|
||||||
|
mock_client = AsyncMock()
|
||||||
|
mock_client.list_tools = AsyncMock(
|
||||||
|
return_value=_MCP_OPTIONAL_FIELDS_TOOL_DEFS
|
||||||
|
)
|
||||||
|
mock_client.connected = False
|
||||||
|
mock_client.connect = AsyncMock()
|
||||||
|
mock_client.disconnect = AsyncMock()
|
||||||
|
mock_client.call_tool = AsyncMock(
|
||||||
|
return_value="Search results: Introduction to agents, Tool integration guide"
|
||||||
|
)
|
||||||
|
mock_client_class.return_value = mock_client
|
||||||
|
|
||||||
|
task = Task(
|
||||||
|
description=(
|
||||||
|
"Search the knowledge base for information about CrewAI agents. "
|
||||||
|
"No filters needed — just a general search."
|
||||||
|
),
|
||||||
|
expected_output="A brief summary of relevant search results.",
|
||||||
|
agent=agent,
|
||||||
|
)
|
||||||
|
|
||||||
|
crew = Crew(agents=[agent], tasks=[task])
|
||||||
|
result = crew.kickoff()
|
||||||
|
|
||||||
|
assert result is not None
|
||||||
|
assert result.raw is not None
|
||||||
|
|
||||||
|
@pytest.mark.vcr()
|
||||||
|
def test_bedrock_mcp_tool_with_optional_fields_no_schema_error(self) -> None:
|
||||||
|
"""MCP tools with Optional fields must not cause Bedrock schema validation errors.
|
||||||
|
|
||||||
|
Regression test for https://github.com/crewAIInc/crewAI/issues/4472.
|
||||||
|
|
||||||
|
Before the fix, generate_model_description() added additionalProperties:false
|
||||||
|
to the tool schema. The Bedrock Converse API rejects this field, raising:
|
||||||
|
"JSON schema is invalid"
|
||||||
|
|
||||||
|
With the fix, strip_openai_specific_schema_fields() removes it before the
|
||||||
|
schema is sent to Bedrock.
|
||||||
|
|
||||||
|
Note: MCPClient is mocked — no Bedrock credentials are needed to run this
|
||||||
|
test. The LLM call is replayed from the VCR cassette.
|
||||||
|
|
||||||
|
To record the cassette:
|
||||||
|
AWS_ACCESS_KEY_ID=<key> AWS_SECRET_ACCESS_KEY=<secret> AWS_DEFAULT_REGION=us-east-1 \\
|
||||||
|
uv run pytest tests/agents/test_native_tool_calling.py \\
|
||||||
|
::TestNativeToolCallingBehavior::test_bedrock_mcp_tool_with_optional_fields_no_schema_error \\
|
||||||
|
--record-mode=once -v
|
||||||
|
"""
|
||||||
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
from crewai.mcp.config import MCPServerStdio
|
||||||
|
|
||||||
|
stdio_config = MCPServerStdio(command="python", args=["fake_mcp_server.py"])
|
||||||
|
|
||||||
|
agent = Agent(
|
||||||
|
role="Knowledge Researcher",
|
||||||
|
goal="Search a knowledge base and return relevant results",
|
||||||
|
backstory="You are a helpful researcher who uses the filtered_search tool.",
|
||||||
|
mcps=[stdio_config],
|
||||||
|
llm=LLM(model="bedrock/anthropic.claude-3-haiku-20240307-v1:0"),
|
||||||
|
verbose=False,
|
||||||
|
max_iter=3,
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch("crewai.agent.core.MCPClient") as mock_client_class:
|
||||||
|
mock_client = AsyncMock()
|
||||||
|
mock_client.list_tools = AsyncMock(
|
||||||
|
return_value=_MCP_OPTIONAL_FIELDS_TOOL_DEFS
|
||||||
|
)
|
||||||
|
mock_client.connected = False
|
||||||
|
mock_client.connect = AsyncMock()
|
||||||
|
mock_client.disconnect = AsyncMock()
|
||||||
|
mock_client.call_tool = AsyncMock(
|
||||||
|
return_value="Search results: Introduction to agents, Tool integration guide"
|
||||||
|
)
|
||||||
|
mock_client_class.return_value = mock_client
|
||||||
|
|
||||||
|
task = Task(
|
||||||
|
description=(
|
||||||
|
"Search the knowledge base for information about CrewAI agents. "
|
||||||
|
"No filters needed — just a general search."
|
||||||
|
),
|
||||||
|
expected_output="A brief summary of relevant search results.",
|
||||||
|
agent=agent,
|
||||||
|
)
|
||||||
|
|
||||||
|
crew = Crew(agents=[agent], tasks=[task])
|
||||||
|
result = crew.kickoff()
|
||||||
|
|
||||||
|
assert result is not None
|
||||||
|
assert result.raw is not None
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Token Usage Tests
|
# Token Usage Tests
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
interactions:
|
||||||
|
- request:
|
||||||
|
body: '{"messages": [{"role": "user", "content": [{"text": "\nCurrent Task: Search
|
||||||
|
the knowledge base for information about CrewAI agents. No filters needed \u2014
|
||||||
|
just a general search.\n\nThis is the expected criteria for your final answer:
|
||||||
|
A brief summary of relevant search results.\nyou MUST return the actual complete
|
||||||
|
content as the final answer, not a summary."}]}], "inferenceConfig": {"stopSequences":
|
||||||
|
["\nObservation:"]}, "system": [{"text": "You are Knowledge Researcher. You
|
||||||
|
are a helpful researcher who uses the filtered_search tool.\nYour personal goal
|
||||||
|
is: Search a knowledge base and return relevant results"}], "toolConfig": {"tools":
|
||||||
|
[{"toolSpec": {"name": "python_fake_mcp_server_py_filtered_search", "description":
|
||||||
|
"Search a knowledge base. Pass null for any filter you don''t need.", "inputSchema":
|
||||||
|
{"json": {"properties": {"query": {"description": "Search query string", "title":
|
||||||
|
"Query", "type": "string"}, "filter_type": {"anyOf": [{"type": "string"}, {"type":
|
||||||
|
"object"}, {"type": "null"}], "default": null, "description": "Optional content-type
|
||||||
|
filter", "title": "Filter Type"}, "page_id": {"anyOf": [{"type": "string"},
|
||||||
|
{"type": "object"}, {"type": "null"}], "default": null, "description": "Optional
|
||||||
|
page UUID", "title": "Page Id"}}, "required": ["query", "filter_type", "page_id"],
|
||||||
|
"type": "object"}}}}]}}'
|
||||||
|
headers:
|
||||||
|
Content-Length:
|
||||||
|
- '1324'
|
||||||
|
Content-Type:
|
||||||
|
- !!binary |
|
||||||
|
YXBwbGljYXRpb24vanNvbg==
|
||||||
|
User-Agent:
|
||||||
|
- X-USER-AGENT-XXX
|
||||||
|
amz-sdk-invocation-id:
|
||||||
|
- AMZ-SDK-INVOCATION-ID-XXX
|
||||||
|
amz-sdk-request:
|
||||||
|
- !!binary |
|
||||||
|
YXR0ZW1wdD0x
|
||||||
|
authorization:
|
||||||
|
- AUTHORIZATION-XXX
|
||||||
|
x-amz-date:
|
||||||
|
- X-AMZ-DATE-XXX
|
||||||
|
method: POST
|
||||||
|
uri: https://bedrock-runtime.us-west-2.amazonaws.com/model/anthropic.claude-3-haiku-20240307-v1%3A0/converse
|
||||||
|
response:
|
||||||
|
body:
|
||||||
|
string: '{"metrics":{"latencyMs":1322},"output":{"message":{"content":[{"text":"Okay,
|
||||||
|
let''s search the knowledge base for information about CrewAI agents:"},{"toolUse":{"input":{"query":"CrewAI
|
||||||
|
agents","filter_type":null,"page_id":null},"name":"python_fake_mcp_server_py_filtered_search","toolUseId":"tooluse_KjK6AV9oUkSLKCHlXDoc05"}}],"role":"assistant"}},"stopReason":"tool_use","usage":{"inputTokens":572,"outputTokens":125,"serverToolUsage":{},"totalTokens":697}}'
|
||||||
|
headers:
|
||||||
|
Connection:
|
||||||
|
- keep-alive
|
||||||
|
Content-Length:
|
||||||
|
- '458'
|
||||||
|
Content-Type:
|
||||||
|
- application/json
|
||||||
|
Date:
|
||||||
|
- Wed, 25 Feb 2026 23:26:08 GMT
|
||||||
|
x-amzn-RequestId:
|
||||||
|
- X-AMZN-REQUESTID-XXX
|
||||||
|
status:
|
||||||
|
code: 200
|
||||||
|
message: OK
|
||||||
|
- request:
|
||||||
|
body: '{"messages": [{"role": "user", "content": [{"text": "\nCurrent Task: Search
|
||||||
|
the knowledge base for information about CrewAI agents. No filters needed \u2014
|
||||||
|
just a general search.\n\nThis is the expected criteria for your final answer:
|
||||||
|
A brief summary of relevant search results.\nyou MUST return the actual complete
|
||||||
|
content as the final answer, not a summary."}]}, {"role": "assistant", "content":
|
||||||
|
[{"toolUse": {"toolUseId": "tooluse_KjK6AV9oUkSLKCHlXDoc05", "name": "python_fake_mcp_server_py_filtered_search",
|
||||||
|
"input": {}}}]}, {"role": "user", "content": [{"toolResult": {"toolUseId": "tooluse_KjK6AV9oUkSLKCHlXDoc05",
|
||||||
|
"content": [{"text": "Search results: Introduction to agents, Tool integration
|
||||||
|
guide"}]}}]}, {"role": "user", "content": [{"text": "Analyze the tool result.
|
||||||
|
If requirements are met, provide the Final Answer. Otherwise, call the next
|
||||||
|
tool. Deliver only the answer without meta-commentary."}]}], "inferenceConfig":
|
||||||
|
{"stopSequences": ["\nObservation:"]}, "system": [{"text": "You are Knowledge
|
||||||
|
Researcher. You are a helpful researcher who uses the filtered_search tool.\nYour
|
||||||
|
personal goal is: Search a knowledge base and return relevant results"}], "toolConfig":
|
||||||
|
{"tools": [{"toolSpec": {"name": "python_fake_mcp_server_py_filtered_search",
|
||||||
|
"description": "Search a knowledge base. Pass null for any filter you don''t
|
||||||
|
need.", "inputSchema": {"json": {"properties": {"query": {"description": "Search
|
||||||
|
query string", "title": "Query", "type": "string"}, "filter_type": {"anyOf":
|
||||||
|
[{"type": "string"}, {"type": "object"}, {"type": "null"}], "default": null,
|
||||||
|
"description": "Optional content-type filter", "title": "Filter Type"}, "page_id":
|
||||||
|
{"anyOf": [{"type": "string"}, {"type": "object"}, {"type": "null"}], "default":
|
||||||
|
null, "description": "Optional page UUID", "title": "Page Id"}}, "required":
|
||||||
|
["query", "filter_type", "page_id"], "type": "object"}}}}]}}'
|
||||||
|
headers:
|
||||||
|
Content-Length:
|
||||||
|
- '1873'
|
||||||
|
Content-Type:
|
||||||
|
- !!binary |
|
||||||
|
YXBwbGljYXRpb24vanNvbg==
|
||||||
|
User-Agent:
|
||||||
|
- X-USER-AGENT-XXX
|
||||||
|
amz-sdk-invocation-id:
|
||||||
|
- AMZ-SDK-INVOCATION-ID-XXX
|
||||||
|
amz-sdk-request:
|
||||||
|
- !!binary |
|
||||||
|
YXR0ZW1wdD0x
|
||||||
|
authorization:
|
||||||
|
- AUTHORIZATION-XXX
|
||||||
|
x-amz-date:
|
||||||
|
- X-AMZ-DATE-XXX
|
||||||
|
method: POST
|
||||||
|
uri: https://bedrock-runtime.us-west-2.amazonaws.com/model/anthropic.claude-3-haiku-20240307-v1%3A0/converse
|
||||||
|
response:
|
||||||
|
body:
|
||||||
|
string: '{"metrics":{"latencyMs":2372},"output":{"message":{"content":[{"text":"<result>\nIntroduction
|
||||||
|
to agents:\nCrewAI agents are designed to assist humans with a variety of
|
||||||
|
tasks. They can be integrated with other tools and systems to provide automation,
|
||||||
|
task completion, and data processing capabilities. Agents have access to information
|
||||||
|
and can collaborate with each other to achieve objectives.\n\nTool integration
|
||||||
|
guide:\nThe CrewAI tool integration guide provides instructions for connecting
|
||||||
|
agents with other applications and services. This allows agents to access
|
||||||
|
data, trigger actions, and exchange information across different platforms.
|
||||||
|
The guide covers API integration, event handling, and customization options
|
||||||
|
to tailor agent behavior.\n</result>"}],"role":"assistant"}},"stopReason":"end_turn","usage":{"inputTokens":680,"outputTokens":130,"serverToolUsage":{},"totalTokens":810}}'
|
||||||
|
headers:
|
||||||
|
Connection:
|
||||||
|
- keep-alive
|
||||||
|
Content-Length:
|
||||||
|
- '890'
|
||||||
|
Content-Type:
|
||||||
|
- application/json
|
||||||
|
Date:
|
||||||
|
- Wed, 25 Feb 2026 23:26:10 GMT
|
||||||
|
x-amzn-RequestId:
|
||||||
|
- X-AMZN-REQUESTID-XXX
|
||||||
|
status:
|
||||||
|
code: 200
|
||||||
|
message: OK
|
||||||
|
version: 1
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
interactions:
|
||||||
|
- request:
|
||||||
|
body: '{"contents": [{"parts": [{"text": "\nCurrent Task: Search the knowledge
|
||||||
|
base for information about CrewAI agents. No filters needed \u2014 just a general
|
||||||
|
search.\n\nThis is the expected criteria for your final answer: A brief summary
|
||||||
|
of relevant search results.\nyou MUST return the actual complete content as
|
||||||
|
the final answer, not a summary."}], "role": "user"}], "systemInstruction":
|
||||||
|
{"parts": [{"text": "You are Knowledge Researcher. You are a helpful researcher
|
||||||
|
who uses the filtered_search tool.\nYour personal goal is: Search a knowledge
|
||||||
|
base and return relevant results"}], "role": "user"}, "tools": [{"functionDeclarations":
|
||||||
|
[{"description": "Search a knowledge base. Pass null for any filter you don''t
|
||||||
|
need.", "name": "python_fake_mcp_server_py_filtered_search", "parameters_json_schema":
|
||||||
|
{"properties": {"query": {"description": "Search query string", "title": "Query",
|
||||||
|
"type": "string"}, "filter_type": {"anyOf": [{"type": "string"}, {"type": "object"},
|
||||||
|
{"type": "null"}], "default": null, "description": "Optional content-type filter",
|
||||||
|
"title": "Filter Type"}, "page_id": {"anyOf": [{"type": "string"}, {"type":
|
||||||
|
"object"}, {"type": "null"}], "default": null, "description": "Optional page
|
||||||
|
UUID", "title": "Page Id"}}, "required": ["query", "filter_type", "page_id"],
|
||||||
|
"type": "object"}}]}], "generationConfig": {"stopSequences": ["\nObservation:"]}}'
|
||||||
|
headers:
|
||||||
|
User-Agent:
|
||||||
|
- X-USER-AGENT-XXX
|
||||||
|
accept:
|
||||||
|
- '*/*'
|
||||||
|
accept-encoding:
|
||||||
|
- ACCEPT-ENCODING-XXX
|
||||||
|
connection:
|
||||||
|
- keep-alive
|
||||||
|
content-length:
|
||||||
|
- '1360'
|
||||||
|
content-type:
|
||||||
|
- application/json
|
||||||
|
host:
|
||||||
|
- generativelanguage.googleapis.com
|
||||||
|
x-goog-api-client:
|
||||||
|
- google-genai-sdk/1.49.0 gl-python/3.13.3
|
||||||
|
x-goog-api-key:
|
||||||
|
- X-GOOG-API-KEY-XXX
|
||||||
|
method: POST
|
||||||
|
uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent
|
||||||
|
response:
|
||||||
|
body:
|
||||||
|
string: "{\n \"candidates\": [\n {\n \"content\": {\n \"parts\":
|
||||||
|
[\n {\n \"functionCall\": {\n \"name\": \"python_fake_mcp_server_py_filtered_search\",\n
|
||||||
|
\ \"args\": {\n \"query\": \"CrewAI agents\"\n
|
||||||
|
\ }\n },\n \"thoughtSignature\": \"CsMEAb4+9vuUI8bbZhI2K5IFCJz+FZPQ+EP/o+hvgBgIAImbCgAE3lyB3kNCn3FcFFSBSPP0KKDiEqq/J0tZjsSmNolNSZr2qAc2kLpzDkI26PhKOk4X3WQ9JEUuaCopvsFYd4mBgZqLaK8PAASQQX8zFD5X54TCk6RFtood4o82kouQbXPCUZAKiC9V/tkz3Qqe5M+K7W69MKzsKGiQakO5I5UfIKc/KD69rROuf/ZUw5oU+8+0qQVhvp8HuPWyQW815eyVZidwIUfFRaswRzZp48PefjNWKhf/1NcVM5fy+RLq5M88FQZ9zhcY9wjV3H66G439VHPCH1SekLJ7A7se/60Sj7/oXfzl93FZ9kb4RMA2PsEJSFqb+EDVDhEmkRxsqCERSoEm7vSVut7AEJiO4UG3+5986MjV52IOpgdTZxWwUoJbQ9xKH7fov6rEDLa1ExSYLmqvnPJbZEhfR39vVmTM105qIlzFSVOBekOsqc6VCSX5P0iwmlxGib7YTRYFaqyY2ZuEWp4Xw+m82MjBIFRZPHW80tSG285VSTFUx6dxkMsfC1XT0TzqjwmeV+xSQ9fvuu8hM6EO5XN+vHIJSC2WPrN+Hc0WIaZb8Ol1q4BITA7jxAp8RLg7PD4FI7VzzMzyDMdf7kffSVMRGOdaNC8zGfM2KbSP3J2QA/InmM9bUawKsOz+nMLsjTs2fTp2nvMmNRePWlNkKOxV0DUfxCQRPtQOZk/OKd9U6GpCd/omHtGkk8hWcgYwhxQxXsNOqnIb\"\n
|
||||||
|
\ }\n ],\n \"role\": \"model\"\n },\n \"finishReason\":
|
||||||
|
\"STOP\",\n \"index\": 0,\n \"finishMessage\": \"Model generated
|
||||||
|
function call(s).\"\n }\n ],\n \"usageMetadata\": {\n \"promptTokenCount\":
|
||||||
|
203,\n \"candidatesTokenCount\": 28,\n \"totalTokenCount\": 361,\n \"promptTokensDetails\":
|
||||||
|
[\n {\n \"modality\": \"TEXT\",\n \"tokenCount\": 203\n
|
||||||
|
\ }\n ],\n \"thoughtsTokenCount\": 130\n },\n \"modelVersion\":
|
||||||
|
\"gemini-2.5-flash\",\n \"responseId\": \"bICfafeTH_T5-8YP8rnE6Qo\"\n}\n"
|
||||||
|
headers:
|
||||||
|
Alt-Svc:
|
||||||
|
- h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
|
||||||
|
Content-Type:
|
||||||
|
- application/json; charset=UTF-8
|
||||||
|
Date:
|
||||||
|
- Wed, 25 Feb 2026 23:06:21 GMT
|
||||||
|
Server:
|
||||||
|
- scaffolding on HTTPServer2
|
||||||
|
Server-Timing:
|
||||||
|
- gfet4t7; dur=1210
|
||||||
|
Transfer-Encoding:
|
||||||
|
- chunked
|
||||||
|
Vary:
|
||||||
|
- Origin
|
||||||
|
- X-Origin
|
||||||
|
- Referer
|
||||||
|
X-Content-Type-Options:
|
||||||
|
- X-CONTENT-TYPE-XXX
|
||||||
|
X-Frame-Options:
|
||||||
|
- X-FRAME-OPTIONS-XXX
|
||||||
|
X-XSS-Protection:
|
||||||
|
- '0'
|
||||||
|
status:
|
||||||
|
code: 200
|
||||||
|
message: OK
|
||||||
|
- request:
|
||||||
|
body: '{"contents": [{"parts": [{"text": "\nCurrent Task: Search the knowledge
|
||||||
|
base for information about CrewAI agents. No filters needed \u2014 just a general
|
||||||
|
search.\n\nThis is the expected criteria for your final answer: A brief summary
|
||||||
|
of relevant search results.\nyou MUST return the actual complete content as
|
||||||
|
the final answer, not a summary."}], "role": "user"}, {"parts": [{"functionCall":
|
||||||
|
{"args": {"query": "CrewAI agents"}, "name": "python_fake_mcp_server_py_filtered_search"}}],
|
||||||
|
"role": "model"}, {"parts": [{"functionResponse": {"name": "python_fake_mcp_server_py_filtered_search",
|
||||||
|
"response": {"result": "Search results: Introduction to agents, Tool integration
|
||||||
|
guide"}}}], "role": "user"}, {"parts": [{"text": "Analyze the tool result. If
|
||||||
|
requirements are met, provide the Final Answer. Otherwise, call the next tool.
|
||||||
|
Deliver only the answer without meta-commentary."}], "role": "user"}], "systemInstruction":
|
||||||
|
{"parts": [{"text": "You are Knowledge Researcher. You are a helpful researcher
|
||||||
|
who uses the filtered_search tool.\nYour personal goal is: Search a knowledge
|
||||||
|
base and return relevant results"}], "role": "user"}, "tools": [{"functionDeclarations":
|
||||||
|
[{"description": "Search a knowledge base. Pass null for any filter you don''t
|
||||||
|
need.", "name": "python_fake_mcp_server_py_filtered_search", "parameters_json_schema":
|
||||||
|
{"properties": {"query": {"description": "Search query string", "title": "Query",
|
||||||
|
"type": "string"}, "filter_type": {"anyOf": [{"type": "string"}, {"type": "object"},
|
||||||
|
{"type": "null"}], "default": null, "description": "Optional content-type filter",
|
||||||
|
"title": "Filter Type"}, "page_id": {"anyOf": [{"type": "string"}, {"type":
|
||||||
|
"object"}, {"type": "null"}], "default": null, "description": "Optional page
|
||||||
|
UUID", "title": "Page Id"}}, "required": ["query", "filter_type", "page_id"],
|
||||||
|
"type": "object"}}]}], "generationConfig": {"stopSequences": ["\nObservation:"]}}'
|
||||||
|
headers:
|
||||||
|
User-Agent:
|
||||||
|
- X-USER-AGENT-XXX
|
||||||
|
accept:
|
||||||
|
- '*/*'
|
||||||
|
accept-encoding:
|
||||||
|
- ACCEPT-ENCODING-XXX
|
||||||
|
connection:
|
||||||
|
- keep-alive
|
||||||
|
content-length:
|
||||||
|
- '1893'
|
||||||
|
content-type:
|
||||||
|
- application/json
|
||||||
|
host:
|
||||||
|
- generativelanguage.googleapis.com
|
||||||
|
x-goog-api-client:
|
||||||
|
- google-genai-sdk/1.49.0 gl-python/3.13.3
|
||||||
|
x-goog-api-key:
|
||||||
|
- X-GOOG-API-KEY-XXX
|
||||||
|
method: POST
|
||||||
|
uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent
|
||||||
|
response:
|
||||||
|
body:
|
||||||
|
string: "{\n \"candidates\": [\n {\n \"content\": {\n \"parts\":
|
||||||
|
[\n {\n \"text\": \"Search results: Introduction to agents,
|
||||||
|
Tool integration guide\"\n }\n ],\n \"role\": \"model\"\n
|
||||||
|
\ },\n \"finishReason\": \"STOP\",\n \"index\": 0\n }\n ],\n
|
||||||
|
\ \"usageMetadata\": {\n \"promptTokenCount\": 299,\n \"candidatesTokenCount\":
|
||||||
|
10,\n \"totalTokenCount\": 309,\n \"promptTokensDetails\": [\n {\n
|
||||||
|
\ \"modality\": \"TEXT\",\n \"tokenCount\": 299\n }\n ]\n
|
||||||
|
\ },\n \"modelVersion\": \"gemini-2.5-flash\",\n \"responseId\": \"bYCfacPnLsD4-8YP6p64sQs\"\n}\n"
|
||||||
|
headers:
|
||||||
|
Alt-Svc:
|
||||||
|
- h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
|
||||||
|
Content-Type:
|
||||||
|
- application/json; charset=UTF-8
|
||||||
|
Date:
|
||||||
|
- Wed, 25 Feb 2026 23:06:22 GMT
|
||||||
|
Server:
|
||||||
|
- scaffolding on HTTPServer2
|
||||||
|
Server-Timing:
|
||||||
|
- gfet4t7; dur=380
|
||||||
|
Transfer-Encoding:
|
||||||
|
- chunked
|
||||||
|
Vary:
|
||||||
|
- Origin
|
||||||
|
- X-Origin
|
||||||
|
- Referer
|
||||||
|
X-Content-Type-Options:
|
||||||
|
- X-CONTENT-TYPE-XXX
|
||||||
|
X-Frame-Options:
|
||||||
|
- X-FRAME-OPTIONS-XXX
|
||||||
|
X-XSS-Protection:
|
||||||
|
- '0'
|
||||||
|
status:
|
||||||
|
code: 200
|
||||||
|
message: OK
|
||||||
|
version: 1
|
||||||
@@ -309,6 +309,105 @@ class TestOptionalFieldsPreserveNull:
|
|||||||
assert "page_id" in params["required"]
|
assert "page_id" in params["required"]
|
||||||
|
|
||||||
|
|
||||||
|
class TestStripOpenAISpecificSchemaFields:
|
||||||
|
"""Tests for strip_openai_specific_schema_fields.
|
||||||
|
|
||||||
|
All tests call the real schema pipeline with real tool instances —
|
||||||
|
no mocking of internal functions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_removes_additional_properties_at_root(self) -> None:
|
||||||
|
"""additionalProperties must be absent after stripping."""
|
||||||
|
from crewai.utilities.pydantic_schema_utils import (
|
||||||
|
strip_openai_specific_schema_fields,
|
||||||
|
)
|
||||||
|
|
||||||
|
tools = [MCPStyleTool()]
|
||||||
|
schemas, _ = convert_tools_to_openai_schema(tools)
|
||||||
|
params = schemas[0]["function"]["parameters"]
|
||||||
|
|
||||||
|
# Confirm it's present in the OpenAI-format schema
|
||||||
|
assert params.get("additionalProperties") is False
|
||||||
|
|
||||||
|
clean = strip_openai_specific_schema_fields(params)
|
||||||
|
assert "additionalProperties" not in clean
|
||||||
|
|
||||||
|
def test_removes_additional_properties_recursively(self) -> None:
|
||||||
|
"""Nested objects must also have additionalProperties removed."""
|
||||||
|
from crewai.utilities.pydantic_schema_utils import (
|
||||||
|
strip_openai_specific_schema_fields,
|
||||||
|
)
|
||||||
|
|
||||||
|
class NestedInput(BaseModel):
|
||||||
|
config: dict[str, Any] = Field(default_factory=dict)
|
||||||
|
query: str = Field(description="query")
|
||||||
|
|
||||||
|
class NestedTool(BaseTool):
|
||||||
|
name: str = "nested_tool"
|
||||||
|
description: str = "tool with nested object"
|
||||||
|
args_schema: type[BaseModel] = NestedInput
|
||||||
|
|
||||||
|
def _run(self, **kwargs: Any) -> str:
|
||||||
|
return "ok"
|
||||||
|
|
||||||
|
schemas, _ = convert_tools_to_openai_schema([NestedTool()])
|
||||||
|
params = schemas[0]["function"]["parameters"]
|
||||||
|
clean = strip_openai_specific_schema_fields(params)
|
||||||
|
|
||||||
|
def has_additional_properties(obj: Any) -> bool:
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
if "additionalProperties" in obj:
|
||||||
|
return True
|
||||||
|
return any(has_additional_properties(v) for v in obj.values())
|
||||||
|
if isinstance(obj, list):
|
||||||
|
return any(has_additional_properties(i) for i in obj)
|
||||||
|
return False
|
||||||
|
|
||||||
|
assert not has_additional_properties(clean)
|
||||||
|
|
||||||
|
def test_preserves_required_and_properties(self) -> None:
|
||||||
|
"""required and properties must survive the strip."""
|
||||||
|
from crewai.utilities.pydantic_schema_utils import (
|
||||||
|
strip_openai_specific_schema_fields,
|
||||||
|
)
|
||||||
|
|
||||||
|
tools = [MCPStyleTool()]
|
||||||
|
schemas, _ = convert_tools_to_openai_schema(tools)
|
||||||
|
params = schemas[0]["function"]["parameters"]
|
||||||
|
clean = strip_openai_specific_schema_fields(params)
|
||||||
|
|
||||||
|
assert "properties" in clean
|
||||||
|
assert "required" in clean
|
||||||
|
assert "query" in clean["properties"]
|
||||||
|
|
||||||
|
def test_gemini_bedrock_path_extracts_clean_parameters(self) -> None:
|
||||||
|
"""extract_tool_info returns the same parameters dict that
|
||||||
|
strip_openai_specific_schema_fields will clean — verifying the
|
||||||
|
full Gemini/Bedrock conversion pipeline produces a valid schema."""
|
||||||
|
from crewai.llms.providers.utils.common import extract_tool_info
|
||||||
|
from crewai.utilities.pydantic_schema_utils import (
|
||||||
|
strip_openai_specific_schema_fields,
|
||||||
|
)
|
||||||
|
|
||||||
|
tools = [MCPStyleTool()]
|
||||||
|
schemas, _ = convert_tools_to_openai_schema(tools)
|
||||||
|
|
||||||
|
# extract_tool_info is what Gemini/Bedrock call internally
|
||||||
|
_name, _desc, parameters = extract_tool_info(schemas[0])
|
||||||
|
|
||||||
|
assert parameters.get("additionalProperties") is False, (
|
||||||
|
"Raw OpenAI schema should have additionalProperties=false"
|
||||||
|
)
|
||||||
|
|
||||||
|
clean = strip_openai_specific_schema_fields(parameters)
|
||||||
|
assert "additionalProperties" not in clean
|
||||||
|
# null types preserved for optional fields
|
||||||
|
assert any(
|
||||||
|
opt.get("type") == "null"
|
||||||
|
for opt in clean["properties"]["page_id"].get("anyOf", [])
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestSummarizeMessages:
|
class TestSummarizeMessages:
|
||||||
"""Tests for summarize_messages function."""
|
"""Tests for summarize_messages function."""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user