openai-agents 0.3.3__py3-none-any.whl → 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of openai-agents might be problematic. Click here for more details.

@@ -3,9 +3,9 @@ from __future__ import annotations
3
3
  import json
4
4
  import time
5
5
  from collections.abc import AsyncIterator
6
- from typing import TYPE_CHECKING, Any, Literal, overload
6
+ from typing import TYPE_CHECKING, Any, Literal, cast, overload
7
7
 
8
- from openai import NOT_GIVEN, AsyncOpenAI, AsyncStream
8
+ from openai import AsyncOpenAI, AsyncStream, Omit, omit
9
9
  from openai.types import ChatModel
10
10
  from openai.types.chat import ChatCompletion, ChatCompletionChunk, ChatCompletionMessage
11
11
  from openai.types.chat.chat_completion import Choice
@@ -44,8 +44,8 @@ class OpenAIChatCompletionsModel(Model):
44
44
  self.model = model
45
45
  self._client = openai_client
46
46
 
47
- def _non_null_or_not_given(self, value: Any) -> Any:
48
- return value if value is not None else NOT_GIVEN
47
+ def _non_null_or_omit(self, value: Any) -> Any:
48
+ return value if value is not None else omit
49
49
 
50
50
  async def get_response(
51
51
  self,
@@ -243,13 +243,12 @@ class OpenAIChatCompletionsModel(Model):
243
243
  if tracing.include_data():
244
244
  span.span_data.input = converted_messages
245
245
 
246
- parallel_tool_calls = (
247
- True
248
- if model_settings.parallel_tool_calls and tools and len(tools) > 0
249
- else False
250
- if model_settings.parallel_tool_calls is False
251
- else NOT_GIVEN
252
- )
246
+ if model_settings.parallel_tool_calls and tools:
247
+ parallel_tool_calls: bool | Omit = True
248
+ elif model_settings.parallel_tool_calls is False:
249
+ parallel_tool_calls = False
250
+ else:
251
+ parallel_tool_calls = omit
253
252
  tool_choice = Converter.convert_tool_choice(model_settings.tool_choice)
254
253
  response_format = Converter.convert_response_format(output_schema)
255
254
 
@@ -259,6 +258,7 @@ class OpenAIChatCompletionsModel(Model):
259
258
  converted_tools.append(Converter.convert_handoff_tool(handoff))
260
259
 
261
260
  converted_tools = _to_dump_compatible(converted_tools)
261
+ tools_param = converted_tools if converted_tools else omit
262
262
 
263
263
  if _debug.DONT_LOG_MODEL_DATA:
264
264
  logger.debug("Calling LLM")
@@ -288,28 +288,30 @@ class OpenAIChatCompletionsModel(Model):
288
288
  self._get_client(), model_settings, stream=stream
289
289
  )
290
290
 
291
+ stream_param: Literal[True] | Omit = True if stream else omit
292
+
291
293
  ret = await self._get_client().chat.completions.create(
292
294
  model=self.model,
293
295
  messages=converted_messages,
294
- tools=converted_tools or NOT_GIVEN,
295
- temperature=self._non_null_or_not_given(model_settings.temperature),
296
- top_p=self._non_null_or_not_given(model_settings.top_p),
297
- frequency_penalty=self._non_null_or_not_given(model_settings.frequency_penalty),
298
- presence_penalty=self._non_null_or_not_given(model_settings.presence_penalty),
299
- max_tokens=self._non_null_or_not_given(model_settings.max_tokens),
296
+ tools=tools_param,
297
+ temperature=self._non_null_or_omit(model_settings.temperature),
298
+ top_p=self._non_null_or_omit(model_settings.top_p),
299
+ frequency_penalty=self._non_null_or_omit(model_settings.frequency_penalty),
300
+ presence_penalty=self._non_null_or_omit(model_settings.presence_penalty),
301
+ max_tokens=self._non_null_or_omit(model_settings.max_tokens),
300
302
  tool_choice=tool_choice,
301
303
  response_format=response_format,
302
304
  parallel_tool_calls=parallel_tool_calls,
303
- stream=stream,
304
- stream_options=self._non_null_or_not_given(stream_options),
305
- store=self._non_null_or_not_given(store),
306
- reasoning_effort=self._non_null_or_not_given(reasoning_effort),
307
- verbosity=self._non_null_or_not_given(model_settings.verbosity),
308
- top_logprobs=self._non_null_or_not_given(model_settings.top_logprobs),
305
+ stream=cast(Any, stream_param),
306
+ stream_options=self._non_null_or_omit(stream_options),
307
+ store=self._non_null_or_omit(store),
308
+ reasoning_effort=self._non_null_or_omit(reasoning_effort),
309
+ verbosity=self._non_null_or_omit(model_settings.verbosity),
310
+ top_logprobs=self._non_null_or_omit(model_settings.top_logprobs),
309
311
  extra_headers=self._merge_headers(model_settings),
310
312
  extra_query=model_settings.extra_query,
311
313
  extra_body=model_settings.extra_body,
312
- metadata=self._non_null_or_not_given(model_settings.metadata),
314
+ metadata=self._non_null_or_omit(model_settings.metadata),
313
315
  **(model_settings.extra_args or {}),
314
316
  )
315
317
 
@@ -319,14 +321,13 @@ class OpenAIChatCompletionsModel(Model):
319
321
  responses_tool_choice = OpenAIResponsesConverter.convert_tool_choice(
320
322
  model_settings.tool_choice
321
323
  )
322
- if responses_tool_choice is None or responses_tool_choice == NOT_GIVEN:
324
+ if responses_tool_choice is None or responses_tool_choice is omit:
323
325
  # For Responses API data compatibility with Chat Completions patterns,
324
326
  # we need to set "none" if tool_choice is absent.
325
327
  # Without this fix, you'll get the following error:
326
328
  # pydantic_core._pydantic_core.ValidationError: 4 validation errors for Response
327
329
  # tool_choice.literal['none','auto','required']
328
330
  # Input should be 'none', 'auto' or 'required'
329
- # [type=literal_error, input_value=NOT_GIVEN, input_type=NotGiven]
330
331
  # see also: https://github.com/openai/openai-agents-python/issues/980
331
332
  responses_tool_choice = "auto"
332
333
 
@@ -4,9 +4,9 @@ import json
4
4
  from collections.abc import AsyncIterator
5
5
  from contextvars import ContextVar
6
6
  from dataclasses import dataclass
7
- from typing import TYPE_CHECKING, Any, Literal, cast, overload
7
+ from typing import TYPE_CHECKING, Any, Literal, Union, cast, overload
8
8
 
9
- from openai import NOT_GIVEN, APIStatusError, AsyncOpenAI, AsyncStream, NotGiven
9
+ from openai import APIStatusError, AsyncOpenAI, AsyncStream, Omit, omit
10
10
  from openai.types import ChatModel
11
11
  from openai.types.responses import (
12
12
  Response,
@@ -69,8 +69,8 @@ class OpenAIResponsesModel(Model):
69
69
  self.model = model
70
70
  self._client = openai_client
71
71
 
72
- def _non_null_or_not_given(self, value: Any) -> Any:
73
- return value if value is not None else NOT_GIVEN
72
+ def _non_null_or_omit(self, value: Any) -> Any:
73
+ return value if value is not None else omit
74
74
 
75
75
  async def get_response(
76
76
  self,
@@ -249,13 +249,12 @@ class OpenAIResponsesModel(Model):
249
249
  list_input = ItemHelpers.input_to_new_input_list(input)
250
250
  list_input = _to_dump_compatible(list_input)
251
251
 
252
- parallel_tool_calls = (
253
- True
254
- if model_settings.parallel_tool_calls and tools and len(tools) > 0
255
- else False
256
- if model_settings.parallel_tool_calls is False
257
- else NOT_GIVEN
258
- )
252
+ if model_settings.parallel_tool_calls and tools:
253
+ parallel_tool_calls: bool | Omit = True
254
+ elif model_settings.parallel_tool_calls is False:
255
+ parallel_tool_calls = False
256
+ else:
257
+ parallel_tool_calls = omit
259
258
 
260
259
  tool_choice = Converter.convert_tool_choice(model_settings.tool_choice)
261
260
  converted_tools = Converter.convert_tools(tools, handoffs)
@@ -297,36 +296,39 @@ class OpenAIResponsesModel(Model):
297
296
  if model_settings.top_logprobs is not None:
298
297
  extra_args["top_logprobs"] = model_settings.top_logprobs
299
298
  if model_settings.verbosity is not None:
300
- if response_format != NOT_GIVEN:
299
+ if response_format is not omit:
301
300
  response_format["verbosity"] = model_settings.verbosity # type: ignore [index]
302
301
  else:
303
302
  response_format = {"verbosity": model_settings.verbosity}
304
303
 
305
- return await self._client.responses.create(
306
- previous_response_id=self._non_null_or_not_given(previous_response_id),
307
- conversation=self._non_null_or_not_given(conversation_id),
308
- instructions=self._non_null_or_not_given(system_instructions),
304
+ stream_param: Literal[True] | Omit = True if stream else omit
305
+
306
+ response = await self._client.responses.create(
307
+ previous_response_id=self._non_null_or_omit(previous_response_id),
308
+ conversation=self._non_null_or_omit(conversation_id),
309
+ instructions=self._non_null_or_omit(system_instructions),
309
310
  model=self.model,
310
311
  input=list_input,
311
312
  include=include,
312
313
  tools=converted_tools_payload,
313
- prompt=self._non_null_or_not_given(prompt),
314
- temperature=self._non_null_or_not_given(model_settings.temperature),
315
- top_p=self._non_null_or_not_given(model_settings.top_p),
316
- truncation=self._non_null_or_not_given(model_settings.truncation),
317
- max_output_tokens=self._non_null_or_not_given(model_settings.max_tokens),
314
+ prompt=self._non_null_or_omit(prompt),
315
+ temperature=self._non_null_or_omit(model_settings.temperature),
316
+ top_p=self._non_null_or_omit(model_settings.top_p),
317
+ truncation=self._non_null_or_omit(model_settings.truncation),
318
+ max_output_tokens=self._non_null_or_omit(model_settings.max_tokens),
318
319
  tool_choice=tool_choice,
319
320
  parallel_tool_calls=parallel_tool_calls,
320
- stream=stream,
321
+ stream=cast(Any, stream_param),
321
322
  extra_headers=self._merge_headers(model_settings),
322
323
  extra_query=model_settings.extra_query,
323
324
  extra_body=model_settings.extra_body,
324
325
  text=response_format,
325
- store=self._non_null_or_not_given(model_settings.store),
326
- reasoning=self._non_null_or_not_given(model_settings.reasoning),
327
- metadata=self._non_null_or_not_given(model_settings.metadata),
326
+ store=self._non_null_or_omit(model_settings.store),
327
+ reasoning=self._non_null_or_omit(model_settings.reasoning),
328
+ metadata=self._non_null_or_omit(model_settings.metadata),
328
329
  **extra_args,
329
330
  )
331
+ return cast(Union[Response, AsyncStream[ResponseStreamEvent]], response)
330
332
 
331
333
  def _get_client(self) -> AsyncOpenAI:
332
334
  if self._client is None:
@@ -351,9 +353,9 @@ class Converter:
351
353
  @classmethod
352
354
  def convert_tool_choice(
353
355
  cls, tool_choice: Literal["auto", "required", "none"] | str | MCPToolChoice | None
354
- ) -> response_create_params.ToolChoice | NotGiven:
356
+ ) -> response_create_params.ToolChoice | Omit:
355
357
  if tool_choice is None:
356
- return NOT_GIVEN
358
+ return omit
357
359
  elif isinstance(tool_choice, MCPToolChoice):
358
360
  return {
359
361
  "server_label": tool_choice.server_label,
@@ -404,9 +406,9 @@ class Converter:
404
406
  @classmethod
405
407
  def get_response_format(
406
408
  cls, output_schema: AgentOutputSchemaBase | None
407
- ) -> ResponseTextConfigParam | NotGiven:
409
+ ) -> ResponseTextConfigParam | Omit:
408
410
  if output_schema is None or output_schema.is_plain_text():
409
- return NOT_GIVEN
411
+ return omit
410
412
  else:
411
413
  return {
412
414
  "format": {
@@ -13,10 +13,10 @@ from ..strict_schema import ensure_strict_json_schema
13
13
  from ..tracing.spans import SpanError
14
14
  from ..util import _error_tracing, _json
15
15
  from ..util._types import MaybeAwaitable
16
+ from . import RealtimeAgent
16
17
 
17
18
  if TYPE_CHECKING:
18
19
  from ..agent import AgentBase
19
- from . import RealtimeAgent
20
20
 
21
21
 
22
22
  # The handoff input type is the type of data passed when the agent is called via a handoff.
agents/result.py CHANGED
@@ -4,7 +4,7 @@ import abc
4
4
  import asyncio
5
5
  from collections.abc import AsyncIterator
6
6
  from dataclasses import dataclass, field
7
- from typing import TYPE_CHECKING, Any, cast
7
+ from typing import TYPE_CHECKING, Any, Literal, cast
8
8
 
9
9
  from typing_extensions import TypeVar
10
10
 
@@ -164,6 +164,9 @@ class RunResultStreaming(RunResultBase):
164
164
  _output_guardrails_task: asyncio.Task[Any] | None = field(default=None, repr=False)
165
165
  _stored_exception: Exception | None = field(default=None, repr=False)
166
166
 
167
+ # Soft cancel state
168
+ _cancel_mode: Literal["none", "immediate", "after_turn"] = field(default="none", repr=False)
169
+
167
170
  @property
168
171
  def last_agent(self) -> Agent[Any]:
169
172
  """The last agent that was run. Updates as the agent run progresses, so the true last agent
@@ -171,17 +174,51 @@ class RunResultStreaming(RunResultBase):
171
174
  """
172
175
  return self.current_agent
173
176
 
174
- def cancel(self) -> None:
175
- """Cancels the streaming run, stopping all background tasks and marking the run as
176
- complete."""
177
- self._cleanup_tasks() # Cancel all running tasks
178
- self.is_complete = True # Mark the run as complete to stop event streaming
177
+ def cancel(self, mode: Literal["immediate", "after_turn"] = "immediate") -> None:
178
+ """Cancel the streaming run.
179
179
 
180
- # Optionally, clear the event queue to prevent processing stale events
181
- while not self._event_queue.empty():
182
- self._event_queue.get_nowait()
183
- while not self._input_guardrail_queue.empty():
184
- self._input_guardrail_queue.get_nowait()
180
+ Args:
181
+ mode: Cancellation strategy:
182
+ - "immediate": Stop immediately, cancel all tasks, clear queues (default)
183
+ - "after_turn": Complete current turn gracefully before stopping
184
+ * Allows LLM response to finish
185
+ * Executes pending tool calls
186
+ * Saves session state properly
187
+ * Tracks usage accurately
188
+ * Stops before next turn begins
189
+
190
+ Example:
191
+ ```python
192
+ result = Runner.run_streamed(agent, "Task", session=session)
193
+
194
+ async for event in result.stream_events():
195
+ if user_interrupted():
196
+ result.cancel(mode="after_turn") # Graceful
197
+ # result.cancel() # Immediate (default)
198
+ ```
199
+
200
+ Note: After calling cancel(), you should continue consuming stream_events()
201
+ to allow the cancellation to complete properly.
202
+ """
203
+ # Store the cancel mode for the background task to check
204
+ self._cancel_mode = mode
205
+
206
+ if mode == "immediate":
207
+ # Existing behavior - immediate shutdown
208
+ self._cleanup_tasks() # Cancel all running tasks
209
+ self.is_complete = True # Mark the run as complete to stop event streaming
210
+
211
+ # Optionally, clear the event queue to prevent processing stale events
212
+ while not self._event_queue.empty():
213
+ self._event_queue.get_nowait()
214
+ while not self._input_guardrail_queue.empty():
215
+ self._input_guardrail_queue.get_nowait()
216
+
217
+ elif mode == "after_turn":
218
+ # Soft cancel - just set the flag
219
+ # The streaming loop will check this and stop gracefully
220
+ # Don't call _cleanup_tasks() or clear queues yet
221
+ pass
185
222
 
186
223
  async def stream_events(self) -> AsyncIterator[StreamEvent]:
187
224
  """Stream deltas for new items as they are generated. We're using the types from the