|
12 | 12 | from sentry_sdk.integrations.openai_agents.utils import _set_input_data, safe_serialize |
13 | 13 | from sentry_sdk.utils import parse_version |
14 | 14 |
|
| 15 | +from openai import AsyncOpenAI |
| 16 | +from agents.models.openai_responses import OpenAIResponsesModel |
| 17 | + |
| 18 | +from unittest import mock |
| 19 | +from unittest.mock import AsyncMock |
| 20 | + |
15 | 21 | import agents |
16 | 22 | from agents import ( |
17 | 23 | Agent, |
|
25 | 31 | ResponseOutputText, |
26 | 32 | ResponseFunctionToolCall, |
27 | 33 | ) |
| 34 | +from agents.tool import HostedMCPTool |
28 | 35 | from agents.exceptions import MaxTurnsExceeded, ModelBehaviorError |
29 | 36 | from agents.version import __version__ as OPENAI_AGENTS_VERSION |
30 | 37 |
|
| 38 | +from openai.types.responses import Response, ResponseUsage |
31 | 39 | from openai.types.responses.response_usage import ( |
32 | 40 | InputTokensDetails, |
33 | 41 | OutputTokensDetails, |
34 | 42 | ) |
35 | 43 |
|
36 | 44 | test_run_config = agents.RunConfig(tracing_disabled=True) |
37 | 45 |
|
| 46 | +EXAMPLE_RESPONSE = Response( |
| 47 | + id="chat-id", |
| 48 | + output=[ |
| 49 | + ResponseOutputMessage( |
| 50 | + id="message-id", |
| 51 | + content=[ |
| 52 | + ResponseOutputText( |
| 53 | + annotations=[], |
| 54 | + text="the model response", |
| 55 | + type="output_text", |
| 56 | + ), |
| 57 | + ], |
| 58 | + role="assistant", |
| 59 | + status="completed", |
| 60 | + type="message", |
| 61 | + ), |
| 62 | + ], |
| 63 | + parallel_tool_calls=False, |
| 64 | + tool_choice="none", |
| 65 | + tools=[], |
| 66 | + created_at=10000000, |
| 67 | + model="response-model-id", |
| 68 | + object="response", |
| 69 | + usage=ResponseUsage( |
| 70 | + input_tokens=20, |
| 71 | + input_tokens_details=InputTokensDetails( |
| 72 | + cached_tokens=5, |
| 73 | + ), |
| 74 | + output_tokens=10, |
| 75 | + output_tokens_details=OutputTokensDetails( |
| 76 | + reasoning_tokens=8, |
| 77 | + ), |
| 78 | + total_tokens=30, |
| 79 | + ), |
| 80 | +) |
| 81 | + |
38 | 82 |
|
39 | 83 | @pytest.fixture |
40 | 84 | def mock_usage(): |
@@ -137,6 +181,7 @@ async def test_agent_invocation_span( |
137 | 181 |
|
138 | 182 | (transaction,) = events |
139 | 183 | spans = transaction["spans"] |
| 184 | + print(spans) |
140 | 185 | invoke_agent_span, ai_client_span = spans |
141 | 186 |
|
142 | 187 | assert transaction["transaction"] == "test_agent workflow" |
@@ -695,6 +740,89 @@ def simple_test_tool(message: str) -> str: |
695 | 740 | assert ai_client_span2["data"]["gen_ai.usage.total_tokens"] == 25 |
696 | 741 |
|
697 | 742 |
|
| 743 | +@pytest.mark.asyncio |
| 744 | +async def test_hosted_mcp_tool_propagation_headers(sentry_init, test_agent): |
| 745 | + """ |
| 746 | + Test responses API is given trace propagation headers with HostedMCPTool. |
| 747 | + """ |
| 748 | + |
| 749 | + hosted_tool = HostedMCPTool( |
| 750 | + tool_config={ |
| 751 | + "type": "mcp", |
| 752 | + "server_label": "...", |
| 753 | + "server_url": "...", |
| 754 | + "require_approval": "never", |
| 755 | + "headers": { |
| 756 | + "baggage": "custom=data", |
| 757 | + }, |
| 758 | + }, |
| 759 | + on_approval_request=None, |
| 760 | + ) |
| 761 | + |
| 762 | + client = AsyncOpenAI(api_key="z") |
| 763 | + client.responses._post = AsyncMock(return_value=EXAMPLE_RESPONSE) |
| 764 | + |
| 765 | + model = OpenAIResponsesModel(model="gpt-4", openai_client=client) |
| 766 | + |
| 767 | + agent_with_tool = test_agent.clone( |
| 768 | + tools=[hosted_tool], |
| 769 | + model=model, |
| 770 | + ) |
| 771 | + |
| 772 | + sentry_init( |
| 773 | + integrations=[OpenAIAgentsIntegration()], |
| 774 | + traces_sample_rate=1.0, |
| 775 | + release="d08ebdb9309e1b004c6f52202de58a09c2268e42", |
| 776 | + ) |
| 777 | + |
| 778 | + with patch.object( |
| 779 | + model._client.responses, |
| 780 | + "create", |
| 781 | + wraps=model._client.responses.create, |
| 782 | + ) as create, mock.patch( |
| 783 | + "sentry_sdk.tracing_utils.Random.randrange", return_value=500000 |
| 784 | + ): |
| 785 | + with sentry_sdk.start_transaction( |
| 786 | + name="/interactions/other-dogs/new-dog", |
| 787 | + op="greeting.sniff", |
| 788 | + trace_id="01234567890123456789012345678901", |
| 789 | + ) as transaction: |
| 790 | + await agents.Runner.run( |
| 791 | + agent_with_tool, |
| 792 | + "Please use the simple test tool", |
| 793 | + run_config=test_run_config, |
| 794 | + ) |
| 795 | + |
| 796 | + ai_client_span = transaction._span_recorder.spans[-1] |
| 797 | + |
| 798 | + args, kwargs = create.call_args |
| 799 | + |
| 800 | + assert "tools" in kwargs |
| 801 | + assert len(kwargs["tools"]) == 1 |
| 802 | + hosted_mcp_tool = kwargs["tools"][0] |
| 803 | + |
| 804 | + assert hosted_mcp_tool["headers"][ |
| 805 | + "sentry-trace" |
| 806 | + ] == "{trace_id}-{parent_span_id}-{sampled}".format( |
| 807 | + trace_id=transaction.trace_id, |
| 808 | + parent_span_id=ai_client_span.span_id, |
| 809 | + sampled=1, |
| 810 | + ) |
| 811 | + |
| 812 | + expected_outgoing_baggage = ( |
| 813 | + "custom=data," |
| 814 | + "sentry-trace_id=01234567890123456789012345678901," |
| 815 | + "sentry-sample_rand=0.500000," |
| 816 | + "sentry-environment=production," |
| 817 | + "sentry-release=d08ebdb9309e1b004c6f52202de58a09c2268e42," |
| 818 | + "sentry-transaction=/interactions/other-dogs/new-dog," |
| 819 | + "sentry-sample_rate=1.0," |
| 820 | + "sentry-sampled=true" |
| 821 | + ) |
| 822 | + |
| 823 | + assert hosted_mcp_tool["headers"]["baggage"] == expected_outgoing_baggage |
| 824 | + |
| 825 | + |
698 | 826 | @pytest.mark.asyncio |
699 | 827 | async def test_model_behavior_error(sentry_init, capture_events, test_agent): |
700 | 828 | """ |
|
0 commit comments