Skip to content

Commit f3529e9

Browse files
vietnamesekidGWeale
authored andcommitted
feat: add error handling to VertexAiMemoryBankService.search_memory
Merge #5707 ## What's the problem? `search_memory` iterates over memory entries from the Vertex AI API but had no error handling. If one entry was malformed or the iterator threw mid-stream, the whole call would blow up and drop everything collected so far. ## What changed? Added two layers of defense inside the iterator loop: - **Per-entry** (`AttributeError`): skips bad entries and logs a warning so the rest still come through - **Iterator-level** (`Exception`): if the stream fails partway, we return whatever we already collected instead of raising Also added guards for a few edge cases I noticed while looking at this: - `memory is None` - `fact` is empty/`None` - `update_time is None` (just sets `timestamp=None` rather than crashing) ## Test plan - [x] `test_search_memory` — happy path unchanged - [x] `test_search_memory_empty_results` — empty results unchanged - [x] `test_search_memory_skips_entry_with_none_memory` - [x] `test_search_memory_skips_entry_with_empty_fact` - [x] `test_search_memory_handles_missing_update_time` - [x] `test_search_memory_skips_malformed_entry` - [x] `test_search_memory_returns_partial_results_on_iterator_error` Co-authored-by: George Weale <gweale@google.com> COPYBARA_INTEGRATE_REVIEW=#5707 from vietnamesekid:feat/vertex-memory-bank-error-handling 0b53afb PiperOrigin-RevId: 936780770
1 parent 5a0b4af commit f3529e9

2 files changed

Lines changed: 136 additions & 12 deletions

File tree

src/google/adk/memory/vertex_ai_memory_bank_service.py

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -541,18 +541,36 @@ async def search_memory(self, *, app_name: str, user_id: str, query: str):
541541
logger.info('Search memory response received.')
542542

543543
memory_events: list[MemoryEntry] = []
544-
async for retrieved_memory in retrieved_memories_iterator:
545-
# TODO: add more complex error handling
546-
logger.debug('Retrieved memory: %s', retrieved_memory)
547-
memory_events.append(
548-
MemoryEntry(
549-
author='user',
550-
content=types.Content(
551-
parts=[types.Part(text=retrieved_memory.memory.fact)],
552-
role='user',
553-
),
554-
timestamp=retrieved_memory.memory.update_time.isoformat(),
544+
try:
545+
async for retrieved_memory in retrieved_memories_iterator:
546+
try:
547+
memory = retrieved_memory.memory
548+
if memory is None:
549+
logger.warning('Skipping memory entry with missing memory object.')
550+
continue
551+
fact = memory.fact
552+
if not fact:
553+
logger.warning('Skipping memory entry with empty or missing fact.')
554+
continue
555+
update_time = memory.update_time
556+
memory_events.append(
557+
MemoryEntry(
558+
author='user',
559+
content=types.Content(
560+
parts=[types.Part(text=fact)],
561+
role='user',
562+
),
563+
timestamp=update_time.isoformat() if update_time else None,
564+
)
565+
)
566+
except AttributeError:
567+
logger.warning(
568+
'Skipping malformed memory entry: %s', retrieved_memory
555569
)
570+
except Exception:
571+
logger.exception(
572+
'Error while iterating memory results. Returning %d partial results.',
573+
len(memory_events),
556574
)
557575
return SearchMemoryResponse(memories=memory_events)
558576

tests/unittests/memory/test_vertex_ai_memory_bank_service.py

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1009,7 +1009,6 @@ async def test_search_memory_empty_results(mock_vertexai_client):
10091009
assert len(result.memories) == 0
10101010

10111011

1012-
@pytest.mark.asyncio
10131012
async def test_search_memory_uses_async_client_path():
10141013
sync_client = mock.MagicMock()
10151014
sync_client.agent_engines.memories.retrieve.side_effect = AssertionError(
@@ -1039,3 +1038,110 @@ async def test_search_memory_uses_async_client_path():
10391038
similarity_search_params={'search_query': 'query'},
10401039
)
10411040
sync_client.agent_engines.memories.retrieve.assert_not_called()
1041+
1042+
1043+
@pytest.mark.asyncio
1044+
async def test_search_memory_skips_entry_with_none_memory(mock_vertexai_client):
1045+
bad_entry = mock.MagicMock()
1046+
bad_entry.memory = None
1047+
1048+
good_entry = mock.MagicMock()
1049+
good_entry.memory.fact = 'good fact'
1050+
good_entry.memory.update_time = datetime.datetime(2024, 1, 1)
1051+
1052+
mock_vertexai_client.agent_engines.memories.retrieve.return_value = (
1053+
_AsyncListIterator([bad_entry, good_entry])
1054+
)
1055+
memory_service = mock_vertex_ai_memory_bank_service()
1056+
1057+
result = await memory_service.search_memory(
1058+
app_name=MOCK_APP_NAME, user_id=MOCK_USER_ID, query='query'
1059+
)
1060+
1061+
assert len(result.memories) == 1
1062+
assert result.memories[0].content.parts[0].text == 'good fact'
1063+
1064+
1065+
@pytest.mark.asyncio
1066+
async def test_search_memory_skips_entry_with_empty_fact(mock_vertexai_client):
1067+
for empty_fact in [None, '']:
1068+
bad_entry = mock.MagicMock()
1069+
bad_entry.memory.fact = empty_fact
1070+
bad_entry.memory.update_time = datetime.datetime(2024, 1, 1)
1071+
1072+
mock_vertexai_client.agent_engines.memories.retrieve.return_value = (
1073+
_AsyncListIterator([bad_entry])
1074+
)
1075+
memory_service = mock_vertex_ai_memory_bank_service()
1076+
1077+
result = await memory_service.search_memory(
1078+
app_name=MOCK_APP_NAME, user_id=MOCK_USER_ID, query='query'
1079+
)
1080+
1081+
assert len(result.memories) == 0
1082+
1083+
1084+
@pytest.mark.asyncio
1085+
async def test_search_memory_handles_missing_update_time(mock_vertexai_client):
1086+
entry = mock.MagicMock()
1087+
entry.memory.fact = 'some fact'
1088+
entry.memory.update_time = None
1089+
1090+
mock_vertexai_client.agent_engines.memories.retrieve.return_value = (
1091+
_AsyncListIterator([entry])
1092+
)
1093+
memory_service = mock_vertex_ai_memory_bank_service()
1094+
1095+
result = await memory_service.search_memory(
1096+
app_name=MOCK_APP_NAME, user_id=MOCK_USER_ID, query='query'
1097+
)
1098+
1099+
assert len(result.memories) == 1
1100+
assert result.memories[0].content.parts[0].text == 'some fact'
1101+
assert result.memories[0].timestamp is None
1102+
1103+
1104+
@pytest.mark.asyncio
1105+
async def test_search_memory_skips_malformed_entry(mock_vertexai_client):
1106+
malformed = mock.MagicMock(spec=[]) # no attributes → AttributeError
1107+
1108+
good_entry = mock.MagicMock()
1109+
good_entry.memory.fact = 'good fact'
1110+
good_entry.memory.update_time = datetime.datetime(2024, 1, 1)
1111+
1112+
mock_vertexai_client.agent_engines.memories.retrieve.return_value = (
1113+
_AsyncListIterator([malformed, good_entry])
1114+
)
1115+
memory_service = mock_vertex_ai_memory_bank_service()
1116+
1117+
result = await memory_service.search_memory(
1118+
app_name=MOCK_APP_NAME, user_id=MOCK_USER_ID, query='query'
1119+
)
1120+
1121+
assert len(result.memories) == 1
1122+
assert result.memories[0].content.parts[0].text == 'good fact'
1123+
1124+
1125+
@pytest.mark.asyncio
1126+
async def test_search_memory_returns_partial_results_on_iterator_error(
1127+
mock_vertexai_client,
1128+
):
1129+
good_entry = mock.MagicMock()
1130+
good_entry.memory.fact = 'good fact'
1131+
good_entry.memory.update_time = datetime.datetime(2024, 1, 1)
1132+
1133+
async def failing_async_iterator():
1134+
yield good_entry
1135+
raise RuntimeError('API stream error')
1136+
1137+
mock_vertexai_client.agent_engines.memories.retrieve.return_value = (
1138+
failing_async_iterator()
1139+
)
1140+
memory_service = mock_vertex_ai_memory_bank_service()
1141+
1142+
result = await memory_service.search_memory(
1143+
app_name=MOCK_APP_NAME, user_id=MOCK_USER_ID, query='query'
1144+
)
1145+
1146+
assert len(result.memories) == 1
1147+
assert result.memories[0].content.parts[0].text == 'good fact'

0 commit comments

Comments
 (0)