The desk: its state and its guardrail
The finished desk is a flow that routes each ticket to a crew, checks every reply, and holds refunds for a manager. It starts with what a ticket carries.
One file, desk.py, holds the whole desk. It opens with the imports and the state that travels from step to step.
import re
from crewai import Agent, Crew, Task
from crewai.flow import Flow, HumanFeedbackPending, HumanFeedbackProvider, human_feedback
from crewai.flow.flow import listen, router, start
from crewai.flow.persistence import SQLiteFlowPersistence
from pydantic import BaseModel
from shop_llm import ShopLLM
from tools import lookup_order
class Ticket(BaseModel):
message: str = ""
order_id: str = ""
reply: str = ""Ticket is the flow's state: the customer's message, the order id once it is found, and the reply as it is written. Every step reads and writes those three fields.
def no_card_numbers(output):
if re.search(r"\d{4} ?\d{4} ?\d{4} ?\d{4}", output.raw):
return (False, "Remove the card number. Never repeat one to a customer.")
return (True, output.raw)
class ManagerInbox(HumanFeedbackProvider):
def request_feedback(self, context, flow):
print("to the manager:", context.method_output)
raise HumanFeedbackPending(context=context)no_card_numbers is the guardrail a reply task runs through: it returns the text when it is safe, and a sentence for the agent to fix when it is not. ManagerInbox is what pauses a refund and shows it to a person.
from desk import no_card_numbers
class Output:
"""A task result has a .raw, which is what a guardrail reads."""
def __init__(self, raw):
self.raw = raw
print(no_card_numbers(Output("Your refund goes back to card 4111 1111 1111 1111.")))
print(no_card_numbers(Output("Dear customer, order A17 shipped on 3 March.")))print(no_card_numbers(Output("Your refund goes back to card 4111 1111 1111 1111.")))
print(no_card_numbers(Output("Dear customer, order A17 shipped on 3 March.")))A reply with a card number comes back refused, with the reason the agent will read. A clean reply comes back unchanged, and the task keeps it.
- Add a rule that refuses a reply containing an email address.
- Return a different message and watch what the agent is told.
- Print
Ticket().model_dump()to see the fields a new ticket starts with.
Slow is fine. Stopping is the only problem.