From fa1c98ff29d36aaca83c20842946fe03b8cddd56 Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Tue, 27 May 2025 22:59:17 -0400 Subject: [PATCH] Add no_op tool to AWSBedrockLLMService --- CHANGELOG.md | 7 ++++ src/pipecat/services/aws/llm.py | 57 ++++++++++++++++++++++++++------- 2 files changed, 53 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d44d108b..aae14f431 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `DailyTransport.stop_transcription()` to be able to start and stop Daily transcription dynamically (maybe with different settings). +### Fixed + +- In `AWSBedrockLLMService`, worked around a possible bug in AWS Bedrock where + a `toolConfig` is required if there has been previous tool use in the + messages array. This workaround includes a no_op factory function call is + used to satisfy the requirement. + ## [0.0.68] - 2025-05-28 ### Added diff --git a/src/pipecat/services/aws/llm.py b/src/pipecat/services/aws/llm.py index 9ec617678..c364c00e9 100644 --- a/src/pipecat/services/aws/llm.py +++ b/src/pipecat/services/aws/llm.py @@ -606,6 +606,21 @@ class AWSBedrockLLMService(LLMService): assistant = AWSBedrockAssistantContextAggregator(context, params=assistant_params) return AWSBedrockContextAggregatorPair(_user=user, _assistant=assistant) + def _create_no_op_tool(self): + """Create a no-operation tool for AWS Bedrock when tool content exists but no tools are defined. + + This is required because AWS Bedrock doesn't allow empty tool configurations after tools were + previously set. Other LLM vendors allow NOT_GIVEN or empty tool configurations, + but AWS Bedrock requires at least one tool to be defined. + """ + return { + "toolSpec": { + "name": "no_operation", + "description": "Internal placeholder function. Do not call this function.", + "inputSchema": {"json": {"type": "object", "properties": {}, "required": []}}, + } + } + @traced_llm async def _process_context(self, context: AWSBedrockLLMContext): # Usage tracking @@ -640,12 +655,27 @@ class AWSBedrockLLMService(LLMService): # Add system message request_params["system"] = context.system - # Add tools if present - if context.tools: - tool_config = {"tools": context.tools} + # Check if messages contain tool use or tool result content blocks + has_tool_content = False + for message in context.messages: + if isinstance(message.get("content"), list): + for content_item in message["content"]: + if "toolUse" in content_item or "toolResult" in content_item: + has_tool_content = True + break + if has_tool_content: + break - # Add tool_choice if specified - if context.tool_choice: + # Handle tools: use current tools, or no-op if tool content exists but no current tools + tools = context.tools or [] + if has_tool_content and not tools: + tools = [self._create_no_op_tool()] + + if tools: + tool_config = {"tools": tools} + + # Only add tool_choice if we have real tools (not just no-op) + if context.tools and context.tool_choice: if context.tool_choice == "auto": tool_config["toolChoice"] = {"auto": {}} elif context.tool_choice == "none": @@ -704,12 +734,17 @@ class AWSBedrockLLMService(LLMService): if event["messageStop"]["stopReason"] == "tool_use" and tool_use_block: try: arguments = json.loads(json_accumulator) if json_accumulator else {} - await self.call_function( - context=context, - tool_call_id=tool_use_block["id"], - function_name=tool_use_block["name"], - arguments=arguments, - ) + + # Only call function if it's not the no-op tool + if tool_use_block["name"] != "no_operation": + await self.call_function( + context=context, + tool_call_id=tool_use_block["id"], + function_name=tool_use_block["name"], + arguments=arguments, + ) + else: + logger.debug("Ignoring no_operation tool call") except json.JSONDecodeError: logger.error(f"Failed to parse tool arguments: {json_accumulator}")