|
21 | 21 | from datetime import datetime |
22 | 22 | from datetime import timezone |
23 | 23 | import json |
| 24 | +import logging |
24 | 25 | from unittest.mock import MagicMock |
25 | 26 |
|
26 | 27 | from google.adk.models import interactions_utils |
@@ -1248,6 +1249,214 @@ def test_image_delta_with_data(self): |
1248 | 1249 | assert result.content.parts[0].inline_data.data == b'image_bytes' |
1249 | 1250 | assert len(state.parts) == 1 |
1250 | 1251 |
|
| 1252 | + def test_thought_summary_delta(self): |
| 1253 | + """thought_summary delta becomes a thought part.""" |
| 1254 | + event = StepDelta( |
| 1255 | + event_type='step.delta', |
| 1256 | + index=0, |
| 1257 | + delta={ |
| 1258 | + 'type': 'thought_summary', |
| 1259 | + 'content': {'type': 'text', 'text': 'Let me think...'}, |
| 1260 | + }, |
| 1261 | + ) |
| 1262 | + state = interactions_utils._StreamState() |
| 1263 | + result = interactions_utils.convert_interaction_event_to_llm_response( |
| 1264 | + event, state, interaction_id='int_t' |
| 1265 | + ) |
| 1266 | + assert result is not None |
| 1267 | + assert result.partial is True |
| 1268 | + part = result.content.parts[0] |
| 1269 | + assert part.text == 'Let me think...' |
| 1270 | + assert part.thought is True |
| 1271 | + assert len(state.parts) == 1 |
| 1272 | + |
| 1273 | + def test_thought_signature_delta_attaches_to_last_thought(self): |
| 1274 | + """thought_signature mutates the last thought part and emits no event.""" |
| 1275 | + state = interactions_utils._StreamState() |
| 1276 | + interactions_utils.convert_interaction_event_to_llm_response( |
| 1277 | + StepDelta( |
| 1278 | + event_type='step.delta', |
| 1279 | + index=0, |
| 1280 | + delta={ |
| 1281 | + 'type': 'thought_summary', |
| 1282 | + 'content': {'type': 'text', 'text': 'reasoning'}, |
| 1283 | + }, |
| 1284 | + ), |
| 1285 | + state, |
| 1286 | + interaction_id='int_ts', |
| 1287 | + ) |
| 1288 | + sig_b64 = base64.b64encode(b'sig-bytes').decode('utf-8') |
| 1289 | + result = interactions_utils.convert_interaction_event_to_llm_response( |
| 1290 | + StepDelta( |
| 1291 | + event_type='step.delta', |
| 1292 | + index=0, |
| 1293 | + delta={'type': 'thought_signature', 'signature': sig_b64}, |
| 1294 | + ), |
| 1295 | + state, |
| 1296 | + interaction_id='int_ts', |
| 1297 | + ) |
| 1298 | + assert result is None |
| 1299 | + assert state.parts[-1].thought_signature == b'sig-bytes' |
| 1300 | + |
| 1301 | + def test_audio_delta_with_data(self): |
| 1302 | + """audio delta becomes an inline_data part via the shared media handler.""" |
| 1303 | + event = StepDelta( |
| 1304 | + event_type='step.delta', |
| 1305 | + index=0, |
| 1306 | + delta={ |
| 1307 | + 'type': 'audio', |
| 1308 | + 'data': base64.b64encode(b'audio_bytes').decode('utf-8'), |
| 1309 | + 'mime_type': 'audio/wav', |
| 1310 | + }, |
| 1311 | + ) |
| 1312 | + state = interactions_utils._StreamState() |
| 1313 | + result = interactions_utils.convert_interaction_event_to_llm_response( |
| 1314 | + event, state, interaction_id='int_a' |
| 1315 | + ) |
| 1316 | + assert result is not None |
| 1317 | + assert result.partial is True |
| 1318 | + assert result.content.parts[0].inline_data.data == b'audio_bytes' |
| 1319 | + assert result.content.parts[0].inline_data.mime_type == 'audio/wav' |
| 1320 | + assert len(state.parts) == 1 |
| 1321 | + |
| 1322 | + def test_code_execution_call_delta(self): |
| 1323 | + """code_execution_call delta becomes an executable_code part.""" |
| 1324 | + event = StepDelta( |
| 1325 | + event_type='step.delta', |
| 1326 | + index=0, |
| 1327 | + delta={ |
| 1328 | + 'type': 'code_execution_call', |
| 1329 | + 'arguments': {'code': 'print(1)', 'language': 'python'}, |
| 1330 | + }, |
| 1331 | + ) |
| 1332 | + state = interactions_utils._StreamState() |
| 1333 | + result = interactions_utils.convert_interaction_event_to_llm_response( |
| 1334 | + event, state, interaction_id='int_c' |
| 1335 | + ) |
| 1336 | + assert result is not None |
| 1337 | + part = result.content.parts[0] |
| 1338 | + assert part.executable_code.code == 'print(1)' |
| 1339 | + assert part.executable_code.language == types.Language.PYTHON |
| 1340 | + assert len(state.parts) == 1 |
| 1341 | + |
| 1342 | + def test_code_execution_result_delta(self): |
| 1343 | + """code_execution_result delta becomes a code_execution_result part.""" |
| 1344 | + event = StepDelta( |
| 1345 | + event_type='step.delta', |
| 1346 | + index=0, |
| 1347 | + delta={ |
| 1348 | + 'type': 'code_execution_result', |
| 1349 | + 'result': '1\n', |
| 1350 | + 'is_error': False, |
| 1351 | + }, |
| 1352 | + ) |
| 1353 | + state = interactions_utils._StreamState() |
| 1354 | + result = interactions_utils.convert_interaction_event_to_llm_response( |
| 1355 | + event, state, interaction_id='int_cr' |
| 1356 | + ) |
| 1357 | + assert result is not None |
| 1358 | + part = result.content.parts[0] |
| 1359 | + assert part.code_execution_result.output == '1\n' |
| 1360 | + assert part.code_execution_result.outcome == types.Outcome.OUTCOME_OK |
| 1361 | + assert len(state.parts) == 1 |
| 1362 | + |
| 1363 | + def test_code_execution_result_error_delta(self): |
| 1364 | + """code_execution_result with is_error maps to OUTCOME_FAILED.""" |
| 1365 | + event = StepDelta( |
| 1366 | + event_type='step.delta', |
| 1367 | + index=0, |
| 1368 | + delta={ |
| 1369 | + 'type': 'code_execution_result', |
| 1370 | + 'result': 'Traceback (most recent call last): ...', |
| 1371 | + 'is_error': True, |
| 1372 | + }, |
| 1373 | + ) |
| 1374 | + state = interactions_utils._StreamState() |
| 1375 | + result = interactions_utils.convert_interaction_event_to_llm_response( |
| 1376 | + event, state, interaction_id='int_cr_err' |
| 1377 | + ) |
| 1378 | + assert result is not None |
| 1379 | + part = result.content.parts[0] |
| 1380 | + assert ( |
| 1381 | + part.code_execution_result.output |
| 1382 | + == 'Traceback (most recent call last): ...' |
| 1383 | + ) |
| 1384 | + assert part.code_execution_result.outcome == types.Outcome.OUTCOME_FAILED |
| 1385 | + assert len(state.parts) == 1 |
| 1386 | + |
| 1387 | + def test_function_result_delta(self): |
| 1388 | + """function_result delta becomes a function_response part.""" |
| 1389 | + event = StepDelta( |
| 1390 | + event_type='step.delta', |
| 1391 | + index=0, |
| 1392 | + delta={ |
| 1393 | + 'type': 'function_result', |
| 1394 | + 'call_id': 'call_9', |
| 1395 | + 'result': {'temp': 72}, |
| 1396 | + }, |
| 1397 | + ) |
| 1398 | + state = interactions_utils._StreamState() |
| 1399 | + result = interactions_utils.convert_interaction_event_to_llm_response( |
| 1400 | + event, state, interaction_id='int_fr' |
| 1401 | + ) |
| 1402 | + assert result is not None |
| 1403 | + part = result.content.parts[0] |
| 1404 | + assert part.function_response.id == 'call_9' |
| 1405 | + assert part.function_response.response == {'temp': 72} |
| 1406 | + assert len(state.parts) == 1 |
| 1407 | + |
| 1408 | + def test_known_unhandled_delta_type_logs_debug_and_drops(self, caplog): |
| 1409 | + """A known but unhandled delta type logs at debug and emits no event.""" |
| 1410 | + # 'url_context_call' is a recognized genai delta variant that ADK does not |
| 1411 | + # handle yet, so it must fall through to the debug branch (not a warning). |
| 1412 | + event = StepDelta( |
| 1413 | + event_type='step.delta', |
| 1414 | + index=0, |
| 1415 | + delta={'type': 'url_context_call', 'arguments': {}}, |
| 1416 | + ) |
| 1417 | + state = interactions_utils._StreamState() |
| 1418 | + with caplog.at_level(logging.DEBUG, logger=interactions_utils.logger.name): |
| 1419 | + result = interactions_utils.convert_interaction_event_to_llm_response( |
| 1420 | + event, state, interaction_id='int_u' |
| 1421 | + ) |
| 1422 | + assert result is None |
| 1423 | + assert not state.parts |
| 1424 | + debug_records = [ |
| 1425 | + r |
| 1426 | + for r in caplog.records |
| 1427 | + if r.levelno == logging.DEBUG |
| 1428 | + and 'unhandled step delta type' in r.message |
| 1429 | + ] |
| 1430 | + assert len(debug_records) == 1 |
| 1431 | + assert not [r for r in caplog.records if r.levelno >= logging.WARNING] |
| 1432 | + |
| 1433 | + def test_unrecognized_delta_logs_raw_warning_and_drops(self, caplog): |
| 1434 | + """A truly-unrecognized delta logs a warning preserving its raw payload.""" |
| 1435 | + event = StepDelta( |
| 1436 | + event_type='step.delta', |
| 1437 | + index=0, |
| 1438 | + delta={'type': 'totally_made_up_xyz', 'foo': 'bar'}, |
| 1439 | + ) |
| 1440 | + state = interactions_utils._StreamState() |
| 1441 | + with caplog.at_level( |
| 1442 | + logging.WARNING, logger=interactions_utils.logger.name |
| 1443 | + ): |
| 1444 | + result = interactions_utils.convert_interaction_event_to_llm_response( |
| 1445 | + event, state, interaction_id='int_u2' |
| 1446 | + ) |
| 1447 | + assert result is None |
| 1448 | + assert not state.parts |
| 1449 | + warnings = [ |
| 1450 | + r |
| 1451 | + for r in caplog.records |
| 1452 | + if r.levelno == logging.WARNING |
| 1453 | + and 'unrecognized step delta' in r.message |
| 1454 | + ] |
| 1455 | + assert len(warnings) == 1 |
| 1456 | + assert 'foo' in warnings[0].message |
| 1457 | + # The full raw payload (not just delta.type='UNKNOWN') is preserved. |
| 1458 | + assert warnings[0].args == {'type': 'totally_made_up_xyz', 'foo': 'bar'} |
| 1459 | + |
1251 | 1460 | def test_unknown_event_type_returns_none(self): |
1252 | 1461 | """Test that unknown event types return None.""" |
1253 | 1462 | event = MagicMock() |
|
0 commit comments