Automating Dispute Management with Agents SDK and Stripe API (part 2)
You're reading part 2 of my exploration of the OpenAI Agents SDK, inspired by Dan Bell's post: Automating Dispute Management with Agents SDK and Stripe API:
-
Check out Part 1 where I explored the Stripe API,
-
Check out Part 3 where I looked at the JSON data of the requests and responses that the OpenAI Agents SDK sends to the Responses API,
-
Check out Part 4 where I decided to try removing all the agents from the original code.
Once I was clear on how to call the Stripe API, I kept diging into the post Dan Bell's post.
Next step: get to know the OpenAI Agents SDK.
By default, OpenAI Agents SDK uses the OpenAI Responses API. Tracing, which is enabled by default, relies on the Traces API (but can also be set to other tracing processors). This setup is convenient since we can monitor our agents straight from the Traces dashboard.
Testing with different Triage agents
To observe behavior when running the agent workflow:
await Runner.run(triage_agent, input=...)
I ran the workflow four times, each with a different
triage_agent
configuration:
-
No tools, no handoffs:
triage_agent = Agent( name="Triage Agent No Tools", instructions=( "Please do the following:\n" "1. Find the order ID from the payment intent's metadata.\n" "2. Retrieve detailed information about the order (e.g., shipping status).\n" "3. If the order has shipped, escalate this dispute to the investigator agent.\n" "4. If the order has not shipped, accept the dispute.\n" ), model="gpt-4o" )
-
One tool, no handoffs:
triage_agent = Agent( name="Triage Agent One Tool", instructions=(...), model="gpt-4o", tools=[retrieve_payment_intent] )
-
Two tools, no handoffs:
triage_agent = Agent( name="Triage Agent With Tools", instructions=(...), model="gpt-4o", tools=[retrieve_payment_intent, get_order] )
-
Two tools, two handoffs (as in the article):
triage_agent = Agent( name="Triage Agent With Tools And Agent", instructions=(...), model="gpt-4o", tools=[retrieve_payment_intent, get_order], handoffs=[accept_dispute_agent, investigator_agent] )
I used the same PaymentIntent each time, created like this:
payment = stripe.PaymentIntent.create(
amount=2000,
currency="usd",
payment_method = "pm_card_createDisputeProductNotReceived",
confirm=True,
metadata={"order_id": "1234"},
off_session=True,
automatic_payment_methods={"enabled": True},
)
And ran the workflow:
import asyncio
asyncio.run(process_dispute(payment.id, triage_agent))
Let's break down what happened:
With no tool provided, the workflow stopped with a text output only. No tool calls were made, and no action was taken on the dispute:
To address the dispute, we need to follow these steps:
1. **Find the Order ID from the Payment Intent's Metadata:**
- Access the payment intent with ID `pi_3RnJp1QuMzbiv2kf14SQqrm8`.
- Extract the metadata to locate the associated order ID.
2. **Retrieve Detailed Information About the Order:**
- Use the order ID to fetch order details such as shipping status.
3. **Check the Order's Shipping Status:**
- If the order has shipped:
- Escalate the dispute to the investigator agent.
- If the order has not shipped:
- Accept the dispute.
Proceed with the above steps to determine the next course of action.
With one tool, after the first call to
gpt-4o
, the agent
invoked
retrieve_payment_intent
.
But since no tool for order info was available, the agent stopped,
asking what method to use next:
The order ID from the payment intent's metadata is "1234".
Next, I'll retrieve detailed information about the order, such as its
shipping status. Could you please provide the service or method you'd
like me to use to obtain the order details?
With both tools, the agent first called
retrieve_payment_intent
to get the order ID, then called
get_order
for
fulfillment details. It then suggested accepting the dispute. The
workflow didn't actually close the dispute, as no handoff was
defined. Here's the final output:
The order with ID **1234** has not been shipped. You should accept the
dispute.
Things got interesting when I added both tools and handoffs to the
triage agent (as in the article). This time, the agent called both
tools, then the LLM decided to accept the dispute, and invoked a
function call to
transfer_to_accept_dispute_agent
tool. The OpenAI Agents SDK treated this as a handoff. The new
agent,
accept_dispute_agent
,
took over, with access to the full conversation history (by
default). It then called the
close_dispute
tool,
closing the dispute in Stripe using
stripe.Dispute.close(...)
. Here's the final output:
Dispute Details:
- Dispute ID: dp_1RnJp2QuMzbiv2kfeTMYrELd
- Amount: $20.00
- Reason for Dispute: Product not received
Order Details:
- Fulfillment status of the order: Not shipped
Reasoning for closing the dispute:
The dispute was accepted and closed because the order associated with
the payment was not shipped. The customer was charged for a product
that they did not receive, justifying the acceptance of the dispute.
Who adds the transfer_to_ prefix to handoffs, Responses API or OpenAI Agents SDK?
Sometimes, you know, sometimes you don't! I didn't and it was not clear to me.
tl;dr: The OpenAI Agents SDK is responsible for adding this prefix.
In the Traces dashboard, I noticed that the LLM could use four
functions:
retrieve_payment_intent
,
get_order
,
transfer_to_accept_dispute_agent
and
transfer_to_dispute_intake_agent
.
Here's what I wondered: when we define
triage_agent
like this
triage_agent = Agent(
name="...",
instructions="...",
model="gpt-4o",
tools=[retrieve_payment_intent, get_order],
handoffs=[accept_dispute_agent, investigator_agent]
)
why does the Responses API offer tool names like
get_order
, but for
handoffs, they become
transfer_to_dispute_agent
and
transfer_to_dispute_intake_agent
(instead of, say,
transfer_to_investigator_agent
)?
Is the Responses API changing the tool name, or is it coming from the request payload?
Nothing in the Response API reference! So this must be the SDK's responsibility.
The Handoffs documentation quickly clears this up:
Handoffs are represented as tools to the LLM. So if there's a handoff to an agent named "Refund Agent", the tool would be called
transfer_to_refund_agent
.
And the reason for
investigator_agent
showing as
transfer_to_dispute_intake_agent
is that its name is "Dispute Intake Agent".
As I never trust the docs blindly, I checked the OpenAI Agents SDK
source. Searching for
transfer_to
,
default_tool_name
, and
handoff\(
, I confirmed
that when the workflow runs, it does exactly what the docs say:
# src/agents/handoffs.py
@dataclass
class Handoff(Generic[TContext, TAgent]):
# ...
@classmethod
def default_tool_name(cls, agent: AgentBase[Any]) -> str:
return _transforms.transform_string_function_style(f"transfer_to_{agent.name}")
# ...
# ...
def handoff(agent: Agent[TContext], ...) -> Handoff[TContext, Agent[TContext]]:
# ...
tool_name = tool_name_override or Handoff.default_tool_name(agent)
# ...
# src/agents/util/_transforms.py
def transform_string_function_style(name: str) -> str:
# Replace spaces with underscores
name = name.replace(" ", "_")
# Replace non-alphanumeric characters with underscores
name = re.sub(r"[^a-zA-Z0-9]", "_", name)
return name.lower()
That's all I have for today! Talk to you soon ;)
Built with one.el.