Skip to content

Commit 049ccdc

Browse files
authored
fix: dont save empty text messages (breaks Converse API) (#185)
1 parent 92272e7 commit 049ccdc

File tree

2 files changed

+113
-3
lines changed

2 files changed

+113
-3
lines changed

src/bedrock_agentcore/memory/integrations/strands/bedrock_converter.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@
1414
class AgentCoreMemoryConverter:
1515
"""Handles conversion between Strands and Bedrock AgentCore Memory formats."""
1616

17+
@staticmethod
18+
def _filter_empty_text(message: dict) -> dict:
19+
"""The Bedrock Converse API can't take empty text as input. So we need to filter out empty text."""
20+
content = message.get("content", [])
21+
filtered_content = [item for item in content if "text" not in item or item.get("text", "").strip() != ""]
22+
return {**message, "content": filtered_content}
23+
1724
@staticmethod
1825
def message_to_payload(session_message: SessionMessage) -> list[Tuple[str, str]]:
1926
"""Convert a SessionMessage to Bedrock AgentCore Memory message format.
@@ -23,9 +30,15 @@ def message_to_payload(session_message: SessionMessage) -> list[Tuple[str, str]]
2330
2431
Returns:
2532
list[Tuple[str, str]]: list of (text, role) tuples for Bedrock AgentCore Memory.
33+
Returns empty list if message has no content after filtering.
2634
"""
35+
filtered_message = AgentCoreMemoryConverter._filter_empty_text(session_message.message)
36+
if not filtered_message.get("content"):
37+
logger.debug("Skipping message with no content after filtering empty text")
38+
return []
2739
session_dict = session_message.to_dict()
28-
return [(json.dumps(session_dict), session_message.message["role"])]
40+
session_dict["message"] = filtered_message
41+
return [(json.dumps(session_dict), filtered_message["role"])]
2942

3043
@staticmethod
3144
def events_to_messages(events: list[dict[str, Any]]) -> list[SessionMessage]:
@@ -61,13 +74,19 @@ def events_to_messages(events: list[dict[str, Any]]) -> list[SessionMessage]:
6174
for payload_item in event.get("payload", []):
6275
if "conversational" in payload_item:
6376
conv = payload_item["conversational"]
64-
messages.append(SessionMessage.from_dict(json.loads(conv["content"]["text"])))
77+
session_msg = SessionMessage.from_dict(json.loads(conv["content"]["text"]))
78+
session_msg.message = AgentCoreMemoryConverter._filter_empty_text(session_msg.message)
79+
if session_msg.message.get("content"):
80+
messages.append(session_msg)
6581
elif "blob" in payload_item:
6682
try:
6783
blob_data = json.loads(payload_item["blob"])
6884
if isinstance(blob_data, (tuple, list)) and len(blob_data) == 2:
6985
try:
70-
messages.append(SessionMessage.from_dict(json.loads(blob_data[0])))
86+
session_msg = SessionMessage.from_dict(json.loads(blob_data[0]))
87+
session_msg.message = AgentCoreMemoryConverter._filter_empty_text(session_msg.message)
88+
if session_msg.message.get("content"):
89+
messages.append(session_msg)
7190
except (json.JSONDecodeError, ValueError):
7291
logger.error("This is not a SessionMessage but just a blob message. Ignoring")
7392
except (json.JSONDecodeError, ValueError):

tests/bedrock_agentcore/memory/integrations/strands/test_bedrock_converter.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,94 @@ def test_exceeds_conversational_limit_true(self):
9696
message = (long_text, long_text)
9797
result = AgentCoreMemoryConverter.exceeds_conversational_limit(message)
9898
assert result is True
99+
100+
def test_filter_empty_text_removes_empty_string(self):
101+
"""Test filtering removes empty text items."""
102+
message = {"role": "user", "content": [{"text": ""}, {"text": "hello"}]}
103+
result = AgentCoreMemoryConverter._filter_empty_text(message)
104+
assert len(result["content"]) == 1
105+
assert result["content"][0]["text"] == "hello"
106+
107+
def test_filter_empty_text_removes_whitespace_only(self):
108+
"""Test filtering removes whitespace-only text items."""
109+
message = {"role": "user", "content": [{"text": " "}, {"text": "hello"}]}
110+
result = AgentCoreMemoryConverter._filter_empty_text(message)
111+
assert len(result["content"]) == 1
112+
assert result["content"][0]["text"] == "hello"
113+
114+
def test_filter_empty_text_keeps_non_text_items(self):
115+
"""Test filtering keeps non-text items like toolUse."""
116+
message = {"role": "user", "content": [{"text": ""}, {"toolUse": {"name": "test"}}]}
117+
result = AgentCoreMemoryConverter._filter_empty_text(message)
118+
assert len(result["content"]) == 1
119+
assert "toolUse" in result["content"][0]
120+
121+
def test_filter_empty_text_all_empty_returns_empty_content(self):
122+
"""Test filtering all empty text returns empty content array."""
123+
message = {"role": "user", "content": [{"text": ""}]}
124+
result = AgentCoreMemoryConverter._filter_empty_text(message)
125+
assert result["content"] == []
126+
127+
def test_message_to_payload_skips_all_empty_text(self):
128+
"""Test message_to_payload returns empty list when all text is empty."""
129+
message = SessionMessage(
130+
message_id=1, message={"role": "user", "content": [{"text": ""}]}, created_at="2023-01-01T00:00:00Z"
131+
)
132+
result = AgentCoreMemoryConverter.message_to_payload(message)
133+
assert result == []
134+
135+
def test_message_to_payload_filters_empty_text_items(self):
136+
"""Test message_to_payload filters out empty text but keeps valid content."""
137+
message = SessionMessage(
138+
message_id=1,
139+
message={"role": "user", "content": [{"text": ""}, {"text": "hello"}]},
140+
created_at="2023-01-01T00:00:00Z",
141+
)
142+
result = AgentCoreMemoryConverter.message_to_payload(message)
143+
assert len(result) == 1
144+
parsed = json.loads(result[0][0])
145+
assert len(parsed["message"]["content"]) == 1
146+
assert parsed["message"]["content"][0]["text"] == "hello"
147+
148+
def test_events_to_messages_filters_empty_text_conversational(self):
149+
"""Test events_to_messages filters empty text from conversational payloads."""
150+
msg_with_empty = SessionMessage(
151+
message_id=1,
152+
message={"role": "user", "content": [{"text": ""}, {"text": "hello"}]},
153+
created_at="2023-01-01T00:00:00Z",
154+
)
155+
events = [
156+
{
157+
"payload": [
158+
{"conversational": {"content": {"text": json.dumps(msg_with_empty.to_dict())}, "role": "USER"}}
159+
]
160+
}
161+
]
162+
result = AgentCoreMemoryConverter.events_to_messages(events)
163+
assert len(result) == 1
164+
assert len(result[0].message["content"]) == 1
165+
assert result[0].message["content"][0]["text"] == "hello"
166+
167+
def test_events_to_messages_drops_all_empty_conversational(self):
168+
"""Test events_to_messages drops messages with only empty text."""
169+
empty_msg = SessionMessage(
170+
message_id=1, message={"role": "user", "content": [{"text": ""}]}, created_at="2023-01-01T00:00:00Z"
171+
)
172+
events = [
173+
{"payload": [{"conversational": {"content": {"text": json.dumps(empty_msg.to_dict())}, "role": "USER"}}]}
174+
]
175+
result = AgentCoreMemoryConverter.events_to_messages(events)
176+
assert len(result) == 0
177+
178+
def test_events_to_messages_filters_empty_text_blob(self):
179+
"""Test events_to_messages filters empty text from blob payloads."""
180+
msg_with_empty = SessionMessage(
181+
message_id=1,
182+
message={"role": "user", "content": [{"text": ""}, {"text": "hello"}]},
183+
created_at="2023-01-01T00:00:00Z",
184+
)
185+
events = [{"payload": [{"blob": json.dumps([json.dumps(msg_with_empty.to_dict()), "user"])}]}]
186+
result = AgentCoreMemoryConverter.events_to_messages(events)
187+
assert len(result) == 1
188+
assert len(result[0].message["content"]) == 1
189+
assert result[0].message["content"][0]["text"] == "hello"

0 commit comments

Comments
 (0)