Skip to content

Commit 57b9d63

Browse files
jssmithclaude
andcommitted
Port continue-as-new fix for customer service workflow
- Add case-insensitive string matching in FAQ lookup tool - Update init_agents to return tuple with agent map for continue-as-new state management - Implement continue-as-new functionality with proper state serialization - Fix client output to skip duplicate user message on print 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 396fb5c commit 57b9d63

3 files changed

Lines changed: 85 additions & 26 deletions

File tree

openai_agents/customer_service/customer_service.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations as _annotations
22

3+
from typing import Dict, Tuple
4+
35
from agents import Agent, RunContextWrapper, function_tool, handoff
46
from agents.extensions.handoff_prompt import RECOMMENDED_PROMPT_PREFIX
57
from pydantic import BaseModel
@@ -23,19 +25,20 @@ class AirlineAgentContext(BaseModel):
2325
description_override="Lookup frequently asked questions.",
2426
)
2527
async def faq_lookup_tool(question: str) -> str:
26-
if "bag" in question or "baggage" in question:
28+
question_lower = question.lower()
29+
if "bag" in question_lower or "baggage" in question_lower:
2730
return (
2831
"You are allowed to bring one bag on the plane. "
2932
"It must be under 50 pounds and 22 inches x 14 inches x 9 inches."
3033
)
31-
elif "seats" in question or "plane" in question:
34+
elif "seats" in question_lower or "plane" in question_lower:
3235
return (
3336
"There are 120 seats on the plane. "
3437
"There are 22 business class seats and 98 economy seats. "
3538
"Exit rows are rows 4 and 16. "
3639
"Rows 5-8 are Economy Plus, with extra legroom. "
3740
)
38-
elif "wifi" in question:
41+
elif "wifi" in question_lower:
3942
return "We have free wifi on the plane, join Airline-Wifi"
4043
return "I'm sorry, I don't know the answer to that question."
4144

@@ -74,7 +77,9 @@ async def on_seat_booking_handoff(
7477
### AGENTS
7578

7679

77-
def init_agents() -> Agent[AirlineAgentContext]:
80+
def init_agents() -> Tuple[
81+
Agent[AirlineAgentContext], Dict[str, Agent[AirlineAgentContext]]
82+
]:
7883
"""
7984
Initialize the agents for the airline customer service workflow.
8085
:return: triage agent
@@ -121,7 +126,9 @@ def init_agents() -> Agent[AirlineAgentContext]:
121126

122127
faq_agent.handoffs.append(triage_agent)
123128
seat_booking_agent.handoffs.append(triage_agent)
124-
return triage_agent
129+
return triage_agent, {
130+
agent.name: agent for agent in [faq_agent, seat_booking_agent, triage_agent]
131+
}
125132

126133

127134
class ProcessUserMessageInput(BaseModel):

openai_agents/customer_service/run_customer_service_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ async def main():
6767
CustomerServiceWorkflow.process_user_message, message_input
6868
)
6969
history.extend(new_history)
70-
print(*new_history, sep="\n")
70+
print(*new_history[1:], sep="\n")
7171
except WorkflowUpdateFailedError:
7272
print("** Stale conversation. Reloading...")
7373
length = len(history)
Lines changed: 72 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations as _annotations
22

33
from agents import (
4-
Agent,
4+
HandoffCallItem,
55
HandoffOutputItem,
66
ItemHelpers,
77
MessageOutputItem,
@@ -12,6 +12,7 @@
1212
TResponseInputItem,
1313
trace,
1414
)
15+
from pydantic import dataclasses
1516
from temporalio import workflow
1617

1718
from openai_agents.customer_service.customer_service import (
@@ -21,32 +22,77 @@
2122
)
2223

2324

25+
@dataclasses.dataclass
26+
class CustomerServiceWorkflowState:
27+
printed_history: list[str]
28+
current_agent_name: str
29+
context: AirlineAgentContext
30+
input_items: list[TResponseInputItem]
31+
32+
2433
@workflow.defn
2534
class CustomerServiceWorkflow:
2635
@workflow.init
27-
def __init__(self, input_items: list[TResponseInputItem] | None = None):
36+
def __init__(
37+
self, customer_service_state: CustomerServiceWorkflowState | None = None
38+
):
2839
self.run_config = RunConfig()
29-
self.chat_history: list[str] = []
30-
self.current_agent: Agent[AirlineAgentContext] = init_agents()
31-
self.context = AirlineAgentContext()
32-
self.input_items = [] if input_items is None else input_items
40+
41+
starting_agent, self.agent_map = init_agents()
42+
self.current_agent = (
43+
self.agent_map[customer_service_state.current_agent_name]
44+
if customer_service_state
45+
else starting_agent
46+
)
47+
self.context = (
48+
customer_service_state.context
49+
if customer_service_state
50+
else AirlineAgentContext()
51+
)
52+
self.printed_history: list[str] = (
53+
customer_service_state.printed_history if customer_service_state else []
54+
)
55+
self.input_items = (
56+
customer_service_state.input_items if customer_service_state else []
57+
)
58+
self.continue_as_new_suggested = False
3359

3460
@workflow.run
35-
async def run(self, input_items: list[TResponseInputItem] | None = None):
61+
async def run(
62+
self, customer_service_state: CustomerServiceWorkflowState | None = None
63+
):
3664
await workflow.wait_condition(
37-
lambda: workflow.info().is_continue_as_new_suggested()
65+
# lambda: workflow.info().is_continue_as_new_suggested()
66+
# and
67+
lambda: self.continue_as_new_suggested
3868
and workflow.all_handlers_finished()
3969
)
40-
workflow.continue_as_new(self.input_items)
70+
# Convert input_items to plain dictionaries for serialization
71+
serializable_input_items = []
72+
for item in self.input_items:
73+
if hasattr(item, "model_dump") and callable(getattr(item, "model_dump")):
74+
# Convert Pydantic objects to dictionaries
75+
serializable_input_items.append(item.model_dump()) # type: ignore
76+
else:
77+
# Already a plain Python object
78+
serializable_input_items.append(item)
79+
workflow.continue_as_new(
80+
CustomerServiceWorkflowState(
81+
printed_history=self.printed_history,
82+
current_agent_name=self.current_agent.name,
83+
context=self.context,
84+
input_items=serializable_input_items,
85+
)
86+
)
4187

4288
@workflow.query
4389
def get_chat_history(self) -> list[str]:
44-
return self.chat_history
90+
return self.printed_history
4591

4692
@workflow.update
4793
async def process_user_message(self, input: ProcessUserMessageInput) -> list[str]:
48-
length = len(self.chat_history)
49-
self.chat_history.append(f"User: {input.user_input}")
94+
length = len(self.printed_history)
95+
self.printed_history.append(f"User: {input.user_input}")
5096
with trace("Customer service", group_id=workflow.info().workflow_id):
5197
self.input_items.append({"content": input.user_input, "role": "user"})
5298
result = await Runner.run(
@@ -59,33 +105,39 @@ async def process_user_message(self, input: ProcessUserMessageInput) -> list[str
59105
for new_item in result.new_items:
60106
agent_name = new_item.agent.name
61107
if isinstance(new_item, MessageOutputItem):
62-
self.chat_history.append(
108+
self.printed_history.append(
63109
f"{agent_name}: {ItemHelpers.text_message_output(new_item)}"
64110
)
65111
elif isinstance(new_item, HandoffOutputItem):
66-
self.chat_history.append(
112+
self.printed_history.append(
67113
f"Handed off from {new_item.source_agent.name} to {new_item.target_agent.name}"
68114
)
115+
elif isinstance(new_item, HandoffCallItem):
116+
self.printed_history.append(
117+
f"{agent_name}: Handed off to tool {new_item.raw_item.name}"
118+
)
69119
elif isinstance(new_item, ToolCallItem):
70-
self.chat_history.append(f"{agent_name}: Calling a tool")
120+
self.printed_history.append(f"{agent_name}: Calling a tool")
71121
elif isinstance(new_item, ToolCallOutputItem):
72-
self.chat_history.append(
122+
self.printed_history.append(
73123
f"{agent_name}: Tool call output: {new_item.output}"
74124
)
75125
else:
76-
self.chat_history.append(
126+
self.printed_history.append(
77127
f"{agent_name}: Skipping item: {new_item.__class__.__name__}"
78128
)
79129
self.input_items = result.to_input_list()
80130
self.current_agent = result.last_agent
81-
workflow.set_current_details("\n\n".join(self.chat_history))
82-
return self.chat_history[length:]
131+
workflow.set_current_details("\n\n".join(self.printed_history))
132+
133+
self.continue_as_new_suggested = True
134+
return self.printed_history[length:]
83135

84136
@process_user_message.validator
85137
def validate_process_user_message(self, input: ProcessUserMessageInput) -> None:
86138
if not input.user_input:
87139
raise ValueError("User input cannot be empty.")
88140
if len(input.user_input) > 1000:
89141
raise ValueError("User input is too long. Please limit to 1000 characters.")
90-
if input.chat_length != len(self.chat_history):
142+
if input.chat_length != len(self.printed_history):
91143
raise ValueError("Stale chat history. Please refresh the chat.")

0 commit comments

Comments
 (0)