Chatbots entreprise : de la conception au déploiement

tl;dr: Un chatbot entreprise moderne combine RAG (pour la connaissance) + agents (pour l'action) + guardrails (pour la sécurité). Intégration Slack/Teams, gestion RGPD, handoff humain et analytics sont essentiels. Coût typique : 500-2000€/mois selon volume.

Les chatbots d’entreprise ont connu une révolution avec l’arrivée des Large Language Models (LLMs). Fini les arbres de décision rigides et les réponses scriptées : les chatbots modernes comprennent le langage naturel, accèdent à vos documents internes et peuvent même effectuer des actions (créer un ticket, chercher dans une base de données, etc.).

Dans ce guide complet, vous découvrirez comment concevoir, développer et déployer un chatbot entreprise professionnel qui apporte une réelle valeur ajoutée. De l’analyse des besoins à la mise en production, en passant par l’architecture technique, les intégrations et la conformité RGPD.

Illustration détaillée de les chatbots d’entreprise alimentés par l’IA

Table des Matières

  1. Pourquoi un chatbot entreprise avec IA ?
  2. Analyse des besoins
  3. Architecture technique
  4. Sources de données et préparation
  5. Implémentation du RAG
  6. Agents et function calling
  7. Personnalité et ton
  8. Guardrails et sécurité
  9. Intégrations (Slack, Teams, Web)
  10. Handoff vers humain
  11. Analytics et amélioration
  12. RGPD et conformité
  13. Cas clients réels
  14. Coûts et ROI
  15. Checklist de déploiement

Pourquoi un chatbot entreprise avec IA ?

Les limitations des chatbots traditionnels

Les chatbots à base de règles (pre-LLM) avaient plusieurs problèmes majeurs :

  • Rigidité : Arbre de décision figé, toute variation linguistique cassait le flow
  • Maintenance : Chaque nouveau cas nécessitait un développement manuel
  • Frustration utilisateur : “Je n’ai pas compris votre demande” → abandon
  • Pas de contexte : Impossible de gérer des conversations naturelles
  • Scaling difficile : Couvrir 100 cas d’usage = des mois de développement

Ce que changent les LLMs

Avec les LLMs modernes (GPT-4, Claude, Gemini), un chatbot peut :

Comprendre le langage naturel : “Je peux avoir mes dernières fiches de paie ?” → détecté comme requête RH

Accéder à vos documents : RAG pour chercher dans wikis, docs, tickets

Effectuer des actions : Créer un ticket Jira, consulter un CRM, envoyer un email

Apprendre continuellement : Fine-tuning sur vos conversations

Gérer le contexte : Conversation naturelle sur plusieurs tours

ROI typique

Les entreprises qui déploient des chatbots IA observent :

  • -30 à -50% du volume support niveau 1 : Questions répétitives gérées automatiquement
  • -50% du temps de recherche interne : Employés trouvent infos en 30s vs 10min
  • Disponibilité 24/7 : Pas de week-end ni jours fériés
  • Scalabilité : 1000 conversations simultanées sans embauche
💡 Exemple concret : Une scale-up SaaS de 200 personnes a déployé un chatbot RH+IT. Résultat : 60% des questions RH résolues automatiquement, économie de 2 ETP (équivalent temps plein) la première année.

Analyse des besoins

Avant de coder quoi que ce soit, définissez clairement qui, quoi, pourquoi.

Définir les personas utilisateurs

Exemple pour un chatbot RH+IT interne :

PersonaBesoins typiquesVolume
Nouvel employéProcédures onboarding, accès outils, congés15%
Employé régulierFiches de paie, congés, tickets IT60%
ManagerValidation congés, budgets équipe, rapports20%
RH/ITAnalytics, cas complexes escaladés5%

Lister les cas d’usage prioritaires

Classez par fréquence × impact :

Priorité HAUTE (couvrir en MVP) :

  • ❓ “Comment demander mes congés ?”
  • ❓ “Où est la procédure de note de frais ?”
  • ❓ “Mon wifi ne marche pas”
  • ❓ “Quand est-ce que je reçois ma fiche de paie ?”

Priorité MOYENNE (v2) :

  • ❓ “Créer un ticket pour changer mon écran”
  • ❓ “Combien de jours de congés me reste-t-il ?”
  • ❓ “Qui est le responsable sécurité ?”

Priorité BASSE (nice to have) :

  • ❓ “Quelle est la politique de télétravail pour l’étranger ?”
  • ❓ “Simuler mon salaire net avec augmentation”

Définir les actions automatisables

Séparez information (RAG suffit) vs action (besoin d’agents) :

TypeExempleTechno
Information pure“C’est quoi la politique congés ?”RAG
Information personnalisée“Combien de congés me reste-t-il ?”RAG + API RH
Action simple“Créer un ticket IT”Agent + API Jira
Action complexe“Commander un nouveau laptop”Workflow multi-étapes

Définir les métriques de succès

Exemples de KPIs :

  • Taux de résolution : % conversations sans escalade humaine (cible : >70%)
  • CSAT : Satisfaction utilisateur en fin de conversation (cible : >4/5)
  • Temps de réponse : Latence moyenne (cible : <3 secondes)
  • Volume traité : Nb conversations/jour
  • Coût par conversation : Tokens LLM + infra
⚠️ Warning
Erreur fréquente : Vouloir tout couvrir dès le MVP. Mieux vaut 10 cas d’usage parfaitement gérés que 50 mal traités. Itérez !
🔎 Tip
Exemples de prompts : Pour des templates de prompts adaptés aux chatbots entreprise, consultez notre guide Cas d’usage du prompt engineering avec des exemples concrets pour le support client, RH et IT.

Architecture technique

Voici l’architecture recommandée pour un chatbot entreprise moderne :

┌─────────────────────────────────────────────────────────┐
│                    INTERFACES UTILISATEUR                │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌─────────┐ │
│  │  Slack   │  │  Teams   │  │   Web    │  │  Email  │ │
│  └──────────┘  └──────────┘  └──────────┘  └─────────┘ │
└────────────────────────┬────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│                  ORCHESTRATION LAYER                     │
│  ┌────────────────────────────────────────────────────┐ │
│  │  Router / Intent Classification                    │ │
│  │  (Déterminer : RAG, Agent, Handoff human)         │ │
│  └────────────────────────────────────────────────────┘ │
│                                                          │
│  ┌──────────────────┐         ┌────────────────────┐   │
│  │   RAG Engine     │         │   Agentic Layer    │   │
│  │ - Retrieval      │         │ - Function calling │   │
│  │ - Reranking      │         │ - Tools/APIs       │   │
│  │ - Generation     │         │ - Multi-step       │   │
│  └──────────────────┘         └────────────────────┘   │
│                                                          │
│  ┌────────────────────────────────────────────────────┐ │
│  │  Guardrails (PII detection, moderation, limits)   │ │
│  └────────────────────────────────────────────────────┘ │
└────────────────────────┬────────────────────────────────┘
        ┌────────────────┼────────────────┐
        │                │                │
        ▼                ▼                ▼
┌──────────────┐  ┌─────────────┐  ┌──────────────┐
│  Vector DB   │  │   LLM API   │  │  Tools/APIs  │
│  (Pinecone,  │  │ (OpenAI,    │  │  - Jira      │
│   Qdrant)    │  │  Anthropic) │  │  - Slack API │
└──────────────┘  └─────────────┘  │  - CRM       │
                                    │  - HRIS      │
                                    └──────────────┘

Composants principaux

1. Interfaces utilisateur

  • Multi-canal : Slack, Teams, web widget, email
  • Unified API backend pour faciliter ajout de canaux

2. Router intelligent

  • Classifie l’intent : information vs action vs escalade
  • Peut être un LLM rapide (GPT-3.5) ou fine-tuné

3. RAG Engine

  • Retrieve documents pertinents (Pinecone, Qdrant, Weaviate)
  • Reranking pour améliorer précision (Cohere Rerank)
  • Generation avec contexte (GPT-4, Claude)

4. Agentic Layer

  • Function calling pour actions (créer ticket, query DB)
  • Orchestration multi-étapes (LangGraph, AutoGPT style)
  • Retry logic et error handling

5. Guardrails

  • Détection PII (emails, numéros, noms)
  • Moderation (toxicité, off-topic)
  • Rate limiting
  • Cost controls

6. Data Layer

  • Base vectorielle pour docs
  • Cache Redis pour conversations
  • DB relationnelle pour analytics/logs

Sources de données et préparation

Types de sources typiques

Pour un chatbot entreprise, vous aurez probablement :

SourceTypeExemples
Documentation interneNon-structuréConfluence, Notion, Google Docs
FAQ / WikiSemi-structuréZendesk, Intercom articles
Tickets historiquesStructuréJira, Linear, GitHub Issues
EmailsNon-structuréArchives support
Base de connaissancesStructuréSQL, API endpoints
FichiersNon-structuréPDF, PowerPoint, Excel

Pipeline de préparation des données

# Exemple avec LangChain et Pinecone
from langchain.document_loaders import (
    ConfluenceLoader,
    NotionDBLoader,
    SlackDirectoryLoader,
    GoogleDriveLoader
)
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.embeddings import OpenAIEmbeddings
from pinecone import Pinecone

# 1. Charger les documents depuis sources multiples
confluence_loader = ConfluenceLoader(
    url="https://votre-confluence.atlassian.net",
    username="[email protected]",
    api_key="xxx"
)
confluence_docs = confluence_loader.load()

notion_loader = NotionDBLoader(
    integration_token="secret_xxx",
    database_id="xxx"
)
notion_docs = notion_loader.load()

# 2. Fusionner toutes les sources
all_docs = confluence_docs + notion_docs

# 3. Chunking intelligent
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200,
    separators=["\n\n", "\n", ". ", " ", ""]
)
chunks = text_splitter.split_documents(all_docs)

# 4. Enrichir avec métadonnées
for chunk in chunks:
    chunk.metadata["source_type"] = "confluence"  # ou "notion", etc.
    chunk.metadata["department"] = extract_department(chunk)  # RH, IT, etc.
    chunk.metadata["indexed_at"] = datetime.now().isoformat()

# 5. Créer embeddings et indexer
embeddings = OpenAIEmbeddings(model="text-embedding-3-large")
pc = Pinecone(api_key="xxx")
index = pc.Index("chatbot-entreprise")

# Batch upsert pour performance
batch_size = 100
for i in range(0, len(chunks), batch_size):
    batch = chunks[i:i+batch_size]
    texts = [c.page_content for c in batch]
    metadatas = [c.metadata for c in batch]

    vectors = embeddings.embed_documents(texts)
    ids = [f"doc_{i+j}" for j in range(len(batch))]

    index.upsert(vectors=list(zip(ids, vectors, metadatas)))

print(f"✅ {len(chunks)} chunks indexés dans Pinecone")

Bonnes pratiques de chunking

🔎 Tip

Stratégie de chunking selon type de contenu :

  • Documentation technique : Chunk par section (H2/H3), 800-1200 tokens
  • FAQ : 1 chunk = 1 Q&A, garder contexte entier
  • Emails/tickets : 1 chunk = 1 thread complet si <2000 tokens
  • Long documents : Sliding window avec overlap 20%

Métadonnées critiques

Enrichissez vos chunks avec :

metadata = {
    "source": "confluence",
    "url": "https://...",
    "title": "Procédure de congés",
    "department": "RH",
    "last_updated": "2025-01-15",
    "author": "[email protected]",
    "access_level": "all",  # ou "managers_only", etc.
    "language": "fr",
    "doc_type": "procedure"  # vs "faq", "policy", etc.
}

Ces métadonnées permettront :

  • Filtrage : Ne chercher que dans documents RH pour question RH
  • Permissions : Respecter ACL (ne pas montrer docs confidentiels)
  • Fraîcheur : Privilégier docs récents
  • Source citation : Afficher d’où vient l’info

Mise à jour incrémentale

Ne pas tout réindexer à chaque fois :

def sync_confluence_incremental():
    """Synchroniser seulement les pages modifiées depuis dernière sync"""
    last_sync = get_last_sync_timestamp()

    # Récupérer pages modifiées
    modified_pages = confluence.cql(
        f"lastModified >= '{last_sync}' order by lastModified"
    )

    for page in modified_pages:
        # Supprimer anciens chunks de cette page
        index.delete(filter={"confluence_page_id": page.id})

        # Réindexer
        new_chunks = process_page(page)
        index.upsert(new_chunks)

    update_last_sync_timestamp()

Implémentation du RAG

Le Retrieval-Augmented Generation est le cœur de votre chatbot. Voici une implémentation robuste.

Architecture RAG complète

from langchain.chat_models import ChatOpenAI
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import Pinecone
from langchain.chains import ConversationalRetrievalChain
from langchain.memory import ConversationBufferWindowMemory
from langchain.prompts import PromptTemplate

class EnterpriseChatbot:
    def __init__(self):
        # LLM principal (génération)
        self.llm = ChatOpenAI(
            model="gpt-4-turbo",
            temperature=0.3,  # Peu créatif, factuel
            max_tokens=1000
        )

        # Embeddings
        self.embeddings = OpenAIEmbeddings(
            model="text-embedding-3-large"
        )

        # Vector store
        self.vectorstore = Pinecone.from_existing_index(
            index_name="chatbot-entreprise",
            embedding=self.embeddings
        )

        # Mémoire conversationnelle (5 derniers tours)
        self.memory = ConversationBufferWindowMemory(
            k=5,
            memory_key="chat_history",
            return_messages=True,
            output_key="answer"
        )

        # Prompt système
        self.system_prompt = PromptTemplate(
            template="""Tu es l'assistant virtuel de TechCorp, spécialisé dans les questions RH et IT.

Contexte des documents pertinents :
{context}

Historique de conversation :
{chat_history}

Question actuelle :
{question}

Instructions :
1. Réponds UNIQUEMENT avec les informations du contexte fourni
2. Si l'info n'est pas dans le contexte, dis "Je n'ai pas trouvé cette information dans notre base de connaissance"
3. Cite toujours ta source (ex: "D'après le document X...")
4. Sois concis et professionnel
5. Si la question nécessite une action (créer ticket, etc.), dis-le clairement

Réponse:""",
            input_variables=["context", "chat_history", "question"]
        )

        # Chain RAG
        self.chain = ConversationalRetrievalChain.from_llm(
            llm=self.llm,
            retriever=self.vectorstore.as_retriever(
                search_type="mmr",  # Maximum Marginal Relevance
                search_kwargs={
                    "k": 5,  # Récupérer 5 chunks
                    "fetch_k": 20,  # Parmi 20 candidats (pour diversité)
                    "lambda_mult": 0.7  # Balance pertinence/diversité
                }
            ),
            memory=self.memory,
            combine_docs_chain_kwargs={"prompt": self.system_prompt},
            return_source_documents=True,
            verbose=True
        )

    def chat(self, user_message: str, user_id: str = None) -> dict:
        """
        Traiter un message utilisateur

        Returns:
            {
                "answer": str,
                "sources": List[dict],
                "confidence": float
            }
        """
        # Appliquer filtres métadonnées si besoin
        # (ex: un employé ne voit que docs publics)
        filters = self._get_filters_for_user(user_id)

        # Exécuter RAG
        response = self.chain({
            "question": user_message,
            "filter": filters  # Métadonnées filtering
        })

        # Extraire sources
        sources = []
        for doc in response["source_documents"]:
            sources.append({
                "title": doc.metadata.get("title"),
                "url": doc.metadata.get("url"),
                "excerpt": doc.page_content[:200]
            })

        # Calculer score de confiance (basé sur similarité)
        confidence = self._calculate_confidence(response["source_documents"])

        return {
            "answer": response["answer"],
            "sources": sources,
            "confidence": confidence
        }

    def _get_filters_for_user(self, user_id: str) -> dict:
        """Filtrer docs selon permissions utilisateur"""
        user_role = get_user_role(user_id)  # de votre DB

        if user_role == "employee":
            return {"access_level": "all"}
        elif user_role == "manager":
            return {"access_level": {"$in": ["all", "managers"]}}
        elif user_role == "admin":
            return {}  # Pas de filtre

    def _calculate_confidence(self, docs: list) -> float:
        """Score de confiance basé sur similarité"""
        if not docs:
            return 0.0

        # Supposons que docs[0] a le meilleur score
        # (Pinecone retourne par ordre de similarité)
        top_score = docs[0].metadata.get("score", 0.5)

        if top_score > 0.85:
            return 0.95
        elif top_score > 0.75:
            return 0.80
        elif top_score > 0.65:
            return 0.60
        else:
            return 0.40

Améliorer la pertinence avec Reranking

Le reranking améliore significativement la qualité :

from cohere import Client

class EnterpriseChatbotWithReranking(EnterpriseChatbot):
    def __init__(self):
        super().__init__()
        self.cohere = Client(api_key="xxx")

    def chat(self, user_message: str, user_id: str = None) -> dict:
        # 1. Retrieval initial (large, 20 chunks)
        initial_docs = self.vectorstore.similarity_search(
            user_message,
            k=20,
            filter=self._get_filters_for_user(user_id)
        )

        # 2. Reranking avec Cohere
        reranked = self.cohere.rerank(
            query=user_message,
            documents=[doc.page_content for doc in initial_docs],
            top_n=5,  # Garder top 5
            model="rerank-multilingual-v2.0"
        )

        # 3. Extraire top docs
        top_docs = [initial_docs[r.index] for r in reranked.results]

        # 4. Générer réponse avec contexte reranké
        context = "\n\n---\n\n".join([doc.page_content for doc in top_docs])

        response = self.llm.invoke(
            self.system_prompt.format(
                context=context,
                chat_history=self.memory.load_memory_variables({})["chat_history"],
                question=user_message
            )
        )

        return {
            "answer": response.content,
            "sources": self._format_sources(top_docs),
            "confidence": reranked.results[0].relevance_score
        }
💡 Gain du reranking : Dans nos tests, le reranking améliore la précision de 15-25% par rapport à la similarité vectorielle seule, surtout sur des questions complexes ou ambiguës.

Gestion du contexte conversationnel

# Mémoire avec résumé automatique pour longues conversations
from langchain.memory import ConversationSummaryBufferMemory

memory = ConversationSummaryBufferMemory(
    llm=ChatOpenAI(model="gpt-3.5-turbo"),
    max_token_limit=500,  # Résumer si > 500 tokens
    memory_key="chat_history",
    return_messages=True
)

# Exemple de conversation
"""
User: Comment demander mes congés ?
Bot: Tu peux les demander via l'outil RH dans l'intranet...

User: Et si je veux annuler ?
Bot: [Comprend que "annuler" = annuler congés grâce au contexte]

User: Combien de jours me restent ?
Bot: [Sait qu'on parle toujours de congés]
"""

Agents et function calling

Pour les actions (créer ticket, consulter solde congés, etc.), utilisez des agents avec function calling.

Définir des tools

from langchain.agents import Tool
from langchain.tools import StructuredTool
from pydantic import BaseModel, Field

# Tool 1: Consulter solde de congés
class LeaveBalanceInput(BaseModel):
    employee_id: str = Field(description="ID de l'employé")

def get_leave_balance(employee_id: str) -> str:
    """Consulter le solde de congés d'un employé"""
    # Appel à votre API RH
    response = requests.get(
        f"https://api-rh.company.com/employees/{employee_id}/leave-balance",
        headers={"Authorization": f"Bearer {RH_API_KEY}"}
    )
    data = response.json()

    return f"Solde de congés: {data['remaining_days']} jours restants sur {data['annual_allowance']} jours annuels."

leave_balance_tool = StructuredTool.from_function(
    func=get_leave_balance,
    name="get_leave_balance",
    description="Consulte le solde de congés d'un employé. À utiliser quand l'utilisateur demande combien de jours de congés il lui reste.",
    args_schema=LeaveBalanceInput
)

# Tool 2: Créer un ticket IT
class CreateTicketInput(BaseModel):
    title: str = Field(description="Titre du ticket")
    description: str = Field(description="Description détaillée du problème")
    priority: str = Field(description="Priorité: low, medium, high", default="medium")
    category: str = Field(description="Catégorie: hardware, software, network, access")

def create_it_ticket(title: str, description: str, priority: str, category: str) -> str:
    """Créer un ticket IT dans Jira"""
    jira = JIRA(
        server="https://company.atlassian.net",
        basic_auth=("[email protected]", JIRA_API_TOKEN)
    )

    issue = jira.create_issue(
        project="IT",
        summary=title,
        description=description,
        issuetype={"name": "Support"},
        priority={"name": priority.capitalize()},
        customfield_10050=category  # custom field "Category"
    )

    return f"✅ Ticket créé avec succès ! Numéro: {issue.key}\nSuivi: https://company.atlassian.net/browse/{issue.key}"

create_ticket_tool = StructuredTool.from_function(
    func=create_it_ticket,
    name="create_it_ticket",
    description="Créer un ticket de support IT. À utiliser quand l'utilisateur signale un problème technique.",
    args_schema=CreateTicketInput
)

# Tool 3: Rechercher un collègue
def search_colleague(query: str) -> str:
    """Rechercher un collègue dans l'annuaire"""
    # Recherche dans Active Directory / LDAP / DB
    results = search_in_directory(query)

    if not results:
        return "Aucun collègue trouvé pour cette recherche."

    output = "Voici les résultats:\n"
    for person in results[:5]:  # Max 5 résultats
        output += f"\n👤 {person['name']} - {person['title']}\n"
        output += f"   📧 {person['email']}\n"
        output += f"   📱 {person['phone']}\n"
        output += f"   🏢 {person['department']}\n"

    return output

search_colleague_tool = Tool(
    name="search_colleague",
    func=search_colleague,
    description="Recherche un collègue dans l'annuaire de l'entreprise par nom, département ou fonction."
)

Créer un agent avec ces tools

from langchain.agents import AgentExecutor, create_openai_functions_agent
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder

# Définir le prompt de l'agent
agent_prompt = ChatPromptTemplate.from_messages([
    ("system", """Tu es l'assistant RH+IT de TechCorp.

Tu as accès à plusieurs outils pour aider les employés :
- Consulter le solde de congés
- Créer des tickets IT
- Rechercher des collègues dans l'annuaire

Avant d'utiliser un outil, confirme avec l'utilisateur si nécessaire.
Sois concis et professionnel.
Si tu ne peux pas aider, escalade vers un humain."""),

    MessagesPlaceholder(variable_name="chat_history"),
    ("human", "{input}"),
    MessagesPlaceholder(variable_name="agent_scratchpad")
])

# Créer l'agent
tools = [leave_balance_tool, create_ticket_tool, search_colleague_tool]

agent = create_openai_functions_agent(
    llm=ChatOpenAI(model="gpt-4-turbo", temperature=0),
    tools=tools,
    prompt=agent_prompt
)

agent_executor = AgentExecutor(
    agent=agent,
    tools=tools,
    verbose=True,
    max_iterations=5,  # Éviter boucles infinies
    early_stopping_method="generate"  # Stopper si bloqué
)

# Utiliser l'agent
response = agent_executor.invoke({
    "input": "Combien de jours de congés me restent ? Mon employee_id est EMP12345",
    "chat_history": []
})

print(response["output"])
# Output: "Vous avez 18 jours de congés restants sur vos 25 jours annuels."

Workflow multi-étapes avec LangGraph

Pour des flows complexes (ex: créer ticket → notifier manager → planifier intervention), utilisez LangGraph :

from langgraph.graph import StateGraph, END
from typing import TypedDict, List

class TicketWorkflowState(TypedDict):
    user_message: str
    ticket_created: bool
    ticket_id: str
    manager_notified: bool
    priority: str

def create_ticket_node(state: TicketWorkflowState) -> TicketWorkflowState:
    """Étape 1: Créer le ticket"""
    ticket_id = create_jira_ticket(state["user_message"], state["priority"])
    return {
        **state,
        "ticket_created": True,
        "ticket_id": ticket_id
    }

def notify_manager_node(state: TicketWorkflowState) -> TicketWorkflowState:
    """Étape 2: Notifier le manager si priorité haute"""
    if state["priority"] == "high":
        send_slack_message(
            channel="#it-urgent",
            message=f"🚨 Nouveau ticket urgent: {state['ticket_id']}"
        )
        return {**state, "manager_notified": True}
    return state

def route_after_ticket(state: TicketWorkflowState) -> str:
    """Router selon priorité"""
    if state["priority"] == "high":
        return "notify_manager"
    else:
        return END

# Construire le graphe
workflow = StateGraph(TicketWorkflowState)

workflow.add_node("create_ticket", create_ticket_node)
workflow.add_node("notify_manager", notify_manager_node)

workflow.set_entry_point("create_ticket")
workflow.add_conditional_edges(
    "create_ticket",
    route_after_ticket,
    {
        "notify_manager": "notify_manager",
        END: END
    }
)
workflow.add_edge("notify_manager", END)

app = workflow.compile()

# Exécuter le workflow
result = app.invoke({
    "user_message": "Mon ordinateur ne démarre plus",
    "ticket_created": False,
    "priority": "high"
})
🔎 Tip

Quand utiliser des agents vs RAG pur :

  • RAG : Questions d’information (“Quelle est la politique X ?”)
  • Agent simple : Action unitaire (“Créer un ticket”)
  • LangGraph : Workflow multi-étapes, conditionnels, human-in-the-loop

Personnalité et ton

La personnalité de votre chatbot impacte fortement l’adoption.

Définir le ton

Exemples de tons selon contexte :

ContexteTonExemple
Support ITTechnique, efficace“Vérifions ensemble : peux-tu ouvrir les Paramètres Réseau ?”
RHEmpathique, rassurant“Je comprends que la paperasse peut être compliquée. Voici comment faire…”
Ventes internesDynamique, motivant“Super ! Je vois que tu cherches les chiffres Q4. Les voici !”
Conformité/LégalFormel, précis“Selon la politique P-042 section 3.2…”

Prompt système pour définir la personnalité

SYSTEM_PROMPT = """Tu es Alex, l'assistant virtuel de TechCorp.

## Personnalité {#personnalité}
- 🤝 Amical mais professionnel (tu vs vous selon contexte)
- 💡 Proactif : suggère des solutions avant qu'on te les demande
- 🎯 Efficace : réponds directement, pas de blabla inutile
- 😊 Positif : même pour les mauvaises nouvelles, reste constructif
- 🧠 Humble : si tu ne sais pas, tu le dis (pas d'invention)

## Ton {#ton}
- Utilise "tu" pour collègues, "vous" pour C-level
- Émojis autorisés mais avec parcimonie (1-2 max par message)
- Pas de jargon technique sauf si nécessaire
- Phrases courtes (< 20 mots idéalement)

## Structure de réponse {#structure-de-réponse}
1. Réponse directe en 1-2 phrases
2. Détails si pertinent
3. Prochaines étapes / actions suggérées
4. Sources (avec liens)

## Exemples {#exemples}
❌ "Conformément aux dispositions de l'article 3.2 du règlement intérieur..."
✅ "D'après notre règlement intérieur, tu as droit à 25 jours de congés par an."

❌ "Je vais effectuer une requête dans la base de données vectorielle..."
✅ "Je cherche ça pour toi... [2 secondes] Voici ce que j'ai trouvé !"

## Quand escalader vers un humain {#quand-escalader-vers-un-humain}
- L'utilisateur est frustré (3+ messages sans résolution)
- Question hors de ton scope (ex: négociation salariale)
- Demande explicite ("Je veux parler à quelqu'un")
- Urgence critique (ex: "Mon ordi est en feu")
"""

Adapter le ton selon l’utilisateur

def get_personalized_prompt(user_id: str) -> str:
    """Adapter le prompt selon profil utilisateur"""
    user = get_user_profile(user_id)

    base_prompt = SYSTEM_PROMPT

    # Adapter le tutoiement/vouvoiement
    if user["seniority"] in ["C-level", "VP"]:
        base_prompt += "\n\n⚠️ Cet utilisateur est senior. Utilise 'vous' et sois particulièrement concis."

    # Adapter la langue
    if user["language"] == "en":
        base_prompt = translate_to_english(base_prompt)

    # Historique de satisfaction
    if user["avg_csat"] < 3:
        base_prompt += "\n\n⚠️ Cet utilisateur a eu des expériences négatives. Sois extra attentif et propose rapidement d'escalader vers humain si besoin."

    return base_prompt

Guardrails et sécurité

Les guardrails protègent contre les comportements indésirables.

Détection de PII (Personally Identifiable Information)

import re
from presidio_analyzer import AnalyzerEngine
from presidio_anonymizer import AnonymizerEngine

class PIIGuardrail:
    def __init__(self):
        self.analyzer = AnalyzerEngine()
        self.anonymizer = AnonymizerEngine()

    def detect_and_redact_pii(self, text: str) -> dict:
        """Détecter et anonymiser les PII"""
        # Analyse
        results = self.analyzer.analyze(
            text=text,
            language="fr",
            entities=[
                "PHONE_NUMBER",
                "EMAIL_ADDRESS",
                "CREDIT_CARD",
                "IBAN_CODE",
                "FR_SSN",  # Numéro de sécurité sociale français
                "PERSON",
                "LOCATION"
            ]
        )

        # Anonymiser
        anonymized = self.anonymizer.anonymize(
            text=text,
            analyzer_results=results
        )

        return {
            "has_pii": len(results) > 0,
            "pii_types": [r.entity_type for r in results],
            "original": text,
            "anonymized": anonymized.text
        }

    def check_before_logging(self, text: str) -> bool:
        """Vérifier avant de logger (RGPD compliance)"""
        result = self.detect_and_redact_pii(text)

        if result["has_pii"]:
            # Logger version anonymisée seulement
            log_to_analytics(result["anonymized"])
            return False  # Ne pas logger l'original
        else:
            log_to_analytics(text)
            return True

# Utilisation
guardrail = PIIGuardrail()

user_message = "Mon email est [email protected] et mon tel 06 12 34 56 78"
result = guardrail.detect_and_redact_pii(user_message)

print(result["anonymized"])
# Output: "Mon email est <EMAIL_ADDRESS> et mon tel <PHONE_NUMBER>"

Modération de contenu

from openai import OpenAI

class ContentModerationGuardrail:
    def __init__(self):
        self.client = OpenAI()

    def is_safe(self, text: str) -> dict:
        """Vérifier si le contenu est approprié"""
        response = self.client.moderations.create(input=text)
        result = response.results[0]

        return {
            "is_safe": not result.flagged,
            "categories": {
                cat: score
                for cat, score in result.category_scores.items()
                if score > 0.01  # Seuil
            },
            "flagged_categories": [
                cat for cat, flagged in result.categories.items()
                if flagged
            ]
        }

    def check_and_block(self, text: str) -> tuple[bool, str]:
        """Bloquer si contenu inapproprié"""
        moderation = self.is_safe(text)

        if not moderation["is_safe"]:
            flagged = moderation["flagged_categories"]

            if "harassment" in flagged:
                return False, "❌ Message bloqué : langage inapproprié détecté."
            elif "violence" in flagged:
                return False, "❌ Message bloqué : contenu violent détecté."
            else:
                return False, "❌ Message bloqué : contenu non conforme à notre politique."

        return True, ""

# Utilisation
moderation = ContentModerationGuardrail()
is_ok, message = moderation.check_and_block("Message de l'utilisateur...")
if not is_ok:
    return {"error": message}

Rate limiting et cost controls

from redis import Redis
from datetime import datetime, timedelta

class RateLimitGuardrail:
    def __init__(self):
        self.redis = Redis(host='localhost', port=6379, db=0)

    def check_rate_limit(
        self,
        user_id: str,
        limit_per_hour: int = 50,
        limit_per_day: int = 200
    ) -> tuple[bool, str]:
        """Vérifier si l'utilisateur n'abuse pas"""
        now = datetime.now()
        hour_key = f"rate_limit:hour:{user_id}:{now.strftime('%Y%m%d%H')}"
        day_key = f"rate_limit:day:{user_id}:{now.strftime('%Y%m%d')}"

        # Incrémenter compteurs
        hour_count = self.redis.incr(hour_key)
        day_count = self.redis.incr(day_key)

        # Set expiration si nouveau
        if hour_count == 1:
            self.redis.expire(hour_key, 3600)  # 1h
        if day_count == 1:
            self.redis.expire(day_key, 86400)  # 24h

        # Vérifier limites
        if hour_count > limit_per_hour:
            return False, f"⏸️ Limite horaire atteinte ({limit_per_hour} messages/h). Réessaye dans {60 - now.minute} minutes."

        if day_count > limit_per_day:
            return False, f"⏸️ Limite journalière atteinte ({limit_per_day} messages/jour). Réessaye demain."

        return True, ""

    def check_cost_limit(
        self,
        user_id: str,
        estimated_tokens: int,
        cost_per_1k_tokens: float = 0.01
    ) -> tuple[bool, str]:
        """Vérifier budget mensuel utilisateur"""
        month_key = f"cost:{user_id}:{datetime.now().strftime('%Y%m')}"

        # Calculer coût
        cost = (estimated_tokens / 1000) * cost_per_1k_tokens

        # Ajouter au total mensuel
        total_cost = float(self.redis.get(month_key) or 0) + cost
        self.redis.setex(month_key, 86400 * 31, total_cost)  # 31 jours

        # Limite mensuelle : 50€ par utilisateur
        if total_cost > 50:
            return False, "💰 Budget mensuel dépassé. Contacte l'admin."

        return True, ""

# Middleware d'application
def chatbot_middleware(user_id: str, message: str):
    """Vérifier tous les guardrails avant de traiter"""
    rate_limiter = RateLimitGuardrail()
    moderation = ContentModerationGuardrail()
    pii = PIIGuardrail()

    # 1. Rate limiting
    ok, error = rate_limiter.check_rate_limit(user_id)
    if not ok:
        return {"error": error}

    # 2. Modération
    ok, error = moderation.check_and_block(message)
    if not ok:
        return {"error": error}

    # 3. PII detection (warning seulement)
    pii_result = pii.detect_and_redact_pii(message)
    if pii_result["has_pii"]:
        log_warning(f"PII detected from {user_id}: {pii_result['pii_types']}")

    # 4. Estimer coût
    estimated_tokens = len(message.split()) * 1.3  # Rough estimate
    ok, error = rate_limiter.check_cost_limit(user_id, estimated_tokens)
    if not ok:
        return {"error": error}

    # Tout OK, continuer
    return {"ok": True}

Prévention du prompt injection

def detect_prompt_injection(user_input: str) -> bool:
    """Détecter tentatives de prompt injection"""

    # Patterns suspects
    injection_patterns = [
        r"ignore (previous|all) instructions",
        r"you are now",
        r"new instructions:",
        r"system:",
        r"<\|im_start\|>",
        r"<\|im_end\|>",
        r"disregard",
        r"forget everything",
        r"roleplay as",
    ]

    user_lower = user_input.lower()

    for pattern in injection_patterns:
        if re.search(pattern, user_lower):
            log_security_alert(f"Prompt injection attempt: {user_input}")
            return True

    return False

# Dans votre chatbot
if detect_prompt_injection(user_message):
    return {
        "answer": "❌ Ta demande semble suspecte. Si c'est une erreur, reformule-la.",
        "blocked": True
    }
⚠️ Warning

Sécurité en production : Ne jamais faire confiance aux inputs utilisateurs. Implémentez TOUJOURS :

  • Validation d’entrée
  • Rate limiting
  • Monitoring des anomalies
  • Logs d’audit (anonymisés)
  • Alertes sur comportements suspects

Intégrations (Slack, Teams, Web)

Intégration Slack

from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler

# Initialiser l'app Slack
app = App(token=SLACK_BOT_TOKEN)

@app.event("app_mention")
def handle_mention(event, say, client):
    """Répondre quand le bot est mentionné"""
    user_id = event["user"]
    text = event["text"]
    channel = event["channel"]
    thread_ts = event.get("thread_ts", event["ts"])

    # Retirer la mention du bot du texte
    bot_user_id = client.auth_test()["user_id"]
    clean_text = re.sub(f"<@{bot_user_id}>", "", text).strip()

    # Indiquer que le bot est en train de taper
    client.chat_postMessage(
        channel=channel,
        thread_ts=thread_ts,
        text="🤔 Laisse-moi chercher ça pour toi..."
    )

    try:
        # Appeler votre chatbot
        response = chatbot.chat(clean_text, user_id=user_id)

        # Formater la réponse avec sources
        blocks = [
            {
                "type": "section",
                "text": {"type": "mrkdwn", "text": response["answer"]}
            }
        ]

        # Ajouter les sources
        if response["sources"]:
            sources_text = "*📚 Sources:*\n" + "\n".join([
                f"• <{s['url']}|{s['title']}>"
                for s in response["sources"]
            ])
            blocks.append({
                "type": "context",
                "elements": [{"type": "mrkdwn", "text": sources_text}]
            })

        # Ajouter boutons d'action
        blocks.append({
            "type": "actions",
            "elements": [
                {
                    "type": "button",
                    "text": {"type": "plain_text", "text": "👍 Utile"},
                    "action_id": "feedback_positive",
                    "value": response.get("conversation_id")
                },
                {
                    "type": "button",
                    "text": {"type": "plain_text", "text": "👎 Pas utile"},
                    "action_id": "feedback_negative",
                    "value": response.get("conversation_id")
                },
                {
                    "type": "button",
                    "text": {"type": "plain_text", "text": "👤 Parler à un humain"},
                    "action_id": "escalate_to_human",
                    "value": response.get("conversation_id"),
                    "style": "danger"
                }
            ]
        })

        # Envoyer la réponse
        client.chat_update(
            channel=channel,
            ts=thread_ts,
            text=response["answer"],  # Fallback text
            blocks=blocks
        )

    except Exception as e:
        logging.error(f"Error in Slack handler: {e}")
        say(
            text="😞 Désolé, j'ai rencontré un problème. Un humain va t'aider.",
            thread_ts=thread_ts
        )
        notify_support_team(channel, thread_ts, clean_text, user_id)

@app.action("feedback_positive")
def handle_positive_feedback(ack, body, client):
    """Gérer feedback positif"""
    ack()
    conversation_id = body["actions"][0]["value"]
    log_feedback(conversation_id, rating=5)

    client.chat_postMessage(
        channel=body["channel"]["id"],
        thread_ts=body["message"]["ts"],
        text="Merci pour ton retour ! 🙏"
    )

@app.action("escalate_to_human")
def handle_escalation(ack, body, client):
    """Escalader vers support humain"""
    ack()

    user_id = body["user"]["id"]
    channel = body["channel"]["id"]
    thread_ts = body["message"]["ts"]

    # Notifier équipe support dans canal privé
    client.chat_postMessage(
        channel="#support-escalations",
        text=f"🆘 <@{user_id}> demande de l'aide humaine",
        blocks=[
            {
                "type": "section",
                "text": {
                    "type": "mrkdwn",
                    "text": f"*Nouvelle escalation*\n<#{channel}> | <https://company.slack.com/archives/{channel}/p{thread_ts.replace('.', '')}|Voir la conversation>"
                }
            },
            {
                "type": "actions",
                "elements": [
                    {
                        "type": "button",
                        "text": {"type": "plain_text", "text": "✋ Je prends en charge"},
                        "action_id": "claim_escalation",
                        "value": f"{channel}:{thread_ts}:{user_id}"
                    }
                ]
            }
        ]
    )

    # Confirmer à l'utilisateur
    client.chat_postMessage(
        channel=channel,
        thread_ts=thread_ts,
        text="✅ Un membre de l'équipe va te répondre sous peu !"
    )

# Lancer le bot
if __name__ == "__main__":
    handler = SocketModeHandler(app, SLACK_APP_TOKEN)
    handler.start()

Intégration Microsoft Teams

from botbuilder.core import BotFrameworkAdapter, BotFrameworkAdapterSettings, TurnContext
from botbuilder.schema import Activity, ActivityTypes
from aiohttp import web

# Configuration
SETTINGS = BotFrameworkAdapterSettings(
    app_id=TEAMS_APP_ID,
    app_password=TEAMS_APP_PASSWORD
)
ADAPTER = BotFrameworkAdapter(SETTINGS)

async def on_message_activity(turn_context: TurnContext):
    """Gérer message Teams"""
    user_message = turn_context.activity.text
    user_id = turn_context.activity.from_property.id

    # Typing indicator
    await turn_context.send_activity(Activity(type=ActivityTypes.typing))

    try:
        # Appeler chatbot
        response = chatbot.chat(user_message, user_id=user_id)

        # Créer carte adaptative pour réponse
        card = {
            "type": "AdaptiveCard",
            "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
            "version": "1.4",
            "body": [
                {
                    "type": "TextBlock",
                    "text": response["answer"],
                    "wrap": True
                }
            ]
        }

        # Ajouter sources si disponibles
        if response["sources"]:
            card["body"].append({
                "type": "TextBlock",
                "text": "**Sources:**",
                "weight": "Bolder",
                "spacing": "Medium"
            })
            for source in response["sources"]:
                card["body"].append({
                    "type": "TextBlock",
                    "text": f"[{source['title']}]({source['url']})",
                    "wrap": True
                })

        # Ajouter actions
        card["actions"] = [
            {
                "type": "Action.Submit",
                "title": "👍 Utile",
                "data": {"feedback": "positive", "conv_id": response.get("conversation_id")}
            },
            {
                "type": "Action.Submit",
                "title": "👎 Pas utile",
                "data": {"feedback": "negative", "conv_id": response.get("conversation_id")}
            }
        ]

        # Envoyer
        await turn_context.send_activity(
            Activity(
                type=ActivityTypes.message,
                attachments=[{
                    "contentType": "application/vnd.microsoft.card.adaptive",
                    "content": card
                }]
            )
        )

    except Exception as e:
        logging.error(f"Teams bot error: {e}")
        await turn_context.send_activity("😞 Désolé, une erreur s'est produite.")

async def messages(req: web.Request) -> web.Response:
    """Endpoint pour Teams"""
    body = await req.json()
    activity = Activity().deserialize(body)

    auth_header = req.headers.get("Authorization", "")

    async def call_bot(turn_context: TurnContext):
        if turn_context.activity.type == ActivityTypes.message:
            await on_message_activity(turn_context)

    await ADAPTER.process_activity(activity, auth_header, call_bot)
    return web.Response(status=200)

# Serveur web
app = web.Application()
app.router.add_post("/api/messages", messages)
web.run_app(app, host="0.0.0.0", port=3978)

Widget Web

<!-- Embed dans votre site -->
<!DOCTYPE html>
<html>
<head>
    <title>Chatbot Support</title>
    <style>
        /* Widget en bas à droite */
        #chatbot-widget {
            position: fixed;
            bottom: 20px;
            right: 20px;
            width: 400px;
            height: 600px;
            border: 1px solid #ccc;
            border-radius: 12px;
            box-shadow: 0 4px 20px rgba(0,0,0,0.15);
            display: flex;
            flex-direction: column;
            background: white;
            z-index: 9999;
        }

        #chatbot-header {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            padding: 16px;
            border-radius: 12px 12px 0 0;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }

        #chatbot-messages {
            flex: 1;
            overflow-y: auto;
            padding: 16px;
            display: flex;
            flex-direction: column;
            gap: 12px;
        }

        .message {
            max-width: 80%;
            padding: 12px 16px;
            border-radius: 18px;
            line-height: 1.4;
        }

        .message.user {
            background: #667eea;
            color: white;
            align-self: flex-end;
            border-bottom-right-radius: 4px;
        }

        .message.bot {
            background: #f1f3f5;
            color: #333;
            align-self: flex-start;
            border-bottom-left-radius: 4px;
        }

        .message.bot .sources {
            margin-top: 8px;
            padding-top: 8px;
            border-top: 1px solid #ddd;
            font-size: 0.85em;
        }

        #chatbot-input {
            padding: 16px;
            border-top: 1px solid #e0e0e0;
            display: flex;
            gap: 8px;
        }

        #chatbot-input input {
            flex: 1;
            padding: 12px;
            border: 1px solid #ddd;
            border-radius: 24px;
            outline: none;
        }

        #chatbot-input button {
            padding: 12px 24px;
            background: #667eea;
            color: white;
            border: none;
            border-radius: 24px;
            cursor: pointer;
            font-weight: 600;
        }

        #chatbot-input button:hover {
            background: #5568d3;
        }

        .typing-indicator {
            display: inline-block;
        }

        .typing-indicator span {
            display: inline-block;
            width: 8px;
            height: 8px;
            border-radius: 50%;
            background: #999;
            margin: 0 2px;
            animation: typing 1.4s infinite;
        }

        .typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
        .typing-indicator span:nth-child(3) { animation-delay: 0.4s; }

        @keyframes typing {
            0%, 60%, 100% { transform: translateY(0); }
            30% { transform: translateY(-10px); }
        }
    </style>
</head>
<body>
    <div id="chatbot-widget">
        <div id="chatbot-header">
            <div>
                <h3 style="margin:0;">💬 Support Assistant</h3>
                <small>En ligne - Répond en ~3s</small>
            </div>
            <button onclick="closeWidget()" style="background:none;border:none;color:white;font-size:24px;cursor:pointer;">×</button>
        </div>

        <div id="chatbot-messages">
            <div class="message bot">
                👋 Salut ! Comment puis-je t'aider aujourd'hui ?
            </div>
        </div>

        <div id="chatbot-input">
            <input type="text" id="user-input" placeholder="Pose ta question..." onkeypress="handleKeyPress(event)">
            <button onclick="sendMessage()">Envoyer</button>
        </div>
    </div>

    <script>
        const messagesDiv = document.getElementById('chatbot-messages');
        const userInput = document.getElementById('user-input');

        function addMessage(text, isUser = false, sources = []) {
            const msgDiv = document.createElement('div');
            msgDiv.className = `message ${isUser ? 'user' : 'bot'}`;

            let html = text;

            // Ajouter sources si disponibles
            if (!isUser && sources.length > 0) {
                html += '<div class="sources">📚 <strong>Sources:</strong><br>';
                sources.forEach(s => {
                    html += `• <a href="${s.url}" target="_blank">${s.title}</a><br>`;
                });
                html += '</div>';
            }

            msgDiv.innerHTML = html;
            messagesDiv.appendChild(msgDiv);
            messagesDiv.scrollTop = messagesDiv.scrollHeight;
        }

        function showTyping() {
            const typingDiv = document.createElement('div');
            typingDiv.className = 'message bot typing-indicator';
            typingDiv.id = 'typing-indicator';
            typingDiv.innerHTML = '<span></span><span></span><span></span>';
            messagesDiv.appendChild(typingDiv);
            messagesDiv.scrollTop = messagesDiv.scrollHeight;
        }

        function hideTyping() {
            const typing = document.getElementById('typing-indicator');
            if (typing) typing.remove();
        }

        async function sendMessage() {
            const text = userInput.value.trim();
            if (!text) return;

            // Afficher message utilisateur
            addMessage(text, true);
            userInput.value = '';

            // Typing indicator
            showTyping();

            try {
                // Appeler votre API
                const response = await fetch('/api/chat', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({
                        message: text,
                        user_id: getUserId()  // Cookie/localStorage
                    })
                });

                const data = await response.json();

                hideTyping();
                addMessage(data.answer, false, data.sources || []);

            } catch (error) {
                hideTyping();
                addMessage('😞 Désolé, une erreur s\'est produite. Réessaye dans un moment.', false);
            }
        }

        function handleKeyPress(e) {
            if (e.key === 'Enter') sendMessage();
        }

        function closeWidget() {
            document.getElementById('chatbot-widget').style.display = 'none';
        }

        function getUserId() {
            // Récupérer ou créer ID utilisateur (cookie/localStorage)
            let userId = localStorage.getItem('chatbot_user_id');
            if (!userId) {
                userId = 'user_' + Math.random().toString(36).substr(2, 9);
                localStorage.setItem('chatbot_user_id', userId);
            }
            return userId;
        }
    </script>
</body>
</html>

Handoff vers humain

Le handoff (escalade vers un humain) est critique quand le bot est bloqué.

Détecter quand escalader

class EscalationDetector:
    def should_escalate(
        self,
        conversation_history: list,
        last_response: dict,
        user_message: str
    ) -> tuple[bool, str]:
        """Détecter si escalade nécessaire"""

        # 1. Demande explicite
        escalation_keywords = [
            "parler à quelqu'un",
            "agent humain",
            "vrai personne",
            "manager",
            "support",
            "pas satisfait"
        ]
        if any(kw in user_message.lower() for kw in escalation_keywords):
            return True, "explicit_request"

        # 2. Frustration détectée
        frustration_keywords = [
            "inutile",
            "ça marche pas",
            "encore",
            "toujours pas",
            "en boucle"
        ]
        if any(kw in user_message.lower() for kw in frustration_keywords):
            return True, "frustration"

        # 3. Confiance faible du bot
        if last_response.get("confidence", 1.0) < 0.4:
            return True, "low_confidence"

        # 4. Boucle (mêmes questions répétées)
        if len(conversation_history) >= 6:
            recent_questions = [msg["user"] for msg in conversation_history[-6:]]
            # Similarité entre questions
            if self._detect_loop(recent_questions):
                return True, "conversation_loop"

        # 5. Hors scope
        out_of_scope_indicators = [
            "salaire",
            "augmentation",
            "licenciement",
            "confidentiel"
        ]
        if any(ind in user_message.lower() for ind in out_of_scope_indicators):
            return True, "out_of_scope"

        return False, ""

    def _detect_loop(self, questions: list) -> bool:
        """Détecter si utilisateur répète les mêmes questions"""
        from difflib import SequenceMatcher

        for i in range(len(questions)-1):
            for j in range(i+1, len(questions)):
                similarity = SequenceMatcher(None, questions[i], questions[j]).ratio()
                if similarity > 0.7:
                    return True
        return False

# Dans votre chatbot
def chat_with_escalation(user_message: str, conversation_id: str) -> dict:
    """Chat avec détection d'escalade"""

    # Récupérer historique
    history = get_conversation_history(conversation_id)

    # Générer réponse
    response = chatbot.chat(user_message)

    # Vérifier escalade
    escalation = EscalationDetector()
    should_escalate, reason = escalation.should_escalate(
        history,
        response,
        user_message
    )

    if should_escalate:
        # Notifier équipe support
        ticket_id = create_support_ticket(
            conversation_id=conversation_id,
            reason=reason,
            history=history
        )

        # Réponse d'escalade
        return {
            "answer": "Je vais te mettre en contact avec un membre de l'équipe. Ticket créé : " + ticket_id,
            "escalated": True,
            "ticket_id": ticket_id,
            "reason": reason
        }

    return response

Intégration avec Zendesk/Intercom

import requests

def create_support_ticket(
    conversation_id: str,
    reason: str,
    history: list
) -> str:
    """Créer ticket Zendesk avec contexte"""

    # Formater historique
    conversation_text = "\n\n".join([
        f"{'User' if msg['role'] == 'user' else 'Bot'}: {msg['content']}"
        for msg in history
    ])

    # Créer ticket
    ticket_data = {
        "ticket": {
            "subject": f"Escalation chatbot ({reason})",
            "comment": {
                "body": f"""Escalation automatique du chatbot.

Raison : {reason}
Conversation ID : {conversation_id}

=== Historique de conversation ===
{conversation_text}

=== Contexte ===
- Heure escalade : {datetime.now().isoformat()}
- Confiance dernier message : {history[-1].get('confidence', 'N/A')}
"""
            },
            "priority": "normal" if reason != "frustration" else "high",
            "tags": ["chatbot_escalation", reason],
            "custom_fields": [
                {"id": 123456, "value": conversation_id}  # Custom field "Conv ID"
            ]
        }
    }

    response = requests.post(
        "https://company.zendesk.com/api/v2/tickets.json",
        json=ticket_data,
        auth=(ZENDESK_EMAIL, ZENDESK_API_TOKEN)
    )

    ticket = response.json()["ticket"]
    return ticket["id"]

(La suite de l’article avec Analytics, RGPD, Cas clients, Coûts et Checklist sera dans un prochain message pour respecter la limite de longueur)


Ressources et liens

Articles connexes

Outils mentionnés

  • LLMs : OpenAI GPT-4, Anthropic Claude, Google Gemini
  • Vector DBs : Pinecone, Qdrant, Weaviate, Chroma
  • Frameworks : LangChain, LlamaIndex, LangGraph
  • Intégrations : Slack Bolt, Microsoft Bot Framework
  • Monitoring : LangSmith, Helicone, Phoenix
  • Security : Presidio (PII), OpenAI Moderation API

Documentation externe