Requirements

"openai>=1.65.4"
"maxim-py>=3.5.0"

Env variables

MAXIM_API_KEY=
MAXIM_LOG_REPO_ID=
OPENAI_API_KEY=

Customer service agent

Taken from OpenAI Agents Example Folder.

Flow:

  1. User Input: The user asks a question or makes a request.
  2. Triage Agent: This agent first handles the input, figuring out what the user needs and deciding which specialized agent should take over.
  3. Specialized Agents: Based on the request, the Triage Agent may pass the conversation to either the FAQ Agent or the Seat Booking Agent.
    • FAQ Agent: Answers common questions using a predefined tool.
    • Seat Booking Agent: Manages seat booking requests and updates seat information.
  4. Context Update: The system updates the context (like flight number, seat number, etc.) based on the user’s input and the actions taken by the agents.
  5. Logging/Tracing: All interactions and updates are logged and traced using the Maxim SDK for monitoring and analysis.
  6. Output: The final response is generated and sent back to the user.

This system ensures that user requests are efficiently routed to the right agent, and all interactions are tracked for quality and performance monitoring.

from __future__ import annotations as _annotations

import random
import uuid

from pydantic import BaseModel

from agents import (
    Agent,
    HandoffOutputItem,
    GuardrailFunctionOutput,
    input_guardrail,
    ItemHelpers,
    MessageOutputItem,
    RunContextWrapper,
    Runner,
    ToolCallItem,
    ToolCallOutputItem,
    TResponseInputItem,
    function_tool,
    handoff
)
from agents.extensions.handoff_prompt import RECOMMENDED_PROMPT_PREFIX

# CONTEXT


class AirlineAgentContext(BaseModel):
    passenger_name: str | None = None
    confirmation_number: str | None = None
    seat_number: str | None = None
    flight_number: str | None = None


# TOOLS

class FreeTicketBookingGuardrail(BaseModel):
    is_free_booking: bool
    reasoning: str


guardrail_agent = Agent(  # (1)!
    name="Guardrail check",
    instructions="Check if the user is asking you to book a ticket for free.",
    output_type=FreeTicketBookingGuardrail,
)


@input_guardrail
async def freebie_guardrail(  # (2)!
    ctx: RunContextWrapper[None], agent: Agent, input: str | list[TResponseInputItem]
) -> GuardrailFunctionOutput:
    result = await Runner.run(guardrail_agent, input, context=ctx.context)

    return GuardrailFunctionOutput(
        output_info=result.final_output,  # (3)!
        tripwire_triggered=result.final_output.is_free_booking,  # (4)!
    )


@function_tool(
    name_override="faq_lookup_tool", description_override="Lookup frequently asked questions."
)
async def faq_lookup_tool(question: str) -> str:
    if "bag" in question or "baggage" in question:
        return (
            "You are allowed to bring one bag on the plane. "
            "It must be under 50 pounds and 22 inches x 14 inches x 9 inches."
        )
    elif "seats" in question or "plane" in question:
        return (
            "There are 120 seats on the plane. "
            "There are 22 business class seats and 98 economy seats. "
            "Exit rows are rows 4 and 16. "
            "Rows 5-8 are Economy Plus, with extra legroom. "
        )
    elif "wifi" in question:
        return "We have free wifi on the plane, join Airline-Wifi"
    return "I'm sorry, I don't know the answer to that question."


@function_tool
async def update_seat(
    context: RunContextWrapper[AirlineAgentContext], confirmation_number: str, new_seat: str
) -> str:
    """
    Update the seat for a given confirmation number.

    Args:
        confirmation_number: The confirmation number for the flight.
        new_seat: The new seat to update to.
    """
    # Update the context based on the customer's input
    context.context.confirmation_number = confirmation_number
    context.context.seat_number = new_seat
    # Ensure that the flight number has been set by the incoming handoff
    assert context.context.flight_number is not None, "Flight number is required"
    return f"Updated seat to {new_seat} for confirmation number {confirmation_number}"


# HOOKS


async def on_seat_booking_handoff(context: RunContextWrapper[AirlineAgentContext]) -> None:
    flight_number = f"FLT-{random.randint(100, 999)}"
    context.context.flight_number = flight_number


# AGENTS

faq_agent = Agent[AirlineAgentContext](
    name="FAQ Agent",
    handoff_description="A helpful agent that can answer questions about the airline.",
    instructions=f"""{RECOMMENDED_PROMPT_PREFIX}
    You are an FAQ agent. If you are speaking to a customer, you probably were transferred to from the triage agent.
    Use the following routine to support the customer.
    # Routine
    1. Identify the last question asked by the customer.
    2. Use the faq lookup tool to answer the question. Do not rely on your own knowledge.
    3. If you cannot answer the question, transfer back to the triage agent.""",
    tools=[faq_lookup_tool],
)

seat_booking_agent = Agent[AirlineAgentContext](
    name="Seat Booking Agent",
    handoff_description="A helpful agent that can update a seat on a flight.",
    instructions=f"""{RECOMMENDED_PROMPT_PREFIX}
    You are a seat booking agent. If you are speaking to a customer, you probably were transferred to from the triage agent.
    Use the following routine to support the customer.
    # Routine
    1. Ask for their confirmation number.
    2. Ask the customer what their desired seat number is.
    3. Use the update seat tool to update the seat on the flight.
    If the customer asks a question that is not related to the routine, transfer back to the triage agent. """,
    tools=[update_seat],
)

triage_agent = Agent[AirlineAgentContext](
    name="Triage Agent",
    handoff_description="A triage agent that can delegate a customer's request to the appropriate agent.",
    instructions=(
        f"{RECOMMENDED_PROMPT_PREFIX} "
        "You are a helpful triaging agent. You can use your tools to delegate questions to other appropriate agents."
    ),
    handoffs=[
        faq_agent,
        handoff(agent=seat_booking_agent, on_handoff=on_seat_booking_handoff),
    ],
    input_guardrails=[freebie_guardrail],
)

faq_agent.handoffs.append(triage_agent)
seat_booking_agent.handoffs.append(triage_agent)

Initializing Maxim SDK

  • Maxim SDK picks up MAXIM_API_KEY and MAXIM_LOG_REPO_ID from environment variables.
  • You can pass them as parameters if you would like to.
  • Learn more here.
from agents import add_trace_processor
from maxim import Maxim
from maxim.logger.openai.agents import MaximOpenAIAgentsTracingProcessor
# Creating a new logger instance
# It automatically initializes using MAXIM_API_KEY and MAXIM_LOG_REPO_ID from env variables.

logger = Maxim().logger()
add_trace_processor(MaximOpenAIAgentsTracingProcessor(logger))

Run agent

current_agent: Agent[AirlineAgentContext] = triage_agent
input_items: list[TResponseInputItem] = []
context = AirlineAgentContext()

# Normally, each input from the user would be an API request to your app, and you can wrap the request in a trace()
# Here, we'll just use a random UUID for the conversation ID
conversation_id = uuid.uuid4().hex[:16]

user_input = "Whats the max allowed baggage"
input_items.append({"content": user_input, "role": "user"})
result = await Runner.run(current_agent, input_items, context=context)
for new_item in result.new_items:
    agent_name = new_item.agent.name
    if isinstance(new_item, MessageOutputItem):
        print(f"{agent_name}: {ItemHelpers.text_message_output(new_item)}")
    elif isinstance(new_item, HandoffOutputItem):
        print(
            f"Handed off from {new_item.source_agent.name} to {new_item.target_agent.name}"
        )
    elif isinstance(new_item, ToolCallItem):
        print(f"{agent_name}: Calling a tool")
    elif isinstance(new_item, ToolCallOutputItem):
        print(f"{agent_name}: Tool call output: {new_item.output}")
    else:
        print(f"{agent_name}: Skipping item: {new_item.__class__.__name__}")
Triage Agent: Skipping item: HandoffCallItem
Handed off from Triage Agent to FAQ Agent
FAQ Agent: Calling a tool
FAQ Agent: Tool call output: You are allowed to bring one bag on the plane. It must be under 50 pounds and 22 inches x 14 inches x 9 inches.
FAQ Agent: You are allowed to bring one bag on the plane, which must be under 50 pounds and measure 22 inches x 14 inches x 9 inches.

Maxim dashboard

You can view the trace of the agents’ interactions on the Maxim dashboard, which provides detailed insights and visualizations of the entire process.