Skip to content

Commit 137e58a

Browse files
Merge pull request #745 from xomanova/main
fix(adk-middleware): filter empty text events to prevent frontend crash
2 parents de21d7d + 8eb64c5 commit 137e58a

File tree

2 files changed

+140
-0
lines changed

2 files changed

+140
-0
lines changed

integrations/adk-middleware/python/src/ag_ui_adk/event_translator.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,8 @@ async def _translate_text_content(
279279
return
280280

281281
combined_text = "".join(text_parts)
282+
if not combined_text:
283+
return
282284

283285
# Use proper ADK streaming detection (handle None values)
284286
is_partial = getattr(adk_event, 'partial', False)

integrations/adk-middleware/python/tests/test_event_translator_comprehensive.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1015,3 +1015,141 @@ async def test_partial_streaming_continuation(self, translator, mock_adk_event_w
10151015
# Should reset streaming state
10161016
assert translator._is_streaming is False
10171017
assert translator._streaming_message_id is None
1018+
1019+
@pytest.fixture
1020+
def mock_adk_event_empty_text(self):
1021+
"""Create a mock ADK event with empty text content."""
1022+
event = MagicMock(spec=ADKEvent)
1023+
event.id = "test_event_id"
1024+
event.author = "model"
1025+
1026+
# Mock content with empty text part
1027+
mock_content = MagicMock()
1028+
mock_part = MagicMock()
1029+
mock_part.text = ""
1030+
mock_content.parts = [mock_part]
1031+
event.content = mock_content
1032+
1033+
event.partial = False
1034+
event.turn_complete = True
1035+
event.is_final_response = False
1036+
return event
1037+
1038+
@pytest.mark.asyncio
1039+
async def test_empty_text_event_does_not_crash(self, translator, mock_adk_event_empty_text):
1040+
"""Test that empty text events are filtered and don't crash the frontend.
1041+
1042+
Previously, empty text content would cause AG-UI's TextMessageContentEvent
1043+
validation to fail. The fix filters out empty text before reaching validation.
1044+
"""
1045+
events = []
1046+
async for event in translator.translate(mock_adk_event_empty_text, "thread_1", "run_1"):
1047+
events.append(event)
1048+
1049+
# Empty text should be filtered out - no events emitted
1050+
assert len(events) == 0
1051+
content_events = [e for e in events if isinstance(e, TextMessageContentEvent)]
1052+
assert len(content_events) == 0
1053+
1054+
@pytest.mark.asyncio
1055+
async def test_whitespace_only_text_event_does_not_crash(self, translator):
1056+
"""Test that whitespace-only text events are also handled.
1057+
1058+
While the current fix checks for empty string, whitespace-only
1059+
content should also not cause issues.
1060+
"""
1061+
event = MagicMock(spec=ADKEvent)
1062+
event.id = "test_event_id"
1063+
event.author = "model"
1064+
1065+
mock_content = MagicMock()
1066+
mock_part = MagicMock()
1067+
mock_part.text = " " # Whitespace only
1068+
mock_content.parts = [mock_part]
1069+
event.content = mock_content
1070+
1071+
event.partial = False
1072+
event.turn_complete = True
1073+
event.is_final_response = False
1074+
1075+
events = []
1076+
async for event in translator.translate(event, "thread_1", "run_1"):
1077+
events.append(event)
1078+
1079+
# Whitespace is valid text content, should be emitted
1080+
content_events = [e for e in events if isinstance(e, TextMessageContentEvent)]
1081+
assert len(content_events) == 1
1082+
assert content_events[0].delta == " "
1083+
1084+
@pytest.mark.asyncio
1085+
async def test_multiple_empty_parts_filtered(self, translator, mock_adk_event):
1086+
"""Test that multiple empty text parts are all filtered."""
1087+
mock_content = MagicMock()
1088+
mock_part1 = MagicMock()
1089+
mock_part1.text = ""
1090+
mock_part2 = MagicMock()
1091+
mock_part2.text = ""
1092+
mock_part3 = MagicMock()
1093+
mock_part3.text = ""
1094+
mock_content.parts = [mock_part1, mock_part2, mock_part3]
1095+
mock_adk_event.content = mock_content
1096+
1097+
events = []
1098+
async for event in translator.translate(mock_adk_event, "thread_1", "run_1"):
1099+
events.append(event)
1100+
1101+
# All empty parts should result in no text events
1102+
assert len(events) == 0
1103+
1104+
@pytest.mark.asyncio
1105+
async def test_mixed_empty_and_valid_parts_filtering(self, translator, mock_adk_event):
1106+
"""Test that valid text parts are still emitted when mixed with empty parts."""
1107+
mock_content = MagicMock()
1108+
mock_part1 = MagicMock()
1109+
mock_part1.text = ""
1110+
mock_part2 = MagicMock()
1111+
mock_part2.text = "Valid content"
1112+
mock_part3 = MagicMock()
1113+
mock_part3.text = ""
1114+
mock_content.parts = [mock_part1, mock_part2, mock_part3]
1115+
mock_adk_event.content = mock_content
1116+
1117+
events = []
1118+
async for event in translator.translate(mock_adk_event, "thread_1", "run_1"):
1119+
events.append(event)
1120+
1121+
# Valid content should still be emitted
1122+
content_events = [e for e in events if isinstance(e, TextMessageContentEvent)]
1123+
assert len(content_events) == 1
1124+
assert content_events[0].delta == "Valid content"
1125+
1126+
@pytest.mark.asyncio
1127+
async def test_empty_combined_text_early_return(self, translator, mock_adk_event):
1128+
"""Test the early return when combined_text is empty.
1129+
1130+
This directly tests the fix at lines 281-283:
1131+
if not combined_text:
1132+
return
1133+
"""
1134+
# Create content where all parts have empty/None text
1135+
mock_content = MagicMock()
1136+
mock_part1 = MagicMock()
1137+
mock_part1.text = ""
1138+
mock_part2 = MagicMock()
1139+
mock_part2.text = None
1140+
mock_content.parts = [mock_part1, mock_part2]
1141+
mock_adk_event.content = mock_content
1142+
1143+
# Verify the translator doesn't start streaming for empty content
1144+
assert translator._is_streaming is False
1145+
1146+
events = []
1147+
async for event in translator.translate(mock_adk_event, "thread_1", "run_1"):
1148+
events.append(event)
1149+
1150+
# No streaming should have started
1151+
assert translator._is_streaming is False
1152+
assert len(events) == 0
1153+
# No TextMessageStartEvent should be created for empty content
1154+
start_events = [e for e in events if isinstance(e, TextMessageStartEvent)]
1155+
assert len(start_events) == 0

0 commit comments

Comments
 (0)