Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions samples-v2/openai_agents/customer_service/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Customer Service Sample

This sample demonstrates a customer service agent built with Azure Durable Functions and OpenAI Agents. The agent can handle airline-related queries including FAQ lookups and seat booking.

## Running the Sample

**For complete setup instructions, configuration details, and troubleshooting, see the [Getting Started Guide](/docs/openai_agents/getting-started.md).**

### Step 1: Start the Azure Functions App

From the OpenAI Agents samples root directory (`/samples-v2/openai_agents`), start the Azure Functions host:

```bash
func start
```

The function app will start and listen on `http://localhost:7071` by default.

### Step 2: Start the Interactive Client

In a separate terminal, navigate to the `customer_service` directory and run the client:

```bash
cd customer_service
python customer_service_client.py
```

If your function app is running on a different host or port, you can specify a custom URL:

```bash
python customer_service_client.py --start-url http://<app-host-URL>/api/orchestrators/customer_service
```

The client will:

1. Start a new orchestration instance
2. Wait for prompts from the agent
3. Allow you to interact with the customer service agent interactively

## Usage

Once the client is running, you can:

- Ask FAQ questions (e.g., "What's the baggage policy?", "Is there wifi?")
- Request seat changes (the agent will guide you through the process)
- Type `exit`, `quit`, or `bye` to end the conversation
176 changes: 176 additions & 0 deletions samples-v2/openai_agents/customer_service/customer_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
from __future__ import annotations as _annotations

import random

from azure.durable_functions.openai_agents import DurableAIAgentContext
from pydantic import BaseModel

from agents import (
Agent,
HandoffOutputItem,
ItemHelpers,
MessageOutputItem,
RunContextWrapper,
Runner,
ToolCallItem,
ToolCallOutputItem,
TResponseInputItem,
function_tool,
handoff,
trace,
)
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


@function_tool(
name_override="faq_lookup_tool", description_override="Lookup frequently asked questions."
)
async def faq_lookup_tool(question: str) -> str:
question_lower = question.lower()
if any(
keyword in question_lower
for keyword in ["bag", "baggage", "luggage", "carry-on", "hand luggage", "hand carry"]
):
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 any(keyword in question_lower for keyword in ["seat", "seats", "seating", "plane"]):
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 any(
keyword in question_lower
for keyword in ["wifi", "internet", "wireless", "connectivity", "network", "online"]
):
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),
],
)

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


### RUN


def main(context: DurableAIAgentContext):
current_agent: Agent[AirlineAgentContext] = triage_agent
input_items: list[TResponseInputItem] = []
airline_agent_context = AirlineAgentContext()

conversation_id = context.instance_id

context.set_custom_status("How can I help you today?")
while True:
user_input = yield context.wait_for_external_event("UserInput")
if user_input is None or user_input.strip().lower() in ["exit", "quit", "bye"]:
context.set_custom_status("Goodbye!")
break
context.set_custom_status("Thinking...")
with trace("Customer service", group_id=conversation_id):
input_items.append({"content": user_input, "role": "user"})
result = Runner.run_sync(current_agent, input_items, context=airline_agent_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__}")
input_items = result.to_input_list()
current_agent = result.last_agent

context.set_custom_status(result.final_output)
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import argparse
import requests
import time
import sys


def main():
parser = argparse.ArgumentParser(description='Customer Service Orchestration Client')
parser.add_argument(
'--start-url',
default='http://localhost:7071/api/orchestrators/customer_service',
help='The orchestrator start URL'
)
args = parser.parse_args()

# Start the orchestration
orchestration = requests.post(args.start_url).json()

while True:
# Wait for a prompt in the custom status
while True:
status = requests.get(orchestration['statusQueryGetUri']).json()

if status['runtimeStatus'] == 'Completed':
print(f"Orchestration completed.")
sys.exit(0)

if status['runtimeStatus'] not in ['Pending', 'Running']:
raise Exception(f"Unexpected orchestration status: {status['runtimeStatus']}")

if status.get('customStatus') and status['customStatus'] != 'Thinking...':
break

time.sleep(1)

# Prompt the user for input interactively
user_input = input(status['customStatus'] + ': ')

# Send the user input to the orchestration as an external event
event_url = orchestration['sendEventPostUri'].replace('{eventName}', 'UserInput')
requests.post(event_url, json=user_input)

time.sleep(2)


if __name__ == '__main__':
main()
6 changes: 6 additions & 0 deletions samples-v2/openai_agents/function_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,9 @@ async def random_number_tool(max: int) -> int:
def message_filter(context):
import handoffs.message_filter
return handoffs.message_filter.main(context.create_activity_tool(random_number_tool))

@app.orchestration_trigger(context_name="context")
@app.durable_openai_agent_orchestrator
def customer_service(context):
import customer_service.customer_service
return (yield from customer_service.customer_service.main(context))