Remove the "needs alternate schema" mechanism in MCPClient, moving the necessary schema massaging into GeminiLLMAdapter instead.
This does a couple of things: - Makes the `MCPClient` LLM agnostic, setting us up for some upcoming improvements (like making it possible to use with `LLMSwitcher`) - Makes `GeminiLLMAdapter` more robust, as the schema massaging that was previously only done in `MCPClient` is useful for all tools, not just for MCP-provided ones
This commit is contained in:
@@ -68,6 +68,11 @@ reason")`.
|
|||||||
- `GeminiLiveLLMService` now properly supports context-provided system
|
- `GeminiLiveLLMService` now properly supports context-provided system
|
||||||
instruction and tools.
|
instruction and tools.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- Removed `needs_mcp_alternate_schema()` from `LLMService`. The mechanism that
|
||||||
|
relied on it went away.
|
||||||
|
|
||||||
## [0.0.92] - 2025-10-31 🎃 "The Haunted Edition" 👻
|
## [0.0.92] - 2025-10-31 🎃 "The Haunted Edition" 👻
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -80,12 +80,48 @@ class GeminiLLMAdapter(BaseLLMAdapter[GeminiLLMInvocationParams]):
|
|||||||
List of tool definitions formatted for Gemini's function-calling API.
|
List of tool definitions formatted for Gemini's function-calling API.
|
||||||
Includes both converted standard tools and any custom Gemini-specific tools.
|
Includes both converted standard tools and any custom Gemini-specific tools.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def _strip_additional_properties(schema: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Recursively remove "additionalProperties" fields from JSON schema, as they're not supported by Gemini.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
schema: The JSON schema dict to process.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON schema dict with "additionalProperties" stripped out.
|
||||||
|
"""
|
||||||
|
if not isinstance(schema, dict):
|
||||||
|
return schema
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
for key, value in schema.items():
|
||||||
|
if key == "additionalProperties":
|
||||||
|
continue
|
||||||
|
elif isinstance(value, dict):
|
||||||
|
result[key] = _strip_additional_properties(value)
|
||||||
|
elif isinstance(value, list):
|
||||||
|
result[key] = [
|
||||||
|
_strip_additional_properties(item) if isinstance(item, dict) else item
|
||||||
|
for item in value
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
result[key] = value
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
functions_schema = tools_schema.standard_tools
|
functions_schema = tools_schema.standard_tools
|
||||||
formatted_standard_tools = (
|
if functions_schema:
|
||||||
[{"function_declarations": [func.to_default_dict() for func in functions_schema]}]
|
formatted_functions = []
|
||||||
if functions_schema
|
for func in functions_schema:
|
||||||
else []
|
func_dict = func.to_default_dict()
|
||||||
)
|
func_dict["parameters"]["properties"] = _strip_additional_properties(
|
||||||
|
func_dict["parameters"]["properties"]
|
||||||
|
)
|
||||||
|
formatted_functions.append(func_dict)
|
||||||
|
formatted_standard_tools = [{"function_declarations": formatted_functions}]
|
||||||
|
else:
|
||||||
|
formatted_standard_tools = []
|
||||||
custom_gemini_tools = []
|
custom_gemini_tools = []
|
||||||
if tools_schema.custom_tools:
|
if tools_schema.custom_tools:
|
||||||
custom_gemini_tools = tools_schema.custom_tools.get(AdapterType.GEMINI, [])
|
custom_gemini_tools = tools_schema.custom_tools.get(AdapterType.GEMINI, [])
|
||||||
|
|||||||
@@ -772,17 +772,6 @@ class GeminiLiveLLMService(LLMService):
|
|||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def needs_mcp_alternate_schema(self) -> bool:
|
|
||||||
"""Check if this LLM service requires alternate MCP schema.
|
|
||||||
|
|
||||||
Google/Gemini has stricter JSON schema validation and requires
|
|
||||||
certain properties to be removed or modified for compatibility.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True for Google/Gemini services.
|
|
||||||
"""
|
|
||||||
return True
|
|
||||||
|
|
||||||
def set_audio_input_paused(self, paused: bool):
|
def set_audio_input_paused(self, paused: bool):
|
||||||
"""Set the audio input pause state.
|
"""Set the audio input pause state.
|
||||||
|
|
||||||
|
|||||||
@@ -778,17 +778,6 @@ class GoogleLLMService(LLMService):
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def needs_mcp_alternate_schema(self) -> bool:
|
|
||||||
"""Check if this LLM service requires alternate MCP schema.
|
|
||||||
|
|
||||||
Google/Gemini has stricter JSON schema validation and requires
|
|
||||||
certain properties to be removed or modified for compatibility.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True for Google/Gemini services.
|
|
||||||
"""
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _maybe_unset_thinking_budget(self, generation_params: Dict[str, Any]):
|
def _maybe_unset_thinking_budget(self, generation_params: Dict[str, Any]):
|
||||||
try:
|
try:
|
||||||
# There's no way to introspect on model capabilities, so
|
# There's no way to introspect on model capabilities, so
|
||||||
|
|||||||
@@ -419,17 +419,6 @@ class LLMService(AIService):
|
|||||||
return True
|
return True
|
||||||
return function_name in self._functions.keys()
|
return function_name in self._functions.keys()
|
||||||
|
|
||||||
def needs_mcp_alternate_schema(self) -> bool:
|
|
||||||
"""Check if this LLM service requires alternate MCP schema.
|
|
||||||
|
|
||||||
Some LLM services have stricter JSON schema validation and require
|
|
||||||
certain properties to be removed or modified for compatibility.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if MCP schemas should be cleaned for this service, False otherwise.
|
|
||||||
"""
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def run_function_calls(self, function_calls: Sequence[FunctionCallFromLLM]):
|
async def run_function_calls(self, function_calls: Sequence[FunctionCallFromLLM]):
|
||||||
"""Execute a sequence of function calls from the LLM.
|
"""Execute a sequence of function calls from the LLM.
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ class MCPClient(BaseObject):
|
|||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self._server_params = server_params
|
self._server_params = server_params
|
||||||
self._session = ClientSession
|
self._session = ClientSession
|
||||||
self._needs_alternate_schema = False
|
|
||||||
|
|
||||||
if isinstance(server_params, StdioServerParameters):
|
if isinstance(server_params, StdioServerParameters):
|
||||||
self._client = stdio_client
|
self._client = stdio_client
|
||||||
@@ -84,48 +83,9 @@ class MCPClient(BaseObject):
|
|||||||
Returns:
|
Returns:
|
||||||
A ToolsSchema containing all successfully registered tools.
|
A ToolsSchema containing all successfully registered tools.
|
||||||
"""
|
"""
|
||||||
# Check once if the LLM needs alternate strict schema
|
|
||||||
self._needs_alternate_schema = llm and llm.needs_mcp_alternate_schema()
|
|
||||||
tools_schema = await self._register_tools(llm)
|
tools_schema = await self._register_tools(llm)
|
||||||
return tools_schema
|
return tools_schema
|
||||||
|
|
||||||
def _get_alternate_schema_for_strict_validation(self, schema: Dict[str, Any]) -> Dict[str, Any]:
|
|
||||||
"""Get an alternate JSON schema to be compatible with LLMs that have strict validation.
|
|
||||||
|
|
||||||
Some LLMs have stricter validation and don't allow certain schema properties
|
|
||||||
that are valid in standard JSON Schema.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
schema: The JSON schema to get an alternate schema for
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
An alternate schema compatible with strict validation
|
|
||||||
"""
|
|
||||||
if not isinstance(schema, dict):
|
|
||||||
return schema
|
|
||||||
|
|
||||||
alternate_schema = {}
|
|
||||||
|
|
||||||
for key, value in schema.items():
|
|
||||||
# Skip additionalProperties as some LLMs don't like additionalProperties: false
|
|
||||||
if key == "additionalProperties":
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Recursively get alternate schema for nested objects
|
|
||||||
if isinstance(value, dict):
|
|
||||||
alternate_schema[key] = self._get_alternate_schema_for_strict_validation(value)
|
|
||||||
elif isinstance(value, list):
|
|
||||||
alternate_schema[key] = [
|
|
||||||
self._get_alternate_schema_for_strict_validation(item)
|
|
||||||
if isinstance(item, dict)
|
|
||||||
else item
|
|
||||||
for item in value
|
|
||||||
]
|
|
||||||
else:
|
|
||||||
alternate_schema[key] = value
|
|
||||||
|
|
||||||
return alternate_schema
|
|
||||||
|
|
||||||
def _convert_mcp_schema_to_pipecat(
|
def _convert_mcp_schema_to_pipecat(
|
||||||
self, tool_name: str, tool_schema: Dict[str, Any]
|
self, tool_name: str, tool_schema: Dict[str, Any]
|
||||||
) -> FunctionSchema:
|
) -> FunctionSchema:
|
||||||
@@ -143,11 +103,6 @@ class MCPClient(BaseObject):
|
|||||||
properties = tool_schema["input_schema"].get("properties", {})
|
properties = tool_schema["input_schema"].get("properties", {})
|
||||||
required = tool_schema["input_schema"].get("required", [])
|
required = tool_schema["input_schema"].get("required", [])
|
||||||
|
|
||||||
# Only get alternate schema for LLMs that need strict schema validation
|
|
||||||
if self._needs_alternate_schema:
|
|
||||||
logger.debug("Getting alternate schema for strict validation")
|
|
||||||
properties = self._get_alternate_schema_for_strict_validation(properties)
|
|
||||||
|
|
||||||
schema = FunctionSchema(
|
schema = FunctionSchema(
|
||||||
name=tool_name,
|
name=tool_name,
|
||||||
description=tool_schema["description"],
|
description=tool_schema["description"],
|
||||||
|
|||||||
Reference in New Issue
Block a user