|
55 | 55 | from models.common.query import Attachment as QueryAttachment |
56 | 56 | from models.common.responses.contexts import ResponseGeneratorContext |
57 | 57 | from models.common.responses.responses_api_params import ResponsesApiParams |
58 | | -from models.common.turn_summary import RAGContext, TurnSummary |
| 58 | +from models.common.turn_summary import RAGContext, ToolCallSummary, TurnSummary |
59 | 59 | from utils.agents.query import AgentFinishReason |
60 | 60 | from utils.agents.streaming import ( |
61 | 61 | DEFAULT_REFUSAL_RESPONSE, |
@@ -884,6 +884,80 @@ async def inner() -> AsyncIterator[str]: |
884 | 884 | assert SpanEvents.TURN_PERSISTED in event_names |
885 | 885 | assert SpanEvents.LLM_RESPONSE_COMPLETED in event_names |
886 | 886 |
|
| 887 | + @pytest.mark.asyncio |
| 888 | + async def test_sets_tool_call_span_attributes( |
| 889 | + self, |
| 890 | + mocker: MockerFixture, |
| 891 | + make_generator_context: Callable[..., ResponseGeneratorContext], |
| 892 | + responses_params: ResponsesApiParams, |
| 893 | + otel: tuple[Any, InMemorySpanExporter], |
| 894 | + ) -> None: |
| 895 | + """Test that tool call OTEL attributes are emitted on the root span.""" |
| 896 | + tracer, exporter = otel |
| 897 | + context = make_generator_context() |
| 898 | + turn_summary = TurnSummary() |
| 899 | + turn_summary.token_usage = TokenCounter(input_tokens=10, output_tokens=5) |
| 900 | + turn_summary.llm_response = "Result" |
| 901 | + turn_summary.tool_calls = [ |
| 902 | + ToolCallSummary(id="tc-1", name="web_search", type="web_search_call"), |
| 903 | + ToolCallSummary(id="tc-2", name="file_search", type="file_search_call"), |
| 904 | + ] |
| 905 | + background_tasks: list[asyncio.Task[None]] = [] |
| 906 | + root_span = tracer.start_span("streaming_query.handle_request") |
| 907 | + |
| 908 | + async def inner() -> AsyncIterator[str]: |
| 909 | + yield serialize_event( |
| 910 | + TokenStreamPayload.create(chunk_id=0, token="Hi"), |
| 911 | + MEDIA_TYPE_JSON, |
| 912 | + ) |
| 913 | + |
| 914 | + mocker.patch("utils.agents.streaming.consume_query_tokens") |
| 915 | + mocker.patch( |
| 916 | + "utils.agents.streaming.get_available_quotas", |
| 917 | + return_value={"daily": 100}, |
| 918 | + ) |
| 919 | + mocker.patch( |
| 920 | + "utils.agents.streaming.maybe_get_topic_summary", |
| 921 | + new=mocker.AsyncMock(return_value=None), |
| 922 | + ) |
| 923 | + mocker.patch("utils.agents.streaming.store_query_results") |
| 924 | + mock_config = mocker.Mock() |
| 925 | + mock_config.quota_limiters = [] |
| 926 | + mocker.patch("utils.agents.streaming.configuration", mock_config) |
| 927 | + mocker.patch( |
| 928 | + "utils.agents.streaming.anonymize_value", |
| 929 | + side_effect=lambda v: f"[anon:{v}]", |
| 930 | + ) |
| 931 | + |
| 932 | + [ |
| 933 | + event |
| 934 | + async for event in generate_agent_response( |
| 935 | + inner(), |
| 936 | + context, |
| 937 | + responses_params, |
| 938 | + turn_summary, |
| 939 | + background_tasks, |
| 940 | + root_span=root_span, |
| 941 | + ) |
| 942 | + ] |
| 943 | + |
| 944 | + spans = exporter.get_finished_spans() |
| 945 | + assert len(spans) == 1 |
| 946 | + span = spans[0] |
| 947 | + assert span.attributes is not None |
| 948 | + assert span.attributes[SpanAttributes.TOOL_CALLS_COUNT] == 2 |
| 949 | + assert span.attributes[SpanAttributes.TOOL_CALLS_NAMES] == ( |
| 950 | + "web_search", |
| 951 | + "file_search", |
| 952 | + ) |
| 953 | + event_names = [e.name for e in span.events] |
| 954 | + assert SpanEvents.TOOL_EXECUTION_COMPLETED in event_names |
| 955 | + tool_event = next( |
| 956 | + e for e in span.events if e.name == SpanEvents.TOOL_EXECUTION_COMPLETED |
| 957 | + ) |
| 958 | + assert tool_event.attributes is not None |
| 959 | + assert tool_event.attributes["tool.calls"] == "web_search, file_search" |
| 960 | + |
887 | 961 | @pytest.mark.asyncio |
888 | 962 | async def test_span_ended_on_stream_error( |
889 | 963 | self, |
|
0 commit comments