# Intelligent Document Retrieval - Architecture Technique

## Vue d'Ensemble

Ce document décrit l'architecture pour la récupération intelligente de documents dans CELUNA. Le système permet à l'AIAssistantModule de sélectionner et lire des documents de manière efficace sans saturer le context window du LLM.

### Problème Adressé

- **Context window limité** : Les LLMs ont une limite de tokens (~200K pour Claude)
- **Volume de documents variable** : 10 à 1000+ documents selon l'utilisateur
- **Coût API** : Charger tous les documents = coût prohibitif et latence élevée
- **Pertinence** : Tous les documents ne sont pas utiles pour chaque requête

### Solution : Retrieval Agentique

Le LLM décide dynamiquement quels documents lire via un système de tools, reproduisant le comportement de Claude Code.

## Architecture Système

### Flux de Données

```
┌─────────────────────────────────────────────────────────────────────────┐
│                         DOCUMENT STORE                                  │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  documents/                      index.json (léger, toujours en RAM)    │
│  ├── doc1.md (5KB)              ┌────────────────────────────────────┐  │
│  ├── doc2.md (12KB)      ──►    │ [{id, title, summary, tags, size}] │  │
│  ├── doc3.md (3KB)              │ [{id, title, summary, tags, size}] │  │
│  └── ...                        │ [...]                              │  │
│                                 └────────────────────────────────────┘  │
│                                          ~100 bytes par document        │
└─────────────────────────────────────────────────────────────────────────┘
                                           │
                                           ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                         AGENTIC LOOP                                    │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  1. User Query ─────────────────────────────────────────────────────►   │
│                                                                         │
│  2. LLM analyse la requête                                              │
│     → Décide d'utiliser un TOOL                                         │
│                                                                         │
│  3. TOOL CALL: list_documents() ou search_documents("query")            │
│     ← Retourne: metadata uniquement (pas le contenu)                    │
│                                                                         │
│  4. LLM analyse les résultats                                           │
│     → Sélectionne les documents pertinents                              │
│                                                                         │
│  5. TOOL CALL: read_document("doc_id", max_chars=8000)                  │
│     ← Retourne: contenu (tronqué si nécessaire)                         │
│                                                                         │
│  6. LLM décide: suffisant? ou besoin de plus?                           │
│     → Continue ou génère la réponse finale                              │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘
```

### Composants Principaux

```
modules/ai-assistant/
├── CMakeLists.txt
├── CLAUDE.md
├── src/
│   └── AIAssistantModule.cpp    # Logique agentique (~200 lignes)
├── shared/
│   ├── DocumentStore.hpp        # Gestion index et lecture
│   ├── DocumentIndex.hpp        # Structure index en mémoire
│   ├── ILLMProvider.hpp         # Interface LLM agnostique
│   └── providers/
│       ├── ClaudeProvider.hpp   # Implémentation Anthropic
│       ├── OpenAIProvider.hpp   # Implémentation OpenAI
│       └── OllamaProvider.hpp   # Implémentation locale
└── tests/
    └── ai_assistant_test.cpp
```

## Spécifications Techniques

### 1. Document Index

Structure légère maintenue en RAM pour accès rapide.

```cpp
struct DocumentMetadata {
    std::string id;              // Identifiant unique (filename sans extension)
    std::string title;           // Extrait du premier # header
    std::string path;            // Chemin absolu vers le fichier
    std::string summary;         // Premières lignes non-vides (~300 chars)
    std::vector<std::string> tags;  // Headers ## extraits
    size_t size_bytes;           // Taille fichier
    std::string last_modified;   // Timestamp ISO 8601
};
```

**Estimation mémoire** :
- 100 documents : ~10KB en RAM
- 1000 documents : ~100KB en RAM

### 2. Tools Disponibles

Le LLM dispose des tools suivants pour naviguer dans les documents :

#### list_documents
```json
{
  "name": "list_documents",
  "description": "Liste tous les documents disponibles avec titre, résumé court et taille. Utiliser pour avoir une vue d'ensemble.",
  "input_schema": {
    "type": "object",
    "properties": {}
  }
}
```

**Output** : Array de metadata (id, title, summary tronqué, size_kb)

#### search_documents
```json
{
  "name": "search_documents",
  "description": "Recherche des documents par mots-clés. Retourne les plus pertinents.",
  "input_schema": {
    "type": "object",
    "properties": {
      "query": {"type": "string", "description": "Mots-clés de recherche"},
      "max_results": {"type": "integer", "default": 5}
    },
    "required": ["query"]
  }
}
```

**Output** : Array de metadata avec score de pertinence

#### read_document
```json
{
  "name": "read_document",
  "description": "Lit le contenu d'un document spécifique. Pour les gros documents, limiter avec max_chars.",
  "input_schema": {
    "type": "object",
    "properties": {
      "document_id": {"type": "string"},
      "max_chars": {"type": "integer", "default": 8000}
    },
    "required": ["document_id"]
  }
}
```

**Output** : Contenu du document (tronqué si > max_chars)

#### read_section
```json
{
  "name": "read_section",
  "description": "Lit une section spécifique d'un document identifiée par son header ##.",
  "input_schema": {
    "type": "object",
    "properties": {
      "document_id": {"type": "string"},
      "section_name": {"type": "string"}
    },
    "required": ["document_id", "section_name"]
  }
}
```

**Output** : Contenu de la section uniquement

### 3. Stratégies de Recherche

#### Phase 1 (MVP) : Keyword Search

Recherche par correspondance de mots-clés dans titre + summary + tags.

```cpp
int calculateScore(const DocumentMetadata& doc, const std::vector<std::string>& keywords) {
    int score = 0;
    std::string searchable = toLower(doc.title + " " + doc.summary);
    for (const auto& tag : doc.tags) {
        searchable += " " + toLower(tag);
    }

    for (const auto& keyword : keywords) {
        if (searchable.find(toLower(keyword)) != std::string::npos) {
            score++;
        }
        // Bonus si match dans le titre
        if (toLower(doc.title).find(toLower(keyword)) != std::string::npos) {
            score += 2;
        }
    }
    return score;
}
```

**Avantages** : Simple, rapide, aucune dépendance externe
**Limitations** : Ne comprend pas la sémantique ("focus" != "concentration")

#### Phase 2 (Optionnel) : Semantic Search avec Embeddings

Utilisation d'embeddings pour recherche sémantique.

```
Document ──► Embedding API ──► Vecteur [1536 dimensions]
                                      │
                                      ▼
                               Vector Store (SQLite + extension)
                                      │
Query ──► Embedding API ──────────────┼──► Cosine Similarity
                                      │
                               Top K documents
```

**Implémentation suggérée** :
- Embeddings : API Anthropic (voyage-3) ou OpenAI (text-embedding-3-small)
- Storage : SQLite avec extension sqlite-vss ou fichier binaire simple
- Coût : ~$0.0001 par embedding (négligeable)

**Avantages** : Comprend la sémantique, trouve des documents connexes
**Limitations** : Nécessite pré-calcul, dépendance API externe

#### Phase 3 (Optionnel) : Hybrid Search

Combinaison keyword + semantic pour meilleurs résultats.

```cpp
std::vector<SearchResult> hybridSearch(const std::string& query) {
    auto keywordResults = keywordSearch(query, 20);
    auto semanticResults = semanticSearch(query, 20);

    // Fusion avec pondération
    std::map<std::string, float> scores;
    for (const auto& r : keywordResults) {
        scores[r.id] += r.score * 0.3;  // 30% keyword
    }
    for (const auto& r : semanticResults) {
        scores[r.id] += r.score * 0.7;  // 70% semantic
    }

    // Trier et retourner top K
    return sortAndLimit(scores, 10);
}
```

### 4. Gestion du Context Window

#### Budget Tokens

```
Context Window Total : 200,000 tokens
├── System Prompt    :   2,000 tokens (fixe)
├── Conversation     :  20,000 tokens (historique)
├── Tools Definition :   1,000 tokens (fixe)
├── Documents        :  50,000 tokens (budget retrieval)
└── Réponse          :   4,000 tokens (output)
    Reserve          : 123,000 tokens (marge sécurité)
```

#### Stratégie de Troncature

```cpp
std::string smartTruncate(const std::string& content, size_t maxChars) {
    if (content.size() <= maxChars) {
        return content;
    }

    // Option 1: Début + fin (préserve intro et conclusion)
    size_t headSize = maxChars * 0.7;
    size_t tailSize = maxChars * 0.2;

    return content.substr(0, headSize)
         + "\n\n[... contenu tronqué ...]\n\n"
         + content.substr(content.size() - tailSize);
}
```

#### Monitoring Usage

```cpp
class ContextBudget {
    const size_t MAX_DOCUMENT_TOKENS = 50000;
    size_t usedTokens = 0;

public:
    bool canFit(size_t documentSize) {
        size_t estimatedTokens = documentSize / 4;  // ~4 chars/token
        return (usedTokens + estimatedTokens) <= MAX_DOCUMENT_TOKENS;
    }

    void recordUsage(size_t documentSize) {
        usedTokens += documentSize / 4;
    }

    size_t remainingBudget() {
        return MAX_DOCUMENT_TOKENS - usedTokens;
    }
};
```

## Architecture LLM Agnostique

Le système est conçu pour supporter plusieurs providers LLM via une interface commune. Le pattern "tool use" est standardisé chez la plupart des providers modernes.

### Compatibilité Providers

| Provider | Tool Use | Format | Qualité Tool Use | Notes |
|----------|----------|--------|------------------|-------|
| **Claude** (Anthropic) | Natif | `tool_use` blocks | Excellent | Meilleur raisonnement |
| **OpenAI** | Natif | `tool_calls` array | Très bon | GPT-5, GPT-4.1, o-series |
| **Gemini** (Google) | Natif | `function_call` | Bon | Gemini 3, 2.5, Flash |
| **DeepSeek** | Natif | Compatible OpenAI | Très bon | V3, R1, ultra low-cost |
| **Kimi** (Moonshot) | Natif | Compatible OpenAI | Bon | K2, 1T params, 128K context |
| **Qwen** (Alibaba) | Natif | Compatible OpenAI | Bon | Qwen-Max, Turbo (1M context) |
| **Mistral** | Natif | Compatible OpenAI | Bon | EU-based, Medium 3, Large |
| **Llama** | Natif | Via Ollama | Variable | Open source (Meta) |
| **Local (Ollama)** | Partiel | Dépend du modèle | Variable | 100% local |

> **Sources** : [Anthropic Docs](https://docs.anthropic.com/en/docs/about-claude/models/overview), [OpenAI Models](https://platform.openai.com/docs/models), [Gemini API](https://ai.google.dev/gemini-api/docs/models), [DeepSeek Pricing](https://api-docs.deepseek.com/quick_start/pricing), [Moonshot Platform](https://platform.moonshot.ai/), [Alibaba Model Studio](https://www.alibabacloud.com/help/en/model-studio/models), [Mistral Pricing](https://docs.mistral.ai/deployment/laplateforme/pricing/)

### Interface ILLMProvider

```cpp
// ILLMProvider.hpp - Interface commune pour tous les providers

struct ToolCall {
    std::string id;      // Identifiant unique du call
    std::string name;    // Nom du tool appelé
    json input;          // Arguments passés au tool
};

struct ToolResult {
    std::string tool_call_id;  // Référence au ToolCall
    std::string content;       // Résultat de l'exécution
    bool is_error = false;     // Indique si erreur
};

class ILLMProvider {
public:
    virtual ~ILLMProvider() = default;

    // Envoie messages + tools, retourne réponse brute du provider
    virtual json chat(const std::string& systemPrompt,
                      const json& messages,
                      const json& tools) = 0;

    // Parse la réponse pour extraire les tool calls (format unifié)
    virtual std::vector<ToolCall> parseToolCalls(const json& response) = 0;

    // Formate les résultats des tools pour le prochain message
    virtual json formatToolResults(const std::vector<ToolResult>& results) = 0;

    // Ajoute la réponse assistant à l'historique
    virtual void appendAssistantMessage(json& messages, const json& response) = 0;

    // Check si la réponse est finale ou demande des tools
    virtual bool isEndTurn(const json& response) = 0;

    // Extrait le texte final de la réponse
    virtual std::string extractText(const json& response) = 0;

    // Retourne le nom du provider pour logging
    virtual std::string getProviderName() const = 0;
};
```

### Implémentation Claude

```cpp
// ClaudeProvider.hpp

class ClaudeProvider : public ILLMProvider {
    std::string apiKey;
    std::string model;
    std::string baseUrl = "https://api.anthropic.com/v1";

public:
    ClaudeProvider(const json& config)
        : apiKey(getEnvVar(config.value("api_key_env", "ANTHROPIC_API_KEY")))
        , model(config.value("model", "claude-sonnet-4-20250514"))
        , baseUrl(config.value("base_url", baseUrl)) {}

    json chat(const std::string& systemPrompt,
              const json& messages,
              const json& tools) override {
        json request = {
            {"model", model},
            {"max_tokens", 4096},
            {"system", systemPrompt},
            {"messages", messages},
            {"tools", tools}
        };
        return httpPost(baseUrl + "/messages", request, {
            {"x-api-key", apiKey},
            {"anthropic-version", "2023-06-01"}
        });
    }

    std::vector<ToolCall> parseToolCalls(const json& response) override {
        std::vector<ToolCall> calls;
        for (const auto& block : response["content"]) {
            if (block["type"] == "tool_use") {
                calls.push_back({
                    block["id"],
                    block["name"],
                    block["input"]
                });
            }
        }
        return calls;
    }

    json formatToolResults(const std::vector<ToolResult>& results) override {
        // Claude: array de tool_result dans un message user
        json content = json::array();
        for (const auto& r : results) {
            content.push_back({
                {"type", "tool_result"},
                {"tool_use_id", r.tool_call_id},
                {"content", r.content},
                {"is_error", r.is_error}
            });
        }
        return {{"role", "user"}, {"content", content}};
    }

    void appendAssistantMessage(json& messages, const json& response) override {
        messages.push_back({
            {"role", "assistant"},
            {"content", response["content"]}
        });
    }

    bool isEndTurn(const json& response) override {
        return response["stop_reason"] == "end_turn";
    }

    std::string extractText(const json& response) override {
        for (const auto& block : response["content"]) {
            if (block["type"] == "text") {
                return block["text"];
            }
        }
        return "";
    }

    std::string getProviderName() const override { return "claude"; }
};
```

### Implémentation OpenAI

```cpp
// OpenAIProvider.hpp

class OpenAIProvider : public ILLMProvider {
    std::string apiKey;
    std::string model;
    std::string baseUrl = "https://api.openai.com/v1";

public:
    OpenAIProvider(const json& config)
        : apiKey(getEnvVar(config.value("api_key_env", "OPENAI_API_KEY")))
        , model(config.value("model", "gpt-4o"))
        , baseUrl(config.value("base_url", baseUrl)) {}

    json chat(const std::string& systemPrompt,
              const json& messages,
              const json& tools) override {
        // Convertir tools au format OpenAI
        json openaiTools = json::array();
        for (const auto& tool : tools) {
            openaiTools.push_back({
                {"type", "function"},
                {"function", {
                    {"name", tool["name"]},
                    {"description", tool["description"]},
                    {"parameters", tool["input_schema"]}
                }}
            });
        }

        // Préparer messages avec system prompt
        json allMessages = json::array();
        allMessages.push_back({{"role", "system"}, {"content", systemPrompt}});
        for (const auto& msg : messages) {
            allMessages.push_back(msg);
        }

        json request = {
            {"model", model},
            {"messages", allMessages},
            {"tools", openaiTools}
        };
        return httpPost(baseUrl + "/chat/completions", request, {
            {"Authorization", "Bearer " + apiKey}
        });
    }

    std::vector<ToolCall> parseToolCalls(const json& response) override {
        std::vector<ToolCall> calls;
        auto& message = response["choices"][0]["message"];
        if (message.contains("tool_calls")) {
            for (const auto& tc : message["tool_calls"]) {
                calls.push_back({
                    tc["id"],
                    tc["function"]["name"],
                    json::parse(tc["function"]["arguments"].get<std::string>())
                });
            }
        }
        return calls;
    }

    json formatToolResults(const std::vector<ToolResult>& results) override {
        // OpenAI: messages séparés avec role "tool"
        json messages = json::array();
        for (const auto& r : results) {
            messages.push_back({
                {"role", "tool"},
                {"tool_call_id", r.tool_call_id},
                {"content", r.content}
            });
        }
        return messages;  // Note: retourne array, pas un seul message
    }

    void appendAssistantMessage(json& messages, const json& response) override {
        messages.push_back(response["choices"][0]["message"]);
    }

    bool isEndTurn(const json& response) override {
        auto& message = response["choices"][0]["message"];
        return !message.contains("tool_calls") || message["tool_calls"].empty();
    }

    std::string extractText(const json& response) override {
        return response["choices"][0]["message"]["content"].get<std::string>();
    }

    std::string getProviderName() const override { return "openai"; }
};
```

### Implémentation Ollama (Local)

```cpp
// OllamaProvider.hpp

class OllamaProvider : public ILLMProvider {
    std::string model;
    std::string baseUrl = "http://localhost:11434/api";

public:
    OllamaProvider(const json& config)
        : model(config.value("model", "llama3.1:70b"))
        , baseUrl(config.value("base_url", baseUrl)) {}

    json chat(const std::string& systemPrompt,
              const json& messages,
              const json& tools) override {
        // Format Ollama avec tools (Llama 3.1+)
        json request = {
            {"model", model},
            {"messages", buildMessages(systemPrompt, messages)},
            {"tools", convertToolsFormat(tools)},
            {"stream", false}
        };
        return httpPost(baseUrl + "/chat", request, {});
    }

    // ... autres méthodes similaires à OpenAI

    std::string getProviderName() const override { return "ollama"; }

private:
    json buildMessages(const std::string& systemPrompt, const json& messages) {
        json result = json::array();
        result.push_back({{"role", "system"}, {"content", systemPrompt}});
        for (const auto& msg : messages) {
            result.push_back(msg);
        }
        return result;
    }
};
```

### Factory Pattern

```cpp
// LLMProviderFactory.hpp

class LLMProviderFactory {
public:
    static std::unique_ptr<ILLMProvider> create(const json& config) {
        std::string provider = config.value("provider", "claude");

        if (provider == "claude") {
            return std::make_unique<ClaudeProvider>(config["providers"]["claude"]);
        }
        if (provider == "openai") {
            return std::make_unique<OpenAIProvider>(config["providers"]["openai"]);
        }
        if (provider == "ollama") {
            return std::make_unique<OllamaProvider>(config["providers"]["ollama"]);
        }

        throw std::runtime_error("Unknown LLM provider: " + provider);
    }
};
```

### Boucle Agentique - Fonctionnement Détaillé

#### Principe fondamental

Le LLM **ne fait PAS** les appels API lui-même. C'est **ton code** qui :
1. Reçoit la demande de tool du LLM
2. Exécute l'appel (API externe, lecture fichier, etc.)
3. Renvoie le résultat au LLM
4. Relance un appel LLM avec le contexte enrichi

#### Flux visuel annoté

```
┌─────────────────────────────────────────────────────────────────────────┐
│  EXEMPLE: User demande "Quelle météo à Paris ?"                         │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  ┌─────────────────────────────────────────────────────────────────┐    │
│  │ ÉTAPE 1: Initialisation                                         │    │
│  │                                                                 │    │
│  │ messages = [                                                    │    │
│  │   {role: "user", content: "Quelle météo à Paris ?"}             │    │
│  │ ]                                                               │    │
│  └─────────────────────────────────────────────────────────────────┘    │
│                              │                                          │
│                              ▼                                          │
│  ┌─────────────────────────────────────────────────────────────────┐    │
│  │ ÉTAPE 2: Premier appel LLM                                      │    │
│  │                                                                 │    │
│  │ response = llm.chat(messages, tools)                            │    │
│  │                                                                 │    │
│  │ LLM analyse et décide: "J'ai besoin de get_weather"             │    │
│  │                                                                 │    │
│  │ Réponse LLM:                                                    │    │
│  │ {                                                               │    │
│  │   stop_reason: "tool_use",     ← PAS "end_turn", on continue!   │    │
│  │   content: [{                                                   │    │
│  │     type: "tool_use",                                           │    │
│  │     id: "call_123",                                             │    │
│  │     name: "get_weather",                                        │    │
│  │     input: {city: "Paris"}                                      │    │
│  │   }]                                                            │    │
│  │ }                                                               │    │
│  └─────────────────────────────────────────────────────────────────┘    │
│                              │                                          │
│                              ▼                                          │
│  ┌─────────────────────────────────────────────────────────────────┐    │
│  │ ÉTAPE 3: TON CODE exécute le tool                               │    │
│  │                                                                 │    │
│  │ // C'est ICI que tu fais l'appel API externe                    │    │
│  │ result = executeTool("get_weather", {city: "Paris"})            │    │
│  │                                                                 │    │
│  │ // Ton implémentation:                                          │    │
│  │ json executeTool(name, input) {                                 │    │
│  │   if (name == "get_weather") {                                  │    │
│  │     return httpGet("https://api.weather.com", input);  ← API!   │    │
│  │   }                                                             │    │
│  │ }                                                               │    │
│  │                                                                 │    │
│  │ Résultat: {temp: 12, condition: "nuageux"}                      │    │
│  └─────────────────────────────────────────────────────────────────┘    │
│                              │                                          │
│                              ▼                                          │
│  ┌─────────────────────────────────────────────────────────────────┐    │
│  │ ÉTAPE 4: Enrichir le contexte                                   │    │
│  │                                                                 │    │
│  │ messages = [                                                    │    │
│  │   {role: "user", content: "Quelle météo à Paris ?"},            │    │
│  │   {role: "assistant", content: [{tool_use...}]},     ← AJOUTÉ   │    │
│  │   {role: "user", content: [{                         ← AJOUTÉ   │    │
│  │     type: "tool_result",                                        │    │
│  │     tool_use_id: "call_123",                                    │    │
│  │     content: "{temp: 12, condition: nuageux}"                   │    │
│  │   }]}                                                           │    │
│  │ ]                                                               │    │
│  └─────────────────────────────────────────────────────────────────┘    │
│                              │                                          │
│                              ▼                                          │
│  ┌─────────────────────────────────────────────────────────────────┐    │
│  │ ÉTAPE 5: DEUXIÈME appel LLM (avec contexte enrichi)             │    │
│  │                                                                 │    │
│  │ response = llm.chat(messages, tools)  ← MÊME array enrichi      │    │
│  │                                                                 │    │
│  │ LLM voit le résultat du tool et génère la réponse finale        │    │
│  │                                                                 │    │
│  │ Réponse LLM:                                                    │    │
│  │ {                                                               │    │
│  │   stop_reason: "end_turn",     ← MAINTENANT c'est fini!         │    │
│  │   content: [{                                                   │    │
│  │     type: "text",                                               │    │
│  │     text: "Il fait 12°C à Paris avec un temps nuageux."         │    │
│  │   }]                                                            │    │
│  │ }                                                               │    │
│  └─────────────────────────────────────────────────────────────────┘    │
│                              │                                          │
│                              ▼                                          │
│  ┌─────────────────────────────────────────────────────────────────┐    │
│  │ ÉTAPE 6: Retourner au user                                      │    │
│  │                                                                 │    │
│  │ return "Il fait 12°C à Paris avec un temps nuageux."            │    │
│  └─────────────────────────────────────────────────────────────────┘    │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘
```

#### Code annoté

```cpp
json AIAssistantModule::agenticLoop(const std::string& userQuery) {

    // ═══════════════════════════════════════════════════════════════════
    // ÉTAPE 1: Initialiser l'historique avec la question du user
    // ═══════════════════════════════════════════════════════════════════
    json messages = json::array();
    messages.push_back({{"role", "user"}, {"content", userQuery}});

    int iterations = 0;
    const int MAX_ITERATIONS = config["max_iterations"].get<int>();

    // ═══════════════════════════════════════════════════════════════════
    // BOUCLE PRINCIPALE - Continue tant que le LLM demande des tools
    // Chaque tour = 1 appel API LLM payant
    // ═══════════════════════════════════════════════════════════════════
    while (iterations++ < MAX_ITERATIONS) {

        // ───────────────────────────────────────────────────────────────
        // ÉTAPE 2/5/8...: Appel LLM avec tout le contexte accumulé
        // Le LLM voit: question initiale + tous les tools calls + résultats
        // ───────────────────────────────────────────────────────────────
        auto response = provider->chat(systemPrompt, messages, tools);

        // ───────────────────────────────────────────────────────────────
        // CHECK: Est-ce que le LLM a fini ou veut encore des tools ?
        // stop_reason == "end_turn" → Réponse finale, on sort
        // stop_reason == "tool_use" → Il veut des données, on continue
        // ───────────────────────────────────────────────────────────────
        if (provider->isEndTurn(response)) {
            // ÉTAPE 6: C'est fini! Extraire et retourner la réponse
            return {
                {"response", provider->extractText(response)},
                {"iterations", iterations},
                {"provider", provider->getProviderName()}
            };
        }

        // ───────────────────────────────────────────────────────────────
        // ÉTAPE 3: Le LLM veut des tools - Parser sa demande
        // Il peut demander PLUSIEURS tools en une fois (parallèle)
        // ───────────────────────────────────────────────────────────────
        auto toolCalls = provider->parseToolCalls(response);
        if (toolCalls.empty()) {
            return {{"error", "unexpected_state"}};
        }

        // ───────────────────────────────────────────────────────────────
        // ÉTAPE 3 (suite): EXÉCUTER les tools
        // C'est ICI que TON CODE fait le travail:
        // - Appels API externes (météo, recherche web, etc.)
        // - Lecture de fichiers
        // - Requêtes base de données
        // - N'importe quelle opération
        // ───────────────────────────────────────────────────────────────
        std::vector<ToolResult> results;
        for (const auto& call : toolCalls) {
            // executeTool() = TA fonction qui fait le vrai travail
            auto result = executeTool(call.name, call.input);
            results.push_back({call.id, result.dump(), false});
        }

        // ───────────────────────────────────────────────────────────────
        // ÉTAPE 4: Enrichir l'historique pour le prochain appel LLM
        // On ajoute:
        // 1. Ce que le LLM a demandé (assistant message avec tool_use)
        // 2. Les résultats qu'on a obtenus (user message avec tool_result)
        // ───────────────────────────────────────────────────────────────
        provider->appendAssistantMessage(messages, response);

        // Formater les résultats selon le provider (Claude vs OpenAI)
        auto toolResultsMsg = provider->formatToolResults(results);
        if (toolResultsMsg.is_array()) {
            // OpenAI: chaque tool_result = un message séparé
            for (const auto& msg : toolResultsMsg) {
                messages.push_back(msg);
            }
        } else {
            // Claude: tous les tool_results dans un seul message
            messages.push_back(toolResultsMsg);
        }

        // → RETOUR AU DÉBUT DE LA BOUCLE
        // Le prochain llm.chat() verra tout l'historique enrichi
    }

    return {{"error", "max_iterations_reached"}};
}
```

#### Coût par scénario

| Scénario | Tours de boucle | Appels LLM | Coût (~Sonnet) |
|----------|-----------------|------------|----------------|
| Question simple (pas de tool) | 1 | 1 | ~$0.01 |
| 1 tool call | 2 | 2 | ~$0.02 |
| 2 tools en parallèle | 2 | 2 | ~$0.02 |
| 2 tools séquentiels | 3 | 3 | ~$0.03 |
| Search docs → Read doc → Réponse | 3 | 3 | ~$0.03 |
| Recherche complexe (5 tools) | 4-6 | 4-6 | ~$0.05 |

### Tools avec Appels API Externes

#### Architecture des Tools

```cpp
// ═══════════════════════════════════════════════════════════════════════
// Interface pour tous les tools (locaux ou API)
// ═══════════════════════════════════════════════════════════════════════

class IToolExecutor {
public:
    virtual ~IToolExecutor() = default;

    // Exécute le tool et retourne le résultat (JSON)
    virtual json execute(const json& input) = 0;

    // Définition du tool pour le LLM (nom, description, paramètres)
    virtual json getToolDefinition() = 0;

    // Timeout spécifique (certaines API sont lentes)
    virtual std::chrono::milliseconds getTimeout() {
        return std::chrono::milliseconds(30000);  // 30s par défaut
    }
};
```

#### Exemple: Tool avec API externe (météo)

```cpp
// ═══════════════════════════════════════════════════════════════════════
// WeatherTool - Appelle une API météo externe
// ═══════════════════════════════════════════════════════════════════════

class WeatherTool : public IToolExecutor {
    std::string apiKey;

public:
    WeatherTool() : apiKey(getEnvVar("WEATHER_API_KEY")) {}

    // ───────────────────────────────────────────────────────────────────
    // Définition envoyée au LLM pour qu'il sache comment utiliser ce tool
    // ───────────────────────────────────────────────────────────────────
    json getToolDefinition() override {
        return {
            {"name", "get_weather"},
            {"description", "Obtient la météo actuelle pour une ville donnée."},
            {"input_schema", {
                {"type", "object"},
                {"properties", {
                    {"city", {
                        {"type", "string"},
                        {"description", "Nom de la ville (ex: Paris, Tokyo)"}
                    }}
                }},
                {"required", {"city"}}
            }}
        };
    }

    // ───────────────────────────────────────────────────────────────────
    // Exécution: C'est ICI que l'appel API externe se fait
    // Le LLM ne fait JAMAIS cet appel - c'est ton code
    // ───────────────────────────────────────────────────────────────────
    json execute(const json& input) override {
        std::string city = input["city"];

        // ┌─────────────────────────────────────────────────────────────┐
        // │ APPEL API EXTERNE - Ton code HTTP                          │
        // └─────────────────────────────────────────────────────────────┘
        auto response = httpGet(
            "https://api.weatherapi.com/v1/current.json",
            {
                {"key", apiKey},
                {"q", city}
            }
        );

        // Gestion erreur
        if (response.status != 200) {
            return {
                {"error", "weather_api_failed"},
                {"status", response.status}
            };
        }

        // ┌─────────────────────────────────────────────────────────────┐
        // │ FORMATER le résultat pour le LLM                           │
        // │ Pas besoin de tout renvoyer, juste l'essentiel             │
        // └─────────────────────────────────────────────────────────────┘
        return {
            {"city", city},
            {"temp_c", response.body["current"]["temp_c"]},
            {"condition", response.body["current"]["condition"]["text"]},
            {"humidity", response.body["current"]["humidity"]}
        };
    }
};
```

#### Exemple: Tool de recherche web

```cpp
class WebSearchTool : public IToolExecutor {
    std::string apiKey;

public:
    WebSearchTool() : apiKey(getEnvVar("SERPER_API_KEY")) {}

    json getToolDefinition() override {
        return {
            {"name", "web_search"},
            {"description", "Recherche sur le web. Pour informations récentes ou externes."},
            {"input_schema", {
                {"type", "object"},
                {"properties", {
                    {"query", {{"type", "string"}, {"description", "Requête de recherche"}}},
                    {"num_results", {{"type", "integer"}, {"default", 5}}}
                }},
                {"required", {"query"}}
            }}
        };
    }

    json execute(const json& input) override {
        std::string query = input["query"];
        int numResults = input.value("num_results", 5);

        // Appel API Serper (moteur de recherche)
        auto response = httpPost(
            "https://google.serper.dev/search",
            {{"q", query}, {"num", numResults}},
            {{"X-API-KEY", apiKey}}
        );

        if (response.status != 200) {
            return {{"error", "search_failed"}};
        }

        // Extraire les résultats pertinents
        json results = json::array();
        for (const auto& item : response.body["organic"]) {
            results.push_back({
                {"title", item["title"]},
                {"snippet", item["snippet"]},
                {"url", item["link"]}
            });
        }

        return {{"results", results}, {"query", query}};
    }

    // Timeout plus long pour recherche web
    std::chrono::milliseconds getTimeout() override {
        return std::chrono::milliseconds(15000);  // 15s
    }
};
```

#### Registry: Gérer tous les tools

```cpp
// ═══════════════════════════════════════════════════════════════════════
// ToolRegistry - Centralise tous les tools disponibles
// ═══════════════════════════════════════════════════════════════════════

class ToolRegistry {
    std::map<std::string, std::unique_ptr<IToolExecutor>> tools;

public:
    // Enregistrer un tool
    void registerTool(std::unique_ptr<IToolExecutor> tool) {
        auto def = tool->getToolDefinition();
        std::string name = def["name"];
        tools[name] = std::move(tool);
    }

    // Générer les définitions pour l'API LLM
    json getToolDefinitions() {
        json defs = json::array();
        for (const auto& [name, tool] : tools) {
            defs.push_back(tool->getToolDefinition());
        }
        return defs;
    }

    // Exécuter un tool par nom (appelé dans la boucle agentique)
    json execute(const std::string& name, const json& input) {
        if (tools.find(name) == tools.end()) {
            return {{"error", "unknown_tool"}, {"name", name}};
        }

        try {
            return tools[name]->execute(input);
        } catch (const std::exception& e) {
            return {{"error", "execution_failed"}, {"message", e.what()}};
        }
    }
};

// ═══════════════════════════════════════════════════════════════════════
// Utilisation dans AIAssistantModule
// ═══════════════════════════════════════════════════════════════════════

class AIAssistantModule {
    ToolRegistry toolRegistry;

public:
    AIAssistantModule() {
        // Enregistrer tous les tools disponibles
        toolRegistry.registerTool(std::make_unique<ListDocumentsTool>());
        toolRegistry.registerTool(std::make_unique<ReadDocumentTool>());
        toolRegistry.registerTool(std::make_unique<SearchDocumentsTool>());
        toolRegistry.registerTool(std::make_unique<WeatherTool>());
        toolRegistry.registerTool(std::make_unique<WebSearchTool>());
        // ... autres tools
    }

    json executeTool(const std::string& name, const json& input) {
        return toolRegistry.execute(name, input);
    }
};
```

#### Qui fait quoi - Récapitulatif

| Action | LLM | Ton Code |
|--------|:---:|:--------:|
| Analyser la question du user | ✅ | |
| Décider quel(s) tool(s) appeler | ✅ | |
| Générer les arguments du tool | ✅ | |
| **Exécuter l'appel HTTP/API** | | ✅ |
| **Gérer les erreurs réseau** | | ✅ |
| **Gérer les timeouts** | | ✅ |
| Interpréter le résultat du tool | ✅ | |
| Décider si besoin d'autres tools | ✅ | |
| Générer la réponse finale | ✅ | |

### Gestion Erreurs API

```cpp
json ILLMProvider::chatWithRetry(const std::string& systemPrompt,
                                  const json& messages,
                                  const json& tools,
                                  int maxRetries) {
    int retries = 0;

    while (retries < maxRetries) {
        try {
            auto response = chat(systemPrompt, messages, tools);

            // Vérifier si réponse valide
            if (isValidResponse(response)) {
                return response;
            }

            // Rate limit (429)
            if (isRateLimited(response)) {
                std::this_thread::sleep_for(
                    std::chrono::seconds(1 << retries)  // Exponential backoff
                );
                retries++;
                continue;
            }

            // Server error (5xx)
            if (isServerError(response)) {
                retries++;
                continue;
            }

            // Client error (4xx) - ne pas retry
            return {{"error", getErrorMessage(response)}};

        } catch (const std::exception& e) {
            retries++;
        }
    }

    return {{"error", "max_retries_exceeded"}};
}
```

## Estimation Coûts

### Coût par Requête par Provider

#### Anthropic Claude (Novembre 2025)

| Modèle | API ID | Input/1M | Output/1M | Context | Notes |
|--------|--------|----------|-----------|---------|-------|
| **Claude Sonnet 4.5** | `claude-sonnet-4-5-20250929` | $3.00 | $15.00 | 200K (1M beta) | Frontier, meilleur coding |
| **Claude Haiku 4.5** | `claude-haiku-4-5` | $1.00 | $5.00 | 200K | Rapide, 1/3 coût Sonnet 4 |
| **Claude Opus 4.1** | `claude-opus-4-1` | $15.00 | $75.00 | 200K | Deep reasoning |
| **Claude Sonnet 4** | `claude-sonnet-4-20250514` | $3.00 | $15.00 | 200K | Stable |

#### OpenAI (Novembre 2025)

| Modèle | API ID | Input/1M | Output/1M | Context | Notes |
|--------|--------|----------|-----------|---------|-------|
| **GPT-5** | `gpt-5` | ~$5.00 | ~$15.00 | 256K | Multimodal, SOTA |
| **GPT-5 Mini** | `gpt-5-mini` | ~$0.50 | ~$1.50 | 128K | Léger |
| **GPT-5 Nano** | `gpt-5-nano` | ~$0.10 | ~$0.30 | 32K | Ultra-léger |
| **GPT-4.1** | `gpt-4.1` | $2.00 | $8.00 | 1M | Long context |
| **GPT-4.1 Mini** | `gpt-4.1-mini` | $0.40 | $1.60 | 1M | Budget-friendly |
| **GPT-4.1 Nano** | `gpt-4.1-nano` | $0.10 | $0.40 | 1M | Ultra low-cost |
| **GPT-4o** | `gpt-4o` | $2.50 | $10.00 | 128K | Legacy multimodal |
| **o3** | `o3` | Variable | Variable | - | Reasoning |

#### Google Gemini (Novembre 2025)

| Modèle | API ID | Input/1M | Output/1M | Context | Notes |
|--------|--------|----------|-----------|---------|-------|
| **Gemini 3 Pro** | `gemini-3.0-pro` | $2.00 | $12.00 | 200K | Newest, best reasoning |
| **Gemini 2.5 Pro** | `gemini-2.5-pro` | $4.00 | $20.00 | 1M | Thinking model |
| **Gemini 2.5 Flash** | `gemini-2.5-flash` | $0.15 | $0.60 | 1M | Fast, thinking enabled |
| **Gemini 2.5 Flash-Lite** | `gemini-2.5-flash-lite` | $0.02 | $0.08 | - | Ultra low-cost |
| **Gemini 1.5 Pro** | `gemini-1.5-pro` | $1.25 | $5.00 | 2M | Stable |
| **Gemini 1.5 Flash** | `gemini-1.5-flash` | $0.075 | $0.30 | 1M | Fast |

#### DeepSeek (Septembre 2025)

| Modèle | API ID | Input/1M | Output/1M | Context | Notes |
|--------|--------|----------|-----------|---------|-------|
| **DeepSeek V3** | `deepseek-chat` | $0.07 (hit) / $0.56 | $1.68 | 128K | Généraliste |
| **DeepSeek R1** | `deepseek-reasoner` | $0.07 (hit) / $0.56 | $1.68 | 64K output | Chain-of-thought |
| **DeepSeek V3.2-Exp** | `deepseek-v3.2-exp` | $0.028 | ~$0.84 | 128K | 50% moins cher, MIT license |

#### Kimi / Moonshot (Novembre 2025)

| Modèle | API ID | Input/1M | Output/1M | Context | Notes |
|--------|--------|----------|-----------|---------|-------|
| **Kimi K2** | `kimi-k2` | $0.15 (hit) | $2.50 | 128K | 1T params MoE, 32B active |
| **Kimi K2 Thinking** | `kimi-k2-thinking` | ~$0.30 | ~$3.00 | 128K | Agentic, reasoning |
| **Moonshot v1 128K** | `moonshot-v1-128k` | $0.82 | $0.82 | 128K | Legacy |

#### Alibaba Qwen (2025)

| Modèle | API ID | Input/1M | Output/1M | Context | Notes |
|--------|--------|----------|-----------|---------|-------|
| **Qwen-Max** | `qwen-max` | ~$1.60 | ~$6.40 | 32K | Flagship |
| **Qwen-Plus** | `qwen-plus` | ~$0.80 | ~$3.20 | 128K | Balanced |
| **Qwen-Turbo** | `qwen-turbo` | ~$0.20 | ~$0.80 | 1M | Fast, long context |
| **Qwen3-Coder** | `qwen3-coder-plus` | Variable | Variable | - | Coding specialized |

#### Mistral (Mai 2025)

| Modèle | API ID | Input/1M | Output/1M | Context | Notes |
|--------|--------|----------|-----------|---------|-------|
| **Mistral Medium 3** | `mistral-medium-3` | $0.40 | $2.00 | - | Nouveau, compétitif |
| **Mistral Large** | `mistral-large-latest` | $2.00 | $6.00 | 128K | Flagship |
| **Mistral Small** | `mistral-small-latest` | $0.20 | $0.60 | 32K | Budget |

#### Local (Ollama)

| Modèle | Coût | Notes |
|--------|------|-------|
| **Llama 3.1 70B** | Gratuit | Requires ~40GB VRAM |
| **Llama 3.1 8B** | Gratuit | Runs on consumer GPU |
| **Qwen 2.5 72B** | Gratuit | Good multilingual |
| **DeepSeek V3** | Gratuit | MIT license, self-host |

> **Sources** : [Anthropic Models](https://docs.anthropic.com/en/docs/about-claude/models/overview), [OpenAI GPT-5](https://openai.com/index/introducing-gpt-5/), [OpenAI GPT-4.1](https://openai.com/index/gpt-4-1/), [Gemini Pricing](https://ai.google.dev/gemini-api/docs/pricing), [DeepSeek Pricing](https://api-docs.deepseek.com/quick_start/pricing), [Moonshot Pricing](https://platform.moonshot.ai/docs/pricing/chat), [Mistral Pricing](https://docs.mistral.ai/deployment/laplateforme/pricing/)

### Détail par Opération (Claude Sonnet)

| Opération | Tokens (input) | Tokens (output) | Coût |
|-----------|----------------|-----------------|------|
| list_documents (100 docs) | ~1,500 | ~50 | $0.005 |
| search_documents | ~500 | ~50 | $0.002 |
| read_document (8KB) | ~2,500 | ~50 | $0.008 |
| Réponse finale | ~500 | ~500 | $0.005 |
| **Total typique** | ~5,000 | ~650 | **~$0.02** |

### Comparaison Stratégies

| Stratégie | Tokens/requête | Coût/requête | Latence |
|-----------|----------------|--------------|---------|
| Charger tous docs | ~125,000 | ~$0.40 | ~10s |
| Retrieval agentique | ~5,000 | ~$0.02 | ~3s |
| **Économie** | **96%** | **95%** | **70%** |

## Plan d'Implémentation

### Phase 1 : MVP (Priorité Haute)

**Objectif** : Système fonctionnel avec keyword search et architecture LLM agnostique

1. **ILLMProvider + ClaudeProvider** (~150 lignes)
   - Interface ILLMProvider
   - Implémentation ClaudeProvider
   - Gestion retry et rate limiting
   - Parsing réponses tool_use

2. **DocumentStore** (~150 lignes)
   - Scan directory et extraction metadata
   - Index en mémoire (JSON)
   - Keyword search basique
   - Lecture avec troncature

3. **AIAssistantModule** (~200 lignes)
   - Boucle agentique unifiée
   - Définition tools
   - Exécution tools
   - Extraction réponse finale

4. **Tests**
   - Test unitaire DocumentStore
   - Test unitaire ILLMProvider (mock)
   - Test intégration avec API réelle

### Phase 2 : Multi-Provider (Priorité Moyenne)

1. **Providers Additionnels**
   - OpenAIProvider (~100 lignes)
   - OllamaProvider (~100 lignes)
   - LLMProviderFactory

2. **Index Persistant**
   - Sauvegarder index.json sur disque
   - Rebuild incrémental (hash fichiers)
   - File watcher pour auto-update

3. **Métriques et Logging**
   - Logging requêtes/réponses par provider
   - Tracking usage tokens
   - Comparaison performance providers

### Phase 3 : Optimisations (Priorité Moyenne)

1. **Cache Réponses**
   - Cache LRU pour documents fréquents
   - Cache embeddings (si Phase 4)
   - TTL configurable

2. **Fallback Automatique**
   - Basculement sur provider secondaire si erreur
   - Configuration priorité providers
   - Health check providers

3. **Provider Selection Dynamique**
   - Sélection basée sur type de requête
   - Budget-aware routing
   - Latency-aware routing

### Phase 4 : Semantic Search (Priorité Basse)

1. **Embeddings Pipeline**
   - Interface IEmbeddingProvider
   - Implémentations (OpenAI, Voyage, local)
   - Génération embeddings au build index
   - Storage SQLite avec sqlite-vss

2. **Hybrid Search**
   - Fusion keyword + semantic
   - Tuning pondération
   - A/B testing efficacité

3. **Advanced Features**
   - Clustering documents similaires
   - Suggestions proactives
   - Résumés automatiques gros documents

## Configuration

### config/ai-assistant.json

```json
{
  "documents_path": "./data/documents",
  "index_path": "./data/index.json",

  "retrieval": {
    "strategy": "keyword",
    "max_results": 10,
    "default_max_chars": 8000
  },

  "context_budget": {
    "max_document_tokens": 50000,
    "truncation_strategy": "head_tail"
  },

  "llm": {
    "provider": "claude",
    "max_iterations": 10,
    "retry_attempts": 3,

    "providers": {
      "_comment": "=== ANTHROPIC CLAUDE ===",
      "claude_sonnet_4_5": {
        "api_key_env": "ANTHROPIC_API_KEY",
        "model": "claude-sonnet-4-5-20250929",
        "max_tokens": 8192,
        "base_url": "https://api.anthropic.com/v1"
      },
      "claude_haiku_4_5": {
        "api_key_env": "ANTHROPIC_API_KEY",
        "model": "claude-haiku-4-5",
        "max_tokens": 8192,
        "base_url": "https://api.anthropic.com/v1"
      },
      "claude_opus_4_1": {
        "api_key_env": "ANTHROPIC_API_KEY",
        "model": "claude-opus-4-1",
        "max_tokens": 8192,
        "base_url": "https://api.anthropic.com/v1"
      },
      "claude_sonnet_4": {
        "api_key_env": "ANTHROPIC_API_KEY",
        "model": "claude-sonnet-4-20250514",
        "max_tokens": 8192,
        "base_url": "https://api.anthropic.com/v1"
      },

      "_comment2": "=== OPENAI ===",
      "gpt5": {
        "api_key_env": "OPENAI_API_KEY",
        "model": "gpt-5",
        "max_tokens": 16384,
        "base_url": "https://api.openai.com/v1"
      },
      "gpt5_mini": {
        "api_key_env": "OPENAI_API_KEY",
        "model": "gpt-5-mini",
        "max_tokens": 16384,
        "base_url": "https://api.openai.com/v1"
      },
      "gpt5_nano": {
        "api_key_env": "OPENAI_API_KEY",
        "model": "gpt-5-nano",
        "max_tokens": 8192,
        "base_url": "https://api.openai.com/v1"
      },
      "gpt4_1": {
        "api_key_env": "OPENAI_API_KEY",
        "model": "gpt-4.1",
        "max_tokens": 16384,
        "base_url": "https://api.openai.com/v1"
      },
      "gpt4_1_mini": {
        "api_key_env": "OPENAI_API_KEY",
        "model": "gpt-4.1-mini",
        "max_tokens": 16384,
        "base_url": "https://api.openai.com/v1"
      },
      "gpt4_1_nano": {
        "api_key_env": "OPENAI_API_KEY",
        "model": "gpt-4.1-nano",
        "max_tokens": 8192,
        "base_url": "https://api.openai.com/v1"
      },
      "gpt4o": {
        "api_key_env": "OPENAI_API_KEY",
        "model": "gpt-4o",
        "max_tokens": 4096,
        "base_url": "https://api.openai.com/v1"
      },
      "o3": {
        "api_key_env": "OPENAI_API_KEY",
        "model": "o3",
        "max_tokens": 32768,
        "base_url": "https://api.openai.com/v1"
      },

      "_comment3": "=== GOOGLE GEMINI ===",
      "gemini_3_pro": {
        "api_key_env": "GOOGLE_API_KEY",
        "model": "gemini-3.0-pro",
        "max_tokens": 8192,
        "base_url": "https://generativelanguage.googleapis.com/v1beta"
      },
      "gemini_2_5_pro": {
        "api_key_env": "GOOGLE_API_KEY",
        "model": "gemini-2.5-pro",
        "max_tokens": 8192,
        "base_url": "https://generativelanguage.googleapis.com/v1beta"
      },
      "gemini_2_5_flash": {
        "api_key_env": "GOOGLE_API_KEY",
        "model": "gemini-2.5-flash",
        "max_tokens": 8192,
        "base_url": "https://generativelanguage.googleapis.com/v1beta"
      },
      "gemini_1_5_pro": {
        "api_key_env": "GOOGLE_API_KEY",
        "model": "gemini-1.5-pro",
        "max_tokens": 8192,
        "base_url": "https://generativelanguage.googleapis.com/v1beta"
      },
      "gemini_1_5_flash": {
        "api_key_env": "GOOGLE_API_KEY",
        "model": "gemini-1.5-flash",
        "max_tokens": 8192,
        "base_url": "https://generativelanguage.googleapis.com/v1beta"
      },

      "_comment4": "=== DEEPSEEK ===",
      "deepseek": {
        "api_key_env": "DEEPSEEK_API_KEY",
        "model": "deepseek-chat",
        "max_tokens": 8192,
        "base_url": "https://api.deepseek.com/v1"
      },
      "deepseek_reasoner": {
        "api_key_env": "DEEPSEEK_API_KEY",
        "model": "deepseek-reasoner",
        "max_tokens": 65536,
        "base_url": "https://api.deepseek.com/v1"
      },

      "_comment5": "=== KIMI / MOONSHOT ===",
      "kimi_k2": {
        "api_key_env": "MOONSHOT_API_KEY",
        "model": "kimi-k2",
        "max_tokens": 8192,
        "base_url": "https://api.moonshot.cn/v1"
      },
      "kimi_k2_thinking": {
        "api_key_env": "MOONSHOT_API_KEY",
        "model": "kimi-k2-thinking",
        "max_tokens": 8192,
        "base_url": "https://api.moonshot.cn/v1"
      },

      "_comment6": "=== ALIBABA QWEN ===",
      "qwen_max": {
        "api_key_env": "DASHSCOPE_API_KEY",
        "model": "qwen-max",
        "max_tokens": 8192,
        "base_url": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
      },
      "qwen_plus": {
        "api_key_env": "DASHSCOPE_API_KEY",
        "model": "qwen-plus",
        "max_tokens": 8192,
        "base_url": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
      },
      "qwen_turbo": {
        "api_key_env": "DASHSCOPE_API_KEY",
        "model": "qwen-turbo",
        "max_tokens": 8192,
        "base_url": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
      },

      "_comment7": "=== MISTRAL ===",
      "mistral_medium": {
        "api_key_env": "MISTRAL_API_KEY",
        "model": "mistral-medium-3",
        "max_tokens": 8192,
        "base_url": "https://api.mistral.ai/v1"
      },
      "mistral_large": {
        "api_key_env": "MISTRAL_API_KEY",
        "model": "mistral-large-latest",
        "max_tokens": 8192,
        "base_url": "https://api.mistral.ai/v1"
      },
      "mistral_small": {
        "api_key_env": "MISTRAL_API_KEY",
        "model": "mistral-small-latest",
        "max_tokens": 8192,
        "base_url": "https://api.mistral.ai/v1"
      },

      "_comment8": "=== LOCAL (OLLAMA) ===",
      "ollama_llama": {
        "model": "llama3.1:70b",
        "base_url": "http://localhost:11434/api"
      },
      "ollama_llama_small": {
        "model": "llama3.1:8b",
        "base_url": "http://localhost:11434/api"
      },
      "ollama_qwen": {
        "model": "qwen2.5:72b",
        "base_url": "http://localhost:11434/api"
      },
      "ollama_deepseek": {
        "model": "deepseek-v3",
        "base_url": "http://localhost:11434/api"
      }
    },

    "fallback": {
      "enabled": true,
      "order": ["claude", "openai", "ollama"]
    }
  },

  "embeddings": {
    "enabled": false,
    "provider": "openai",
    "providers": {
      "openai": {
        "api_key_env": "OPENAI_API_KEY",
        "model": "text-embedding-3-small"
      },
      "voyage": {
        "api_key_env": "VOYAGE_API_KEY",
        "model": "voyage-3"
      }
    },
    "cache_path": "./data/embeddings.db"
  }
}
```

### Variables d'Environnement

```bash
# === ANTHROPIC ===
export ANTHROPIC_API_KEY="sk-ant-..."

# === OPENAI ===
export OPENAI_API_KEY="sk-..."

# === GOOGLE ===
export GOOGLE_API_KEY="..."

# === DEEPSEEK ===
export DEEPSEEK_API_KEY="sk-..."

# === MOONSHOT (Kimi) ===
export MOONSHOT_API_KEY="sk-..."

# === ALIBABA (Qwen) ===
export DASHSCOPE_API_KEY="sk-..."

# === MISTRAL ===
export MISTRAL_API_KEY="..."

# === EMBEDDINGS (Phase 4) ===
export VOYAGE_API_KEY="..."
```

### Changement de Provider à Runtime

Le provider peut être changé via la configuration sans recompilation :

```json
{
  "llm": {
    "provider": "ollama"  // Switch de "claude" à "ollama"
  }
}
```

Ou programmatiquement :

```cpp
// Changement dynamique
auto newProvider = LLMProviderFactory::create(newConfig);
aiAssistant->setProvider(std::move(newProvider));
```

## Considérations Multi-Provider

### Différences de Comportement

| Aspect | Claude | OpenAI | Gemini | DeepSeek | Kimi | Qwen | Mistral | Ollama |
|--------|--------|--------|--------|----------|------|------|---------|--------|
| Qualité tool use | Excellent | Très bon | Bon | Très bon | Bon | Bon | Bon | Variable |
| Respect instructions | Excellent | Très bon | Bon | Bon | Bon | Bon | Bon | Variable |
| Vitesse | ~2-3s | ~1-2s | ~1-2s | ~2-3s | ~2-3s | ~2-3s | ~1-2s | Hardware |
| Coût | $$$ | $$$ | $$ | $ | $ | $$ | $$ | Gratuit |
| Privacy | Cloud US | Cloud US | Cloud US | Cloud CN | Cloud CN | Cloud CN | Cloud EU | 100% local |
| Long context | 1M beta | 256K | 2M | 128K | 128K | 1M | 128K | Dépend modèle |

### Recommandations d'Usage

| Cas d'usage | Provider recommandé | Raison |
|-------------|---------------------|--------|
| **Production critique** | Claude Sonnet 4.5 | Meilleur raisonnement tool use |
| **Production budget** | DeepSeek V3 | 95% moins cher, qualité comparable |
| **Développement/Tests** | Ollama | Gratuit, pas de rate limit |
| **Long context (>200K)** | Gemini 1.5 Pro / Qwen Turbo | 2M / 1M tokens |
| **Ultra low-cost** | DeepSeek / Gemini Flash-Lite | < $0.10/1M tokens |
| **Privacy EU** | Mistral | Serveurs EU, GDPR compliant |
| **Privacy totale** | Ollama | 100% local, zero data leak |
| **Reasoning complex** | Claude Opus 4.1 / o3 | Deep thinking |
| **Coding** | Claude Sonnet 4.5 / DeepSeek | SOTA sur SWE-bench |
| **Multilingue (CN/EN)** | Qwen / Kimi | Optimisés bilingue |

### Tests Cross-Provider

```cpp
// Exemple de test comparatif
void testProviderConsistency() {
    std::vector<std::string> providers = {"claude", "openai", "ollama"};
    std::string testQuery = "Liste les documents sur le planning";

    for (const auto& p : providers) {
        config["llm"]["provider"] = p;
        auto provider = LLMProviderFactory::create(config);
        auto result = aiAssistant->query(testQuery);

        // Vérifier que tous trouvent les mêmes documents pertinents
        ASSERT_TRUE(result.contains("planning"));
    }
}
```

## Références

- [Anthropic API Documentation](https://docs.anthropic.com/)
- [Anthropic Tool Use Guide](https://docs.anthropic.com/en/docs/build-with-claude/tool-use)
- [OpenAI Function Calling](https://platform.openai.com/docs/guides/function-calling)
- [Ollama API](https://github.com/ollama/ollama/blob/main/docs/api.md)
- [architecture-technique.md](./architecture-technique.md) - Architecture système CELUNA
- [claude-code-integration.md](./claude-code-integration.md) - Patterns développement Claude Code
