Skip to content
Open
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
36 changes: 36 additions & 0 deletions src/openai/types/beta/beta_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from .beta_response_output_item import BetaResponseOutputItem
from .beta_response_text_config import BetaResponseTextConfig
from .beta_tool_choice_function import BetaToolChoiceFunction
from .beta_response_output_message import BetaResponseOutputMessage
from .beta_tool_choice_apply_patch import BetaToolChoiceApplyPatch

__all__ = [
Expand Down Expand Up @@ -614,3 +615,38 @@ class BetaResponse(BaseModel):
similar requests and to help OpenAI detect and prevent abuse.
[Learn more](https://platform.openai.com/docs/guides/safety-best-practices#safety-identifiers).
"""

@property
def output_text(self) -> str:
"""Convenience property that returns the response's output text.

For multi-agent responses, this returns text from the last root
`final_answer` message. Otherwise, it aggregates all `output_text` content
blocks from the `output` list.
"""
Comment on lines +621 to +626
has_agent_metadata = False
final_root_message: Optional[BetaResponseOutputMessage] = None

for output in self.output:
if output.type != "message" or output.agent is None:
continue
Comment on lines +631 to +632

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Treat missing agent metadata as the root coordinator

When the coordinator message omits agent—the established convention in examples/responses/multi_agent_streaming.py:12-13 and multi_agent_websocket.py:12-13, where missing metadata maps to /root—this condition skips the actual root final_answer. If a child message has metadata, has_agent_metadata becomes true and output_text returns an empty string; otherwise it falls back to aggregating root commentary with the final answer. Consequently, the new property does not select the coordinator's answer for normal multi-agent output.

Useful? React with 👍 / 👎.


has_agent_metadata = True
if output.agent.agent_name == "/root" and output.phase == "final_answer":
final_root_message = output

if has_agent_metadata and final_root_message is None:
return ""

texts: List[str] = []
for output in self.output:
if output.type != "message":
continue
if has_agent_metadata and output is not final_root_message:
continue

for content in output.content:
if content.type == "output_text":
texts.append(content.text)

return "".join(texts)
63 changes: 63 additions & 0 deletions tests/lib/responses/test_beta_responses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from __future__ import annotations

from typing import Any

from openai._models import construct_type_unchecked
from openai.types.beta import BetaResponse


def _message(
*texts: str,
agent_name: str | None = None,
phase: str | None = None,
) -> dict[str, Any]:
message: dict[str, Any] = {
"id": "msg_123",
"type": "message",
"role": "assistant",
"status": "completed",
"content": [
{
"type": "output_text",
"annotations": [],
"logprobs": [],
"text": text,
}
for text in texts
],
}
if agent_name is not None:
message["agent"] = {"agent_name": agent_name}
if phase is not None:
message["phase"] = phase
return message


def _response(*output: dict[str, Any]) -> BetaResponse:
return construct_type_unchecked(type_=BetaResponse, value={"output": list(output)})


def test_output_text_uses_last_root_final_message_for_multi_agent_response() -> None:
response = _response(
_message("old answer", agent_name="/root", phase="final_answer"),
_message("child answer", agent_name="/root/reviewer", phase="final_answer"),
_message("root commentary", agent_name="/root", phase="commentary"),
_message("final ", "answer", agent_name="/root", phase="final_answer"),
)

assert response.output_text == "final answer"


def test_output_text_is_empty_without_root_final_message_for_multi_agent_response() -> None:
response = _response(
_message("child answer", agent_name="/root/reviewer", phase="final_answer"),
_message("root commentary", agent_name="/root", phase="commentary"),
)

assert response.output_text == ""


def test_output_text_aggregates_non_multi_agent_response() -> None:
response = _response(_message("first "), _message("second"))

assert response.output_text == "first second"
Loading