diff --git a/.gitignore b/.gitignore index 83cefe3..d7dd2af 100644 --- a/.gitignore +++ b/.gitignore @@ -62,4 +62,13 @@ Thumbs.db *.mov *.wmv *.mp3 -*.wav \ No newline at end of file +*.wavbuild/ +*.o +*.a +*.so +*.dll +build/ +*.o +*.a +*.so +*.dll diff --git a/docs/architecture/intelligent-document-retrieval.md b/docs/architecture/intelligent-document-retrieval.md index 38c0802..7dcd8b1 100644 --- a/docs/architecture/intelligent-document-retrieval.md +++ b/docs/architecture/intelligent-document-retrieval.md @@ -615,20 +615,144 @@ public: }; ``` -### Boucle Agentique Unifiée +### 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(); + // ═══════════════════════════════════════════════════════════════════ + // 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}, @@ -636,38 +760,306 @@ json AIAssistantModule::agenticLoop(const std::string& userQuery) { }; } + // ─────────────────────────────────────────────────────────────── + // É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"}}; } - // Exécuter les tools (logique commune à tous les providers) + // ─────────────────────────────────────────────────────────────── + // É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 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}); } - // Ajouter à l'historique (format dépend du provider) + // ─────────────────────────────────────────────────────────────── + // É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); - // Gérer la différence de format pour tool results + // Formater les résultats selon le provider (Claude vs OpenAI) auto toolResultsMsg = provider->formatToolResults(results); if (toolResultsMsg.is_array()) { - // OpenAI: plusieurs messages + // OpenAI: chaque tool_result = un message séparé for (const auto& msg : toolResultsMsg) { messages.push_back(msg); } } else { - // Claude: un seul message + // 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> tools; + +public: + // Enregistrer un tool + void registerTool(std::unique_ptr 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()); + toolRegistry.registerTool(std::make_unique()); + toolRegistry.registerTool(std::make_unique()); + toolRegistry.registerTool(std::make_unique()); + toolRegistry.registerTool(std::make_unique()); + // ... 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