Qu'est-ce qu'un Agent IA ?

Objectifs d'apprentissage
  • Comprendre la définition et les caractéristiques d'un agent IA
  • Maîtriser la boucle agentique (perception-raisonnement-action)
  • Identifier les différences entre chatbot et agent autonome
  • Construire votre premier agent conceptuel en Python

1. Définition d'un Agent IA

Un agent IA est un système qui perçoit son environnement, prend des décisions basées sur ses perceptions et ses objectifs, puis agit de manière autonome pour atteindre ces objectifs.

┌─────────────────────────────────────────────────────────────┐
│                    ANATOMIE D'UN AGENT IA                   │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│     ┌──────────────┐         ┌──────────────┐             │
│     │ ENVIRONNEMENT│◄────────┤   ACTIONS    │             │
│     │   (Monde)    │         │  (Effecteurs)│             │
│     └──────┬───────┘         └──────▲───────┘             │
│            │                        │                      │
│            │ Perceptions            │ Commandes            │
│            │                        │                      │
│     ┌──────▼───────┐         ┌─────┴────────┐             │
│     │   CAPTEURS   │         │  RAISONNEMENT│             │
│     │  (Perceive)  ├────────►│   (Decide)   │             │
│     └──────────────┘         └──────────────┘             │
│                                     │                      │
│                              ┌──────▼───────┐             │
│                              │   MÉMOIRE    │             │
│                              │  (Context)   │             │
│                              └──────────────┘             │
│                                                             │
│  BOUCLE AGENTIQUE: Perceive → Decide → Act → Repeat       │
└─────────────────────────────────────────────────────────────┘
                

2. Chatbot vs Agent IA

Caractéristique Chatbot Classique Agent IA Autonome
Mode d'interaction Réactif (répond uniquement) Proactif (initie des actions)
Autonomie Faible (attend des prompts) Élevée (boucle autonome)
Capacités Génération de texte Utilisation d'outils, API, navigation
Planification Aucune Multi-étapes, sous-objectifs
Mémoire Contexte de conversation Court/long terme, épisodique
Correction d'erreurs Non Oui (réflexion, retry)
Astuce

Un agent IA n'est pas juste un LLM avec des outils. C'est un système complet avec une boucle de contrôle, une mémoire, et la capacité de poursuivre des objectifs complexes de manière autonome.

3. Composants Essentiels d'un Agent

3.1 Perception (Sensors)

L'agent doit pouvoir observer son environnement :

3.2 Raisonnement (Brain)

Le cœur décisionnel de l'agent :

3.3 Action (Effectors)

L'agent peut agir sur le monde :

3.4 Mémoire (Memory)

Persistance de l'information :

┌────────────────────────────────────────────────────────────────┐
│              BOUCLE AGENTIQUE DÉTAILLÉE                        │
├────────────────────────────────────────────────────────────────┤
│                                                                │
│  1. OBSERVATION                                                │
│     └─► Lire l'environnement (user input, state, context)     │
│                                                                │
│  2. RÉFLEXION                                                  │
│     └─► LLM analyse la situation                              │
│         "Quel est l'objectif ? Que sais-je déjà ?"            │
│                                                                │
│  3. PLANIFICATION                                              │
│     └─► Décomposer en sous-tâches                             │
│         "Étape 1: rechercher info, Étape 2: calculer..."      │
│                                                                │
│  4. SÉLECTION D'ACTION                                         │
│     └─► Choisir l'outil ou la réponse                         │
│         "J'utilise l'outil 'web_search'"                      │
│                                                                │
│  5. EXÉCUTION                                                  │
│     └─► Appeler l'outil, obtenir le résultat                  │
│         Result: {...}                                          │
│                                                                │
│  6. ÉVALUATION                                                 │
│     └─► Est-ce que ça a fonctionné ?                          │
│         ├─► Succès → Continuer                                │
│         └─► Échec → Corriger et réessayer                     │
│                                                                │
│  7. MÉMORISATION                                               │
│     └─► Stocker l'expérience pour apprentissage               │
│                                                                │
│  8. DÉCISION                                                   │
│     └─► Objectif atteint ? → STOP                             │
│         Sinon → Retour à l'étape 1                            │
│                                                                │
└────────────────────────────────────────────────────────────────┘
                

4. Premier Agent Conceptuel en Python

Créons un agent minimaliste qui illustre la boucle agentique :

import openai
import json
from typing import Dict, List, Any

class SimpleAgent:
    """Agent IA minimaliste avec boucle perception-raisonnement-action"""

    def __init__(self, model: str = "gpt-4"):
        self.model = model
        self.memory = []  # Historique des interactions
        self.tools = {
            "calculator": self.calculator,
            "web_search": self.web_search,
            "final_answer": self.final_answer
        }
        self.max_iterations = 10

    def calculator(self, expression: str) -> Dict:
        """Outil: calculatrice simple"""
        try:
            result = eval(expression)
            return {"result": result, "success": True}
        except Exception as e:
            return {"error": str(e), "success": False}

    def web_search(self, query: str) -> Dict:
        """Outil: simulation de recherche web"""
        mock_results = f"Résultats pour '{query}': Information pertinente..."
        return {"results": mock_results, "success": True}

    def final_answer(self, answer: str) -> Dict:
        """Outil: fourni la réponse finale"""
        return {"answer": answer, "success": True, "final": True}

    def perceive(self, user_input: str) -> str:
        """1. PERCEPTION: Capturer l'input utilisateur"""
        observation = f"User: {user_input}"
        self.memory.append({"role": "user", "content": user_input})
        return observation

    def reason(self) -> Dict[str, Any]:
        """2. RAISONNEMENT: Le LLM décide de l'action"""

        system_prompt = """Tu es un agent IA autonome.

Outils disponibles:
- calculator(expression): calcule une expression mathématique
- web_search(query): recherche sur le web
- final_answer(answer): fourni la réponse finale

Réponds au format JSON:
{
    "thought": "mon raisonnement",
    "tool": "nom_outil",
    "tool_input": "paramètre",
    "final": false
}

Quand tu as la réponse finale, utilise final_answer et met final=true."""

        messages = [{"role": "system", "content": system_prompt}] + self.memory

        response = openai.ChatCompletion.create(
            model=self.model,
            messages=messages,
            temperature=0
        )

        content = response.choices[0].message.content
        try:
            decision = json.loads(content)
        except:
            decision = {
                "thought": content,
                "tool": "final_answer",
                "tool_input": content,
                "final": True
            }

        return decision

    def act(self, decision: Dict) -> Dict:
        """3. ACTION: Exécuter l'outil choisi"""

        tool_name = decision.get("tool")
        tool_input = decision.get("tool_input")

        if tool_name not in self.tools:
            return {"error": f"Outil inconnu: {tool_name}", "success": False}

        tool_result = self.tools[tool_name](tool_input)

        self.memory.append({
            "role": "assistant",
            "content": json.dumps(decision)
        })
        self.memory.append({
            "role": "system",
            "content": f"Tool result: {json.dumps(tool_result)}"
        })

        return tool_result

    def run(self, task: str) -> str:
        """Boucle agentique complète"""

        print(f"\n🤖 Agent démarré avec la tâche: {task}\n")

        self.perceive(task)

        for iteration in range(self.max_iterations):
            print(f"--- Itération {iteration + 1} ---")

            decision = self.reason()
            print(f"💭 Pensée: {decision.get('thought')}")
            print(f"🔧 Outil: {decision.get('tool')}({decision.get('tool_input')})")

            result = self.act(decision)
            print(f"📊 Résultat: {result}\n")

            if decision.get("final") or result.get("final"):
                print("✅ Tâche terminée!")
                return result.get("answer", str(result))

        print("⚠️ Limite d'itérations atteinte")
        return "Tâche non terminée"


# UTILISATION
if __name__ == "__main__":
    agent = SimpleAgent()
    result = agent.run("Calcule (15 * 8) + 42")
    print(f"\nRéponse finale: {result}")
                
Sécurité

L'utilisation d'eval() pour la calculatrice est dangereuse en production. Utilisez une bibliothèque comme numexpr ou ast.literal_eval() pour évaluer des expressions en toute sécurité.

5. Types d'Agents IA

Type d'Agent Description Cas d'usage
Agent Réflexe Simple Condition → Action directe Chatbot FAQ, Autoréponses
Agent Basé sur Modèle Maintient un modèle du monde Assistant personnel, Recommandations
Agent Orienté Objectif Planifie pour atteindre un but Résolution de problèmes, Recherche
Agent Basé sur l'Utilité Optimise une fonction d'utilité Trading, Optimisation de ressources
Agent Apprenant S'améliore avec l'expérience Jeux, Personnalisation

6. Caractéristiques Clés d'un Bon Agent

Les 7 Propriétés d'un Agent Efficace
  1. Autonomie : Opère sans intervention constante
  2. Réactivité : Répond aux changements de l'environnement
  3. Proactivité : Prend des initiatives pour atteindre ses objectifs
  4. Sociabilité : Interagit avec d'autres agents et humains
  5. Robustesse : Gère les erreurs et situations imprévues
  6. Adaptabilité : Apprend et s'améliore avec l'expérience
  7. Transparence : Explique ses raisonnements et actions
Conseil du Mentor

Commencez toujours par un agent simple et ajoutez de la complexité progressivement. Un agent mal conçu peut entrer dans des boucles infinies ou faire des appels coûteux inutiles. Le logging est votre meilleur ami pour comprendre ce qui se passe.

Patterns Agentiques

Objectifs d'apprentissage
  • Maîtriser le pattern ReAct (Reasoning + Acting)
  • Comprendre Plan-and-Execute pour les tâches complexes
  • Implémenter Reflexion pour l'auto-correction
  • Découvrir LATS (Language Agent Tree Search)

1. ReAct: Reasoning + Acting

ReAct est le pattern agentique le plus populaire, introduit dans un paper de 2022. Il combine raisonnement explicite (Reasoning) et actions (Acting) en une boucle itérative où l'agent verbalise sa pensée à chaque étape.

┌────────────────────────────────────────────────────────────────┐
│                    PATTERN REACT                               │
├────────────────────────────────────────────────────────────────┤
│                                                                │
│  User: "Quelle est la météo à Paris aujourd'hui ?"            │
│                                                                │
│  ┌─────────────────────────────────────────────────────────┐  │
│  │ Iteration 1                                             │  │
│  │                                                         │  │
│  │ Thought: "Je dois chercher la météo actuelle à Paris"  │  │
│  │ Action: web_search("météo Paris aujourd'hui")          │  │
│  │ Observation: "Paris: 15°C, nuageux, vent 10km/h"       │  │
│  └─────────────────────────────────────────────────────────┘  │
│                                                                │
│  ┌─────────────────────────────────────────────────────────┐  │
│  │ Iteration 2                                             │  │
│  │                                                         │  │
│  │ Thought: "J'ai l'info nécessaire, je peux répondre"    │  │
│  │ Action: final_answer("Il fait 15°C à Paris...")        │  │
│  │ Observation: DONE                                       │  │
│  └─────────────────────────────────────────────────────────┘  │
│                                                                │
│  Format: Thought → Action → Observation → Repeat              │
└────────────────────────────────────────────────────────────────┘
                

1.1 Anatomie d'une Itération ReAct

Chaque itération comporte trois éléments obligatoires :

1.2 Implémentation ReAct Complète

import openai
import re
from typing import Dict, Optional, List

class ReActAgent:
    """Agent utilisant le pattern ReAct (Reasoning + Acting)"""

    def __init__(self, model: str = "gpt-4", api_key: str = None):
        self.model = model
        self.api_key = api_key or os.getenv("OPENAI_API_KEY")
        openai.api_key = self.api_key
        self.tools = self.register_tools()
        self.max_iterations = 10

    def register_tools(self) -> Dict:
        """Enregistrer les outils disponibles"""
        return {
            "web_search": {
                "description": "Search the web for current information",
                "function": self.web_search
            },
            "calculator": {
                "description": "Calculate mathematical expressions",
                "function": self.calculator
            },
            "python_repl": {
                "description": "Execute Python code and return result",
                "function": self.python_repl
            },
            "database_query": {
                "description": "Query a database with SQL",
                "function": self.database_query
            }
        }

    def web_search(self, query: str) -> str:
        """Simuler une recherche web (utiliser Tavily/SerpAPI en prod)"""
        # En production: import tavily; client = tavily.TavilyClient(api_key=...)
        # results = client.search(query=query, max_results=3)
        mock_results = f"[Mock Search] Top 3 results for '{query}':\n"
        mock_results += "1. Relevant information about the topic\n"
        mock_results += "2. Additional context and data\n"
        mock_results += "3. Related findings"
        return mock_results

    def calculator(self, expression: str) -> str:
        """Calculatrice sécurisée"""
        try:
            # En production: utiliser numexpr pour la sécurité
            # import numexpr as ne
            # result = ne.evaluate(expression)
            result = eval(expression)  # ⚠️ Dangereux en prod
            return str(result)
        except Exception as e:
            return f"Error: {str(e)}"

    def python_repl(self, code: str) -> str:
        """Exécuter du code Python (nécessite sandbox en prod!)"""
        try:
            # ⚠️ TRÈS DANGEREUX sans sandbox
            # En production: utiliser docker/E2B/modal.com
            exec_globals = {"__builtins__": __builtins__}
            exec_locals = {}
            exec(code, exec_globals, exec_locals)

            # Chercher une variable 'result' ou la dernière valeur
            if 'result' in exec_locals:
                return str(exec_locals['result'])
            elif exec_locals:
                return str(list(exec_locals.values())[-1])
            else:
                return "Code executed successfully (no output)"
        except Exception as e:
            return f"Error: {str(e)}"

    def database_query(self, sql: str) -> str:
        """Simuler une requête SQL"""
        # En production: connexion réelle à la DB
        return f"[Mock DB] Executed: {sql}\nReturned 42 rows"

    def create_prompt(self, query: str, scratchpad: str) -> str:
        """Créer le prompt ReAct avec l'historique"""

        tools_desc = "\n".join([
            f"- {name}: {info['description']}"
            for name, info in self.tools.items()
        ])

        prompt = f"""You are a ReAct (Reasoning + Acting) agent. Answer the user's question by reasoning step by step and using tools when needed.

Available tools:
{tools_desc}

You MUST use this EXACT format for each step:

Thought: [your reasoning about what to do next]
Action: [tool_name]
Action Input: [input for the tool]

After calling a tool, you will receive:
Observation: [result from the tool]

Then continue with another Thought/Action/Observation cycle, or provide the final answer:

Thought: I now know the final answer
Final Answer: [your complete answer to the user]

IMPORTANT RULES:
1. Always start with a Thought
2. Only call ONE tool per iteration
3. Wait for the Observation before the next Thought
4. When you have enough information, provide Final Answer

Question: {query}

{scratchpad}"""

        return prompt

    def parse_action(self, text: str) -> Optional[tuple]:
        """Parser la réponse du LLM pour extraire l'action"""

        # Chercher le pattern Thought/Action/Action Input
        thought_match = re.search(r'Thought:\s*(.+?)(?:\n|$)', text, re.DOTALL)
        action_match = re.search(r'Action:\s*(\w+)', text)
        input_match = re.search(r'Action Input:\s*(.+?)(?:\n|$)', text, re.DOTALL)

        if action_match and input_match:
            action = action_match.group(1).strip()
            action_input = input_match.group(1).strip()
            thought = thought_match.group(1).strip() if thought_match else "No thought provided"
            return (action, action_input, thought)

        # Vérifier si c'est une réponse finale
        if "Final Answer:" in text:
            final_answer = text.split("Final Answer:")[1].strip()
            thought = thought_match.group(1).strip() if thought_match else "Ready to answer"
            return ("final_answer", final_answer, thought)

        return None

    def run(self, query: str, verbose: bool = True) -> str:
        """Exécuter la boucle ReAct complète"""

        scratchpad = ""

        if verbose:
            print(f"\n{'='*70}")
            print(f"🤖 ReAct Agent Starting")
            print(f"{'='*70}")
            print(f"Query: {query}\n")

        for iteration in range(self.max_iterations):
            if verbose:
                print(f"\n{'─'*70}")
                print(f"Iteration {iteration + 1}/{self.max_iterations}")
                print(f"{'─'*70}\n")

            # Créer le prompt avec l'historique
            prompt = self.create_prompt(query, scratchpad)

            # Appeler le LLM
            try:
                response = openai.ChatCompletion.create(
                    model=self.model,
                    messages=[{"role": "user", "content": prompt}],
                    temperature=0,
                    max_tokens=500
                )
                llm_output = response.choices[0].message.content
            except Exception as e:
                return f"Error calling LLM: {str(e)}"

            if verbose:
                print(f"🤖 LLM Output:\n{llm_output}\n")

            # Parser l'action
            parsed = self.parse_action(llm_output)

            if parsed is None:
                if verbose:
                    print("⚠️ Could not parse action. Retrying with guidance...")
                scratchpad += f"\n{llm_output}\n"
                scratchpad += "\n[System: Invalid format. Please follow the exact format: Thought/Action/Action Input]\n"
                continue

            action, action_input, thought = parsed

            if verbose:
                print(f"💭 Thought: {thought}")
                print(f"🔧 Action: {action}")
                print(f"📝 Input: {action_input}\n")

            # Vérifier si c'est la réponse finale
            if action == "final_answer":
                if verbose:
                    print(f"✅ Final Answer:\n{action_input}")
                    print(f"\n{'='*70}")
                    print(f"Agent completed in {iteration + 1} iterations")
                    print(f"{'='*70}\n")
                return action_input

            # Exécuter l'outil
            if action not in self.tools:
                observation = f"❌ Error: Unknown tool '{action}'. Available tools: {', '.join(self.tools.keys())}"
            else:
                try:
                    tool_func = self.tools[action]["function"]
                    observation = tool_func(action_input)
                except Exception as e:
                    observation = f"❌ Error executing {action}: {str(e)}"

            if verbose:
                print(f"📊 Observation: {observation}\n")

            # Ajouter au scratchpad pour la prochaine itération
            scratchpad += f"\n{llm_output}\nObservation: {observation}\n"

        # Si on atteint la limite d'itérations
        if verbose:
            print(f"⚠️ Reached maximum iterations ({self.max_iterations})")
        return f"Task incomplete after {self.max_iterations} iterations"


# EXEMPLES D'UTILISATION
if __name__ == "__main__":
    agent = ReActAgent()

    # Test 1: Calcul mathématique
    print("\n" + "="*70)
    print("TEST 1: Mathematical Reasoning")
    print("="*70)
    result1 = agent.run("What is 25% of 80, then multiply the result by 3?")

    # Test 2: Recherche + raisonnement
    print("\n" + "="*70)
    print("TEST 2: Information Retrieval")
    print("="*70)
    result2 = agent.run("Find the capital of Japan and tell me its approximate population")

    # Test 3: Tâche complexe multi-outils
    print("\n" + "="*70)
    print("TEST 3: Multi-tool Task")
    print("="*70)
    result3 = agent.run(
        "Search for the current price of Bitcoin, "
        "then calculate how many Bitcoin I can buy with $10,000"
    )
                
Avantages de ReAct
  • Transparence : Chaque étape de raisonnement est explicite et visible
  • Debuggable : Facile d'identifier où l'agent fait une erreur
  • Flexible : S'adapte à des tâches très variées
  • Prouvé : Paper académique de 2022, largement utilisé en production
  • Performant : Améliore significativement les résultats vs prompting classique

2. Plan-and-Execute

Pour les tâches complexes nécessitant plusieurs étapes, le pattern Plan-and-Execute est plus efficace. L'idée : planifier d'abord toutes les étapes, puis les exécuter séquentiellement.

┌────────────────────────────────────────────────────────────────┐
│                 PATTERN PLAN-AND-EXECUTE                       │
├────────────────────────────────────────────────────────────────┤
│                                                                │
│  User: "Analyse le marché crypto et recommande un achat"      │
│                                                                │
│  ┌──────────────────────────────────────────────────────────┐ │
│  │ PHASE 1: PLANNING (Planner Agent)                       │ │
│  │ ────────────────────────────────                         │ │
│  │                                                          │ │
│  │ Step 1: Rechercher prix actuels BTC, ETH, SOL           │ │
│  │ Step 2: Analyser tendances des 30 derniers jours        │ │
│  │ Step 3: Comparer la volatilité de chaque crypto         │ │
│  │ Step 4: Lire les news récentes sur chaque crypto        │ │
│  │ Step 5: Synthétiser et recommander un achat             │ │
│  └──────────────────────────────────────────────────────────┘ │
│                                                                │
│  ┌──────────────────────────────────────────────────────────┐ │
│  │ PHASE 2: EXECUTION (Executor Agent)                     │ │
│  │ ───────────────────────────────                          │ │
│  │                                                          │ │
│  │ [Execute Step 1] → Result 1 ✓                           │ │
│  │ [Execute Step 2] → Result 2 ✓                           │ │
│  │ [Execute Step 3] → Result 3 ✓                           │ │
│  │ [Execute Step 4] → Result 4 ✓                           │ │
│  │ [Execute Step 5] → Result 5 ✓                           │ │
│  └──────────────────────────────────────────────────────────┘ │
│                                                                │
│  ┌──────────────────────────────────────────────────────────┐ │
│  │ PHASE 3: REPLANNING (Optional, si échec)                │ │
│  │ ────────────────────────────────────                     │ │
│  │                                                          │ │
│  │ Évaluer les résultats, ajuster le plan si nécessaire    │ │
│  └──────────────────────────────────────────────────────────┘ │
│                                                                │
└────────────────────────────────────────────────────────────────┘
                

2.1 Pourquoi Plan-and-Execute ?

Aspect ReAct Plan-and-Execute
Planification Pas de plan global Plan complet dès le début
Vision Myope (étape par étape) Vision d'ensemble
Optimisation Difficile d'optimiser Peut paralléliser/optimiser
Coût Variable Plus prévisible
Meilleur pour Tâches simples, exploratoires Tâches complexes, structurées

2.2 Implémentation Plan-and-Execute

from typing import List, Dict
import openai
import json

class PlanAndExecuteAgent:
    """Agent utilisant le pattern Plan-and-Execute"""

    def __init__(self, model: str = "gpt-4"):
        self.model = model
        self.tools = {
            "web_search": self.web_search,
            "calculator": self.calculator,
            "data_analysis": self.data_analysis,
            "database_query": self.database_query
        }

    def web_search(self, query: str) -> str:
        return f"[Search results for: {query}]\nRelevant information found..."

    def calculator(self, expr: str) -> str:
        try:
            return str(eval(expr))
        except:
            return "Error in calculation"

    def data_analysis(self, data: str) -> str:
        return f"[Analysis completed for: {data}]\nKey insights: ..."

    def database_query(self, sql: str) -> str:
        return f"[Query executed: {sql}]\n42 rows returned"

    def create_plan(self, objective: str) -> List[Dict]:
        """Phase 1: Créer un plan d'action détaillé"""

        tools_list = ", ".join(self.tools.keys())

        prompt = f"""You are a planning agent. Break down this complex objective into concrete, sequential steps.

Objective: {objective}

Available tools: {tools_list}

For each step, specify:
1. A clear action description
2. Which tool to use
3. What input to provide

Output format (JSON array):
[
  {{"step": 1, "description": "...", "tool": "...", "input": "..."}},
  {{"step": 2, "description": "...", "tool": "...", "input": "..."}},
  ...
]

Plan:"""

        response = openai.ChatCompletion.create(
            model=self.model,
            messages=[{"role": "user", "content": prompt}],
            temperature=0
        )

        plan_text = response.choices[0].message.content

        # Tenter de parser le JSON
        try:
            # Extraire le JSON s'il est entre ```json et ```
            if "```json" in plan_text:
                plan_text = plan_text.split("```json")[1].split("```")[0]
            elif "```" in plan_text:
                plan_text = plan_text.split("```")[1].split("```")[0]

            plan = json.loads(plan_text.strip())
            return plan
        except json.JSONDecodeError:
            # Fallback: parser manuellement
            steps = []
            for i, line in enumerate(plan_text.split('\n'), 1):
                line = line.strip()
                if line and (line[0].isdigit() or line.startswith('-')):
                    step_text = line.split('.', 1)[-1].strip()
                    step_text = line.split('-', 1)[-1].strip()
                    if step_text:
                        steps.append({
                            "step": i,
                            "description": step_text,
                            "tool": "web_search",  # Default
                            "input": step_text
                        })
            return steps

    def execute_step(self, step: Dict, context: str) -> Dict:
        """Phase 2: Exécuter une étape du plan"""

        step_num = step.get("step", "?")
        description = step.get("description", "")
        tool_name = step.get("tool", "web_search")
        tool_input = step.get("input", description)

        print(f"\n[Step {step_num}] {description}")
        print(f"  Tool: {tool_name}")
        print(f"  Input: {tool_input[:100]}...")

        # Exécuter l'outil
        if tool_name in self.tools:
            try:
                result = self.tools[tool_name](tool_input)
                success = True
            except Exception as e:
                result = f"Error: {str(e)}"
                success = False
        else:
            result = f"Unknown tool: {tool_name}"
            success = False

        print(f"  Result: {result[:150]}...")

        return {
            "step": step_num,
            "description": description,
            "tool": tool_name,
            "input": tool_input,
            "result": result,
            "success": success
        }

    def should_replan(self, results: List[Dict]) -> bool:
        """Décider si on doit replanner (si trop d'échecs)"""
        failures = sum(1 for r in results if not r.get("success", True))
        failure_rate = failures / len(results) if results else 0
        return failure_rate > 0.3  # Si plus de 30% d'échecs

    def replan(self, objective: str, previous_plan: List[Dict], results: List[Dict]) -> List[Dict]:
        """Créer un nouveau plan basé sur les résultats précédents"""

        results_summary = "\n".join([
            f"Step {r['step']}: {'✓' if r.get('success') else '✗'} {r['description']}"
            for r in results
        ])

        prompt = f"""The previous plan had some failures. Create an improved plan.

Objective: {objective}

Previous attempt:
{results_summary}

Create a NEW, BETTER plan (JSON format):"""

        response = openai.ChatCompletion.create(
            model=self.model,
            messages=[{"role": "user", "content": prompt}],
            temperature=0.3
        )

        # Parser le nouveau plan
        return self.create_plan(objective)

    def synthesize(self, objective: str, results: List[Dict]) -> str:
        """Phase 3: Synthétiser tous les résultats"""

        results_text = "\n".join([
            f"Step {r['step']} ({r['tool']}): {r['result'][:200]}"
            for r in results
        ])

        prompt = f"""Synthesize these execution results to answer the objective.

Objective: {objective}

Execution Results:
{results_text}

Provide a comprehensive final answer:"""

        response = openai.ChatCompletion.create(
            model=self.model,
            messages=[{"role": "user", "content": prompt}],
            temperature=0.3
        )

        return response.choices[0].message.content

    def run(self, objective: str, allow_replanning: bool = True) -> str:
        """Exécuter le cycle complet Plan-and-Execute"""

        print(f"\n{'='*70}")
        print(f"PLAN-AND-EXECUTE AGENT")
        print(f"{'='*70}")
        print(f"Objective: {objective}\n")

        # PHASE 1: PLANNING
        print(f"{'─'*70}")
        print("📋 PHASE 1: PLANNING")
        print(f"{'─'*70}\n")

        plan = self.create_plan(objective)

        print(f"Plan created with {len(plan)} steps:")
        for step in plan:
            print(f"  {step['step']}. {step['description']}")

        # PHASE 2: EXECUTION
        print(f"\n{'─'*70}")
        print("⚙️  PHASE 2: EXECUTION")
        print(f"{'─'*70}")

        context = ""
        results = []

        for step in plan:
            result = self.execute_step(step, context)
            results.append(result)
            context += f"\nStep {step['step']}: {result['result'][:100]}"

        # Vérifier si replanning nécessaire
        if allow_replanning and self.should_replan(results):
            print(f"\n⚠️  Too many failures. REPLANNING...\n")

            new_plan = self.replan(objective, plan, results)

            print(f"New plan created with {len(new_plan)} steps")

            # Ré-exécuter avec le nouveau plan
            results = []
            for step in new_plan:
                result = self.execute_step(step, context)
                results.append(result)

        # PHASE 3: SYNTHESIS
        print(f"\n{'─'*70}")
        print("📊 PHASE 3: SYNTHESIS")
        print(f"{'─'*70}\n")

        final_answer = self.synthesize(objective, results)

        print(f"Final Answer:\n{final_answer}")

        print(f"\n{'='*70}")
        print(f"Execution Summary:")
        print(f"  Total steps: {len(results)}")
        print(f"  Successful: {sum(1 for r in results if r.get('success'))}")
        print(f"  Failed: {sum(1 for r in results if not r.get('success'))}")
        print(f"{'='*70}\n")

        return final_answer


# TESTS
if __name__ == "__main__":
    agent = PlanAndExecuteAgent()

    # Test complexe multi-étapes
    result = agent.run(
        "Research the top 3 AI startups in France founded after 2020, "
        "compare their funding amounts, and recommend which one has "
        "the best growth potential based on market trends"
    )
                
Quand utiliser Plan-and-Execute ?
  • Tâches longues : Plus de 5-10 étapes nécessaires
  • Objectifs bien définis : Le but final est clair dès le début
  • Besoin d'optimisation : Certaines étapes peuvent être parallélisées
  • Budget contrôlé : Coût plus prévisible qu'avec ReAct
  • Qualité critique : Le plan peut être revu par un humain avant exécution

3. Reflexion: Auto-Correction et Amélioration

Le pattern Reflexion ajoute une capacité d'auto-évaluation et d'amélioration itérative. L'agent génère une solution, la critique lui-même, puis l'améliore en boucle jusqu'à satisfaction.

┌────────────────────────────────────────────────────────────────┐
│                    PATTERN REFLEXION                           │
├────────────────────────────────────────────────────────────────┤
│                                                                │
│  Task: "Write an efficient prime number finder in Python"     │
│                                                                │
│  ┌──────────────────────────────────────────────────────────┐ │
│  │ Iteration 1: GENERATE                                    │ │
│  │                                                          │ │
│  │ def find_primes(n):                                      │ │
│  │     primes = []                                          │ │
│  │     for num in range(2, n):                              │ │
│  │         if all(num % i != 0 for i in range(2, num)):    │ │
│  │             primes.append(num)                           │ │
│  │     return primes                                        │ │
│  └──────────────────────────────────────────────────────────┘ │
│                       │                                        │
│                       ▼                                        │
│  ┌──────────────────────────────────────────────────────────┐ │
│  │ REFLECT: Score 4/10                                      │ │
│  │                                                          │ │
│  │ Issues:                                                  │ │
│  │ - Very inefficient for large n (O(n²))                   │ │
│  │ - No docstring                                           │ │
│  │ - Could use Sieve of Eratosthenes                        │ │
│  │                                                          │ │
│  │ Suggestions:                                             │ │
│  │ - Implement Sieve algorithm                              │ │
│  │ - Add documentation                                      │ │
│  │ - Add type hints                                         │ │
│  └──────────────────────────────────────────────────────────┘ │
│                       │                                        │
│                       ▼                                        │
│  ┌──────────────────────────────────────────────────────────┐ │
│  │ Iteration 2: IMPROVE                                     │ │
│  │                                                          │ │
│  │ def find_primes(n: int) -> List[int]:                    │ │
│  │     """Find all primes up to n using Sieve."""          │ │
│  │     if n < 2: return []                                  │ │
│  │     sieve = [True] * (n + 1)                             │ │
│  │     sieve[0] = sieve[1] = False                          │ │
│  │     for i in range(2, int(n**0.5) + 1):                  │ │
│  │         if sieve[i]:                                     │ │
│  │             sieve[i*i:n+1:i] = [False] * len(...)       │ │
│  │     return [i for i, is_prime in enumerate(sieve) ...]  │ │
│  └──────────────────────────────────────────────────────────┘ │
│                       │                                        │
│                       ▼                                        │
│  ┌──────────────────────────────────────────────────────────┐ │
│  │ REFLECT: Score 9/10 ✓                                    │ │
│  │ Excellent! Efficient algorithm, well-documented          │ │
│  └──────────────────────────────────────────────────────────┘ │
│                                                                │
│  Loop: Generate → Reflect → Improve → Repeat                  │
└────────────────────────────────────────────────────────────────┘
                

3.1 Implémentation Reflexion

import openai
from typing import Dict, List

class ReflexionAgent:
    """Agent avec capacité d'auto-réflexion et amélioration itérative"""

    def __init__(self, model: str = "gpt-4"):
        self.model = model
        self.max_iterations = 3
        self.target_score = 8  # Score minimum pour considérer la solution acceptable
        self.trajectory = []

    def generate(self, task: str, previous_attempt: str = None, feedback: str = None) -> str:
        """Générer une solution (ou l'améliorer si feedback fourni)"""

        if previous_attempt is None:
            # Première tentative
            prompt = f"""Solve this task to the best of your ability:

Task: {task}

Provide your solution:"""
        else:
            # Amélioration basée sur le feedback
            prompt = f"""Improve your previous solution based on the feedback provided.

Task: {task}

Previous attempt:
{previous_attempt}

Feedback received:
{feedback}

Provide an IMPROVED solution addressing all the feedback:"""

        response = openai.ChatCompletion.create(
            model=self.model,
            messages=[{"role": "user", "content": prompt}],
            temperature=0.7
        )

        return response.choices[0].message.content

    def reflect(self, task: str, attempt: str, iteration: int) -> Dict:
        """Auto-évaluer la solution de manière critique"""

        prompt = f"""You are a critical evaluator. Evaluate this solution objectively and harshly.

Task: {task}

Solution (Iteration {iteration}):
{attempt}

Provide a detailed evaluation:

1. **Score (0-10)**: How good is this solution?
   - 0-3: Poor, major issues
   - 4-6: Acceptable but needs improvement
   - 7-8: Good, minor issues
   - 9-10: Excellent, minimal issues

2. **Strengths**: What's good about this solution?

3. **Issues**: What are the problems? Be specific.

4. **Suggestions**: How can it be improved?

Format:
Score: [0-10]
Strengths:
- [strength 1]
- [strength 2]
Issues:
- [issue 1]
- [issue 2]
Suggestions:
- [suggestion 1]
- [suggestion 2]"""

        response = openai.ChatCompletion.create(
            model=self.model,
            messages=[{"role": "user", "content": prompt}],
            temperature=0.3  # Moins de randomness pour évaluation
        )

        reflection_text = response.choices[0].message.content

        # Parser le score
        score = 5  # Default
        for line in reflection_text.split('\n'):
            if line.strip().startswith('Score:'):
                try:
                    score_part = line.split('Score:')[1].strip()
                    score = int(''.join(filter(str.isdigit, score_part.split('/')[0])))
                    score = max(0, min(10, score))  # Clamp entre 0-10
                except:
                    pass

        return {
            "score": score,
            "reflection": reflection_text,
            "should_continue": score < self.target_score,
            "iteration": iteration
        }

    def extract_feedback(self, reflection: str) -> str:
        """Extraire les parties pertinentes de la réflexion pour le feedback"""

        # Extraire Issues et Suggestions
        feedback_parts = []

        if "Issues:" in reflection:
            issues_section = reflection.split("Issues:")[1]
            if "Suggestions:" in issues_section:
                issues_section = issues_section.split("Suggestions:")[0]
            feedback_parts.append(f"Issues identified:\n{issues_section.strip()}")

        if "Suggestions:" in reflection:
            suggestions_section = reflection.split("Suggestions:")[1]
            # Prendre jusqu'à la fin ou la prochaine section
            feedback_parts.append(f"Improvement suggestions:\n{suggestions_section.strip()}")

        return "\n\n".join(feedback_parts)

    def run(self, task: str, verbose: bool = True) -> Dict:
        """Exécuter la boucle Reflexion complète"""

        if verbose:
            print(f"\n{'='*70}")
            print(f"REFLEXION AGENT")
            print(f"{'='*70}")
            print(f"Task: {task}")
            print(f"Target Score: {self.target_score}/10")
            print(f"Max Iterations: {self.max_iterations}\n")

        current_attempt = None
        feedback = None

        for iteration in range(1, self.max_iterations + 1):
            if verbose:
                print(f"\n{'─'*70}")
                print(f"Iteration {iteration}/{self.max_iterations}")
                print(f"{'─'*70}\n")

            # GENERATE (ou IMPROVE si pas la première itération)
            if verbose:
                print("📝 GENERATING solution...")

            current_attempt = self.generate(task, current_attempt, feedback)

            if verbose:
                print(f"\nSolution (length: {len(current_attempt)} chars):")
                print(current_attempt[:300] + "..." if len(current_attempt) > 300 else current_attempt)
                print()

            # REFLECT
            if verbose:
                print("🤔 REFLECTING on solution...")

            reflection_result = self.reflect(task, current_attempt, iteration)
            score = reflection_result["score"]
            reflection = reflection_result["reflection"]

            if verbose:
                print(f"\n📊 Evaluation (Score: {score}/10):")
                print(reflection)
                print()

            # Sauvegarder dans la trajectoire
            self.trajectory.append({
                "iteration": iteration,
                "attempt": current_attempt,
                "score": score,
                "reflection": reflection
            })

            # Vérifier si on peut s'arrêter
            if not reflection_result["should_continue"]:
                if verbose:
                    print(f"✅ Target score reached! ({score}/10 >= {self.target_score}/10)")
                break

            if iteration < self.max_iterations:
                # Préparer le feedback pour la prochaine itération
                feedback = self.extract_feedback(reflection)
                if verbose:
                    print(f"🔄 Preparing to improve in next iteration...")
            else:
                if verbose:
                    print(f"⚠️ Max iterations reached")

        # Choisir la meilleure solution
        best = max(self.trajectory, key=lambda x: x["score"])

        if verbose:
            print(f"\n{'='*70}")
            print(f"REFLEXION COMPLETE")
            print(f"{'='*70}")
            print(f"Iterations: {len(self.trajectory)}")
            print(f"Best Score: {best['score']}/10 (Iteration {best['iteration']})")
            print(f"{'='*70}\n")
            print("Best Solution:")
            print(best["attempt"])
            print()

        return {
            "solution": best["attempt"],
            "score": best["score"],
            "iteration": best["iteration"],
            "trajectory": self.trajectory
        }


# TESTS
if __name__ == "__main__":
    agent = ReflexionAgent()

    # Test 1: Tâche de programmation
    result1 = agent.run(
        "Write a Python function to find all prime numbers up to N. "
        "It should be efficient, well-documented, and include type hints."
    )

    print("\n" + "="*70 + "\n")

    # Test 2: Rédaction
    result2 = agent.run(
        "Write a compelling 3-paragraph product description for an AI-powered "
        "code review tool that helps developers write better code."
    )
                
Avantages de Reflexion
  • Qualité supérieure : Amélioration itérative garantit de meilleurs résultats
  • Auto-correction : Détecte et corrige ses propres erreurs
  • Apprentissage : Accumule de l'expérience dans la trajectoire
  • Explicabilité : Chaque amélioration est documentée avec raisons

4. LATS: Language Agent Tree Search

LATS (Language Agent Tree Search) combine recherche arborescente (inspirée de Monte Carlo Tree Search) avec des LLMs pour explorer systématiquement l'espace des solutions.

┌────────────────────────────────────────────────────────────────┐
│              PATTERN LATS (Tree Search)                        │
├────────────────────────────────────────────────────────────────┤
│                                                                │
│                      [Root: Tâche initiale]                    │
│                               │                                │
│                  ┌────────────┼────────────┐                   │
│                  ▼            ▼            ▼                   │
│             [Action A]   [Action B]   [Action C]               │
│              V=0.6        V=0.8        V=0.5                   │
│                  │            │                                │
│                  │      ┌─────┴─────┐                          │
│                  │      ▼           ▼                          │
│                  │  [B→D]       [B→E]                          │
│                  │  V=0.7       V=0.9  ← BEST                  │
│                  │                  │                          │
│                  │            ┌─────┴─────┐                    │
│                  │            ▼           ▼                    │
│                  │        [E→F]       [E→G]                    │
│                  │        V=0.95      V=0.85                   │
│                  │            │                                │
│                  │            ▼                                │
│                  │        [GOAL!] ✓                            │
│                                                                │
│  V = Value (score estimé du nœud)                             │
│                                                                │
│  Processus:                                                    │
│  1. SELECT: Choisir le nœud le plus prometteur (UCB1)         │
│  2. EXPAND: Générer plusieurs actions enfants                 │
│  3. EVALUATE: Scorer chaque action                            │
│  4. BACKPROPAGATE: Propager les scores vers le haut           │
│  5. REPEAT: Jusqu'à trouver la solution ou timeout            │
│                                                                │
└────────────────────────────────────────────────────────────────┘
                

4.1 Quand utiliser LATS ?

LATS est particulièrement puissant pour :

Attention aux Coûts

LATS peut générer des dizaines d'appels LLM pour une seule tâche. Avec GPT-4, cela peut coûter plusieurs dollars par requête. Toujours définir un budget maximum et un timeout.

5. Comparaison Complète des Patterns

Pattern Complexité Appels LLM Coût Qualité Latence Cas d'usage
ReAct Faible 3-10 💰 Moyen ⭐⭐⭐ Bonne Rapide Tâches simples, outils
Plan-Execute Moyenne 5-15 💰💰 Moyen ⭐⭐⭐⭐ Très bonne Moyen Tâches complexes, multi-étapes
Reflexion Moyenne 6-18 💰💰💰 Élevé ⭐⭐⭐⭐⭐ Excellente Lent Qualité critique, code
LATS Élevée 20-100+ 💰💰💰💰 Très élevé ⭐⭐⭐⭐⭐ Excellente Très lent Exploration, optimisation

6. Patterns Hybrides

En production, on combine souvent plusieurs patterns :

6.1 ReAct + Reflexion

ReAct avec auto-évaluation après chaque action majeure.

6.2 Plan-Execute + ReAct

Planifier globalement avec Plan-Execute, mais exécuter chaque étape avec ReAct pour plus de flexibilité.

6.3 Reflexion + LATS

Recherche arborescente où chaque nœud est évalué avec Reflexion.

class HybridAgent:
    """Agent combinant Plan-Execute pour la structure et ReAct pour l'exécution"""

    def __init__(self):
        self.planner = PlanAndExecuteAgent()
        self.executor = ReActAgent()

    def run(self, objective: str) -> str:
        # Phase 1: Créer un plan global
        plan = self.planner.create_plan(objective)

        print(f"Global Plan: {len(plan)} steps")

        # Phase 2: Exécuter chaque étape avec ReAct
        results = []
        for step in plan:
            print(f"\nExecuting step {step['step']} with ReAct...")
            result = self.executor.run(step['description'], verbose=False)
            results.append(result)

        # Phase 3: Synthèse
        return self.planner.synthesize(objective, results)
                
Conseil du Mentor

En production, commencez TOUJOURS par ReAct. C'est le pattern le plus simple, le mieux testé, et il suffit pour 80% des cas. Passez à Plan-Execute uniquement si vos tâches ont systématiquement plus de 7-8 étapes. Reflexion et LATS sont réservés aux cas où la qualité est absolument critique et le budget le permet (recherche, génération de code complexe, etc.).

7. Métriques et Monitoring

Pour chaque pattern, trackez :

import time
from dataclasses import dataclass
from typing import Dict

@dataclass
class AgentMetrics:
    iterations: int = 0
    llm_calls: int = 0
    tool_calls: int = 0
    tool_errors: int = 0
    total_tokens: int = 0
    start_time: float = 0
    end_time: float = 0

    def record_llm_call(self, tokens: int):
        self.llm_calls += 1
        self.total_tokens += tokens

    def record_tool_call(self, success: bool):
        self.tool_calls += 1
        if not success:
            self.tool_errors += 1

    def finalize(self):
        self.end_time = time.time()

    def summary(self) -> Dict:
        duration = self.end_time - self.start_time
        return {
            "iterations": self.iterations,
            "llm_calls": self.llm_calls,
            "tool_calls": self.tool_calls,
            "tool_error_rate": self.tool_errors / self.tool_calls if self.tool_calls > 0 else 0,
            "total_tokens": self.total_tokens,
            "duration_seconds": duration,
            "tokens_per_second": self.total_tokens / duration if duration > 0 else 0
        }
                

Tool Use & Function Calling

Objectifs d'apprentissage
  • Maîtriser l'API Function Calling d'OpenAI et Anthropic
  • Définir des outils robustes avec JSON Schema
  • Implémenter le parallel function calling
  • Créer un routeur d'outils intelligent et gérer les erreurs

1. Qu'est-ce que le Function Calling ?

Le Function Calling (aussi appelé Tool Use chez Anthropic) est une fonctionnalité native des LLMs modernes qui permet à l'agent de décider quand et comment appeler des fonctions externes, avec validation automatique des arguments via JSON Schema.

┌────────────────────────────────────────────────────────────────┐
│                   FUNCTION CALLING FLOW                        │
├────────────────────────────────────────────────────────────────┤
│                                                                │
│  1️⃣ DÉFINITION DES OUTILS                                      │
│  ┌──────────────────────────────────────────────────────────┐ │
│  │ tools = [{                                               │ │
│  │   "type": "function",                                    │ │
│  │   "function": {                                          │ │
│  │     "name": "get_weather",                               │ │
│  │     "description": "Get current weather for a location", │ │
│  │     "parameters": {                                      │ │
│  │       "type": "object",                                  │ │
│  │       "properties": {                                    │ │
│  │         "location": {"type": "string"},                  │ │
│  │         "unit": {"enum": ["C", "F"]}                     │ │
│  │       },                                                 │ │
│  │       "required": ["location"]                           │ │
│  │     }                                                     │ │
│  │   }                                                       │ │
│  │ }]                                                        │ │
│  └──────────────────────────────────────────────────────────┘ │
│                                                                │
│  2️⃣ USER INPUT                                                 │
│     "Quelle est la météo à Paris ?"                            │
│                                                                │
│  3️⃣ LLM DÉCIDE D'APPELER UN OUTIL                              │
│  ┌──────────────────────────────────────────────────────────┐ │
│  │ {                                                        │ │
│  │   "tool_calls": [{                                       │ │
│  │     "id": "call_abc123",                                 │ │
│  │     "function": {                                        │ │
│  │       "name": "get_weather",                             │ │
│  │       "arguments": {                                     │ │
│  │         "location": "Paris",                             │ │
│  │         "unit": "C"                                      │ │
│  │       }                                                   │ │
│  │     }                                                     │ │
│  │   }]                                                      │ │
│  │ }                                                         │ │
│  └──────────────────────────────────────────────────────────┘ │
│                                                                │
│  4️⃣ VOTRE CODE EXÉCUTE                                         │
│     result = get_weather("Paris", "C")                         │
│     → {"temp": 15, "condition": "cloudy"}                      │
│                                                                │
│  5️⃣ RENVOYER LE RÉSULTAT AU LLM                                │
│  ┌──────────────────────────────────────────────────────────┐ │
│  │ {                                                        │ │
│  │   "role": "tool",                                        │ │
│  │   "tool_call_id": "call_abc123",                         │ │
│  │   "content": '{"temp": 15, "condition": "cloudy"}'      │ │
│  │ }                                                         │ │
│  └──────────────────────────────────────────────────────────┘ │
│                                                                │
│  6️⃣ LLM GÉNÈRE LA RÉPONSE FINALE                               │
│     "Il fait 15°C à Paris avec un ciel nuageux."              │
│                                                                │
└────────────────────────────────────────────────────────────────┘
                

2. Function Calling avec OpenAI

2.1 Définir des Outils avec JSON Schema

import openai
import json
import os

# Configuration
openai.api_key = os.getenv("OPENAI_API_KEY")

# Définir les outils disponibles avec JSON Schema
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_current_weather",
            "description": "Get the current weather in a given location",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "The city and state, e.g. San Francisco, CA"
                    },
                    "unit": {
                        "type": "string",
                        "enum": ["celsius", "fahrenheit"],
                        "description": "Temperature unit"
                    }
                },
                "required": ["location"],
                "additionalProperties": False
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "search_web",
            "description": "Search the web for current information",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "The search query"
                    },
                    "num_results": {
                        "type": "integer",
                        "description": "Number of results to return",
                        "minimum": 1,
                        "maximum": 10,
                        "default": 5
                    }
                },
                "required": ["query"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "calculate",
            "description": "Perform mathematical calculations",
            "parameters": {
                "type": "object",
                "properties": {
                    "expression": {
                        "type": "string",
                        "description": "Mathematical expression to evaluate, e.g. '2 + 2 * 3'"
                    }
                },
                "required": ["expression"]
            }
        }
    }
]

# Implémenter les fonctions réelles
def get_current_weather(location: str, unit: str = "celsius") -> dict:
    """Simule un appel API météo"""
    # En production: utiliser OpenWeatherMap, WeatherAPI, etc.
    mock_data = {
        "location": location,
        "temperature": 15 if unit == "celsius" else 59,
        "unit": unit,
        "condition": "partly cloudy",
        "humidity": 65,
        "wind_speed": 10
    }
    return mock_data


def search_web(query: str, num_results: int = 5) -> dict:
    """Simule une recherche web"""
    # En production: utiliser Tavily, SerpAPI, DuckDuckGo
    mock_results = {
        "query": query,
        "results": [
            {
                "title": f"Result {i+1} for {query}",
                "url": f"https://example.com/result-{i+1}",
                "snippet": f"Relevant information about {query}..."
            }
            for i in range(num_results)
        ],
        "total": num_results
    }
    return mock_results


def calculate(expression: str) -> dict:
    """Calculatrice sécurisée"""
    try:
        # ⚠️ eval() est dangereux en production
        # Utiliser numexpr ou ast.literal_eval
        result = eval(expression)
        return {
            "expression": expression,
            "result": result,
            "success": True
        }
    except Exception as e:
        return {
            "expression": expression,
            "error": str(e),
            "success": False
        }


# Mapper les noms de fonctions aux implémentations
available_functions = {
    "get_current_weather": get_current_weather,
    "search_web": search_web,
    "calculate": calculate
}


def run_agent_with_tools(user_message: str, verbose: bool = True):
    """Agent avec function calling"""

    messages = [{"role": "user", "content": user_message}]

    if verbose:
        print(f"\n{'='*70}")
        print(f"User: {user_message}")
        print(f"{'='*70}\n")

    # Premier appel: demander au LLM s'il veut appeler un outil
    response = openai.ChatCompletion.create(
        model="gpt-4-turbo-preview",
        messages=messages,
        tools=tools,
        tool_choice="auto"  # Le LLM décide automatiquement
    )

    response_message = response.choices[0].message
    tool_calls = response_message.tool_calls

    # Vérifier si le LLM veut appeler des outils
    if tool_calls:
        if verbose:
            print(f"🔧 LLM wants to call {len(tool_calls)} tool(s):\n")

        # Ajouter la réponse du LLM aux messages
        messages.append(response_message)

        # Exécuter chaque outil appelé
        for tool_call in tool_calls:
            function_name = tool_call.function.name
            function_args = json.loads(tool_call.function.arguments)

            if verbose:
                print(f"  Tool: {function_name}")
                print(f"  Args: {function_args}")

            # Appeler la vraie fonction
            if function_name in available_functions:
                function_to_call = available_functions[function_name]
                function_response = function_to_call(**function_args)

                if verbose:
                    print(f"  Result: {json.dumps(function_response, indent=2)}\n")

                # Ajouter le résultat aux messages
                messages.append({
                    "tool_call_id": tool_call.id,
                    "role": "tool",
                    "name": function_name,
                    "content": json.dumps(function_response)
                })
            else:
                if verbose:
                    print(f"  ⚠️ Unknown function: {function_name}\n")

        # Deuxième appel: le LLM génère la réponse finale
        if verbose:
            print("💬 Generating final response...\n")

        second_response = openai.ChatCompletion.create(
            model="gpt-4-turbo-preview",
            messages=messages
        )

        final_answer = second_response.choices[0].message.content

        if verbose:
            print(f"✅ Final Answer:\n{final_answer}\n")

        return final_answer

    else:
        # Pas d'outil nécessaire, réponse directe
        if verbose:
            print("💬 Direct response (no tools needed):\n")
            print(f"{response_message.content}\n")

        return response_message.content


# TESTS
if __name__ == "__main__":
    # Test 1: Météo
    result1 = run_agent_with_tools("What's the weather like in Tokyo?")

    # Test 2: Recherche web
    result2 = run_agent_with_tools("Search for recent news about Claude AI")

    # Test 3: Calcul
    result3 = run_agent_with_tools("Calculate 15% of 250, then add 42")

    # Test 4: Pas d'outil nécessaire
    result4 = run_agent_with_tools("What is the capital of France?")
                
Avantages du Function Calling
  • Structuré : Arguments validés automatiquement via JSON Schema
  • Fiable : Le LLM ne hallucine pas les appels de fonction
  • Type-safe : Validation des types, enums, min/max, patterns
  • Natif : Supporté par OpenAI, Anthropic, Mistral, Llama 3.1+

3. JSON Schema Avancé

JSON Schema offre de puissantes fonctionnalités de validation :

# Outil complexe avec validation avancée
advanced_tool = {
    "type": "function",
    "function": {
        "name": "create_user",
        "description": "Create a new user account with comprehensive validation",
        "parameters": {
            "type": "object",
            "properties": {
                "username": {
                    "type": "string",
                    "description": "Username (3-20 chars, alphanumeric + underscore)",
                    "minLength": 3,
                    "maxLength": 20,
                    "pattern": "^[a-zA-Z0-9_]+$"
                },
                "email": {
                    "type": "string",
                    "format": "email",
                    "description": "Valid email address"
                },
                "age": {
                    "type": "integer",
                    "minimum": 18,
                    "maximum": 120,
                    "description": "User age (must be 18+)"
                },
                "role": {
                    "type": "string",
                    "enum": ["admin", "user", "guest", "moderator"],
                    "default": "user",
                    "description": "User role in the system"
                },
                "permissions": {
                    "type": "array",
                    "items": {
                        "type": "string",
                        "enum": ["read", "write", "delete", "manage"]
                    },
                    "uniqueItems": True,
                    "maxItems": 10,
                    "description": "User permissions (unique, max 10)"
                },
                "preferences": {
                    "type": "object",
                    "properties": {
                        "newsletter": {
                            "type": "boolean",
                            "default": False
                        },
                        "theme": {
                            "type": "string",
                            "enum": ["light", "dark", "auto"],
                            "default": "auto"
                        },
                        "language": {
                            "type": "string",
                            "pattern": "^[a-z]{2}$",
                            "description": "ISO 639-1 language code (e.g., 'en', 'fr')"
                        }
                    },
                    "required": ["theme"]
                },
                "metadata": {
                    "type": "object",
                    "additionalProperties": True,
                    "description": "Additional metadata (flexible)"
                },
                "tags": {
                    "type": "array",
                    "items": {"type": "string"},
                    "minItems": 1,
                    "maxItems": 5,
                    "description": "User tags (1-5 tags)"
                },
                "website": {
                    "type": "string",
                    "format": "uri",
                    "description": "User website URL"
                },
                "birth_date": {
                    "type": "string",
                    "format": "date",
                    "description": "Birth date in YYYY-MM-DD format"
                }
            },
            "required": ["username", "email", "age"],
            "additionalProperties": False
        }
    }
}

# Le LLM respectera automatiquement:
# - minLength/maxLength
# - pattern (regex)
# - enum (valeurs possibles)
# - minimum/maximum
# - format (email, uri, date, date-time, etc.)
# - required fields
# - uniqueItems dans les arrays
# - type validation
                

3.1 Formats JSON Schema Standard

Format Description Exemple
email Adresse email valide user@example.com
uri URI complète https://example.com/path
date Date ISO 8601 2024-12-31
date-time DateTime ISO 8601 2024-12-31T23:59:59Z
time Heure 14:30:00
ipv4 Adresse IPv4 192.168.1.1
ipv6 Adresse IPv6 2001:0db8::1
uuid UUID valide 550e8400-e29b-41d4-a716-446655440000

4. Parallel Function Calling

Les LLMs modernes peuvent appeler plusieurs outils en parallèle en une seule fois, ce qui réduit la latence et améliore l'efficacité.

┌────────────────────────────────────────────────────────────────┐
│               PARALLEL FUNCTION CALLING                        │
├────────────────────────────────────────────────────────────────┤
│                                                                │
│  User: "Weather in Paris + news about Eiffel Tower"           │
│                                                                │
│                    LLM Decision                                │
│                         │                                      │
│          ┌──────────────┼──────────────┐                       │
│          ▼              ▼              ▼                       │
│   get_weather("Paris")  search_web("Eiffel Tower")            │
│          │              │                                      │
│          │   PARALLEL   │                                      │
│          │   EXECUTION  │                                      │
│          │              │                                      │
│          ▼              ▼                                      │
│   {"temp": 15°C}   [news_results...]                          │
│          │              │                                      │
│          └──────────┬───┘                                      │
│                     ▼                                          │
│              LLM Synthesis                                     │
│                     │                                          │
│                     ▼                                          │
│  "Il fait 15°C à Paris. Actualités: ..."                      │
│                                                                │
│  Avantage: 1 round au lieu de 2 (séquentiel)                  │
│  Gain latence: ~50%                                            │
└────────────────────────────────────────────────────────────────┘
                
def run_agent_with_parallel_tools(user_message: str):
    """Agent supportant les appels d'outils parallèles"""

    messages = [{"role": "user", "content": user_message}]

    print(f"\n{'='*70}")
    print(f"User: {user_message}")
    print(f"{'='*70}\n")

    response = openai.ChatCompletion.create(
        model="gpt-4-turbo-preview",
        messages=messages,
        tools=tools,
        tool_choice="auto"
    )

    response_message = response.choices[0].message
    tool_calls = response_message.tool_calls

    if tool_calls:
        print(f"🔧 LLM requested {len(tool_calls)} tool(s) IN PARALLEL:\n")

        messages.append(response_message)

        # Exécuter tous les outils (potentiellement en parallèle)
        for i, tool_call in enumerate(tool_calls, 1):
            function_name = tool_call.function.name
            function_args = json.loads(tool_call.function.arguments)

            print(f"  [{i}] {function_name}({function_args})")

            # Exécuter
            function_to_call = available_functions[function_name]
            function_response = function_to_call(**function_args)

            print(f"      → Result: {json.dumps(function_response, indent=8)[:100]}...\n")

            # Ajouter le résultat
            messages.append({
                "tool_call_id": tool_call.id,
                "role": "tool",
                "name": function_name,
                "content": json.dumps(function_response)
            })

        # Réponse finale synthétisant tous les résultats
        print("💬 Synthesizing all results...\n")

        final_response = openai.ChatCompletion.create(
            model="gpt-4-turbo-preview",
            messages=messages
        )

        final_answer = final_response.choices[0].message.content
        print(f"✅ Final Answer:\n{final_answer}\n")

        return final_answer

    return response_message.content


# TEST: Le LLM appellera plusieurs outils en parallèle
result = run_agent_with_parallel_tools(
    "What's the weather in New York? Also search for recent news about the city, "
    "and calculate 25% of 500"
)
                
Quand utiliser Parallel Calling ?
  • Requêtes indépendantes : Les outils n'ont pas de dépendances entre eux
  • Gain de latence : Réduire le nombre de rounds d'aller-retour
  • Tâches composites : "Fais X et Y et Z"

5. Tool Use avec Anthropic Claude

Claude utilise une API légèrement différente mais avec le même concept :

import anthropic
import json

client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))

# Définir les outils pour Claude (syntaxe légèrement différente)
claude_tools = [
    {
        "name": "get_weather",
        "description": "Get the current weather in a given location",
        "input_schema": {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                    "description": "The city name, e.g. Paris"
                },
                "unit": {
                    "type": "string",
                    "enum": ["celsius", "fahrenheit"],
                    "description": "Temperature unit"
                }
            },
            "required": ["location"]
        }
    },
    {
        "name": "search_web",
        "description": "Search the web for information",
        "input_schema": {
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "Search query"
                }
            },
            "required": ["query"]
        }
    }
]


def run_claude_with_tools(user_message: str):
    """Agent Claude avec tool use"""

    messages = [{"role": "user", "content": user_message}]

    print(f"\n{'='*70}")
    print(f"User: {user_message}")
    print(f"{'='*70}\n")

    # Premier appel
    response = client.messages.create(
        model="claude-3-5-sonnet-20241022",
        max_tokens=1024,
        tools=claude_tools,
        messages=messages
    )

    print(f"Stop reason: {response.stop_reason}\n")

    # Vérifier si Claude veut utiliser un outil
    if response.stop_reason == "tool_use":
        # Extraire les tool uses
        tool_uses = [block for block in response.content if block.type == "tool_use"]

        print(f"🔧 Claude wants to use {len(tool_uses)} tool(s):\n")

        # Ajouter la réponse de Claude
        messages.append({"role": "assistant", "content": response.content})

        # Préparer les résultats des outils
        tool_results = []

        for tool_use in tool_uses:
            tool_name = tool_use.name
            tool_input = tool_use.input

            print(f"  Tool: {tool_name}")
            print(f"  Input: {tool_input}")

            # Exécuter l'outil
            if tool_name == "get_weather":
                result = get_current_weather(**tool_input)
            elif tool_name == "search_web":
                result = search_web(**tool_input)
            else:
                result = {"error": "Unknown tool"}

            print(f"  Result: {result}\n")

            tool_results.append({
                "type": "tool_result",
                "tool_use_id": tool_use.id,
                "content": json.dumps(result)
            })

        # Ajouter les résultats
        messages.append({
            "role": "user",
            "content": tool_results
        })

        # Deuxième appel pour la réponse finale
        print("💬 Getting final response from Claude...\n")

        final_response = client.messages.create(
            model="claude-3-5-sonnet-20241022",
            max_tokens=1024,
            tools=claude_tools,
            messages=messages
        )

        # Extraire le texte de la réponse
        final_text = next(
            (block.text for block in final_response.content if hasattr(block, 'text')),
            str(final_response.content)
        )

        print(f"✅ Claude's Answer:\n{final_text}\n")

        return final_text

    else:
        # Réponse directe
        final_text = next(
            (block.text for block in response.content if hasattr(block, 'text')),
            str(response.content)
        )
        print(f"💬 Direct answer:\n{final_text}\n")
        return final_text


# TEST
result = run_claude_with_tools(
    "What's the weather in Tokyo in Fahrenheit? Also search for recent tech news"
)
                

6. Tool Router Intelligent

Pour gérer de nombreux outils (20+), créez un routeur qui sélectionne intelligemment les outils pertinents :

from typing import List, Dict, Callable
import openai

class ToolRouter:
    """Routeur intelligent d'outils avec catégorisation"""

    def __init__(self, model: str = "gpt-4-turbo-preview"):
        self.model = model
        self.tools = {}
        self.tool_schemas = []
        self.categories = {}

    def register_tool(
        self,
        func: Callable,
        description: str,
        parameters: Dict,
        category: str = "general"
    ):
        """Enregistrer un outil dans une catégorie"""

        tool_name = func.__name__

        self.tools[tool_name] = func

        schema = {
            "type": "function",
            "function": {
                "name": tool_name,
                "description": description,
                "parameters": parameters
            }
        }

        self.tool_schemas.append(schema)

        # Catégoriser
        if category not in self.categories:
            self.categories[category] = []
        self.categories[category].append(tool_name)

    def select_relevant_tools(self, query: str) -> List[Dict]:
        """Sélectionner les outils pertinents pour la requête"""

        if len(self.tool_schemas) <= 5:
            # Si peu d'outils, tous sont pertinents
            return self.tool_schemas

        categories_desc = "\n".join([
            f"- {cat}: {', '.join(tools)}"
            for cat, tools in self.categories.items()
        ])

        prompt = f"""Given this user query, which tool categories are most relevant?

Query: {query}

Available categories:
{categories_desc}

Respond with ONLY the category names that are relevant (comma-separated):"""

        response = openai.ChatCompletion.create(
            model=self.model,
            messages=[{"role": "user", "content": prompt}],
            temperature=0,
            max_tokens=100
        )

        selected_cats = response.choices[0].message.content.strip().split(',')
        selected_cats = [cat.strip() for cat in selected_cats]

        # Filtrer les outils
        relevant_tools = []
        for schema in self.tool_schemas:
            tool_name = schema["function"]["name"]
            for cat in selected_cats:
                if tool_name in self.categories.get(cat, []):
                    relevant_tools.append(schema)
                    break

        return relevant_tools if relevant_tools else self.tool_schemas[:5]

    def run(self, user_message: str, use_routing: bool = True):
        """Exécuter l'agent avec routing optionnel"""

        # Sélectionner les outils pertinents
        if use_routing and len(self.tool_schemas) > 5:
            tools = self.select_relevant_tools(user_message)
            print(f"🔀 Router selected {len(tools)}/{len(self.tool_schemas)} tools\n")
        else:
            tools = self.tool_schemas

        # Appeler le LLM avec les outils sélectionnés
        messages = [{"role": "user", "content": user_message}]

        response = openai.ChatCompletion.create(
            model=self.model,
            messages=messages,
            tools=tools,
            tool_choice="auto"
        )

        response_message = response.choices[0].message
        tool_calls = response_message.tool_calls

        if tool_calls:
            messages.append(response_message)

            for tool_call in tool_calls:
                function_name = tool_call.function.name
                function_args = json.loads(tool_call.function.arguments)

                print(f"🔧 Calling: {function_name}({function_args})")

                # Exécuter l'outil
                if function_name in self.tools:
                    function_response = self.tools[function_name](**function_args)
                else:
                    function_response = {"error": f"Tool {function_name} not found"}

                messages.append({
                    "tool_call_id": tool_call.id,
                    "role": "tool",
                    "name": function_name,
                    "content": json.dumps(function_response)
                })

            # Réponse finale
            final_response = openai.ChatCompletion.create(
                model=self.model,
                messages=messages
            )

            return final_response.choices[0].message.content

        return response_message.content


# EXEMPLE D'UTILISATION
router = ToolRouter()

# Catégorie: Data
def query_database(sql: str) -> dict:
    return {"rows": 42, "query": sql}

router.register_tool(
    func=query_database,
    description="Execute SQL query on database",
    parameters={
        "type": "object",
        "properties": {
            "sql": {"type": "string", "description": "SQL query"}
        },
        "required": ["sql"]
    },
    category="data"
)

# Catégorie: Web
def scrape_website(url: str) -> dict:
    return {"content": f"Content from {url}", "status": 200}

router.register_tool(
    func=scrape_website,
    description="Scrape content from a website",
    parameters={
        "type": "object",
        "properties": {
            "url": {"type": "string", "format": "uri"}
        },
        "required": ["url"]
    },
    category="web"
)

# Catégorie: Communication
def send_email(to: str, subject: str, body: str) -> dict:
    return {"status": "sent", "to": to, "timestamp": "2024-01-01T12:00:00Z"}

router.register_tool(
    func=send_email,
    description="Send an email",
    parameters={
        "type": "object",
        "properties": {
            "to": {"type": "string", "format": "email"},
            "subject": {"type": "string"},
            "body": {"type": "string"}
        },
        "required": ["to", "subject", "body"]
    },
    category="communication"
)

# TEST avec routing
result = router.run("Query the database for users created last month", use_routing=True)
print(f"\n✅ Result: {result}")
                
Pourquoi Router les Outils ?
  • Performance : Moins d'outils dans le prompt = réponse plus rapide
  • Coût : Moins de tokens = moins cher
  • Précision : Le LLM choisit mieux parmi 5 outils que 50
  • Limite de context : Certains LLMs ont des limites sur la taille des tools

7. Error Handling & Retry Logic

from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type

class RobustToolAgent:
    """Agent avec gestion d'erreurs robuste"""

    def __init__(self):
        self.tools = {
            "web_search": self.web_search,
            "calculator": self.calculator,
            "database_query": self.database_query
        }
        self.fallbacks = {
            "web_search": "duckduckgo_search",
            "calculator": "python_eval"
        }

    @retry(
        stop=stop_after_attempt(3),
        wait=wait_exponential(multiplier=1, min=2, max=10),
        retry=retry_if_exception_type((ConnectionError, TimeoutError))
    )
    def web_search(self, query: str) -> dict:
        """Recherche web avec retry automatique"""
        # Simuler un appel qui peut échouer
        import random
        if random.random() < 0.3:  # 30% chance d'échec
            raise ConnectionError("API temporarily unavailable")

        return {"results": f"Results for {query}"}

    def calculator(self, expression: str) -> dict:
        """Calculatrice avec validation"""
        try:
            # Valider que l'expression est sûre
            allowed_chars = set("0123456789+-*/(). ")
            if not all(c in allowed_chars for c in expression):
                raise ValueError("Invalid characters in expression")

            result = eval(expression)
            return {"result": result, "success": True}
        except Exception as e:
            return {"error": str(e), "success": False}

    def database_query(self, sql: str) -> dict:
        """Query avec validation SQL basique"""
        # Vérifier que c'est un SELECT (pas de modification)
        sql_upper = sql.strip().upper()
        if not sql_upper.startswith("SELECT"):
            return {"error": "Only SELECT queries allowed", "success": False}

        # Bloquer les mots-clés dangereux
        dangerous = ["DROP", "DELETE", "UPDATE", "INSERT", "ALTER"]
        if any(word in sql_upper for word in dangerous):
            return {"error": "Dangerous SQL detected", "success": False}

        return {"rows": 42, "success": True}

    def call_tool_with_fallback(self, tool_name: str, **kwargs) -> dict:
        """Appeler un outil avec fallback si échec"""

        print(f"🔧 Calling {tool_name}...")

        try:
            # Essayer l'outil principal
            result = self.tools[tool_name](**kwargs)

            if isinstance(result, dict) and not result.get("success", True):
                raise Exception(f"Tool returned error: {result.get('error')}")

            print(f"   ✓ Success")
            return result

        except Exception as e:
            print(f"   ✗ Error: {str(e)}")

            # Essayer le fallback si disponible
            if tool_name in self.fallbacks:
                fallback_name = self.fallbacks[tool_name]
                print(f"   🔄 Trying fallback: {fallback_name}...")

                try:
                    fallback_result = self.tools.get(fallback_name, lambda **k: {"error": "No fallback"})(** kwargs)
                    print(f"   ✓ Fallback succeeded")
                    return fallback_result
                except Exception as fallback_error:
                    print(f"   ✗ Fallback also failed: {fallback_error}")

            # Si tout échoue, retourner l'erreur
            return {
                "error": str(e),
                "tool": tool_name,
                "success": False
            }


# TEST
agent = RobustToolAgent()

# Test avec potentiel d'échec et retry
for i in range(3):
    print(f"\nAttempt {i+1}:")
    result = agent.call_tool_with_fallback("web_search", query="AI agents")
    print(f"Result: {result}\n")
                

8. Comparaison: OpenAI vs Anthropic vs Open Source

Provider API Name Parallel Calls Validation Streaming Notes
OpenAI Function Calling ✅ Oui ✅ Strict JSON Schema ✅ Oui Le plus mature, documentation excellente
Anthropic Tool Use ✅ Oui ✅ JSON Schema ✅ Oui API légèrement différente mais puissante
Mistral Function Calling ✅ Oui ✅ JSON Schema ✅ Oui Compatible OpenAI API
Llama 3.1+ Tool Use (native) ✅ Oui ⚠️ Moins strict ✅ Oui Nécessite prompting spécifique
Older Models Prompt-based ❌ Non ❌ Manuel N/A Parsing manuel du texte libre
Conseil du Mentor

En production, toujours implémenter : validation stricte des inputs, retry logic avec backoff exponentiel, timeouts, rate limiting, et logging complet de tous les appels d'outils. Un outil qui échoue ne doit JAMAIS faire crasher tout l'agent.

Mémoire pour Agents

Objectifs d'apprentissage
  • Implémenter la mémoire court terme (contexte conversationnel)
  • Créer une mémoire long terme avec vector store
  • Comprendre la mémoire épisodique vs sémantique
  • Optimiser la gestion de la fenêtre de contexte

1. Types de Mémoire pour Agents

Un agent intelligent a besoin de plusieurs types de mémoire pour fonctionner efficacement et maintenir un contexte cohérent sur de longues périodes.

┌────────────────────────────────────────────────────────────────┐
│                  SYSTÈME DE MÉMOIRE AGENT                      │
├────────────────────────────────────────────────────────────────┤
│                                                                │
│  1. MÉMOIRE DE TRAVAIL (Working Memory)                        │
│  ┌──────────────────────────────────────────────────────────┐ │
│  │ • Contexte actuel, variables temporaires                │ │
│  │ • Durée: Session courante uniquement                    │ │
│  │ • Taille: Limitée par context window du LLM             │ │
│  │ • Exemple: "L'utilisateur vient de demander X"          │ │
│  │ • Implémentation: Messages dans le prompt               │ │
│  └──────────────────────────────────────────────────────────┘ │
│                                                                │
│  2. MÉMOIRE ÉPISODIQUE (Episodic Memory)                       │
│  ┌──────────────────────────────────────────────────────────┐ │
│  │ • Historique des interactions et expériences            │ │
│  │ • Durée: Persistante (jours, semaines, mois)            │ │
│  │ • Taille: Illimitée (stockage externe)                  │ │
│  │ • Exemple: "Le 15/02, l'utilisateur a créé X"           │ │
│  │ • Implémentation: Vector DB, recherche sémantique       │ │
│  └──────────────────────────────────────────────────────────┘ │
│                                                                │
│  3. MÉMOIRE SÉMANTIQUE (Semantic Memory)                       │
│  ┌──────────────────────────────────────────────────────────┐ │
│  │ • Connaissances factuelles, concepts appris             │ │
│  │ • Durée: Permanente                                     │ │
│  │ • Taille: Structurée, optimisée                         │ │
│  │ • Exemple: "API key format: sk-...", "User prefs"       │ │
│  │ • Implémentation: DB relationnelle + Vector DB          │ │
│  └──────────────────────────────────────────────────────────┘ │
│                                                                │
│  4. MÉMOIRE PROCÉDURALE (Procedural Memory)                    │
│  ┌──────────────────────────────────────────────────────────┐ │
│  │ • Comment faire des tâches (workflows, procédures)      │ │
│  │ • Durée: Permanente                                     │ │
│  │ • Exemple: "Pour deploy: build → test → push"           │ │
│  │ • Implémentation: Templates, knowledge base             │ │
│  └──────────────────────────────────────────────────────────┘ │
│                                                                │
└────────────────────────────────────────────────────────────────┘
                

2. Implémentation de la Mémoire Court Terme

from typing import List, Dict
from collections import deque
import tiktoken

class ShortTermMemory:
    """Mémoire court terme avec fenêtre glissante et gestion des tokens"""

    def __init__(self, max_messages: int = 20, max_tokens: int = 4000, model: str = "gpt-4"):
        self.messages = deque(maxlen=max_messages)
        self.max_tokens = max_tokens
        self.model = model
        self.encoding = tiktoken.encoding_for_model(model)
        self.system_message = None

    def set_system_message(self, content: str):
        """Définir le message système (toujours gardé)"""
        self.system_message = {"role": "system", "content": content}

    def add_message(self, role: str, content: str):
        """Ajouter un message à la mémoire"""
        message = {"role": role, "content": content}
        self.messages.append(message)
        self._trim_to_token_limit()

    def count_tokens(self, text: str) -> int:
        """Compter précisément le nombre de tokens"""
        return len(self.encoding.encode(text))

    def count_messages_tokens(self, messages: List[Dict]) -> int:
        """Compter les tokens d'une liste de messages"""
        total = 0
        for message in messages:
            # ~4 tokens par message (overhead)
            total += 4
            total += self.count_tokens(message.get("content", ""))
            if "name" in message:
                total += self.count_tokens(message["name"])
        total += 2  # Completion prompt
        return total

    def _trim_to_token_limit(self):
        """Réduire les messages pour respecter la limite de tokens"""
        while len(self.messages) > 1:
            messages = self.get_messages()
            total_tokens = self.count_messages_tokens(messages)

            if total_tokens <= self.max_tokens:
                break

            # Supprimer le message le plus ancien (sauf system)
            # On garde toujours le premier message user et le dernier assistant
            if len(self.messages) > 3:
                # Supprimer l'élément à l'index 0 (le plus vieux)
                removed = self.messages.popleft()
                print(f"⚠️ Trimmed message ({self.count_tokens(removed['content'])} tokens): {removed['content'][:50]}...")
            else:
                break

    def get_messages(self) -> List[Dict]:
        """Récupérer tous les messages avec le system message"""
        if self.system_message:
            return [self.system_message] + list(self.messages)
        return list(self.messages)

    def clear(self):
        """Vider la mémoire (garder le system message)"""
        self.messages.clear()

    def summarize_and_compress(self, llm_client) -> str:
        """Résumer les vieux messages pour gagner de l'espace"""
        if len(self.messages) < 8:
            return None

        # Prendre les messages du milieu pour résumer (garder début et fin)
        to_summarize = list(self.messages)[2:-2]

        if not to_summarize:
            return None

        conversation = "\n".join([
            f"{msg['role']}: {msg['content']}"
            for msg in to_summarize
        ])

        prompt = f"""Summarize this conversation very concisely (2-3 sentences max).
Focus on key facts, decisions, and context that would be useful later.

Conversation:
{conversation}

Concise summary:"""

        # Appeler le LLM pour résumer
        response = llm_client.create_completion(prompt)
        summary = response.strip()

        print(f"📝 Compressed {len(to_summarize)} messages into summary:")
        print(f"   {summary}\n")

        # Remplacer les messages résumés par le résumé
        # Garder les 2 premiers et 2 derniers
        new_messages = deque(list(self.messages)[:2], maxlen=self.messages.maxlen)
        new_messages.append({"role": "system", "content": f"[Previous conversation summary: {summary}]"})
        new_messages.extend(list(self.messages)[-2:])

        self.messages = new_messages

        return summary

    def get_stats(self) -> Dict:
        """Statistiques sur la mémoire"""
        messages = self.get_messages()
        return {
            "num_messages": len(self.messages),
            "total_tokens": self.count_messages_tokens(messages),
            "max_tokens": self.max_tokens,
            "utilization": f"{(self.count_messages_tokens(messages) / self.max_tokens * 100):.1f}%"
        }


# EXEMPLE D'UTILISATION
if __name__ == "__main__":
    memory = ShortTermMemory(max_messages=10, max_tokens=1000)

    memory.set_system_message("You are a helpful assistant.")

    # Simuler une conversation
    memory.add_message("user", "Hello! My name is Alice.")
    memory.add_message("assistant", "Hello Alice! Nice to meet you.")
    memory.add_message("user", "I'm working on a Python project about AI agents.")
    memory.add_message("assistant", "That sounds interesting! What specific aspects are you working on?")

    # Afficher les statistiques
    stats = memory.get_stats()
    print(f"Memory stats: {stats}")

    # Récupérer les messages
    messages = memory.get_messages()
    print(f"\nCurrent messages: {len(messages)}")
    for msg in messages:
        print(f"  {msg['role']}: {msg['content'][:50]}...")
                
Stratégies de Gestion de Context Window
  • Fenêtre glissante : Garder les N derniers messages (simple mais perte de contexte)
  • Résumé progressif : Résumer les vieux messages périodiquement
  • Mémoire sélective : Garder les messages "importants" (avec scoring)
  • Compression : Utiliser un modèle plus petit pour compresser le contexte

3. Mémoire Long Terme avec Vector Store

import chromadb
from chromadb.config import Settings
from datetime import datetime
import uuid
import hashlib

class LongTermMemory:
    """Mémoire long terme avec recherche sémantique"""

    def __init__(self, collection_name: str = "agent_memory", persist_directory: str = "./chroma_db"):
        self.client = chromadb.PersistentClient(
            path=persist_directory,
            settings=Settings(anonymized_telemetry=False)
        )

        self.collection = self.client.get_or_create_collection(
            name=collection_name,
            metadata={"hnsw:space": "cosine"}
        )

    def store_interaction(
        self,
        user_message: str,
        agent_response: str,
        metadata: Dict = None
    ) -> str:
        """Sauvegarder une interaction complète"""

        interaction_id = str(uuid.uuid4())
        timestamp = datetime.now().isoformat()

        # Construire le document (combinaison user + agent)
        document = f"User: {user_message}\nAgent: {agent_response}"

        meta = {
            "type": "interaction",
            "timestamp": timestamp,
            "user_message": user_message,
            "agent_response": agent_response,
            **(metadata or {})
        }

        self.collection.add(
            documents=[document],
            metadatas=[meta],
            ids=[interaction_id]
        )

        return interaction_id

    def store_fact(self, fact: str, category: str = "general", source: str = None):
        """Sauvegarder un fait/connaissance"""

        # Créer un ID basé sur le hash du contenu (éviter les doublons)
        fact_hash = hashlib.md5(fact.encode()).hexdigest()
        fact_id = f"fact_{fact_hash}"

        # Vérifier si existe déjà
        existing = self.collection.get(ids=[fact_id])
        if existing['ids']:
            print(f"ℹ️ Fact already exists, updating...")
            self.collection.update(
                ids=[fact_id],
                documents=[fact],
                metadatas=[{
                    "type": "fact",
                    "category": category,
                    "source": source,
                    "updated_at": datetime.now().isoformat()
                }]
            )
        else:
            self.collection.add(
                documents=[fact],
                metadatas={
                    "type": "fact",
                    "category": category,
                    "source": source,
                    "timestamp": datetime.now().isoformat()
                },
                ids=[fact_id]
            )

        return fact_id

    def store_user_preference(self, key: str, value: str):
        """Sauvegarder une préférence utilisateur"""
        pref_id = f"pref_{key}"

        self.collection.upsert(
            documents=[f"{key}: {value}"],
            metadatas=[{
                "type": "preference",
                "key": key,
                "value": value,
                "timestamp": datetime.now().isoformat()
            }],
            ids=[pref_id]
        )

    def recall(self, query: str, n_results: int = 3, filter_type: str = None) -> List[Dict]:
        """Rechercher dans la mémoire avec filtrage optionnel"""

        where_filter = {"type": filter_type} if filter_type else None

        results = self.collection.query(
            query_texts=[query],
            n_results=n_results,
            where=where_filter
        )

        memories = []
        if results['documents'][0]:
            for i in range(len(results['documents'][0])):
                memories.append({
                    "id": results['ids'][0][i],
                    "content": results['documents'][0][i],
                    "metadata": results['metadatas'][0][i],
                    "distance": results['distances'][0][i],
                    "relevance": 1 - results['distances'][0][i]  # Cosine similarity
                })

        return memories

    def get_recent_memories(self, n: int = 5, memory_type: str = None) -> List[Dict]:
        """Récupérer les N mémoires les plus récentes"""

        # ChromaDB ne supporte pas le tri natif, on doit tout récupérer et trier
        where_filter = {"type": memory_type} if memory_type else None

        all_memories = self.collection.get(
            where=where_filter,
            limit=100  # Limiter pour la performance
        )

        # Trier par timestamp
        memories_with_time = []
        for i in range(len(all_memories['ids'])):
            meta = all_memories['metadatas'][i]
            timestamp = meta.get('timestamp', meta.get('updated_at', '1970-01-01'))

            memories_with_time.append({
                "id": all_memories['ids'][i],
                "content": all_memories['documents'][i],
                "metadata": meta,
                "timestamp": timestamp
            })

        sorted_memories = sorted(
            memories_with_time,
            key=lambda x: x['timestamp'],
            reverse=True
        )

        return sorted_memories[:n]

    def forget(self, memory_id: str):
        """Supprimer une mémoire"""
        self.collection.delete(ids=[memory_id])

    def get_stats(self) -> Dict:
        """Statistiques sur la mémoire"""
        count = self.collection.count()

        # Compter par type
        types_count = {}
        all_data = self.collection.get()

        for meta in all_data['metadatas']:
            mem_type = meta.get('type', 'unknown')
            types_count[mem_type] = types_count.get(mem_type, 0) + 1

        return {
            "total_memories": count,
            "by_type": types_count
        }


# Agent avec mémoire complète
class MemoryAgent:
    """Agent avec système de mémoire court et long terme"""

    def __init__(self, model: str = "gpt-4"):
        self.model = model
        self.short_term = ShortTermMemory(max_messages=10, max_tokens=4000)
        self.long_term = LongTermMemory(collection_name="my_agent_memory")

        # System prompt initial
        self.short_term.set_system_message(
            "You are a helpful assistant with memory. "
            "You can remember past conversations and learn from them."
        )

    def chat(self, user_message: str) -> str:
        """Conversation avec mémoire"""

        print(f"\n{'='*70}")
        print(f"User: {user_message}")
        print(f"{'='*70}\n")

        # 1. Rappel de mémoires pertinentes du long terme
        print("🔍 Searching long-term memory...")
        relevant_memories = self.long_term.recall(user_message, n_results=2)

        context = ""
        if relevant_memories:
            print(f"   Found {len(relevant_memories)} relevant memories:\n")
            context = "\n\nRelevant past information:\n"
            for i, mem in enumerate(relevant_memories, 1):
                relevance = mem['relevance']
                content_preview = mem['content'][:100]
                print(f"   {i}. (relevance: {relevance:.2f}) {content_preview}...")
                context += f"- {mem['content']}\n"

        # 2. Ajouter le contexte au prompt si pertinent
        if context and relevant_memories[0]['relevance'] > 0.7:
            enhanced_system = self.short_term.system_message['content'] + context
            self.short_term.set_system_message(enhanced_system)

        # 3. Ajouter à la mémoire court terme
        self.short_term.add_message("user", user_message)

        # 4. Appeler le LLM
        print("\n💬 Generating response...")
        messages = self.short_term.get_messages()

        response = openai.ChatCompletion.create(
            model=self.model,
            messages=messages,
            temperature=0.7
        )

        agent_response = response.choices[0].message.content

        # 5. Mettre à jour les mémoires
        self.short_term.add_message("assistant", agent_response)
        self.long_term.store_interaction(user_message, agent_response)

        # 6. Afficher les stats
        print(f"\n📊 Memory stats:")
        print(f"   Short-term: {self.short_term.get_stats()}")
        print(f"   Long-term: {self.long_term.get_stats()}")

        print(f"\n✅ Agent: {agent_response}\n")

        return agent_response

    def learn_fact(self, fact: str, category: str = "general"):
        """Apprendre un nouveau fait"""
        fact_id = self.long_term.store_fact(fact, category)
        print(f"✅ Learned fact (ID: {fact_id}): {fact}")

    def set_preference(self, key: str, value: str):
        """Définir une préférence utilisateur"""
        self.long_term.store_user_preference(key, value)
        print(f"✅ Preference set: {key} = {value}")

    def recall_memories(self, query: str, n: int = 5):
        """Rechercher dans les mémoires"""
        print(f"\n🔍 Searching memories for: '{query}'")
        memories = self.long_term.recall(query, n_results=n)

        for i, mem in enumerate(memories, 1):
            print(f"\n{i}. Relevance: {mem['relevance']:.2f}")
            print(f"   {mem['content'][:200]}...")
            print(f"   Type: {mem['metadata'].get('type')}")
            print(f"   Date: {mem['metadata'].get('timestamp', 'N/A')[:10]}")


# TEST
if __name__ == "__main__":
    agent = MemoryAgent()

    # Conversation 1
    agent.chat("My name is Alice and I love Python programming")

    # Apprendre des faits
    agent.learn_fact("Python was created by Guido van Rossum in 1991", category="tech")
    agent.learn_fact("Alice prefers dark mode in her IDE", category="preferences")

    # Conversation 2 (plus tard)
    agent.chat("What's my name and what do I like?")

    # Conversation 3 (test de rappel)
    agent.chat("Tell me about Python's creator")

    # Rechercher dans les mémoires
    agent.recall_memories("Alice preferences", n=3)
                

4. Stratégies de Gestion de Mémoire

Stratégie Description Avantages Inconvénients Cas d'usage
Fenêtre glissante Garder les N derniers messages Simple, prévisible, rapide Perte du contexte ancien Conversations courtes
Résumé progressif Résumer l'historique périodiquement Préserve l'essence, flexible Coût LLM, perte détails Sessions longues
Mémoire externe Vector DB, rappel sémantique Capacité illimitée, pertinence Complexité, latence Multi-sessions
Mémoire hiérarchique Niveaux: court/moyen/long terme Optimal qualité/coût Complexe à implémenter Production avancée
Mémoire sélective Scorer et garder l'important Maximise pertinence Coût de scoring Conversations critiques
Conseil du Mentor

Pour un agent production, utilisez une approche hybride : fenêtre glissante (10-20 messages) pour le contexte immédiat + vector store (ChromaDB/Pinecone) pour la mémoire long terme + base relationnelle (PostgreSQL) pour les données structurées (préférences, paramètres). N'oubliez pas d'implémenter un mécanisme de "oubli" pour respecter le RGPD.

LangChain Agents

Objectifs d'apprentissage
  • Maîtriser l'AgentExecutor de LangChain
  • Créer et utiliser des outils LangChain
  • Implémenter des output parsers personnalisés
  • Utiliser les callbacks pour le monitoring et streaming

1. Introduction à LangChain Agents

LangChain est le framework le plus populaire pour construire des applications avec des LLMs. Les Agents LangChain implémentent les patterns agentiques (principalement ReAct) de manière standardisée et facile à utiliser.

┌────────────────────────────────────────────────────────────────┐
│                  ARCHITECTURE LANGCHAIN AGENT                  │
├────────────────────────────────────────────────────────────────┤
│                                                                │
│                    ┌─────────────────┐                         │
│                    │  AgentExecutor  │                         │
│                    │   (Orchestre)   │                         │
│                    └────────┬────────┘                         │
│                             │                                  │
│              ┌──────────────┼──────────────┐                   │
│              ▼              ▼              ▼                   │
│         ┌─────────┐   ┌─────────┐   ┌──────────┐             │
│         │  Agent  │   │  Tools  │   │  Memory  │             │
│         │ (ReAct) │   │ (Outils)│   │(Optional)│             │
│         └────┬────┘   └────┬────┘   └──────────┘             │
│              │             │                                   │
│              ▼             ▼                                   │
│         ┌─────────┐   ┌─────────┐                             │
│         │   LLM   │   │  Tool   │                             │
│         │(GPT-4,  │   │ Execute │                             │
│         │Claude...)│   │         │                             │
│         └─────────┘   └─────────┘                             │
│                                                                │
│  Flow: User Input → Agent → LLM → Tool Selection →            │
│        Tool Execute → Result → LLM → Final Answer             │
│                                                                │
└────────────────────────────────────────────────────────────────┘
                

2. Installation et Setup

# Installer LangChain et dépendances
pip install langchain langchain-openai langchain-community langchain-core

# Outils additionnels
pip install langchainhub  # Pour accéder aux prompts partagés
pip install tavily-python  # Pour la recherche web
pip install duckduckgo-search  # Alternative gratuite

# Imports essentiels
from langchain.agents import AgentExecutor, create_react_agent
from langchain_openai import ChatOpenAI
from langchain.tools import Tool, StructuredTool
from langchain.prompts import PromptTemplate
from langchain import hub
from langchain.callbacks import StdOutCallbackHandler
import os

# Configuration
os.environ["OPENAI_API_KEY"] = "your-api-key"
os.environ["TAVILY_API_KEY"] = "your-tavily-key"  # Pour web search
                

3. Créer des Outils LangChain

3.1 Outils Simples avec Tool

from langchain.tools import Tool
from langchain.utilities import DuckDuckGoSearchAPIWrapper
import requests

# Méthode 1: Tool simple avec fonction
def calculate(expression: str) -> str:
    """Calculate a mathematical expression."""
    try:
        result = eval(expression)
        return f"The result is: {result}"
    except Exception as e:
        return f"Error: {str(e)}"

calculator_tool = Tool(
    name="Calculator",
    func=calculate,
    description="Useful for when you need to calculate mathematical expressions. Input should be a valid Python expression like '2 + 2' or '10 * 3.5'"
)

# Méthode 2: Tool avec classe wrapper
search = DuckDuckGoSearchAPIWrapper()
search_tool = Tool(
    name="Web_Search",
    func=search.run,
    description="Useful for when you need to search the web for current information. Input should be a search query string."
)

# Méthode 3: Tool personnalisé
def get_weather(location: str) -> str:
    """Get current weather for a location."""
    # En production: utiliser une vraie API météo
    mock_data = {
        "Paris": "15°C, partly cloudy",
        "London": "12°C, rainy",
        "Tokyo": "20°C, sunny"
    }
    return mock_data.get(location, f"Weather data not available for {location}")

weather_tool = Tool(
    name="Weather",
    func=get_weather,
    description="Get the current weather for a specific city. Input should be a city name like 'Paris' or 'Tokyo'."
)

# Liste des outils
tools = [calculator_tool, search_tool, weather_tool]
                

3.2 Outils Structurés avec StructuredTool

from langchain.tools import StructuredTool
from pydantic import BaseModel, Field
from typing import Optional

# Définir le schéma d'input avec Pydantic
class SearchInput(BaseModel):
    query: str = Field(description="The search query")
    num_results: int = Field(default=5, description="Number of results to return", ge=1, le=10)

def advanced_search(query: str, num_results: int = 5) -> str:
    """Perform an advanced web search."""
    # Simuler une recherche
    results = [f"Result {i+1} for '{query}'" for i in range(num_results)]
    return "\n".join(results)

# Créer l'outil structuré
advanced_search_tool = StructuredTool.from_function(
    func=advanced_search,
    name="Advanced_Search",
    description="Perform an advanced web search with configurable number of results",
    args_schema=SearchInput
)

# Autre exemple: Outil avec plusieurs paramètres
class EmailInput(BaseModel):
    to: str = Field(description="Recipient email address")
    subject: str = Field(description="Email subject")
    body: str = Field(description="Email body content")
    priority: Optional[str] = Field(default="normal", description="Email priority: low, normal, high")

def send_email(to: str, subject: str, body: str, priority: str = "normal") -> str:
    """Send an email."""
    return f"Email sent to {to} with subject '{subject}' (priority: {priority})"

email_tool = StructuredTool.from_function(
    func=send_email,
    name="Send_Email",
    description="Send an email to a recipient",
    args_schema=EmailInput
)
                

4. Créer un Agent avec AgentExecutor

from langchain.agents import create_react_agent, AgentExecutor
from langchain_openai import ChatOpenAI
from langchain import hub

# 1. Initialiser le LLM
llm = ChatOpenAI(
    model="gpt-4-turbo-preview",
    temperature=0,
    streaming=True  # Pour voir la réponse en temps réel
)

# 2. Récupérer un prompt ReAct depuis LangChain Hub
# Ou créer un prompt personnalisé
prompt = hub.pull("hwchase17/react")

# Prompt personnalisé (optionnel)
from langchain.prompts import PromptTemplate

custom_prompt = PromptTemplate.from_template("""Answer the following questions as best you can. You have access to the following tools:

{tools}

Use the following format:

Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [{tool_names}]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question

Begin!

Question: {input}
Thought: {agent_scratchpad}""")

# 3. Créer l'agent ReAct
agent = create_react_agent(
    llm=llm,
    tools=tools,
    prompt=prompt  # ou custom_prompt
)

# 4. Créer l'AgentExecutor
agent_executor = AgentExecutor(
    agent=agent,
    tools=tools,
    verbose=True,  # Afficher les étapes intermédiaires
    handle_parsing_errors=True,  # Gérer les erreurs de parsing
    max_iterations=10,  # Limite de sécurité
    max_execution_time=60,  # Timeout en secondes
    return_intermediate_steps=True  # Retourner les étapes pour debugging
)

# 5. Utiliser l'agent
if __name__ == "__main__":
    # Test 1: Calcul simple
    result1 = agent_executor.invoke({
        "input": "What is 15% of 250, then multiply by 3?"
    })
    print(f"\n{'='*70}")
    print("Final Answer:", result1["output"])
    print(f"{'='*70}\n")

    # Test 2: Recherche web
    result2 = agent_executor.invoke({
        "input": "Search for recent news about Claude AI and summarize the top 3 results"
    })
    print(f"\n{'='*70}")
    print("Final Answer:", result2["output"])
    print(f"{'='*70}\n")

    # Test 3: Multi-outils
    result3 = agent_executor.invoke({
        "input": "What's the weather in Paris? Also calculate how many days until Christmas 2024"
    })
    print(f"\n{'='*70}")
    print("Final Answer:", result3["output"])

    # Accéder aux étapes intermédiaires
    if result3.get("intermediate_steps"):
        print("\nIntermediate Steps:")
        for i, (action, observation) in enumerate(result3["intermediate_steps"], 1):
            print(f"\nStep {i}:")
            print(f"  Action: {action.tool} - {action.tool_input}")
            print(f"  Observation: {observation[:100]}...")
                
Avantages de LangChain Agents
  • Batteries incluses : Patterns agentiques pré-implémentés
  • Écosystème riche : 100+ intégrations d'outils et services
  • Abstractions propres : Code réutilisable et maintenable
  • Production-ready : Error handling, retries, callbacks intégrés

5. Callbacks pour Monitoring et Streaming

from langchain.callbacks.base import BaseCallbackHandler
from langchain.schema import AgentAction, AgentFinish, LLMResult
from typing import Any, Dict, List
import time

class CustomCallbackHandler(BaseCallbackHandler):
    """Callback personnalisé pour tracking détaillé"""

    def __init__(self):
        self.start_time = None
        self.llm_calls = 0
        self.tool_calls = 0
        self.total_tokens = 0

    def on_llm_start(self, serialized: Dict[str, Any], prompts: List[str], **kwargs: Any) -> None:
        """Appelé quand le LLM démarre"""
        self.llm_calls += 1
        print(f"\n🤖 LLM Call #{self.llm_calls}")
        print(f"   Prompt length: {len(prompts[0])} chars")

    def on_llm_end(self, response: LLMResult, **kwargs: Any) -> None:
        """Appelé quand le LLM termine"""
        if response.llm_output:
            tokens = response.llm_output.get("token_usage", {})
            self.total_tokens += tokens.get("total_tokens", 0)
            print(f"   Tokens used: {tokens.get('total_tokens', 0)}")

    def on_llm_error(self, error: Exception, **kwargs: Any) -> None:
        """Appelé en cas d'erreur LLM"""
        print(f"   ❌ LLM Error: {str(error)}")

    def on_tool_start(self, serialized: Dict[str, Any], input_str: str, **kwargs: Any) -> None:
        """Appelé quand un outil démarre"""
        self.tool_calls += 1
        tool_name = serialized.get("name", "Unknown")
        print(f"\n🔧 Tool Call #{self.tool_calls}: {tool_name}")
        print(f"   Input: {input_str[:100]}...")

    def on_tool_end(self, output: str, **kwargs: Any) -> None:
        """Appelé quand un outil termine"""
        print(f"   Output: {output[:100]}...")

    def on_tool_error(self, error: Exception, **kwargs: Any) -> None:
        """Appelé en cas d'erreur d'outil"""
        print(f"   ❌ Tool Error: {str(error)}")

    def on_agent_action(self, action: AgentAction, **kwargs: Any) -> None:
        """Appelé quand l'agent décide d'une action"""
        print(f"\n💭 Agent Thought:")
        print(f"   Tool: {action.tool}")
        print(f"   Input: {action.tool_input}")
        if action.log:
            print(f"   Reasoning: {action.log[:150]}...")

    def on_agent_finish(self, finish: AgentFinish, **kwargs: Any) -> None:
        """Appelé quand l'agent termine"""
        print(f"\n✅ Agent Finished")
        print(f"   Final Answer: {finish.return_values.get('output', '')[:150]}...")

    def get_stats(self) -> Dict:
        """Retourner les statistiques"""
        return {
            "llm_calls": self.llm_calls,
            "tool_calls": self.tool_calls,
            "total_tokens": self.total_tokens
        }


# Callback pour streaming en temps réel
class StreamingCallbackHandler(BaseCallbackHandler):
    """Callback pour afficher la réponse token par token"""

    def on_llm_new_token(self, token: str, **kwargs: Any) -> None:
        """Appelé pour chaque nouveau token généré"""
        print(token, end="", flush=True)


# Utiliser les callbacks
custom_callback = CustomCallbackHandler()
streaming_callback = StreamingCallbackHandler()

agent_executor_with_callbacks = AgentExecutor(
    agent=agent,
    tools=tools,
    verbose=False,  # Désactiver le verbose par défaut
    callbacks=[custom_callback, streaming_callback],
    return_intermediate_steps=True
)

# Exécuter
print("\n" + "="*70)
print("Running agent with custom callbacks...")
print("="*70)

result = agent_executor_with_callbacks.invoke({
    "input": "What's 25% of 80, then search for info about Python"
})

# Afficher les stats
print("\n\n" + "="*70)
print("📊 Execution Statistics:")
print("="*70)
stats = custom_callback.get_stats()
for key, value in stats.items():
    print(f"  {key}: {value}")
print("="*70)
                

6. Memory Integration

from langchain.memory import ConversationBufferMemory, ConversationSummaryMemory
from langchain.agents import initialize_agent, AgentType

# Memory type 1: Buffer (garde tout)
buffer_memory = ConversationBufferMemory(
    memory_key="chat_history",
    return_messages=True
)

# Memory type 2: Summary (résume automatiquement)
summary_memory = ConversationSummaryMemory(
    llm=llm,
    memory_key="chat_history",
    return_messages=True
)

# Créer un agent avec mémoire
agent_with_memory = initialize_agent(
    tools=tools,
    llm=llm,
    agent=AgentType.CHAT_CONVERSATIONAL_REACT_DESCRIPTION,
    memory=buffer_memory,
    verbose=True,
    handle_parsing_errors=True
)

# Conversation avec mémoire
print("\nConversation 1:")
result1 = agent_with_memory.invoke({
    "input": "My name is Alice and I live in Paris"
})
print(f"Agent: {result1['output']}\n")

print("Conversation 2:")
result2 = agent_with_memory.invoke({
    "input": "What's the weather in my city?"
})
print(f"Agent: {result2['output']}\n")

print("Conversation 3:")
result3 = agent_with_memory.invoke({
    "input": "What's my name?"
})
print(f"Agent: {result3['output']}\n")

# L'agent se souvient que Alice vit à Paris!
                

7. Agents Spécialisés de LangChain

Type d'Agent Description Cas d'usage
ZERO_SHOT_REACT_DESCRIPTION Agent ReAct standard Usage général
CHAT_CONVERSATIONAL_REACT_DESCRIPTION ReAct avec mémoire conversationnelle Chatbots, assistants
STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION ReAct avec outils structurés Outils complexes multi-paramètres
OPENAI_FUNCTIONS Utilise l'API Function Calling d'OpenAI Performance optimale avec OpenAI
SELF_ASK_WITH_SEARCH Agent qui se pose des sous-questions Recherche complexe, reasoning
REACT_DOCSTORE ReAct spécialisé pour chercher dans des documents Q&A sur documentation

8. Output Parsers Personnalisés

from langchain.schema import AgentAction, AgentFinish
from langchain.agents.output_parsers import ReActSingleInputOutputParser
import re

class CustomReActParser(ReActSingleInputOutputParser):
    """Parser personnalisé pour format ReAct modifié"""

    def parse(self, text: str):
        """Parser le texte de sortie du LLM"""

        # Chercher "Final Answer:"
        if "Final Answer:" in text:
            return AgentFinish(
                return_values={"output": text.split("Final Answer:")[-1].strip()},
                log=text
            )

        # Parser Action et Action Input (format personnalisé)
        # Supporter plusieurs formats pour plus de robustesse

        # Format 1: Action: XXX\nAction Input: YYY
        action_match = re.search(r"Action:\s*(.+?)(?:\n|$)", text, re.IGNORECASE)
        input_match = re.search(r"Action Input:\s*(.+?)(?:\n|$)", text, re.IGNORECASE | re.DOTALL)

        if action_match and input_match:
            action = action_match.group(1).strip()
            action_input = input_match.group(1).strip()

            return AgentAction(
                tool=action,
                tool_input=action_input,
                log=text
            )

        # Format 2: [ACTION: XXX](INPUT: YYY)
        alt_match = re.search(r"\[ACTION:\s*(.+?)\]\(INPUT:\s*(.+?)\)", text, re.DOTALL)
        if alt_match:
            return AgentAction(
                tool=alt_match.group(1).strip(),
                tool_input=alt_match.group(2).strip(),
                log=text
            )

        # Si aucun format reconnu, erreur
        raise ValueError(f"Could not parse LLM output: {text}")


# Utiliser le parser personnalisé
from langchain.agents import create_react_agent

custom_parser = CustomReActParser()

agent_with_custom_parser = create_react_agent(
    llm=llm,
    tools=tools,
    prompt=custom_prompt,
    output_parser=custom_parser
)

executor = AgentExecutor(
    agent=agent_with_custom_parser,
    tools=tools,
    verbose=True
)
                
Conseil du Mentor

LangChain est excellent pour prototyper rapidement, mais pour la production critique, considérez implémenter votre propre logique d'agent. LangChain ajoute des abstractions qui peuvent rendre le debugging difficile. Utilisez les callbacks extensivement pour comprendre ce qui se passe sous le capot.

9. Exemple Complet: Agent de Recherche

from langchain.agents import Tool, AgentExecutor, create_react_agent
from langchain_openai import ChatOpenAI
from langchain.prompts import PromptTemplate
from langchain.callbacks import StdOutCallbackHandler
from langchain.utilities import DuckDuckGoSearchAPIWrapper, WikipediaAPIWrapper

# Outils de recherche
search = DuckDuckGoSearchAPIWrapper()
wikipedia = WikipediaAPIWrapper()

tools = [
    Tool(
        name="Web_Search",
        func=search.run,
        description="Search the web for current information. Use this for recent events, news, current data."
    ),
    Tool(
        name="Wikipedia",
        func=wikipedia.run,
        description="Search Wikipedia for factual, historical information. Use this for well-established facts."
    ),
    Tool(
        name="Calculator",
        func=lambda x: str(eval(x)),
        description="Calculate mathematical expressions. Input must be a valid Python expression."
    )
]

# LLM
llm = ChatOpenAI(model="gpt-4-turbo-preview", temperature=0)

# Prompt optimisé pour la recherche
research_prompt = PromptTemplate.from_template("""You are a research assistant. Your goal is to find accurate, up-to-date information.

Tools available:
{tools}

When researching:
1. Use Web_Search for current events and recent data
2. Use Wikipedia for established facts and historical info
3. Use Calculator for any numerical computations
4. Always verify information from multiple sources when possible
5. Cite your sources in the final answer

Format:
Question: the research question
Thought: think about the best approach
Action: choose a tool from [{tool_names}]
Action Input: input for the tool
Observation: result from the tool
... (repeat as needed)
Thought: I have enough information to answer
Final Answer: comprehensive answer with sources

Question: {input}
{agent_scratchpad}""")

# Créer l'agent
agent = create_react_agent(llm=llm, tools=tools, prompt=research_prompt)

executor = AgentExecutor(
    agent=agent,
    tools=tools,
    verbose=True,
    max_iterations=7,
    return_intermediate_steps=True,
    handle_parsing_errors=True
)

# Tester avec une question de recherche complexe
question = """Research and answer: What was the population of Paris in 2020,
and how much has it changed compared to 2010? Also calculate the percentage change."""

result = executor.invoke({"input": question})

print("\n" + "="*70)
print("RESEARCH RESULT")
print("="*70)
print(result["output"])
print("="*70)
                

LangGraph : Graphes d'Agents

Objectifs d'apprentissage
  • Maîtriser StateGraph pour créer des workflows d'agents
  • Implémenter des nœuds, edges et routage conditionnel
  • Utiliser les checkpoints pour la persistence
  • Créer des workflows avec cycles et human-in-the-loop

1. Introduction à LangGraph

LangGraph est une extension de LangChain qui permet de créer des workflows d'agents sous forme de graphes. Contrairement aux agents classiques (chaîne linéaire), LangGraph permet des cycles, des branches conditionnelles, et des workflows complexes.

┌────────────────────────────────────────────────────────────────┐
│              LANGGRAPH vs AGENT CLASSIQUE                      │
├────────────────────────────────────────────────────────────────┤
│                                                                │
│  AGENT CLASSIQUE (Linéaire):                                  │
│  ┌──────┐   ┌──────┐   ┌──────┐   ┌──────┐                   │
│  │Input │──►│ LLM  │──►│Tool  │──►│Output│                   │
│  └──────┘   └──────┘   └──────┘   └──────┘                   │
│                                                                │
│  LANGGRAPH (Graphe):                                           │
│                   ┌─────────┐                                  │
│                   │  Input  │                                  │
│                   └────┬────┘                                  │
│                        │                                       │
│              ┌─────────▼─────────┐                             │
│              │   Analyze Query   │                             │
│              └────┬────────┬─────┘                             │
│                   │        │                                   │
│         Simple    │        │    Complex                        │
│                   │        │                                   │
│           ┌───────▼──┐  ┌──▼────────┐                         │
│           │  Direct  │  │   Plan    │                         │
│           │  Answer  │  │  & Execute│                         │
│           └────┬─────┘  └───┬───────┘                         │
│                │            │                                  │
│                │      ┌─────▼─────┐                            │
│                │      │ Tool Call │                            │
│                │      └─────┬─────┘                            │
│                │            │                                  │
│                │      ┌─────▼─────┐       ┌──────────┐        │
│                │      │  Review   │──No──►│ Revise   │        │
│                │      │  Result   │       └────┬─────┘        │
│                │      └─────┬─────┘            │              │
│                │            │ Yes              │              │
│                │            │◄─────────────────┘              │
│                └────────┬───┘                                  │
│                         │                                      │
│                    ┌────▼────┐                                 │
│                    │  Output │                                 │
│                    └─────────┘                                 │
│                                                                │
│  Avantages: Cycles, Conditions, Parallélisme, Checkpoints     │
└────────────────────────────────────────────────────────────────┘
                

2. Installation et Setup

# Installer LangGraph
pip install langgraph langchain langchain-openai

# Imports essentiels
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.memory import MemorySaver
from typing import TypedDict, Annotated, Sequence
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
from langchain_openai import ChatOpenAI
import operator
                

3. Concepts de Base: State, Nodes, Edges

3.1 Définir l'État (State)

from typing import TypedDict, Annotated
import operator

# L'état est une structure de données partagée entre tous les nœuds
class AgentState(TypedDict):
    """État partagé dans le graphe"""

    # Messages de la conversation
    messages: Annotated[list, operator.add]  # operator.add pour append automatique

    # Autres champs
    current_task: str
    completed_tasks: list[str]
    iteration: int
    max_iterations: int
    final_answer: str | None


# État minimal pour commencer
class SimpleState(TypedDict):
    messages: Annotated[list, operator.add]
                

3.2 Créer des Nœuds (Nodes)

from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4-turbo-preview", temperature=0)

# Nœud 1: Appeler le LLM
def call_model(state: AgentState) -> AgentState:
    """Nœud qui appelle le LLM"""
    messages = state["messages"]
    response = llm.invoke(messages)

    # Retourner l'état modifié
    return {
        "messages": [response],  # Sera ajouté à la liste grâce à operator.add
        "iteration": state.get("iteration", 0) + 1
    }

# Nœud 2: Exécuter un outil
def execute_tool(state: AgentState) -> AgentState:
    """Nœud qui exécute un outil"""
    last_message = state["messages"][-1]

    # Extraire l'outil à appeler depuis le message
    tool_name = "search"  # Simplifié
    tool_input = "AI agents"

    # Simuler l'exécution
    tool_result = f"Results for {tool_input}..."

    # Créer un message avec le résultat
    result_message = HumanMessage(content=f"Tool result: {tool_result}")

    return {
        "messages": [result_message],
        "completed_tasks": state.get("completed_tasks", []) + [tool_name]
    }

# Nœud 3: Vérifier si terminé
def should_continue(state: AgentState) -> str:
    """Nœud de décision (routage conditionnel)"""
    last_message = state["messages"][-1]

    # Si le message contient "FINAL ANSWER", on termine
    if "FINAL ANSWER" in last_message.content:
        return "end"

    # Si trop d'itérations, on termine
    if state.get("iteration", 0) >= state.get("max_iterations", 10):
        return "end"

    # Sinon, on continue
    return "continue"
                

4. Construire un Graphe Simple

from langgraph.graph import StateGraph, END

# 1. Créer le graphe
workflow = StateGraph(AgentState)

# 2. Ajouter les nœuds
workflow.add_node("model", call_model)
workflow.add_node("tool", execute_tool)

# 3. Définir le point d'entrée
workflow.set_entry_point("model")

# 4. Ajouter des edges (transitions)

# Edge simple: model → tool
workflow.add_edge("model", "tool")

# Edge conditionnel: tool → ?
workflow.add_conditional_edges(
    "tool",  # Depuis ce nœud
    should_continue,  # Fonction de décision
    {
        "continue": "model",  # Si "continue", retour à model
        "end": END  # Si "end", terminer
    }
)

# 5. Compiler le graphe
app = workflow.compile()

# 6. Utiliser le graphe
initial_state = {
    "messages": [HumanMessage(content="Search for information about AI agents")],
    "iteration": 0,
    "max_iterations": 5,
    "completed_tasks": []
}

# Exécuter le graphe
result = app.invoke(initial_state)

print("Final State:")
print(f"Iterations: {result['iteration']}")
print(f"Completed tasks: {result['completed_tasks']}")
print(f"Last message: {result['messages'][-1].content}")
                
Avantages de LangGraph
  • Flexibilité : Créer des workflows arbitrairement complexes
  • Cycles : Permettre des boucles de rétroaction (impossible avec chains)
  • Routage conditionnel : Décisions dynamiques sur le prochain nœud
  • Checkpoints : Sauvegarder et reprendre l'état à n'importe quel point
  • Human-in-the-loop : Interrompre pour intervention humaine

5. Workflow Avancé: Agent ReAct avec LangGraph

from langchain.tools import Tool
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage, ToolMessage
import json

# Définir les outils
def search_tool(query: str) -> str:
    """Recherche web simulée"""
    return f"Search results for: {query}"

def calculator_tool(expression: str) -> str:
    """Calculatrice"""
    try:
        return str(eval(expression))
    except:
        return "Error in calculation"

tools = {
    "search": search_tool,
    "calculator": calculator_tool
}

# État pour agent ReAct
class ReActState(TypedDict):
    messages: Annotated[list[BaseMessage], operator.add]
    iteration: int

# Nœud: Agent décide de l'action
def agent_node(state: ReActState) -> ReActState:
    """L'agent décide quoi faire"""
    messages = state["messages"]

    # Ajouter le système prompt si premier appel
    if len(messages) == 1:
        system_msg = SystemMessage(content=f"""You are a helpful agent with tools.

Available tools:
- search(query): Search the web
- calculator(expression): Calculate math expressions

Use this format:
Thought: [your reasoning]
Action: [tool_name]
Action Input: [input for tool]

Or when done:
Thought: I have the answer
Final Answer: [your answer]""")
        messages = [system_msg] + messages

    response = llm.invoke(messages)

    return {
        "messages": [response],
        "iteration": state.get("iteration", 0) + 1
    }

# Nœud: Exécuter l'outil
def tool_node(state: ReActState) -> ReActState:
    """Exécuter l'outil demandé par l'agent"""
    last_message = state["messages"][-1]

    # Parser l'action depuis le message
    content = last_message.content

    import re
    action_match = re.search(r'Action:\s*(\w+)', content)
    input_match = re.search(r'Action Input:\s*(.+?)(?:\n|$)', content, re.DOTALL)

    if action_match and input_match:
        tool_name = action_match.group(1).strip()
        tool_input = input_match.group(1).strip()

        print(f"\n🔧 Executing {tool_name}({tool_input})")

        # Exécuter l'outil
        if tool_name in tools:
            result = tools[tool_name](tool_input)
        else:
            result = f"Unknown tool: {tool_name}"

        print(f"📊 Result: {result}")

        # Créer un message avec le résultat
        observation_msg = HumanMessage(content=f"Observation: {result}")

        return {"messages": [observation_msg]}

    # Si pas d'action trouvée, retourner un message d'erreur
    return {"messages": [HumanMessage(content="Error: No valid action found")]}

# Fonction de routage
def should_continue_react(state: ReActState) -> str:
    """Décider si on continue ou termine"""
    last_message = state["messages"][-1]

    # Si c'est la réponse finale
    if "Final Answer:" in last_message.content:
        return "end"

    # Si trop d'itérations
    if state.get("iteration", 0) >= 10:
        return "end"

    # Si c'est un message de l'agent (pas un tool result), exécuter l'outil
    if isinstance(last_message, AIMessage):
        return "tool"

    # Sinon, retour à l'agent
    return "agent"

# Construire le graphe ReAct
react_workflow = StateGraph(ReActState)

react_workflow.add_node("agent", agent_node)
react_workflow.add_node("tool", tool_node)

react_workflow.set_entry_point("agent")

react_workflow.add_conditional_edges(
    "agent",
    should_continue_react,
    {
        "tool": "tool",
        "end": END
    }
)

react_workflow.add_edge("tool", "agent")  # Toujours retourner à agent après outil

# Compiler
react_app = react_workflow.compile()

# Tester
print("\n" + "="*70)
print("TESTING REACT AGENT WITH LANGGRAPH")
print("="*70)

result = react_app.invoke({
    "messages": [HumanMessage(content="Search for AI agents, then calculate 25% of 80")],
    "iteration": 0
})

print("\n" + "="*70)
print("FINAL RESULT")
print("="*70)
print(f"Iterations: {result['iteration']}")
print(f"Last message: {result['messages'][-1].content}")
                

6. Checkpoints et Persistence

Les checkpoints permettent de sauvegarder l'état à chaque étape et de reprendre l'exécution plus tard.

from langgraph.checkpoint.memory import MemorySaver

# Créer un saver pour persister l'état
memory = MemorySaver()

# Compiler avec checkpoints
app_with_checkpoints = react_workflow.compile(checkpointer=memory)

# Configuration avec un thread_id pour identifier la session
config = {"configurable": {"thread_id": "conversation_1"}}

# Première exécution
print("=== First Run ===")
result1 = app_with_checkpoints.invoke(
    {
        "messages": [HumanMessage(content="Search for Python")],
        "iteration": 0
    },
    config=config
)

print(f"After first run: {result1['iteration']} iterations")

# Deuxième exécution (reprend où la première s'est arrêtée)
print("\n=== Continue from checkpoint ===")
result2 = app_with_checkpoints.invoke(
    {
        "messages": [HumanMessage(content="Now calculate 10 * 5")]
    },
    config=config
)

print(f"After second run: {result2['iteration']} iterations (cumulative)")

# Récupérer l'historique complet
history = memory.list(config)
print(f"\nCheckpoint history: {len(list(history))} checkpoints saved")
                

7. Human-in-the-Loop

from langgraph.graph import StateGraph, END
from langgraph.checkpoint.memory import MemorySaver

class HITLState(TypedDict):
    messages: Annotated[list[BaseMessage], operator.add]
    needs_approval: bool
    approved: bool

def process_task(state: HITLState) -> HITLState:
    """Traiter une tâche qui nécessite potentiellement une approbation"""
    messages = state["messages"]
    last_msg = messages[-1].content

    # Simuler un traitement
    result = f"Processed: {last_msg}"

    # Si tâche sensible, demander approbation
    if "delete" in last_msg.lower() or "send" in last_msg.lower():
        return {
            "messages": [AIMessage(content=result)],
            "needs_approval": True,
            "approved": False
        }

    return {
        "messages": [AIMessage(content=result)],
        "needs_approval": False,
        "approved": True
    }

def human_approval_node(state: HITLState) -> HITLState:
    """Nœud d'attente d'approbation humaine"""
    print("\n⏸️  WAITING FOR HUMAN APPROVAL")
    print(f"Task: {state['messages'][-1].content}")

    # En production, ceci serait une vraie interruption
    # Ici on simule une approbation automatique
    approval = input("Approve? (yes/no): ").lower() == "yes"

    return {"approved": approval}

def should_wait_for_human(state: HITLState) -> str:
    """Décider si on attend l'humain"""
    if state.get("needs_approval", False) and not state.get("approved", False):
        return "wait_human"
    return "continue"

def finalize(state: HITLState) -> HITLState:
    """Finaliser la tâche"""
    if state.get("approved", True):
        return {"messages": [AIMessage(content="Task completed successfully")]}
    else:
        return {"messages": [AIMessage(content="Task cancelled by user")]}

# Construire le graphe avec HITL
hitl_workflow = StateGraph(HITLState)

hitl_workflow.add_node("process", process_task)
hitl_workflow.add_node("human_approval", human_approval_node)
hitl_workflow.add_node("finalize", finalize)

hitl_workflow.set_entry_point("process")

hitl_workflow.add_conditional_edges(
    "process",
    should_wait_for_human,
    {
        "wait_human": "human_approval",
        "continue": "finalize"
    }
)

hitl_workflow.add_edge("human_approval", "finalize")
hitl_workflow.add_edge("finalize", END)

# Compiler avec checkpoints (important pour HITL)
memory = MemorySaver()
hitl_app = hitl_workflow.compile(checkpointer=memory)

# Tester
config = {"configurable": {"thread_id": "hitl_session_1"}}

# Tâche sensible nécessitant approbation
result = hitl_app.invoke(
    {"messages": [HumanMessage(content="Delete all old files")]},
    config=config
)

print(f"\nFinal result: {result['messages'][-1].content}")
                

8. Workflow Parallèle

from langgraph.graph import StateGraph, END
from concurrent.futures import ThreadPoolExecutor

class ParallelState(TypedDict):
    query: str
    web_results: str
    db_results: str
    wiki_results: str
    final_answer: str

def search_web(state: ParallelState) -> ParallelState:
    """Recherche web (peut être lent)"""
    import time
    time.sleep(1)  # Simuler latence
    return {"web_results": f"Web results for: {state['query']}"}

def search_database(state: ParallelState) -> ParallelState:
    """Recherche DB (peut être lent)"""
    import time
    time.sleep(1)
    return {"db_results": f"DB results for: {state['query']}"}

def search_wikipedia(state: ParallelState) -> ParallelState:
    """Recherche Wikipedia"""
    import time
    time.sleep(1)
    return {"wiki_results": f"Wikipedia results for: {state['query']}"}

def aggregate_results(state: ParallelState) -> ParallelState:
    """Agréger tous les résultats"""
    combined = f"""
Web: {state.get('web_results', 'N/A')}
DB: {state.get('db_results', 'N/A')}
Wiki: {state.get('wiki_results', 'N/A')}
    """
    return {"final_answer": combined}

# Construire le graphe parallèle
parallel_workflow = StateGraph(ParallelState)

# Ajouter les nœuds
parallel_workflow.add_node("web", search_web)
parallel_workflow.add_node("db", search_database)
parallel_workflow.add_node("wiki", search_wikipedia)
parallel_workflow.add_node("aggregate", aggregate_results)

# Point d'entrée
parallel_workflow.set_entry_point("web")

# Exécuter web, db, wiki en parallèle
# (En pratique, ils démarrent tous depuis l'entrée)
parallel_workflow.add_edge("web", "aggregate")
parallel_workflow.add_edge("db", "aggregate")
parallel_workflow.add_edge("wiki", "aggregate")
parallel_workflow.add_edge("aggregate", END)

# Pour vraiment paralléliser, il faut démarrer tous les nœuds en même temps
# Une approche est d'avoir un nœud "dispatcher"
def dispatch(state: ParallelState) -> ParallelState:
    return state

parallel_workflow.set_entry_point("web")  # Simplification

# Compiler et tester
import time

parallel_app = parallel_workflow.compile()

start = time.time()
result = parallel_app.invoke({"query": "AI agents"})
duration = time.time() - start

print(f"\nParallel execution took: {duration:.2f}s")
print(f"Final answer:\n{result.get('final_answer', 'N/A')}")
                

9. Visualiser le Graphe

# LangGraph peut générer une visualisation du graphe
from IPython.display import Image, display

# Obtenir la représentation PNG du graphe
try:
    graph_image = react_app.get_graph().draw_png()
    display(Image(graph_image))
except Exception as e:
    print(f"Could not display graph: {e}")
    print("Install graphviz: pip install pygraphviz")

# Ou obtenir la représentation texte
print("\nGraph structure:")
print(react_app.get_graph().draw_ascii())
                
Conseil du Mentor

LangGraph est puissant pour des workflows complexes, mais n'en faites pas trop. Commencez simple avec des agents linéaires, puis ajoutez de la complexité (cycles, branches) seulement quand nécessaire. Les graphes complexes sont difficiles à débugger. Utilisez toujours les checkpoints en production pour pouvoir reprendre après une erreur.

10. Cas d'usage LangGraph

Cas d'usage Pattern Avantage LangGraph
Agent avec retry Cycle: agent → tool → validate → agent Boucle de correction automatique
Multi-agent coordination Branches: supervisor → [agent1, agent2, agent3] → synthesis Orchestration parallèle
Human approval workflow Interruption: process → approval → continue Checkpoints natifs
Plan-Execute-Revise Cycle: plan → execute → evaluate → replan Loops conditionnels
Recherche itérative Cycle: query → search → refine → search Amélioration progressive

Smolagents (Hugging Face)

Objectifs d'apprentissage
  • Maîtriser les agents Hugging Face Smolagents
  • Utiliser ToolCallingAgent pour des tâches code-first
  • Implémenter ManagedAgent pour la collaboration
  • Créer des outils personnalisés avec @tool decorator

1. Introduction à Smolagents

Smolagents (anciennement Transformers Agents) est le framework d'agents de Hugging Face. Contrairement à LangChain qui est agnostique au modèle, Smolagents est optimisé pour les modèles Hugging Face et adopte une approche code-first.

┌────────────────────────────────────────────────────────────────┐
│                  SMOLAGENTS ARCHITECTURE                       │
├────────────────────────────────────────────────────────────────┤
│                                                                │
│                    User Query                                  │
│                         │                                      │
│                         ▼                                      │
│              ┌──────────────────┐                              │
│              │  ToolCallingAgent│                              │
│              │  (Code-based)    │                              │
│              └────────┬─────────┘                              │
│                       │                                        │
│         ┌─────────────┼─────────────┐                          │
│         ▼             ▼             ▼                          │
│   ┌─────────┐   ┌─────────┐   ┌─────────┐                    │
│   │  LLM    │   │  Tools  │   │ Code    │                    │
│   │(Llama,  │   │(HF Hub) │   │Executor │                    │
│   │Mistral) │   │         │   │(Python) │                    │
│   └─────────┘   └─────────┘   └─────────┘                    │
│                                                                │
│  Différence clé: L'agent génère du CODE Python                │
│  au lieu de simple JSON function calling                      │
│                                                                │
│  Exemple:                                                      │
│  Agent: result = search_tool("AI agents")                     │
│         answer = analyze(result)                              │
│         return answer                                          │
│                                                                │
└────────────────────────────────────────────────────────────────┘
                

2. Installation et Setup

# Installer smolagents
pip install smolagents

# Dépendances optionnelles
pip install transformers torch accelerate
pip install duckduckgo-search  # Pour web search

# Imports
from smolagents import (
    CodeAgent,
    ToolCallingAgent,
    ManagedAgent,
    tool,
    HfApiModel,
    LiteLLMModel
)
                

3. Créer des Outils avec @tool

from smolagents import tool
import requests

# Méthode 1: Tool simple avec décorateur
@tool
def get_weather(location: str) -> str:
    """
    Get the current weather for a location.

    Args:
        location: The city name (e.g., 'Paris', 'London')

    Returns:
        A string describing the weather
    """
    # Simuler un appel API météo
    # En production: utiliser OpenWeatherMap API
    weather_data = {
        "Paris": "15°C, partly cloudy",
        "London": "12°C, rainy",
        "Tokyo": "20°C, sunny",
        "New York": "10°C, clear"
    }

    result = weather_data.get(location, f"Weather data not available for {location}")
    return result


# Méthode 2: Tool avec calculs
@tool
def calculate_percentage(value: float, percentage: float) -> float:
    """
    Calculate a percentage of a value.

    Args:
        value: The base value
        percentage: The percentage to calculate (e.g., 25 for 25%)

    Returns:
        The calculated result
    """
    result = (value * percentage) / 100
    return result


# Méthode 3: Tool complexe avec API réelle
@tool
def web_search(query: str, max_results: int = 5) -> str:
    """
    Search the web for information.

    Args:
        query: The search query string
        max_results: Maximum number of results to return (default: 5)

    Returns:
        A formatted string with search results
    """
    try:
        from duckduckgo_search import DDGS

        with DDGS() as ddgs:
            results = list(ddgs.text(query, max_results=max_results))

        formatted_results = []
        for i, result in enumerate(results, 1):
            formatted_results.append(
                f"{i}. {result['title']}\n   {result['body'][:150]}...\n   URL: {result['href']}"
            )

        return "\n\n".join(formatted_results)

    except Exception as e:
        return f"Search error: {str(e)}"


# Méthode 4: Tool avec état
class CounterTool:
    """Tool qui maintient un état interne"""

    def __init__(self):
        self.count = 0

    @tool
    def increment_counter(self) -> int:
        """
        Increment an internal counter and return the new value.

        Returns:
            The new counter value
        """
        self.count += 1
        return self.count

    @tool
    def get_count(self) -> int:
        """
        Get the current counter value.

        Returns:
            The current count
        """
        return self.count


counter = CounterTool()

# Liste des outils
tools = [
    get_weather,
    calculate_percentage,
    web_search,
    counter.increment_counter,
    counter.get_count
]
                
Bonnes Pratiques pour les Tools
  • Docstring claire : Le LLM utilise la docstring pour comprendre l'outil
  • Type hints : Toujours annoter les types des paramètres
  • Description des Args : Expliquer chaque paramètre dans la docstring
  • Retour explicite : Toujours documenter ce que retourne la fonction
  • Error handling : Gérer les exceptions et retourner des messages clairs

4. ToolCallingAgent

Le ToolCallingAgent est l'agent principal de Smolagents. Il génère du code Python pour utiliser les outils.

from smolagents import ToolCallingAgent, HfApiModel
import os

# Option 1: Utiliser un modèle Hugging Face
model = HfApiModel(
    model_id="meta-llama/Meta-Llama-3.1-70B-Instruct",
    token=os.getenv("HF_TOKEN")
)

# Option 2: Utiliser OpenAI via LiteLLM
from smolagents import LiteLLMModel

openai_model = LiteLLMModel(
    model_id="gpt-4-turbo-preview",
    api_key=os.getenv("OPENAI_API_KEY")
)

# Créer l'agent
agent = ToolCallingAgent(
    tools=tools,
    model=openai_model,  # ou model
    max_steps=10,
    verbosity_level=2  # 0=silent, 1=info, 2=debug
)

# Utiliser l'agent
if __name__ == "__main__":
    print("="*70)
    print("SMOLAGENTS - ToolCallingAgent Demo")
    print("="*70 + "\n")

    # Test 1: Tâche simple
    result1 = agent.run("What's the weather in Tokyo?")
    print(f"\nResult 1: {result1}\n")

    # Test 2: Calcul
    result2 = agent.run("Calculate 15% of 250")
    print(f"\nResult 2: {result2}\n")

    # Test 3: Multi-outils
    result3 = agent.run(
        "Search for information about Python programming, "
        "then tell me what's the weather in Paris"
    )
    print(f"\nResult 3: {result3}\n")

    # Test 4: Tâche complexe avec état
    result4 = agent.run(
        "Increment the counter, then increment it again, "
        "then tell me what the count is"
    )
    print(f"\nResult 4: {result4}\n")
                

5. Code Execution et Sécurité

Smolagents exécute du code Python généré par le LLM. C'est puissant mais potentiellement dangereux.

# L'agent génère du code comme ceci:
"""
# Agent-generated code
weather_result = get_weather("Paris")
percentage_result = calculate_percentage(100, 15)
final_answer = f"Weather: {weather_result}, 15% of 100 is {percentage_result}"
"""

# Ce code est exécuté dans un environnement Python

# ⚠️ SÉCURITÉ: En production, utilisez un sandbox
# Options de sandboxing:

# 1. Docker container avec ressources limitées
from smolagents import DockerExecutor

docker_executor = DockerExecutor(
    image="python:3.11-slim",
    timeout=30,  # Timeout en secondes
    memory_limit="512m"
)

agent_with_docker = ToolCallingAgent(
    tools=tools,
    model=openai_model,
    code_executor=docker_executor
)

# 2. Restricted Python (RestrictedPython)
from smolagents import RestrictedExecutor

restricted_executor = RestrictedExecutor(
    allowed_imports=["math", "datetime", "json"],
    timeout=10
)

agent_secure = ToolCallingAgent(
    tools=tools,
    model=openai_model,
    code_executor=restricted_executor
)

# 3. E2B (Sandboxed cloud execution)
# pip install e2b
from smolagents import E2BExecutor

e2b_executor = E2BExecutor(
    api_key=os.getenv("E2B_API_KEY"),
    timeout=60
)

agent_e2b = ToolCallingAgent(
    tools=tools,
    model=openai_model,
    code_executor=e2b_executor
)
                
Sécurité Critique

JAMAIS exécuter du code généré par LLM en production sans sandbox. Un attacker pourrait faire générer du code malveillant via prompt injection. Utilisez toujours Docker, E2B, ou un environnement isolé avec restrictions strictes.

6. ManagedAgent pour Collaboration

Un ManagedAgent est un agent qui peut être utilisé comme outil par un autre agent (agent multi-niveaux).

from smolagents import ToolCallingAgent, ManagedAgent, tool

# Créer des agents spécialisés

# Agent 1: Spécialiste web search
@tool
def specialized_search(query: str) -> str:
    """Advanced web search with analysis"""
    # Logique de recherche avancée
    return f"Advanced search results for: {query}"

search_agent = ToolCallingAgent(
    tools=[specialized_search, web_search],
    model=openai_model,
    name="SearchAgent",
    description="Expert in web search and information retrieval"
)

# Agent 2: Spécialiste calculs
@tool
def advanced_calculation(expression: str) -> str:
    """Perform complex mathematical calculations"""
    try:
        import numpy as np
        # Permettre numpy dans eval
        result = eval(expression, {"np": np, "__builtins__": {}})
        return str(result)
    except Exception as e:
        return f"Calculation error: {str(e)}"

calc_agent = ToolCallingAgent(
    tools=[advanced_calculation, calculate_percentage],
    model=openai_model,
    name="CalculatorAgent",
    description="Expert in mathematical calculations and analysis"
)

# Convertir les agents en ManagedAgents (tools)
search_managed = ManagedAgent(
    agent=search_agent,
    name="search_specialist",
    description="Delegate to this agent for web search and research tasks"
)

calc_managed = ManagedAgent(
    agent=calc_agent,
    name="calculator_specialist",
    description="Delegate to this agent for mathematical calculations"
)

# Agent superviseur qui utilise les sous-agents
supervisor_agent = ToolCallingAgent(
    tools=[search_managed, calc_managed, get_weather],
    model=openai_model,
    name="SupervisorAgent"
)

# Utiliser le système multi-agents
if __name__ == "__main__":
    print("\n" + "="*70)
    print("MULTI-AGENT SYSTEM")
    print("="*70 + "\n")

    # Tâche qui nécessite plusieurs spécialistes
    result = supervisor_agent.run(
        "Search for the population of Paris in 2020, "
        "then calculate what 10% of that population would be"
    )

    print(f"\nFinal Result: {result}\n")

    # Le SupervisorAgent va:
    # 1. Déléguer la recherche au SearchAgent
    # 2. Déléguer le calcul au CalculatorAgent
    # 3. Synthétiser les résultats
                

7. Comparaison: Smolagents vs LangChain

Aspect Smolagents LangChain
Philosophie Code-first (génère Python) Prompt-first (JSON function calling)
Modèles optimisés Hugging Face (Llama, Mistral) OpenAI, Anthropic
Flexibilité Très élevée (code arbitraire) Limitée (outils prédéfinis)
Sécurité ⚠️ Nécessite sandbox strict ✅ Plus sûr (pas d'exec)
Performance Excellente avec Llama 3.1 Excellente avec GPT-4
Écosystème En croissance Très mature (1000+ intégrations)
Multi-agents ✅ ManagedAgent natif ✅ Via LangGraph
Meilleur pour Tâches code/data science Applications d'entreprise

8. Outils Avancés avec État

from dataclasses import dataclass
from typing import List, Dict
import json

@dataclass
class Task:
    id: int
    description: str
    completed: bool = False

class TaskManager:
    """Gestionnaire de tâches avec état persistant"""

    def __init__(self):
        self.tasks: List[Task] = []
        self.next_id = 1

    @tool
    def add_task(self, description: str) -> str:
        """
        Add a new task to the list.

        Args:
            description: Description of the task

        Returns:
            Confirmation message with task ID
        """
        task = Task(id=self.next_id, description=description)
        self.tasks.append(task)
        self.next_id += 1
        return f"Task added with ID {task.id}: {description}"

    @tool
    def list_tasks(self) -> str:
        """
        List all tasks with their status.

        Returns:
            Formatted list of tasks
        """
        if not self.tasks:
            return "No tasks found"

        result = "Current tasks:\n"
        for task in self.tasks:
            status = "✓" if task.completed else "○"
            result += f"{status} [{task.id}] {task.description}\n"
        return result

    @tool
    def complete_task(self, task_id: int) -> str:
        """
        Mark a task as completed.

        Args:
            task_id: The ID of the task to complete

        Returns:
            Confirmation message
        """
        for task in self.tasks:
            if task.id == task_id:
                task.completed = True
                return f"Task {task_id} marked as completed"

        return f"Task {task_id} not found"

    @tool
    def get_stats(self) -> str:
        """
        Get statistics about tasks.

        Returns:
            Task statistics
        """
        total = len(self.tasks)
        completed = sum(1 for t in self.tasks if t.completed)
        pending = total - completed

        return f"Total: {total}, Completed: {completed}, Pending: {pending}"


# Créer le manager et utiliser ses méthodes comme outils
task_manager = TaskManager()

agent_with_state = ToolCallingAgent(
    tools=[
        task_manager.add_task,
        task_manager.list_tasks,
        task_manager.complete_task,
        task_manager.get_stats
    ],
    model=openai_model
)

# Test avec état persistant
result1 = agent_with_state.run("Add a task: 'Buy groceries'")
print(result1)

result2 = agent_with_state.run("Add a task: 'Call dentist'")
print(result2)

result3 = agent_with_state.run("List all tasks")
print(result3)

result4 = agent_with_state.run("Complete task 1")
print(result4)

result5 = agent_with_state.run("Show me the statistics")
print(result5)
                

9. Streaming et Callbacks

from smolagents import ToolCallingAgent
from typing import Dict, Any

class CustomCallback:
    """Callback pour monitorer l'agent"""

    def __init__(self):
        self.steps = []
        self.tool_calls = []

    def on_step_start(self, step_info: Dict[str, Any]):
        """Appelé au début de chaque étape"""
        print(f"\n🔄 Step {step_info.get('step_num', '?')} starting...")
        self.steps.append(step_info)

    def on_tool_start(self, tool_name: str, tool_input: Any):
        """Appelé avant l'exécution d'un outil"""
        print(f"🔧 Calling tool: {tool_name}")
        print(f"   Input: {str(tool_input)[:100]}...")
        self.tool_calls.append({"tool": tool_name, "input": tool_input})

    def on_tool_end(self, tool_name: str, tool_output: Any):
        """Appelé après l'exécution d'un outil"""
        print(f"✓ Tool completed: {tool_name}")
        print(f"   Output: {str(tool_output)[:100]}...")

    def on_step_end(self, step_info: Dict[str, Any]):
        """Appelé à la fin de chaque étape"""
        print(f"✓ Step {step_info.get('step_num', '?')} completed\n")

    def get_stats(self) -> Dict:
        """Retourner les statistiques"""
        return {
            "total_steps": len(self.steps),
            "total_tool_calls": len(self.tool_calls),
            "tools_used": list(set(tc["tool"] for tc in self.tool_calls))
        }


# Utiliser avec callback
callback = CustomCallback()

agent_with_callback = ToolCallingAgent(
    tools=tools,
    model=openai_model,
    callbacks=[callback]
)

result = agent_with_callback.run(
    "Search for AI agents and calculate 25% of 80"
)

print("\n" + "="*70)
print("EXECUTION STATISTICS")
print("="*70)
stats = callback.get_stats()
for key, value in stats.items():
    print(f"{key}: {value}")
                
Conseil du Mentor

Smolagents est excellent pour les data scientists et les cas d'usage impliquant du code (data analysis, ML). Pour des applications business classiques, LangChain reste plus approprié. Si vous utilisez des modèles Hugging Face open source (Llama, Mistral), Smolagents est optimisé pour eux. Toujours utiliser un sandbox en production!

Quiz Module 6.1 : Fondamentaux des Agents

Objectifs du Quiz
  • Évaluer votre compréhension des concepts d'agents IA
  • Tester vos connaissances sur les patterns agentiques
  • Vérifier votre maîtrise des outils et frameworks
  • Score minimum requis : 70% (11/15 questions)

15 Questions - Module 6.1

Question 1: Définition d'un Agent IA

Quelle est la principale différence entre un chatbot classique et un agent IA autonome?

Question 2: Pattern ReAct

Dans le pattern ReAct, quel est l'ordre correct des étapes?

Question 3: Plan-and-Execute

Quand devriez-vous utiliser Plan-and-Execute au lieu de ReAct?

Question 4: Function Calling

Quel est l'avantage principal du Function Calling natif (OpenAI/Anthropic) par rapport au parsing manuel?

Question 5: Parallel Function Calling

Quel est l'avantage du parallel function calling?

Question 6: Mémoire Court Terme

Quelle stratégie de gestion de contexte est la plus simple mais perd le contexte ancien?

Question 7: Mémoire Long Terme

Quel type de stockage est recommandé pour la mémoire long terme avec recherche sémantique?

Question 8: LangChain AgentExecutor

Quel paramètre de l'AgentExecutor permet de limiter les boucles infinies?

Question 9: LangChain Callbacks

À quoi servent les callbacks dans LangChain?

Question 10: LangGraph - Avantage Principal

Quelle est la principale capacité que LangGraph ajoute par rapport aux agents LangChain classiques?

Question 11: LangGraph Checkpoints

À quoi servent les checkpoints dans LangGraph?

Question 12: Smolagents - Différence Clé

Quelle est la différence principale entre Smolagents et LangChain?

Question 13: Sécurité Smolagents

Pourquoi est-il CRITIQUE d'utiliser un sandbox avec Smolagents en production?

Question 14: Reflexion Pattern

Qu'est-ce que le pattern Reflexion ajoute à un agent?

Question 15: Best Practice Production

Quelle est la meilleure pratique ESSENTIELLE pour tous les agents en production?

Après le Quiz

Si vous obtenez moins de 70%, revoyez les leçons 0-6 avant de continuer vers le Module 6.2. La compréhension des fondamentaux est essentielle pour les systèmes multi-agents avancés!

CrewAI : Orchestration d'Équipes IA

Objectifs d'apprentissage
  • Comprendre le concept de Crew (équipe) d'agents IA
  • Créer des Agents spécialisés avec rôles et objectifs
  • Orchestrer des Tasks avec dépendances
  • Maîtriser les Process types (Sequential, Hierarchical, Consensus)

1. Introduction à CrewAI

CrewAI est un framework de pointe pour créer des équipes d'agents IA qui collaborent. Inspiré par les équipes humaines, chaque agent a un rôle, des objectifs et des compétences spécifiques.

┌────────────────────────────────────────────────────────────────┐
│                  ARCHITECTURE CREWAI                           │
├────────────────────────────────────────────────────────────────┤
│                                                                │
│                     ┌──────────┐                               │
│                     │   CREW   │                               │
│                     │(Manager) │                               │
│                     └────┬─────┘                               │
│                          │                                     │
│              ┌───────────┼───────────┐                         │
│              ▼           ▼           ▼                         │
│         ┌────────┐  ┌────────┐  ┌────────┐                    │
│         │Agent 1 │  │Agent 2 │  │Agent 3 │                    │
│         │Researcher  │Analyst│  │Writer │                    │
│         └───┬────┘  └───┬────┘  └───┬────┘                    │
│             │           │           │                          │
│             ▼           ▼           ▼                          │
│         ┌────────┐  ┌────────┐  ┌────────┐                    │
│         │Task 1  │  │Task 2  │  │Task 3  │                    │
│         │Research│  │Analyze │  │Write   │                    │
│         └────────┘  └────────┘  └────────┘                    │
│                                                                │
│  Process Flow:                                                 │
│  • Sequential: Task 1 → Task 2 → Task 3                       │
│  • Hierarchical: Manager delegates → Agents execute           │
│  • Consensus: All agents vote on decisions                    │
│                                                                │
└────────────────────────────────────────────────────────────────┘
                

2. Installation

pip install crewai crewai-tools

# Dépendances pour les outils
pip install duckduckgo-search selenium playwright
                

3. Créer votre Premier Crew

from crewai import Agent, Task, Crew, Process
from langchain_openai import ChatOpenAI
import os

# Configurer le LLM
llm = ChatOpenAI(
    model="gpt-4-turbo-preview",
    temperature=0.7,
    api_key=os.getenv("OPENAI_API_KEY")
)

# Agent 1: Researcher (Chercheur)
researcher = Agent(
    role='Senior Research Analyst',
    goal='Uncover cutting-edge developments in AI and data science',
    backstory="""You are an expert research analyst with a PhD in Computer Science.
    You have 10 years of experience in AI research and always find the most
    relevant and up-to-date information. You're meticulous and cite sources.""",
    verbose=True,
    allow_delegation=False,  # Ne peut pas déléguer à d'autres agents
    llm=llm
)

# Agent 2: Writer (Rédacteur)
writer = Agent(
    role='Tech Content Strategist',
    goal='Craft compelling content on tech advancements',
    backstory="""You are a renowned content creator, known for insightful
    and engaging articles on technology and AI. You transform complex
    technical concepts into clear, accessible content.""",
    verbose=True,
    allow_delegation=False,
    llm=llm
)

# Créer les tâches
task1 = Task(
    description="""Research the latest advancements in AI agents technology.
    Focus on developments from 2024, including new frameworks, techniques,
    and real-world applications. Provide detailed findings with sources.""",
    agent=researcher,
    expected_output="A comprehensive research report with key findings and sources"
)

task2 = Task(
    description="""Using the research provided, write a 500-word blog post
    about the latest AI agents advancements. Make it engaging and accessible
    to a general tech audience. Include practical examples.""",
    agent=writer,
    expected_output="A well-written 500-word blog post",
    context=[task1]  # Cette tâche dépend du résultat de task1
)

# Créer le Crew
crew = Crew(
    agents=[researcher, writer],
    tasks=[task1, task2],
    process=Process.sequential,  # Exécution séquentielle
    verbose=2  # Max verbosity pour voir tout
)

# Lancer l'exécution
result = crew.kickoff()

print("\n" + "="*70)
print("FINAL RESULT")
print("="*70)
print(result)
                
Concepts Clés CrewAI
  • Agent : Un membre de l'équipe avec role, goal, backstory
  • Task : Une tâche assignée à un agent avec contexte
  • Crew : L'équipe qui orchestre agents et tasks
  • Process : Le mode d'exécution (sequential/hierarchical/consensus)

4. Agents avec Outils

from crewai_tools import (
    SerperDevTool,  # Google Search
    ScrapeWebsiteTool,
    FileReadTool,
    DirectoryReadTool,
    CodeInterpreterTool
)

# Initialiser les outils
search_tool = SerperDevTool()
scrape_tool = ScrapeWebsiteTool()
file_tool = FileReadTool()
code_tool = CodeInterpreterTool()

# Agent avec outils multiples
data_analyst = Agent(
    role='Senior Data Analyst',
    goal='Analyze data and provide actionable insights',
    backstory="""You are an expert data analyst with strong skills in
    Python, statistics, and data visualization. You can interpret
    complex datasets and communicate findings clearly.""",
    tools=[search_tool, code_tool, file_tool],  # Outils disponibles
    verbose=True,
    llm=llm
)

# Agent spécialisé web
web_researcher = Agent(
    role='Web Research Specialist',
    goal='Find and extract information from websites',
    backstory="""You excel at finding information online and extracting
    relevant data from websites. You verify sources and cross-reference.""",
    tools=[search_tool, scrape_tool],
    verbose=True,
    llm=llm
)

# Tâche nécessitant web research
research_task = Task(
    description="""Search for the top 5 AI agent frameworks in 2024.
    For each framework, find:
    - Official website
    - GitHub stars
    - Key features
    - Use cases
    Scrape their websites for detailed information.""",
    agent=web_researcher,
    expected_output="Structured comparison of top 5 frameworks"
)

# Tâche d'analyse
analysis_task = Task(
    description="""Analyze the research data and create a comparison table.
    Identify which framework is best for different use cases.
    Provide recommendations.""",
    agent=data_analyst,
    expected_output="Analysis report with recommendations",
    context=[research_task]
)

# Crew avec outils
crew_with_tools = Crew(
    agents=[web_researcher, data_analyst],
    tasks=[research_task, analysis_task],
    process=Process.sequential,
    verbose=2
)

result = crew_with_tools.kickoff()
                

5. Process Types

5.1 Sequential Process

Les tâches s'exécutent dans l'ordre, chaque tâche peut utiliser les résultats des précédentes.

# Processus séquentiel: Recherche → Analyse → Rédaction → Révision

# Agent 1: Researcher
researcher = Agent(
    role='Researcher',
    goal='Find comprehensive information',
    backstory='Expert researcher',
    llm=llm
)

# Agent 2: Analyst
analyst = Agent(
    role='Data Analyst',
    goal='Analyze and synthesize information',
    backstory='Expert analyst',
    llm=llm
)

# Agent 3: Writer
writer = Agent(
    role='Content Writer',
    goal='Create engaging content',
    backstory='Professional writer',
    llm=llm
)

# Agent 4: Editor
editor = Agent(
    role='Editor',
    goal='Review and improve content',
    backstory='Experienced editor',
    llm=llm
)

# Tâches en chaîne
t1 = Task(description="Research AI agents", agent=researcher)
t2 = Task(description="Analyze findings", agent=analyst, context=[t1])
t3 = Task(description="Write article", agent=writer, context=[t2])
t4 = Task(description="Edit and finalize", agent=editor, context=[t3])

sequential_crew = Crew(
    agents=[researcher, analyst, writer, editor],
    tasks=[t1, t2, t3, t4],
    process=Process.sequential
)
                

5.2 Hierarchical Process

Un manager agent délègue et coordonne les autres agents.

# Processus hiérarchique avec manager

# Agents spécialistes (pas de manager ici, il sera auto-créé)
specialist1 = Agent(
    role='Backend Developer',
    goal='Develop backend systems',
    backstory='Expert in Python and databases',
    llm=llm
)

specialist2 = Agent(
    role='Frontend Developer',
    goal='Create user interfaces',
    backstory='Expert in React and UI/UX',
    llm=llm
)

specialist3 = Agent(
    role='DevOps Engineer',
    goal='Handle deployment and infrastructure',
    backstory='Expert in Docker, K8s, CI/CD',
    llm=llm
)

# Tâche globale
project_task = Task(
    description="""Build a complete web application for task management.
    Requirements:
    - RESTful API backend
    - Modern React frontend
    - Deployed on AWS
    Coordinate the team to complete this project.""",
    expected_output="Complete, deployed application"
)

# Crew hiérarchique (CrewAI crée automatiquement un manager)
hierarchical_crew = Crew(
    agents=[specialist1, specialist2, specialist3],
    tasks=[project_task],
    process=Process.hierarchical,  # Manager auto-créé
    manager_llm=llm,  # LLM pour le manager
    verbose=2
)

# Le manager va:
# 1. Décomposer le projet en sous-tâches
# 2. Assigner chaque sous-tâche au spécialiste approprié
# 3. Coordonner l'exécution
# 4. Intégrer les résultats
                

6. Delegation et Collaboration

# Agent avec délégation activée
senior_agent = Agent(
    role='Senior AI Architect',
    goal='Design comprehensive AI systems',
    backstory='20 years of experience in AI',
    allow_delegation=True,  # Peut déléguer aux autres agents du crew
    llm=llm,
    verbose=True
)

junior_researcher = Agent(
    role='Junior Researcher',
    goal='Assist with research tasks',
    backstory='Eager learner, detail-oriented',
    allow_delegation=False,
    llm=llm
)

junior_coder = Agent(
    role='Junior Developer',
    goal='Implement code solutions',
    backstory='Fresh graduate, strong coding skills',
    allow_delegation=False,
    llm=llm
)

# Tâche complexe
complex_task = Task(
    description="""Design a multi-agent system for customer support automation.
    Include:
    - Architecture diagram
    - Technology stack
    - Implementation plan
    - Sample code
    You can delegate specific subtasks to team members.""",
    agent=senior_agent,
    expected_output="Complete system design document"
)

# Le senior_agent peut déléguer:
# - Research à junior_researcher
# - Coding à junior_coder

delegation_crew = Crew(
    agents=[senior_agent, junior_researcher, junior_coder],
    tasks=[complex_task],
    process=Process.sequential,
    verbose=2
)

result = delegation_crew.kickoff()
                

7. Memory et Context

from crewai import Agent, Task, Crew, Process

# Agent avec mémoire
agent_with_memory = Agent(
    role='Personal Assistant',
    goal='Help with daily tasks and remember preferences',
    backstory='Attentive assistant who remembers details',
    memory=True,  # Active la mémoire long-terme
    verbose=True,
    llm=llm
)

# Session 1
task1 = Task(
    description="The user's name is Alice and she likes Python programming",
    agent=agent_with_memory
)

crew1 = Crew(
    agents=[agent_with_memory],
    tasks=[task1],
    process=Process.sequential
)

crew1.kickoff()

# Session 2 (plus tard)
task2 = Task(
    description="What is the user's name and what does she like?",
    agent=agent_with_memory
)

crew2 = Crew(
    agents=[agent_with_memory],
    tasks=[task2],
    process=Process.sequential
)

result = crew2.kickoff()
# L'agent se souvient: "Alice likes Python"
                

8. Exemple Complet: Équipe de Recherche

from crewai import Agent, Task, Crew, Process
from crewai_tools import SerperDevTool, ScrapeWebsiteTool
from langchain_openai import ChatOpenAI

# LLM
llm = ChatOpenAI(model="gpt-4-turbo-preview", temperature=0.7)

# Outils
search_tool = SerperDevTool()
scrape_tool = ScrapeWebsiteTool()

# Équipe de 4 agents spécialisés

# 1. Market Researcher
market_researcher = Agent(
    role='Market Research Analyst',
    goal='Conduct thorough market research and competitive analysis',
    backstory="""Expert market researcher with 15 years of experience.
    Skilled in identifying market trends, competitor strategies, and opportunities.""",
    tools=[search_tool, scrape_tool],
    verbose=True,
    llm=llm
)

# 2. Tech Analyst
tech_analyst = Agent(
    role='Technology Analyst',
    goal='Analyze technical aspects and provide technical insights',
    backstory="""Senior technology analyst with deep expertise in software
    architecture, cloud technologies, and AI systems.""",
    tools=[search_tool],
    verbose=True,
    llm=llm
)

# 3. Business Strategist
strategist = Agent(
    role='Business Strategy Consultant',
    goal='Develop strategic recommendations',
    backstory="""Former McKinsey consultant with expertise in business strategy,
    growth planning, and market positioning.""",
    verbose=True,
    llm=llm
)

# 4. Report Writer
report_writer = Agent(
    role='Senior Report Writer',
    goal='Create comprehensive, professional reports',
    backstory="""Professional writer specializing in business reports.
    Creates clear, well-structured documents with executive summaries.""",
    verbose=True,
    llm=llm
)

# Tâches

task1 = Task(
    description="""Research the AI agent market in 2024.
    Find:
    - Market size and growth rate
    - Major players and their market share
    - Recent funding rounds
    - Key trends and drivers
    Provide detailed findings with sources.""",
    agent=market_researcher,
    expected_output="Detailed market research report with data and sources"
)

task2 = Task(
    description="""Analyze the technical landscape of AI agents.
    Cover:
    - Popular frameworks and their features
    - Technology stacks being used
    - Technical challenges and solutions
    - Emerging technologies""",
    agent=tech_analyst,
    expected_output="Technical analysis report",
    context=[task1]
)

task3 = Task(
    description="""Based on market and technical research, develop
    strategic recommendations for a company wanting to enter
    the AI agent market.
    Include:
    - Market opportunities
    - Competitive positioning
    - Go-to-market strategy
    - Risk assessment""",
    agent=strategist,
    expected_output="Strategic recommendations document",
    context=[task1, task2]
)

task4 = Task(
    description="""Compile all research and analysis into a comprehensive
    executive report.
    Structure:
    1. Executive Summary
    2. Market Analysis
    3. Technical Landscape
    4. Strategic Recommendations
    5. Conclusion
    Make it professional and actionable.""",
    agent=report_writer,
    expected_output="Final executive report (2000+ words)",
    context=[task1, task2, task3]
)

# Créer le Crew
research_crew = Crew(
    agents=[market_researcher, tech_analyst, strategist, report_writer],
    tasks=[task1, task2, task3, task4],
    process=Process.sequential,
    verbose=2
)

# Exécuter
print("\n" + "="*70)
print("RESEARCH CREW STARTING")
print("="*70 + "\n")

final_report = research_crew.kickoff()

print("\n" + "="*70)
print("FINAL EXECUTIVE REPORT")
print("="*70)
print(final_report)

# Sauvegarder le rapport
with open("ai_agents_market_report.txt", "w") as f:
    f.write(final_report)

print("\n✅ Report saved to ai_agents_market_report.txt")
                

9. Best Practices CrewAI

Best Practice Description Pourquoi
Rôles spécialisés Chaque agent un rôle clair et spécifique Meilleure qualité, moins de confusion
Backstory détaillée Donner du contexte et de l'expertise Le LLM joue mieux son rôle
Context entre tasks Passer les résultats aux tâches suivantes Continuité et cohérence
Expected output clair Définir précisément ce qui est attendu Résultats plus conformes
Outils appropriés Donner les bons outils aux bons agents Efficacité maximale
Process adapté Sequential pour pipeline, Hierarchical pour projets Meilleure coordination
Conseil du Mentor

CrewAI excelle pour les workflows complexes multi-agents avec des rôles bien définis. Utilisez-le quand vous avez besoin d'une vraie collaboration d'équipe (recherche → analyse → rédaction). Pour des agents plus simples, LangChain suffit. Le coût augmente rapidement (chaque agent = plusieurs appels LLM), donc commencez avec 2-3 agents max.