From 32714e7a221b2e0a0c051e7521540a5977ac91c0 Mon Sep 17 00:00:00 2001 From: Nick Clegg Date: Fri, 11 Jul 2025 14:30:28 +0000 Subject: [PATCH] refactor: Remove message processor --- src/strands/event_loop/__init__.py | 4 +- src/strands/event_loop/event_loop.py | 4 - src/strands/event_loop/message_processor.py | 105 ------------------ .../event_loop/test_message_processor.py | 47 -------- 4 files changed, 2 insertions(+), 158 deletions(-) delete mode 100644 src/strands/event_loop/message_processor.py delete mode 100644 tests/strands/event_loop/test_message_processor.py diff --git a/src/strands/event_loop/__init__.py b/src/strands/event_loop/__init__.py index 21ae4a70e..2540d552e 100644 --- a/src/strands/event_loop/__init__.py +++ b/src/strands/event_loop/__init__.py @@ -4,6 +4,6 @@ iterative manner. """ -from . import event_loop, message_processor +from . import event_loop -__all__ = ["event_loop", "message_processor"] +__all__ = ["event_loop"] diff --git a/src/strands/event_loop/event_loop.py b/src/strands/event_loop/event_loop.py index c5bf611f7..6323bbb27 100644 --- a/src/strands/event_loop/event_loop.py +++ b/src/strands/event_loop/event_loop.py @@ -28,7 +28,6 @@ from ..types.exceptions import ContextWindowOverflowException, EventLoopException, ModelThrottledException from ..types.streaming import Metrics, StopReason from ..types.tools import ToolChoice, ToolChoiceAuto, ToolConfig, ToolGenerator, ToolResult, ToolUse -from .message_processor import clean_orphaned_empty_tool_uses from .streaming import stream_messages if TYPE_CHECKING: @@ -99,9 +98,6 @@ async def event_loop_cycle(agent: "Agent", kwargs: dict[str, Any]) -> AsyncGener stream_trace = Trace("stream_messages", parent_id=cycle_trace.id) cycle_trace.add_child(stream_trace) - # Clean up orphaned empty tool uses - clean_orphaned_empty_tool_uses(agent.messages) - # Process messages with exponential backoff for throttling message: Message stop_reason: StopReason diff --git a/src/strands/event_loop/message_processor.py b/src/strands/event_loop/message_processor.py deleted file mode 100644 index 4e1a39dc7..000000000 --- a/src/strands/event_loop/message_processor.py +++ /dev/null @@ -1,105 +0,0 @@ -"""This module provides utilities for processing and manipulating conversation messages within the event loop. - -It includes functions for cleaning up orphaned tool uses, finding messages with specific content types, and truncating -large tool results to prevent context window overflow. -""" - -import logging -from typing import Dict, Set, Tuple - -from ..types.content import Messages - -logger = logging.getLogger(__name__) - - -def clean_orphaned_empty_tool_uses(messages: Messages) -> bool: - """Clean up orphaned empty tool uses in conversation messages. - - This function identifies and removes any toolUse entries with empty input that don't have a corresponding - toolResult. This prevents validation errors that occur when the model expects matching toolResult blocks for each - toolUse. - - The function applies fixes by either: - - 1. Replacing a message containing only an orphaned toolUse with a context message - 2. Removing the orphaned toolUse entry from a message with multiple content items - - Args: - messages: The conversation message history. - - Returns: - True if any fixes were applied, False otherwise. - """ - if not messages: - return False - - # Dictionary to track empty toolUse entries: {tool_id: (msg_index, content_index, tool_name)} - empty_tool_uses: Dict[str, Tuple[int, int, str]] = {} - - # Set to track toolResults that have been seen - tool_results: Set[str] = set() - - # Identify empty toolUse entries - for i, msg in enumerate(messages): - if msg.get("role") != "assistant": - continue - - for j, content in enumerate(msg.get("content", [])): - if isinstance(content, dict) and "toolUse" in content: - tool_use = content.get("toolUse", {}) - tool_id = tool_use.get("toolUseId") - tool_input = tool_use.get("input", {}) - tool_name = tool_use.get("name", "unknown tool") - - # Check if this is an empty toolUse - if tool_id and (not tool_input or tool_input == {}): - empty_tool_uses[tool_id] = (i, j, tool_name) - - # Identify toolResults - for msg in messages: - if msg.get("role") != "user": - continue - - for content in msg.get("content", []): - if isinstance(content, dict) and "toolResult" in content: - tool_result = content.get("toolResult", {}) - tool_id = tool_result.get("toolUseId") - if tool_id: - tool_results.add(tool_id) - - # Filter for orphaned empty toolUses (no corresponding toolResult) - orphaned_tool_uses = {tool_id: info for tool_id, info in empty_tool_uses.items() if tool_id not in tool_results} - - # Apply fixes in reverse order of occurrence (to avoid index shifting) - if not orphaned_tool_uses: - return False - - # Sort by message index and content index in reverse order - sorted_orphaned = sorted(orphaned_tool_uses.items(), key=lambda x: (x[1][0], x[1][1]), reverse=True) - - # Apply fixes - for tool_id, (msg_idx, content_idx, tool_name) in sorted_orphaned: - logger.debug( - "tool_name=<%s>, tool_id=<%s>, message_index=<%s>, content_index=<%s> " - "fixing orphaned empty tool use at message index", - tool_name, - tool_id, - msg_idx, - content_idx, - ) - try: - # Check if this is the sole content in the message - if len(messages[msg_idx]["content"]) == 1: - # Replace with a message indicating the attempted tool - messages[msg_idx]["content"] = [{"text": f"[Attempted to use {tool_name}, but operation was canceled]"}] - logger.debug("message_index=<%s> | replaced content with context message", msg_idx) - else: - # Simply remove the orphaned toolUse entry - messages[msg_idx]["content"].pop(content_idx) - logger.debug( - "message_index=<%s>, content_index=<%s> | removed content item from message", msg_idx, content_idx - ) - except Exception as e: - logger.warning("failed to fix orphaned tool use | %s", e) - - return True diff --git a/tests/strands/event_loop/test_message_processor.py b/tests/strands/event_loop/test_message_processor.py deleted file mode 100644 index fcf531dfd..000000000 --- a/tests/strands/event_loop/test_message_processor.py +++ /dev/null @@ -1,47 +0,0 @@ -import copy - -import pytest - -from strands.event_loop import message_processor - - -@pytest.mark.parametrize( - "messages,expected,expected_messages", - [ - # Orphaned toolUse with empty input, no toolResult - ( - [ - {"role": "assistant", "content": [{"toolUse": {"toolUseId": "1", "input": {}, "name": "foo"}}]}, - {"role": "user", "content": [{"toolResult": {"toolUseId": "2"}}]}, - ], - True, - [ - {"role": "assistant", "content": [{"text": "[Attempted to use foo, but operation was canceled]"}]}, - {"role": "user", "content": [{"toolResult": {"toolUseId": "2"}}]}, - ], - ), - # toolUse with input, has matching toolResult - ( - [ - {"role": "assistant", "content": [{"toolUse": {"toolUseId": "1", "input": {"a": 1}, "name": "foo"}}]}, - {"role": "user", "content": [{"toolResult": {"toolUseId": "1"}}]}, - ], - False, - [ - {"role": "assistant", "content": [{"toolUse": {"toolUseId": "1", "input": {"a": 1}, "name": "foo"}}]}, - {"role": "user", "content": [{"toolResult": {"toolUseId": "1"}}]}, - ], - ), - # No messages - ( - [], - False, - [], - ), - ], -) -def test_clean_orphaned_empty_tool_uses(messages, expected, expected_messages): - test_messages = copy.deepcopy(messages) - result = message_processor.clean_orphaned_empty_tool_uses(test_messages) - assert result == expected - assert test_messages == expected_messages