The crew and the flow around it
The desk answers order questions with a crew of two agents, and decides which tickets reach it with a router.
The crew is the clerk and the writer, with the guardrail on the reply task so no email with a card number leaves it.
def answer_crew():
clerk = Agent(role="Order clerk", goal="Find the status of customers' orders",
backstory="You can look up any order.", llm=ShopLLM(model="shop"),
tools=[lookup_order])
writer = Agent(role="Reply writer", goal="Write replies to customers",
backstory="You write short emails.", llm=ShopLLM(model="shop"))
look = Task(description="Find the order in this message: {question}",
expected_output="The order's status.", agent=clerk)
reply = Task(description="Write the customer a reply.",
expected_output="A short, friendly email.", agent=writer,
guardrail=no_card_numbers)
return Crew(agents=[clerk, writer], tasks=[look, reply])The clerk has the lookup tool and answers with the order's status. The writer turns that into an email the customer can read.
class SupportDesk(Flow[Ticket]):
@start()
def read_ticket(self):
found = re.findall(r"\b[A-Z]\d+\b", self.state.message)
self.state.order_id = found[0] if found else ""
@router(read_ticket)
def route(self):
if not self.state.order_id:
return "no_order"
return "refund" if "refund" in self.state.message.lower() else "status"The flow starts by reading the ticket for an order id. The router then sends it one of three ways: a refund, a status question, or a ticket with no order in it at all, which never reaches a model.
@listen("status")
def check_status(self):
result = answer_crew().kickoff(inputs={"question": self.state.message})
self.state.reply = result.raw
@listen("no_order")
def ask_for_order(self):
self.state.reply = "Dear customer, which order is this about?"The status path runs the crew and keeps its reply. The no-order path answers without one.
@human_feedback(message="Approve this refund?", provider=ManagerInbox())
@listen("refund")
def propose_refund(self):
return f"Refund {self.state.order_id} in full"
@router(propose_refund)
def decide(self, result):
return "approved" if result.feedback.lower().startswith("yes") else "refused"A refund is proposed, then paused: @human_feedback hands it to ManagerInbox and stops the flow until someone answers. The second router reads that answer.
@listen("approved")
def pay(self):
self.state.reply = f"Dear customer, your refund for {self.state.order_id} is on its way."
@listen("refused")
def refuse(self):
self.state.reply = f"Dear customer, a manager could not approve a refund for {self.state.order_id}."Whatever the manager said, the customer gets a reply. Nothing is paid without a yes.
Pick one to watch it run, step by step.
The whole desk, with every path a ticket can take. Click through the four traces: a status question, an order that does not exist, a refund approved and a refund refused.
- Add a
"cancel"label to the router with its own handler. - Give the clerk the MCP tool in place of
lookup_order. - Move the guardrail to the clerk's task and see which text it checks.
This is what real progress feels like.