|
13 | 13 | import pytest |
14 | 14 | from agent_framework import ( |
15 | 15 | Agent, |
| 16 | + Annotation, |
16 | 17 | ChatOptions, |
17 | 18 | ChatResponse, |
18 | 19 | ChatResponseUpdate, |
@@ -3574,9 +3575,17 @@ def _make_mcp_call_done_event(output: str) -> MagicMock: |
3574 | 3575 | return event |
3575 | 3576 |
|
3576 | 3577 |
|
3577 | | -def _make_azure_ai_search_output_event(output: Any, *, event_type: str = "response.output_item.done") -> MagicMock: |
| 3578 | +def _make_azure_ai_search_output_event( |
| 3579 | + output: Any, |
| 3580 | + *, |
| 3581 | + event_type: str = "response.output_item.done", |
| 3582 | + top_level_output: bool = False, |
| 3583 | +) -> MagicMock: |
3578 | 3584 | event = MagicMock() |
3579 | 3585 | event.type = event_type |
| 3586 | + if top_level_output: |
| 3587 | + event.output = output |
| 3588 | + return event |
3580 | 3589 | event.item = MagicMock() |
3581 | 3590 | event.item.type = "azure_ai_search_call_output" |
3582 | 3591 | event.item.output = output |
@@ -3675,6 +3684,81 @@ def test_streaming_azure_ai_search_output_does_not_overwrite_existing_get_url() |
3675 | 3684 | assert annotation["additional_properties"]["get_url"] == existing_get_url |
3676 | 3685 |
|
3677 | 3686 |
|
| 3687 | +def test_streaming_azure_ai_search_output_normalizes_non_dict_additional_properties() -> None: |
| 3688 | + """Existing non-dict additional_properties should be normalized before enriching get_url.""" |
| 3689 | + client = OpenAIChatClient(model="test-model", api_key="test-key") |
| 3690 | + chat_options = ChatOptions() |
| 3691 | + function_call_ids: dict[int, tuple[str, str]] = {} |
| 3692 | + get_url = "https://example.search.windows.net/indexes/my-index/docs/doc-123?api-version=2024-07-01" |
| 3693 | + |
| 3694 | + citation_update = client._parse_chunk_from_openai( |
| 3695 | + _make_url_citation_event(title="doc_0"), |
| 3696 | + chat_options, |
| 3697 | + function_call_ids, |
| 3698 | + ) |
| 3699 | + citation_update.contents[0].annotations[0]["additional_properties"] = None |
| 3700 | + search_update = client._parse_chunk_from_openai( |
| 3701 | + _make_azure_ai_search_output_event(json.dumps({"get_urls": [get_url]})), |
| 3702 | + chat_options, |
| 3703 | + function_call_ids, |
| 3704 | + ) |
| 3705 | + |
| 3706 | + response = client._finalize_response_updates([citation_update, search_update]) |
| 3707 | + |
| 3708 | + annotation = response.messages[0].contents[0].annotations[0] |
| 3709 | + assert annotation["additional_properties"] == {"get_url": get_url} |
| 3710 | + |
| 3711 | + |
| 3712 | +def test_streaming_azure_ai_search_output_does_not_create_additional_properties_for_unusable_citation() -> None: |
| 3713 | + """Unenrichable Azure AI Search citations should keep their original annotation shape.""" |
| 3714 | + update = ChatResponseUpdate( |
| 3715 | + contents=[ |
| 3716 | + Content.from_text( |
| 3717 | + text="hello", |
| 3718 | + annotations=[Annotation(type="citation", title="source_0", url="https://example.invalid")], |
| 3719 | + ) |
| 3720 | + ], |
| 3721 | + raw_representation=_make_azure_ai_search_output_event( |
| 3722 | + json.dumps({"get_urls": ["https://example.search.windows.net/indexes/my-index/docs/doc-0"]}) |
| 3723 | + ), |
| 3724 | + ) |
| 3725 | + |
| 3726 | + RawOpenAIChatClient._enrich_streamed_azure_ai_search_citations([update]) |
| 3727 | + |
| 3728 | + annotation = update.contents[0].annotations[0] |
| 3729 | + assert annotation.get("additional_properties") is None |
| 3730 | + |
| 3731 | + |
| 3732 | +def test_extract_azure_ai_search_get_urls_accepts_dedicated_output_event() -> None: |
| 3733 | + """Dedicated response.azure_ai_search_call_output.* events should yield get_urls too.""" |
| 3734 | + get_url = "https://example.search.windows.net/indexes/my-index/docs/doc-123?api-version=2024-07-01" |
| 3735 | + event = _make_azure_ai_search_output_event( |
| 3736 | + json.dumps({"get_urls": [get_url]}), |
| 3737 | + event_type="response.azure_ai_search_call_output.done", |
| 3738 | + top_level_output=True, |
| 3739 | + ) |
| 3740 | + |
| 3741 | + assert RawOpenAIChatClient._extract_azure_ai_search_get_urls(event) == [get_url] |
| 3742 | + |
| 3743 | + |
| 3744 | +def test_parse_chunk_from_openai_ignores_dedicated_azure_ai_search_events() -> None: |
| 3745 | + """Dedicated Azure AI Search events should be treated as intentional no-op updates.""" |
| 3746 | + client = OpenAIChatClient(model="test-model", api_key="test-key") |
| 3747 | + chat_options = ChatOptions() |
| 3748 | + function_call_ids: dict[int, tuple[str, str]] = {} |
| 3749 | + event = _make_azure_ai_search_output_event( |
| 3750 | + json.dumps({"get_urls": ["https://example.search.windows.net/indexes/my-index/docs/doc-0"]}), |
| 3751 | + event_type="response.azure_ai_search_call_output.done", |
| 3752 | + top_level_output=True, |
| 3753 | + ) |
| 3754 | + |
| 3755 | + with patch("agent_framework_openai._chat_client.logger.debug") as mock_debug: |
| 3756 | + update = client._parse_chunk_from_openai(event, chat_options, function_call_ids) |
| 3757 | + |
| 3758 | + assert update.contents == [] |
| 3759 | + mock_debug.assert_not_called() |
| 3760 | + |
| 3761 | + |
3678 | 3762 | @pytest.mark.parametrize( |
3679 | 3763 | ("title", "output"), |
3680 | 3764 | [ |
|
0 commit comments