Files
openai--openai-agents-python/examples/basic/stream_ws.py
T
2026-07-13 12:39:17 +08:00

237 lines
8.0 KiB
Python

"""Responses websocket streaming example with function tools, agent-as-tool, and approval.
This example shows a user-facing websocket workflow using
`responses_websocket_session(...)`:
- Streaming output (including reasoning summary deltas when available)
- Regular function tools
- An `Agent.as_tool(...)` specialist agent
- HITL approval for a sensitive tool call
- A follow-up turn using `previous_response_id` on the same trace
Required environment variable:
- `OPENAI_API_KEY`
Optional environment variables:
- `OPENAI_MODEL` (defaults to `gpt-5.6-sol`)
- `OPENAI_BASE_URL`
- `OPENAI_WEBSOCKET_BASE_URL`
- `EXAMPLES_INTERACTIVE_MODE=auto` (auto-approve HITL prompts for scripted runs)
"""
import asyncio
import os
from typing import Any
from openai.types.shared import Reasoning
from agents import (
Agent,
ModelSettings,
ResponsesWebSocketSession,
function_tool,
responses_websocket_session,
trace,
)
from examples.auto_mode import confirm_with_fallback
@function_tool
def lookup_order(order_id: str) -> dict[str, Any]:
"""Return deterministic order data for the demo."""
orders = {
"ORD-1001": {
"order_id": "ORD-1001",
"status": "delivered",
"delivered_days_ago": 3,
"amount": 49.99,
"currency": "USD",
"item": "Wireless Mouse",
},
"ORD-2002": {
"order_id": "ORD-2002",
"status": "delivered",
"delivered_days_ago": 12,
"amount": 129.0,
"currency": "USD",
"item": "Keyboard",
},
}
return orders.get(
order_id,
{
"order_id": order_id,
"status": "unknown",
"delivered_days_ago": 999,
"amount": 0.0,
"currency": "USD",
"item": "unknown",
},
)
@function_tool(needs_approval=True)
def submit_refund(order_id: str, amount: float, reason: str) -> dict[str, Any]:
"""Create a refund request. This tool requires approval."""
ticket = "RF-1001" if order_id == "ORD-1001" else f"RF-{order_id[-4:]}"
return {
"refund_ticket": ticket,
"order_id": order_id,
"amount": amount,
"reason": reason,
"status": "approved_pending_processing",
}
def ask_approval(question: str) -> bool:
"""Prompt for approval (or auto-approve in examples auto mode)."""
return confirm_with_fallback(f"[approval] {question} [y/N]: ", default=True)
async def run_streamed_turn(
ws: ResponsesWebSocketSession,
agent: Agent[Any],
prompt: str,
*,
previous_response_id: str | None = None,
) -> tuple[str, str]:
"""Run one streamed turn and handle HITL approvals if needed."""
print(f"\nUser: {prompt}\n")
result = ws.run_streamed(
agent,
prompt,
previous_response_id=previous_response_id,
)
printed_reasoning = False
printed_output = False
while True:
async for event in result.stream_events():
if event.type == "raw_response_event":
raw = event.data
if raw.type == "response.reasoning_summary_text.delta":
if not printed_reasoning:
print("Reasoning:")
printed_reasoning = True
print(raw.delta, end="", flush=True)
elif raw.type == "response.output_text.delta":
if printed_reasoning and not printed_output:
print("\n")
if not printed_output:
print("Assistant:")
printed_output = True
print(raw.delta, end="", flush=True)
continue
if event.type != "run_item_stream_event":
continue
item = event.item
if item.type == "tool_call_item":
tool_name = getattr(item.raw_item, "name", "unknown")
tool_args = getattr(item.raw_item, "arguments", "")
print(f"\n[tool call] {tool_name}({tool_args})")
elif item.type == "tool_call_output_item":
print(f"[tool result] {item.output}")
if printed_reasoning or printed_output:
print("\n")
if not result.interruptions:
break
state = result.to_state()
for interruption in result.interruptions:
question = f"Approve {interruption.name} with args {interruption.arguments}?"
if ask_approval(question):
state.approve(interruption)
else:
state.reject(interruption)
result = ws.run_streamed(agent, state)
if result.last_response_id is None:
raise RuntimeError("The streamed run completed without a response_id.")
final_output = str(result.final_output)
print(f"response_id: {result.last_response_id}")
print(f"final_output: {final_output}\n")
return result.last_response_id, final_output
async def main() -> None:
model_name = os.getenv("OPENAI_MODEL", "gpt-5.6-sol")
policy_agent = Agent(
name="RefundPolicySpecialist",
instructions=(
"You are a refund policy specialist. The policy is simple: orders delivered "
"within 7 days are eligible for a full refund, and older delivered orders "
"are not. Return a short answer with eligibility and a one-line reason."
),
model=model_name,
model_settings=ModelSettings(max_tokens=120),
)
support_agent = Agent(
name="SupportAgent",
instructions=(
"You are a support agent. For refund requests, do this in order: "
"1) call lookup_order, 2) call refund_policy_specialist, 3) if the user "
"asked to proceed and the order is eligible, call submit_refund. "
"When asked for only the refund ticket, return only the ticket token "
"(for example RF-1001)."
),
tools=[
lookup_order,
policy_agent.as_tool(
tool_name="refund_policy_specialist",
tool_description="Check refund eligibility and explain the policy decision.",
),
submit_refund,
],
model=model_name,
model_settings=ModelSettings(
max_tokens=200,
reasoning=Reasoning(effort="medium", summary="detailed"),
),
)
try:
# You can skip this helper and call Runner.run_streamed(...) directly.
# It will still work, but each run will create/connect again unless you manually
# reuse the same RunConfig/provider. This helper makes that reuse easy across turns
# (and nested agent-as-tool runs) so the websocket connection can stay warm.
async with responses_websocket_session() as ws:
with trace("Responses WS support example") as current_trace:
print(f"Using model={model_name}")
print(f"trace_id={current_trace.trace_id}")
first_response_id, _ = await run_streamed_turn(
ws,
support_agent,
(
"Customer wants a refund for order ORD-1001 because the mouse arrived "
"damaged. Please check the order, ask the refund policy specialist, and "
"if it is eligible submit the refund. Reply with only the refund ticket."
),
)
await run_streamed_turn(
ws,
support_agent,
"What refund ticket did you just create? Reply with only the ticket.",
previous_response_id=first_response_id,
)
except RuntimeError as exc:
if "closed before any response events" in str(exc):
print(
"\nWebsocket mode closed before sending events. This usually means the "
"feature is not enabled for this account/model yet."
)
return
raise
if __name__ == "__main__":
asyncio.run(main())