From 4b8472da7ecb36b25b29d3ca96de5f7e54fcd449 Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Mon, 21 Apr 2025 11:14:25 -0400 Subject: [PATCH 01/35] Examples for image inputs (#553) --- examples/basic/local_image.py | 48 +++++++++++++++++++++++++++ examples/basic/media/image_bison.jpg | Bin 0 -> 235620 bytes examples/basic/remote_image.py | 31 +++++++++++++++++ 3 files changed, 79 insertions(+) create mode 100644 examples/basic/local_image.py create mode 100644 examples/basic/media/image_bison.jpg create mode 100644 examples/basic/remote_image.py diff --git a/examples/basic/local_image.py b/examples/basic/local_image.py new file mode 100644 index 000000000..d4a784ba2 --- /dev/null +++ b/examples/basic/local_image.py @@ -0,0 +1,48 @@ +import asyncio +import base64 +import os + +from agents import Agent, Runner + +FILEPATH = os.path.join(os.path.dirname(__file__), "media/image_bison.jpg") + + +def image_to_base64(image_path): + with open(image_path, "rb") as image_file: + encoded_string = base64.b64encode(image_file.read()).decode("utf-8") + return encoded_string + + +async def main(): + # Print base64-encoded image + b64_image = image_to_base64(FILEPATH) + + agent = Agent( + name="Assistant", + instructions="You are a helpful assistant.", + ) + + result = await Runner.run( + agent, + [ + { + "role": "user", + "content": [ + { + "type": "input_image", + "detail": "auto", + "image_url": f"data:image/jpeg;base64,{b64_image}", + } + ], + }, + { + "role": "user", + "content": "What do you see in this image?", + }, + ], + ) + print(result.final_output) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/basic/media/image_bison.jpg b/examples/basic/media/image_bison.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b113c91f601eac2369c4b859b18f86a24a228d89 GIT binary patch literal 235620 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 z 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 Date: Mon, 21 Apr 2025 11:58:36 -0400 Subject: [PATCH 02/35] Enable non-strict output types (#539) See #528, some folks are having issues because their output types are not strict-compatible. My approach was: 1. Create `AgentOutputSchemaBase`, which represents the base methods for an output type - the json schema + validation 2. Make the existing `AgentOutputSchema` subclass `AgentOutputSchemaBase` 3. Allow users to pass a `AgentOutputSchemaBase` to `Agent(output_type=...)` --- examples/basic/non_strict_output_type.py | 81 +++++++++++++++++++ src/agents/__init__.py | 3 +- src/agents/_run_impl.py | 6 +- src/agents/agent.py | 11 ++- src/agents/agent_output.py | 66 +++++++++++++-- src/agents/extensions/models/litellm_model.py | 12 +-- src/agents/models/chatcmpl_converter.py | 6 +- src/agents/models/interface.py | 6 +- src/agents/models/openai_chatcompletions.py | 12 +-- src/agents/models/openai_responses.py | 16 ++-- src/agents/result.py | 4 +- src/agents/run.py | 14 ++-- tests/fake_model.py | 6 +- tests/test_agent_config.py | 5 +- .../test_openai_chatcompletions_converter.py | 2 +- tests/test_openai_responses_converter.py | 2 +- tests/test_output_tool.py | 59 +++++++++++++- tests/voice/test_workflow.py | 6 +- 18 files changed, 256 insertions(+), 61 deletions(-) create mode 100644 examples/basic/non_strict_output_type.py diff --git a/examples/basic/non_strict_output_type.py b/examples/basic/non_strict_output_type.py new file mode 100644 index 000000000..49fcc4e2c --- /dev/null +++ b/examples/basic/non_strict_output_type.py @@ -0,0 +1,81 @@ +import asyncio +import json +from dataclasses import dataclass +from typing import Any + +from agents import Agent, AgentOutputSchema, AgentOutputSchemaBase, Runner + +"""This example demonstrates how to use an output type that is not in strict mode. Strict mode +allows us to guarantee valid JSON output, but some schemas are not strict-compatible. + +In this example, we define an output type that is not strict-compatible, and then we run the +agent with strict_json_schema=False. + +We also demonstrate a custom output type. + +To understand which schemas are strict-compatible, see: +https://platform.openai.com/docs/guides/structured-outputs?api-mode=responses#supported-schemas +""" + + +@dataclass +class OutputType: + jokes: dict[int, str] + """A list of jokes, indexed by joke number.""" + + +class CustomOutputSchema(AgentOutputSchemaBase): + """A demonstration of a custom output schema.""" + + def is_plain_text(self) -> bool: + return False + + def name(self) -> str: + return "CustomOutputSchema" + + def json_schema(self) -> dict[str, Any]: + return { + "type": "object", + "properties": {"jokes": {"type": "object", "properties": {"joke": {"type": "string"}}}}, + } + + def is_strict_json_schema(self) -> bool: + return False + + def validate_json(self, json_str: str) -> Any: + json_obj = json.loads(json_str) + # Just for demonstration, we'll return a list. + return list(json_obj["jokes"].values()) + + +async def main(): + agent = Agent( + name="Assistant", + instructions="You are a helpful assistant.", + output_type=OutputType, + ) + + input = "Tell me 3 short jokes." + + # First, let's try with a strict output type. This should raise an exception. + try: + result = await Runner.run(agent, input) + raise AssertionError("Should have raised an exception") + except Exception as e: + print(f"Error (expected): {e}") + + # Now let's try again with a non-strict output type. This should work. + # In some cases, it will raise an error - the schema isn't strict, so the model may + # produce an invalid JSON object. + agent.output_type = AgentOutputSchema(OutputType, strict_json_schema=False) + result = await Runner.run(agent, input) + print(result.final_output) + + # Finally, let's try a custom output type. + agent.output_type = CustomOutputSchema() + result = await Runner.run(agent, input) + print(result.final_output) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/src/agents/__init__.py b/src/agents/__init__.py index db7d312e4..6d7c90b4f 100644 --- a/src/agents/__init__.py +++ b/src/agents/__init__.py @@ -6,7 +6,7 @@ from . import _config from .agent import Agent, ToolsToFinalOutputFunction, ToolsToFinalOutputResult -from .agent_output import AgentOutputSchema +from .agent_output import AgentOutputSchema, AgentOutputSchemaBase from .computer import AsyncComputer, Button, Computer, Environment from .exceptions import ( AgentsException, @@ -158,6 +158,7 @@ def enable_verbose_stdout_logging(): "OpenAIProvider", "OpenAIResponsesModel", "AgentOutputSchema", + "AgentOutputSchemaBase", "Computer", "AsyncComputer", "Environment", diff --git a/src/agents/_run_impl.py b/src/agents/_run_impl.py index 94c181b7f..b5a83685c 100644 --- a/src/agents/_run_impl.py +++ b/src/agents/_run_impl.py @@ -29,7 +29,7 @@ from openai.types.responses.response_reasoning_item import ResponseReasoningItem from .agent import Agent, ToolsToFinalOutputResult -from .agent_output import AgentOutputSchema +from .agent_output import AgentOutputSchemaBase from .computer import AsyncComputer, Computer from .exceptions import AgentsException, ModelBehaviorError, UserError from .guardrail import InputGuardrail, InputGuardrailResult, OutputGuardrail, OutputGuardrailResult @@ -195,7 +195,7 @@ async def execute_tools_and_side_effects( pre_step_items: list[RunItem], new_response: ModelResponse, processed_response: ProcessedResponse, - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, hooks: RunHooks[TContext], context_wrapper: RunContextWrapper[TContext], run_config: RunConfig, @@ -335,7 +335,7 @@ def process_model_response( agent: Agent[Any], all_tools: list[Tool], response: ModelResponse, - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], ) -> ProcessedResponse: items: list[RunItem] = [] diff --git a/src/agents/agent.py b/src/agents/agent.py index a24456b06..e22f579fa 100644 --- a/src/agents/agent.py +++ b/src/agents/agent.py @@ -8,6 +8,7 @@ from typing_extensions import NotRequired, TypeAlias, TypedDict +from .agent_output import AgentOutputSchemaBase from .guardrail import InputGuardrail, OutputGuardrail from .handoffs import Handoff from .items import ItemHelpers @@ -141,8 +142,14 @@ class Agent(Generic[TContext]): Runs only if the agent produces a final output. """ - output_type: type[Any] | None = None - """The type of the output object. If not provided, the output will be `str`.""" + output_type: type[Any] | AgentOutputSchemaBase | None = None + """The type of the output object. If not provided, the output will be `str`. In most cases, + you should pass a regular Python type (e.g. a dataclass, Pydantic model, TypedDict, etc). + You can customize this in two ways: + 1. If you want non-strict schemas, pass `AgentOutputSchema(MyClass, strict_json_schema=False)`. + 2. If you want to use a custom JSON schema (i.e. without using the SDK's automatic schema) + creation, subclass and pass an `AgentOutputSchemaBase` subclass. + """ hooks: AgentHooks[TContext] | None = None """A class that receives callbacks on various lifecycle events for this agent. diff --git a/src/agents/agent_output.py b/src/agents/agent_output.py index 3262c57d6..066bbd835 100644 --- a/src/agents/agent_output.py +++ b/src/agents/agent_output.py @@ -1,3 +1,4 @@ +import abc from dataclasses import dataclass from typing import Any @@ -12,8 +13,46 @@ _WRAPPER_DICT_KEY = "response" +class AgentOutputSchemaBase(abc.ABC): + """An object that captures the JSON schema of the output, as well as validating/parsing JSON + produced by the LLM into the output type. + """ + + @abc.abstractmethod + def is_plain_text(self) -> bool: + """Whether the output type is plain text (versus a JSON object).""" + pass + + @abc.abstractmethod + def name(self) -> str: + """The name of the output type.""" + pass + + @abc.abstractmethod + def json_schema(self) -> dict[str, Any]: + """Returns the JSON schema of the output. Will only be called if the output type is not + plain text. + """ + pass + + @abc.abstractmethod + def is_strict_json_schema(self) -> bool: + """Whether the JSON schema is in strict mode. Strict mode constrains the JSON schema + features, but guarantees valis JSON. See here for details: + https://platform.openai.com/docs/guides/structured-outputs#supported-schemas + """ + pass + + @abc.abstractmethod + def validate_json(self, json_str: str) -> Any: + """Validate a JSON string against the output type. You must return the validated object, + or raise a `ModelBehaviorError` if the JSON is invalid. + """ + pass + + @dataclass(init=False) -class AgentOutputSchema: +class AgentOutputSchema(AgentOutputSchemaBase): """An object that captures the JSON schema of the output, as well as validating/parsing JSON produced by the LLM into the output type. """ @@ -32,7 +71,7 @@ class AgentOutputSchema: _output_schema: dict[str, Any] """The JSON schema of the output.""" - strict_json_schema: bool + _strict_json_schema: bool """Whether the JSON schema is in strict mode. We **strongly** recommend setting this to True, as it increases the likelihood of correct JSON input. """ @@ -45,7 +84,7 @@ def __init__(self, output_type: type[Any], strict_json_schema: bool = True): setting this to True, as it increases the likelihood of correct JSON input. """ self.output_type = output_type - self.strict_json_schema = strict_json_schema + self._strict_json_schema = strict_json_schema if output_type is None or output_type is str: self._is_wrapped = False @@ -70,24 +109,35 @@ def __init__(self, output_type: type[Any], strict_json_schema: bool = True): self._type_adapter = TypeAdapter(output_type) self._output_schema = self._type_adapter.json_schema() - if self.strict_json_schema: - self._output_schema = ensure_strict_json_schema(self._output_schema) + if self._strict_json_schema: + try: + self._output_schema = ensure_strict_json_schema(self._output_schema) + except UserError as e: + raise UserError( + "Strict JSON schema is enabled, but the output type is not valid. " + "Either make the output type strict, or pass output_schema_strict=False to " + "your Agent()" + ) from e def is_plain_text(self) -> bool: """Whether the output type is plain text (versus a JSON object).""" return self.output_type is None or self.output_type is str + def is_strict_json_schema(self) -> bool: + """Whether the JSON schema is in strict mode.""" + return self._strict_json_schema + def json_schema(self) -> dict[str, Any]: """The JSON schema of the output type.""" if self.is_plain_text(): raise UserError("Output type is plain text, so no JSON schema is available") return self._output_schema - def validate_json(self, json_str: str, partial: bool = False) -> Any: + def validate_json(self, json_str: str) -> Any: """Validate a JSON string against the output type. Returns the validated object, or raises a `ModelBehaviorError` if the JSON is invalid. """ - validated = _json.validate_json(json_str, self._type_adapter, partial) + validated = _json.validate_json(json_str, self._type_adapter, partial=False) if self._is_wrapped: if not isinstance(validated, dict): _error_tracing.attach_error_to_current_span( @@ -113,7 +163,7 @@ def validate_json(self, json_str: str, partial: bool = False) -> Any: return validated[_WRAPPER_DICT_KEY] return validated - def output_type_name(self) -> str: + def name(self) -> str: """The name of the output type.""" return _type_to_str(self.output_type) diff --git a/src/agents/extensions/models/litellm_model.py b/src/agents/extensions/models/litellm_model.py index 0fc277c35..e939ee8da 100644 --- a/src/agents/extensions/models/litellm_model.py +++ b/src/agents/extensions/models/litellm_model.py @@ -29,7 +29,7 @@ from openai.types.responses import Response from ... import _debug -from ...agent_output import AgentOutputSchema +from ...agent_output import AgentOutputSchemaBase from ...handoffs import Handoff from ...items import ModelResponse, TResponseInputItem, TResponseStreamEvent from ...logger import logger @@ -68,7 +68,7 @@ async def get_response( input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], tracing: ModelTracing, previous_response_id: str | None, @@ -139,7 +139,7 @@ async def stream_response( input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], tracing: ModelTracing, *, @@ -186,7 +186,7 @@ async def _fetch_response( input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], span: Span[GenerationSpanData], tracing: ModelTracing, @@ -200,7 +200,7 @@ async def _fetch_response( input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], span: Span[GenerationSpanData], tracing: ModelTracing, @@ -213,7 +213,7 @@ async def _fetch_response( input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], span: Span[GenerationSpanData], tracing: ModelTracing, diff --git a/src/agents/models/chatcmpl_converter.py b/src/agents/models/chatcmpl_converter.py index 00175a16a..613a37453 100644 --- a/src/agents/models/chatcmpl_converter.py +++ b/src/agents/models/chatcmpl_converter.py @@ -36,7 +36,7 @@ ) from openai.types.responses.response_input_param import FunctionCallOutput, ItemReference, Message -from ..agent_output import AgentOutputSchema +from ..agent_output import AgentOutputSchemaBase from ..exceptions import AgentsException, UserError from ..handoffs import Handoff from ..items import TResponseInputItem, TResponseOutputItem @@ -67,7 +67,7 @@ def convert_tool_choice( @classmethod def convert_response_format( - cls, final_output_schema: AgentOutputSchema | None + cls, final_output_schema: AgentOutputSchemaBase | None ) -> ResponseFormat | NotGiven: if not final_output_schema or final_output_schema.is_plain_text(): return NOT_GIVEN @@ -76,7 +76,7 @@ def convert_response_format( "type": "json_schema", "json_schema": { "name": "final_output", - "strict": final_output_schema.strict_json_schema, + "strict": final_output_schema.is_strict_json_schema(), "schema": final_output_schema.json_schema(), }, } diff --git a/src/agents/models/interface.py b/src/agents/models/interface.py index bcf2c1a65..3a79e5640 100644 --- a/src/agents/models/interface.py +++ b/src/agents/models/interface.py @@ -5,7 +5,7 @@ from collections.abc import AsyncIterator from typing import TYPE_CHECKING -from ..agent_output import AgentOutputSchema +from ..agent_output import AgentOutputSchemaBase from ..handoffs import Handoff from ..items import ModelResponse, TResponseInputItem, TResponseStreamEvent from ..tool import Tool @@ -41,7 +41,7 @@ async def get_response( input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], tracing: ModelTracing, *, @@ -72,7 +72,7 @@ def stream_response( input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], tracing: ModelTracing, *, diff --git a/src/agents/models/openai_chatcompletions.py b/src/agents/models/openai_chatcompletions.py index 9989c1ee0..9fd102690 100644 --- a/src/agents/models/openai_chatcompletions.py +++ b/src/agents/models/openai_chatcompletions.py @@ -12,7 +12,7 @@ from openai.types.responses import Response from .. import _debug -from ..agent_output import AgentOutputSchema +from ..agent_output import AgentOutputSchemaBase from ..handoffs import Handoff from ..items import ModelResponse, TResponseInputItem, TResponseStreamEvent from ..logger import logger @@ -49,7 +49,7 @@ async def get_response( input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], tracing: ModelTracing, previous_response_id: str | None, @@ -110,7 +110,7 @@ async def stream_response( input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], tracing: ModelTracing, *, @@ -160,7 +160,7 @@ async def _fetch_response( input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], span: Span[GenerationSpanData], tracing: ModelTracing, @@ -174,7 +174,7 @@ async def _fetch_response( input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], span: Span[GenerationSpanData], tracing: ModelTracing, @@ -187,7 +187,7 @@ async def _fetch_response( input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], span: Span[GenerationSpanData], tracing: ModelTracing, diff --git a/src/agents/models/openai_responses.py b/src/agents/models/openai_responses.py index ab4617d46..b751663da 100644 --- a/src/agents/models/openai_responses.py +++ b/src/agents/models/openai_responses.py @@ -18,7 +18,7 @@ ) from .. import _debug -from ..agent_output import AgentOutputSchema +from ..agent_output import AgentOutputSchemaBase from ..exceptions import UserError from ..handoffs import Handoff from ..items import ItemHelpers, ModelResponse, TResponseInputItem @@ -66,7 +66,7 @@ async def get_response( input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], tracing: ModelTracing, previous_response_id: str | None, @@ -131,7 +131,7 @@ async def stream_response( input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], tracing: ModelTracing, previous_response_id: str | None, @@ -182,7 +182,7 @@ async def _fetch_response( input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], previous_response_id: str | None, stream: Literal[True], @@ -195,7 +195,7 @@ async def _fetch_response( input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], previous_response_id: str | None, stream: Literal[False], @@ -207,7 +207,7 @@ async def _fetch_response( input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], previous_response_id: str | None, stream: Literal[True] | Literal[False] = False, @@ -307,7 +307,7 @@ def convert_tool_choice( @classmethod def get_response_format( - cls, output_schema: AgentOutputSchema | None + cls, output_schema: AgentOutputSchemaBase | None ) -> ResponseTextConfigParam | NotGiven: if output_schema is None or output_schema.is_plain_text(): return NOT_GIVEN @@ -317,7 +317,7 @@ def get_response_format( "type": "json_schema", "name": "final_output", "schema": output_schema.json_schema(), - "strict": output_schema.strict_json_schema, + "strict": output_schema.is_strict_json_schema(), } } diff --git a/src/agents/result.py b/src/agents/result.py index a2a6cc4a5..2996eaf91 100644 --- a/src/agents/result.py +++ b/src/agents/result.py @@ -10,7 +10,7 @@ from ._run_impl import QueueCompleteSentinel from .agent import Agent -from .agent_output import AgentOutputSchema +from .agent_output import AgentOutputSchemaBase from .exceptions import InputGuardrailTripwireTriggered, MaxTurnsExceeded from .guardrail import InputGuardrailResult, OutputGuardrailResult from .items import ItemHelpers, ModelResponse, RunItem, TResponseInputItem @@ -124,7 +124,7 @@ class RunResultStreaming(RunResultBase): final_output: Any """The final output of the agent. This is None until the agent has finished running.""" - _current_agent_output_schema: AgentOutputSchema | None = field(repr=False) + _current_agent_output_schema: AgentOutputSchemaBase | None = field(repr=False) _trace: Trace | None = field(repr=False) diff --git a/src/agents/run.py b/src/agents/run.py index e2b0dbceb..1fc7b52f1 100644 --- a/src/agents/run.py +++ b/src/agents/run.py @@ -19,7 +19,7 @@ get_model_tracing_impl, ) from .agent import Agent -from .agent_output import AgentOutputSchema +from .agent_output import AgentOutputSchema, AgentOutputSchemaBase from .exceptions import ( AgentsException, InputGuardrailTripwireTriggered, @@ -185,7 +185,7 @@ async def run( if current_span is None: handoff_names = [h.agent_name for h in cls._get_handoffs(current_agent)] if output_schema := cls._get_output_schema(current_agent): - output_type_name = output_schema.output_type_name() + output_type_name = output_schema.name() else: output_type_name = "str" @@ -517,7 +517,7 @@ async def _run_streamed_impl( if current_span is None: handoff_names = [h.agent_name for h in cls._get_handoffs(current_agent)] if output_schema := cls._get_output_schema(current_agent): - output_type_name = output_schema.output_type_name() + output_type_name = output_schema.name() else: output_type_name = "str" @@ -789,7 +789,7 @@ async def _get_single_step_result_from_response( original_input: str | list[TResponseInputItem], pre_step_items: list[RunItem], new_response: ModelResponse, - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], hooks: RunHooks[TContext], context_wrapper: RunContextWrapper[TContext], @@ -900,7 +900,7 @@ async def _get_new_response( agent: Agent[TContext], system_prompt: str | None, input: list[TResponseInputItem], - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, all_tools: list[Tool], handoffs: list[Handoff], context_wrapper: RunContextWrapper[TContext], @@ -930,9 +930,11 @@ async def _get_new_response( return new_response @classmethod - def _get_output_schema(cls, agent: Agent[Any]) -> AgentOutputSchema | None: + def _get_output_schema(cls, agent: Agent[Any]) -> AgentOutputSchemaBase | None: if agent.output_type is None or agent.output_type is str: return None + elif isinstance(agent.output_type, AgentOutputSchemaBase): + return agent.output_type return AgentOutputSchema(agent.output_type) diff --git a/tests/fake_model.py b/tests/fake_model.py index 52d3a3b2b..c6b3ba924 100644 --- a/tests/fake_model.py +++ b/tests/fake_model.py @@ -5,7 +5,7 @@ from openai.types.responses import Response, ResponseCompletedEvent -from agents.agent_output import AgentOutputSchema +from agents.agent_output import AgentOutputSchemaBase from agents.handoffs import Handoff from agents.items import ( ModelResponse, @@ -51,7 +51,7 @@ async def get_response( input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], tracing: ModelTracing, *, @@ -93,7 +93,7 @@ async def stream_response( input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], tracing: ModelTracing, *, diff --git a/tests/test_agent_config.py b/tests/test_agent_config.py index 44339dad3..f79c0cf8a 100644 --- a/tests/test_agent_config.py +++ b/tests/test_agent_config.py @@ -1,7 +1,7 @@ import pytest from pydantic import BaseModel -from agents import Agent, Handoff, RunContextWrapper, Runner, handoff +from agents import Agent, AgentOutputSchema, Handoff, RunContextWrapper, Runner, handoff @pytest.mark.asyncio @@ -160,8 +160,9 @@ async def test_agent_final_output(): ) schema = Runner._get_output_schema(agent) + assert isinstance(schema, AgentOutputSchema) assert schema is not None assert schema.output_type == Foo - assert schema.strict_json_schema is True + assert schema.is_strict_json_schema() is True assert schema.json_schema() is not None assert not schema.is_plain_text() diff --git a/tests/test_openai_chatcompletions_converter.py b/tests/test_openai_chatcompletions_converter.py index e3a18b255..bcfca5495 100644 --- a/tests/test_openai_chatcompletions_converter.py +++ b/tests/test_openai_chatcompletions_converter.py @@ -232,7 +232,7 @@ def test_convert_response_format_returns_not_given_for_plain_text_and_dict_for_s assert resp_format["type"] == "json_schema" assert resp_format["json_schema"]["name"] == "final_output" assert "strict" in resp_format["json_schema"] - assert resp_format["json_schema"]["strict"] == schema.strict_json_schema + assert resp_format["json_schema"]["strict"] == schema.is_strict_json_schema() assert "schema" in resp_format["json_schema"] assert resp_format["json_schema"]["schema"] == schema.json_schema() diff --git a/tests/test_openai_responses_converter.py b/tests/test_openai_responses_converter.py index 34cbac5c5..8e4866656 100644 --- a/tests/test_openai_responses_converter.py +++ b/tests/test_openai_responses_converter.py @@ -92,7 +92,7 @@ class OutModel(BaseModel): assert inner.get("name") == "final_output" assert isinstance(inner.get("schema"), dict) # Should include a strict flag matching the schema's strictness setting. - assert inner.get("strict") == out_schema.strict_json_schema + assert inner.get("strict") == out_schema.is_strict_json_schema() def test_convert_tools_basic_types_and_includes(): diff --git a/tests/test_output_tool.py b/tests/test_output_tool.py index 86c4b3b5a..37c1b1b67 100644 --- a/tests/test_output_tool.py +++ b/tests/test_output_tool.py @@ -1,10 +1,18 @@ import json +from typing import Any import pytest from pydantic import BaseModel from typing_extensions import TypedDict -from agents import Agent, AgentOutputSchema, ModelBehaviorError, Runner, UserError +from agents import ( + Agent, + AgentOutputSchema, + AgentOutputSchemaBase, + ModelBehaviorError, + Runner, + UserError, +) from agents.agent_output import _WRAPPER_DICT_KEY from agents.util import _json @@ -27,6 +35,7 @@ def test_structured_output_pydantic(): output_schema = Runner._get_output_schema(agent) assert output_schema, "Should have an output tool config with a structured output type" + assert isinstance(output_schema, AgentOutputSchema) assert output_schema.output_type == Foo, "Should have the correct output type" assert not output_schema._is_wrapped, "Pydantic objects should not be wrapped" for key, value in Foo.model_json_schema().items(): @@ -45,6 +54,7 @@ def test_structured_output_typed_dict(): agent = Agent(name="test", output_type=Bar) output_schema = Runner._get_output_schema(agent) assert output_schema, "Should have an output tool config with a structured output type" + assert isinstance(output_schema, AgentOutputSchema) assert output_schema.output_type == Bar, "Should have the correct output type" assert not output_schema._is_wrapped, "TypedDicts should not be wrapped" @@ -57,6 +67,7 @@ def test_structured_output_list(): agent = Agent(name="test", output_type=list[str]) output_schema = Runner._get_output_schema(agent) assert output_schema, "Should have an output tool config with a structured output type" + assert isinstance(output_schema, AgentOutputSchema) assert output_schema.output_type == list[str], "Should have the correct output type" assert output_schema._is_wrapped, "Lists should be wrapped" @@ -98,7 +109,7 @@ def test_plain_text_obj_doesnt_produce_schema(): def test_structured_output_is_strict(): output_wrapper = AgentOutputSchema(output_type=Foo) - assert output_wrapper.strict_json_schema + assert output_wrapper.is_strict_json_schema() for key, value in Foo.model_json_schema().items(): assert output_wrapper.json_schema()[key] == value @@ -110,6 +121,48 @@ def test_structured_output_is_strict(): def test_setting_strict_false_works(): output_wrapper = AgentOutputSchema(output_type=Foo, strict_json_schema=False) - assert not output_wrapper.strict_json_schema + assert not output_wrapper.is_strict_json_schema() assert output_wrapper.json_schema() == Foo.model_json_schema() assert output_wrapper.json_schema() == Foo.model_json_schema() + + +_CUSTOM_OUTPUT_SCHEMA_JSON_SCHEMA = { + "type": "object", + "properties": { + "foo": {"type": "string"}, + }, + "required": ["foo"], +} + + +class CustomOutputSchema(AgentOutputSchemaBase): + def is_plain_text(self) -> bool: + return False + + def name(self) -> str: + return "FooBarBaz" + + def json_schema(self) -> dict[str, Any]: + return _CUSTOM_OUTPUT_SCHEMA_JSON_SCHEMA + + def is_strict_json_schema(self) -> bool: + return False + + def validate_json(self, json_str: str) -> Any: + return ["some", "output"] + + +def test_custom_output_schema(): + custom_output_schema = CustomOutputSchema() + agent = Agent(name="test", output_type=custom_output_schema) + output_schema = Runner._get_output_schema(agent) + + assert output_schema, "Should have an output tool config with a structured output type" + assert isinstance(output_schema, CustomOutputSchema) + assert output_schema.json_schema() == _CUSTOM_OUTPUT_SCHEMA_JSON_SCHEMA + assert not output_schema.is_strict_json_schema() + assert not output_schema.is_plain_text() + + json_str = json.dumps({"foo": "bar"}) + validated = output_schema.validate_json(json_str) + assert validated == ["some", "output"] diff --git a/tests/voice/test_workflow.py b/tests/voice/test_workflow.py index 72a3370d3..2bdf2a657 100644 --- a/tests/voice/test_workflow.py +++ b/tests/voice/test_workflow.py @@ -9,7 +9,7 @@ from openai.types.responses.response_text_delta_event import ResponseTextDeltaEvent from agents import Agent, Model, ModelSettings, ModelTracing, Tool -from agents.agent_output import AgentOutputSchema +from agents.agent_output import AgentOutputSchemaBase from agents.handoffs import Handoff from agents.items import ( ModelResponse, @@ -48,7 +48,7 @@ async def get_response( input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], tracing: ModelTracing, *, @@ -62,7 +62,7 @@ async def stream_response( input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], tracing: ModelTracing, *, From 616d8e7f4b1d1b806e2ef903209d968d00cc425b Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Mon, 21 Apr 2025 13:08:38 -0400 Subject: [PATCH 03/35] Start and finish streaming trace in impl metod (#540) Closes #435 and closes #538. Unit tests. --- .gitignore | 3 ++- pyproject.toml | 1 + src/agents/result.py | 5 +---- src/agents/run.py | 11 ++++----- tests/fastapi/__init__.py | 0 tests/fastapi/streaming_app.py | 30 +++++++++++++++++++++++++ tests/fastapi/test_streaming_context.py | 29 ++++++++++++++++++++++++ uv.lock | 19 +++++++++++++++- 8 files changed, 87 insertions(+), 11 deletions(-) create mode 100644 tests/fastapi/__init__.py create mode 100644 tests/fastapi/streaming_app.py create mode 100644 tests/fastapi/test_streaming_context.py diff --git a/.gitignore b/.gitignore index 7dd22b88d..2e9b92379 100644 --- a/.gitignore +++ b/.gitignore @@ -141,4 +141,5 @@ cython_debug/ .ruff_cache/ # PyPI configuration file -.pypirc \ No newline at end of file +.pypirc +.aider* diff --git a/pyproject.toml b/pyproject.toml index f94eb8593..c06a8416b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,6 +61,7 @@ dev = [ "graphviz", "mkdocs-static-i18n>=1.3.0", "eval-type-backport>=0.2.2", + "fastapi >= 0.110.0, <1", ] [tool.uv.workspace] diff --git a/src/agents/result.py b/src/agents/result.py index 2996eaf91..0d8372c86 100644 --- a/src/agents/result.py +++ b/src/agents/result.py @@ -126,7 +126,7 @@ class RunResultStreaming(RunResultBase): _current_agent_output_schema: AgentOutputSchemaBase | None = field(repr=False) - _trace: Trace | None = field(repr=False) + trace: Trace | None = field(repr=False) is_complete: bool = False """Whether the agent has finished running.""" @@ -185,9 +185,6 @@ async def stream_events(self) -> AsyncIterator[StreamEvent]: yield item self._event_queue.task_done() - if self._trace: - self._trace.finish(reset_current=True) - self._cleanup_tasks() if self._stored_exception: diff --git a/src/agents/run.py b/src/agents/run.py index 1fc7b52f1..38e11152f 100644 --- a/src/agents/run.py +++ b/src/agents/run.py @@ -404,10 +404,6 @@ def run_streamed( disabled=run_config.tracing_disabled, ) ) - # Need to start the trace here, because the current trace contextvar is captured at - # asyncio.create_task time - if new_trace: - new_trace.start(mark_as_current=True) output_schema = cls._get_output_schema(starting_agent) context_wrapper: RunContextWrapper[TContext] = RunContextWrapper( @@ -426,7 +422,7 @@ def run_streamed( input_guardrail_results=[], output_guardrail_results=[], _current_agent_output_schema=output_schema, - _trace=new_trace, + trace=new_trace, ) # Kick off the actual agent loop in the background and return the streamed result object. @@ -499,6 +495,9 @@ async def _run_streamed_impl( run_config: RunConfig, previous_response_id: str | None, ): + if streamed_result.trace: + streamed_result.trace.start(mark_as_current=True) + current_span: Span[AgentSpanData] | None = None current_agent = starting_agent current_turn = 0 @@ -625,6 +624,8 @@ async def _run_streamed_impl( finally: if current_span: current_span.finish(reset_current=True) + if streamed_result.trace: + streamed_result.trace.finish(reset_current=True) @classmethod async def _run_single_turn_streamed( diff --git a/tests/fastapi/__init__.py b/tests/fastapi/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/fastapi/streaming_app.py b/tests/fastapi/streaming_app.py new file mode 100644 index 000000000..b93ccf3f3 --- /dev/null +++ b/tests/fastapi/streaming_app.py @@ -0,0 +1,30 @@ +from collections.abc import AsyncIterator + +from fastapi import FastAPI +from starlette.responses import StreamingResponse + +from agents import Agent, Runner, RunResultStreaming + +agent = Agent( + name="Assistant", + instructions="You are a helpful assistant.", +) + + +app = FastAPI() + + +@app.post("/stream") +async def stream(): + result = Runner.run_streamed(agent, input="Tell me a joke") + stream_handler = StreamHandler(result) + return StreamingResponse(stream_handler.stream_events(), media_type="application/x-ndjson") + + +class StreamHandler: + def __init__(self, result: RunResultStreaming): + self.result = result + + async def stream_events(self) -> AsyncIterator[str]: + async for event in self.result.stream_events(): + yield f"{event.type}\n\n" diff --git a/tests/fastapi/test_streaming_context.py b/tests/fastapi/test_streaming_context.py new file mode 100644 index 000000000..ee13045e4 --- /dev/null +++ b/tests/fastapi/test_streaming_context.py @@ -0,0 +1,29 @@ +import pytest +from httpx import ASGITransport, AsyncClient +from inline_snapshot import snapshot + +from ..fake_model import FakeModel +from ..test_responses import get_text_message +from .streaming_app import agent, app + + +@pytest.mark.asyncio +async def test_streaming_context(): + """This ensures that FastAPI streaming works. The context for this test is that the Runner + method was called in one async context, and the streaming was ended in another context, + leading to a tracing error because the context was closed in the wrong context. This test + ensures that this actually works. + """ + model = FakeModel() + agent.model = model + model.set_next_output([get_text_message("done")]) + + transport = ASGITransport(app) + async with AsyncClient(transport=transport, base_url="http://test") as ac: + async with ac.stream("POST", "/stream") as r: + assert r.status_code == 200 + body = (await r.aread()).decode("utf-8") + lines = [line for line in body.splitlines() if line] + assert lines == snapshot( + ["agent_updated_stream_event", "raw_response_event", "run_item_stream_event"] + ) diff --git a/uv.lock b/uv.lock index 24d089a50..3ee65d21a 100644 --- a/uv.lock +++ b/uv.lock @@ -483,6 +483,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa", size = 26702 }, ] +[[package]] +name = "fastapi" +version = "0.115.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/55/ae499352d82338331ca1e28c7f4a63bfd09479b16395dce38cf50a39e2c2/fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", size = 295236 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/b3/b51f09c2ba432a576fe63758bddc81f78f0c6309d9e5c10d194313bf021e/fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d", size = 95164 }, +] + [[package]] name = "filelock" version = "3.18.0" @@ -1496,6 +1510,7 @@ voice = [ dev = [ { name = "coverage" }, { name = "eval-type-backport" }, + { name = "fastapi" }, { name = "graphviz" }, { name = "inline-snapshot" }, { name = "mkdocs" }, @@ -1536,6 +1551,7 @@ provides-extras = ["voice", "viz", "litellm"] dev = [ { name = "coverage", specifier = ">=7.6.12" }, { name = "eval-type-backport", specifier = ">=0.2.2" }, + { name = "fastapi", specifier = ">=0.110.0,<1" }, { name = "graphviz" }, { name = "inline-snapshot", specifier = ">=0.20.7" }, { name = "mkdocs", specifier = ">=1.6.0" }, @@ -2474,7 +2490,8 @@ name = "starlette" version = "0.46.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio", marker = "python_full_version >= '3.10'" }, + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846 } wheels = [ From 0a3dfa071a0853a115bac067cb18bd6a803d087e Mon Sep 17 00:00:00 2001 From: Yuya Haruna <121998910+yuya-haruna@users.noreply.github.com> Date: Tue, 22 Apr 2025 02:47:51 +0900 Subject: [PATCH 04/35] Fix visualize graph filename to without extension. (#554) Only the file name is needed since graphviz's `render()` automatically adds the file extension. Also, unnecessary .gv (.dot) files are output, so the `cleanup=True` option has been modified to prevent them from being saved. Here is a similar modification, but in a different content. - https://github.com/openai/openai-agents-python/pull/451 --- docs/ja/visualization.md | 4 ++-- docs/visualization.md | 4 +--- src/agents/extensions/visualization.py | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/ja/visualization.md b/docs/ja/visualization.md index 3c5c64ece..9093bb659 100644 --- a/docs/ja/visualization.md +++ b/docs/ja/visualization.md @@ -81,7 +81,7 @@ draw_graph(triage_agent).view() デフォルトでは、`draw_graph` はグラフをインラインで表示します。ファイルとして保存するには、ファイル名を指定します: ```python -draw_graph(triage_agent, filename="agent_graph.png") +draw_graph(triage_agent, filename="agent_graph") ``` -これにより、作業ディレクトリに `agent_graph.png` が生成されます。 \ No newline at end of file +これにより、作業ディレクトリに `agent_graph.png` が生成されます。 diff --git a/docs/visualization.md b/docs/visualization.md index 00f3126db..409803f76 100644 --- a/docs/visualization.md +++ b/docs/visualization.md @@ -78,9 +78,7 @@ draw_graph(triage_agent).view() By default, `draw_graph` displays the graph inline. To save it as a file, specify a filename: ```python -draw_graph(triage_agent, filename="agent_graph.png") +draw_graph(triage_agent, filename="agent_graph") ``` This will generate `agent_graph.png` in the working directory. - - diff --git a/src/agents/extensions/visualization.py b/src/agents/extensions/visualization.py index 5fb350626..888e262c3 100644 --- a/src/agents/extensions/visualization.py +++ b/src/agents/extensions/visualization.py @@ -132,6 +132,6 @@ def draw_graph(agent: Agent, filename: Optional[str] = None) -> graphviz.Source: graph = graphviz.Source(dot_code) if filename: - graph.render(filename, format="png") + graph.render(filename, format="png", cleanup=True) return graph From a0254b0b74180f931c25c617a2548dac0f8aebab Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Mon, 21 Apr 2025 15:03:06 -0400 Subject: [PATCH 05/35] RFC: automatically use litellm if possible (#534) ## Summary This replaces the default model provider with a `MultiProvider`, which has the logic: - if the model name starts with `openai/` or doesn't contain "/", use OpenAI - if the model name starts with `litellm/`, use LiteLLM to use the appropriate model provider. It's also extensible, so users can create their own mappings. I also imagine that if we natively supported Anthropic/Gemini etc, we can add it to MultiProvider to make it work. The goal is that it should be really easy to use any model provider. Today if you pass `model="gpt-4.1"`, it works great. But `model="claude-sonnet-3.7"` doesn't. If we can make it that easy, it's a win for devx. I'm not entirely sure if this is a good idea - is it too magical? Is the API too reliant on litellm? Comments welcome. ## Test plan For now, the example. Will add unit tests if we agree its worth mergin. --------- Co-authored-by: Steven Heidel --- examples/model_providers/litellm_auto.py | 41 +++++ .../extensions/models/litellm_provider.py | 21 +++ src/agents/models/multi_provider.py | 144 ++++++++++++++++++ src/agents/run.py | 4 +- 4 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 examples/model_providers/litellm_auto.py create mode 100644 src/agents/extensions/models/litellm_provider.py create mode 100644 src/agents/models/multi_provider.py diff --git a/examples/model_providers/litellm_auto.py b/examples/model_providers/litellm_auto.py new file mode 100644 index 000000000..12b1e8914 --- /dev/null +++ b/examples/model_providers/litellm_auto.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +import asyncio + +from agents import Agent, Runner, function_tool, set_tracing_disabled + +"""This example uses the built-in support for LiteLLM. To use this, ensure you have the +ANTHROPIC_API_KEY environment variable set. +""" + +set_tracing_disabled(disabled=True) + + +@function_tool +def get_weather(city: str): + print(f"[debug] getting weather for {city}") + return f"The weather in {city} is sunny." + + +async def main(): + agent = Agent( + name="Assistant", + instructions="You only respond in haikus.", + # We prefix with litellm/ to tell the Runner to use the LitellmModel + model="litellm/anthropic/claude-3-5-sonnet-20240620", + tools=[get_weather], + ) + + result = await Runner.run(agent, "What's the weather in Tokyo?") + print(result.final_output) + + +if __name__ == "__main__": + import os + + if os.getenv("ANTHROPIC_API_KEY") is None: + raise ValueError( + "ANTHROPIC_API_KEY is not set. Please set it the environment variable and try again." + ) + + asyncio.run(main()) diff --git a/src/agents/extensions/models/litellm_provider.py b/src/agents/extensions/models/litellm_provider.py new file mode 100644 index 000000000..5a2dc1666 --- /dev/null +++ b/src/agents/extensions/models/litellm_provider.py @@ -0,0 +1,21 @@ +from ...models.interface import Model, ModelProvider +from .litellm_model import LitellmModel + +DEFAULT_MODEL: str = "gpt-4.1" + + +class LitellmProvider(ModelProvider): + """A ModelProvider that uses LiteLLM to route to any model provider. You can use it via: + ```python + Runner.run(agent, input, run_config=RunConfig(model_provider=LitellmProvider())) + ``` + See supported models here: [litellm models](https://docs.litellm.ai/docs/providers). + + NOTE: API keys must be set via environment variables. If you're using models that require + additional configuration (e.g. Azure API base or version), those must also be set via the + environment variables that LiteLLM expects. If you have more advanced needs, we recommend + copy-pasting this class and making any modifications you need. + """ + + def get_model(self, model_name: str | None) -> Model: + return LitellmModel(model_name or DEFAULT_MODEL) diff --git a/src/agents/models/multi_provider.py b/src/agents/models/multi_provider.py new file mode 100644 index 000000000..d075ac9b6 --- /dev/null +++ b/src/agents/models/multi_provider.py @@ -0,0 +1,144 @@ +from __future__ import annotations + +from openai import AsyncOpenAI + +from ..exceptions import UserError +from .interface import Model, ModelProvider +from .openai_provider import OpenAIProvider + + +class MultiProviderMap: + """A map of model name prefixes to ModelProviders.""" + + def __init__(self): + self._mapping: dict[str, ModelProvider] = {} + + def has_prefix(self, prefix: str) -> bool: + """Returns True if the given prefix is in the mapping.""" + return prefix in self._mapping + + def get_mapping(self) -> dict[str, ModelProvider]: + """Returns a copy of the current prefix -> ModelProvider mapping.""" + return self._mapping.copy() + + def set_mapping(self, mapping: dict[str, ModelProvider]): + """Overwrites the current mapping with a new one.""" + self._mapping = mapping + + def get_provider(self, prefix: str) -> ModelProvider | None: + """Returns the ModelProvider for the given prefix. + + Args: + prefix: The prefix of the model name e.g. "openai" or "my_prefix". + """ + return self._mapping.get(prefix) + + def add_provider(self, prefix: str, provider: ModelProvider): + """Adds a new prefix -> ModelProvider mapping. + + Args: + prefix: The prefix of the model name e.g. "openai" or "my_prefix". + provider: The ModelProvider to use for the given prefix. + """ + self._mapping[prefix] = provider + + def remove_provider(self, prefix: str): + """Removes the mapping for the given prefix. + + Args: + prefix: The prefix of the model name e.g. "openai" or "my_prefix". + """ + del self._mapping[prefix] + + +class MultiProvider(ModelProvider): + """This ModelProvider maps to a Model based on the prefix of the model name. By default, the + mapping is: + - "openai/" prefix or no prefix -> OpenAIProvider. e.g. "openai/gpt-4.1", "gpt-4.1" + - "litellm/" prefix -> LitellmProvider. e.g. "litellm/openai/gpt-4.1" + + You can override or customize this mapping. + """ + + def __init__( + self, + *, + provider_map: MultiProviderMap | None = None, + openai_api_key: str | None = None, + openai_base_url: str | None = None, + openai_client: AsyncOpenAI | None = None, + openai_organization: str | None = None, + openai_project: str | None = None, + openai_use_responses: bool | None = None, + ) -> None: + """Create a new OpenAI provider. + + Args: + provider_map: A MultiProviderMap that maps prefixes to ModelProviders. If not provided, + we will use a default mapping. See the documentation for this class to see the + default mapping. + openai_api_key: The API key to use for the OpenAI provider. If not provided, we will use + the default API key. + openai_base_url: The base URL to use for the OpenAI provider. If not provided, we will + use the default base URL. + openai_client: An optional OpenAI client to use. If not provided, we will create a new + OpenAI client using the api_key and base_url. + openai_organization: The organization to use for the OpenAI provider. + openai_project: The project to use for the OpenAI provider. + openai_use_responses: Whether to use the OpenAI responses API. + """ + self.provider_map = provider_map + self.openai_provider = OpenAIProvider( + api_key=openai_api_key, + base_url=openai_base_url, + openai_client=openai_client, + organization=openai_organization, + project=openai_project, + use_responses=openai_use_responses, + ) + + self._fallback_providers: dict[str, ModelProvider] = {} + + def _get_prefix_and_model_name(self, model_name: str | None) -> tuple[str | None, str | None]: + if model_name is None: + return None, None + elif "/" in model_name: + prefix, model_name = model_name.split("/", 1) + return prefix, model_name + else: + return None, model_name + + def _create_fallback_provider(self, prefix: str) -> ModelProvider: + if prefix == "litellm": + from ..extensions.models.litellm_provider import LitellmProvider + + return LitellmProvider() + else: + raise UserError(f"Unknown prefix: {prefix}") + + def _get_fallback_provider(self, prefix: str | None) -> ModelProvider: + if prefix is None or prefix == "openai": + return self.openai_provider + elif prefix in self._fallback_providers: + return self._fallback_providers[prefix] + else: + self._fallback_providers[prefix] = self._create_fallback_provider(prefix) + return self._fallback_providers[prefix] + + def get_model(self, model_name: str | None) -> Model: + """Returns a Model based on the model name. The model name can have a prefix, ending with + a "/", which will be used to look up the ModelProvider. If there is no prefix, we will use + the OpenAI provider. + + Args: + model_name: The name of the model to get. + + Returns: + A Model. + """ + prefix, model_name = self._get_prefix_and_model_name(model_name) + + if prefix and self.provider_map and (provider := self.provider_map.get_provider(prefix)): + return provider.get_model(model_name) + else: + return self._get_fallback_provider(prefix).get_model(model_name) diff --git a/src/agents/run.py b/src/agents/run.py index 38e11152f..2af558d58 100644 --- a/src/agents/run.py +++ b/src/agents/run.py @@ -34,7 +34,7 @@ from .logger import logger from .model_settings import ModelSettings from .models.interface import Model, ModelProvider -from .models.openai_provider import OpenAIProvider +from .models.multi_provider import MultiProvider from .result import RunResult, RunResultStreaming from .run_context import RunContextWrapper, TContext from .stream_events import AgentUpdatedStreamEvent, RawResponsesStreamEvent @@ -56,7 +56,7 @@ class RunConfig: agent. The model_provider passed in below must be able to resolve this model name. """ - model_provider: ModelProvider = field(default_factory=OpenAIProvider) + model_provider: ModelProvider = field(default_factory=MultiProvider) """The model provider to use when looking up string model names. Defaults to OpenAI.""" model_settings: ModelSettings | None = None From 942ba9840ba2bedf3726570f33049946ec4849a1 Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Mon, 21 Apr 2025 16:59:01 -0400 Subject: [PATCH 06/35] Docs and tests for litellm (#561) --- README.md | 4 +-- docs/models/index.md | 55 +++++++++++++++++++++++++++++----------- tests/models/__init__.py | 0 tests/models/conftest.py | 11 ++++++++ tests/models/test_map.py | 20 +++++++++++++++ 5 files changed, 72 insertions(+), 18 deletions(-) create mode 100644 tests/models/__init__.py create mode 100644 tests/models/conftest.py create mode 100644 tests/models/test_map.py diff --git a/README.md b/README.md index bbd4a5a05..7dcd97b33 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # OpenAI Agents SDK -The OpenAI Agents SDK is a lightweight yet powerful framework for building multi-agent workflows. +The OpenAI Agents SDK is a lightweight yet powerful framework for building multi-agent workflows. It is provider-agnostic, supporting the OpenAI Responses and Chat Completions APIs, as well as 100+ other LLMs. Image of the Agents Tracing UI @@ -13,8 +13,6 @@ The OpenAI Agents SDK is a lightweight yet powerful framework for building multi Explore the [examples](examples) directory to see the SDK in action, and read our [documentation](https://openai.github.io/openai-agents-python/) for more details. -Notably, our SDK [is compatible](https://openai.github.io/openai-agents-python/models/) with any model providers that support the OpenAI Chat Completions API format. - ## Get started 1. Set up your Python environment diff --git a/docs/models/index.md b/docs/models/index.md index 4cf4f6439..1c89d778a 100644 --- a/docs/models/index.md +++ b/docs/models/index.md @@ -5,11 +5,40 @@ The Agents SDK comes with out-of-the-box support for OpenAI models in two flavor - **Recommended**: the [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel], which calls OpenAI APIs using the new [Responses API](https://platform.openai.com/docs/api-reference/responses). - The [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel], which calls OpenAI APIs using the [Chat Completions API](https://platform.openai.com/docs/api-reference/chat). +## Non-OpenAI models + +You can use most other non-OpenAI models via the [LiteLLM integration](./litellm.md). First, install the litellm dependency group: + +```bash +pip install "openai-agents[litellm]" +``` + +Then, use any of the [supported models](https://docs.litellm.ai/docs/providers) with the `litellm/` prefix: + +```python +claude_agent = Agent(model="litellm/anthropic/claude-3-5-sonnet-20240620", ...) +gemini_agent = Agent(model="litellm/gemini/gemini-2.5-flash-preview-04-17", ...) +``` + +### Other ways to use non-OpenAI models + +You can integrate other LLM providers in 3 more ways (examples [here](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/)): + +1. [`set_default_openai_client`][agents.set_default_openai_client] is useful in cases where you want to globally use an instance of `AsyncOpenAI` as the LLM client. This is for cases where the LLM provider has an OpenAI compatible API endpoint, and you can set the `base_url` and `api_key`. See a configurable example in [examples/model_providers/custom_example_global.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_global.py). +2. [`ModelProvider`][agents.models.interface.ModelProvider] is at the `Runner.run` level. This lets you say "use a custom model provider for all agents in this run". See a configurable example in [examples/model_providers/custom_example_provider.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_provider.py). +3. [`Agent.model`][agents.agent.Agent.model] lets you specify the model on a specific Agent instance. This enables you to mix and match different providers for different agents. See a configurable example in [examples/model_providers/custom_example_agent.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_agent.py). An easy way to use most available models is via the [LiteLLM integration](./litellm.md). + +In cases where you do not have an API key from `platform.openai.com`, we recommend disabling tracing via `set_tracing_disabled()`, or setting up a [different tracing processor](../tracing.md). + +!!! note + + In these examples, we use the Chat Completions API/model, because most LLM providers don't yet support the Responses API. If your LLM provider does support it, we recommend using Responses. + ## Mixing and matching models Within a single workflow, you may want to use different models for each agent. For example, you could use a smaller, faster model for triage, while using a larger, more capable model for complex tasks. When configuring an [`Agent`][agents.Agent], you can select a specific model by either: -1. Passing the name of an OpenAI model. +1. Passing the name of a model. 2. Passing any model name + a [`ModelProvider`][agents.models.interface.ModelProvider] that can map that name to a Model instance. 3. Directly providing a [`Model`][agents.models.interface.Model] implementation. @@ -64,20 +93,6 @@ english_agent = Agent( ) ``` -## Using other LLM providers - -You can use other LLM providers in 3 ways (examples [here](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/)): - -1. [`set_default_openai_client`][agents.set_default_openai_client] is useful in cases where you want to globally use an instance of `AsyncOpenAI` as the LLM client. This is for cases where the LLM provider has an OpenAI compatible API endpoint, and you can set the `base_url` and `api_key`. See a configurable example in [examples/model_providers/custom_example_global.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_global.py). -2. [`ModelProvider`][agents.models.interface.ModelProvider] is at the `Runner.run` level. This lets you say "use a custom model provider for all agents in this run". See a configurable example in [examples/model_providers/custom_example_provider.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_provider.py). -3. [`Agent.model`][agents.agent.Agent.model] lets you specify the model on a specific Agent instance. This enables you to mix and match different providers for different agents. See a configurable example in [examples/model_providers/custom_example_agent.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_agent.py). An easy way to use most available models is via the [LiteLLM integration](./litellm.md). - -In cases where you do not have an API key from `platform.openai.com`, we recommend disabling tracing via `set_tracing_disabled()`, or setting up a [different tracing processor](../tracing.md). - -!!! note - - In these examples, we use the Chat Completions API/model, because most LLM providers don't yet support the Responses API. If your LLM provider does support it, we recommend using Responses. - ## Common issues with using other LLM providers ### Tracing client error 401 @@ -100,7 +115,17 @@ The SDK uses the Responses API by default, but most other LLM providers don't ye Some model providers don't have support for [structured outputs](https://platform.openai.com/docs/guides/structured-outputs). This sometimes results in an error that looks something like this: ``` + BadRequestError: Error code: 400 - {'error': {'message': "'response_format.type' : value is not one of the allowed values ['text','json_object']", 'type': 'invalid_request_error'}} + ``` This is a shortcoming of some model providers - they support JSON outputs, but don't allow you to specify the `json_schema` to use for the output. We are working on a fix for this, but we suggest relying on providers that do have support for JSON schema output, because otherwise your app will often break because of malformed JSON. + +## Mixing models across providers + +You need to be aware of feature differences between model providers, or you may run into errors. For example, OpenAI supports structured outputs, multimodal input, and hosted file search and web search, but many other providers don't support these features. Be aware of these limitations: + +- Don't send unsupported `tools` to providers that don't understand them +- Filter out multimodal inputs before calling models that are text-only +- Be aware that providers that don't support structured JSON outputs will occasionally produce invalid JSON. diff --git a/tests/models/__init__.py b/tests/models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/models/conftest.py b/tests/models/conftest.py new file mode 100644 index 000000000..79d85d8b4 --- /dev/null +++ b/tests/models/conftest.py @@ -0,0 +1,11 @@ +import os +import sys + + +# Skip voice tests on Python 3.9 +def pytest_ignore_collect(collection_path, config): + if sys.version_info[:2] == (3, 9): + this_dir = os.path.dirname(__file__) + + if str(collection_path).startswith(this_dir): + return True diff --git a/tests/models/test_map.py b/tests/models/test_map.py new file mode 100644 index 000000000..6b65fc094 --- /dev/null +++ b/tests/models/test_map.py @@ -0,0 +1,20 @@ +from agents import Agent, OpenAIResponsesModel, RunConfig, Runner +from agents.extensions.models.litellm_model import LitellmModel + + +def test_no_prefix_is_openai(): + agent = Agent(model="gpt-4o", instructions="", name="test") + model = Runner._get_model(agent, RunConfig()) + assert isinstance(model, OpenAIResponsesModel) + + +def openai_prefix_is_openai(): + agent = Agent(model="openai/gpt-4o", instructions="", name="test") + model = Runner._get_model(agent, RunConfig()) + assert isinstance(model, OpenAIResponsesModel) + + +def test_litellm_prefix_is_litellm(): + agent = Agent(model="litellm/foo/bar", instructions="", name="test") + model = Runner._get_model(agent, RunConfig()) + assert isinstance(model, LitellmModel) From 2bdf9b7b1121d5a31f500f5e38ac87e195d1de32 Mon Sep 17 00:00:00 2001 From: Steven Heidel Date: Mon, 21 Apr 2025 13:59:46 -0700 Subject: [PATCH 07/35] Pass through organization/project headers to tracing backend, fix speech_group enum (#562) --- src/agents/mcp/server.py | 3 ++- src/agents/tracing/processors.py | 6 ++++++ src/agents/tracing/span_data.py | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/agents/mcp/server.py b/src/agents/mcp/server.py index e70d7ce69..9a137bbdd 100644 --- a/src/agents/mcp/server.py +++ b/src/agents/mcp/server.py @@ -137,9 +137,10 @@ async def cleanup(self): async with self._cleanup_lock: try: await self.exit_stack.aclose() - self.session = None except Exception as e: logger.error(f"Error cleaning up server: {e}") + finally: + self.session = None class MCPServerStdioParams(TypedDict): diff --git a/src/agents/tracing/processors.py b/src/agents/tracing/processors.py index f929d05da..2913b11a4 100644 --- a/src/agents/tracing/processors.py +++ b/src/agents/tracing/processors.py @@ -102,6 +102,12 @@ def export(self, items: list[Trace | Span[Any]]) -> None: "OpenAI-Beta": "traces=v1", } + if self.organization: + headers["OpenAI-Organization"] = self.organization + + if self.project: + headers["OpenAI-Project"] = self.project + # Exponential backoff loop attempt = 0 delay = self.base_delay diff --git a/src/agents/tracing/span_data.py b/src/agents/tracing/span_data.py index 1a4502803..cb3e8491d 100644 --- a/src/agents/tracing/span_data.py +++ b/src/agents/tracing/span_data.py @@ -338,7 +338,7 @@ def __init__( @property def type(self) -> str: - return "speech-group" + return "speech_group" def export(self) -> dict[str, Any]: return { From 83ce49ec7e065a9688783341507b21af6a919de9 Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Mon, 21 Apr 2025 23:06:09 -0400 Subject: [PATCH 08/35] v0.0.12 (#564) --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c06a8416b..eeeb6d3d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "openai-agents" -version = "0.0.11" +version = "0.0.12" description = "OpenAI Agents SDK" readme = "README.md" requires-python = ">=3.9" diff --git a/uv.lock b/uv.lock index 3ee65d21a..3a737cf37 100644 --- a/uv.lock +++ b/uv.lock @@ -1482,7 +1482,7 @@ wheels = [ [[package]] name = "openai-agents" -version = "0.0.11" +version = "0.0.12" source = { editable = "." } dependencies = [ { name = "griffe" }, From 111fc9ee66c5f6f4af3ba6faaedba4789da3d03a Mon Sep 17 00:00:00 2001 From: Jonny Kalambay Date: Tue, 22 Apr 2025 19:26:47 -0700 Subject: [PATCH 09/35] Adding extra_headers parameters to ModelSettings (#550) --- src/agents/extensions/models/litellm_model.py | 2 +- src/agents/model_settings.py | 6 +- src/agents/models/openai_chatcompletions.py | 2 +- src/agents/models/openai_responses.py | 2 +- tests/test_extra_headers.py | 92 +++++++++++++++++++ 5 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 tests/test_extra_headers.py diff --git a/src/agents/extensions/models/litellm_model.py b/src/agents/extensions/models/litellm_model.py index e939ee8da..f5e7752fa 100644 --- a/src/agents/extensions/models/litellm_model.py +++ b/src/agents/extensions/models/litellm_model.py @@ -286,7 +286,7 @@ async def _fetch_response( stream=stream, stream_options=stream_options, reasoning_effort=reasoning_effort, - extra_headers=HEADERS, + extra_headers={**HEADERS, **(model_settings.extra_headers or {})}, api_key=self.api_key, base_url=self.base_url, **extra_kwargs, diff --git a/src/agents/model_settings.py b/src/agents/model_settings.py index ed9a01318..fee92b4e0 100644 --- a/src/agents/model_settings.py +++ b/src/agents/model_settings.py @@ -3,7 +3,7 @@ from dataclasses import dataclass, fields, replace from typing import Literal -from openai._types import Body, Query +from openai._types import Body, Headers, Query from openai.types.shared import Reasoning @@ -67,6 +67,10 @@ class ModelSettings: """Additional body fields to provide with the request. Defaults to None if not provided.""" + extra_headers: Headers | None = None + """Additional headers to provide with the request. + Defaults to None if not provided.""" + def resolve(self, override: ModelSettings | None) -> ModelSettings: """Produce a new ModelSettings by overlaying any non-None values from the override on top of this instance.""" diff --git a/src/agents/models/openai_chatcompletions.py b/src/agents/models/openai_chatcompletions.py index 9fd102690..15bf19cb3 100644 --- a/src/agents/models/openai_chatcompletions.py +++ b/src/agents/models/openai_chatcompletions.py @@ -255,7 +255,7 @@ async def _fetch_response( stream_options=self._non_null_or_not_given(stream_options), store=self._non_null_or_not_given(store), reasoning_effort=self._non_null_or_not_given(reasoning_effort), - extra_headers=HEADERS, + extra_headers={ **HEADERS, **(model_settings.extra_headers or {}) }, extra_query=model_settings.extra_query, extra_body=model_settings.extra_body, metadata=self._non_null_or_not_given(model_settings.metadata), diff --git a/src/agents/models/openai_responses.py b/src/agents/models/openai_responses.py index b751663da..c1ff85b98 100644 --- a/src/agents/models/openai_responses.py +++ b/src/agents/models/openai_responses.py @@ -253,7 +253,7 @@ async def _fetch_response( tool_choice=tool_choice, parallel_tool_calls=parallel_tool_calls, stream=stream, - extra_headers=_HEADERS, + extra_headers={**_HEADERS, **(model_settings.extra_headers or {})}, extra_query=model_settings.extra_query, extra_body=model_settings.extra_body, text=response_format, diff --git a/tests/test_extra_headers.py b/tests/test_extra_headers.py new file mode 100644 index 000000000..f29c25408 --- /dev/null +++ b/tests/test_extra_headers.py @@ -0,0 +1,92 @@ +import pytest +from openai.types.chat.chat_completion import ChatCompletion, Choice +from openai.types.chat.chat_completion_message import ChatCompletionMessage + +from agents import ModelSettings, ModelTracing, OpenAIChatCompletionsModel, OpenAIResponsesModel + + +@pytest.mark.allow_call_model_methods +@pytest.mark.asyncio +async def test_extra_headers_passed_to_openai_responses_model(): + """ + Ensure extra_headers in ModelSettings is passed to the OpenAIResponsesModel client. + """ + called_kwargs = {} + + class DummyResponses: + async def create(self, **kwargs): + nonlocal called_kwargs + called_kwargs = kwargs + class DummyResponse: + id = "dummy" + output = [] + usage = type( + "Usage", (), {"input_tokens": 0, "output_tokens": 0, "total_tokens": 0} + )() + return DummyResponse() + + class DummyClient: + def __init__(self): + self.responses = DummyResponses() + + + + model = OpenAIResponsesModel(model="gpt-4", openai_client=DummyClient()) # type: ignore + extra_headers = {"X-Test-Header": "test-value"} + await model.get_response( + system_instructions=None, + input="hi", + model_settings=ModelSettings(extra_headers=extra_headers), + tools=[], + output_schema=None, + handoffs=[], + tracing=ModelTracing.DISABLED, + previous_response_id=None, + ) + assert "extra_headers" in called_kwargs + assert called_kwargs["extra_headers"]["X-Test-Header"] == "test-value" + + + +@pytest.mark.allow_call_model_methods +@pytest.mark.asyncio +async def test_extra_headers_passed_to_openai_client(): + """ + Ensure extra_headers in ModelSettings is passed to the OpenAI client. + """ + called_kwargs = {} + + class DummyCompletions: + async def create(self, **kwargs): + nonlocal called_kwargs + called_kwargs = kwargs + msg = ChatCompletionMessage(role="assistant", content="Hello") + choice = Choice(index=0, finish_reason="stop", message=msg) + return ChatCompletion( + id="resp-id", + created=0, + model="fake", + object="chat.completion", + choices=[choice], + usage=None, + ) + + class DummyClient: + def __init__(self): + self.chat = type("_Chat", (), {"completions": DummyCompletions()})() + self.base_url = "https://api.openai.com" + + model = OpenAIChatCompletionsModel(model="gpt-4", openai_client=DummyClient()) # type: ignore + extra_headers = {"X-Test-Header": "test-value"} + await model.get_response( + system_instructions=None, + input="hi", + model_settings=ModelSettings(extra_headers=extra_headers), + tools=[], + output_schema=None, + handoffs=[], + tracing=ModelTracing.DISABLED, + previous_response_id=None, + ) + assert "extra_headers" in called_kwargs + assert called_kwargs["extra_headers"]["X-Test-Header"] == "test-value" From 178020ea33980e5873a82dc715e79f0c6a285623 Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Wed, 23 Apr 2025 11:29:12 +0900 Subject: [PATCH 10/35] Examples: Fix financial_research_agent instructions (#573) --- examples/financial_research_agent/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/financial_research_agent/main.py b/examples/financial_research_agent/main.py index 3fa8a7e08..b5b6cfdfd 100644 --- a/examples/financial_research_agent/main.py +++ b/examples/financial_research_agent/main.py @@ -4,7 +4,7 @@ # Entrypoint for the financial bot example. -# Run this as `python -m examples.financial_bot.main` and enter a +# Run this as `python -m examples.financial_research_agent.main` and enter a # financial research query, for example: # "Write up an analysis of Apple Inc.'s most recent quarter." async def main() -> None: From a113fea0eef82bb37a0a803eaae42c4761d0ebdf Mon Sep 17 00:00:00 2001 From: Andrew Han Date: Wed, 23 Apr 2025 16:51:10 -0700 Subject: [PATCH 11/35] Allow cancel out of the streaming result (#579) Fix for #574 @rm-openai I'm not sure how to add a test within the repo but I have pasted a test script below that seems to work ```python import asyncio from openai.types.responses import ResponseTextDeltaEvent from agents import Agent, Runner async def main(): agent = Agent( name="Joker", instructions="You are a helpful assistant.", ) result = Runner.run_streamed(agent, input="Please tell me 5 jokes.") num_visible_event = 0 async for event in result.stream_events(): if event.type == "raw_response_event" and isinstance(event.data, ResponseTextDeltaEvent): print(event.data.delta, end="", flush=True) num_visible_event += 1 print(num_visible_event) if num_visible_event == 3: result.cancel() if __name__ == "__main__": asyncio.run(main()) ```` --- src/agents/result.py | 24 +++++++++++++++++++++--- tests/test_cancel_streaming.py | 22 ++++++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 tests/test_cancel_streaming.py diff --git a/src/agents/result.py b/src/agents/result.py index 0d8372c86..1f1c78328 100644 --- a/src/agents/result.py +++ b/src/agents/result.py @@ -75,7 +75,9 @@ def final_output_as(self, cls: type[T], raise_if_incorrect_type: bool = False) - def to_input_list(self) -> list[TResponseInputItem]: """Creates a new input list, merging the original input with all the new items generated.""" - original_items: list[TResponseInputItem] = ItemHelpers.input_to_new_input_list(self.input) + original_items: list[TResponseInputItem] = ItemHelpers.input_to_new_input_list( + self.input + ) new_items = [item.to_input_item() for item in self.new_items] return original_items + new_items @@ -152,6 +154,18 @@ def last_agent(self) -> Agent[Any]: """ return self.current_agent + def cancel(self) -> None: + """Cancels the streaming run, stopping all background tasks and marking the run as + complete.""" + self._cleanup_tasks() # Cancel all running tasks + self.is_complete = True # Mark the run as complete to stop event streaming + + # Optionally, clear the event queue to prevent processing stale events + while not self._event_queue.empty(): + self._event_queue.get_nowait() + while not self._input_guardrail_queue.empty(): + self._input_guardrail_queue.get_nowait() + async def stream_events(self) -> AsyncIterator[StreamEvent]: """Stream deltas for new items as they are generated. We're using the types from the OpenAI Responses API, so these are semantic events: each event has a `type` field that @@ -192,13 +206,17 @@ async def stream_events(self) -> AsyncIterator[StreamEvent]: def _check_errors(self): if self.current_turn > self.max_turns: - self._stored_exception = MaxTurnsExceeded(f"Max turns ({self.max_turns}) exceeded") + self._stored_exception = MaxTurnsExceeded( + f"Max turns ({self.max_turns}) exceeded" + ) # Fetch all the completed guardrail results from the queue and raise if needed while not self._input_guardrail_queue.empty(): guardrail_result = self._input_guardrail_queue.get_nowait() if guardrail_result.output.tripwire_triggered: - self._stored_exception = InputGuardrailTripwireTriggered(guardrail_result) + self._stored_exception = InputGuardrailTripwireTriggered( + guardrail_result + ) # Check the tasks for any exceptions if self._run_impl_task and self._run_impl_task.done(): diff --git a/tests/test_cancel_streaming.py b/tests/test_cancel_streaming.py new file mode 100644 index 000000000..6d1807d7c --- /dev/null +++ b/tests/test_cancel_streaming.py @@ -0,0 +1,22 @@ +import pytest + +from agents import Agent, Runner + +from .fake_model import FakeModel + + +@pytest.mark.asyncio +async def test_joker_streamed_jokes_with_cancel(): + model = FakeModel() + agent = Agent(name="Joker", model=model) + + result = Runner.run_streamed(agent, input="Please tell me 5 jokes.") + num_events = 0 + stop_after = 1 # There are two that the model gives back. + + async for _event in result.stream_events(): + num_events += 1 + if num_events == 1: + result.cancel() + + assert num_events == 1, f"Expected {stop_after} visible events, but got {num_events}" From 3755ea86589b8e929c5b2bdd51df9f62c1cad8bf Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Wed, 23 Apr 2025 20:39:07 -0400 Subject: [PATCH 12/35] Create to_json_dict for ModelSettings (#582) Now that `ModelSettings` has `Reasoning`, a non-primitive object, `dataclasses.as_dict()` wont work. It will raise an error when you try to serialize (e.g. for tracing). This ensures the object is actually serializable. --- pyproject.toml | 2 +- src/agents/extensions/models/litellm_model.py | 5 +- src/agents/model_settings.py | 17 +++++- src/agents/models/openai_chatcompletions.py | 7 +-- tests/model_settings/test_serialization.py | 59 +++++++++++++++++++ tests/voice/conftest.py | 1 - uv.lock | 8 +-- 7 files changed, 84 insertions(+), 15 deletions(-) create mode 100644 tests/model_settings/test_serialization.py diff --git a/pyproject.toml b/pyproject.toml index eeeb6d3d3..12ffff1ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ requires-python = ">=3.9" license = "MIT" authors = [{ name = "OpenAI", email = "support@openai.com" }] dependencies = [ - "openai>=1.66.5", + "openai>=1.76.0", "pydantic>=2.10, <3", "griffe>=1.5.6, <2", "typing-extensions>=4.12.2, <5", diff --git a/src/agents/extensions/models/litellm_model.py b/src/agents/extensions/models/litellm_model.py index f5e7752fa..dc672acd4 100644 --- a/src/agents/extensions/models/litellm_model.py +++ b/src/agents/extensions/models/litellm_model.py @@ -1,6 +1,5 @@ from __future__ import annotations -import dataclasses import json import time from collections.abc import AsyncIterator @@ -75,7 +74,7 @@ async def get_response( ) -> ModelResponse: with generation_span( model=str(self.model), - model_config=dataclasses.asdict(model_settings) + model_config=model_settings.to_json_dict() | {"base_url": str(self.base_url or ""), "model_impl": "litellm"}, disabled=tracing.is_disabled(), ) as span_generation: @@ -147,7 +146,7 @@ async def stream_response( ) -> AsyncIterator[TResponseStreamEvent]: with generation_span( model=str(self.model), - model_config=dataclasses.asdict(model_settings) + model_config=model_settings.to_json_dict() | {"base_url": str(self.base_url or ""), "model_impl": "litellm"}, disabled=tracing.is_disabled(), ) as span_generation: diff --git a/src/agents/model_settings.py b/src/agents/model_settings.py index fee92b4e0..7b016c98f 100644 --- a/src/agents/model_settings.py +++ b/src/agents/model_settings.py @@ -1,10 +1,12 @@ from __future__ import annotations +import dataclasses from dataclasses import dataclass, fields, replace -from typing import Literal +from typing import Any, Literal from openai._types import Body, Headers, Query from openai.types.shared import Reasoning +from pydantic import BaseModel @dataclass @@ -83,3 +85,16 @@ def resolve(self, override: ModelSettings | None) -> ModelSettings: if getattr(override, field.name) is not None } return replace(self, **changes) + + def to_json_dict(self) -> dict[str, Any]: + dataclass_dict = dataclasses.asdict(self) + + json_dict: dict[str, Any] = {} + + for field_name, value in dataclass_dict.items(): + if isinstance(value, BaseModel): + json_dict[field_name] = value.model_dump(mode="json") + else: + json_dict[field_name] = value + + return json_dict diff --git a/src/agents/models/openai_chatcompletions.py b/src/agents/models/openai_chatcompletions.py index 15bf19cb3..89619f838 100644 --- a/src/agents/models/openai_chatcompletions.py +++ b/src/agents/models/openai_chatcompletions.py @@ -1,6 +1,5 @@ from __future__ import annotations -import dataclasses import json import time from collections.abc import AsyncIterator @@ -56,8 +55,7 @@ async def get_response( ) -> ModelResponse: with generation_span( model=str(self.model), - model_config=dataclasses.asdict(model_settings) - | {"base_url": str(self._client.base_url)}, + model_config=model_settings.to_json_dict() | {"base_url": str(self._client.base_url)}, disabled=tracing.is_disabled(), ) as span_generation: response = await self._fetch_response( @@ -121,8 +119,7 @@ async def stream_response( """ with generation_span( model=str(self.model), - model_config=dataclasses.asdict(model_settings) - | {"base_url": str(self._client.base_url)}, + model_config=model_settings.to_json_dict() | {"base_url": str(self._client.base_url)}, disabled=tracing.is_disabled(), ) as span_generation: response, stream = await self._fetch_response( diff --git a/tests/model_settings/test_serialization.py b/tests/model_settings/test_serialization.py new file mode 100644 index 000000000..d76a58d17 --- /dev/null +++ b/tests/model_settings/test_serialization.py @@ -0,0 +1,59 @@ +import json +from dataclasses import fields + +from openai.types.shared import Reasoning + +from agents.model_settings import ModelSettings + + +def verify_serialization(model_settings: ModelSettings) -> None: + """Verify that ModelSettings can be serialized to a JSON string.""" + json_dict = model_settings.to_json_dict() + json_string = json.dumps(json_dict) + assert json_string is not None + + +def test_basic_serialization() -> None: + """Tests whether ModelSettings can be serialized to a JSON string.""" + + # First, lets create a ModelSettings instance + model_settings = ModelSettings( + temperature=0.5, + top_p=0.9, + max_tokens=100, + ) + + # Now, lets serialize the ModelSettings instance to a JSON string + verify_serialization(model_settings) + + +def test_all_fields_serialization() -> None: + """Tests whether ModelSettings can be serialized to a JSON string.""" + + # First, lets create a ModelSettings instance + model_settings = ModelSettings( + temperature=0.5, + top_p=0.9, + frequency_penalty=0.0, + presence_penalty=0.0, + tool_choice="auto", + parallel_tool_calls=True, + truncation="auto", + max_tokens=100, + reasoning=Reasoning(), + metadata={"foo": "bar"}, + store=False, + include_usage=False, + extra_query={"foo": "bar"}, + extra_body={"foo": "bar"}, + extra_headers={"foo": "bar"}, + ) + + # Verify that every single field is set to a non-None value + for field in fields(model_settings): + assert getattr(model_settings, field.name) is not None, ( + f"You must set the {field.name} field" + ) + + # Now, lets serialize the ModelSettings instance to a JSON string + verify_serialization(model_settings) diff --git a/tests/voice/conftest.py b/tests/voice/conftest.py index 6ed7422ce..79d85d8b4 100644 --- a/tests/voice/conftest.py +++ b/tests/voice/conftest.py @@ -9,4 +9,3 @@ def pytest_ignore_collect(collection_path, config): if str(collection_path).startswith(this_dir): return True - diff --git a/uv.lock b/uv.lock index 3a737cf37..4c6c370ad 100644 --- a/uv.lock +++ b/uv.lock @@ -1463,7 +1463,7 @@ wheels = [ [[package]] name = "openai" -version = "1.74.0" +version = "1.76.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1475,9 +1475,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/75/86/c605a6e84da0248f2cebfcd864b5a6076ecf78849245af5e11d2a5ec7977/openai-1.74.0.tar.gz", hash = "sha256:592c25b8747a7cad33a841958f5eb859a785caea9ee22b9e4f4a2ec062236526", size = 427571 } +sdist = { url = "https://files.pythonhosted.org/packages/84/51/817969ec969b73d8ddad085670ecd8a45ef1af1811d8c3b8a177ca4d1309/openai-1.76.0.tar.gz", hash = "sha256:fd2bfaf4608f48102d6b74f9e11c5ecaa058b60dad9c36e409c12477dfd91fb2", size = 434660 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/91/8c150f16a96367e14bd7d20e86e0bbbec3080e3eb593e63f21a7f013f8e4/openai-1.74.0-py3-none-any.whl", hash = "sha256:aff3e0f9fb209836382ec112778667027f4fd6ae38bdb2334bc9e173598b092a", size = 644790 }, + { url = "https://files.pythonhosted.org/packages/59/aa/84e02ab500ca871eb8f62784426963a1c7c17a72fea3c7f268af4bbaafa5/openai-1.76.0-py3-none-any.whl", hash = "sha256:a712b50e78cf78e6d7b2a8f69c4978243517c2c36999756673e07a14ce37dc0a", size = 661201 }, ] [[package]] @@ -1538,7 +1538,7 @@ requires-dist = [ { name = "litellm", marker = "extra == 'litellm'", specifier = ">=1.65.0,<2" }, { name = "mcp", marker = "python_full_version >= '3.10'", specifier = ">=1.6.0,<2" }, { name = "numpy", marker = "python_full_version >= '3.10' and extra == 'voice'", specifier = ">=2.2.0,<3" }, - { name = "openai", specifier = ">=1.66.5" }, + { name = "openai", specifier = ">=1.76.0" }, { name = "pydantic", specifier = ">=2.10,<3" }, { name = "requests", specifier = ">=2.0,<3" }, { name = "types-requests", specifier = ">=2.0,<3" }, From af80e3a97123a5a0ad0fba695fbc257163c23224 Mon Sep 17 00:00:00 2001 From: Nathan Brake <33383515+njbrake@users.noreply.github.com> Date: Thu, 24 Apr 2025 12:12:46 -0400 Subject: [PATCH 13/35] Prevent MCP ClientSession hang (#580) Per https://modelcontextprotocol.io/specification/draft/basic/lifecycle#timeouts "Implementations SHOULD establish timeouts for all sent requests, to prevent hung connections and resource exhaustion. When the request has not received a success or error response within the timeout period, the sender SHOULD issue a cancellation notification for that request and stop waiting for a response. SDKs and other middleware SHOULD allow these timeouts to be configured on a per-request basis." I picked 5 seconds since that's the default for SSE --- src/agents/mcp/server.py | 26 ++++++++++++++++++++++---- tests/mcp/test_server_errors.py | 2 +- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/agents/mcp/server.py b/src/agents/mcp/server.py index 9a137bbdd..9916c92b0 100644 --- a/src/agents/mcp/server.py +++ b/src/agents/mcp/server.py @@ -3,6 +3,7 @@ import abc import asyncio from contextlib import AbstractAsyncContextManager, AsyncExitStack +from datetime import timedelta from pathlib import Path from typing import Any, Literal @@ -54,7 +55,7 @@ async def call_tool(self, tool_name: str, arguments: dict[str, Any] | None) -> C class _MCPServerWithClientSession(MCPServer, abc.ABC): """Base class for MCP servers that use a `ClientSession` to communicate with the server.""" - def __init__(self, cache_tools_list: bool): + def __init__(self, cache_tools_list: bool, client_session_timeout_seconds: float | None): """ Args: cache_tools_list: Whether to cache the tools list. If `True`, the tools list will be @@ -63,12 +64,16 @@ def __init__(self, cache_tools_list: bool): by calling `invalidate_tools_cache()`. You should set this to `True` if you know the server will not change its tools list, because it can drastically improve latency (by avoiding a round-trip to the server every time). + + client_session_timeout_seconds: the read timeout passed to the MCP ClientSession. """ self.session: ClientSession | None = None self.exit_stack: AsyncExitStack = AsyncExitStack() self._cleanup_lock: asyncio.Lock = asyncio.Lock() self.cache_tools_list = cache_tools_list + self.client_session_timeout_seconds = client_session_timeout_seconds + # The cache is always dirty at startup, so that we fetch tools at least once self._cache_dirty = True self._tools_list: list[MCPTool] | None = None @@ -101,7 +106,15 @@ async def connect(self): try: transport = await self.exit_stack.enter_async_context(self.create_streams()) read, write = transport - session = await self.exit_stack.enter_async_context(ClientSession(read, write)) + session = await self.exit_stack.enter_async_context( + ClientSession( + read, + write, + timedelta(seconds=self.client_session_timeout_seconds) + if self.client_session_timeout_seconds + else None, + ) + ) await session.initialize() self.session = session except Exception as e: @@ -183,6 +196,7 @@ def __init__( params: MCPServerStdioParams, cache_tools_list: bool = False, name: str | None = None, + client_session_timeout_seconds: float | None = 5, ): """Create a new MCP server based on the stdio transport. @@ -199,8 +213,9 @@ def __init__( improve latency (by avoiding a round-trip to the server every time). name: A readable name for the server. If not provided, we'll create one from the command. + client_session_timeout_seconds: the read timeout passed to the MCP ClientSession. """ - super().__init__(cache_tools_list) + super().__init__(cache_tools_list, client_session_timeout_seconds) self.params = StdioServerParameters( command=params["command"], @@ -257,6 +272,7 @@ def __init__( params: MCPServerSseParams, cache_tools_list: bool = False, name: str | None = None, + client_session_timeout_seconds: float | None = 5, ): """Create a new MCP server based on the HTTP with SSE transport. @@ -274,8 +290,10 @@ def __init__( name: A readable name for the server. If not provided, we'll create one from the URL. + + client_session_timeout_seconds: the read timeout passed to the MCP ClientSession. """ - super().__init__(cache_tools_list) + super().__init__(cache_tools_list, client_session_timeout_seconds) self.params = params self._name = name or f"sse: {self.params['url']}" diff --git a/tests/mcp/test_server_errors.py b/tests/mcp/test_server_errors.py index bdca7ce62..fbd8db17d 100644 --- a/tests/mcp/test_server_errors.py +++ b/tests/mcp/test_server_errors.py @@ -6,7 +6,7 @@ class CrashingClientSessionServer(_MCPServerWithClientSession): def __init__(self): - super().__init__(cache_tools_list=False) + super().__init__(cache_tools_list=False, client_session_timeout_seconds=5) self.cleanup_called = False def create_streams(self): From e11b822d5f075fc32683c8df71ac9388a7df79e5 Mon Sep 17 00:00:00 2001 From: Daniele Morotti <58258368+DanieleMorotti@users.noreply.github.com> Date: Thu, 24 Apr 2025 18:53:39 +0200 Subject: [PATCH 14/35] Fix stream error using LiteLLM (#589) In response to issue #587 , I implemented a solution to first check if `refusal` and `usage` attributes exist in the `delta` object. I added a unit test similar to `test_openai_chatcompletions_stream.py`. Let me know if I should change something. --------- Co-authored-by: Rohan Mehta --- src/agents/models/chatcmpl_stream_handler.py | 6 +- .../test_litellm_chatcompletions_stream.py | 286 ++++++++++++++++++ 2 files changed, 290 insertions(+), 2 deletions(-) create mode 100644 tests/models/test_litellm_chatcompletions_stream.py diff --git a/src/agents/models/chatcmpl_stream_handler.py b/src/agents/models/chatcmpl_stream_handler.py index 32f04acb4..c71adeb55 100644 --- a/src/agents/models/chatcmpl_stream_handler.py +++ b/src/agents/models/chatcmpl_stream_handler.py @@ -56,7 +56,8 @@ async def handle_stream( type="response.created", ) - usage = chunk.usage + # This is always set by the OpenAI API, but not by others e.g. LiteLLM + usage = chunk.usage if hasattr(chunk, "usage") else None if not chunk.choices or not chunk.choices[0].delta: continue @@ -112,7 +113,8 @@ async def handle_stream( state.text_content_index_and_output[1].text += delta.content # Handle refusals (model declines to answer) - if delta.refusal: + # This is always set by the OpenAI API, but not by others e.g. LiteLLM + if hasattr(delta, "refusal") and delta.refusal: if not state.refusal_content_index_and_output: # Initialize a content tracker for streaming refusal text state.refusal_content_index_and_output = ( diff --git a/tests/models/test_litellm_chatcompletions_stream.py b/tests/models/test_litellm_chatcompletions_stream.py new file mode 100644 index 000000000..80bd8ea22 --- /dev/null +++ b/tests/models/test_litellm_chatcompletions_stream.py @@ -0,0 +1,286 @@ +from collections.abc import AsyncIterator + +import pytest +from openai.types.chat.chat_completion_chunk import ( + ChatCompletionChunk, + Choice, + ChoiceDelta, + ChoiceDeltaToolCall, + ChoiceDeltaToolCallFunction, +) +from openai.types.completion_usage import CompletionUsage +from openai.types.responses import ( + Response, + ResponseFunctionToolCall, + ResponseOutputMessage, + ResponseOutputRefusal, + ResponseOutputText, +) + +from agents.extensions.models.litellm_model import LitellmModel +from agents.extensions.models.litellm_provider import LitellmProvider +from agents.model_settings import ModelSettings +from agents.models.interface import ModelTracing + + +@pytest.mark.allow_call_model_methods +@pytest.mark.asyncio +async def test_stream_response_yields_events_for_text_content(monkeypatch) -> None: + """ + Validate that `stream_response` emits the correct sequence of events when + streaming a simple assistant message consisting of plain text content. + We simulate two chunks of text returned from the chat completion stream. + """ + # Create two chunks that will be emitted by the fake stream. + chunk1 = ChatCompletionChunk( + id="chunk-id", + created=1, + model="fake", + object="chat.completion.chunk", + choices=[Choice(index=0, delta=ChoiceDelta(content="He"))], + ) + # Mark last chunk with usage so stream_response knows this is final. + chunk2 = ChatCompletionChunk( + id="chunk-id", + created=1, + model="fake", + object="chat.completion.chunk", + choices=[Choice(index=0, delta=ChoiceDelta(content="llo"))], + usage=CompletionUsage(completion_tokens=5, prompt_tokens=7, total_tokens=12), + ) + + async def fake_stream() -> AsyncIterator[ChatCompletionChunk]: + for c in (chunk1, chunk2): + yield c + + # Patch _fetch_response to inject our fake stream + async def patched_fetch_response(self, *args, **kwargs): + # `_fetch_response` is expected to return a Response skeleton and the async stream + resp = Response( + id="resp-id", + created_at=0, + model="fake-model", + object="response", + output=[], + tool_choice="none", + tools=[], + parallel_tool_calls=False, + ) + return resp, fake_stream() + + monkeypatch.setattr(LitellmModel, "_fetch_response", patched_fetch_response) + model = LitellmProvider().get_model("gpt-4") + output_events = [] + async for event in model.stream_response( + system_instructions=None, + input="", + model_settings=ModelSettings(), + tools=[], + output_schema=None, + handoffs=[], + tracing=ModelTracing.DISABLED, + previous_response_id=None, + ): + output_events.append(event) + # We expect a response.created, then a response.output_item.added, content part added, + # two content delta events (for "He" and "llo"), a content part done, the assistant message + # output_item.done, and finally response.completed. + # There should be 8 events in total. + assert len(output_events) == 8 + # First event indicates creation. + assert output_events[0].type == "response.created" + # The output item added and content part added events should mark the assistant message. + assert output_events[1].type == "response.output_item.added" + assert output_events[2].type == "response.content_part.added" + # Two text delta events. + assert output_events[3].type == "response.output_text.delta" + assert output_events[3].delta == "He" + assert output_events[4].type == "response.output_text.delta" + assert output_events[4].delta == "llo" + # After streaming, the content part and item should be marked done. + assert output_events[5].type == "response.content_part.done" + assert output_events[6].type == "response.output_item.done" + # Last event indicates completion of the stream. + assert output_events[7].type == "response.completed" + # The completed response should have one output message with full text. + completed_resp = output_events[7].response + assert isinstance(completed_resp.output[0], ResponseOutputMessage) + assert isinstance(completed_resp.output[0].content[0], ResponseOutputText) + assert completed_resp.output[0].content[0].text == "Hello" + + assert completed_resp.usage, "usage should not be None" + assert completed_resp.usage.input_tokens == 7 + assert completed_resp.usage.output_tokens == 5 + assert completed_resp.usage.total_tokens == 12 + + +@pytest.mark.allow_call_model_methods +@pytest.mark.asyncio +async def test_stream_response_yields_events_for_refusal_content(monkeypatch) -> None: + """ + Validate that when the model streams a refusal string instead of normal content, + `stream_response` emits the appropriate sequence of events including + `response.refusal.delta` events for each chunk of the refusal message and + constructs a completed assistant message with a `ResponseOutputRefusal` part. + """ + # Simulate refusal text coming in two pieces, like content but using the `refusal` + # field on the delta rather than `content`. + chunk1 = ChatCompletionChunk( + id="chunk-id", + created=1, + model="fake", + object="chat.completion.chunk", + choices=[Choice(index=0, delta=ChoiceDelta(refusal="No"))], + ) + chunk2 = ChatCompletionChunk( + id="chunk-id", + created=1, + model="fake", + object="chat.completion.chunk", + choices=[Choice(index=0, delta=ChoiceDelta(refusal="Thanks"))], + usage=CompletionUsage(completion_tokens=2, prompt_tokens=2, total_tokens=4), + ) + + async def fake_stream() -> AsyncIterator[ChatCompletionChunk]: + for c in (chunk1, chunk2): + yield c + + async def patched_fetch_response(self, *args, **kwargs): + resp = Response( + id="resp-id", + created_at=0, + model="fake-model", + object="response", + output=[], + tool_choice="none", + tools=[], + parallel_tool_calls=False, + ) + return resp, fake_stream() + + monkeypatch.setattr(LitellmModel, "_fetch_response", patched_fetch_response) + model = LitellmProvider().get_model("gpt-4") + output_events = [] + async for event in model.stream_response( + system_instructions=None, + input="", + model_settings=ModelSettings(), + tools=[], + output_schema=None, + handoffs=[], + tracing=ModelTracing.DISABLED, + previous_response_id=None, + ): + output_events.append(event) + # Expect sequence similar to text: created, output_item.added, content part added, + # two refusal delta events, content part done, output_item.done, completed. + assert len(output_events) == 8 + assert output_events[0].type == "response.created" + assert output_events[1].type == "response.output_item.added" + assert output_events[2].type == "response.content_part.added" + assert output_events[3].type == "response.refusal.delta" + assert output_events[3].delta == "No" + assert output_events[4].type == "response.refusal.delta" + assert output_events[4].delta == "Thanks" + assert output_events[5].type == "response.content_part.done" + assert output_events[6].type == "response.output_item.done" + assert output_events[7].type == "response.completed" + completed_resp = output_events[7].response + assert isinstance(completed_resp.output[0], ResponseOutputMessage) + refusal_part = completed_resp.output[0].content[0] + assert isinstance(refusal_part, ResponseOutputRefusal) + assert refusal_part.refusal == "NoThanks" + + +@pytest.mark.allow_call_model_methods +@pytest.mark.asyncio +async def test_stream_response_yields_events_for_tool_call(monkeypatch) -> None: + """ + Validate that `stream_response` emits the correct sequence of events when + the model is streaming a function/tool call instead of plain text. + The function call will be split across two chunks. + """ + # Simulate a single tool call whose ID stays constant and function name/args built over chunks. + tool_call_delta1 = ChoiceDeltaToolCall( + index=0, + id="tool-id", + function=ChoiceDeltaToolCallFunction(name="my_", arguments="arg1"), + type="function", + ) + tool_call_delta2 = ChoiceDeltaToolCall( + index=0, + id="tool-id", + function=ChoiceDeltaToolCallFunction(name="func", arguments="arg2"), + type="function", + ) + chunk1 = ChatCompletionChunk( + id="chunk-id", + created=1, + model="fake", + object="chat.completion.chunk", + choices=[Choice(index=0, delta=ChoiceDelta(tool_calls=[tool_call_delta1]))], + ) + chunk2 = ChatCompletionChunk( + id="chunk-id", + created=1, + model="fake", + object="chat.completion.chunk", + choices=[Choice(index=0, delta=ChoiceDelta(tool_calls=[tool_call_delta2]))], + usage=CompletionUsage(completion_tokens=1, prompt_tokens=1, total_tokens=2), + ) + + async def fake_stream() -> AsyncIterator[ChatCompletionChunk]: + for c in (chunk1, chunk2): + yield c + + async def patched_fetch_response(self, *args, **kwargs): + resp = Response( + id="resp-id", + created_at=0, + model="fake-model", + object="response", + output=[], + tool_choice="none", + tools=[], + parallel_tool_calls=False, + ) + return resp, fake_stream() + + monkeypatch.setattr(LitellmModel, "_fetch_response", patched_fetch_response) + model = LitellmProvider().get_model("gpt-4") + output_events = [] + async for event in model.stream_response( + system_instructions=None, + input="", + model_settings=ModelSettings(), + tools=[], + output_schema=None, + handoffs=[], + tracing=ModelTracing.DISABLED, + previous_response_id=None, + ): + output_events.append(event) + # Sequence should be: response.created, then after loop we expect function call-related events: + # one response.output_item.added for function call, a response.function_call_arguments.delta, + # a response.output_item.done, and finally response.completed. + assert output_events[0].type == "response.created" + # The next three events are about the tool call. + assert output_events[1].type == "response.output_item.added" + # The added item should be a ResponseFunctionToolCall. + added_fn = output_events[1].item + assert isinstance(added_fn, ResponseFunctionToolCall) + assert added_fn.name == "my_func" # Name should be concatenation of both chunks. + assert added_fn.arguments == "arg1arg2" + assert output_events[2].type == "response.function_call_arguments.delta" + assert output_events[2].delta == "arg1arg2" + assert output_events[3].type == "response.output_item.done" + assert output_events[4].type == "response.completed" + assert output_events[2].delta == "arg1arg2" + assert output_events[3].type == "response.output_item.done" + assert output_events[4].type == "response.completed" + assert added_fn.name == "my_func" # Name should be concatenation of both chunks. + assert added_fn.arguments == "arg1arg2" + assert output_events[2].type == "response.function_call_arguments.delta" + assert output_events[2].delta == "arg1arg2" + assert output_events[3].type == "response.output_item.done" + assert output_events[4].type == "response.completed" From 45eb41f1e668a45c6b53b64e06fa7db9eab4db46 Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Thu, 24 Apr 2025 14:45:03 -0400 Subject: [PATCH 15/35] More tests for cancelling streamed run (#590) --- tests/fake_model.py | 5 +- tests/test_cancel_streaming.py | 98 +++++++++++++++++++++++++++++++++- 2 files changed, 100 insertions(+), 3 deletions(-) diff --git a/tests/fake_model.py b/tests/fake_model.py index c6b3ba924..da3019a0f 100644 --- a/tests/fake_model.py +++ b/tests/fake_model.py @@ -127,7 +127,10 @@ async def stream_response( ) -def get_response_obj(output: list[TResponseOutputItem], response_id: str | None = None) -> Response: +def get_response_obj( + output: list[TResponseOutputItem], + response_id: str | None = None, +) -> Response: return Response( id=response_id or "123", created_at=123, diff --git a/tests/test_cancel_streaming.py b/tests/test_cancel_streaming.py index 6d1807d7c..3417a3c5d 100644 --- a/tests/test_cancel_streaming.py +++ b/tests/test_cancel_streaming.py @@ -1,12 +1,15 @@ +import json + import pytest from agents import Agent, Runner from .fake_model import FakeModel +from .test_responses import get_function_tool, get_function_tool_call, get_text_message @pytest.mark.asyncio -async def test_joker_streamed_jokes_with_cancel(): +async def test_simple_streaming_with_cancel(): model = FakeModel() agent = Agent(name="Joker", model=model) @@ -16,7 +19,98 @@ async def test_joker_streamed_jokes_with_cancel(): async for _event in result.stream_events(): num_events += 1 - if num_events == 1: + if num_events == stop_after: result.cancel() assert num_events == 1, f"Expected {stop_after} visible events, but got {num_events}" + + +@pytest.mark.asyncio +async def test_multiple_events_streaming_with_cancel(): + model = FakeModel() + agent = Agent( + name="Joker", + model=model, + tools=[get_function_tool("foo", "tool_result")], + ) + + model.add_multiple_turn_outputs( + [ + # First turn: a message and tool call + [ + get_text_message("a_message"), + get_function_tool_call("foo", json.dumps({"a": "b"})), + ], + # Second turn: text message + [get_text_message("done")], + ] + ) + + result = Runner.run_streamed(agent, input="Please tell me 5 jokes.") + num_events = 0 + stop_after = 2 + + async for _ in result.stream_events(): + num_events += 1 + if num_events == stop_after: + result.cancel() + + assert num_events == stop_after, f"Expected {stop_after} visible events, but got {num_events}" + + +@pytest.mark.asyncio +async def test_cancel_prevents_further_events(): + model = FakeModel() + agent = Agent(name="Joker", model=model) + result = Runner.run_streamed(agent, input="Please tell me 5 jokes.") + events = [] + async for event in result.stream_events(): + events.append(event) + result.cancel() + break # Cancel after first event + # Try to get more events after cancel + more_events = [e async for e in result.stream_events()] + assert len(events) == 1 + assert more_events == [], "No events should be yielded after cancel()" + + +@pytest.mark.asyncio +async def test_cancel_is_idempotent(): + model = FakeModel() + agent = Agent(name="Joker", model=model) + result = Runner.run_streamed(agent, input="Please tell me 5 jokes.") + events = [] + async for event in result.stream_events(): + events.append(event) + result.cancel() + result.cancel() # Call cancel again + break + # Should not raise or misbehave + assert len(events) == 1 + + +@pytest.mark.asyncio +async def test_cancel_before_streaming(): + model = FakeModel() + agent = Agent(name="Joker", model=model) + result = Runner.run_streamed(agent, input="Please tell me 5 jokes.") + result.cancel() # Cancel before streaming + events = [e async for e in result.stream_events()] + assert events == [], "No events should be yielded if cancel() is called before streaming." + + +@pytest.mark.asyncio +async def test_cancel_cleans_up_resources(): + model = FakeModel() + agent = Agent(name="Joker", model=model) + result = Runner.run_streamed(agent, input="Please tell me 5 jokes.") + # Start streaming, then cancel + async for _ in result.stream_events(): + result.cancel() + break + # After cancel, queues should be empty and is_complete True + assert result.is_complete, "Result should be marked complete after cancel." + assert result._event_queue.empty(), "Event queue should be empty after cancel." + assert result._input_guardrail_queue.empty(), ( + "Input guardrail queue should be empty after cancel." + ) From 3bbc7c48cb9ee80ed4b3dfbbd55efddf7f77d6a3 Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Thu, 24 Apr 2025 14:58:38 -0400 Subject: [PATCH 16/35] v0.0.13 (#593) --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 12ffff1ff..c1ae467a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "openai-agents" -version = "0.0.12" +version = "0.0.13" description = "OpenAI Agents SDK" readme = "README.md" requires-python = ">=3.9" diff --git a/uv.lock b/uv.lock index 4c6c370ad..c6824a082 100644 --- a/uv.lock +++ b/uv.lock @@ -1482,7 +1482,7 @@ wheels = [ [[package]] name = "openai-agents" -version = "0.0.12" +version = "0.0.13" source = { editable = "." } dependencies = [ { name = "griffe" }, From 8fd7773a5ef5121d9349edbabebc9522a2f3c4f0 Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Thu, 24 Apr 2025 18:20:35 -0400 Subject: [PATCH 17/35] Add usage to context in streaming (#595) --- src/agents/result.py | 16 +++++++--------- src/agents/run.py | 3 +++ tests/fake_model.py | 19 ++++++++++++++++--- tests/test_result_cast.py | 3 ++- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/src/agents/result.py b/src/agents/result.py index 1f1c78328..243db155c 100644 --- a/src/agents/result.py +++ b/src/agents/result.py @@ -15,6 +15,7 @@ from .guardrail import InputGuardrailResult, OutputGuardrailResult from .items import ItemHelpers, ModelResponse, RunItem, TResponseInputItem from .logger import logger +from .run_context import RunContextWrapper from .stream_events import StreamEvent from .tracing import Trace from .util._pretty_print import pretty_print_result, pretty_print_run_result_streaming @@ -50,6 +51,9 @@ class RunResultBase(abc.ABC): output_guardrail_results: list[OutputGuardrailResult] """Guardrail results for the final output of the agent.""" + context_wrapper: RunContextWrapper[Any] + """The context wrapper for the agent run.""" + @property @abc.abstractmethod def last_agent(self) -> Agent[Any]: @@ -75,9 +79,7 @@ def final_output_as(self, cls: type[T], raise_if_incorrect_type: bool = False) - def to_input_list(self) -> list[TResponseInputItem]: """Creates a new input list, merging the original input with all the new items generated.""" - original_items: list[TResponseInputItem] = ItemHelpers.input_to_new_input_list( - self.input - ) + original_items: list[TResponseInputItem] = ItemHelpers.input_to_new_input_list(self.input) new_items = [item.to_input_item() for item in self.new_items] return original_items + new_items @@ -206,17 +208,13 @@ async def stream_events(self) -> AsyncIterator[StreamEvent]: def _check_errors(self): if self.current_turn > self.max_turns: - self._stored_exception = MaxTurnsExceeded( - f"Max turns ({self.max_turns}) exceeded" - ) + self._stored_exception = MaxTurnsExceeded(f"Max turns ({self.max_turns}) exceeded") # Fetch all the completed guardrail results from the queue and raise if needed while not self._input_guardrail_queue.empty(): guardrail_result = self._input_guardrail_queue.get_nowait() if guardrail_result.output.tripwire_triggered: - self._stored_exception = InputGuardrailTripwireTriggered( - guardrail_result - ) + self._stored_exception = InputGuardrailTripwireTriggered(guardrail_result) # Check the tasks for any exceptions if self._run_impl_task and self._run_impl_task.done(): diff --git a/src/agents/run.py b/src/agents/run.py index 2af558d58..849da7bfc 100644 --- a/src/agents/run.py +++ b/src/agents/run.py @@ -270,6 +270,7 @@ async def run( _last_agent=current_agent, input_guardrail_results=input_guardrail_results, output_guardrail_results=output_guardrail_results, + context_wrapper=context_wrapper, ) elif isinstance(turn_result.next_step, NextStepHandoff): current_agent = cast(Agent[TContext], turn_result.next_step.new_agent) @@ -423,6 +424,7 @@ def run_streamed( output_guardrail_results=[], _current_agent_output_schema=output_schema, trace=new_trace, + context_wrapper=context_wrapper, ) # Kick off the actual agent loop in the background and return the streamed result object. @@ -696,6 +698,7 @@ async def _run_single_turn_streamed( usage=usage, response_id=event.response.id, ) + context_wrapper.usage.add(usage) streamed_result._event_queue.put_nowait(RawResponsesStreamEvent(data=event)) diff --git a/tests/fake_model.py b/tests/fake_model.py index da3019a0f..32f919ef1 100644 --- a/tests/fake_model.py +++ b/tests/fake_model.py @@ -3,7 +3,8 @@ from collections.abc import AsyncIterator from typing import Any -from openai.types.responses import Response, ResponseCompletedEvent +from openai.types.responses import Response, ResponseCompletedEvent, ResponseUsage +from openai.types.responses.response_usage import InputTokensDetails, OutputTokensDetails from agents.agent_output import AgentOutputSchemaBase from agents.handoffs import Handoff @@ -33,6 +34,10 @@ def __init__( ) self.tracing_enabled = tracing_enabled self.last_turn_args: dict[str, Any] = {} + self.hardcoded_usage: Usage | None = None + + def set_hardcoded_usage(self, usage: Usage): + self.hardcoded_usage = usage def set_next_output(self, output: list[TResponseOutputItem] | Exception): self.turn_outputs.append(output) @@ -83,7 +88,7 @@ async def get_response( return ModelResponse( output=output, - usage=Usage(), + usage=self.hardcoded_usage or Usage(), response_id=None, ) @@ -123,13 +128,14 @@ async def stream_response( yield ResponseCompletedEvent( type="response.completed", - response=get_response_obj(output), + response=get_response_obj(output, usage=self.hardcoded_usage), ) def get_response_obj( output: list[TResponseOutputItem], response_id: str | None = None, + usage: Usage | None = None, ) -> Response: return Response( id=response_id or "123", @@ -141,4 +147,11 @@ def get_response_obj( tools=[], top_p=None, parallel_tool_calls=False, + usage=ResponseUsage( + input_tokens=usage.input_tokens if usage else 0, + output_tokens=usage.output_tokens if usage else 0, + total_tokens=usage.total_tokens if usage else 0, + input_tokens_details=InputTokensDetails(cached_tokens=0), + output_tokens_details=OutputTokensDetails(reasoning_tokens=0), + ), ) diff --git a/tests/test_result_cast.py b/tests/test_result_cast.py index ec17e3275..c621e7352 100644 --- a/tests/test_result_cast.py +++ b/tests/test_result_cast.py @@ -3,7 +3,7 @@ import pytest from pydantic import BaseModel -from agents import Agent, RunResult +from agents import Agent, RunContextWrapper, RunResult def create_run_result(final_output: Any) -> RunResult: @@ -15,6 +15,7 @@ def create_run_result(final_output: Any) -> RunResult: input_guardrail_results=[], output_guardrail_results=[], _last_agent=Agent(name="test"), + context_wrapper=RunContextWrapper(context=None), ) From aa197e1e1431ec8ad381575d0ea5a9667a9c2fcb Mon Sep 17 00:00:00 2001 From: Stefano Baccianella <4247706+mangiucugna@users.noreply.github.com> Date: Fri, 25 Apr 2025 01:11:25 +0200 Subject: [PATCH 18/35] Make the TTS voices type exportable (#577) When using the voice agent in typed code, it is suboptimal and error prone to type the TTS voice variables in your code independently. With this commit we are making the type exportable so that developers can just use that and be future-proof. Example of usage in code: ``` DEFAULT_TTS_VOICE: TTSModelSettings.TTSVoice = "alloy" ... tts_voice: TTSModelSettings.TTSVoice = DEFAULT_TTS_VOICE ... output = await VoicePipeline( workflow=workflow, config=VoicePipelineConfig( tts_settings=TTSModelSettings( buffer_size=512, transform_data=transform_data, voice=tts_voice, instructions=tts_instructions, )) ).run(audio_input) ``` --------- Co-authored-by: Rohan Mehta --- src/agents/voice/__init__.py | 2 ++ src/agents/voice/model.py | 7 +++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/agents/voice/__init__.py b/src/agents/voice/__init__.py index 499c064c5..e11ee4467 100644 --- a/src/agents/voice/__init__.py +++ b/src/agents/voice/__init__.py @@ -7,6 +7,7 @@ STTModelSettings, TTSModel, TTSModelSettings, + TTSVoice, VoiceModelProvider, ) from .models.openai_model_provider import OpenAIVoiceModelProvider @@ -30,6 +31,7 @@ "STTModelSettings", "TTSModel", "TTSModelSettings", + "TTSVoice", "VoiceModelProvider", "StreamedAudioResult", "SingleAgentVoiceWorkflow", diff --git a/src/agents/voice/model.py b/src/agents/voice/model.py index 220d4b480..c36a4de76 100644 --- a/src/agents/voice/model.py +++ b/src/agents/voice/model.py @@ -14,14 +14,13 @@ ) DEFAULT_TTS_BUFFER_SIZE = 120 +TTSVoice = Literal["alloy", "ash", "coral", "echo", "fable", "onyx", "nova", "sage", "shimmer"] +"""Exportable type for the TTSModelSettings voice enum""" @dataclass class TTSModelSettings: """Settings for a TTS model.""" - - voice: ( - Literal["alloy", "ash", "coral", "echo", "fable", "onyx", "nova", "sage", "shimmer"] | None - ) = None + voice: TTSVoice | None = None """ The voice to use for the TTS model. If not provided, the default voice for the respective model will be used. From 4187fba9556683c5ace4706ebd5dbe90cd14e315 Mon Sep 17 00:00:00 2001 From: N V J K Kartik <114015823+NVJKKartik@users.noreply.github.com> Date: Fri, 25 Apr 2025 21:40:59 +0530 Subject: [PATCH 19/35] docs: add FutureAGI to tracing documentation (#592) Hi Team! This PR adds FutureAGI to the tracing documentation as one of the automatic tracing processors for OpenAI agents SDK. ![image](https://github.com/user-attachments/assets/4de3aadc-5efa-4712-8b02-decdedf8f8ef) --- docs/tracing.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/tracing.md b/docs/tracing.md index ea48a2e28..dd883c5aa 100644 --- a/docs/tracing.md +++ b/docs/tracing.md @@ -101,6 +101,7 @@ To customize this default setup, to send traces to alternative or additional bac - [Weights & Biases](https://weave-docs.wandb.ai/guides/integrations/openai_agents) - [Arize-Phoenix](https://docs.arize.com/phoenix/tracing/integrations-tracing/openai-agents-sdk) +- [Future AGI](https://docs.futureagi.com/future-agi/products/observability/auto-instrumentation/openai_agents) - [MLflow (self-hosted/OSS](https://mlflow.org/docs/latest/tracing/integrations/openai-agent) - [MLflow (Databricks hosted](https://docs.databricks.com/aws/en/mlflow/mlflow-tracing#-automatic-tracing) - [Braintrust](https://braintrust.dev/docs/guides/traces/integrations#openai-agents-sdk) From db0ee9d5a5e7c24adfcc0a98a94540341e43c6e5 Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Tue, 29 Apr 2025 16:12:24 -0700 Subject: [PATCH 20/35] Update litellm version (#626) Addresses https://github.com/openai/openai-agents-python/issues/614 --- pyproject.toml | 2 +- uv.lock | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c1ae467a2..d5600976b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ Repository = "https://github.com/openai/openai-agents-python" [project.optional-dependencies] voice = ["numpy>=2.2.0, <3; python_version>='3.10'", "websockets>=15.0, <16"] viz = ["graphviz>=0.17"] -litellm = ["litellm>=1.65.0, <2"] +litellm = ["litellm>=1.67.4.post1, <2"] [dependency-groups] dev = [ diff --git a/uv.lock b/uv.lock index c6824a082..636dbdd12 100644 --- a/uv.lock +++ b/uv.lock @@ -928,7 +928,7 @@ wheels = [ [[package]] name = "litellm" -version = "1.66.1" +version = "1.67.4.post1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -943,10 +943,7 @@ dependencies = [ { name = "tiktoken" }, { name = "tokenizers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c1/21/12562c37310254456afdd277454dac4d14b8b40796216e8a438a9e1c5e86/litellm-1.66.1.tar.gz", hash = "sha256:98f7add913e5eae2131dd412ee27532d9a309defd9dbb64f6c6c42ea8a2af068", size = 7203211 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/33/fdc4615ca621940406e3b0b303e900bc2868cfcd8c62c4a6f5e7d2f6a56c/litellm-1.66.1-py3-none-any.whl", hash = "sha256:1f601fea3f086c1d2d91be60b9db115082a2f3a697e4e0def72f8b9c777c7232", size = 7559553 }, -] +sdist = { url = "https://files.pythonhosted.org/packages/4d/89/bacf75633dd43d6c5536380fb652c4af25046c29f5c6e5fdb4e8fe5af505/litellm-1.67.4.post1.tar.gz", hash = "sha256:057f2505f82d8c3f83d705c375b0d1931de998b13e239a6b06e16ee351fda648", size = 7243930 } [[package]] name = "markdown" @@ -1535,7 +1532,7 @@ dev = [ requires-dist = [ { name = "graphviz", marker = "extra == 'viz'", specifier = ">=0.17" }, { name = "griffe", specifier = ">=1.5.6,<2" }, - { name = "litellm", marker = "extra == 'litellm'", specifier = ">=1.65.0,<2" }, + { name = "litellm", marker = "extra == 'litellm'", specifier = ">=1.67.4.post1,<2" }, { name = "mcp", marker = "python_full_version >= '3.10'", specifier = ">=1.6.0,<2" }, { name = "numpy", marker = "python_full_version >= '3.10' and extra == 'voice'", specifier = ">=2.2.0,<3" }, { name = "openai", specifier = ">=1.76.0" }, From f9763495b86afcf0c421451a92200e1141fa8dcb Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Wed, 30 Apr 2025 08:15:35 -0700 Subject: [PATCH 21/35] 0.0.14 release (#635) --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d5600976b..22b028ae7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "openai-agents" -version = "0.0.13" +version = "0.0.14" description = "OpenAI Agents SDK" readme = "README.md" requires-python = ">=3.9" diff --git a/uv.lock b/uv.lock index 636dbdd12..87dd3cd2c 100644 --- a/uv.lock +++ b/uv.lock @@ -1479,7 +1479,7 @@ wheels = [ [[package]] name = "openai-agents" -version = "0.0.13" +version = "0.0.14" source = { editable = "." } dependencies = [ { name = "griffe" }, From 2c46dae37787e36f68db954f37addf2ddaa69458 Mon Sep 17 00:00:00 2001 From: Daniele Morotti <58258368+DanieleMorotti@users.noreply.github.com> Date: Wed, 14 May 2025 17:31:42 +0200 Subject: [PATCH 22/35] Fixed a bug for "detail" attribute in input image (#685) When an input image is given as input, the code tries to access the 'detail' key, that may not be present as noted in #159. With this pull request, now it tries to access the key, otherwise set the value to `None`. @pakrym-oai or @rm-openai let me know if you want any changes. --- src/agents/models/chatcmpl_converter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/agents/models/chatcmpl_converter.py b/src/agents/models/chatcmpl_converter.py index 613a37453..1d599e8c0 100644 --- a/src/agents/models/chatcmpl_converter.py +++ b/src/agents/models/chatcmpl_converter.py @@ -234,7 +234,7 @@ def extract_all_content( type="image_url", image_url={ "url": casted_image_param["image_url"], - "detail": casted_image_param["detail"], + "detail": casted_image_param.get("detail", "auto"), }, ) ) From 1994f9d4c4bb807fd5460919b4352b715ef1324a Mon Sep 17 00:00:00 2001 From: Ashok Saravanan <90977640+AshokSaravanan222@users.noreply.github.com> Date: Wed, 14 May 2025 11:34:27 -0500 Subject: [PATCH 23/35] feat: pass extra_body through to LiteLLM acompletion (#638) **Purpose** Allow arbitrary `extra_body` parameters (e.g. `cached_content`) to be forwarded into the LiteLLM call. Useful for context caching in Gemini models ([docs](https://ai.google.dev/gemini-api/docs/caching?lang=python)). **Example usage** ```python import os from agents import Agent, ModelSettings from agents.extensions.models.litellm_model import LitellmModel cache_name = "cachedContents/34jopukfx5di" # previously stored context gemini_model = LitellmModel( model="gemini/gemini-1.5-flash-002", api_key=os.getenv("GOOGLE_API_KEY") ) agent = Agent( name="Cached Gemini Agent", model=gemini_model, model_settings=ModelSettings( extra_body={"cached_content": cache_name} ) ) --- src/agents/extensions/models/litellm_model.py | 2 + tests/models/test_litellm_extra_body.py | 45 +++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 tests/models/test_litellm_extra_body.py diff --git a/src/agents/extensions/models/litellm_model.py b/src/agents/extensions/models/litellm_model.py index dc672acd4..d3b25a198 100644 --- a/src/agents/extensions/models/litellm_model.py +++ b/src/agents/extensions/models/litellm_model.py @@ -269,6 +269,8 @@ async def _fetch_response( extra_kwargs["extra_query"] = model_settings.extra_query if model_settings.metadata: extra_kwargs["metadata"] = model_settings.metadata + if model_settings.extra_body and isinstance(model_settings.extra_body, dict): + extra_kwargs.update(model_settings.extra_body) ret = await litellm.acompletion( model=self.model, diff --git a/tests/models/test_litellm_extra_body.py b/tests/models/test_litellm_extra_body.py new file mode 100644 index 000000000..ac56c25cf --- /dev/null +++ b/tests/models/test_litellm_extra_body.py @@ -0,0 +1,45 @@ +import litellm +import pytest +from litellm.types.utils import Choices, Message, ModelResponse, Usage + +from agents.extensions.models.litellm_model import LitellmModel +from agents.model_settings import ModelSettings +from agents.models.interface import ModelTracing + + +@pytest.mark.allow_call_model_methods +@pytest.mark.asyncio +async def test_extra_body_is_forwarded(monkeypatch): + """ + Forward `extra_body` entries into litellm.acompletion kwargs. + + This ensures that user-provided parameters (e.g. cached_content) + arrive alongside default arguments. + """ + captured: dict[str, object] = {} + + async def fake_acompletion(model, messages=None, **kwargs): + captured.update(kwargs) + msg = Message(role="assistant", content="ok") + choice = Choices(index=0, message=msg) + return ModelResponse(choices=[choice], usage=Usage(0, 0, 0)) + + monkeypatch.setattr(litellm, "acompletion", fake_acompletion) + settings = ModelSettings( + temperature=0.1, + extra_body={"cached_content": "some_cache", "foo": 123} + ) + model = LitellmModel(model="test-model") + + await model.get_response( + system_instructions=None, + input=[], + model_settings=settings, + tools=[], + output_schema=None, + handoffs=[], + tracing=ModelTracing.DISABLED, + previous_response_id=None, + ) + + assert {"cached_content": "some_cache", "foo": 123}.items() <= captured.items() From 02b6e7013cf00de92b453610f459be40f344e6a8 Mon Sep 17 00:00:00 2001 From: leohpark <135409779+leohpark@users.noreply.github.com> Date: Wed, 14 May 2025 09:37:06 -0700 Subject: [PATCH 24/35] Update search_agent.py (#677) Added missing word "be" in prompt instructions. This is unlikely to change the agent functionality in most cases, but optimal clarity in prompt language is a best practice. --- examples/research_bot/agents/search_agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/research_bot/agents/search_agent.py b/examples/research_bot/agents/search_agent.py index 0212ce5b5..61f91701f 100644 --- a/examples/research_bot/agents/search_agent.py +++ b/examples/research_bot/agents/search_agent.py @@ -3,7 +3,7 @@ INSTRUCTIONS = ( "You are a research assistant. Given a search term, you search the web for that term and " - "produce a concise summary of the results. The summary must 2-3 paragraphs and less than 300 " + "produce a concise summary of the results. The summary must be 2-3 paragraphs and less than 300 " "words. Capture the main points. Write succinctly, no need to have complete sentences or good " "grammar. This will be consumed by someone synthesizing a report, so its vital you capture the " "essence and ignore any fluff. Do not include any additional commentary other than the summary " From 1847008e0f247e5468d691e4e9d780f984db9a18 Mon Sep 17 00:00:00 2001 From: Akshit97 Date: Thu, 15 May 2025 00:15:14 +0530 Subject: [PATCH 25/35] feat: Streamable HTTP support (#643) Co-authored-by: aagarwal25 --- examples/mcp/streamablehttp_example/README.md | 13 +++ examples/mcp/streamablehttp_example/main.py | 83 ++++++++++++++ examples/mcp/streamablehttp_example/server.py | 33 ++++++ pyproject.toml | 2 +- src/agents/mcp/__init__.py | 4 + src/agents/mcp/server.py | 106 ++++++++++++++++-- uv.lock | 20 +++- 7 files changed, 247 insertions(+), 14 deletions(-) create mode 100644 examples/mcp/streamablehttp_example/README.md create mode 100644 examples/mcp/streamablehttp_example/main.py create mode 100644 examples/mcp/streamablehttp_example/server.py diff --git a/examples/mcp/streamablehttp_example/README.md b/examples/mcp/streamablehttp_example/README.md new file mode 100644 index 000000000..a07fe19be --- /dev/null +++ b/examples/mcp/streamablehttp_example/README.md @@ -0,0 +1,13 @@ +# MCP Streamable HTTP Example + +This example uses a local Streamable HTTP server in [server.py](server.py). + +Run the example via: + +``` +uv run python examples/mcp/streamablehttp_example/main.py +``` + +## Details + +The example uses the `MCPServerStreamableHttp` class from `agents.mcp`. The server runs in a sub-process at `https://localhost:8000/mcp`. diff --git a/examples/mcp/streamablehttp_example/main.py b/examples/mcp/streamablehttp_example/main.py new file mode 100644 index 000000000..cc95e798b --- /dev/null +++ b/examples/mcp/streamablehttp_example/main.py @@ -0,0 +1,83 @@ +import asyncio +import os +import shutil +import subprocess +import time +from typing import Any + +from agents import Agent, Runner, gen_trace_id, trace +from agents.mcp import MCPServer, MCPServerStreamableHttp +from agents.model_settings import ModelSettings + + +async def run(mcp_server: MCPServer): + agent = Agent( + name="Assistant", + instructions="Use the tools to answer the questions.", + mcp_servers=[mcp_server], + model_settings=ModelSettings(tool_choice="required"), + ) + + # Use the `add` tool to add two numbers + message = "Add these numbers: 7 and 22." + print(f"Running: {message}") + result = await Runner.run(starting_agent=agent, input=message) + print(result.final_output) + + # Run the `get_weather` tool + message = "What's the weather in Tokyo?" + print(f"\n\nRunning: {message}") + result = await Runner.run(starting_agent=agent, input=message) + print(result.final_output) + + # Run the `get_secret_word` tool + message = "What's the secret word?" + print(f"\n\nRunning: {message}") + result = await Runner.run(starting_agent=agent, input=message) + print(result.final_output) + + +async def main(): + async with MCPServerStreamableHttp( + name="Streamable HTTP Python Server", + params={ + "url": "http://localhost:8000/mcp", + }, + ) as server: + trace_id = gen_trace_id() + with trace(workflow_name="Streamable HTTP Example", trace_id=trace_id): + print(f"View trace: https://platform.openai.com/traces/trace?trace_id={trace_id}\n") + await run(server) + + +if __name__ == "__main__": + # Let's make sure the user has uv installed + if not shutil.which("uv"): + raise RuntimeError( + "uv is not installed. Please install it: https://docs.astral.sh/uv/getting-started/installation/" + ) + + # We'll run the Streamable HTTP server in a subprocess. Usually this would be a remote server, but for this + # demo, we'll run it locally at http://localhost:8000/mcp + process: subprocess.Popen[Any] | None = None + try: + this_dir = os.path.dirname(os.path.abspath(__file__)) + server_file = os.path.join(this_dir, "server.py") + + print("Starting Streamable HTTP server at http://localhost:8000/mcp ...") + + # Run `uv run server.py` to start the Streamable HTTP server + process = subprocess.Popen(["uv", "run", server_file]) + # Give it 3 seconds to start + time.sleep(3) + + print("Streamable HTTP server started. Running example...\n\n") + except Exception as e: + print(f"Error starting Streamable HTTP server: {e}") + exit(1) + + try: + asyncio.run(main()) + finally: + if process: + process.terminate() diff --git a/examples/mcp/streamablehttp_example/server.py b/examples/mcp/streamablehttp_example/server.py new file mode 100644 index 000000000..d8f839652 --- /dev/null +++ b/examples/mcp/streamablehttp_example/server.py @@ -0,0 +1,33 @@ +import random + +import requests +from mcp.server.fastmcp import FastMCP + +# Create server +mcp = FastMCP("Echo Server") + + +@mcp.tool() +def add(a: int, b: int) -> int: + """Add two numbers""" + print(f"[debug-server] add({a}, {b})") + return a + b + + +@mcp.tool() +def get_secret_word() -> str: + print("[debug-server] get_secret_word()") + return random.choice(["apple", "banana", "cherry"]) + + +@mcp.tool() +def get_current_weather(city: str) -> str: + print(f"[debug-server] get_current_weather({city})") + + endpoint = "https://wttr.in" + response = requests.get(f"{endpoint}/{city}") + return response.text + + +if __name__ == "__main__": + mcp.run(transport="streamable-http") diff --git a/pyproject.toml b/pyproject.toml index 22b028ae7..87a707d3d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ dependencies = [ "typing-extensions>=4.12.2, <5", "requests>=2.0, <3", "types-requests>=2.0, <3", - "mcp>=1.6.0, <2; python_version >= '3.10'", + "mcp>=1.8.0, <2; python_version >= '3.10'", ] classifiers = [ "Typing :: Typed", diff --git a/src/agents/mcp/__init__.py b/src/agents/mcp/__init__.py index 1a72a89f0..d4eb8fa68 100644 --- a/src/agents/mcp/__init__.py +++ b/src/agents/mcp/__init__.py @@ -5,6 +5,8 @@ MCPServerSseParams, MCPServerStdio, MCPServerStdioParams, + MCPServerStreamableHttp, + MCPServerStreamableHttpParams, ) except ImportError: pass @@ -17,5 +19,7 @@ "MCPServerSseParams", "MCPServerStdio", "MCPServerStdioParams", + "MCPServerStreamableHttp", + "MCPServerStreamableHttpParams", "MCPUtil", ] diff --git a/src/agents/mcp/server.py b/src/agents/mcp/server.py index 9916c92b0..c5255ead7 100644 --- a/src/agents/mcp/server.py +++ b/src/agents/mcp/server.py @@ -10,7 +10,9 @@ from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream from mcp import ClientSession, StdioServerParameters, Tool as MCPTool, stdio_client from mcp.client.sse import sse_client -from mcp.types import CallToolResult, JSONRPCMessage +from mcp.client.streamable_http import GetSessionIdCallback, streamablehttp_client +from mcp.shared.message import SessionMessage +from mcp.types import CallToolResult from typing_extensions import NotRequired, TypedDict from ..exceptions import UserError @@ -83,8 +85,9 @@ def create_streams( self, ) -> AbstractAsyncContextManager[ tuple[ - MemoryObjectReceiveStream[JSONRPCMessage | Exception], - MemoryObjectSendStream[JSONRPCMessage], + MemoryObjectReceiveStream[SessionMessage | Exception], + MemoryObjectSendStream[SessionMessage], + GetSessionIdCallback | None ] ]: """Create the streams for the server.""" @@ -105,7 +108,11 @@ async def connect(self): """Connect to the server.""" try: transport = await self.exit_stack.enter_async_context(self.create_streams()) - read, write = transport + # streamablehttp_client returns (read, write, get_session_id) + # sse_client returns (read, write) + + read, write, *_ = transport + session = await self.exit_stack.enter_async_context( ClientSession( read, @@ -232,8 +239,9 @@ def create_streams( self, ) -> AbstractAsyncContextManager[ tuple[ - MemoryObjectReceiveStream[JSONRPCMessage | Exception], - MemoryObjectSendStream[JSONRPCMessage], + MemoryObjectReceiveStream[SessionMessage | Exception], + MemoryObjectSendStream[SessionMessage], + GetSessionIdCallback | None ] ]: """Create the streams for the server.""" @@ -302,8 +310,9 @@ def create_streams( self, ) -> AbstractAsyncContextManager[ tuple[ - MemoryObjectReceiveStream[JSONRPCMessage | Exception], - MemoryObjectSendStream[JSONRPCMessage], + MemoryObjectReceiveStream[SessionMessage | Exception], + MemoryObjectSendStream[SessionMessage], + GetSessionIdCallback | None ] ]: """Create the streams for the server.""" @@ -318,3 +327,84 @@ def create_streams( def name(self) -> str: """A readable name for the server.""" return self._name + + +class MCPServerStreamableHttpParams(TypedDict): + """Mirrors the params in`mcp.client.streamable_http.streamablehttp_client`.""" + + url: str + """The URL of the server.""" + + headers: NotRequired[dict[str, str]] + """The headers to send to the server.""" + + timeout: NotRequired[timedelta] + """The timeout for the HTTP request. Defaults to 5 seconds.""" + + sse_read_timeout: NotRequired[timedelta] + """The timeout for the SSE connection, in seconds. Defaults to 5 minutes.""" + + terminate_on_close: NotRequired[bool] + """Terminate on close""" + + +class MCPServerStreamableHttp(_MCPServerWithClientSession): + """MCP server implementation that uses the Streamable HTTP transport. See the [spec] + (https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) + for details. + """ + + def __init__( + self, + params: MCPServerStreamableHttpParams, + cache_tools_list: bool = False, + name: str | None = None, + client_session_timeout_seconds: float | None = 5, + ): + """Create a new MCP server based on the Streamable HTTP transport. + + Args: + params: The params that configure the server. This includes the URL of the server, + the headers to send to the server, the timeout for the HTTP request, and the + timeout for the Streamable HTTP connection and whether we need to + terminate on close. + + cache_tools_list: Whether to cache the tools list. If `True`, the tools list will be + cached and only fetched from the server once. If `False`, the tools list will be + fetched from the server on each call to `list_tools()`. The cache can be + invalidated by calling `invalidate_tools_cache()`. You should set this to `True` + if you know the server will not change its tools list, because it can drastically + improve latency (by avoiding a round-trip to the server every time). + + name: A readable name for the server. If not provided, we'll create one from the + URL. + + client_session_timeout_seconds: the read timeout passed to the MCP ClientSession. + """ + super().__init__(cache_tools_list, client_session_timeout_seconds) + + self.params = params + self._name = name or f"streamable_http: {self.params['url']}" + + def create_streams( + self, + ) -> AbstractAsyncContextManager[ + tuple[ + MemoryObjectReceiveStream[SessionMessage | Exception], + MemoryObjectSendStream[SessionMessage], + GetSessionIdCallback | None + ] + ]: + """Create the streams for the server.""" + return streamablehttp_client( + url=self.params["url"], + headers=self.params.get("headers", None), + timeout=self.params.get("timeout", timedelta(seconds=30)), + sse_read_timeout=self.params.get("sse_read_timeout", timedelta(seconds=60 * 5)), + terminate_on_close=self.params.get("terminate_on_close", True) + ) + + @property + def name(self) -> str: + """A readable name for the server.""" + return self._name diff --git a/uv.lock b/uv.lock index 87dd3cd2c..6ccc19966 100644 --- a/uv.lock +++ b/uv.lock @@ -1047,7 +1047,7 @@ wheels = [ [[package]] name = "mcp" -version = "1.6.0" +version = "1.8.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", marker = "python_full_version >= '3.10'" }, @@ -1055,13 +1055,14 @@ dependencies = [ { name = "httpx-sse", marker = "python_full_version >= '3.10'" }, { name = "pydantic", marker = "python_full_version >= '3.10'" }, { name = "pydantic-settings", marker = "python_full_version >= '3.10'" }, + { name = "python-multipart", marker = "python_full_version >= '3.10'" }, { name = "sse-starlette", marker = "python_full_version >= '3.10'" }, { name = "starlette", marker = "python_full_version >= '3.10'" }, - { name = "uvicorn", marker = "python_full_version >= '3.10'" }, + { name = "uvicorn", marker = "python_full_version >= '3.10' and sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/d2/f587cb965a56e992634bebc8611c5b579af912b74e04eb9164bd49527d21/mcp-1.6.0.tar.gz", hash = "sha256:d9324876de2c5637369f43161cd71eebfd803df5a95e46225cab8d280e366723", size = 200031 } +sdist = { url = "https://files.pythonhosted.org/packages/7c/13/16b712e8a3be6a736b411df2fc6b4e75eb1d3e99b1cd57a3a1decf17f612/mcp-1.8.1.tar.gz", hash = "sha256:ec0646271d93749f784d2316fb5fe6102fb0d1be788ec70a9e2517e8f2722c0e", size = 265605 } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/30/20a7f33b0b884a9d14dd3aa94ff1ac9da1479fe2ad66dd9e2736075d2506/mcp-1.6.0-py3-none-any.whl", hash = "sha256:7bd24c6ea042dbec44c754f100984d186620d8b841ec30f1b19eda9b93a634d0", size = 76077 }, + { url = "https://files.pythonhosted.org/packages/1c/5d/91cf0d40e40ae9ecf8d4004e0f9611eea86085aa0b5505493e0ff53972da/mcp-1.8.1-py3-none-any.whl", hash = "sha256:948e03783859fa35abe05b9b6c0a1d5519be452fc079dc8d7f682549591c1770", size = 119761 }, ] [[package]] @@ -1533,7 +1534,7 @@ requires-dist = [ { name = "graphviz", marker = "extra == 'viz'", specifier = ">=0.17" }, { name = "griffe", specifier = ">=1.5.6,<2" }, { name = "litellm", marker = "extra == 'litellm'", specifier = ">=1.67.4.post1,<2" }, - { name = "mcp", marker = "python_full_version >= '3.10'", specifier = ">=1.6.0,<2" }, + { name = "mcp", marker = "python_full_version >= '3.10'", specifier = ">=1.8.0,<2" }, { name = "numpy", marker = "python_full_version >= '3.10' and extra == 'voice'", specifier = ">=2.2.0,<3" }, { name = "openai", specifier = ">=1.76.0" }, { name = "pydantic", specifier = ">=2.10,<3" }, @@ -2085,6 +2086,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 }, ] +[[package]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546 }, +] + [[package]] name = "python-xlib" version = "0.33" From 5fe096df67cf7ada53f0bd38954b7f12743b790b Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Thu, 15 May 2025 18:41:00 -0400 Subject: [PATCH 26/35] v0.0.15 (#701) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 87a707d3d..672258c42 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "openai-agents" -version = "0.0.14" +version = "0.0.15" description = "OpenAI Agents SDK" readme = "README.md" requires-python = ">=3.9" From c282324d9519b037ced8abe3678c86f7dd344a97 Mon Sep 17 00:00:00 2001 From: Dominik Kundel Date: Sun, 18 May 2025 10:24:24 -0700 Subject: [PATCH 27/35] Create AGENTS.md (#707) Adding an AGENTS.md file for Codex use --- AGENTS.md | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..ff37db326 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,69 @@ +Welcome to the OpenAI Agents SDK repository. This file contains the main points for new contributors. + +## Repository overview + +- **Source code**: `src/agents/` contains the implementation. +- **Tests**: `tests/` with a short guide in `tests/README.md`. +- **Examples**: under `examples/`. +- **Documentation**: markdown pages live in `docs/` with `mkdocs.yml` controlling the site. +- **Utilities**: developer commands are defined in the `Makefile`. +- **PR template**: `.github/PULL_REQUEST_TEMPLATE/pull_request_template.md` describes the information every PR must include. + +## Local workflow + +1. Format, lint and type‑check your changes: + + ```bash + make format + make lint + make mypy + ``` + +2. Run the tests: + + ```bash + make tests + ``` + + To run a single test, use `uv run pytest -s -k `. + +3. Build the documentation (optional but recommended for docs changes): + + ```bash + make build-docs + ``` + + Coverage can be generated with `make coverage`. + +## Snapshot tests + +Some tests rely on inline snapshots. See `tests/README.md` for details on updating them: + +```bash +make snapshots-fix # update existing snapshots +make snapshots-create # create new snapshots +``` + +Run `make tests` again after updating snapshots to ensure they pass. + +## Style notes + +- Write comments as full sentences and end them with a period. + +## Pull request expectations + +PRs should use the template located at `.github/PULL_REQUEST_TEMPLATE/pull_request_template.md`. Provide a summary, test plan and issue number if applicable, then check that: + +- New tests are added when needed. +- Documentation is updated. +- `make lint` and `make format` have been run. +- The full test suite passes. + +Commit messages should be concise and written in the imperative mood. Small, focused commits are preferred. + +## What reviewers look for + +- Tests covering new behaviour. +- Consistent style: code formatted with `ruff format`, imports sorted, and type hints passing `mypy`. +- Clear documentation for any public API changes. +- Clean history and a helpful PR description. From 003cbfe5f5820cd73ad3adfbae56c054e3cb73ca Mon Sep 17 00:00:00 2001 From: Daniele Morotti <58258368+DanieleMorotti@users.noreply.github.com> Date: Sun, 18 May 2025 19:25:08 +0200 Subject: [PATCH 28/35] Added mcp 'instructions' attribute to the server (#706) Added the `instructions` attribute to the MCP servers to solve #704 . Let me know if you want to add an example to the documentation. --- src/agents/mcp/server.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/agents/mcp/server.py b/src/agents/mcp/server.py index c5255ead7..414b517ab 100644 --- a/src/agents/mcp/server.py +++ b/src/agents/mcp/server.py @@ -12,7 +12,7 @@ from mcp.client.sse import sse_client from mcp.client.streamable_http import GetSessionIdCallback, streamablehttp_client from mcp.shared.message import SessionMessage -from mcp.types import CallToolResult +from mcp.types import CallToolResult, InitializeResult from typing_extensions import NotRequired, TypedDict from ..exceptions import UserError @@ -73,6 +73,7 @@ def __init__(self, cache_tools_list: bool, client_session_timeout_seconds: float self.exit_stack: AsyncExitStack = AsyncExitStack() self._cleanup_lock: asyncio.Lock = asyncio.Lock() self.cache_tools_list = cache_tools_list + self.server_initialize_result: InitializeResult | None = None self.client_session_timeout_seconds = client_session_timeout_seconds @@ -122,7 +123,8 @@ async def connect(self): else None, ) ) - await session.initialize() + server_result = await session.initialize() + self.server_initialize_result = server_result self.session = session except Exception as e: logger.error(f"Error initializing MCP server: {e}") From 428c9a65bf0c17198d9a2a616159b9eb8badb2b6 Mon Sep 17 00:00:00 2001 From: franz101 Date: Mon, 19 May 2025 21:41:11 +0200 Subject: [PATCH 29/35] Add Galileo to external tracing processors list (#662) --- docs/tracing.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/tracing.md b/docs/tracing.md index dd883c5aa..4a9c1bd90 100644 --- a/docs/tracing.md +++ b/docs/tracing.md @@ -115,3 +115,4 @@ To customize this default setup, to send traces to alternative or additional bac - [Langfuse](https://langfuse.com/docs/integrations/openaiagentssdk/openai-agents) - [Langtrace](https://docs.langtrace.ai/supported-integrations/llm-frameworks/openai-agents-sdk) - [Okahu-Monocle](https://github.com/monocle2ai/monocle) +- [Galileo](https://v2docs.galileo.ai/integrations/openai-agent-integration#openai-agent-integration) From 466b44df180718a5d53c45293db2f57b6e719f95 Mon Sep 17 00:00:00 2001 From: WJPBProjects <76624567+WJPBProjects@users.noreply.github.com> Date: Tue, 20 May 2025 18:23:56 +0100 Subject: [PATCH 30/35] Dev/add usage details to Usage class (#726) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR to enhance the `Usage` object and related logic, to support more granular token accounting, matching the details available in the [OpenAI Responses API](https://platform.openai.com/docs/api-reference/responses) . Specifically, it: - Adds `input_tokens_details` and `output_tokens_details` fields to the `Usage` dataclass, storing detailed token breakdowns (e.g., `cached_tokens`, `reasoning_tokens`). - Flows this change through - Updates and extends tests to match - Adds a test for the Usage.add method ### Motivation - Aligns the SDK’s usage with the latest OpenAI responses API Usage object - Supports downstream use cases that require fine-grained token usage data (e.g., billing, analytics, optimization) requested by startups --------- Co-authored-by: Wulfie Bain --- src/agents/extensions/models/litellm_model.py | 11 ++++ src/agents/models/openai_chatcompletions.py | 15 +++++- src/agents/models/openai_responses.py | 2 + src/agents/run.py | 2 + src/agents/usage.py | 22 +++++++- .../test_litellm_chatcompletions_stream.py | 16 +++++- tests/test_extra_headers.py | 20 ++++--- tests/test_openai_chatcompletions.py | 17 +++++- tests/test_openai_chatcompletions_stream.py | 16 +++++- tests/test_responses_tracing.py | 20 ++++++- tests/test_usage.py | 52 +++++++++++++++++++ 11 files changed, 178 insertions(+), 15 deletions(-) create mode 100644 tests/test_usage.py diff --git a/src/agents/extensions/models/litellm_model.py b/src/agents/extensions/models/litellm_model.py index d3b25a198..ffb2c3c1c 100644 --- a/src/agents/extensions/models/litellm_model.py +++ b/src/agents/extensions/models/litellm_model.py @@ -6,6 +6,7 @@ from typing import Any, Literal, cast, overload import litellm.types +from openai.types.responses.response_usage import InputTokensDetails, OutputTokensDetails from agents.exceptions import ModelBehaviorError @@ -107,6 +108,16 @@ async def get_response( input_tokens=response_usage.prompt_tokens, output_tokens=response_usage.completion_tokens, total_tokens=response_usage.total_tokens, + input_tokens_details=InputTokensDetails( + cached_tokens=getattr( + response_usage.prompt_tokens_details, "cached_tokens", 0 + ) + ), + output_tokens_details=OutputTokensDetails( + reasoning_tokens=getattr( + response_usage.completion_tokens_details, "reasoning_tokens", 0 + ) + ), ) if response.usage else Usage() diff --git a/src/agents/models/openai_chatcompletions.py b/src/agents/models/openai_chatcompletions.py index 89619f838..4465ff2fd 100644 --- a/src/agents/models/openai_chatcompletions.py +++ b/src/agents/models/openai_chatcompletions.py @@ -9,6 +9,7 @@ from openai.types import ChatModel from openai.types.chat import ChatCompletion, ChatCompletionChunk from openai.types.responses import Response +from openai.types.responses.response_usage import InputTokensDetails, OutputTokensDetails from .. import _debug from ..agent_output import AgentOutputSchemaBase @@ -83,6 +84,18 @@ async def get_response( input_tokens=response.usage.prompt_tokens, output_tokens=response.usage.completion_tokens, total_tokens=response.usage.total_tokens, + input_tokens_details=InputTokensDetails( + cached_tokens=getattr( + response.usage.prompt_tokens_details, "cached_tokens", 0 + ) + or 0, + ), + output_tokens_details=OutputTokensDetails( + reasoning_tokens=getattr( + response.usage.completion_tokens_details, "reasoning_tokens", 0 + ) + or 0, + ), ) if response.usage else Usage() @@ -252,7 +265,7 @@ async def _fetch_response( stream_options=self._non_null_or_not_given(stream_options), store=self._non_null_or_not_given(store), reasoning_effort=self._non_null_or_not_given(reasoning_effort), - extra_headers={ **HEADERS, **(model_settings.extra_headers or {}) }, + extra_headers={**HEADERS, **(model_settings.extra_headers or {})}, extra_query=model_settings.extra_query, extra_body=model_settings.extra_body, metadata=self._non_null_or_not_given(model_settings.metadata), diff --git a/src/agents/models/openai_responses.py b/src/agents/models/openai_responses.py index c1ff85b98..6ec8f8f7b 100644 --- a/src/agents/models/openai_responses.py +++ b/src/agents/models/openai_responses.py @@ -98,6 +98,8 @@ async def get_response( input_tokens=response.usage.input_tokens, output_tokens=response.usage.output_tokens, total_tokens=response.usage.total_tokens, + input_tokens_details=response.usage.input_tokens_details, + output_tokens_details=response.usage.output_tokens_details, ) if response.usage else Usage() diff --git a/src/agents/run.py b/src/agents/run.py index 849da7bfc..b196c3bf1 100644 --- a/src/agents/run.py +++ b/src/agents/run.py @@ -689,6 +689,8 @@ async def _run_single_turn_streamed( input_tokens=event.response.usage.input_tokens, output_tokens=event.response.usage.output_tokens, total_tokens=event.response.usage.total_tokens, + input_tokens_details=event.response.usage.input_tokens_details, + output_tokens_details=event.response.usage.output_tokens_details, ) if event.response.usage else Usage() diff --git a/src/agents/usage.py b/src/agents/usage.py index 23d989b4b..843f62937 100644 --- a/src/agents/usage.py +++ b/src/agents/usage.py @@ -1,4 +1,6 @@ -from dataclasses import dataclass +from dataclasses import dataclass, field + +from openai.types.responses.response_usage import InputTokensDetails, OutputTokensDetails @dataclass @@ -9,9 +11,18 @@ class Usage: input_tokens: int = 0 """Total input tokens sent, across all requests.""" + input_tokens_details: InputTokensDetails = field( + default_factory=lambda: InputTokensDetails(cached_tokens=0) + ) + """Details about the input tokens, matching responses API usage details.""" output_tokens: int = 0 """Total output tokens received, across all requests.""" + output_tokens_details: OutputTokensDetails = field( + default_factory=lambda: OutputTokensDetails(reasoning_tokens=0) + ) + """Details about the output tokens, matching responses API usage details.""" + total_tokens: int = 0 """Total tokens sent and received, across all requests.""" @@ -20,3 +31,12 @@ def add(self, other: "Usage") -> None: self.input_tokens += other.input_tokens if other.input_tokens else 0 self.output_tokens += other.output_tokens if other.output_tokens else 0 self.total_tokens += other.total_tokens if other.total_tokens else 0 + self.input_tokens_details = InputTokensDetails( + cached_tokens=self.input_tokens_details.cached_tokens + + other.input_tokens_details.cached_tokens + ) + + self.output_tokens_details = OutputTokensDetails( + reasoning_tokens=self.output_tokens_details.reasoning_tokens + + other.output_tokens_details.reasoning_tokens + ) diff --git a/tests/models/test_litellm_chatcompletions_stream.py b/tests/models/test_litellm_chatcompletions_stream.py index 80bd8ea22..06e46b39c 100644 --- a/tests/models/test_litellm_chatcompletions_stream.py +++ b/tests/models/test_litellm_chatcompletions_stream.py @@ -8,7 +8,11 @@ ChoiceDeltaToolCall, ChoiceDeltaToolCallFunction, ) -from openai.types.completion_usage import CompletionUsage +from openai.types.completion_usage import ( + CompletionTokensDetails, + CompletionUsage, + PromptTokensDetails, +) from openai.types.responses import ( Response, ResponseFunctionToolCall, @@ -46,7 +50,13 @@ async def test_stream_response_yields_events_for_text_content(monkeypatch) -> No model="fake", object="chat.completion.chunk", choices=[Choice(index=0, delta=ChoiceDelta(content="llo"))], - usage=CompletionUsage(completion_tokens=5, prompt_tokens=7, total_tokens=12), + usage=CompletionUsage( + completion_tokens=5, + prompt_tokens=7, + total_tokens=12, + completion_tokens_details=CompletionTokensDetails(reasoning_tokens=2), + prompt_tokens_details=PromptTokensDetails(cached_tokens=6), + ), ) async def fake_stream() -> AsyncIterator[ChatCompletionChunk]: @@ -112,6 +122,8 @@ async def patched_fetch_response(self, *args, **kwargs): assert completed_resp.usage.input_tokens == 7 assert completed_resp.usage.output_tokens == 5 assert completed_resp.usage.total_tokens == 12 + assert completed_resp.usage.input_tokens_details.cached_tokens == 6 + assert completed_resp.usage.output_tokens_details.reasoning_tokens == 2 @pytest.mark.allow_call_model_methods diff --git a/tests/test_extra_headers.py b/tests/test_extra_headers.py index f29c25408..a6af30077 100644 --- a/tests/test_extra_headers.py +++ b/tests/test_extra_headers.py @@ -1,6 +1,7 @@ import pytest from openai.types.chat.chat_completion import ChatCompletion, Choice from openai.types.chat.chat_completion_message import ChatCompletionMessage +from openai.types.responses.response_usage import InputTokensDetails, OutputTokensDetails from agents import ModelSettings, ModelTracing, OpenAIChatCompletionsModel, OpenAIResponsesModel @@ -17,21 +18,29 @@ class DummyResponses: async def create(self, **kwargs): nonlocal called_kwargs called_kwargs = kwargs + class DummyResponse: id = "dummy" output = [] usage = type( - "Usage", (), {"input_tokens": 0, "output_tokens": 0, "total_tokens": 0} + "Usage", + (), + { + "input_tokens": 0, + "output_tokens": 0, + "total_tokens": 0, + "input_tokens_details": InputTokensDetails(cached_tokens=0), + "output_tokens_details": OutputTokensDetails(reasoning_tokens=0), + }, )() + return DummyResponse() class DummyClient: def __init__(self): self.responses = DummyResponses() - - - model = OpenAIResponsesModel(model="gpt-4", openai_client=DummyClient()) # type: ignore + model = OpenAIResponsesModel(model="gpt-4", openai_client=DummyClient()) # type: ignore extra_headers = {"X-Test-Header": "test-value"} await model.get_response( system_instructions=None, @@ -47,7 +56,6 @@ def __init__(self): assert called_kwargs["extra_headers"]["X-Test-Header"] == "test-value" - @pytest.mark.allow_call_model_methods @pytest.mark.asyncio async def test_extra_headers_passed_to_openai_client(): @@ -76,7 +84,7 @@ def __init__(self): self.chat = type("_Chat", (), {"completions": DummyCompletions()})() self.base_url = "https://api.openai.com" - model = OpenAIChatCompletionsModel(model="gpt-4", openai_client=DummyClient()) # type: ignore + model = OpenAIChatCompletionsModel(model="gpt-4", openai_client=DummyClient()) # type: ignore extra_headers = {"X-Test-Header": "test-value"} await model.get_response( system_instructions=None, diff --git a/tests/test_openai_chatcompletions.py b/tests/test_openai_chatcompletions.py index ba3ec68d0..ba4605d08 100644 --- a/tests/test_openai_chatcompletions.py +++ b/tests/test_openai_chatcompletions.py @@ -13,7 +13,10 @@ ChatCompletionMessageToolCall, Function, ) -from openai.types.completion_usage import CompletionUsage +from openai.types.completion_usage import ( + CompletionUsage, + PromptTokensDetails, +) from openai.types.responses import ( Response, ResponseFunctionToolCall, @@ -51,7 +54,13 @@ async def test_get_response_with_text_message(monkeypatch) -> None: model="fake", object="chat.completion", choices=[choice], - usage=CompletionUsage(completion_tokens=5, prompt_tokens=7, total_tokens=12), + usage=CompletionUsage( + completion_tokens=5, + prompt_tokens=7, + total_tokens=12, + # completion_tokens_details left blank to test default + prompt_tokens_details=PromptTokensDetails(cached_tokens=3), + ), ) async def patched_fetch_response(self, *args, **kwargs): @@ -81,6 +90,8 @@ async def patched_fetch_response(self, *args, **kwargs): assert resp.usage.input_tokens == 7 assert resp.usage.output_tokens == 5 assert resp.usage.total_tokens == 12 + assert resp.usage.input_tokens_details.cached_tokens == 3 + assert resp.usage.output_tokens_details.reasoning_tokens == 0 assert resp.response_id is None @@ -127,6 +138,8 @@ async def patched_fetch_response(self, *args, **kwargs): assert resp.usage.requests == 0 assert resp.usage.input_tokens == 0 assert resp.usage.output_tokens == 0 + assert resp.usage.input_tokens_details.cached_tokens == 0 + assert resp.usage.output_tokens_details.reasoning_tokens == 0 @pytest.mark.allow_call_model_methods diff --git a/tests/test_openai_chatcompletions_stream.py b/tests/test_openai_chatcompletions_stream.py index b82f24303..5c8bb9e3a 100644 --- a/tests/test_openai_chatcompletions_stream.py +++ b/tests/test_openai_chatcompletions_stream.py @@ -8,7 +8,11 @@ ChoiceDeltaToolCall, ChoiceDeltaToolCallFunction, ) -from openai.types.completion_usage import CompletionUsage +from openai.types.completion_usage import ( + CompletionTokensDetails, + CompletionUsage, + PromptTokensDetails, +) from openai.types.responses import ( Response, ResponseFunctionToolCall, @@ -46,7 +50,13 @@ async def test_stream_response_yields_events_for_text_content(monkeypatch) -> No model="fake", object="chat.completion.chunk", choices=[Choice(index=0, delta=ChoiceDelta(content="llo"))], - usage=CompletionUsage(completion_tokens=5, prompt_tokens=7, total_tokens=12), + usage=CompletionUsage( + completion_tokens=5, + prompt_tokens=7, + total_tokens=12, + prompt_tokens_details=PromptTokensDetails(cached_tokens=2), + completion_tokens_details=CompletionTokensDetails(reasoning_tokens=3), + ), ) async def fake_stream() -> AsyncIterator[ChatCompletionChunk]: @@ -112,6 +122,8 @@ async def patched_fetch_response(self, *args, **kwargs): assert completed_resp.usage.input_tokens == 7 assert completed_resp.usage.output_tokens == 5 assert completed_resp.usage.total_tokens == 12 + assert completed_resp.usage.input_tokens_details.cached_tokens == 2 + assert completed_resp.usage.output_tokens_details.reasoning_tokens == 3 @pytest.mark.allow_call_model_methods diff --git a/tests/test_responses_tracing.py b/tests/test_responses_tracing.py index 0bc97a953..dfac74bb9 100644 --- a/tests/test_responses_tracing.py +++ b/tests/test_responses_tracing.py @@ -1,7 +1,10 @@ +from typing import Optional + import pytest from inline_snapshot import snapshot from openai import AsyncOpenAI from openai.types.responses import ResponseCompletedEvent +from openai.types.responses.response_usage import InputTokensDetails, OutputTokensDetails from agents import ModelSettings, ModelTracing, OpenAIResponsesModel, trace from agents.tracing.span_data import ResponseSpanData @@ -16,10 +19,25 @@ def is_disabled(self): class DummyUsage: - def __init__(self, input_tokens=1, output_tokens=1, total_tokens=2): + def __init__( + self, + input_tokens: int = 1, + input_tokens_details: Optional[InputTokensDetails] = None, + output_tokens: int = 1, + output_tokens_details: Optional[OutputTokensDetails] = None, + total_tokens: int = 2, + ): self.input_tokens = input_tokens self.output_tokens = output_tokens self.total_tokens = total_tokens + self.input_tokens_details = ( + input_tokens_details if input_tokens_details else InputTokensDetails(cached_tokens=0) + ) + self.output_tokens_details = ( + output_tokens_details + if output_tokens_details + else OutputTokensDetails(reasoning_tokens=0) + ) class DummyResponse: diff --git a/tests/test_usage.py b/tests/test_usage.py new file mode 100644 index 000000000..405f99ddf --- /dev/null +++ b/tests/test_usage.py @@ -0,0 +1,52 @@ +from openai.types.responses.response_usage import InputTokensDetails, OutputTokensDetails + +from agents.usage import Usage + + +def test_usage_add_aggregates_all_fields(): + u1 = Usage( + requests=1, + input_tokens=10, + input_tokens_details=InputTokensDetails(cached_tokens=3), + output_tokens=20, + output_tokens_details=OutputTokensDetails(reasoning_tokens=5), + total_tokens=30, + ) + u2 = Usage( + requests=2, + input_tokens=7, + input_tokens_details=InputTokensDetails(cached_tokens=4), + output_tokens=8, + output_tokens_details=OutputTokensDetails(reasoning_tokens=6), + total_tokens=15, + ) + + u1.add(u2) + + assert u1.requests == 3 + assert u1.input_tokens == 17 + assert u1.output_tokens == 28 + assert u1.total_tokens == 45 + assert u1.input_tokens_details.cached_tokens == 7 + assert u1.output_tokens_details.reasoning_tokens == 11 + + +def test_usage_add_aggregates_with_none_values(): + u1 = Usage() + u2 = Usage( + requests=2, + input_tokens=7, + input_tokens_details=InputTokensDetails(cached_tokens=4), + output_tokens=8, + output_tokens_details=OutputTokensDetails(reasoning_tokens=6), + total_tokens=15, + ) + + u1.add(u2) + + assert u1.requests == 2 + assert u1.input_tokens == 7 + assert u1.output_tokens == 8 + assert u1.total_tokens == 15 + assert u1.input_tokens_details.cached_tokens == 4 + assert u1.output_tokens_details.reasoning_tokens == 6 From ce2e2a4571c2b176e8641c558fedaa7bc1692013 Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Wed, 21 May 2025 15:17:58 -0400 Subject: [PATCH 31/35] Upgrade openAI sdk version (#730) --- [//]: # (BEGIN SAPLING FOOTER) * #732 * #731 * __->__ #730 --- pyproject.toml | 2 +- src/agents/models/chatcmpl_stream_handler.py | 26 +++++++++++++++++++- src/agents/models/openai_responses.py | 16 ++++-------- tests/fake_model.py | 1 + tests/test_responses_tracing.py | 4 +++ tests/voice/test_workflow.py | 2 ++ uv.lock | 10 ++++---- 7 files changed, 43 insertions(+), 18 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 672258c42..200ac2485 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ requires-python = ">=3.9" license = "MIT" authors = [{ name = "OpenAI", email = "support@openai.com" }] dependencies = [ - "openai>=1.76.0", + "openai>=1.81.0", "pydantic>=2.10, <3", "griffe>=1.5.6, <2", "typing-extensions>=4.12.2, <5", diff --git a/src/agents/models/chatcmpl_stream_handler.py b/src/agents/models/chatcmpl_stream_handler.py index c71adeb55..d18f5912a 100644 --- a/src/agents/models/chatcmpl_stream_handler.py +++ b/src/agents/models/chatcmpl_stream_handler.py @@ -38,6 +38,16 @@ class StreamingState: function_calls: dict[int, ResponseFunctionToolCall] = field(default_factory=dict) +class SequenceNumber: + def __init__(self): + self._sequence_number = 0 + + def get_and_increment(self) -> int: + num = self._sequence_number + self._sequence_number += 1 + return num + + class ChatCmplStreamHandler: @classmethod async def handle_stream( @@ -47,13 +57,14 @@ async def handle_stream( ) -> AsyncIterator[TResponseStreamEvent]: usage: CompletionUsage | None = None state = StreamingState() - + sequence_number = SequenceNumber() async for chunk in stream: if not state.started: state.started = True yield ResponseCreatedEvent( response=response, type="response.created", + sequence_number=sequence_number.get_and_increment(), ) # This is always set by the OpenAI API, but not by others e.g. LiteLLM @@ -89,6 +100,7 @@ async def handle_stream( item=assistant_item, output_index=0, type="response.output_item.added", + sequence_number=sequence_number.get_and_increment(), ) yield ResponseContentPartAddedEvent( content_index=state.text_content_index_and_output[0], @@ -100,6 +112,7 @@ async def handle_stream( annotations=[], ), type="response.content_part.added", + sequence_number=sequence_number.get_and_increment(), ) # Emit the delta for this segment of content yield ResponseTextDeltaEvent( @@ -108,6 +121,7 @@ async def handle_stream( item_id=FAKE_RESPONSES_ID, output_index=0, type="response.output_text.delta", + sequence_number=sequence_number.get_and_increment(), ) # Accumulate the text into the response part state.text_content_index_and_output[1].text += delta.content @@ -134,6 +148,7 @@ async def handle_stream( item=assistant_item, output_index=0, type="response.output_item.added", + sequence_number=sequence_number.get_and_increment(), ) yield ResponseContentPartAddedEvent( content_index=state.refusal_content_index_and_output[0], @@ -145,6 +160,7 @@ async def handle_stream( annotations=[], ), type="response.content_part.added", + sequence_number=sequence_number.get_and_increment(), ) # Emit the delta for this segment of refusal yield ResponseRefusalDeltaEvent( @@ -153,6 +169,7 @@ async def handle_stream( item_id=FAKE_RESPONSES_ID, output_index=0, type="response.refusal.delta", + sequence_number=sequence_number.get_and_increment(), ) # Accumulate the refusal string in the output part state.refusal_content_index_and_output[1].refusal += delta.refusal @@ -190,6 +207,7 @@ async def handle_stream( output_index=0, part=state.text_content_index_and_output[1], type="response.content_part.done", + sequence_number=sequence_number.get_and_increment(), ) if state.refusal_content_index_and_output: @@ -201,6 +219,7 @@ async def handle_stream( output_index=0, part=state.refusal_content_index_and_output[1], type="response.content_part.done", + sequence_number=sequence_number.get_and_increment(), ) # Actually send events for the function calls @@ -216,6 +235,7 @@ async def handle_stream( ), output_index=function_call_starting_index, type="response.output_item.added", + sequence_number=sequence_number.get_and_increment(), ) # Then, yield the args yield ResponseFunctionCallArgumentsDeltaEvent( @@ -223,6 +243,7 @@ async def handle_stream( item_id=FAKE_RESPONSES_ID, output_index=function_call_starting_index, type="response.function_call_arguments.delta", + sequence_number=sequence_number.get_and_increment(), ) # Finally, the ResponseOutputItemDone yield ResponseOutputItemDoneEvent( @@ -235,6 +256,7 @@ async def handle_stream( ), output_index=function_call_starting_index, type="response.output_item.done", + sequence_number=sequence_number.get_and_increment(), ) # Finally, send the Response completed event @@ -258,6 +280,7 @@ async def handle_stream( item=assistant_msg, output_index=0, type="response.output_item.done", + sequence_number=sequence_number.get_and_increment(), ) for function_call in state.function_calls.values(): @@ -289,4 +312,5 @@ async def handle_stream( yield ResponseCompletedEvent( response=final_response, type="response.completed", + sequence_number=sequence_number.get_and_increment(), ) diff --git a/src/agents/models/openai_responses.py b/src/agents/models/openai_responses.py index 6ec8f8f7b..cb6567909 100644 --- a/src/agents/models/openai_responses.py +++ b/src/agents/models/openai_responses.py @@ -10,6 +10,7 @@ from openai.types.responses import ( Response, ResponseCompletedEvent, + ResponseIncludable, ResponseStreamEvent, ResponseTextConfigParam, ToolParam, @@ -36,13 +37,6 @@ _USER_AGENT = f"Agents/Python {__version__}" _HEADERS = {"User-Agent": _USER_AGENT} -# From the Responses API -IncludeLiteral = Literal[ - "file_search_call.results", - "message.input_image.image_url", - "computer_call_output.output.image_url", -] - class OpenAIResponsesModel(Model): """ @@ -273,7 +267,7 @@ def _get_client(self) -> AsyncOpenAI: @dataclass class ConvertedTools: tools: list[ToolParam] - includes: list[IncludeLiteral] + includes: list[ResponseIncludable] class Converter: @@ -330,7 +324,7 @@ def convert_tools( handoffs: list[Handoff[Any]], ) -> ConvertedTools: converted_tools: list[ToolParam] = [] - includes: list[IncludeLiteral] = [] + includes: list[ResponseIncludable] = [] computer_tools = [tool for tool in tools if isinstance(tool, ComputerTool)] if len(computer_tools) > 1: @@ -348,7 +342,7 @@ def convert_tools( return ConvertedTools(tools=converted_tools, includes=includes) @classmethod - def _convert_tool(cls, tool: Tool) -> tuple[ToolParam, IncludeLiteral | None]: + def _convert_tool(cls, tool: Tool) -> tuple[ToolParam, ResponseIncludable | None]: """Returns converted tool and includes""" if isinstance(tool, FunctionTool): @@ -359,7 +353,7 @@ def _convert_tool(cls, tool: Tool) -> tuple[ToolParam, IncludeLiteral | None]: "type": "function", "description": tool.description, } - includes: IncludeLiteral | None = None + includes: ResponseIncludable | None = None elif isinstance(tool, WebSearchTool): ws: WebSearchToolParam = { "type": "web_search_preview", diff --git a/tests/fake_model.py b/tests/fake_model.py index 32f919ef1..9f0c83a2f 100644 --- a/tests/fake_model.py +++ b/tests/fake_model.py @@ -129,6 +129,7 @@ async def stream_response( yield ResponseCompletedEvent( type="response.completed", response=get_response_obj(output, usage=self.hardcoded_usage), + sequence_number=0, ) diff --git a/tests/test_responses_tracing.py b/tests/test_responses_tracing.py index dfac74bb9..db24fe496 100644 --- a/tests/test_responses_tracing.py +++ b/tests/test_responses_tracing.py @@ -50,6 +50,7 @@ def __aiter__(self): yield ResponseCompletedEvent( type="response.completed", response=fake_model.get_response_obj(self.output), + sequence_number=0, ) @@ -201,6 +202,7 @@ async def __aiter__(self): yield ResponseCompletedEvent( type="response.completed", response=fake_model.get_response_obj([], "dummy-id-123"), + sequence_number=0, ) return DummyStream() @@ -253,6 +255,7 @@ async def __aiter__(self): yield ResponseCompletedEvent( type="response.completed", response=fake_model.get_response_obj([], "dummy-id-123"), + sequence_number=0, ) return DummyStream() @@ -304,6 +307,7 @@ async def __aiter__(self): yield ResponseCompletedEvent( type="response.completed", response=fake_model.get_response_obj([], "dummy-id-123"), + sequence_number=0, ) return DummyStream() diff --git a/tests/voice/test_workflow.py b/tests/voice/test_workflow.py index 2bdf2a657..035a05d56 100644 --- a/tests/voice/test_workflow.py +++ b/tests/voice/test_workflow.py @@ -81,11 +81,13 @@ async def stream_response( type="response.output_text.delta", output_index=0, item_id=item.id, + sequence_number=0, ) yield ResponseCompletedEvent( type="response.completed", response=get_response_obj(output), + sequence_number=1, ) diff --git a/uv.lock b/uv.lock index 6ccc19966..7a0cb1e6b 100644 --- a/uv.lock +++ b/uv.lock @@ -1461,7 +1461,7 @@ wheels = [ [[package]] name = "openai" -version = "1.76.0" +version = "1.81.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1473,14 +1473,14 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/84/51/817969ec969b73d8ddad085670ecd8a45ef1af1811d8c3b8a177ca4d1309/openai-1.76.0.tar.gz", hash = "sha256:fd2bfaf4608f48102d6b74f9e11c5ecaa058b60dad9c36e409c12477dfd91fb2", size = 434660 } +sdist = { url = "https://files.pythonhosted.org/packages/1c/89/a1e4f3fa7ca4f7fec90dbf47d93b7cd5ff65924926733af15044e302a192/openai-1.81.0.tar.gz", hash = "sha256:349567a8607e0bcffd28e02f96b5c2397d0d25d06732d90ab3ecbf97abf030f9", size = 456861 } wheels = [ - { url = "https://files.pythonhosted.org/packages/59/aa/84e02ab500ca871eb8f62784426963a1c7c17a72fea3c7f268af4bbaafa5/openai-1.76.0-py3-none-any.whl", hash = "sha256:a712b50e78cf78e6d7b2a8f69c4978243517c2c36999756673e07a14ce37dc0a", size = 661201 }, + { url = "https://files.pythonhosted.org/packages/02/66/bcc7f9bf48e8610a33e3b5c96a5a644dad032d92404ea2a5e8b43ba067e8/openai-1.81.0-py3-none-any.whl", hash = "sha256:1c71572e22b43876c5d7d65ade0b7b516bb527c3d44ae94111267a09125f7bae", size = 717529 }, ] [[package]] name = "openai-agents" -version = "0.0.14" +version = "0.0.15" source = { editable = "." } dependencies = [ { name = "griffe" }, @@ -1536,7 +1536,7 @@ requires-dist = [ { name = "litellm", marker = "extra == 'litellm'", specifier = ">=1.67.4.post1,<2" }, { name = "mcp", marker = "python_full_version >= '3.10'", specifier = ">=1.8.0,<2" }, { name = "numpy", marker = "python_full_version >= '3.10' and extra == 'voice'", specifier = ">=2.2.0,<3" }, - { name = "openai", specifier = ">=1.76.0" }, + { name = "openai", specifier = ">=1.81.0" }, { name = "pydantic", specifier = ">=2.10,<3" }, { name = "requests", specifier = ">=2.0,<3" }, { name = "types-requests", specifier = ">=2.0,<3" }, From 9fa5c39d69937a215a6f247883243fe38c5a39c2 Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Wed, 21 May 2025 15:21:37 -0400 Subject: [PATCH 32/35] Hosted MCP support (#731) --- [//]: # (BEGIN SAPLING FOOTER) * #732 * __->__ #731 --- examples/hosted_mcp/__init__.py | 0 examples/hosted_mcp/approvals.py | 61 ++++++++++++++ examples/hosted_mcp/simple.py | 47 +++++++++++ src/agents/__init__.py | 8 ++ src/agents/_run_impl.py | 114 ++++++++++++++++++++++++-- src/agents/items.py | 41 ++++++++- src/agents/models/openai_responses.py | 6 +- src/agents/stream_events.py | 2 + src/agents/tool.py | 64 ++++++++++++++- 9 files changed, 332 insertions(+), 11 deletions(-) create mode 100644 examples/hosted_mcp/__init__.py create mode 100644 examples/hosted_mcp/approvals.py create mode 100644 examples/hosted_mcp/simple.py diff --git a/examples/hosted_mcp/__init__.py b/examples/hosted_mcp/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/hosted_mcp/approvals.py b/examples/hosted_mcp/approvals.py new file mode 100644 index 000000000..2cabb3ee2 --- /dev/null +++ b/examples/hosted_mcp/approvals.py @@ -0,0 +1,61 @@ +import argparse +import asyncio + +from agents import ( + Agent, + HostedMCPTool, + MCPToolApprovalFunctionResult, + MCPToolApprovalRequest, + Runner, +) + +"""This example demonstrates how to use the hosted MCP support in the OpenAI Responses API, with +approval callbacks.""" + + +def approval_callback(request: MCPToolApprovalRequest) -> MCPToolApprovalFunctionResult: + answer = input(f"Approve running the tool `{request.data.name}`? (y/n) ") + result: MCPToolApprovalFunctionResult = {"approve": answer == "y"} + if not result["approve"]: + result["reason"] = "User denied" + return result + + +async def main(verbose: bool, stream: bool): + agent = Agent( + name="Assistant", + tools=[ + HostedMCPTool( + tool_config={ + "type": "mcp", + "server_label": "gitmcp", + "server_url": "https://gitmcp.io/openai/codex", + "require_approval": "always", + }, + on_approval_request=approval_callback, + ) + ], + ) + + if stream: + result = Runner.run_streamed(agent, "Which language is this repo written in?") + async for event in result.stream_events(): + if event.type == "run_item_stream_event": + print(f"Got event of type {event.item.__class__.__name__}") + print(f"Done streaming; final result: {result.final_output}") + else: + res = await Runner.run(agent, "Which language is this repo written in?") + print(res.final_output) + + if verbose: + for item in result.new_items: + print(item) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--verbose", action="store_true", default=False) + parser.add_argument("--stream", action="store_true", default=False) + args = parser.parse_args() + + asyncio.run(main(args.verbose, args.stream)) diff --git a/examples/hosted_mcp/simple.py b/examples/hosted_mcp/simple.py new file mode 100644 index 000000000..508c3a7ae --- /dev/null +++ b/examples/hosted_mcp/simple.py @@ -0,0 +1,47 @@ +import argparse +import asyncio + +from agents import Agent, HostedMCPTool, Runner + +"""This example demonstrates how to use the hosted MCP support in the OpenAI Responses API, with +approvals not required for any tools. You should only use this for trusted MCP servers.""" + + +async def main(verbose: bool, stream: bool): + agent = Agent( + name="Assistant", + tools=[ + HostedMCPTool( + tool_config={ + "type": "mcp", + "server_label": "gitmcp", + "server_url": "https://gitmcp.io/openai/codex", + "require_approval": "never", + } + ) + ], + ) + + if stream: + result = Runner.run_streamed(agent, "Which language is this repo written in?") + async for event in result.stream_events(): + if event.type == "run_item_stream_event": + print(f"Got event of type {event.item.__class__.__name__}") + print(f"Done streaming; final result: {result.final_output}") + else: + res = await Runner.run(agent, "Which language is this repo written in?") + print(res.final_output) + # The repository is primarily written in multiple languages, including Rust and TypeScript... + + if verbose: + for item in result.new_items: + print(item) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--verbose", action="store_true", default=False) + parser.add_argument("--stream", action="store_true", default=False) + args = parser.parse_args() + + asyncio.run(main(args.verbose, args.stream)) diff --git a/src/agents/__init__.py b/src/agents/__init__.py index 6d7c90b4f..36c26b80d 100644 --- a/src/agents/__init__.py +++ b/src/agents/__init__.py @@ -58,6 +58,10 @@ FileSearchTool, FunctionTool, FunctionToolResult, + HostedMCPTool, + MCPToolApprovalFunction, + MCPToolApprovalFunctionResult, + MCPToolApprovalRequest, Tool, WebSearchTool, default_tool_error_function, @@ -208,6 +212,10 @@ def enable_verbose_stdout_logging(): "FileSearchTool", "Tool", "WebSearchTool", + "HostedMCPTool", + "MCPToolApprovalFunction", + "MCPToolApprovalRequest", + "MCPToolApprovalFunctionResult", "function_tool", "Usage", "add_trace_processor", diff --git a/src/agents/_run_impl.py b/src/agents/_run_impl.py index b5a83685c..ab1e78797 100644 --- a/src/agents/_run_impl.py +++ b/src/agents/_run_impl.py @@ -25,7 +25,8 @@ ActionType, ActionWait, ) -from openai.types.responses.response_input_param import ComputerCallOutput +from openai.types.responses.response_input_param import ComputerCallOutput, McpApprovalResponse +from openai.types.responses.response_output_item import McpApprovalRequest, McpCall, McpListTools from openai.types.responses.response_reasoning_item import ResponseReasoningItem from .agent import Agent, ToolsToFinalOutputResult @@ -38,6 +39,9 @@ HandoffCallItem, HandoffOutputItem, ItemHelpers, + MCPApprovalRequestItem, + MCPApprovalResponseItem, + MCPListToolsItem, MessageOutputItem, ModelResponse, ReasoningItem, @@ -52,7 +56,14 @@ from .models.interface import ModelTracing from .run_context import RunContextWrapper, TContext from .stream_events import RunItemStreamEvent, StreamEvent -from .tool import ComputerTool, FunctionTool, FunctionToolResult, Tool +from .tool import ( + ComputerTool, + FunctionTool, + FunctionToolResult, + HostedMCPTool, + MCPToolApprovalRequest, + Tool, +) from .tracing import ( SpanError, Trace, @@ -112,6 +123,12 @@ class ToolRunComputerAction: computer_tool: ComputerTool +@dataclass +class ToolRunMCPApprovalRequest: + request_item: McpApprovalRequest + mcp_tool: HostedMCPTool + + @dataclass class ProcessedResponse: new_items: list[RunItem] @@ -119,8 +136,9 @@ class ProcessedResponse: functions: list[ToolRunFunction] computer_actions: list[ToolRunComputerAction] tools_used: list[str] # Names of all tools used, including hosted tools + mcp_approval_requests: list[ToolRunMCPApprovalRequest] # Only requests with callbacks - def has_tools_to_run(self) -> bool: + def has_tools_or_approvals_to_run(self) -> bool: # Handoffs, functions and computer actions need local processing # Hosted tools have already run, so there's nothing to do. return any( @@ -128,6 +146,7 @@ def has_tools_to_run(self) -> bool: self.handoffs, self.functions, self.computer_actions, + self.mcp_approval_requests, ] ) @@ -226,7 +245,16 @@ async def execute_tools_and_side_effects( new_step_items.extend([result.run_item for result in function_results]) new_step_items.extend(computer_results) - # Second, check if there are any handoffs + # Next, run the MCP approval requests + if processed_response.mcp_approval_requests: + approval_results = await cls.execute_mcp_approval_requests( + agent=agent, + approval_requests=processed_response.mcp_approval_requests, + context_wrapper=context_wrapper, + ) + new_step_items.extend(approval_results) + + # Next, check if there are any handoffs if run_handoffs := processed_response.handoffs: return await cls.execute_handoffs( agent=agent, @@ -240,7 +268,7 @@ async def execute_tools_and_side_effects( run_config=run_config, ) - # Third, we'll check if the tool use should result in a final output + # Next, we'll check if the tool use should result in a final output check_tool_use = await cls._check_for_final_output_from_tools( agent=agent, tool_results=function_results, @@ -295,7 +323,7 @@ async def execute_tools_and_side_effects( ) elif ( not output_schema or output_schema.is_plain_text() - ) and not processed_response.has_tools_to_run(): + ) and not processed_response.has_tools_or_approvals_to_run(): return await cls.execute_final_output( agent=agent, original_input=original_input, @@ -343,10 +371,16 @@ def process_model_response( run_handoffs = [] functions = [] computer_actions = [] + mcp_approval_requests = [] tools_used: list[str] = [] handoff_map = {handoff.tool_name: handoff for handoff in handoffs} function_map = {tool.name: tool for tool in all_tools if isinstance(tool, FunctionTool)} computer_tool = next((tool for tool in all_tools if isinstance(tool, ComputerTool)), None) + hosted_mcp_server_map = { + tool.tool_config["server_label"]: tool + for tool in all_tools + if isinstance(tool, HostedMCPTool) + } for output in response.output: if isinstance(output, ResponseOutputMessage): @@ -375,6 +409,34 @@ def process_model_response( computer_actions.append( ToolRunComputerAction(tool_call=output, computer_tool=computer_tool) ) + elif isinstance(output, McpApprovalRequest): + items.append(MCPApprovalRequestItem(raw_item=output, agent=agent)) + if output.server_label not in hosted_mcp_server_map: + _error_tracing.attach_error_to_current_span( + SpanError( + message="MCP server label not found", + data={"server_label": output.server_label}, + ) + ) + raise ModelBehaviorError(f"MCP server label {output.server_label} not found") + else: + server = hosted_mcp_server_map[output.server_label] + if server.on_approval_request: + mcp_approval_requests.append( + ToolRunMCPApprovalRequest( + request_item=output, + mcp_tool=server, + ) + ) + else: + logger.warning( + f"MCP server {output.server_label} has no on_approval_request hook" + ) + elif isinstance(output, McpListTools): + items.append(MCPListToolsItem(raw_item=output, agent=agent)) + elif isinstance(output, McpCall): + items.append(ToolCallItem(raw_item=output, agent=agent)) + tools_used.append(output.name) elif not isinstance(output, ResponseFunctionToolCall): logger.warning(f"Unexpected output type, ignoring: {type(output)}") continue @@ -417,6 +479,7 @@ def process_model_response( functions=functions, computer_actions=computer_actions, tools_used=tools_used, + mcp_approval_requests=mcp_approval_requests, ) @classmethod @@ -643,6 +706,40 @@ async def execute_handoffs( next_step=NextStepHandoff(new_agent), ) + @classmethod + async def execute_mcp_approval_requests( + cls, + *, + agent: Agent[TContext], + approval_requests: list[ToolRunMCPApprovalRequest], + context_wrapper: RunContextWrapper[TContext], + ) -> list[RunItem]: + async def run_single_approval(approval_request: ToolRunMCPApprovalRequest) -> RunItem: + callback = approval_request.mcp_tool.on_approval_request + assert callback is not None, "Callback is required for MCP approval requests" + maybe_awaitable_result = callback( + MCPToolApprovalRequest(context_wrapper, approval_request.request_item) + ) + if inspect.isawaitable(maybe_awaitable_result): + result = await maybe_awaitable_result + else: + result = maybe_awaitable_result + reason = result.get("reason", None) + raw_item: McpApprovalResponse = { + "approval_request_id": approval_request.request_item.id, + "approve": result["approve"], + "type": "mcp_approval_response", + } + if not result["approve"] and reason: + raw_item["reason"] = reason + return MCPApprovalResponseItem( + raw_item=raw_item, + agent=agent, + ) + + tasks = [run_single_approval(approval_request) for approval_request in approval_requests] + return await asyncio.gather(*tasks) + @classmethod async def execute_final_output( cls, @@ -727,6 +824,11 @@ def stream_step_result_to_queue( event = RunItemStreamEvent(item=item, name="tool_output") elif isinstance(item, ReasoningItem): event = RunItemStreamEvent(item=item, name="reasoning_item_created") + elif isinstance(item, MCPApprovalRequestItem): + event = RunItemStreamEvent(item=item, name="mcp_approval_requested") + elif isinstance(item, MCPListToolsItem): + event = RunItemStreamEvent(item=item, name="mcp_list_tools") + else: logger.warning(f"Unexpected item type: {type(item)}") event = None diff --git a/src/agents/items.py b/src/agents/items.py index 8fb2b52a3..65a911798 100644 --- a/src/agents/items.py +++ b/src/agents/items.py @@ -18,7 +18,12 @@ ResponseOutputText, ResponseStreamEvent, ) -from openai.types.responses.response_input_item_param import ComputerCallOutput, FunctionCallOutput +from openai.types.responses.response_input_item_param import ( + ComputerCallOutput, + FunctionCallOutput, + McpApprovalResponse, +) +from openai.types.responses.response_output_item import McpApprovalRequest, McpCall, McpListTools from openai.types.responses.response_reasoning_item import ResponseReasoningItem from pydantic import BaseModel from typing_extensions import TypeAlias @@ -108,6 +113,7 @@ class HandoffOutputItem(RunItemBase[TResponseInputItem]): ResponseComputerToolCall, ResponseFileSearchToolCall, ResponseFunctionWebSearch, + McpCall, ] """A type that represents a tool call item.""" @@ -147,6 +153,36 @@ class ReasoningItem(RunItemBase[ResponseReasoningItem]): type: Literal["reasoning_item"] = "reasoning_item" +@dataclass +class MCPListToolsItem(RunItemBase[McpListTools]): + """Represents a call to an MCP server to list tools.""" + + raw_item: McpListTools + """The raw MCP list tools call.""" + + type: Literal["mcp_list_tools_item"] = "mcp_list_tools_item" + + +@dataclass +class MCPApprovalRequestItem(RunItemBase[McpApprovalRequest]): + """Represents a request for MCP approval.""" + + raw_item: McpApprovalRequest + """The raw MCP approval request.""" + + type: Literal["mcp_approval_request_item"] = "mcp_approval_request_item" + + +@dataclass +class MCPApprovalResponseItem(RunItemBase[McpApprovalResponse]): + """Represents a response to an MCP approval request.""" + + raw_item: McpApprovalResponse + """The raw MCP approval response.""" + + type: Literal["mcp_approval_response_item"] = "mcp_approval_response_item" + + RunItem: TypeAlias = Union[ MessageOutputItem, HandoffCallItem, @@ -154,6 +190,9 @@ class ReasoningItem(RunItemBase[ResponseReasoningItem]): ToolCallItem, ToolCallOutputItem, ReasoningItem, + MCPListToolsItem, + MCPApprovalRequestItem, + MCPApprovalResponseItem, ] """An item generated by an agent.""" diff --git a/src/agents/models/openai_responses.py b/src/agents/models/openai_responses.py index cb6567909..65a4f5caf 100644 --- a/src/agents/models/openai_responses.py +++ b/src/agents/models/openai_responses.py @@ -24,7 +24,7 @@ from ..handoffs import Handoff from ..items import ItemHelpers, ModelResponse, TResponseInputItem from ..logger import logger -from ..tool import ComputerTool, FileSearchTool, FunctionTool, Tool, WebSearchTool +from ..tool import ComputerTool, FileSearchTool, FunctionTool, HostedMCPTool, Tool, WebSearchTool from ..tracing import SpanError, response_span from ..usage import Usage from ..version import __version__ @@ -383,7 +383,9 @@ def _convert_tool(cls, tool: Tool) -> tuple[ToolParam, ResponseIncludable | None "display_height": tool.computer.dimensions[1], } includes = None - + elif isinstance(tool, HostedMCPTool): + converted_tool = tool.tool_config + includes = None else: raise UserError(f"Unknown tool type: {type(tool)}, tool") diff --git a/src/agents/stream_events.py b/src/agents/stream_events.py index bd37d11f3..111d0b951 100644 --- a/src/agents/stream_events.py +++ b/src/agents/stream_events.py @@ -35,6 +35,8 @@ class RunItemStreamEvent: "tool_called", "tool_output", "reasoning_item_created", + "mcp_approval_requested", + "mcp_list_tools", ] """The name of the event.""" diff --git a/src/agents/tool.py b/src/agents/tool.py index c1c162423..3bcd57c2e 100644 --- a/src/agents/tool.py +++ b/src/agents/tool.py @@ -7,9 +7,11 @@ from typing import Any, Callable, Literal, Union, overload from openai.types.responses.file_search_tool_param import Filters, RankingOptions +from openai.types.responses.response_output_item import McpApprovalRequest +from openai.types.responses.tool_param import Mcp from openai.types.responses.web_search_tool_param import UserLocation from pydantic import ValidationError -from typing_extensions import Concatenate, ParamSpec +from typing_extensions import Concatenate, NotRequired, ParamSpec, TypedDict from . import _debug from .computer import AsyncComputer, Computer @@ -130,7 +132,55 @@ def name(self): return "computer_use_preview" -Tool = Union[FunctionTool, FileSearchTool, WebSearchTool, ComputerTool] +@dataclass +class MCPToolApprovalRequest: + """A request to approve a tool call.""" + + ctx_wrapper: RunContextWrapper[Any] + """The run context.""" + + data: McpApprovalRequest + """The data from the MCP tool approval request.""" + + +class MCPToolApprovalFunctionResult(TypedDict): + """The result of an MCP tool approval function.""" + + approve: bool + """Whether to approve the tool call.""" + + reason: NotRequired[str] + """An optional reason, if rejected.""" + + +MCPToolApprovalFunction = Callable[ + [MCPToolApprovalRequest], MaybeAwaitable[MCPToolApprovalFunctionResult] +] +"""A function that approves or rejects a tool call.""" + + +@dataclass +class HostedMCPTool: + """A tool that allows the LLM to use a remote MCP server. The LLM will automatically list and + call tools, without requiring a a round trip back to your code. + If you want to run MCP servers locally via stdio, in a VPC or other non-publicly-accessible + environment, or you just prefer to run tool calls locally, then you can instead use the servers + in `agents.mcp` and pass `Agent(mcp_servers=[...])` to the agent.""" + + tool_config: Mcp + """The MCP tool config, which includes the server URL and other settings.""" + + on_approval_request: MCPToolApprovalFunction | None = None + """An optional function that will be called if approval is requested for an MCP tool. If not + provided, you will need to manually add approvals/rejections to the input and call + `Runner.run(...)` again.""" + + @property + def name(self): + return "hosted_mcp" + + +Tool = Union[FunctionTool, FileSearchTool, WebSearchTool, ComputerTool, HostedMCPTool] """A tool that can be used in an agent.""" @@ -308,3 +358,13 @@ def decorator(real_func: ToolFunction[...]) -> FunctionTool: return _create_function_tool(real_func) return decorator + return decorator + return decorator + return decorator + return decorator + return decorator + return decorator + return decorator + return decorator + return decorator + return decorator From 079764f0ab463fda9ecf397b0a5d8e466e87a86c Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Wed, 21 May 2025 15:26:22 -0400 Subject: [PATCH 33/35] Add support for local shell, image generator, code interpreter tools (#732) --- examples/tools/code_interpreter.py | 34 +++++++ examples/tools/image_generator.py | 54 +++++++++++ src/agents/__init__.py | 10 +++ src/agents/_run_impl.py | 124 +++++++++++++++++++++++++- src/agents/items.py | 21 ++++- src/agents/models/openai_responses.py | 35 +++++++- src/agents/tool.py | 76 +++++++++++++--- 7 files changed, 334 insertions(+), 20 deletions(-) create mode 100644 examples/tools/code_interpreter.py create mode 100644 examples/tools/image_generator.py diff --git a/examples/tools/code_interpreter.py b/examples/tools/code_interpreter.py new file mode 100644 index 000000000..a5843ce3f --- /dev/null +++ b/examples/tools/code_interpreter.py @@ -0,0 +1,34 @@ +import asyncio + +from agents import Agent, CodeInterpreterTool, Runner, trace + + +async def main(): + agent = Agent( + name="Code interpreter", + instructions="You love doing math.", + tools=[ + CodeInterpreterTool( + tool_config={"type": "code_interpreter", "container": {"type": "auto"}}, + ) + ], + ) + + with trace("Code interpreter example"): + print("Solving math problem...") + result = Runner.run_streamed(agent, "What is the square root of273 * 312821 plus 1782?") + async for event in result.stream_events(): + if ( + event.type == "run_item_stream_event" + and event.item.type == "tool_call_item" + and event.item.raw_item.type == "code_interpreter_call" + ): + print(f"Code interpreter code:\n```\n{event.item.raw_item.code}\n```\n") + elif event.type == "run_item_stream_event": + print(f"Other event: {event.item.type}") + + print(f"Final output: {result.final_output}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/tools/image_generator.py b/examples/tools/image_generator.py new file mode 100644 index 000000000..fd6fcc6ba --- /dev/null +++ b/examples/tools/image_generator.py @@ -0,0 +1,54 @@ +import asyncio +import base64 +import os +import subprocess +import sys +import tempfile + +from agents import Agent, ImageGenerationTool, Runner, trace + + +def open_file(path: str) -> None: + if sys.platform.startswith("darwin"): + subprocess.run(["open", path], check=False) # macOS + elif os.name == "nt": # Windows + os.astartfile(path) # type: ignore + elif os.name == "posix": + subprocess.run(["xdg-open", path], check=False) # Linux/Unix + else: + print(f"Don't know how to open files on this platform: {sys.platform}") + + +async def main(): + agent = Agent( + name="Image generator", + instructions="You are a helpful agent.", + tools=[ + ImageGenerationTool( + tool_config={"type": "image_generation", "quality": "low"}, + ) + ], + ) + + with trace("Image generation example"): + print("Generating image, this may take a while...") + result = await Runner.run( + agent, "Create an image of a frog eating a pizza, comic book style." + ) + print(result.final_output) + for item in result.new_items: + if ( + item.type == "tool_call_item" + and item.raw_item.type == "image_generation_call" + and (img_result := item.raw_item.result) + ): + with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp: + tmp.write(base64.b64decode(img_result)) + temp_path = tmp.name + + # Open the image + open_file(temp_path) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/src/agents/__init__.py b/src/agents/__init__.py index 36c26b80d..58949157a 100644 --- a/src/agents/__init__.py +++ b/src/agents/__init__.py @@ -54,11 +54,16 @@ StreamEvent, ) from .tool import ( + CodeInterpreterTool, ComputerTool, FileSearchTool, FunctionTool, FunctionToolResult, HostedMCPTool, + ImageGenerationTool, + LocalShellCommandRequest, + LocalShellExecutor, + LocalShellTool, MCPToolApprovalFunction, MCPToolApprovalFunctionResult, MCPToolApprovalRequest, @@ -210,6 +215,11 @@ def enable_verbose_stdout_logging(): "FunctionToolResult", "ComputerTool", "FileSearchTool", + "CodeInterpreterTool", + "ImageGenerationTool", + "LocalShellCommandRequest", + "LocalShellExecutor", + "LocalShellTool", "Tool", "WebSearchTool", "HostedMCPTool", diff --git a/src/agents/_run_impl.py b/src/agents/_run_impl.py index ab1e78797..2cfa270e0 100644 --- a/src/agents/_run_impl.py +++ b/src/agents/_run_impl.py @@ -14,6 +14,9 @@ ResponseFunctionWebSearch, ResponseOutputMessage, ) +from openai.types.responses.response_code_interpreter_tool_call import ( + ResponseCodeInterpreterToolCall, +) from openai.types.responses.response_computer_tool_call import ( ActionClick, ActionDoubleClick, @@ -26,7 +29,12 @@ ActionWait, ) from openai.types.responses.response_input_param import ComputerCallOutput, McpApprovalResponse -from openai.types.responses.response_output_item import McpApprovalRequest, McpCall, McpListTools +from openai.types.responses.response_output_item import ( + ImageGenerationCall, + LocalShellCall, + McpApprovalRequest, + McpListTools, +) from openai.types.responses.response_reasoning_item import ResponseReasoningItem from .agent import Agent, ToolsToFinalOutputResult @@ -61,6 +69,8 @@ FunctionTool, FunctionToolResult, HostedMCPTool, + LocalShellCommandRequest, + LocalShellTool, MCPToolApprovalRequest, Tool, ) @@ -129,12 +139,19 @@ class ToolRunMCPApprovalRequest: mcp_tool: HostedMCPTool +@dataclass +class ToolRunLocalShellCall: + tool_call: LocalShellCall + local_shell_tool: LocalShellTool + + @dataclass class ProcessedResponse: new_items: list[RunItem] handoffs: list[ToolRunHandoff] functions: list[ToolRunFunction] computer_actions: list[ToolRunComputerAction] + local_shell_calls: list[ToolRunLocalShellCall] tools_used: list[str] # Names of all tools used, including hosted tools mcp_approval_requests: list[ToolRunMCPApprovalRequest] # Only requests with callbacks @@ -146,6 +163,7 @@ def has_tools_or_approvals_to_run(self) -> bool: self.handoffs, self.functions, self.computer_actions, + self.local_shell_calls, self.mcp_approval_requests, ] ) @@ -371,11 +389,15 @@ def process_model_response( run_handoffs = [] functions = [] computer_actions = [] + local_shell_calls = [] mcp_approval_requests = [] tools_used: list[str] = [] handoff_map = {handoff.tool_name: handoff for handoff in handoffs} function_map = {tool.name: tool for tool in all_tools if isinstance(tool, FunctionTool)} computer_tool = next((tool for tool in all_tools if isinstance(tool, ComputerTool)), None) + local_shell_tool = next( + (tool for tool in all_tools if isinstance(tool, LocalShellTool)), None + ) hosted_mcp_server_map = { tool.tool_config["server_label"]: tool for tool in all_tools @@ -434,9 +456,29 @@ def process_model_response( ) elif isinstance(output, McpListTools): items.append(MCPListToolsItem(raw_item=output, agent=agent)) - elif isinstance(output, McpCall): + elif isinstance(output, ImageGenerationCall): + items.append(ToolCallItem(raw_item=output, agent=agent)) + tools_used.append("image_generation") + elif isinstance(output, ResponseCodeInterpreterToolCall): items.append(ToolCallItem(raw_item=output, agent=agent)) - tools_used.append(output.name) + tools_used.append("code_interpreter") + elif isinstance(output, LocalShellCall): + items.append(ToolCallItem(raw_item=output, agent=agent)) + tools_used.append("local_shell") + if not local_shell_tool: + _error_tracing.attach_error_to_current_span( + SpanError( + message="Local shell tool not found", + data={}, + ) + ) + raise ModelBehaviorError( + "Model produced local shell call without a local shell tool." + ) + local_shell_calls.append( + ToolRunLocalShellCall(tool_call=output, local_shell_tool=local_shell_tool) + ) + elif not isinstance(output, ResponseFunctionToolCall): logger.warning(f"Unexpected output type, ignoring: {type(output)}") continue @@ -478,6 +520,7 @@ def process_model_response( handoffs=run_handoffs, functions=functions, computer_actions=computer_actions, + local_shell_calls=local_shell_calls, tools_used=tools_used, mcp_approval_requests=mcp_approval_requests, ) @@ -552,6 +595,30 @@ async def run_single_tool( for tool_run, result in zip(tool_runs, results) ] + @classmethod + async def execute_local_shell_calls( + cls, + *, + agent: Agent[TContext], + calls: list[ToolRunLocalShellCall], + context_wrapper: RunContextWrapper[TContext], + hooks: RunHooks[TContext], + config: RunConfig, + ) -> list[RunItem]: + results: list[RunItem] = [] + # Need to run these serially, because each call can affect the local shell state + for call in calls: + results.append( + await LocalShellAction.execute( + agent=agent, + call=call, + hooks=hooks, + context_wrapper=context_wrapper, + config=config, + ) + ) + return results + @classmethod async def execute_computer_actions( cls, @@ -1021,3 +1088,54 @@ async def _get_screenshot_async( await computer.wait() return await computer.screenshot() + + +class LocalShellAction: + @classmethod + async def execute( + cls, + *, + agent: Agent[TContext], + call: ToolRunLocalShellCall, + hooks: RunHooks[TContext], + context_wrapper: RunContextWrapper[TContext], + config: RunConfig, + ) -> RunItem: + await asyncio.gather( + hooks.on_tool_start(context_wrapper, agent, call.local_shell_tool), + ( + agent.hooks.on_tool_start(context_wrapper, agent, call.local_shell_tool) + if agent.hooks + else _coro.noop_coroutine() + ), + ) + + request = LocalShellCommandRequest( + ctx_wrapper=context_wrapper, + data=call.tool_call, + ) + output = call.local_shell_tool.executor(request) + if inspect.isawaitable(output): + result = await output + else: + result = output + + await asyncio.gather( + hooks.on_tool_end(context_wrapper, agent, call.local_shell_tool, result), + ( + agent.hooks.on_tool_end(context_wrapper, agent, call.local_shell_tool, result) + if agent.hooks + else _coro.noop_coroutine() + ), + ) + + return ToolCallOutputItem( + agent=agent, + output=output, + raw_item={ + "type": "local_shell_call_output", + "id": call.tool_call.call_id, + "output": result, + # "id": "out" + call.tool_call.id, # TODO remove this, it should be optional + }, + ) diff --git a/src/agents/items.py b/src/agents/items.py index 65a911798..64797ad22 100644 --- a/src/agents/items.py +++ b/src/agents/items.py @@ -18,12 +18,22 @@ ResponseOutputText, ResponseStreamEvent, ) +from openai.types.responses.response_code_interpreter_tool_call import ( + ResponseCodeInterpreterToolCall, +) from openai.types.responses.response_input_item_param import ( ComputerCallOutput, FunctionCallOutput, + LocalShellCallOutput, McpApprovalResponse, ) -from openai.types.responses.response_output_item import McpApprovalRequest, McpCall, McpListTools +from openai.types.responses.response_output_item import ( + ImageGenerationCall, + LocalShellCall, + McpApprovalRequest, + McpCall, + McpListTools, +) from openai.types.responses.response_reasoning_item import ResponseReasoningItem from pydantic import BaseModel from typing_extensions import TypeAlias @@ -114,6 +124,9 @@ class HandoffOutputItem(RunItemBase[TResponseInputItem]): ResponseFileSearchToolCall, ResponseFunctionWebSearch, McpCall, + ResponseCodeInterpreterToolCall, + ImageGenerationCall, + LocalShellCall, ] """A type that represents a tool call item.""" @@ -129,10 +142,12 @@ class ToolCallItem(RunItemBase[ToolCallItemTypes]): @dataclass -class ToolCallOutputItem(RunItemBase[Union[FunctionCallOutput, ComputerCallOutput]]): +class ToolCallOutputItem( + RunItemBase[Union[FunctionCallOutput, ComputerCallOutput, LocalShellCallOutput]] +): """Represents the output of a tool call.""" - raw_item: FunctionCallOutput | ComputerCallOutput + raw_item: FunctionCallOutput | ComputerCallOutput | LocalShellCallOutput """The raw item from the model.""" output: Any diff --git a/src/agents/models/openai_responses.py b/src/agents/models/openai_responses.py index 65a4f5caf..86c8e69cb 100644 --- a/src/agents/models/openai_responses.py +++ b/src/agents/models/openai_responses.py @@ -24,7 +24,17 @@ from ..handoffs import Handoff from ..items import ItemHelpers, ModelResponse, TResponseInputItem from ..logger import logger -from ..tool import ComputerTool, FileSearchTool, FunctionTool, HostedMCPTool, Tool, WebSearchTool +from ..tool import ( + CodeInterpreterTool, + ComputerTool, + FileSearchTool, + FunctionTool, + HostedMCPTool, + ImageGenerationTool, + LocalShellTool, + Tool, + WebSearchTool, +) from ..tracing import SpanError, response_span from ..usage import Usage from ..version import __version__ @@ -295,6 +305,18 @@ def convert_tool_choice( return { "type": "computer_use_preview", } + elif tool_choice == "image_generation": + return { + "type": "image_generation", + } + elif tool_choice == "code_interpreter": + return { + "type": "code_interpreter", + } + elif tool_choice == "mcp": + return { + "type": "mcp", + } else: return { "type": "function", @@ -386,6 +408,17 @@ def _convert_tool(cls, tool: Tool) -> tuple[ToolParam, ResponseIncludable | None elif isinstance(tool, HostedMCPTool): converted_tool = tool.tool_config includes = None + elif isinstance(tool, ImageGenerationTool): + converted_tool = tool.tool_config + includes = None + elif isinstance(tool, CodeInterpreterTool): + converted_tool = tool.tool_config + includes = None + elif isinstance(tool, LocalShellTool): + converted_tool = { + "type": "local_shell", + } + includes = None else: raise UserError(f"Unknown tool type: {type(tool)}, tool") diff --git a/src/agents/tool.py b/src/agents/tool.py index 3bcd57c2e..fd5a21c89 100644 --- a/src/agents/tool.py +++ b/src/agents/tool.py @@ -7,8 +7,8 @@ from typing import Any, Callable, Literal, Union, overload from openai.types.responses.file_search_tool_param import Filters, RankingOptions -from openai.types.responses.response_output_item import McpApprovalRequest -from openai.types.responses.tool_param import Mcp +from openai.types.responses.response_output_item import LocalShellCall, McpApprovalRequest +from openai.types.responses.tool_param import CodeInterpreter, ImageGeneration, Mcp from openai.types.responses.web_search_tool_param import UserLocation from pydantic import ValidationError from typing_extensions import Concatenate, NotRequired, ParamSpec, TypedDict @@ -180,7 +180,67 @@ def name(self): return "hosted_mcp" -Tool = Union[FunctionTool, FileSearchTool, WebSearchTool, ComputerTool, HostedMCPTool] +@dataclass +class CodeInterpreterTool: + """A tool that allows the LLM to execute code in a sandboxed environment.""" + + tool_config: CodeInterpreter + """The tool config, which includes the container and other settings.""" + + @property + def name(self): + return "code_interpreter" + + +@dataclass +class ImageGenerationTool: + """A tool that allows the LLM to generate images.""" + + tool_config: ImageGeneration + """The tool config, which image generation settings.""" + + @property + def name(self): + return "image_generation" + + +@dataclass +class LocalShellCommandRequest: + """A request to execute a command on a shell.""" + + ctx_wrapper: RunContextWrapper[Any] + """The run context.""" + + data: LocalShellCall + """The data from the local shell tool call.""" + + +LocalShellExecutor = Callable[[LocalShellCommandRequest], MaybeAwaitable[str]] +"""A function that executes a command on a shell.""" + + +@dataclass +class LocalShellTool: + """A tool that allows the LLM to execute commands on a shell.""" + + executor: LocalShellExecutor + """A function that executes a command on a shell.""" + + @property + def name(self): + return "local_shell" + + +Tool = Union[ + FunctionTool, + FileSearchTool, + WebSearchTool, + ComputerTool, + HostedMCPTool, + LocalShellTool, + ImageGenerationTool, + CodeInterpreterTool, +] """A tool that can be used in an agent.""" @@ -358,13 +418,3 @@ def decorator(real_func: ToolFunction[...]) -> FunctionTool: return _create_function_tool(real_func) return decorator - return decorator - return decorator - return decorator - return decorator - return decorator - return decorator - return decorator - return decorator - return decorator - return decorator From 1992be3e8d1746164f0f47f3e2001de4ab4059b9 Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Wed, 21 May 2025 16:06:13 -0400 Subject: [PATCH 34/35] v0.0.16 (#733) --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 200ac2485..38a2f2b64 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "openai-agents" -version = "0.0.15" +version = "0.0.16" description = "OpenAI Agents SDK" readme = "README.md" requires-python = ">=3.9" diff --git a/uv.lock b/uv.lock index 7a0cb1e6b..6f2f3f843 100644 --- a/uv.lock +++ b/uv.lock @@ -1480,7 +1480,7 @@ wheels = [ [[package]] name = "openai-agents" -version = "0.0.15" +version = "0.0.16" source = { editable = "." } dependencies = [ { name = "griffe" }, From 1364f4408e8e0d8bec1d6ac8337f68b3b888a4a7 Mon Sep 17 00:00:00 2001 From: Andrew Han Date: Wed, 21 May 2025 14:59:47 -0700 Subject: [PATCH 35/35] fix Gemini token validation issue with LiteLLM (#735) Fix for #734 --- src/agents/extensions/models/litellm_model.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/agents/extensions/models/litellm_model.py b/src/agents/extensions/models/litellm_model.py index ffb2c3c1c..49e2d42d7 100644 --- a/src/agents/extensions/models/litellm_model.py +++ b/src/agents/extensions/models/litellm_model.py @@ -111,12 +111,12 @@ async def get_response( input_tokens_details=InputTokensDetails( cached_tokens=getattr( response_usage.prompt_tokens_details, "cached_tokens", 0 - ) + ) or 0 ), output_tokens_details=OutputTokensDetails( reasoning_tokens=getattr( response_usage.completion_tokens_details, "reasoning_tokens", 0 - ) + ) or 0 ), ) if response.usage