Skip to content

Commit 68a5b96

Browse files
committed
Merge branch 'master' into ivana/span-first-9-start-end
2 parents 09b88f0 + c7e394c commit 68a5b96

File tree

7 files changed

+128
-46
lines changed

7 files changed

+128
-46
lines changed

.github/workflows/ai-integration-test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
token: ${{ secrets.GITHUB_TOKEN }}
3535

3636
- name: Run Python SDK Tests
37-
uses: getsentry/testing-ai-sdk-integrations@121da677853244cedfe11e95184b2b431af102eb
37+
uses: getsentry/testing-ai-sdk-integrations@285c012e522f241581534dfc89bd99ec3b1da4f6
3838
env:
3939
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
4040
with:

sentry_sdk/integrations/httpx.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424

2525
try:
26-
from httpx import AsyncClient, Client, Request, Response # type: ignore
26+
from httpx import AsyncClient, Client, Request, Response
2727
except ImportError:
2828
raise DidNotEnable("httpx is not installed")
2929

@@ -94,7 +94,7 @@ def send(self: "Client", request: "Request", **kwargs: "Any") -> "Response":
9494

9595
return rv
9696

97-
Client.send = send
97+
Client.send = send # type: ignore
9898

9999

100100
def _install_httpx_async_client() -> None:
@@ -150,4 +150,4 @@ async def send(
150150

151151
return rv
152152

153-
AsyncClient.send = send
153+
AsyncClient.send = send # type: ignore

sentry_sdk/integrations/pydantic_ai/patches/tools.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ async def wrapped_execute_tool_call(
5050
call = validated.call
5151
name = call.tool_name
5252
tool = self.tools.get(name) if self.tools else None
53+
selected_tool_definition = getattr(tool, "tool_def", None)
5354

5455
# Determine tool type by checking tool.toolset
5556
tool_type = "function"
@@ -73,6 +74,7 @@ async def wrapped_execute_tool_call(
7374
args_dict,
7475
agent,
7576
tool_type=tool_type,
77+
tool_definition=selected_tool_definition,
7678
) as span:
7779
try:
7880
result = await original_execute_tool_call(
@@ -127,6 +129,7 @@ async def wrapped_call_tool(
127129
# Extract tool info before calling original
128130
name = call.tool_name
129131
tool = self.tools.get(name) if self.tools else None
132+
selected_tool_definition = getattr(tool, "tool_def", None)
130133

131134
# Determine tool type by checking tool.toolset
132135
tool_type = "function" # default
@@ -150,6 +153,7 @@ async def wrapped_call_tool(
150153
args_dict,
151154
agent,
152155
tool_type=tool_type,
156+
tool_definition=selected_tool_definition,
153157
) as span:
154158
try:
155159
result = await original_call_tool(

sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,15 @@
99

1010
if TYPE_CHECKING:
1111
from typing import Any, Optional
12+
from pydantic_ai._tool_manager import ToolDefinition # type: ignore
1213

1314

1415
def execute_tool_span(
15-
tool_name: str, tool_args: "Any", agent: "Any", tool_type: str = "function"
16+
tool_name: str,
17+
tool_args: "Any",
18+
agent: "Any",
19+
tool_type: str = "function",
20+
tool_definition: "Optional[ToolDefinition]" = None,
1621
) -> "sentry_sdk.tracing.Span":
1722
"""Create a span for tool execution.
1823
@@ -21,6 +26,7 @@ def execute_tool_span(
2126
tool_args: The arguments passed to the tool
2227
agent: The agent executing the tool
2328
tool_type: The type of tool ("function" for regular tools, "mcp" for MCP services)
29+
tool_definition: The definition of the tool, if available
2430
"""
2531
span = sentry_sdk.start_span(
2632
op=OP.GEN_AI_EXECUTE_TOOL,
@@ -32,6 +38,12 @@ def execute_tool_span(
3238
span.set_data(SPANDATA.GEN_AI_TOOL_TYPE, tool_type)
3339
span.set_data(SPANDATA.GEN_AI_TOOL_NAME, tool_name)
3440

41+
if tool_definition is not None and hasattr(tool_definition, "description"):
42+
span.set_data(
43+
SPANDATA.GEN_AI_TOOL_DESCRIPTION,
44+
tool_definition.description,
45+
)
46+
3547
_set_agent_data(span, agent)
3648

3749
if _should_send_prompts() and tool_args is not None:

tests/integrations/openai_agents/test_openai_agents.py

Lines changed: 63 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1220,55 +1220,57 @@ def simple_test_tool(message: str) -> str:
12201220
)
12211221
tool_span = next(span for span in spans if span["op"] == OP.GEN_AI_EXECUTE_TOOL)
12221222

1223-
available_tools = [
1224-
{
1225-
"name": "simple_test_tool",
1226-
"description": "A simple tool",
1227-
"params_json_schema": {
1228-
"properties": {"message": {"title": "Message", "type": "string"}},
1229-
"required": ["message"],
1230-
"title": "simple_test_tool_args",
1231-
"type": "object",
1232-
"additionalProperties": False,
1233-
},
1234-
"on_invoke_tool": "<function agents.tool.function_tool.<locals>._create_function_tool.<locals>._on_invoke_tool>",
1235-
"strict_json_schema": True,
1236-
"is_enabled": True,
1237-
}
1238-
]
1223+
available_tool = {
1224+
"name": "simple_test_tool",
1225+
"description": "A simple tool",
1226+
"params_json_schema": {
1227+
"properties": {"message": {"title": "Message", "type": "string"}},
1228+
"required": ["message"],
1229+
"title": "simple_test_tool_args",
1230+
"type": "object",
1231+
"additionalProperties": False,
1232+
},
1233+
"on_invoke_tool": mock.ANY,
1234+
"strict_json_schema": True,
1235+
"is_enabled": True,
1236+
}
1237+
12391238
if parse_version(OPENAI_AGENTS_VERSION) >= (0, 3, 3):
1240-
available_tools[0].update(
1239+
available_tool.update(
12411240
{"tool_input_guardrails": None, "tool_output_guardrails": None}
12421241
)
12431242

12441243
if parse_version(OPENAI_AGENTS_VERSION) >= (
12451244
0,
12461245
8,
12471246
):
1248-
available_tools[0]["needs_approval"] = False
1247+
available_tool["needs_approval"] = False
12491248
if parse_version(OPENAI_AGENTS_VERSION) >= (
12501249
0,
12511250
9,
12521251
0,
12531252
):
1254-
available_tools[0].update(
1253+
available_tool.update(
12551254
{
12561255
"timeout_seconds": None,
12571256
"timeout_behavior": "error_as_result",
12581257
"timeout_error_function": None,
12591258
}
12601259
)
12611260

1262-
available_tools = safe_serialize(available_tools)
1263-
12641261
assert transaction["transaction"] == "test_agent workflow"
12651262
assert transaction["contexts"]["trace"]["origin"] == "auto.ai.openai_agents"
12661263

12671264
assert agent_span["description"] == "invoke_agent test_agent"
12681265
assert agent_span["origin"] == "auto.ai.openai_agents"
12691266
assert agent_span["data"]["gen_ai.agent.name"] == "test_agent"
12701267
assert agent_span["data"]["gen_ai.operation.name"] == "invoke_agent"
1271-
assert agent_span["data"]["gen_ai.request.available_tools"] == available_tools
1268+
1269+
agent_span_available_tool = json.loads(
1270+
agent_span["data"]["gen_ai.request.available_tools"]
1271+
)[0]
1272+
assert all(agent_span_available_tool[k] == v for k, v in available_tool.items())
1273+
12721274
assert agent_span["data"]["gen_ai.request.max_tokens"] == 100
12731275
assert agent_span["data"]["gen_ai.request.model"] == "gpt-4"
12741276
assert agent_span["data"]["gen_ai.request.temperature"] == 0.7
@@ -1279,7 +1281,14 @@ def simple_test_tool(message: str) -> str:
12791281
assert ai_client_span1["data"]["gen_ai.operation.name"] == "chat"
12801282
assert ai_client_span1["data"]["gen_ai.system"] == "openai"
12811283
assert ai_client_span1["data"]["gen_ai.agent.name"] == "test_agent"
1282-
assert ai_client_span1["data"]["gen_ai.request.available_tools"] == available_tools
1284+
1285+
ai_client_span1_available_tool = json.loads(
1286+
ai_client_span1["data"]["gen_ai.request.available_tools"]
1287+
)[0]
1288+
assert all(
1289+
ai_client_span1_available_tool[k] == v for k, v in available_tool.items()
1290+
)
1291+
12831292
assert ai_client_span1["data"]["gen_ai.request.max_tokens"] == 100
12841293
assert ai_client_span1["data"]["gen_ai.request.messages"] == safe_serialize(
12851294
[
@@ -1319,14 +1328,12 @@ def simple_test_tool(message: str) -> str:
13191328
assert tool_span["description"] == "execute_tool simple_test_tool"
13201329
assert tool_span["data"]["gen_ai.agent.name"] == "test_agent"
13211330
assert tool_span["data"]["gen_ai.operation.name"] == "execute_tool"
1322-
assert (
1323-
re.sub(
1324-
"<.*>(,)",
1325-
r"'NOT_CHECKED'\1",
1326-
agent_span["data"]["gen_ai.request.available_tools"],
1327-
)
1328-
== available_tools
1329-
)
1331+
1332+
tool_span_available_tool = json.loads(
1333+
tool_span["data"]["gen_ai.request.available_tools"]
1334+
)[0]
1335+
assert all(tool_span_available_tool[k] == v for k, v in available_tool.items())
1336+
13301337
assert tool_span["data"]["gen_ai.request.max_tokens"] == 100
13311338
assert tool_span["data"]["gen_ai.request.model"] == "gpt-4"
13321339
assert tool_span["data"]["gen_ai.request.temperature"] == 0.7
@@ -1341,14 +1348,14 @@ def simple_test_tool(message: str) -> str:
13411348
assert ai_client_span2["description"] == "chat gpt-4"
13421349
assert ai_client_span2["data"]["gen_ai.agent.name"] == "test_agent"
13431350
assert ai_client_span2["data"]["gen_ai.operation.name"] == "chat"
1344-
assert (
1345-
re.sub(
1346-
"<.*>(,)",
1347-
r"'NOT_CHECKED'\1",
1348-
agent_span["data"]["gen_ai.request.available_tools"],
1349-
)
1350-
== available_tools
1351+
1352+
ai_client_span2_available_tool = json.loads(
1353+
ai_client_span2["data"]["gen_ai.request.available_tools"]
1354+
)[0]
1355+
assert all(
1356+
ai_client_span2_available_tool[k] == v for k, v in available_tool.items()
13511357
)
1358+
13521359
assert ai_client_span2["data"]["gen_ai.request.max_tokens"] == 100
13531360
assert ai_client_span2["data"]["gen_ai.request.messages"] == safe_serialize(
13541361
[
@@ -1425,6 +1432,15 @@ async def test_hosted_mcp_tool_propagation_header_streamed(
14251432
"/responses",
14261433
)
14271434

1435+
# openai-agents calls with_streaming_response() if available starting with
1436+
# https://github.com/openai/openai-agents-python/commit/159beb56130f7d85192acfd593c9168757984dc0.
1437+
# When using with_streaming_response() the header set below changes the response type:
1438+
# https://github.com/openai/openai-python/blob/656e3cab4a18262a49b961d41293367e45ee71b9/src/openai/_response.py#L67.
1439+
if parse_version(OPENAI_AGENTS_VERSION) >= (0, 10, 3) and hasattr(
1440+
agent_with_tool.model._client.responses, "with_streaming_response"
1441+
):
1442+
request.headers["X-Stainless-Raw-Response"] = "stream"
1443+
14281444
response = httpx.Response(
14291445
200,
14301446
request=request,
@@ -3178,6 +3194,15 @@ async def test_streaming_ttft_on_chat_span(sentry_init, test_agent, async_iterat
31783194
"/responses",
31793195
)
31803196

3197+
# openai-agents calls with_streaming_response() if available starting with
3198+
# https://github.com/openai/openai-agents-python/commit/159beb56130f7d85192acfd593c9168757984dc0.
3199+
# When using with_streaming_response() the header set below changes the response type:
3200+
# https://github.com/openai/openai-python/blob/656e3cab4a18262a49b961d41293367e45ee71b9/src/openai/_response.py#L67.
3201+
if parse_version(OPENAI_AGENTS_VERSION) >= (0, 10, 3) and hasattr(
3202+
agent_with_tool.model._client.responses, "with_streaming_response"
3203+
):
3204+
request.headers["X-Stainless-Raw-Response"] = "stream"
3205+
31813206
response = httpx.Response(
31823207
200,
31833208
request=request,

tests/integrations/pydantic_ai/test_pydantic_ai.py

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
from pydantic_ai import Agent
1717
from pydantic_ai.messages import BinaryContent, UserPromptPart
1818
from pydantic_ai.usage import RequestUsage
19-
from pydantic_ai.models.test import TestModel
2019
from pydantic_ai.exceptions import ModelRetry, UnexpectedModelBehavior
2120

2221

@@ -2386,7 +2385,9 @@ async def test_execute_tool_span_with_mcp_type(sentry_init, capture_events):
23862385
Test execute_tool span with MCP tool type.
23872386
"""
23882387
import sentry_sdk
2389-
from sentry_sdk.integrations.pydantic_ai.spans.execute_tool import execute_tool_span
2388+
from sentry_sdk.integrations.pydantic_ai.spans.execute_tool import (
2389+
execute_tool_span,
2390+
)
23902391

23912392
sentry_init(
23922393
integrations=[PydanticAIIntegration()],
@@ -2794,3 +2795,42 @@ async def test_set_usage_data_with_cache_tokens(sentry_init, capture_events):
27942795
(span_data,) = event["spans"]
27952796
assert span_data["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED] == 80
27962797
assert span_data["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE] == 20
2798+
2799+
2800+
@pytest.mark.asyncio
2801+
async def test_tool_description_in_execute_tool_span(sentry_init, capture_events):
2802+
"""
2803+
Test that tool description from the tool's docstring is included in execute_tool spans.
2804+
"""
2805+
agent = Agent(
2806+
"test",
2807+
name="test_agent",
2808+
system_prompt="You are a helpful test assistant.",
2809+
)
2810+
2811+
@agent.tool_plain
2812+
def multiply_numbers(a: int, b: int) -> int:
2813+
"""Multiply two numbers and return the product."""
2814+
return a * b
2815+
2816+
sentry_init(
2817+
integrations=[PydanticAIIntegration()],
2818+
traces_sample_rate=1.0,
2819+
send_default_pii=True,
2820+
)
2821+
2822+
events = capture_events()
2823+
2824+
result = await agent.run("What is 5 times 3?")
2825+
assert result is not None
2826+
2827+
(transaction,) = events
2828+
spans = transaction["spans"]
2829+
2830+
tool_spans = [s for s in spans if s["op"] == "gen_ai.execute_tool"]
2831+
assert len(tool_spans) >= 1
2832+
2833+
tool_span = tool_spans[0]
2834+
assert tool_span["data"]["gen_ai.tool.name"] == "multiply_numbers"
2835+
assert SPANDATA.GEN_AI_TOOL_DESCRIPTION in tool_span["data"]
2836+
assert "Multiply two numbers" in tool_span["data"][SPANDATA.GEN_AI_TOOL_DESCRIPTION]

tests/integrations/pyramid/test_pyramid.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import pytest
77
from pyramid.authorization import ACLAuthorizationPolicy
88
from pyramid.response import Response
9+
from packaging.version import Version
910
from werkzeug.test import Client
1011

1112
from sentry_sdk import capture_message, add_breadcrumb
@@ -18,7 +19,7 @@
1819
try:
1920
from importlib.metadata import version
2021

21-
PYRAMID_VERSION = tuple(map(int, version("pyramid").split(".")))
22+
PYRAMID_VERSION = Version(version("pyramid")).release
2223

2324
except ImportError:
2425
# < py3.8

0 commit comments

Comments
 (0)