I’m experimenting with crewai and trying to build a simple ticket handling flow using the Flow API. Here’s my code:
from crewai.flow.flow import Flow, listen, router, start, or_
from dotenv import load_dotenv
from litellm import completion
import os
import json
class CustomerSupportFlow(Flow):
def __init__(self):
super().__init__()
load_dotenv()
self.llm_model = os.getenv("MODEL")
@start()
def receive_ticket(self):
# Inizializza il ticket
self.ticket_customer = "Mario Rossi"
self.ticket_issue = "Problema con l'ordine"
self.ticket_status = "Aperto"
print(f"Ticket ricevuto: {self.ticket_customer}, {self.ticket_issue}, {self.ticket_status}")
@listen(receive_ticket)
def categorize_ticket(self):
print("Inizio categorizzazione del ticket...")
self.ticket_status = "in attesa"
with open('config/prompts.json', 'r') as f:
prompts = json.load(f)
messages = [
{**p, "content": p["content"].format(issue=self.ticket_issue)}
for p in prompts["classification"]
]
response = completion(
model=self.llm_model,
messages=messages,
)
category = response["choices"][0]["message"]["content"]
self.ticket_category = category
print(f"Ticket categorizzato: {category}")
@router(categorize_ticket)
def route_ticket(self):
print("Inidirizzamento del ticket al giusto team...")
return self.ticket_category.lower().replace(" ", "_")
@listen("accesso_account")
def handle_account_access(self):
print("Ticket indirizzato al team di gestione account...")
self.ticket_status = "in lavorazione"
self.ticket_resolution = "Gestione account completata"
return "Gestione completata"
@listen("fatturazione")
def handle_billing(self):
print("Ticket indirizzato al team di gestione fatturazione...")
self.ticket_status = "in lavorazione"
self.ticket_resolution = "Gestione fatturazione completata"
return "Gestione completata"
@listen(or_("problema_tecnico", "richiesta_di_funzionalità", "altro"))
def handle_other_issues(self):
print("Non esiste un team per gestire il problema...")
self.ticket_status = "in lavorazione"
self.ticket_resolution = "Problema in fase di risoluzione"
return "Gestione completata"
@listen("Gestione completata")
def close_ticket(self):
print("Chiusura del ticket...")
self.ticket_status = "chiuso"
print(f"Ticket chiuso: {self.ticket_customer}, {self.ticket_issue}, {self.ticket_status}")
When I call plot() to visualize the flow, I get the following warnings:
Warning: No node found for 'accesso_account' or 'handle_account_access'. Skipping edge.
Warning: No node found for 'fatturazione' or 'handle_billing'. Skipping edge.
Warning: No node found for 'problema_tecnico' or 'handle_other_issues'. Skipping edge.
Warning: No node found for 'richiesta_di_funzionalità' or 'handle_other_issues'. Skipping edge.
Warning: No node found for 'altro' or 'handle_other_issues'. Skipping edge.
Warning: No node found for 'Gestione completata' or 'close_ticket'. Skipping edge.
As a result, the graph is incomplete, and the downstream nodes are not rendered.
Questions:
Am I using the @listen decorator correctly with string labels (e.g., "accesso_account")?
Does route_ticket need to emit events in a specific format for the router to resolve to the appropriate listener?
Are string labels in @listen("...") required to match something declared earlier in the flow?
Any help understanding why these nodes aren’t being recognized would be greatly appreciated!
Relying on an LLM to pick the category? That’s gonna hit you with two main problems:
plot() has absolutely no idea what string your router is going to generate, so it can’t possibly guess the potential routes.
Your LLM, no matter how good it is, can just hallucinate or go off-script and reply with something like "ho scelto la fatturazione!". Next thing you know, you’re on a one-way trip to Narnia. Seriously, at the bare minimum, you need a check like this:
if "accesso_account" in self.ticket_category.lower():
return "accesso_account"
elif "fatturazione" in self.ticket_category.lower():
return "fatturazione"
# Add more elif blocks here for other valid categories...
else:
raise ValueError(f"Invalid ticket category from LLM: '{self.ticket_category}'.")
Another key thing: routers should only return strings, not the other steps in the flow. All the other steps in the flow need to listen for the function name (meaning, they listen for when functions complete, not for the strings those functions return). So, close_ticket isn’t listening for "Gestione completata", and return "Gestione completata" just won’t cut it. You’ve got to fix that step too.
Thanks Max, your reply really helped clarify a lot of things!
Quick follow-up to make sure I’ve got it right: if I use string labels like "accesso_account" in both the @listen(...) decorators and as explicit return values from the router, everything wires up correctly and plot() can render the flow.
But I was wondering — what happens if I use an Enum instead?
For example, I’m thinking of defining something like this:
from enum import Enum
class TicketCategory(Enum):
ACCOUNT = "accesso_account"
BILLING = "fatturazione"
TECHNICAL = "problema_tecnico"
OTHER = "altro"
Then in route_ticket, I’d return TicketCategory.ACCOUNT.value, and use the same .value in the @listen(...) decorators.
Would plot() still work in this case? Or does it expect literal strings only, and enum indirection would break the static graph resolution?
So, plot() is going to look for methodS (you can have more than one) decorated with @router and use some low-level inspection with the ast (Abstract Syntax Tree) module to parse the source code. And what exactly is it looking for? It’s specifically targeting return statements. If the value being returned is an ast.Constant where the value attribute is a str, it means you’re returning a string literal (definitely keep that in mind). Then, it gathers all those unique string literals found in the return statements. For every router_name and its possible_paths, it attempts to draw an edge from the router_name node to each path_name node, got it? And a path_name node (like your "accesso_account") will only get drawn if there’s a listener method explicitly decorated with @listen("accesso_account"). Sweet.
Based on this logic, if I had to bet a dime, I’d say no, your Enum isn’t explicit enough to be picked up as a string literal by plot(). For heaven’s sake, just be crystal clear with your string routes!
So, go ahead and build your own test to see if I owe you a dime or if you owe me one. Keep in mind, I was only thinking about how plot() works, which is all a priori stuff; your Enum logic might still totally work at runtime, a posteriori.
You’re right: it actually doesn’t work.
You won the dime
Apparently, plot() really wants to see plain, raw string literals in the return statements. The Enum, as elegant and valid as it is at runtime, is just too “opaque” for its static analysis.