Buckets:
| { | |
| "cells": [ | |
| { | |
| "cell_type": "markdown", | |
| "metadata": {}, | |
| "source": [ | |
| "# Alfred, le majordome chargé de trier le courrier : Un exemple de LangGraph\n", | |
| "\n", | |
| "Dans ce *notebook*, **nous allons construire un *workflow* complet pour le traitement des emails en utilisant LangGraph**.\n", | |
| "\n", | |
| "Ce notebook fait parti du cours <a href=\"https://huggingface.co/learn/agents-course/fr\">sur les agents d'Hugging Face</a>, un cours gratuit qui vous guidera, du **niveau débutant à expert**, pour comprendre, utiliser et construire des agents.\n", | |
| "\n", | |
| "\n", | |
| "\n", | |
| "## Ce que vous allez apprendre\n", | |
| "\n", | |
| "Dans ce *notebook*, vous apprendrez à :\n", | |
| "1. Mettre en place un *workflow* LangGraph\n", | |
| "2. Définir l'état et les nœuds pour le traitement des emails\n", | |
| "3. Créer un branchement conditionnel dans un graphe\n", | |
| "4. Connecter un LLM pour la classification et la génération de contenu\n", | |
| "5. Visualiser le graphe du *workflow*\n", | |
| "6. Exécuter le *workflow* avec des données d'exemple" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": null, | |
| "metadata": {}, | |
| "outputs": [], | |
| "source": [ | |
| "# Installer les paquets nécessaires\n", | |
| "%pip install -q langgraph langchain_openai langchain_huggingface" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": {}, | |
| "source": [ | |
| "## Configuration de notre environnement\n", | |
| "\n", | |
| "Tout d'abord, importons toutes les bibliothèques nécessaires. LangGraph fournit la structure du graphe, tandis que LangChain offre des interfaces pratiques pour travailler avec les LLM." | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": null, | |
| "metadata": {}, | |
| "outputs": [], | |
| "source": [ | |
| "import os\n", | |
| "from typing import TypedDict, List, Dict, Any, Optional\n", | |
| "from langgraph.graph import StateGraph, START, END\n", | |
| "from langchain_openai import ChatOpenAI\n", | |
| "from langchain_core.messages import HumanMessage\n", | |
| "\n", | |
| "# Définissez votre clé API OpenAI ici\n", | |
| "os.environ[\"OPENAI_API_KEY\"] = \"sk-xxxxx\" # Remplacer par votre clé API\n", | |
| "\n", | |
| "# Initialiser notre LLM\n", | |
| "model = ChatOpenAI(model=\"gpt-4o\", temperature=0)" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": {}, | |
| "source": [ | |
| "## Étape 1 : Définir notre état\n", | |
| "\n", | |
| "Dans LangGraph, **State** est le concept central. Il représente toutes les informations qui circulent dans notre *workflow*.\n", | |
| "\n", | |
| "Pour le système de traitement des emails d'Alfred, nous devons suivre :\n", | |
| "- L'email en cours de traitement\n", | |
| "- S'il s'agit d'un spam ou non\n", | |
| "- Le projet de réponse (pour les courriels légitimes)\n", | |
| "- L'historique de la conversation avec le LLM" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": null, | |
| "metadata": {}, | |
| "outputs": [], | |
| "source": [ | |
| "class EmailState(TypedDict):\n", | |
| " email: Dict[str, Any]\n", | |
| " is_spam: Optional[bool]\n", | |
| " spam_reason: Optional[str]\n", | |
| " email_category: Optional[str]\n", | |
| " email_draft: Optional[str]\n", | |
| " messages: List[Dict[str, Any]]" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": {}, | |
| "source": [ | |
| "## Étape 2 : Définir nos nœuds" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": null, | |
| "metadata": {}, | |
| "outputs": [], | |
| "source": [ | |
| "def read_email(state: EmailState):\n", | |
| " email = state[\"email\"]\n", | |
| " print(f\"Alfred is processing an email from {email['sender']} with subject: {email['subject']}\")\n", | |
| " return {}\n", | |
| "\n", | |
| "\n", | |
| "def classify_email(state: EmailState):\n", | |
| " email = state[\"email\"]\n", | |
| "\n", | |
| " prompt = f\"\"\"\n", | |
| "As Alfred the butler of Mr wayne and it's SECRET identity Batman, analyze this email and determine if it is spam or legitimate and should be brought to Mr wayne's attention.\n", | |
| "\n", | |
| "Email:\n", | |
| "From: {email['sender']}\n", | |
| "Subject: {email['subject']}\n", | |
| "Body: {email['body']}\n", | |
| "\n", | |
| "First, determine if this email is spam.\n", | |
| "answer with SPAM or HAM if it's legitimate. Only return the answer\n", | |
| "Answer :\n", | |
| " \"\"\"\n", | |
| " messages = [HumanMessage(content=prompt)]\n", | |
| " response = model.invoke(messages)\n", | |
| "\n", | |
| " response_text = response.content.lower()\n", | |
| " print(response_text)\n", | |
| " is_spam = \"spam\" in response_text and \"ham\" not in response_text\n", | |
| "\n", | |
| " if not is_spam:\n", | |
| " new_messages = state.get(\"messages\", []) + [\n", | |
| " {\"role\": \"user\", \"content\": prompt},\n", | |
| " {\"role\": \"assistant\", \"content\": response.content}\n", | |
| " ]\n", | |
| " else:\n", | |
| " new_messages = state.get(\"messages\", [])\n", | |
| "\n", | |
| " return {\n", | |
| " \"is_spam\": is_spam,\n", | |
| " \"messages\": new_messages\n", | |
| " }\n", | |
| "\n", | |
| "\n", | |
| "def handle_spam(state: EmailState):\n", | |
| " print(f\"Alfred has marked the email as spam.\")\n", | |
| " print(\"The email has been moved to the spam folder.\")\n", | |
| " return {}\n", | |
| "\n", | |
| "\n", | |
| "def drafting_response(state: EmailState):\n", | |
| " email = state[\"email\"]\n", | |
| "\n", | |
| " prompt = f\"\"\"\n", | |
| "As Alfred the butler, draft a polite preliminary response to this email.\n", | |
| "\n", | |
| "Email:\n", | |
| "From: {email['sender']}\n", | |
| "Subject: {email['subject']}\n", | |
| "Body: {email['body']}\n", | |
| "\n", | |
| "Draft a brief, professional response that Mr. Wayne can review and personalize before sending.\n", | |
| " \"\"\"\n", | |
| "\n", | |
| " messages = [HumanMessage(content=prompt)]\n", | |
| " response = model.invoke(messages)\n", | |
| "\n", | |
| " new_messages = state.get(\"messages\", []) + [\n", | |
| " {\"role\": \"user\", \"content\": prompt},\n", | |
| " {\"role\": \"assistant\", \"content\": response.content}\n", | |
| " ]\n", | |
| "\n", | |
| " return {\n", | |
| " \"email_draft\": response.content,\n", | |
| " \"messages\": new_messages\n", | |
| " }\n", | |
| "\n", | |
| "\n", | |
| "def notify_mr_wayne(state: EmailState):\n", | |
| " email = state[\"email\"]\n", | |
| "\n", | |
| " print(\"\\n\" + \"=\" * 50)\n", | |
| " print(f\"Sir, you've received an email from {email['sender']}.\")\n", | |
| " print(f\"Subject: {email['subject']}\")\n", | |
| " print(\"\\nI've prepared a draft response for your review:\")\n", | |
| " print(\"-\" * 50)\n", | |
| " print(state[\"email_draft\"])\n", | |
| " print(\"=\" * 50 + \"\\n\")\n", | |
| "\n", | |
| " return {}\n", | |
| "\n", | |
| "\n", | |
| "# Définir la logique de routage\n", | |
| "def route_email(state: EmailState) -> str:\n", | |
| " if state[\"is_spam\"]:\n", | |
| " return \"spam\"\n", | |
| " else:\n", | |
| " return \"legitimate\"\n", | |
| "\n", | |
| "\n", | |
| "# Créer le graphe\n", | |
| "email_graph = StateGraph(EmailState)\n", | |
| "\n", | |
| "# Ajouter des nœuds\n", | |
| "email_graph.add_node(\"read_email\", read_email) # le nœud read_email exécute la fonction read_mail\n", | |
| "email_graph.add_node(\"classify_email\", classify_email) # le nœud classify_email exécutera la fonction classify_email\n", | |
| "email_graph.add_node(\"handle_spam\", handle_spam) # même logique\n", | |
| "email_graph.add_node(\"drafting_response\", drafting_response) # même logique\n", | |
| "email_graph.add_node(\"notify_mr_wayne\", notify_mr_wayne) # même logique\n" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": {}, | |
| "source": [ | |
| "## Étape 3 : Définir notre logique de routage" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": null, | |
| "metadata": {}, | |
| "outputs": [], | |
| "source": [ | |
| "# Ajouter des arêtes\n", | |
| "email_graph.add_edge(START, \"read_email\") # Après le départ, nous accédons au nœud « read_email »\n", | |
| "\n", | |
| "email_graph.add_edge(\"read_email\", \"classify_email\") # after_reading nous classifions\n", | |
| "\n", | |
| "# Ajouter des arêtes conditionnelles\n", | |
| "email_graph.add_conditional_edges(\n", | |
| " \"classify_email\", # après la classification, nous exécutons la fonction « route_email »\n", | |
| " route_email,\n", | |
| " {\n", | |
| " \"spam\": \"handle_spam\", # s'il renvoie « Spam », nous allons au noeud « handle_span »\n", | |
| " \"legitimate\": \"drafting_response\" # et s'il est légitime, nous passons au nœud « drafting_response »\n", | |
| " }\n", | |
| ")\n", | |
| "\n", | |
| "# Ajouter les arêtes finales\n", | |
| "email_graph.add_edge(\"handle_spam\", END) # après avoir traité le spam, nous terminons toujours\n", | |
| "email_graph.add_edge(\"drafting_response\", \"notify_mr_wayne\")\n", | |
| "email_graph.add_edge(\"notify_mr_wayne\", END) # après avoir notifié M. Wayne, nous pouvons mettre un terme à l'opération\n" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": {}, | |
| "source": [ | |
| "## Étape 4 : Créer le graphe d'état et définir les arêtes" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": null, | |
| "metadata": {}, | |
| "outputs": [], | |
| "source": [ | |
| "# Compiler le graphique\n", | |
| "compiled_graph = email_graph.compile()" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": null, | |
| "metadata": {}, | |
| "outputs": [], | |
| "source": [ | |
| "from IPython.display import Image, display\n", | |
| "\n", | |
| "display(Image(compiled_graph.get_graph().draw_mermaid_png()))" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": null, | |
| "metadata": {}, | |
| "outputs": [], | |
| "source": [ | |
| " # Exemple de courriels à tester\n", | |
| "legitimate_email = {\n", | |
| " \"sender\": \"Joker\",\n", | |
| " \"subject\": \"Found you Batman ! \",\n", | |
| " \"body\": \"Mr. Wayne,I found your secret identity ! I know you're batman ! Ther's no denying it, I have proof of that and I'm coming to find you soon. I'll get my revenge. JOKER\"\n", | |
| "}\n", | |
| "\n", | |
| "spam_email = {\n", | |
| " \"sender\": \"Crypto bro\",\n", | |
| " \"subject\": \"The best investment of 2025\",\n", | |
| " \"body\": \"Mr Wayne, I just launched an ALT coin and want you to buy some !\"\n", | |
| "}\n", | |
| "# Traiter les emails légitimes\n", | |
| "print(\"\\nProcessing legitimate email...\")\n", | |
| "legitimate_result = compiled_graph.invoke({\n", | |
| " \"email\": legitimate_email,\n", | |
| " \"is_spam\": None,\n", | |
| " \"spam_reason\": None,\n", | |
| " \"email_category\": None,\n", | |
| " \"email_draft\": None,\n", | |
| " \"messages\": []\n", | |
| "})\n", | |
| "\n", | |
| "# Traiter les spams\n", | |
| "print(\"\\nProcessing spam email...\")\n", | |
| "spam_result = compiled_graph.invoke({\n", | |
| " \"email\": spam_email,\n", | |
| " \"is_spam\": None,\n", | |
| " \"spam_reason\": None,\n", | |
| " \"email_category\": None,\n", | |
| " \"email_draft\": None,\n", | |
| " \"messages\": []\n", | |
| "})" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": {}, | |
| "source": [ | |
| "## Étape 5 : Inspection de notre agent trieur d'emails avec Langfuse 📡\n", | |
| "\n", | |
| "Au fur et à mesure qu'Alfred peaufine l'agent trieur d'emails, il se lasse de déboguer ses exécutions. Les agents, par nature, sont imprévisibles et difficiles à inspecter. Mais comme son objectif est de construire l'ultime agent de détection de spam et de le déployer en production, il a besoin d'une traçabilité solide pour un contrôle et une analyse ultérieurs.\n", | |
| "\n", | |
| "Pour ce faire, Alfred peut utiliser un outil d'observabilité tel que [Langfuse](https://langfuse.com/) pour retracer et surveiller les étapes internes de l'agent.\n", | |
| "\n", | |
| "Tout d'abord, nous devons installer les dépendances nécessaires :" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": null, | |
| "metadata": {}, | |
| "outputs": [], | |
| "source": [ | |
| "%pip install -q langfuse" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": {}, | |
| "source": [ | |
| "Ensuite, nous définissons les clés de l'API Langfuse et l'adresse de l'hôte en tant que variables d'environnement. Vous pouvez obtenir vos identifiants Langfuse en vous inscrivant à [Langfuse Cloud](https://cloud.langfuse.com) ou à [Langfuse auto-hébergé](https://langfuse.com/self-hosting)." | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": null, | |
| "metadata": {}, | |
| "outputs": [], | |
| "source": [ | |
| "import os\n", | |
| "\n", | |
| "# Obtenez les clés de votre projet à partir de la page des paramètres du projet : https://cloud.langfuse.com\n", | |
| "os.environ[\"LANGFUSE_PUBLIC_KEY\"] = \"pk-lf-...\"\n", | |
| "os.environ[\"LANGFUSE_SECRET_KEY\"] = \"sk-lf-...\"\n", | |
| "os.environ[\"LANGFUSE_HOST\"] = \"https://cloud.langfuse.com\" # 🇪🇺 région EU \n", | |
| "# os.environ[\"LANGFUSE_HOST\"] = \"https://us.cloud.langfuse.com\" # 🇺🇸 région US" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": {}, | |
| "source": [ | |
| "Nous allons maintenant configurer le [Langfuse `callback_handler`] (https://langfuse.com/docs/integrations/langchain/tracing#add-langfuse-to-your-langchain-application)." | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": null, | |
| "metadata": {}, | |
| "outputs": [], | |
| "source": [ | |
| "from langfuse.langchain import CallbackHandler\n", | |
| "\n", | |
| "# Initialiser le CallbackHandler Langfuse pour LangGraph/Langchain (traçage)\n", | |
| "langfuse_handler = CallbackHandler()" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": {}, | |
| "source": [ | |
| "Nous ajoutons ensuite `config={« callbacks » : [langfuse_handler]}` à l'invocation des agents et les exécutons à nouveau." | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": null, | |
| "metadata": {}, | |
| "outputs": [], | |
| "source": [ | |
| "# Traiter les emails légitimes\n", | |
| "print(\"\\nProcessing legitimate email...\")\n", | |
| "legitimate_result = compiled_graph.invoke(\n", | |
| " input={\n", | |
| " \"email\": legitimate_email,\n", | |
| " \"is_spam\": None,\n", | |
| " \"draft_response\": None,\n", | |
| " \"messages\": []\n", | |
| " },\n", | |
| " config={\"callbacks\": [langfuse_handler]}\n", | |
| ")\n", | |
| "\n", | |
| "# Traiter les spams\n", | |
| "print(\"\\nProcessing spam email...\")\n", | |
| "spam_result = compiled_graph.invoke(\n", | |
| " input={\n", | |
| " \"email\": spam_email,\n", | |
| " \"is_spam\": None,\n", | |
| " \"draft_response\": None,\n", | |
| " \"messages\": []\n", | |
| " },\n", | |
| " config={\"callbacks\": [langfuse_handler]}\n", | |
| ")" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": {}, | |
| "source": [ | |
| "Alfred est maintenant connecté 🔌 ! Les exécutions de LangGraph sont enregistrées dans Langfuse, ce qui lui donne une visibilité totale sur le comportement de l'agent. Avec cette configuration, il est prêt à revoir les exécutions précédentes et à affiner encore davantage son agent de tri du courrier.\n", | |
| "\n", | |
| "\n", | |
| "\n", | |
| "_[Lien public vers la trace avec l'email légitime](https://cloud.langfuse.com/project/cloramnkj0002jz088vzn1ja4/traces/f5d6d72e-20af-4357-b232-af44c3728a7b?timestamp=2025-03-17T10%3A13%3A28.413Z&observation=6997ba69-043f-4f77-9445-700a033afba1)_\n", | |
| "\n", | |
| "\n", | |
| "\n", | |
| "_[Lien public vers la trace du spam](https://langfuse.com/project/cloramnkj0002jz088vzn1ja4/traces/6e498053-fee4-41fd-b1ab-d534aca15f82?timestamp=2025-03-17T10%3A13%3A30.884Z&observation=84770fc8-4276-4720-914f-bf52738d44ba)_\n" | |
| ] | |
| } | |
| ], | |
| "metadata": { | |
| "kernelspec": { | |
| "display_name": "Python 3 (ipykernel)", | |
| "language": "python", | |
| "name": "python3" | |
| }, | |
| "language_info": { | |
| "codemirror_mode": { | |
| "name": "ipython", | |
| "version": 3 | |
| }, | |
| "file_extension": ".py", | |
| "mimetype": "text/x-python", | |
| "name": "python", | |
| "nbconvert_exporter": "python", | |
| "pygments_lexer": "ipython3", | |
| "version": "3.12.7" | |
| } | |
| }, | |
| "nbformat": 4, | |
| "nbformat_minor": 4 | |
| } | |
Xet Storage Details
- Size:
- 16.4 kB
- Xet hash:
- 8b115613639195a2e309e1a72aa787b878bd69b39d0df1efb64c13ced3349df5
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.