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

207 lines
9.0 KiB
Python

"""
Show how sandbox agents can be exposed as tools to a normal orchestrator.
Each sandbox reviewer gets its own isolated workspace. The outer orchestrator
does not inspect files directly. It calls the reviewers as tools and combines
their outputs with a normal Python function tool.
"""
import argparse
import asyncio
import json
import sys
from pathlib import Path
from typing import Literal
from openai.types.shared import Reasoning
from pydantic import BaseModel, Field
from agents import Agent, ModelSettings, Runner, function_tool
from agents.run import RunConfig
from agents.sandbox import SandboxAgent, SandboxRunConfig
from agents.sandbox.sandboxes.unix_local import UnixLocalSandboxClient
if __package__ is None or __package__ == "":
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from examples.sandbox.misc.example_support import text_manifest, tool_call_name
from examples.sandbox.misc.workspace_shell import WorkspaceShellCapability
DEFAULT_QUESTION = (
"Review the Acme renewal materials and give me a short recommendation for the deal desk. "
"Include pricing risk, rollout risk, and the most important next step."
)
class PricingPacketReview(BaseModel):
requested_discount_percent: int = Field(
description="Exact requested discount percentage from pricing_summary.md."
)
requested_term_months: int = Field(
description="Exact requested renewal term in months from pricing_summary.md."
)
pricing_risk: Literal["low", "medium", "high"]
summary: str = Field(description="Short pricing risk summary grounded in the reviewed files.")
recommended_next_step: str = Field(
description="Most important commercial next step for the deal desk."
)
evidence_files: list[str] = Field(
description="File names that support the review.", min_length=1
)
class RolloutRiskReview(BaseModel):
rollout_risk: Literal["low", "medium", "high"]
summary: str = Field(description="Short rollout risk summary grounded in the reviewed files.")
blockers: list[str] = Field(description="Concrete rollout blockers from the reviewed files.")
recommended_next_step: str = Field(
description="Most important delivery next step for the deal desk."
)
evidence_files: list[str] = Field(
description="File names that support the review.", min_length=1
)
async def _structured_tool_output_extractor(result) -> str:
final_output = result.final_output
if isinstance(final_output, BaseModel):
return json.dumps(final_output.model_dump(mode="json"), sort_keys=True)
return str(final_output)
@function_tool
def get_discount_approval_rule(discount_percent: int) -> str:
"""Return the internal approver required for a proposed discount."""
if discount_percent <= 10:
return "Discounts up to 10 percent can be approved by the account executive."
if discount_percent <= 15:
return "Discounts from 11 to 15 percent require regional sales director approval."
return "Discounts above 15 percent require finance and regional sales director approval."
async def main(model: str, question: str) -> None:
# This manifest is visible only to the pricing reviewer.
pricing_manifest = text_manifest(
{
"pricing_summary.md": (
"# Pricing summary\n\n"
"- Current annual contract: $220,000.\n"
"- Requested renewal term: 24 months.\n"
"- Requested discount: 15 percent.\n"
"- Account executive target discount band: 8 to 10 percent.\n"
),
"commercial_notes.md": (
"# Commercial notes\n\n"
"- The customer expanded from 120 to 170 paid seats in the last 6 months.\n"
"- Procurement asked for one final concession to close before quarter end.\n"
),
}
)
# This separate manifest is visible only to the rollout reviewer.
rollout_manifest = text_manifest(
{
"rollout_plan.md": (
"# Rollout plan\n\n"
"- Customer wants a 30-day rollout for three new regional teams.\n"
"- Regional admins have not completed training yet.\n"
"- SSO migration is scheduled for the second week of the rollout.\n"
),
"support_history.md": (
"# Support history\n\n"
"- Two high-priority onboarding tickets were closed in the last quarter.\n"
"- No open production incidents.\n"
"- Customer success manager asked for a phased launch if the contract closes.\n"
),
}
)
pricing_agent = SandboxAgent(
name="Pricing Packet Reviewer",
model=model,
instructions=(
"You inspect renewal pricing documents and return a structured commercial review. "
"Inspect the files before answering and extract the exact requested discount percent "
"and renewal term from pricing_summary.md. "
"Use the shell tool before answering. requested_discount_percent must match the exact "
"integer in pricing_summary.md. requested_term_months must match the exact renewal "
"term from pricing_summary.md. Do not introduce any facts, incidents, or numbers that "
"are not present in pricing_summary.md or commercial_notes.md. evidence_files must "
"list only files you actually inspected."
),
default_manifest=pricing_manifest,
capabilities=[WorkspaceShellCapability()],
model_settings=ModelSettings(tool_choice="required", reasoning=Reasoning(effort="none")),
output_type=PricingPacketReview,
)
rollout_agent = SandboxAgent(
name="Rollout Risk Reviewer",
model=model,
instructions=(
"You inspect rollout plans and return a structured delivery review. Inspect the files "
"before answering and keep the output tightly grounded in the rollout documents. "
"Use the shell tool before answering. blockers must only contain issues that appear in "
"rollout_plan.md or support_history.md. Do not introduce any extra numbers, incidents, "
"or stakeholders beyond those files. evidence_files must list only files you actually "
"inspected."
),
default_manifest=rollout_manifest,
capabilities=[WorkspaceShellCapability()],
model_settings=ModelSettings(tool_choice="required", reasoning=Reasoning(effort="none")),
output_type=RolloutRiskReview,
)
# Each sandbox-backed tool gets its own run configuration so the workspaces stay isolated.
pricing_run_config = RunConfig(sandbox=SandboxRunConfig(client=UnixLocalSandboxClient()))
rollout_run_config = RunConfig(sandbox=SandboxRunConfig(client=UnixLocalSandboxClient()))
orchestrator = Agent(
name="Revenue Operations Coordinator",
model=model,
instructions=(
"You coordinate renewal reviews. Before answering, you must use all three tools: "
"`review_pricing_packet`, `review_rollout_risk`, and `get_discount_approval_rule`. "
"The review tools return JSON. Use the exact `requested_discount_percent` field from "
"`review_pricing_packet` when calling `get_discount_approval_rule`. In the final "
"recommendation, use only facts and numbers that appear in the tool outputs, and do "
"not add any extra incidents, price points, or contract terms."
),
model_settings=ModelSettings(tool_choice="required", reasoning=Reasoning(effort="none")),
tools=[
pricing_agent.as_tool(
tool_name="review_pricing_packet",
tool_description="Inspect the pricing packet and summarize commercial risk.",
custom_output_extractor=_structured_tool_output_extractor,
run_config=pricing_run_config,
max_turns=6,
),
rollout_agent.as_tool(
tool_name="review_rollout_risk",
tool_description="Inspect the rollout packet and summarize implementation risk.",
custom_output_extractor=_structured_tool_output_extractor,
run_config=rollout_run_config,
max_turns=6,
),
get_discount_approval_rule,
],
)
result = await Runner.run(orchestrator, question, max_turns=8)
tool_names = [
tool_call_name(item.raw_item)
for item in result.new_items
if getattr(item, "type", None) == "tool_call_item"
]
if tool_names:
print(f"[tools used] {', '.join(tool_names)}")
print(result.final_output)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--model", default="gpt-5.6-sol", help="Model name to use.")
parser.add_argument("--question", default=DEFAULT_QUESTION, help="Prompt to send to the agent.")
args = parser.parse_args()
asyncio.run(main(args.model, args.question))