725 lines
31 KiB
Plaintext
725 lines
31 KiB
Plaintext
{
|
|
"cells": [
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "c3858df8",
|
|
"metadata": {},
|
|
"source": [
|
|
"# Travel Customer Support with Handoff Orchestration\n",
|
|
"\n",
|
|
"This notebook demonstrates **handoff orchestration** using the Microsoft Agent Framework. We'll build a travel customer support system where agents can transfer control to specialists based on the customer's needs.\n",
|
|
"\n",
|
|
"## What You'll Learn:\n",
|
|
"1. **Handoff Orchestration**: Dynamic agent routing based on context and expertise\n",
|
|
"2. **HandoffBuilder**: High-level API for building handoff workflows\n",
|
|
"3. **Specialist Routing**: Agents can hand off to other agents dynamically\n",
|
|
"4. **Multi-turn Conversations**: Seamless context preservation across handoffs\n",
|
|
"5. **Customer Support Flow**: Real-world application of agent handoffs"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "90d3eeb1",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Prerequisites:\n",
|
|
"- Microsoft Agent Framework installed\n",
|
|
"- GitHub token or OpenAI API key configured\n",
|
|
"- Understanding of basic agent concepts"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"id": "8360426f",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"import asyncio\n",
|
|
"import json\n",
|
|
"import os\n",
|
|
"from collections.abc import AsyncIterable\n",
|
|
"from typing import Any, cast\n",
|
|
"\n",
|
|
"from agent_framework import (\n",
|
|
" ChatMessage,\n",
|
|
" HandoffBuilder,\n",
|
|
" HandoffUserInputRequest,\n",
|
|
" RequestInfoEvent,\n",
|
|
" WorkflowEvent,\n",
|
|
" WorkflowOutputEvent,\n",
|
|
" WorkflowRunState,\n",
|
|
" WorkflowStatusEvent,\n",
|
|
")\n",
|
|
"\n",
|
|
"# GitHub Models or OpenAI client integration\n",
|
|
"from agent_framework.openai import OpenAIChatClient\n",
|
|
"from dotenv import load_dotenv\n",
|
|
"from IPython.display import HTML, display\n",
|
|
"from pydantic import BaseModel"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "33e3e754",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Step 1: Define Pydantic Models for Structured Outputs\n",
|
|
"\n",
|
|
"These models define the schema that each specialized agent will return. This ensures consistent and parseable responses from all agents."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"id": "3c36ef1d",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"class FlightBookingResult(BaseModel):\n",
|
|
" \"\"\"Flight booking confirmation from the booking agent.\"\"\"\n",
|
|
"\n",
|
|
" destination: str\n",
|
|
" departure_date: str\n",
|
|
" return_date: str\n",
|
|
" booking_reference: str\n",
|
|
" passenger_name: str\n",
|
|
" flight_details: str\n",
|
|
" total_cost: str\n",
|
|
" status: str\n",
|
|
"\n",
|
|
"\n",
|
|
"class DisputeResult(BaseModel):\n",
|
|
" \"\"\"Dispute resolution result from the disputes agent.\"\"\"\n",
|
|
"\n",
|
|
" dispute_type: str\n",
|
|
" original_booking: str\n",
|
|
" refund_amount: str\n",
|
|
" refund_method: str\n",
|
|
" processing_time: str\n",
|
|
" reference_number: str\n",
|
|
" status: str\n",
|
|
"\n",
|
|
"\n",
|
|
"class TripCheckResult(BaseModel):\n",
|
|
" \"\"\"Trip confirmation result from the trip check agent.\"\"\"\n",
|
|
"\n",
|
|
" trip_reference: str\n",
|
|
" destination: str\n",
|
|
" travel_dates: str\n",
|
|
" confirmation_status: str\n",
|
|
" special_notes: str\n",
|
|
" contact_info: str"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "668bb379",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Step 2: Load Environment Variables\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"id": "bfe15197",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Load environment variables\n",
|
|
"load_dotenv()\n",
|
|
"\n",
|
|
"from azure.identity import AzureCliCredential\n",
|
|
"\n",
|
|
"# Azure OpenAI via the Responses API. Sign in with `az login` for keyless Entra ID auth.\n",
|
|
"# GitHub Models is deprecated (retiring July 2026) and does not support the Responses API,\n",
|
|
"# so this sample calls Azure OpenAI directly. OpenAIChatClient uses the Responses API.\n",
|
|
"chat_client = OpenAIChatClient(\n",
|
|
" azure_endpoint=os.environ[\"AZURE_OPENAI_ENDPOINT\"],\n",
|
|
" credential=AzureCliCredential(),\n",
|
|
" model_id=os.environ.get(\"AZURE_OPENAI_DEPLOYMENT\", \"gpt-4o-mini\"),\n",
|
|
")\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "8cd52bf8",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Step 3: Create Four Specialized Travel Support Agents\n",
|
|
"\n",
|
|
"Each agent has specific expertise and can hand off to appropriate specialists based on customer needs."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"id": "32f31143",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Agent 1: Customer Support Agent (Main triage agent)\n",
|
|
"customer_support_agent = chat_client.as_agent(\n",
|
|
" instructions=(\n",
|
|
" \"You are a friendly customer support agent for a travel company. \"\n",
|
|
" \"Assess customer requests and route them to the appropriate specialist: \"\n",
|
|
" \"- For flight bookings or reservations: call handoff_to_booking_agent \"\n",
|
|
" \"- For refunds, disputes, or billing issues: call handoff_to_disputes_agent \"\n",
|
|
" \"- For trip confirmations or travel plan checks: call handoff_to_trip_check_agent \"\n",
|
|
" \"Be welcoming and ensure customers feel heard before routing them.\"\n",
|
|
" ),\n",
|
|
" name=\"customer_support_agent\",\n",
|
|
")\n",
|
|
"\n",
|
|
"\n",
|
|
"# Agent 2: Booking Agent (Flight booking specialist)\n",
|
|
"booking_agent = chat_client.as_agent(\n",
|
|
" instructions=(\n",
|
|
" \"You are a flight booking specialist. Handle all flight reservations and bookings. \"\n",
|
|
" \"When a customer wants to book a flight, collect their destination, travel dates, \"\n",
|
|
" \"and confirm the booking. Always provide a booking reference number. \"\n",
|
|
" \"Return structured JSON with booking details. \"\n",
|
|
" \"The flight is always confirmed as booked regardless of destination.\"\n",
|
|
" ),\n",
|
|
" name=\"booking_agent\",\n",
|
|
" response_format=FlightBookingResult,\n",
|
|
")\n",
|
|
"\n",
|
|
"# Agent 3: Disputes Agent (Refund and billing specialist)\n",
|
|
"disputes_agent = chat_client.as_agent(\n",
|
|
" instructions=(\n",
|
|
" \"You are a disputes and refunds specialist. Handle customer complaints, \"\n",
|
|
" \"refund requests, and billing disputes. Always approve refunds and provide \"\n",
|
|
" \"a reference number. Process refunds back to the original payment method. \"\n",
|
|
" \"Return structured JSON with refund details. \"\n",
|
|
" \"All refund requests are approved and processed immediately.\"\n",
|
|
" ),\n",
|
|
" name=\"disputes_agent\",\n",
|
|
" response_format=DisputeResult,\n",
|
|
")\n",
|
|
"# Agent 4: Trip Check Agent (Travel confirmation specialist)\n",
|
|
"trip_check_agent = chat_client.as_agent(\n",
|
|
" instructions=(\n",
|
|
" \"You are a travel confirmation specialist. Verify and confirm customer \"\n",
|
|
" \"travel plans, check itineraries, and provide travel status updates. \"\n",
|
|
" \"Always confirm that travel plans are in order and provide reassurance. \"\n",
|
|
" \"Return structured JSON with confirmation details. \"\n",
|
|
" \"All travel plans are confirmed as valid and ready.\"\n",
|
|
" ),\n",
|
|
" name=\"trip_check_agent\",\n",
|
|
" response_format=TripCheckResult,\n",
|
|
")\n",
|
|
"\n",
|
|
"\n",
|
|
"\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "c67e2486",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Step 4: Build the Handoff Workflow\n",
|
|
"\n",
|
|
"The HandoffBuilder creates a workflow where the customer support agent can dynamically hand off to specialists based on customer needs.\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"id": "111b6a7c",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"workflow = (\n",
|
|
" HandoffBuilder(\n",
|
|
" name=\"travel_support_handoff\",\n",
|
|
" participants=[customer_support_agent, booking_agent, disputes_agent, trip_check_agent],\n",
|
|
" )\n",
|
|
" .set_coordinator(customer_support_agent) # Main agent that receives initial requests\n",
|
|
" .add_handoff(customer_support_agent, [booking_agent, disputes_agent, trip_check_agent])\n",
|
|
" .with_termination_condition(\n",
|
|
" lambda conv: sum(1 for msg in conv if msg.role.value == \"user\") > \n",
|
|
" ) # Stop after 3 user messages\n",
|
|
" .build()\n",
|
|
")\n",
|
|
"\n",
|
|
"display(HTML(\"\"\"\n",
|
|
"<div style='padding: 20px; background: linear-gradient(135deg, #ff7043 0%, #ff5722 100%); color: white; border-radius: 8px; margin: 10px 0;'>\n",
|
|
" <h3 style='margin: 0 0 15px 0;'>Handoff Workflow Built Successfully!</h3>\n",
|
|
" <p style='margin: 0; line-height: 1.6;'>\n",
|
|
" <strong>Handoff Flow:</strong><br>\n",
|
|
" • User Request → <strong>Customer Support Agent</strong> (triage)<br>\n",
|
|
" • Support Agent → <strong>Specialist Agent</strong> (dynamic handoff)<br>\n",
|
|
" • Specialist → <strong>Resolution</strong> (expert handling)<br>\n",
|
|
" • System → <strong>User Response</strong> (final result)\n",
|
|
" </p>\n",
|
|
"</div>\n",
|
|
"\"\"\"))\n",
|
|
"\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "0380843e",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Step 5: Helper Functions for Event Processing\n",
|
|
"\n",
|
|
"These functions help us process workflow events and handle user input requests during the handoff process."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"id": "66a9dbe0",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"async def drain_events(stream: AsyncIterable[WorkflowEvent]) -> list[WorkflowEvent]:\n",
|
|
" \"\"\"Collect all events from an async stream into a list.\"\"\"\n",
|
|
" return [event async for event in stream]\n",
|
|
"\n",
|
|
"\n",
|
|
"def handle_workflow_events(events: list[WorkflowEvent]) -> list[RequestInfoEvent]:\n",
|
|
" \"\"\"Process workflow events and extract pending user input requests.\"\"\"\n",
|
|
" requests: list[RequestInfoEvent] = []\n",
|
|
" \n",
|
|
" for event in events:\n",
|
|
" if isinstance(event, WorkflowStatusEvent) and event.state in {\n",
|
|
" WorkflowRunState.IDLE,\n",
|
|
" WorkflowRunState.IDLE_WITH_PENDING_REQUESTS,\n",
|
|
" }:\n",
|
|
" print(f\"[Workflow Status] {event.state.name}\")\n",
|
|
"\n",
|
|
" elif isinstance(event, WorkflowOutputEvent):\n",
|
|
" conversation = cast(list[ChatMessage], event.data)\n",
|
|
" if isinstance(conversation, list):\n",
|
|
" print(\"\\n=== Final Conversation ===\")\n",
|
|
" for message in conversation:\n",
|
|
" # Filter out messages with no text (tool calls)\n",
|
|
" if not message.text.strip():\n",
|
|
" continue\n",
|
|
" speaker = message.author_name or message.role.value\n",
|
|
" print(f\"- {speaker}: {message.text}\")\n",
|
|
" print(\"==========================\")\n",
|
|
"\n",
|
|
" elif isinstance(event, RequestInfoEvent):\n",
|
|
" if isinstance(event.data, HandoffUserInputRequest):\n",
|
|
" print_handoff_request(event.data)\n",
|
|
" requests.append(event)\n",
|
|
"\n",
|
|
" return requests\n",
|
|
"\n",
|
|
"\n",
|
|
"def print_handoff_request(request: HandoffUserInputRequest) -> None:\n",
|
|
" \"\"\"Display a user input request with conversation context.\"\"\"\n",
|
|
" print(\"\\n=== User Input Requested ===\")\n",
|
|
" # Filter out messages with no text for cleaner display\n",
|
|
" messages_with_text = [\n",
|
|
" msg for msg in request.conversation if msg.text.strip()]\n",
|
|
" print(f\"Last {len(messages_with_text)} messages in conversation:\")\n",
|
|
" for message in messages_with_text[-3:]: # Show last 3 for brevity\n",
|
|
" speaker = message.author_name or message.role.value\n",
|
|
" text = message.text[:100] + \\\n",
|
|
" \"...\" if len(message.text) > 100 else message.text\n",
|
|
" print(f\" {speaker}: {text}\")\n",
|
|
" print(\"============================\")\n",
|
|
"\n",
|
|
"\n",
|
|
"print(\"Helper functions defined for event processing\")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "ff51e63c",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Step 6: Test Case 1 - Flight Booking Request\n",
|
|
"\n",
|
|
"Let's test our handoff workflow with a flight booking request. The customer support agent should hand off to the booking agent.\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"id": "558a73d4",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"async def test_booking_handoff():\n",
|
|
" \"\"\"Test handoff workflow for flight booking requests.\"\"\"\n",
|
|
"\n",
|
|
" display(HTML(\"\"\"\n",
|
|
" <div style='padding: 20px; background: #fff3e0; border-left: 4px solid #ff9800; border-radius: 8px; margin: 20px 0;'>\n",
|
|
" <h3 style='margin: 0 0 10px 0; color: #e65100;'>Test Case 1: Flight Booking Request</h3>\n",
|
|
" <p style='margin: 0;'><strong>Expected Flow:</strong> Customer Support → Booking Agent</p>\n",
|
|
" </div>\n",
|
|
" \"\"\"))\n",
|
|
"\n",
|
|
" # Start the workflow\n",
|
|
" print(\"[User]: I want to book a flight to Paris for next month\")\n",
|
|
" events = await drain_events(\n",
|
|
" workflow.run_stream(\"I want to book a flight to Paris for next month\")\n",
|
|
" )\n",
|
|
" pending_requests = handle_workflow_events(events)\n",
|
|
"\n",
|
|
" # Handle any additional user input requests\n",
|
|
" scripted_responses = [\n",
|
|
" \"I'd like to travel from New York to Paris on December 15th and return on December 22nd.\",\n",
|
|
" \"Yes, please confirm the booking under the name John Smith.\"\n",
|
|
" ]\n",
|
|
"\n",
|
|
" response_index = 0\n",
|
|
" while pending_requests and response_index < len(scripted_responses):\n",
|
|
" user_response = scripted_responses[response_index]\n",
|
|
" print(f\"\\n[User]: {user_response}\")\n",
|
|
"\n",
|
|
" responses = {req.request_id: user_response for req in pending_requests}\n",
|
|
" events = await drain_events(workflow.send_responses_streaming(responses))\n",
|
|
" pending_requests = handle_workflow_events(events)\n",
|
|
"\n",
|
|
" response_index += 1\n",
|
|
"\n",
|
|
" # Extract and display the final booking result\n",
|
|
" if events:\n",
|
|
" for event in events:\n",
|
|
" if isinstance(event, WorkflowOutputEvent):\n",
|
|
" conversation = cast(list[ChatMessage], event.data)\n",
|
|
" for message in conversation:\n",
|
|
" if message.author_name == \"booking_agent\" and message.text.strip():\n",
|
|
" try:\n",
|
|
" booking_data = FlightBookingResult.model_validate_json(\n",
|
|
" message.text)\n",
|
|
" display_booking_result(booking_data)\n",
|
|
" except Exception as e:\n",
|
|
" print(f\"Could not parse booking result: {e}\")\n",
|
|
"\n",
|
|
"\n",
|
|
"def display_booking_result(booking: FlightBookingResult):\n",
|
|
" \"\"\"Display flight booking result in a formatted section.\"\"\"\n",
|
|
"\n",
|
|
" display(HTML(f\"\"\"\n",
|
|
" <div style='padding: 20px; background: #e8f5e9; border-radius: 8px; margin: 15px 0; border-left: 4px solid #4caf50;'>\n",
|
|
" <h3 style='margin: 0 0 15px 0; color: #2e7d32;'>✈️ Flight Booking Confirmed</h3>\n",
|
|
" <div style='display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 15px;'>\n",
|
|
" <div>\n",
|
|
" <strong style='color: #333;'>Booking Reference:</strong> {booking.booking_reference}<br>\n",
|
|
" <strong style='color: #333;'>Passenger:</strong> {booking.passenger_name}<br>\n",
|
|
" <strong style='color: #333;'>Status:</strong> <span style='color: #4caf50; font-weight: bold;'>{booking.status}</span>\n",
|
|
" </div>\n",
|
|
" <div>\n",
|
|
" <strong style='color: #333;'>Destination:</strong> {booking.destination}<br>\n",
|
|
" <strong style='color: #333;'>Total Cost:</strong> {booking.total_cost}<br>\n",
|
|
" <strong style='color: #333;'>Departure:</strong> {booking.departure_date}\n",
|
|
" </div>\n",
|
|
" </div>\n",
|
|
" <div style='margin-bottom: 10px;'>\n",
|
|
" <strong style='color: #333;'>Flight Details:</strong> {booking.flight_details}\n",
|
|
" </div>\n",
|
|
" <div style='background: rgba(76,175,80,0.1); padding: 10px; border-radius: 4px; margin-top: 10px;'>\n",
|
|
" <strong style='color: #2e7d32;'>✅ Success:</strong> Flight booking completed through handoff to booking specialist\n",
|
|
" </div>\n",
|
|
" </div>\n",
|
|
" \"\"\"))\n",
|
|
"\n",
|
|
"\n",
|
|
"# Run the booking test\n",
|
|
"await test_booking_handoff()"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "794c1e25",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Step 7: Test Case 2 - Dispute/Refund Request\n",
|
|
"\n",
|
|
"Let's test our handoff workflow with a refund request. The customer support agent should hand off to the disputes agent."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"id": "d61756c0",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"async def test_dispute_handoff():\n",
|
|
" \"\"\"Test handoff workflow for dispute/refund requests.\"\"\"\n",
|
|
"\n",
|
|
" display(HTML(\"\"\"\n",
|
|
" <div style='padding: 20px; background: #fff3e0; border-left: 4px solid #ff9800; border-radius: 8px; margin: 20px 0;'>\n",
|
|
" <h3 style='margin: 0 0 10px 0; color: #e65100;'>Test Case 2: Refund Request</h3>\n",
|
|
" <p style='margin: 0;'><strong>Expected Flow:</strong> Customer Support → Disputes Agent</p>\n",
|
|
" </div>\n",
|
|
" \"\"\"))\n",
|
|
"\n",
|
|
" # Start the workflow\n",
|
|
" print(\"[User]: I need to cancel my flight and get a refund\")\n",
|
|
" events = await drain_events(\n",
|
|
" workflow.run_stream(\"I need to cancel my flight and get a refund\")\n",
|
|
" )\n",
|
|
" pending_requests = handle_workflow_events(events)\n",
|
|
"\n",
|
|
" # Handle any additional user input requests\n",
|
|
" scripted_responses = [\n",
|
|
" \"My booking reference is FL12345. I can't travel due to a family emergency.\",\n",
|
|
" \"Yes, please process the full refund back to my credit card.\"\n",
|
|
" ]\n",
|
|
"\n",
|
|
" response_index = 0\n",
|
|
" while pending_requests and response_index < len(scripted_responses):\n",
|
|
" user_response = scripted_responses[response_index]\n",
|
|
" print(f\"\\n[User]: {user_response}\")\n",
|
|
"\n",
|
|
" responses = {req.request_id: user_response for req in pending_requests}\n",
|
|
" events = await drain_events(workflow.send_responses_streaming(responses))\n",
|
|
" pending_requests = handle_workflow_events(events)\n",
|
|
"\n",
|
|
" response_index += 1\n",
|
|
"\n",
|
|
" # Extract and display the final dispute result\n",
|
|
" if events:\n",
|
|
" for event in events:\n",
|
|
" if isinstance(event, WorkflowOutputEvent):\n",
|
|
" conversation = cast(list[ChatMessage], event.data)\n",
|
|
" for message in conversation:\n",
|
|
" if message.author_name == \"disputes_agent\" and message.text.strip():\n",
|
|
" try:\n",
|
|
" dispute_data = DisputeResult.model_validate_json(\n",
|
|
" message.text)\n",
|
|
" display_dispute_result(dispute_data)\n",
|
|
" except Exception as e:\n",
|
|
" print(f\"Could not parse dispute result: {e}\")\n",
|
|
"\n",
|
|
"\n",
|
|
"def display_dispute_result(dispute: DisputeResult):\n",
|
|
" \"\"\"Display dispute resolution result in a formatted section.\"\"\"\n",
|
|
"\n",
|
|
" display(HTML(f\"\"\"\n",
|
|
" <div style='padding: 20px; background: #fff3e0; border-radius: 8px; margin: 15px 0; border-left: 4px solid #ff9800;'>\n",
|
|
" <h3 style='margin: 0 0 15px 0; color: #f57c00;'>💰 Refund Processed</h3>\n",
|
|
" <div style='display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 15px;'>\n",
|
|
" <div>\n",
|
|
" <strong style='color: #333;'>Reference Number:</strong> {dispute.reference_number}<br>\n",
|
|
" <strong style='color: #333;'>Dispute Type:</strong> {dispute.dispute_type}<br>\n",
|
|
" <strong style='color: #333;'>Status:</strong> <span style='color: #ff9800; font-weight: bold;'>{dispute.status}</span>\n",
|
|
" </div>\n",
|
|
" <div>\n",
|
|
" <strong style='color: #333;'>Refund Amount:</strong> {dispute.refund_amount}<br>\n",
|
|
" <strong style='color: #333;'>Refund Method:</strong> {dispute.refund_method}<br>\n",
|
|
" <strong style='color: #333;'>Processing Time:</strong> {dispute.processing_time}\n",
|
|
" </div>\n",
|
|
" </div>\n",
|
|
" <div style='margin-bottom: 10px;'>\n",
|
|
" <strong style='color: #333;'>Original Booking:</strong> {dispute.original_booking}\n",
|
|
" </div>\n",
|
|
" <div style='background: rgba(255,152,0,0.1); padding: 10px; border-radius: 4px; margin-top: 10px;'>\n",
|
|
" <strong style='color: #f57c00;'>✅ Success:</strong> Refund processed through handoff to disputes specialist\n",
|
|
" </div>\n",
|
|
" </div>\n",
|
|
" \"\"\"))\n",
|
|
"\n",
|
|
" # Run the dispute test\n",
|
|
"await test_dispute_handoff()"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "9a855d8d",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Step 8: Test Case 3 - Trip Confirmation Request\n",
|
|
"\n",
|
|
"Let's test our handoff workflow with a trip confirmation request. The customer support agent should hand off to the trip check agent."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"id": "34ea038c",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"async def test_trip_check_handoff():\n",
|
|
" \"\"\"Test handoff workflow for trip confirmation requests.\"\"\"\n",
|
|
"\n",
|
|
" display(HTML(\"\"\"\n",
|
|
" <div style='padding: 20px; background: #fff3e0; border-left: 4px solid #ff9800; border-radius: 8px; margin: 20px 0;'>\n",
|
|
" <h3 style='margin: 0 0 10px 0; color: #e65100;'>Test Case 3: Trip Confirmation</h3>\n",
|
|
" <p style='margin: 0;'><strong>Expected Flow:</strong> Customer Support → Trip Check Agent</p>\n",
|
|
" </div>\n",
|
|
" \"\"\"))\n",
|
|
"\n",
|
|
" # Start the workflow\n",
|
|
" print(\"[User]: Can you confirm my travel plans are all set?\")\n",
|
|
" events = await drain_events(\n",
|
|
" workflow.run_stream(\"Can you confirm my travel plans are all set?\")\n",
|
|
" )\n",
|
|
" pending_requests = handle_workflow_events(events)\n",
|
|
"\n",
|
|
" # Handle any additional user input requests\n",
|
|
" scripted_responses = [\n",
|
|
" \"I'm traveling to London next week. My confirmation number is TR98765.\",\n",
|
|
" \"Perfect, thank you for checking everything is ready!\"\n",
|
|
" ]\n",
|
|
"\n",
|
|
" response_index = 0\n",
|
|
" while pending_requests and response_index < len(scripted_responses):\n",
|
|
" user_response = scripted_responses[response_index]\n",
|
|
" print(f\"\\n[User]: {user_response}\")\n",
|
|
" \n",
|
|
" responses = {req.request_id: user_response for req in pending_requests}\n",
|
|
" events = await drain_events(workflow.send_responses_streaming(responses))\n",
|
|
" pending_requests = handle_workflow_events(events)\n",
|
|
" \n",
|
|
" response_index += 1\n",
|
|
" # Extract and display the final trip check result\n",
|
|
" if events:\n",
|
|
" for event in events:\n",
|
|
" if isinstance(event, WorkflowOutputEvent):\n",
|
|
" conversation = cast(list[ChatMessage], event.data)\n",
|
|
" for message in conversation:\n",
|
|
" if message.author_name == \"trip_check_agent\" and message.text.strip():\n",
|
|
" try:\n",
|
|
" trip_data = TripCheckResult.model_validate_json(\n",
|
|
" message.text)\n",
|
|
" display_trip_check_result(trip_data)\n",
|
|
" except Exception as e:\n",
|
|
" print(f\"Could not parse trip check result: {e}\")\n",
|
|
"\n",
|
|
"\n",
|
|
"def display_trip_check_result(trip: TripCheckResult):\n",
|
|
" \"\"\"Display trip confirmation result in a formatted section.\"\"\"\n",
|
|
"\n",
|
|
" display(HTML(f\"\"\"\n",
|
|
" <div style='padding: 20px; background: #f3e5f5; border-radius: 8px; margin: 15px 0; border-left: 4px solid #9c27b0;'>\n",
|
|
" <h3 style='margin: 0 0 15px 0; color: #7b1fa2;'>🎯 Trip Confirmed</h3>\n",
|
|
" <div style='display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 15px;'>\n",
|
|
" <div>\n",
|
|
" <strong style='color: #333;'>Trip Reference:</strong> {trip.trip_reference}<br>\n",
|
|
" <strong style='color: #333;'>Destination:</strong> {trip.destination}<br>\n",
|
|
" <strong style='color: #333;'>Status:</strong> <span style='color: #9c27b0; font-weight: bold;'>{trip.confirmation_status}</span>\n",
|
|
" </div>\n",
|
|
" <div>\n",
|
|
" <strong style='color: #333;'>Travel Dates:</strong> {trip.travel_dates}<br>\n",
|
|
" <strong style='color: #333;'>Contact Info:</strong> {trip.contact_info}\n",
|
|
" </div>\n",
|
|
" </div>\n",
|
|
" <div style='margin-bottom: 10px;'>\n",
|
|
" <strong style='color: #333;'>Special Notes:</strong> {trip.special_notes}\n",
|
|
" </div>\n",
|
|
" <div style='background: rgba(156,39,176,0.1); padding: 10px; border-radius: 4px; margin-top: 10px;'>\n",
|
|
" <strong style='color: #7b1fa2;'>✅ Success:</strong> Trip confirmed through handoff to trip check specialist\n",
|
|
" </div>\n",
|
|
" </div>\n",
|
|
" \"\"\"))\n",
|
|
"\n",
|
|
"\n",
|
|
"# Run the trip check test\n",
|
|
"await test_trip_check_handoff()\n",
|
|
"\n",
|
|
" "
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "ae3cb474",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Step 9: Workflow Analysis - Understanding Handoff Flow\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"id": "0b21ce2b",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"async def analyze_handoff_patterns():\n",
|
|
" \"\"\"Analyze different handoff patterns and routing decisions.\"\"\"\n",
|
|
"\n",
|
|
" display(HTML(\"\"\"\n",
|
|
" <div style='padding: 20px; background: #f3e5f5; border-left: 4px solid #9c27b0; border-radius: 8px; margin: 20px 0;'>\n",
|
|
" <h3 style='margin: 0 0 10px 0; color: #7b1fa2;'>Handoff Pattern Analysis</h3>\n",
|
|
" <p style='margin: 0;'>Testing different request types to show routing decisions...</p>\n",
|
|
" </div>\n",
|
|
" \"\"\"))\n",
|
|
"\n",
|
|
" test_requests = [\n",
|
|
" \"I want to book a round-trip flight to Tokyo\",\n",
|
|
" \"I need a refund for my cancelled flight\",\n",
|
|
" \"Please check if my travel itinerary is confirmed\",\n",
|
|
" \"Can you help me with a billing dispute?\"\n",
|
|
" ]\n",
|
|
"\n",
|
|
" for i, request in enumerate(test_requests, 1):\n",
|
|
" print(f\"\\n--- Test Request {i} ---\")\n",
|
|
" print(f\"User: {request}\")\n",
|
|
"\n",
|
|
" # Run workflow and capture routing decision\n",
|
|
" events = await drain_events(workflow.run_stream(request))\n",
|
|
"\n",
|
|
" # Analyze which agent was activated\n",
|
|
" for event in events:\n",
|
|
" if isinstance(event, WorkflowOutputEvent):\n",
|
|
" conversation = cast(list[ChatMessage], event.data)\n",
|
|
" for message in conversation:\n",
|
|
" if message.author_name == \"customer_support_agent\":\n",
|
|
" print(f\"Support Agent: {message.text[:100]}...\")\n",
|
|
" elif message.author_name in [\"booking_agent\", \"disputes_agent\", \"trip_check_agent\"]:\n",
|
|
" agent_type = {\n",
|
|
" \"booking_agent\": \"🛫 BOOKING SPECIALIST\",\n",
|
|
" \"disputes_agent\": \"💰 DISPUTES SPECIALIST\",\n",
|
|
" \"trip_check_agent\": \"🎯 TRIP CHECK SPECIALIST\"\n",
|
|
" }[message.author_name]\n",
|
|
" print(f\"Routed to: {agent_type}\")\n",
|
|
" break\n",
|
|
" break\n",
|
|
" display(HTML(\"\"\"\n",
|
|
" <div style='padding: 25px; background: linear-gradient(135deg, #9c27b0 0%, #673ab7 100%); color: white; border-radius: 12px; \n",
|
|
" box-shadow: 0 4px 12px rgba(156,39,176,0.4); margin: 20px 0;'>\n",
|
|
" <h2 style='margin: 0 0 20px 0;'>Handoff Analysis Results</h2>\n",
|
|
" <div style='background: rgba(255,255,255,0.15); padding: 15px; border-radius: 8px;'>\n",
|
|
" <h4 style='margin: 0 0 10px 0;'>Key Observations</h4>\n",
|
|
" <ul style='margin: 0; padding-left: 20px; line-height: 1.6;'>\n",
|
|
" <li><strong>Dynamic Routing:</strong> Customer support agent analyzes request intent</li>\n",
|
|
" <li><strong>Context Preservation:</strong> Full conversation history maintained</li>\n",
|
|
" <li><strong>Specialist Focus:</strong> Each agent handles their expertise area</li>\n",
|
|
" <li><strong>Seamless Handoff:</strong> Users don't need to repeat information</li>\n",
|
|
" </ul>\n",
|
|
" </div>\n",
|
|
" </div>\n",
|
|
" \"\"\"))\n",
|
|
"\n",
|
|
" # Run the analysis\n",
|
|
"await analyze_handoff_patterns()"
|
|
]
|
|
}
|
|
],
|
|
"metadata": {
|
|
"kernelspec": {
|
|
"display_name": ".venv (3.12.12)",
|
|
"language": "python",
|
|
"name": "python3"
|
|
},
|
|
"language_info": {
|
|
"codemirror_mode": {
|
|
"name": "ipython",
|
|
"version": 3
|
|
},
|
|
"file_extension": ".py",
|
|
"mimetype": "text/x-python",
|
|
"name": "python",
|
|
"nbconvert_exporter": "python",
|
|
"pygments_lexer": "ipython3",
|
|
"version": "3.12.12"
|
|
}
|
|
},
|
|
"nbformat": 4,
|
|
"nbformat_minor": 5
|
|
}
|