Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions src/agents/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -991,11 +991,13 @@ async def get_system_prompt(self, run_context: RunContextWrapper[TContext]) -> s
f"but got {len(params)}: {[p.name for p in params]}"
)

# Call the instructions function properly
if inspect.iscoroutinefunction(self.instructions):
return await cast(Awaitable[str], self.instructions(run_context, self))
else:
return cast(str, self.instructions(run_context, self))
# Call once, then await if needed. Callable instances with async
# ``__call__`` are not coroutine functions, so checking
# ``iscoroutinefunction(self.instructions)`` would skip the await.
result = self.instructions(run_context, self)
if inspect.isawaitable(result):
return await result
return result

elif self.instructions is not None:
logger.error(
Expand Down
22 changes: 22 additions & 0 deletions src/agents/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
logger = logging.getLogger("openai.agents")

_DiagnosticExtra = Callable[[], Mapping[str, object]]
_DiagnosticArgs = Callable[[], tuple[object, ...]]
_DIAGNOSTIC_CONTEXT_FIELD = "openai_agents_diagnostic_context"


Expand Down Expand Up @@ -242,3 +243,24 @@ def log_model_and_tool_action_warning(
stacklevel=stacklevel,
diagnostic_extra=diagnostic_extra,
)


def log_model_and_tool_data_warning(
target_logger: logging.Logger,
redacted_message: str,
*,
diagnostic_message: str,
diagnostic_args: _DiagnosticArgs | None = None,
stacklevel: int = 2,
) -> None:
"""Log mixed model/tool data only when both data policies allow it."""
if _debug.DONT_LOG_MODEL_DATA or _debug.DONT_LOG_TOOL_DATA:
target_logger.warning(redacted_message, stacklevel=stacklevel)
return

try:
args = diagnostic_args() if diagnostic_args is not None else ()
except Exception:
target_logger.warning(redacted_message, stacklevel=stacklevel)
return
target_logger.warning(diagnostic_message, *args, stacklevel=stacklevel)
15 changes: 9 additions & 6 deletions src/agents/realtime/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

import dataclasses
import inspect
from collections.abc import Awaitable, Callable
from collections.abc import Callable
from dataclasses import dataclass, field
from typing import Any, Generic, cast
from typing import Any, Generic

from agents.prompts import Prompt

Expand Down Expand Up @@ -121,10 +121,13 @@ async def get_system_prompt(self, run_context: RunContextWrapper[TContext]) -> s
if isinstance(self.instructions, str):
return self.instructions
elif callable(self.instructions):
if inspect.iscoroutinefunction(self.instructions):
return await cast(Awaitable[str], self.instructions(run_context, self))
else:
return cast(str, self.instructions(run_context, self))
# Call once, then await if needed. Callable instances with async
# ``__call__`` are not coroutine functions, so checking
# ``iscoroutinefunction(self.instructions)`` would skip the await.
result = self.instructions(run_context, self)
if inspect.isawaitable(result):
return await result
return result
elif self.instructions is not None:
if _debug.DONT_LOG_MODEL_DATA:
logger.error("Instructions must be a string or a function")
Expand Down
14 changes: 6 additions & 8 deletions src/agents/realtime/handoffs.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,16 +166,14 @@ async def _invoke_handoff(
strict=True,
)
input_func = cast(OnHandoffWithInput[THandoffInput], on_handoff)
if inspect.iscoroutinefunction(input_func):
await input_func(ctx, validated_input)
else:
input_func(ctx, validated_input)
result = input_func(ctx, validated_input)
if inspect.isawaitable(result):
await result
elif on_handoff is not None:
no_input_func = cast(OnHandoffWithoutInput, on_handoff)
if inspect.iscoroutinefunction(no_input_func):
await no_input_func(ctx)
else:
no_input_func(ctx)
result = no_input_func(ctx)
if inspect.isawaitable(result):
await result

return agent

Expand Down
35 changes: 9 additions & 26 deletions src/agents/realtime/openai_realtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,37 +195,19 @@ async def get_api_key(key: str | Callable[[], MaybeAwaitable[str]] | None) -> st
ServerEventTypeAdapter: TypeAdapter[AllRealtimeServerEvents] | None = None


def _server_event_validation_summary(error: BaseException) -> str:
if isinstance(error, pydantic.ValidationError):
return f"{error.error_count()} validation error(s)"

if not _debug.DONT_LOG_MODEL_DATA:
return type(error).__name__
return "validation failed"


def _server_event_identity(event: Any) -> tuple[Any, Any]:
def _server_event_type(event: Any) -> Any:
if not isinstance(event, dict):
return "unknown", None
return "unknown"

return event.get("type", "unknown"), event.get("event_id")
return event.get("type", "unknown")


def _log_server_event_validation_failure(event: Any, error: BaseException) -> str:
event_type, event_id = _server_event_identity(event)

def _log_server_event_validation_failure(event: Any) -> None:
if _debug.DONT_LOG_MODEL_DATA:
logger.error(
"Failed to validate server event type=%s event_id=%s: %s",
event_type,
event_id,
_server_event_validation_summary(error),
)
logger.error("Failed to validate server event")
else:
logger.error("Failed to validate server event: %s", event, exc_info=True)

return str(event_type)


@dataclass(frozen=True)
class _PendingResponseCreate:
Expand Down Expand Up @@ -723,7 +705,7 @@ async def send_event(self, event: RealtimeModelSendEvent) -> None:
else:
await self._send_raw_message(converted)
elif _debug.DONT_LOG_MODEL_DATA:
logger.error("Failed to convert raw message type=%s", event.message.get("type"))
logger.error("Failed to convert raw message")
else:
logger.error("Failed to convert raw message: %s", event)
elif isinstance(event, RealtimeModelSendUserInput):
Expand Down Expand Up @@ -1140,11 +1122,12 @@ async def _handle_ws_event(self, event: dict[str, Any]):
validation_event
)
except pydantic.ValidationError as e:
_log_server_event_validation_failure(event, e)
_log_server_event_validation_failure(event)
await self._emit_event(RealtimeModelErrorEvent(error=e))
return
except Exception as e:
event_type = _log_server_event_validation_failure(event, e)
_log_server_event_validation_failure(event)
event_type = str(_server_event_type(event))
exception_event = RealtimeModelExceptionEvent(
exception=e,
context=f"Failed to validate server event: {event_type}",
Expand Down
12 changes: 4 additions & 8 deletions src/agents/run_internal/turn_resolution.py
Original file line number Diff line number Diff line change
Expand Up @@ -688,14 +688,10 @@ async def check_for_final_output_from_tools(
)
return ToolsToFinalOutputResult(is_final_output=False, final_output=None)
elif callable(agent.tool_use_behavior):
if inspect.iscoroutinefunction(agent.tool_use_behavior):
return await cast(
Awaitable[ToolsToFinalOutputResult],
agent.tool_use_behavior(context_wrapper, tool_results),
)
return cast(
ToolsToFinalOutputResult, agent.tool_use_behavior(context_wrapper, tool_results)
)
result = agent.tool_use_behavior(context_wrapper, tool_results)
if inspect.isawaitable(result):
return await result
return result

logger.error("Invalid tool_use_behavior: %s", agent.tool_use_behavior)
raise UserError(f"Invalid tool_use_behavior: {agent.tool_use_behavior}")
Expand Down
50 changes: 41 additions & 9 deletions src/agents/run_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,11 @@
coerce_tool_search_call_raw_item,
coerce_tool_search_output_raw_item,
)
from .logger import log_model_and_tool_action_warning, logger
from .logger import (
log_model_and_tool_action_warning,
log_model_and_tool_data_warning,
logger,
)
from .run_context import RunContextWrapper
from .run_internal.items import (
NestedHistoryOwnedItemRef,
Expand Down Expand Up @@ -3535,6 +3539,18 @@ def _deserialize_items(

result: list[RunItem] = []

def _capture_diagnostic_args(*values: object) -> Callable[[], tuple[object, ...]]:
def diagnostic_args() -> tuple[object, ...]:
return values

return diagnostic_args

def _capture_diagnostic_extra(**values: object) -> Callable[[], Mapping[str, object]]:
def diagnostic_extra() -> Mapping[str, object]:
return values

return diagnostic_extra

def _resolve_agent_info(
item_data: Mapping[str, Any], item_type: str
) -> tuple[Agent[Any] | None, str | None]:
Expand Down Expand Up @@ -3570,9 +3586,19 @@ def _resolve_agent_info(
agent, agent_name = _resolve_agent_info(item_data, item_type)
if not agent:
if agent_name:
logger.warning("Agent %s not found, skipping item", agent_name)
log_model_and_tool_data_warning(
logger,
"Agent not found, skipping item",
diagnostic_message="Agent %s not found, skipping item",
diagnostic_args=_capture_diagnostic_args(agent_name),
)
else:
logger.warning("Item missing agent field, skipping: %s", item_type)
log_model_and_tool_data_warning(
logger,
"Item missing agent field, skipping",
diagnostic_message="Item missing agent field, skipping: %s",
diagnostic_args=_capture_diagnostic_args(item_type),
)
continue

raw_item_data = item_data["raw_item"]
Expand Down Expand Up @@ -3661,11 +3687,14 @@ def _resolve_agent_info(
if not source_agent or not target_agent:
source_name = item_data.get("source_agent")
target_name = item_data.get("target_agent")
logger.warning(
"Skipping handoff_output_item: could not resolve agents "
"(source=%s, target=%s).",
source_name,
target_name,
log_model_and_tool_data_warning(
logger,
"Skipping handoff output item: could not resolve agents",
diagnostic_message=(
"Skipping handoff_output_item: could not resolve agents "
"(source=%s, target=%s)."
),
diagnostic_args=_capture_diagnostic_args(source_name, target_name),
)
continue

Expand Down Expand Up @@ -3732,7 +3761,10 @@ def _resolve_agent_info(
raise
except Exception as e:
log_model_and_tool_action_warning(
logger, f"Failed to deserialize item of type {item_type}", e
logger,
"Failed to deserialize item",
e,
diagnostic_extra=_capture_diagnostic_extra(item_type=item_type),
)
continue

Expand Down
25 changes: 20 additions & 5 deletions src/agents/sandbox/util/parse_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,26 @@ def parse_ls_la(output: str, *, base: str) -> list[FileEntry]:
permissions_str = parts[0]
owner = parts[2]
group = parts[3]
try:
size = int(parts[4])
except ValueError:
continue
# Character and block devices report a device identifier in place of the
# size column, in one of two formats. `stat` reports size 0 for them.
if permissions_str[:1] in {"c", "b"}:
size = 0
if parts[4].endswith(","):
# GNU coreutils prints "major, minor", which occupies two
# fields and shifts every following field by one.
parts = line.split(maxsplit=9)
if len(parts) < 10:
continue
name = parts[9]
else:
# BSD ls prints a single hexadecimal identifier, e.g. 0x3000002.
name = parts[8]
else:
try:
size = int(parts[4])
except ValueError:
continue
name = parts[8]

kind_map: dict[str, EntryKind] = {
"d": EntryKind.DIRECTORY,
Expand All @@ -37,7 +53,6 @@ def parse_ls_la(output: str, *, base: str) -> list[FileEntry]:
if permissions_str[:1] not in {"d", "-"} and len(permissions_str) >= 2:
permissions_str = "-" + permissions_str[1:]

name = parts[8]
if kind == EntryKind.SYMLINK and " -> " in name:
name = name.split(" -> ", 1)[0]

Expand Down
4 changes: 3 additions & 1 deletion src/agents/voice/models/openai_stt.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from openai import AsyncOpenAI

from ... import _debug
from ...exceptions import AgentsException
from ...exceptions import AgentsException, UserError
from ...logger import logger
from ...tracing import Span, SpanError, TranscriptionSpanData, transcription_span
from ...util._error_tracing import get_trace_error
Expand Down Expand Up @@ -49,6 +49,8 @@ def _audio_buffer_to_base64(buffer: npt.NDArray[np.int16 | np.float32]) -> str:
# Convert to int16.
buffer = np.clip(buffer, -1.0, 1.0)
buffer = (buffer * 32767).astype(np.int16)
elif buffer.dtype != np.int16:
raise UserError("Buffer must be a numpy array of int16 or float32")
return base64.b64encode(buffer.tobytes()).decode("utf-8")


Expand Down
30 changes: 28 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from __future__ import annotations

import os
import sys
from collections.abc import MutableMapping

import pytest

Expand All @@ -13,6 +15,32 @@

from .testing_processor import SPAN_PROCESSOR_TESTING

_PROXY_ENVIRONMENT_VARIABLES = (
"ALL_PROXY",
"HTTP_PROXY",
"HTTPS_PROXY",
"all_proxy",
"http_proxy",
"https_proxy",
)
_PROXY_OPT_IN_ENVIRONMENT_VARIABLE = "OPENAI_AGENTS_TEST_USE_PROXY"


def _remove_ambient_proxy_environment(environment: MutableMapping[str, str]) -> None:
"""Keep unit tests independent from host proxy configuration."""
if environment.get(_PROXY_OPT_IN_ENVIRONMENT_VARIABLE, "").lower() in {
"1",
"true",
"yes",
}:
return

for variable in _PROXY_ENVIRONMENT_VARIABLES:
environment.pop(variable, None)


_remove_ambient_proxy_environment(os.environ)

collect_ignore: list[str] = []

if sys.platform == "win32":
Expand Down Expand Up @@ -50,8 +78,6 @@ def setup_span_processor():
# monkeypatch.delenv("OPENAI_API_KEY", ...) to remove it locally.
@pytest.fixture(scope="session", autouse=True)
def ensure_openai_api_key():
import os

if not os.environ.get("OPENAI_API_KEY"):
os.environ["OPENAI_API_KEY"] = "test_key"

Expand Down
Loading
Loading