"""Pausing an OpenAI Agents SDK agent on Raposa until a human decides. The point of the example is the shape, not the framework. An agent is about to do something expensive or irreversible; instead of doing it, it calls Raposa, waits, and acts on the answer. The record of that decision outlives the agent run, the process, and the framework — which is the whole reason to put it outside the runtime rather than inside it. Run it yourself: pip install openai-agents httpx export RAPOSA_API_KEY=... # from your account page export OPENAI_API_KEY=... python examples/openai_agents_sdk.py The Raposa half of this file is exercised in CI without either key — see tests/test_example_openai.py. """ import os import time DEFAULT_BASE = os.environ.get("RAPOSA_BASE_URL", "https://dcescrypt.com/api") class Denied(Exception): """A human said no. Not an error — an answer.""" class TimedOut(Exception): """Nobody answered in time. The action must not happen by default.""" class RaposaGate: """One call: ask a human, wait, come back with the answer. `http` exists so tests can hand in a client bound to a local app instead of the network. In real use it is httpx. """ def __init__(self, api_key, base_url=DEFAULT_BASE, http=None, poll_sec=2.0): self.api_key = api_key self.base_url = base_url.rstrip("/") self.poll_sec = poll_sec if http is None: import httpx http = httpx.Client(base_url=self.base_url, timeout=30.0) self.http = http def _headers(self): return {"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"} def request(self, action, context, risk="high", requested_by="agent", expires_in_sec=3600, webhook_url=None): body = {"action": action, "context": context, "risk": risk, "requested_by": requested_by, "expires_in_sec": expires_in_sec} if webhook_url: body["webhook_url"] = webhook_url r = self.http.post(f"{self.base_url}/v1/approvals", json=body, headers=self._headers()) if r.status_code == 429: raise RuntimeError("Raposa quota exceeded for this month: " + r.text) r.raise_for_status() return r.json() def status(self, approval_id): r = self.http.get(f"{self.base_url}/v1/approvals/{approval_id}", headers=self._headers()) r.raise_for_status() return r.json() def wait(self, approval_id, timeout_sec=600, sleep=None): """Block until decided. Anything that is not an explicit approval is a no.""" sleep = sleep or time.sleep deadline = time.monotonic() + timeout_sec while True: state = self.status(approval_id) if state["status"] == "approved": return state if state["status"] in ("rejected", "expired"): raise Denied(f"{state['status']}: {state.get('decision_comment') or ''}") if time.monotonic() >= deadline: raise TimedOut(f"no decision on {approval_id} within {timeout_sec}s") sleep(self.poll_sec) def guard(self, action, context, timeout_sec=600, **kw): """Ask and wait in one call. Raises unless a human said yes. `timeout_sec` is named here rather than pulled out of **kw: popping it after the call had already forwarded it made request() raise TypeError, which is exactly the kind of thing an unrun example ships with. """ made = self.request(action, context, **kw) return self.wait(made["id"], timeout_sec=timeout_sec) # ---- wiring it into the OpenAI Agents SDK -------------------------------- def build_agent(gate): """An agent whose money-moving tool cannot fire without a human. The tool body is ordinary Python: it asks Raposa first and only then does the thing. The model cannot skip the gate, because the gate is not something the model decides — it is the first line of the function. """ from agents import Agent, function_tool # openai-agents @function_tool def issue_refund(order_id: str, amount_eur: float, reason: str) -> str: """Refund a customer. Requires human approval before any money moves.""" try: decision = gate.guard( action="issue_refund", context=f"Order {order_id}, EUR {amount_eur:.2f}. Reason: {reason}", risk="high" if amount_eur >= 100 else "medium", requested_by="support-agent", ) except Denied as exc: return f"Refund not issued — a human declined: {exc}" except TimedOut as exc: return f"Refund not issued — nobody answered in time: {exc}" # ... the real refund call goes here, and only here ... return (f"Refund issued for {order_id}. Approved by {decision.get('decided_by')} " f"at {decision.get('decided_at')}. Approval id {decision.get('id')}.") return Agent( name="Support agent", instructions=("You help with refunds. Never promise a refund before the tool " "returns — a human has to approve it first."), tools=[issue_refund], ) def main(): api_key = os.environ.get("RAPOSA_API_KEY") if not api_key: raise SystemExit("set RAPOSA_API_KEY (see your account page at /api/portal)") gate = RaposaGate(api_key) from agents import Runner agent = build_agent(gate) result = Runner.run_sync( agent, "Customer says order 48213 was charged twice. Refund EUR 249.00.") print(result.final_output) if __name__ == "__main__": main()