|
13 | 13 | import pytest |
14 | 14 | from agent_framework import ( |
15 | 15 | Agent, |
| 16 | + Annotation, |
16 | 17 | ChatOptions, |
17 | 18 | ChatResponse, |
18 | 19 | ChatResponseUpdate, |
@@ -3618,9 +3619,17 @@ def _make_mcp_call_done_event(output: str) -> MagicMock: |
3618 | 3619 | return event |
3619 | 3620 |
|
3620 | 3621 |
|
3621 | | -def _make_azure_ai_search_output_event(output: Any, *, event_type: str = "response.output_item.done") -> MagicMock: |
| 3622 | +def _make_azure_ai_search_output_event( |
| 3623 | + output: Any, |
| 3624 | + *, |
| 3625 | + event_type: str = "response.output_item.done", |
| 3626 | + top_level_output: bool = False, |
| 3627 | +) -> MagicMock: |
3622 | 3628 | event = MagicMock() |
3623 | 3629 | event.type = event_type |
| 3630 | + if top_level_output: |
| 3631 | + event.output = output |
| 3632 | + return event |
3624 | 3633 | event.item = MagicMock() |
3625 | 3634 | event.item.type = "azure_ai_search_call_output" |
3626 | 3635 | event.item.output = output |
@@ -3719,6 +3728,81 @@ def test_streaming_azure_ai_search_output_does_not_overwrite_existing_get_url() |
3719 | 3728 | assert annotation["additional_properties"]["get_url"] == existing_get_url |
3720 | 3729 |
|
3721 | 3730 |
|
| 3731 | +def test_streaming_azure_ai_search_output_normalizes_non_dict_additional_properties() -> None: |
| 3732 | + """Existing non-dict additional_properties should be normalized before enriching get_url.""" |
| 3733 | + client = OpenAIChatClient(model="test-model", api_key="test-key") |
| 3734 | + chat_options = ChatOptions() |
| 3735 | + function_call_ids: dict[int, tuple[str, str]] = {} |
| 3736 | + get_url = "https://example.search.windows.net/indexes/my-index/docs/doc-123?api-version=2024-07-01" |
| 3737 | + |
| 3738 | + citation_update = client._parse_chunk_from_openai( |
| 3739 | + _make_url_citation_event(title="doc_0"), |
| 3740 | + chat_options, |
| 3741 | + function_call_ids, |
| 3742 | + ) |
| 3743 | + citation_update.contents[0].annotations[0]["additional_properties"] = None |
| 3744 | + search_update = client._parse_chunk_from_openai( |
| 3745 | + _make_azure_ai_search_output_event(json.dumps({"get_urls": [get_url]})), |
| 3746 | + chat_options, |
| 3747 | + function_call_ids, |
| 3748 | + ) |
| 3749 | + |
| 3750 | + response = client._finalize_response_updates([citation_update, search_update]) |
| 3751 | + |
| 3752 | + annotation = response.messages[0].contents[0].annotations[0] |
| 3753 | + assert annotation["additional_properties"] == {"get_url": get_url} |
| 3754 | + |
| 3755 | + |
| 3756 | +def test_streaming_azure_ai_search_output_does_not_create_additional_properties_for_unusable_citation() -> None: |
| 3757 | + """Unenrichable Azure AI Search citations should keep their original annotation shape.""" |
| 3758 | + update = ChatResponseUpdate( |
| 3759 | + contents=[ |
| 3760 | + Content.from_text( |
| 3761 | + text="hello", |
| 3762 | + annotations=[Annotation(type="citation", title="source_0", url="https://example.invalid")], |
| 3763 | + ) |
| 3764 | + ], |
| 3765 | + raw_representation=_make_azure_ai_search_output_event( |
| 3766 | + json.dumps({"get_urls": ["https://example.search.windows.net/indexes/my-index/docs/doc-0"]}) |
| 3767 | + ), |
| 3768 | + ) |
| 3769 | + |
| 3770 | + RawOpenAIChatClient._enrich_streamed_azure_ai_search_citations([update]) |
| 3771 | + |
| 3772 | + annotation = update.contents[0].annotations[0] |
| 3773 | + assert annotation.get("additional_properties") is None |
| 3774 | + |
| 3775 | + |
| 3776 | +def test_extract_azure_ai_search_get_urls_accepts_dedicated_output_event() -> None: |
| 3777 | + """Dedicated response.azure_ai_search_call_output.* events should yield get_urls too.""" |
| 3778 | + get_url = "https://example.search.windows.net/indexes/my-index/docs/doc-123?api-version=2024-07-01" |
| 3779 | + event = _make_azure_ai_search_output_event( |
| 3780 | + json.dumps({"get_urls": [get_url]}), |
| 3781 | + event_type="response.azure_ai_search_call_output.done", |
| 3782 | + top_level_output=True, |
| 3783 | + ) |
| 3784 | + |
| 3785 | + assert RawOpenAIChatClient._extract_azure_ai_search_get_urls(event) == [get_url] |
| 3786 | + |
| 3787 | + |
| 3788 | +def test_parse_chunk_from_openai_ignores_dedicated_azure_ai_search_events() -> None: |
| 3789 | + """Dedicated Azure AI Search events should be treated as intentional no-op updates.""" |
| 3790 | + client = OpenAIChatClient(model="test-model", api_key="test-key") |
| 3791 | + chat_options = ChatOptions() |
| 3792 | + function_call_ids: dict[int, tuple[str, str]] = {} |
| 3793 | + event = _make_azure_ai_search_output_event( |
| 3794 | + json.dumps({"get_urls": ["https://example.search.windows.net/indexes/my-index/docs/doc-0"]}), |
| 3795 | + event_type="response.azure_ai_search_call_output.done", |
| 3796 | + top_level_output=True, |
| 3797 | + ) |
| 3798 | + |
| 3799 | + with patch("agent_framework_openai._chat_client.logger.debug") as mock_debug: |
| 3800 | + update = client._parse_chunk_from_openai(event, chat_options, function_call_ids) |
| 3801 | + |
| 3802 | + assert update.contents == [] |
| 3803 | + mock_debug.assert_not_called() |
| 3804 | + |
| 3805 | + |
3722 | 3806 | @pytest.mark.parametrize( |
3723 | 3807 | ("title", "output"), |
3724 | 3808 | [ |
|
0 commit comments