|
3 | 3 | import asyncio
|
4 | 4 | import random
|
5 | 5 | import uuid
|
| 6 | +from typing import Literal |
6 | 7 |
|
7 | 8 | from pydantic import BaseModel
|
8 | 9 |
|
|
11 | 12 | HandoffOutputItem,
|
12 | 13 | ItemHelpers,
|
13 | 14 | MessageOutputItem,
|
14 |
| - RunContextWrapper, |
| 15 | + RunContextWrapper, |
15 | 16 | Runner,
|
16 | 17 | ToolCallItem,
|
17 | 18 | ToolCallOutputItem,
|
|
22 | 23 | )
|
23 | 24 | from agents.extensions.handoff_prompt import RECOMMENDED_PROMPT_PREFIX
|
24 | 25 |
|
25 |
| -### CONTEXT |
26 | 26 |
|
| 27 | +class EcommerceAgentContext(BaseModel): |
| 28 | + """Holds conversation state for e-commerce support.""" |
| 29 | + customer_name: str | None = None |
| 30 | + order_id: str | None = None |
| 31 | + product_sku: str | None = None |
| 32 | + last_inquiry_type: Literal["order", "product", "other"] | None = None |
27 | 33 |
|
28 |
| -class AirlineAgentContext(BaseModel): |
29 |
| - passenger_name: str | None = None |
30 |
| - confirmation_number: str | None = None |
31 |
| - seat_number: str | None = None |
32 |
| - flight_number: str | None = None |
33 | 34 |
|
| 35 | +@function_tool |
| 36 | +async def get_order_status(order_id: str) -> str: |
| 37 | + """ |
| 38 | + Looks up the status of a given order ID. |
34 | 39 |
|
35 |
| -### TOOLS |
36 |
| - |
37 |
| - |
38 |
| -@function_tool( |
39 |
| - name_override="faq_lookup_tool", description_override="Lookup frequently asked questions." |
40 |
| -) |
41 |
| -async def faq_lookup_tool(question: str) -> str: |
42 |
| - if "bag" in question or "baggage" in question: |
43 |
| - return ( |
44 |
| - "You are allowed to bring one bag on the plane. " |
45 |
| - "It must be under 50 pounds and 22 inches x 14 inches x 9 inches." |
46 |
| - ) |
47 |
| - elif "seats" in question or "plane" in question: |
48 |
| - return ( |
49 |
| - "There are 120 seats on the plane. " |
50 |
| - "There are 22 business class seats and 98 economy seats. " |
51 |
| - "Exit rows are rows 4 and 16. " |
52 |
| - "Rows 5-8 are Economy Plus, with extra legroom. " |
53 |
| - ) |
54 |
| - elif "wifi" in question: |
55 |
| - return "We have free wifi on the plane, join Airline-Wifi" |
56 |
| - return "I'm sorry, I don't know the answer to that question." |
57 |
| - |
| 40 | + Args: |
| 41 | + order_id: The unique identifier for the order (e.g., ORD-12345). |
| 42 | + """ |
| 43 | + print(f"--- Tool: Simulating lookup for order: {order_id} ---") |
| 44 | + if not order_id or not order_id.startswith("ORD-"): |
| 45 | + return "Invalid order ID format. Please provide an ID like 'ORD-12345'." |
| 46 | + |
| 47 | + possible_statuses = ["Processing", "Shipped", "Delivered", "Delayed", "Cancelled"] |
| 48 | + status = possible_statuses[hash(order_id) % len(possible_statuses)] |
| 49 | + |
| 50 | + if status == "Shipped": |
| 51 | + tracking = f"TRK-{random.randint(100000000, 999999999)}" |
| 52 | + return f"Order {order_id} has been Shipped. Tracking number: {tracking}" |
| 53 | + elif status == "Delivered": |
| 54 | + return f"Order {order_id} was Delivered successfully." |
| 55 | + elif status == "Processing": |
| 56 | + return f"Order {order_id} is currently Processing. Expected ship date is in 2 business days." |
| 57 | + elif status == "Delayed": |
| 58 | + return f"Order {order_id} is currently Delayed due to high volume. We apologize for the inconvenience." |
| 59 | + else: # Cancelled |
| 60 | + return f"Order {order_id} has been Cancelled." |
58 | 61 |
|
59 | 62 | @function_tool
|
60 |
| -async def update_seat( |
61 |
| - context: RunContextWrapper[AirlineAgentContext], confirmation_number: str, new_seat: str |
62 |
| -) -> str: |
| 63 | +async def get_product_info(product_sku: str) -> str: |
63 | 64 | """
|
64 |
| - Update the seat for a given confirmation number. |
| 65 | + Provides information about a product based on its SKU. |
65 | 66 |
|
66 | 67 | Args:
|
67 |
| - confirmation_number: The confirmation number for the flight. |
68 |
| - new_seat: The new seat to update to. |
| 68 | + product_sku: The Stock Keeping Unit (SKU) of the product (e.g., SKU-TECH-001). |
69 | 69 | """
|
70 |
| - # Update the context based on the customer's input |
71 |
| - context.context.confirmation_number = confirmation_number |
72 |
| - context.context.seat_number = new_seat |
73 |
| - # Ensure that the flight number has been set by the incoming handoff |
74 |
| - assert context.context.flight_number is not None, "Flight number is required" |
75 |
| - return f"Updated seat to {new_seat} for confirmation number {confirmation_number}" |
| 70 | + print(f"--- Tool: Simulating lookup for product SKU: {product_sku} ---") |
| 71 | + if not product_sku or not product_sku.startswith("SKU-"): |
| 72 | + return "Invalid SKU format. Please provide an SKU like 'SKU-TECH-001'." |
| 73 | + |
| 74 | + products = { |
| 75 | + "SKU-TECH-001": {"name": "Wireless Mouse", "price": 25.99, "stock": 150, "desc": "A reliable ergonomic wireless mouse."}, |
| 76 | + "SKU-TECH-002": {"name": "Mechanical Keyboard", "price": 79.99, "stock": 50, "desc": "A backlit mechanical keyboard with blue switches."}, |
| 77 | + "SKU-HOME-001": {"name": "Coffee Mug", "price": 12.50, "stock": 0, "desc": "A ceramic coffee mug with our logo."}, |
| 78 | + } |
76 | 79 |
|
| 80 | + info = products.get(product_sku) |
77 | 81 |
|
78 |
| -### HOOKS |
| 82 | + if info: |
| 83 | + stock_status = f"In Stock ({info['stock']} available)" if info['stock'] > 0 else "Out of Stock" |
| 84 | + return ( |
| 85 | + f"Product: {info['name']} (SKU: {product_sku})\n" |
| 86 | + f"Description: {info['desc']}\n" |
| 87 | + f"Price: ${info['price']:.2f}\n" |
| 88 | + f"Availability: {stock_status}" |
| 89 | + ) |
| 90 | + else: |
| 91 | + return f"Sorry, I could not find any information for product SKU: {product_sku}." |
79 | 92 |
|
80 | 93 |
|
81 |
| -async def on_seat_booking_handoff(context: RunContextWrapper[AirlineAgentContext]) -> None: |
82 |
| - flight_number = f"FLT-{random.randint(100, 999)}" |
83 |
| - context.context.flight_number = flight_number |
| 94 | +async def on_order_status_handoff(context: RunContextWrapper[EcommerceAgentContext]) -> None: |
| 95 | + print("--- Hook: Handing off to Order Status Agent ---") |
84 | 96 |
|
85 | 97 |
|
86 |
| -### AGENTS |
| 98 | +triage_agent: Agent[EcommerceAgentContext] |
87 | 99 |
|
88 |
| -faq_agent = Agent[AirlineAgentContext]( |
89 |
| - name="FAQ Agent", |
90 |
| - handoff_description="A helpful agent that can answer questions about the airline.", |
| 100 | +order_status_agent = Agent[EcommerceAgentContext]( |
| 101 | + name="Order Status Agent", |
| 102 | + handoff_description="Handles inquiries about the status of existing orders.", |
91 | 103 | instructions=f"""{RECOMMENDED_PROMPT_PREFIX}
|
92 |
| - You are an FAQ agent. If you are speaking to a customer, you probably were transferred to from the triage agent. |
93 |
| - Use the following routine to support the customer. |
94 |
| - # Routine |
95 |
| - 1. Identify the last question asked by the customer. |
96 |
| - 2. Use the faq lookup tool to answer the question. Do not rely on your own knowledge. |
97 |
| - 3. If you cannot answer the question, transfer back to the triage agent.""", |
98 |
| - tools=[faq_lookup_tool], |
| 104 | + You are a specialized agent responsible for providing order status updates. |
| 105 | + Your goal is to assist customers who have questions about their orders. |
| 106 | +
|
| 107 | + # Routine: |
| 108 | + 1. Check if the user has provided an order ID. If not, politely ask for it (e.g., "Could you please provide your order ID, usually starting with 'ORD-'?"). |
| 109 | + 2. Once you have the order ID, use the `get_order_status` tool to look it up. |
| 110 | + 3. Provide the status information clearly to the customer. |
| 111 | + 4. If the customer asks about something *other* than order status (e.g., product details, returns, general questions), hand the conversation back to the Triage Agent. Do not attempt to answer unrelated questions yourself. |
| 112 | + """, |
| 113 | + tools=[get_order_status], |
| 114 | + handoffs=[], # Will be set after triage_agent is defined |
99 | 115 | )
|
100 | 116 |
|
101 |
| -seat_booking_agent = Agent[AirlineAgentContext]( |
102 |
| - name="Seat Booking Agent", |
103 |
| - handoff_description="A helpful agent that can update a seat on a flight.", |
| 117 | +product_info_agent = Agent[EcommerceAgentContext]( |
| 118 | + name="Product Info Agent", |
| 119 | + handoff_description="Provides details about specific products based on their SKU.", |
104 | 120 | instructions=f"""{RECOMMENDED_PROMPT_PREFIX}
|
105 |
| - You are a seat booking agent. If you are speaking to a customer, you probably were transferred to from the triage agent. |
106 |
| - Use the following routine to support the customer. |
107 |
| - # Routine |
108 |
| - 1. Ask for their confirmation number. |
109 |
| - 2. Ask the customer what their desired seat number is. |
110 |
| - 3. Use the update seat tool to update the seat on the flight. |
111 |
| - If the customer asks a question that is not related to the routine, transfer back to the triage agent. """, |
112 |
| - tools=[update_seat], |
| 121 | + You are a specialized agent responsible for providing product information. |
| 122 | + Your goal is to assist customers looking for details about products we sell. |
| 123 | +
|
| 124 | + # Routine: |
| 125 | + 1. Check if the user has provided a product SKU. If not, politely ask for it (e.g., "Do you have the product SKU, usually starting with 'SKU-'?"). You can also try to infer it if they describe a product mentioned by the `get_product_info` tool. |
| 126 | + 2. Once you have the SKU, use the `get_product_info` tool to look it up. |
| 127 | + 3. Provide the product details (description, price, availability) clearly to the customer. |
| 128 | + 4. If the customer asks about something *other* than product information (e.g., order status, returns, general questions), hand the conversation back to the Triage Agent. Do not attempt to answer unrelated questions yourself. |
| 129 | + """, |
| 130 | + tools=[get_product_info], |
| 131 | + handoffs=[], # Will be set after triage_agent is defined |
113 | 132 | )
|
114 | 133 |
|
115 |
| -triage_agent = Agent[AirlineAgentContext]( |
| 134 | +triage_agent = Agent[EcommerceAgentContext]( |
116 | 135 | name="Triage Agent",
|
117 |
| - handoff_description="A triage agent that can delegate a customer's request to the appropriate agent.", |
| 136 | + handoff_description="The main customer support agent that directs inquiries to the correct specialist.", |
118 | 137 | instructions=(
|
119 | 138 | f"{RECOMMENDED_PROMPT_PREFIX} "
|
120 |
| - "You are a helpful triaging agent. You can use your tools to delegate questions to other appropriate agents." |
| 139 | + "You are the primary E-commerce Support Agent. Your main role is to understand the customer's needs and delegate the query to the appropriate specialist agent using your tools.\n" |
| 140 | + "Available Specialists:\n" |
| 141 | + "- **Order Status Agent:** Handles questions about existing order status.\n" |
| 142 | + "- **Product Info Agent:** Provides details about specific products.\n\n" |
| 143 | + "Analyze the customer's message. If it's clearly about an order status, hand off to the Order Status Agent. If it's clearly about product details, hand off to the Product Info Agent. If you are unsure, or it's a general question, try to clarify or answer briefly if possible, but prioritize handing off to specialists for their specific tasks." |
| 144 | + " If a specialist agent hands back to you, understand the context and see if another specialist is needed or if you can handle the request now." |
121 | 145 | ),
|
122 | 146 | handoffs=[
|
123 |
| - faq_agent, |
124 |
| - handoff(agent=seat_booking_agent, on_handoff=on_seat_booking_handoff), |
| 147 | + order_status_agent, |
| 148 | + product_info_agent, |
| 149 | + handoff(agent=order_status_agent, on_handoff=on_order_status_handoff), |
125 | 150 | ],
|
126 | 151 | )
|
127 | 152 |
|
128 |
| -faq_agent.handoffs.append(triage_agent) |
129 |
| -seat_booking_agent.handoffs.append(triage_agent) |
130 |
| - |
131 |
| - |
132 |
| -### RUN |
| 153 | +order_status_agent.handoffs.append(triage_agent) |
| 154 | +product_info_agent.handoffs.append(triage_agent) |
133 | 155 |
|
134 | 156 |
|
135 | 157 | async def main():
|
136 |
| - current_agent: Agent[AirlineAgentContext] = triage_agent |
| 158 | + current_agent: Agent[EcommerceAgentContext] = triage_agent |
137 | 159 | input_items: list[TResponseInputItem] = []
|
138 |
| - context = AirlineAgentContext() |
| 160 | + context = EcommerceAgentContext() # Initialize the context |
139 | 161 |
|
140 |
| - # Normally, each input from the user would be an API request to your app, and you can wrap the request in a trace() |
141 |
| - # Here, we'll just use a random UUID for the conversation ID |
142 | 162 | conversation_id = uuid.uuid4().hex[:16]
|
| 163 | + print(f"Starting E-commerce Support Conversation (ID: {conversation_id})") |
| 164 | + print("Enter 'quit' to exit.") |
| 165 | + print(f"Agent: {current_agent.name}: How can I help you today?") # Initial greeting |
143 | 166 |
|
144 | 167 | while True:
|
145 |
| - user_input = input("Enter your message: ") |
146 |
| - with trace("Customer service", group_id=conversation_id): |
| 168 | + user_input = input("You: ") |
| 169 | + if user_input.lower() == 'quit': |
| 170 | + print("Ending conversation.") |
| 171 | + break |
| 172 | + |
| 173 | + with trace("E-commerce Support Turn", group_id=conversation_id): |
147 | 174 | input_items.append({"content": user_input, "role": "user"})
|
148 |
| - result = await Runner.run(current_agent, input_items, context=context) |
| 175 | + |
| 176 | + result = await Runner.run( |
| 177 | + current_agent, |
| 178 | + input_items, |
| 179 | + context=context |
| 180 | + ) |
149 | 181 |
|
150 | 182 | for new_item in result.new_items:
|
151 | 183 | agent_name = new_item.agent.name
|
152 | 184 | if isinstance(new_item, MessageOutputItem):
|
153 |
| - print(f"{agent_name}: {ItemHelpers.text_message_output(new_item)}") |
| 185 | + message = ItemHelpers.text_message_output(new_item) |
| 186 | + print(f"Agent: {agent_name}: {message}") |
154 | 187 | elif isinstance(new_item, HandoffOutputItem):
|
155 | 188 | print(
|
156 |
| - f"Handed off from {new_item.source_agent.name} to {new_item.target_agent.name}" |
| 189 | + f"--- System: Handed off from {new_item.source_agent.name} to {new_item.target_agent.name} ---" |
157 | 190 | )
|
| 191 | + if new_item.target_agent is order_status_agent: |
| 192 | + context.last_inquiry_type = "order" |
| 193 | + elif new_item.target_agent is product_info_agent: |
| 194 | + context.last_inquiry_type = "product" |
| 195 | + else: |
| 196 | + context.last_inquiry_type = "other" |
| 197 | + |
158 | 198 | elif isinstance(new_item, ToolCallItem):
|
159 |
| - print(f"{agent_name}: Calling a tool") |
| 199 | + tool_name = new_item.tool_call.function.name |
| 200 | + args = new_item.tool_call.function.arguments |
| 201 | + print(f"--- System: {agent_name} calling tool `{tool_name}` with args: {args} ---") |
160 | 202 | elif isinstance(new_item, ToolCallOutputItem):
|
161 |
| - print(f"{agent_name}: Tool call output: {new_item.output}") |
| 203 | + pass # Often the agent summarizes this in its next message |
162 | 204 | else:
|
163 |
| - print(f"{agent_name}: Skipping item: {new_item.__class__.__name__}") |
| 205 | + print(f"--- System: {agent_name} produced item: {new_item.__class__.__name__} ---") |
| 206 | + |
164 | 207 | input_items = result.to_input_list()
|
165 | 208 | current_agent = result.last_agent
|
166 | 209 |
|
167 |
| - |
168 | 210 | if __name__ == "__main__":
|
169 |
| - asyncio.run(main()) |
| 211 | + try: |
| 212 | + asyncio.run(main()) |
| 213 | + except KeyboardInterrupt: |
| 214 | + print("\nExiting...") |
0 commit comments