From fa1c98ff29d36aaca83c20842946fe03b8cddd56 Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Tue, 27 May 2025 22:59:17 -0400 Subject: [PATCH 1/2] 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}") From bee242b7818065f5ef16353fdd57c5a1f4e72e21 Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Thu, 29 May 2025 10:23:31 -0400 Subject: [PATCH 2/2] Safer check when using the no_operation tool --- src/pipecat/services/aws/llm.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/pipecat/services/aws/llm.py b/src/pipecat/services/aws/llm.py index c364c00e9..a90620b20 100644 --- a/src/pipecat/services/aws/llm.py +++ b/src/pipecat/services/aws/llm.py @@ -631,6 +631,8 @@ class AWSBedrockLLMService(LLMService): cache_creation_input_tokens = 0 use_completion_tokens_estimate = False + using_noop_tool = False + try: await self.push_frame(LLMFullResponseStartFrame()) await self.start_processing_metrics() @@ -670,12 +672,13 @@ class AWSBedrockLLMService(LLMService): tools = context.tools or [] if has_tool_content and not tools: tools = [self._create_no_op_tool()] + using_noop_tool = True 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 not using_noop_tool and context.tool_choice: if context.tool_choice == "auto": tool_config["toolChoice"] = {"auto": {}} elif context.tool_choice == "none": @@ -735,8 +738,8 @@ class AWSBedrockLLMService(LLMService): try: arguments = json.loads(json_accumulator) if json_accumulator else {} - # Only call function if it's not the no-op tool - if tool_use_block["name"] != "no_operation": + # Only call function if it's not the no_operation tool + if not using_noop_tool: await self.call_function( context=context, tool_call_id=tool_use_block["id"],