Files
2026-07-13 12:39:17 +08:00

180 lines
6.6 KiB
Python

from __future__ import annotations
import json
import os
from typing import Any
import pytest
from openai import AsyncOpenAI
from agents import Agent, ModelSettings, RunConfig, Runner, function_tool
from agents.extensions.experimental.hosted_multi_agent import (
HostedMultiAgentConfig,
OpenAIHostedMultiAgentModel,
get_hosted_agent_metadata,
)
from agents.tool_context import ToolContext
pytestmark = [
pytest.mark.allow_call_model_methods,
pytest.mark.skipif(
os.environ.get("OPENAI_RUN_LIVE_HOSTED_MULTI_AGENT_TESTS") != "1",
reason="Set OPENAI_RUN_LIVE_HOSTED_MULTI_AGENT_TESTS=1 to run live beta tests.",
),
]
_PROPOSALS = {
"alpha": {"estimated_weeks": 6, "risk": "medium"},
"beta": {"estimated_weeks": 8, "risk": "low"},
}
def _tool_output(arguments: str) -> str:
proposal = json.loads(arguments)["proposal"]
return json.dumps(_PROPOSALS[proposal], sort_keys=True)
async def _run_direct_baseline(client: AsyncOpenAI) -> tuple[str, set[str], set[str]]:
beta = getattr(client, "beta", None)
responses = getattr(beta, "responses", None)
connect = getattr(responses, "connect", None)
if not callable(connect):
pytest.fail("The installed openai package does not provide client.beta.responses.connect.")
tools = [
{
"type": "function",
"name": "get_proposal",
"description": "Return deterministic details for one proposal.",
"parameters": {
"type": "object",
"properties": {"proposal": {"type": "string", "enum": ["alpha", "beta"]}},
"required": ["proposal"],
"additionalProperties": False,
},
"strict": True,
}
]
callers: set[str] = set()
call_ids: set[str] = set()
completed_response: Any | None = None
response_id: str | None = None
pending_injections = 0
async with connect(
extra_headers={"OpenAI-Beta": "responses_multi_agent=v1"},
max_retries=0,
) as connection:
await connection.send(
{
"type": "response.create",
"model": "gpt-5.6-sol",
"input": [{"role": "user", "content": "Compare proposal alpha and proposal beta."}],
"instructions": (
"Create two subagents. Assign proposal alpha to one and proposal beta to the "
"other. Each subagent must call get_proposal, then the root must synthesize."
),
"tools": tools,
"store": True,
"multi_agent": {"enabled": True, "max_concurrent_subagents": 2},
}
)
async for event in connection:
if event.type == "response.created":
response_id = event.response.id
elif event.type == "response.output_item.done" and event.item.type == "function_call":
if response_id is None:
pytest.fail("Direct baseline received a function call before response.created.")
call_ids.add(event.item.call_id)
agent = getattr(event.item, "agent", None)
callers.add(getattr(agent, "agent_name", "/root"))
pending_injections += 1
await connection.send(
{
"type": "response.inject",
"response_id": response_id,
"input": [
{
"type": "function_call_output",
"call_id": event.item.call_id,
"output": _tool_output(event.item.arguments),
}
],
}
)
elif event.type == "response.inject.created":
pending_injections -= 1
elif event.type == "response.inject.failed":
pytest.fail(f"Direct baseline injection failed: {event.error}")
elif event.type == "response.completed":
completed_response = event.response
elif event.type in {"error", "response.failed", "response.incomplete"}:
pytest.fail(f"Direct baseline failed: {event}")
if completed_response is not None and pending_injections == 0:
break
if completed_response is None:
pytest.fail("Direct hosted multi-agent baseline did not complete.")
root_text: list[str] = []
for item in completed_response.output:
if (
item.type == "message"
and getattr(getattr(item, "agent", None), "agent_name", None) == "/root"
and getattr(item, "phase", None) == "final_answer"
):
root_text.extend(part.text for part in item.content if part.type == "output_text")
return "".join(root_text), callers, call_ids
@pytest.mark.asyncio
async def test_live_direct_and_agents_sdk_semantic_parity() -> None:
if os.environ.get("OPENAI_API_KEY") in {None, "", "test_key"}:
pytest.fail("A real OPENAI_API_KEY is required for the live beta test.")
client = AsyncOpenAI()
direct_text, direct_callers, direct_call_ids = await _run_direct_baseline(client)
sdk_callers: set[str] = set()
sdk_call_ids: set[str] = set()
@function_tool
def get_proposal(ctx: ToolContext[Any], proposal: str) -> dict[str, object]:
metadata = get_hosted_agent_metadata(ctx)
sdk_callers.add(metadata.agent_name if metadata else "/root")
sdk_call_ids.add(ctx.tool_call_id)
return _PROPOSALS[proposal]
model = OpenAIHostedMultiAgentModel(
model="gpt-5.6-sol",
openai_client=client,
config=HostedMultiAgentConfig(max_concurrent_subagents=2),
)
agent = Agent(
name="Hosted proposal coordinator",
instructions=(
"Create two subagents. Assign proposal alpha to one and proposal beta to the other. "
"Each subagent must call get_proposal, then the root must synthesize."
),
model=model,
model_settings=ModelSettings(
store=False,
response_include=["reasoning.encrypted_content"],
),
tools=[get_proposal],
)
result = await Runner.run(
agent,
"Compare proposal alpha and proposal beta.",
run_config=RunConfig(tracing_disabled=True),
max_turns=6,
)
assert direct_text
assert result.final_output
assert len(direct_call_ids) == 2
assert len(sdk_call_ids) == 2
assert all(caller != "/root" for caller in direct_callers)
assert all(caller != "/root" for caller in sdk_callers)