From e6f881bb08ba22bfc6e16a960ac546ecaa4ece25 Mon Sep 17 00:00:00 2001 From: Paul Kompfner Date: Mon, 3 Nov 2025 18:03:48 -0500 Subject: [PATCH] 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 --- CHANGELOG.md | 5 ++ .../adapters/services/gemini_adapter.py | 46 +++++++++++++++++-- .../services/google/gemini_live/llm.py | 11 ----- src/pipecat/services/google/llm.py | 11 ----- src/pipecat/services/llm_service.py | 11 ----- src/pipecat/services/mcp_service.py | 45 ------------------ 6 files changed, 46 insertions(+), 83 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fbc62fb33..9e5985218 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,11 @@ reason")`. - `GeminiLiveLLMService` now properly supports context-provided system 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" 👻 ### Added diff --git a/src/pipecat/adapters/services/gemini_adapter.py b/src/pipecat/adapters/services/gemini_adapter.py index dc5bae559..a4f70b1fa 100644 --- a/src/pipecat/adapters/services/gemini_adapter.py +++ b/src/pipecat/adapters/services/gemini_adapter.py @@ -80,12 +80,48 @@ class GeminiLLMAdapter(BaseLLMAdapter[GeminiLLMInvocationParams]): List of tool definitions formatted for Gemini's function-calling API. 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 - formatted_standard_tools = ( - [{"function_declarations": [func.to_default_dict() for func in functions_schema]}] - if functions_schema - else [] - ) + if functions_schema: + formatted_functions = [] + for func in functions_schema: + 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 = [] if tools_schema.custom_tools: custom_gemini_tools = tools_schema.custom_tools.get(AdapterType.GEMINI, []) diff --git a/src/pipecat/services/google/gemini_live/llm.py b/src/pipecat/services/google/gemini_live/llm.py index 70beed86c..56d81e12b 100644 --- a/src/pipecat/services/google/gemini_live/llm.py +++ b/src/pipecat/services/google/gemini_live/llm.py @@ -772,17 +772,6 @@ class GeminiLiveLLMService(LLMService): """ 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): """Set the audio input pause state. diff --git a/src/pipecat/services/google/llm.py b/src/pipecat/services/google/llm.py index 7cbc82789..47877c4df 100644 --- a/src/pipecat/services/google/llm.py +++ b/src/pipecat/services/google/llm.py @@ -778,17 +778,6 @@ class GoogleLLMService(LLMService): 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]): try: # There's no way to introspect on model capabilities, so diff --git a/src/pipecat/services/llm_service.py b/src/pipecat/services/llm_service.py index 0a1a835f7..e342be7eb 100644 --- a/src/pipecat/services/llm_service.py +++ b/src/pipecat/services/llm_service.py @@ -419,17 +419,6 @@ class LLMService(AIService): return True 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]): """Execute a sequence of function calls from the LLM. diff --git a/src/pipecat/services/mcp_service.py b/src/pipecat/services/mcp_service.py index 3d851e050..5d11cfa29 100644 --- a/src/pipecat/services/mcp_service.py +++ b/src/pipecat/services/mcp_service.py @@ -56,7 +56,6 @@ class MCPClient(BaseObject): super().__init__(**kwargs) self._server_params = server_params self._session = ClientSession - self._needs_alternate_schema = False if isinstance(server_params, StdioServerParameters): self._client = stdio_client @@ -84,48 +83,9 @@ class MCPClient(BaseObject): Returns: 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) 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( self, tool_name: str, tool_schema: Dict[str, Any] ) -> FunctionSchema: @@ -143,11 +103,6 @@ class MCPClient(BaseObject): properties = tool_schema["input_schema"].get("properties", {}) 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( name=tool_name, description=tool_schema["description"],