|
14 | 14 | ModelSettings, |
15 | 15 | Usage, |
16 | 16 | ) |
| 17 | +from agents.computer import Computer |
17 | 18 | from agents.exceptions import MaxTurnsExceeded, ModelBehaviorError |
18 | 19 | from agents.items import ( |
19 | 20 | ResponseFunctionToolCall, |
|
24 | 25 | from agents.tool import HostedMCPTool |
25 | 26 | from agents.version import __version__ as OPENAI_AGENTS_VERSION |
26 | 27 | from openai import AsyncOpenAI, InternalServerError |
| 28 | +from openai.types.responses.tool_param import CodeInterpreter, ImageGeneration |
| 29 | + |
| 30 | +from sentry_sdk.integrations import DidNotEnable |
| 31 | + |
| 32 | +try: |
| 33 | + from agents import ( |
| 34 | + CodeInterpreterTool, |
| 35 | + ComputerTool, |
| 36 | + FileSearchTool, |
| 37 | + HostedMCPTool, |
| 38 | + ImageGenerationTool, |
| 39 | + LocalShellTool, |
| 40 | + WebSearchTool, |
| 41 | + ) |
| 42 | +except ImportError: |
| 43 | + raise DidNotEnable("OpenAI Agents not installed") |
| 44 | + |
| 45 | +try: |
| 46 | + from agents import ApplyPatchTool, ShellTool |
| 47 | +except ImportError: |
| 48 | + ShellTool = None |
| 49 | + ApplyPatchTool = None |
| 50 | + |
| 51 | +try: |
| 52 | + from agents import ToolSearchTool |
| 53 | +except ImportError: |
| 54 | + ToolSearchTool = None |
| 55 | + |
| 56 | +try: |
| 57 | + from agents import CustomTool |
| 58 | +except ImportError: |
| 59 | + CustomTool = None |
| 60 | + |
| 61 | +try: |
| 62 | + from agents import ProgrammaticToolCallingTool |
| 63 | +except ImportError: |
| 64 | + ProgrammaticToolCallingTool = None |
| 65 | + |
| 66 | + |
| 67 | +from typing import Any, cast |
27 | 68 |
|
28 | 69 | import sentry_sdk |
29 | 70 | from sentry_sdk import start_span |
@@ -173,6 +214,225 @@ def test_agent_custom_model(): |
173 | 214 | ) |
174 | 215 |
|
175 | 216 |
|
| 217 | +class DummyEditor: |
| 218 | + def create_file(self, operation): |
| 219 | + return None |
| 220 | + |
| 221 | + def update_file(self, operation): |
| 222 | + return None |
| 223 | + |
| 224 | + def delete_file(self, operation): |
| 225 | + return None |
| 226 | + |
| 227 | + |
| 228 | +class TrackingComputer(Computer): |
| 229 | + def __init__(self): |
| 230 | + self.calls = [] |
| 231 | + |
| 232 | + @property |
| 233 | + def environment(self): |
| 234 | + return "mac" |
| 235 | + |
| 236 | + @property |
| 237 | + def dimensions(self): |
| 238 | + return (1, 1) |
| 239 | + |
| 240 | + def screenshot(self): |
| 241 | + self.calls.append("screenshot") |
| 242 | + return "img" |
| 243 | + |
| 244 | + def click(self, _x, _y, _button): |
| 245 | + self.calls.append("click") |
| 246 | + |
| 247 | + def double_click(self, _x, _y): |
| 248 | + self.calls.append("double_click") |
| 249 | + |
| 250 | + def scroll(self, _x, _y, _scroll_x, _scroll_y): |
| 251 | + self.calls.append("scroll") |
| 252 | + |
| 253 | + def type(self, _text): |
| 254 | + self.calls.append("type") |
| 255 | + |
| 256 | + def wait(self): |
| 257 | + self.calls.append("wait") |
| 258 | + |
| 259 | + def move(self, _x, _y): |
| 260 | + self.calls.append("move") |
| 261 | + |
| 262 | + def keypress(self, _keys): |
| 263 | + self.calls.append("keypress") |
| 264 | + |
| 265 | + def drag(self, _path): |
| 266 | + self.calls.append("drag") |
| 267 | + |
| 268 | + |
| 269 | +@pytest.mark.parametrize("span_streaming", [True, False]) |
| 270 | +@pytest.mark.parametrize("stream_gen_ai_spans", [True, False]) |
| 271 | +@pytest.mark.asyncio |
| 272 | +async def test_tool_definitions( |
| 273 | + sentry_init, |
| 274 | + capture_events, |
| 275 | + capture_items, |
| 276 | + test_agent, |
| 277 | + nonstreaming_responses_model_response, |
| 278 | + get_model_response, |
| 279 | + stream_gen_ai_spans, |
| 280 | + span_streaming, |
| 281 | +): |
| 282 | + client = AsyncOpenAI(api_key="test-key") |
| 283 | + model = OpenAIResponsesModel(model="gpt-4", openai_client=client) |
| 284 | + |
| 285 | + def some_function(a: str, b: list[int]) -> str: |
| 286 | + return "hello" |
| 287 | + |
| 288 | + tools = [ |
| 289 | + agents.function_tool(some_function, defer_loading=True), |
| 290 | + WebSearchTool(), |
| 291 | + FileSearchTool(vector_store_ids=[]), |
| 292 | + ComputerTool(computer=TrackingComputer()), |
| 293 | + HostedMCPTool( |
| 294 | + tool_config=cast( |
| 295 | + Any, |
| 296 | + { |
| 297 | + "type": "mcp", |
| 298 | + "server_label": "docs_server", |
| 299 | + "server_url": "https://example.com/mcp", |
| 300 | + }, |
| 301 | + ) |
| 302 | + ), |
| 303 | + ImageGenerationTool( |
| 304 | + tool_config=cast( |
| 305 | + ImageGeneration, {"type": "image_generation", "model": "gpt-image-1"} |
| 306 | + ) |
| 307 | + ), |
| 308 | + CodeInterpreterTool( |
| 309 | + tool_config=cast( |
| 310 | + CodeInterpreter, {"type": "code_interpreter", "container": "python"} |
| 311 | + ) |
| 312 | + ), |
| 313 | + LocalShellTool(executor=lambda req: "ok"), |
| 314 | + ] |
| 315 | + |
| 316 | + if CustomTool is not None: |
| 317 | + tools.append( |
| 318 | + CustomTool( |
| 319 | + name="custom", |
| 320 | + description="Custom tool", |
| 321 | + on_invoke_tool=lambda _context, _input: "ok", |
| 322 | + ) |
| 323 | + ) |
| 324 | + |
| 325 | + if ApplyPatchTool is not None: |
| 326 | + tools.append(ApplyPatchTool(editor=DummyEditor())) |
| 327 | + |
| 328 | + if ShellTool is not None: |
| 329 | + tools.append(ShellTool(executor=lambda req: "ok")) |
| 330 | + |
| 331 | + if ToolSearchTool is not None: |
| 332 | + tools.append(ToolSearchTool()) |
| 333 | + |
| 334 | + if ProgrammaticToolCallingTool is not None: |
| 335 | + tools.append(ProgrammaticToolCallingTool()) |
| 336 | + |
| 337 | + agent = test_agent.clone(model=model, tools=tools) |
| 338 | + |
| 339 | + response = get_model_response( |
| 340 | + nonstreaming_responses_model_response, serialize_pydantic=True |
| 341 | + ) |
| 342 | + |
| 343 | + if span_streaming: |
| 344 | + with patch.object( |
| 345 | + agent.model._client._client, |
| 346 | + "send", |
| 347 | + return_value=response, |
| 348 | + ) as _: |
| 349 | + sentry_init( |
| 350 | + integrations=[OpenAIAgentsIntegration()], |
| 351 | + disabled_integrations=[StdlibIntegration], |
| 352 | + traces_sample_rate=1.0, |
| 353 | + send_default_pii=False, |
| 354 | + stream_gen_ai_spans=stream_gen_ai_spans, |
| 355 | + trace_lifecycle="stream", |
| 356 | + ) |
| 357 | + |
| 358 | + items = capture_items("span") |
| 359 | + |
| 360 | + result = await agents.Runner.run( |
| 361 | + agent, |
| 362 | + "Test input", |
| 363 | + run_config=test_run_config, |
| 364 | + ) |
| 365 | + |
| 366 | + assert result is not None |
| 367 | + assert result.final_output == "Hello, how can I help you?" |
| 368 | + |
| 369 | + sentry_sdk.flush() |
| 370 | + spans = [item.payload for item in items] |
| 371 | + ai_client_span = next( |
| 372 | + span for span in spans if span["attributes"]["sentry.op"] == OP.GEN_AI_CHAT |
| 373 | + ) |
| 374 | + |
| 375 | + assert ai_client_span["attributes"][SPANDATA.GEN_AI_TOOL_DEFINITIONS] == 1.0 |
| 376 | + elif stream_gen_ai_spans: |
| 377 | + with patch.object( |
| 378 | + agent.model._client._client, |
| 379 | + "send", |
| 380 | + return_value=response, |
| 381 | + ) as _: |
| 382 | + sentry_init( |
| 383 | + integrations=[OpenAIAgentsIntegration()], |
| 384 | + traces_sample_rate=1.0, |
| 385 | + send_default_pii=False, |
| 386 | + stream_gen_ai_spans=stream_gen_ai_spans, |
| 387 | + ) |
| 388 | + |
| 389 | + items = capture_items("span", "transaction") |
| 390 | + |
| 391 | + result = await agents.Runner.run( |
| 392 | + agent, |
| 393 | + "Test input", |
| 394 | + run_config=test_run_config, |
| 395 | + ) |
| 396 | + |
| 397 | + assert result is not None |
| 398 | + assert result.final_output == "Hello, how can I help you?" |
| 399 | + |
| 400 | + spans = [item.payload for item in items if item.type == "span"] |
| 401 | + ai_client_span = next( |
| 402 | + span for span in spans if span["attributes"]["sentry.op"] == OP.GEN_AI_CHAT |
| 403 | + ) |
| 404 | + |
| 405 | + assert ai_client_span["attributes"][SPANDATA.GEN_AI_TOOL_DEFINITIONS] == 1.0 |
| 406 | + else: |
| 407 | + with patch.object( |
| 408 | + agent.model._client._client, |
| 409 | + "send", |
| 410 | + return_value=response, |
| 411 | + ) as _: |
| 412 | + sentry_init( |
| 413 | + integrations=[OpenAIAgentsIntegration()], |
| 414 | + traces_sample_rate=1.0, |
| 415 | + send_default_pii=False, |
| 416 | + stream_gen_ai_spans=stream_gen_ai_spans, |
| 417 | + ) |
| 418 | + events = capture_events() |
| 419 | + |
| 420 | + result = await agents.Runner.run( |
| 421 | + agent, |
| 422 | + "Test input", |
| 423 | + run_config=test_run_config, |
| 424 | + ) |
| 425 | + |
| 426 | + assert result is not None |
| 427 | + assert result.final_output == "Hello, how can I help you?" |
| 428 | + |
| 429 | + (transaction,) = events |
| 430 | + spans = transaction["spans"] |
| 431 | + ai_client_span = next(span for span in spans if span["op"] == OP.GEN_AI_CHAT) |
| 432 | + |
| 433 | + assert ai_client_span["data"][SPANDATA.GEN_AI_TOOL_DEFINITIONS] == 1.0 |
| 434 | + |
| 435 | + |
176 | 436 | @pytest.mark.parametrize("span_streaming", [True, False]) |
177 | 437 | @pytest.mark.parametrize("stream_gen_ai_spans", [True, False]) |
178 | 438 | @pytest.mark.asyncio |
|
0 commit comments