Blog

Jev dans CrewAI : un @router de Flow à seuil de confiance

Ce guide montre comment confier le choix de la branche d’un Flow CrewAI à Jev, le modèle de décision de TypeSafe AI. Chaque branche lance le bon agent. Quand Jev n’est pas assez sûr, votre LLM actuel décide, comme aujourd’hui.

Par Étienne Lescot · 23 septembre 2026

TicketJevconfiance ≥ seuil ? oui : bon servicenon, erreur : LLM CrewAI Flow · journal de chaque décision

Ce que vous allez construire

Un Flow qui route un ticket de support vers l’agent facturation, technique ou commercial. C’est le même scénario que notre guide n8n, cette fois dans CrewAI.

  • ask_jev, marquée @start(), envoie le ticket à Jev avec deux questions : quel service, et est-ce urgent.
  • pick_team, marquée @router, renvoie le nom de la branche selon la confiance de Jev.
  • run_team_agent écoute les trois branches et lance l’agent du service choisi.
  • send_to_human reçoit les tickets que personne n’a su classer.
  • L’état du Flow sert de journal : version du modèle, probabilités, confiance et chemin suivi.

Pourquoi un @router plutôt qu’un agent manager

Dans une crew, le routage est souvent confié à un LLM. Un agent manager lit le ticket et délègue. Chaque décision coûte alors un appel au LLM, et sa justification tient dans du texte libre.

Les Flows sont la couche d’orchestration de CrewAI. Du code Python décide de l’enchaînement, et les crews ou agents font le travail. Le @router est l’endroit naturel pour une décision typée. Jev y renvoie un service, des probabilités et une confiance. Votre code applique le seuil.

Avant de commencer

  • Python 3.10 ou plus récent.
  • Une clé API Jev, créée dans la console TypeSafe, dans la variable TYPESAFE_API_KEY. Les inscriptions à Jev sont suspendues temporairement : vérifiez votre accès.
  • La clé de votre LLM actuel. L’exemple utilise Claude Haiku 4.5 via ANTHROPIC_API_KEY. Remplacez-le par le modèle de vos agents.
pip install "crewai[anthropic]" typesafe-sdk

L’extra anthropic de CrewAI fixe sa propre version du SDK Anthropic. Installez ce projet dans un environnement virtuel dédié pour éviter les conflits.

Le code complet

Le fichier tient en moins de 80 lignes. Copiez-le tel quel, puis lisez les sections suivantes pour l’adapter.

import logging

from crewai import LLM, Agent
from crewai.flow.flow import Flow, listen, or_, router, start
from pydantic import BaseModel
from typesafe_sdk import Choice, Noul, TypeSafeClient, TypeSafeError

JEV_MODEL = "jev-1.13.0"  # pinned: a threshold is calibrated for one model version
THRESHOLD = 0.6  # starting point only, calibrate it on your own tickets
SHADOW = True  # start here: Jev is logged, your current LLM still decides
TEAMS = {"billing": "Payments, invoices, refunds", "technical": "Bugs, outages, integrations",
         "sales": "Pricing, quotes, new accounts"}
QUESTIONS = {
    "department": Choice(instructions="Which team should handle this ticket?", criteria=TEAMS),
    "urgent": Noul(instructions="Does the message express urgency?")}
PROMPT = "Which team should handle this ticket: billing, technical or sales? One word only.\n\n"

jev = TypeSafeClient(model=JEV_MODEL)  # reads TYPESAFE_API_KEY, retries 429 and 5xx itself
current_llm = LLM(model="anthropic/claude-haiku-4-5", temperature=0)  # the model you use today
log = logging.getLogger("ticket_routing")
AGENTS = {team: Agent(role=f"{team.title()} support agent", goal=f"Resolve tickets about: {scope}",
                      backstory="You answer customer tickets clearly and briefly.", llm=current_llm)
          for team, scope in TEAMS.items()}

class TicketState(BaseModel):
    ticket: str = ""
    jev_choice: str = ""
    confidence: float = 0.0  # stays 0.0 when Jev fails, which sends the ticket to the fallback
    probabilities: dict[str, float] = {}
    urgent: float | None = None
    model: str = ""
    error: str = ""
    department: str = ""
    decided_by: str = ""
    threshold: float = THRESHOLD

class TicketFlow(Flow[TicketState]):
    @start()
    def ask_jev(self):
        try:
            res = jev.system_one(state=self.state.ticket, questions=QUESTIONS)
        except TypeSafeError as err:  # 401, 422, or 429/529 once the SDK retries are spent
            self.state.error = type(err).__name__
            return
        dept = res.choices["department"]
        self.state.jev_choice, self.state.confidence = dept.choice, dept.confidence
        self.state.probabilities, self.state.model = dept.probabilities, res.model
        self.state.urgent = res.nouls["urgent"].noul

    @router(ask_jev)
    def pick_team(self):
        s = self.state
        if SHADOW or s.confidence < THRESHOLD:  # Jev unsure or down: your current LLM decides
            answer = current_llm.call(PROMPT + s.ticket)
            s.department, s.decided_by = answer.strip().lower(), "llm"
        else:
            s.department, s.decided_by = s.jev_choice, "jev"
        log.info(s.model_dump_json(exclude={"ticket"}))
        return s.department if s.department in TEAMS else "human"  # an unknown label runs nothing

    @listen(or_("billing", "technical", "sales"))
    def run_team_agent(self):
        return AGENTS[self.state.department].kickoff(self.state.ticket).raw

    @listen("human")
    def send_to_human(self):
        return "Sent to the human triage queue."

if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)
    text = "Hi, my September invoice shows up twice on my statement. Can you refund the duplicate?"
    print(TicketFlow().kickoff(inputs={"ticket": text}))

1. L’étape de départ qui appelle Jev

Le client épingle la version du modèle. TypeSafeClient(model="jev-1.13.0") fixe le modèle pour tous les appels. L’alias jev-latest change à chaque nouvelle version, et vos seuils avec lui.

Jev reçoit deux questions typées. Une choice choisit le service parmi trois options décrites. Une noul renvoie la probabilité que le message soit urgent. L’étape range les réponses dans l’état du Flow, un modèle Pydantic.

Le SDK retente déjà deux fois les codes 429 et 5xx, dont le 529 de surcharge. Il respecte l’en-tête Retry-After. Si l’erreur persiste, l’étape note son nom et s’arrête. La confiance reste alors à 0, ce qui envoie le ticket vers le repli.

2. Le @router qui choisit la branche

La décision tient dans pick_team. Le décorateur @router(ask_jev) la lance dès que l’étape Jev se termine. La méthode renvoie une chaîne, et cette chaîne déclenche les écouteurs qui portent le même nom.

  • Confiance au-dessus du seuil : la branche est le choix de Jev.
  • Confiance en dessous, erreur de Jev ou mode shadow : votre LLM actuel choisit, via LLM.call.
  • Réponse du LLM hors liste : la branche devient human.

Ce dernier garde-fou compte. Nous l’avons vérifié avec CrewAI 1.15.22 : une étiquette qu’aucun écouteur n’attend ne lance rien. Le Flow s’arrête sans erreur, et kickoff() renvoie simplement l’étiquette. Un ticket peut ainsi disparaître en silence.

Le seuil de départ est 0,6, la valeur prise dans les exemples de TypeSafe. Ce n’est pas une valeur de production. La section « Choisir le bon seuil » explique comment la mesurer.

3. Les branches qui lancent les agents

run_team_agent écoute les trois étiquettes grâce à or_. Il lance l’agent du service choisi avec Agent.kickoff, qui renvoie un résultat dont le texte est dans .raw.

Si chaque service a sa propre crew, écrivez un écouteur par étiquette. Chacun appelle sa crew avec kickoff(inputs=...). Donnez à ces méthodes un nom différent de l’étiquette. CrewAI 1.15 refuse un écouteur nommé billing qui écoute "billing", car il se déclencherait lui-même en boucle.

4. Commencer en mode shadow

Laissez SHADOW = True pendant les premières semaines. Jev répond sur chaque ticket et sa réponse est journalisée. Mais c’est toujours votre LLM qui choisit la branche. Vos clients ne voient aucune différence.

Le journal vous donne, pour chaque ticket, le choix de Jev et celui de votre LLM. Mesurez leur accord avant de confier la moindre décision à Jev. Passez ensuite SHADOW à False pour activer le seuil.

5. Le journal de décision

Le router écrit l’état du Flow en JSON, sans le texte du ticket. Voici la ligne d’un ticket de facturation traité par Jev, avec les valeurs d’exemple de la documentation :

{"jev_choice":"billing","confidence":0.81,"probabilities":{"billing":0.88,"technical":0.12,"sales":0.0},"urgent":0.12,"model":"jev-1.13.0","error":"","department":"billing","decided_by":"jev","threshold":0.6,"id":"66faddc1-c1a6-47f6-a45b-61601be72b85"}
  • model : la version exacte qui a répondu, renvoyée par l’API.
  • probabilities et confidence : ce qui permet de recalibrer le seuil.
  • decided_by : le chemin suivi, jev ou llm.
  • id : l’identifiant du Flow, ajouté par CrewAI. Il relie la décision au reste de l’exécution.

Comment nous avons testé ce code

Nous avons exécuté ce fichier tel quel, avec crewai 1.15.22 et typesafe-sdk 0.7.1. Le vrai Flow CrewAI et le vrai SDK Jev tournaient. Les appels HTTP de Jev partaient vers une fausse API, qui renvoyait les réponses documentées par TypeSafe. Les agents et le LLM étaient remplacés par des doublures.

  • Confiance de 0,81 : Jev choisit facturation, le LLM n’est pas appelé.
  • Confiance de 0,10 : le LLM choisit, la branche technique tourne.
  • Erreur 529 répétée : le LLM choisit, l’erreur reste dans le journal.
  • Réponse du LLM hors liste : le ticket part vers human.
  • Mode shadow : le LLM décide, le choix de Jev reste dans le journal.

Nous avons aussi exécuté la partie Jev de ce code contre la vraie API le 23/09/2026 : jev-1.13.0 a répondu billing, confiance 1, en 403 ms depuis la France, pour 368 tokens d’entrée.

Choisir le bon seuil

Un seuil ne se devine pas. Il se mesure sur vos données, dans votre langue.

  1. Prenez quelques centaines de tickets passés dont le bon service est connu.
  2. Faites-les passer dans le Flow en mode shadow.
  3. Pour chaque seuil entre 0,5 et 0,95, calculez la part de tickets que Jev traiterait seul, et son taux d’erreur sur cette part.
  4. Retenez le seuil le plus bas dont le taux d’erreur vous convient.

Jev est d’abord entraîné en anglais. TypeSafe recommande de tester sur vos propres contenus avant de l’utiliser dans une autre langue. Le guide n8n détaille la même méthode.

Avant la production

  • Données : Jev est hébergé aux États-Unis. Pseudonymisez les données personnelles avant l’envoi, et faites valider le transfert par votre DPO.
  • Version : gardez jev-1.13.0 épinglé. Recalibrez avant de passer à la suivante.
  • Journal : conservez chaque décision, y compris celles du LLM.
  • Coûts : Jev remplace la décision de routage, pas le travail des agents. Les agents restent facturés au prix de leur LLM.
  • Décisions sensibles : une confiance élevée ne vaut pas autorisation. Toute issue défavorable à une personne doit passer par un humain.

Vous voulez voir Jev décider avant d’écrire du code ? Essayez la démo avec vos propres exemples de tickets.

Jev et TypeSafe sont des marques de TypeSafe AI, Inc. Cet article est indépendant et n’est pas affilié à TypeSafe AI.