From 7d6db13e23a372eb92167ea05fabe2da10465540 Mon Sep 17 00:00:00 2001 From: Anatoli Beliaev Date: Tue, 30 Sep 2025 15:45:05 -0700 Subject: [PATCH] Port customer_service sample from OpenAI SDK --- .../openai_agents/customer_service/README.md | 46 +++++ .../customer_service/customer_service.py | 176 ++++++++++++++++++ .../customer_service_client.py | 47 +++++ samples-v2/openai_agents/function_app.py | 6 + 4 files changed, 275 insertions(+) create mode 100644 samples-v2/openai_agents/customer_service/README.md create mode 100644 samples-v2/openai_agents/customer_service/customer_service.py create mode 100644 samples-v2/openai_agents/customer_service/customer_service_client.py diff --git a/samples-v2/openai_agents/customer_service/README.md b/samples-v2/openai_agents/customer_service/README.md new file mode 100644 index 00000000..b0a28c2f --- /dev/null +++ b/samples-v2/openai_agents/customer_service/README.md @@ -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:///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 diff --git a/samples-v2/openai_agents/customer_service/customer_service.py b/samples-v2/openai_agents/customer_service/customer_service.py new file mode 100644 index 00000000..a4c80773 --- /dev/null +++ b/samples-v2/openai_agents/customer_service/customer_service.py @@ -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) diff --git a/samples-v2/openai_agents/customer_service/customer_service_client.py b/samples-v2/openai_agents/customer_service/customer_service_client.py new file mode 100644 index 00000000..73c92a91 --- /dev/null +++ b/samples-v2/openai_agents/customer_service/customer_service_client.py @@ -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() diff --git a/samples-v2/openai_agents/function_app.py b/samples-v2/openai_agents/function_app.py index 68805de7..1f52fde4 100644 --- a/samples-v2/openai_agents/function_app.py +++ b/samples-v2/openai_agents/function_app.py @@ -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))