From 80f26aea54e4d16ae3cbfde1ac254fc15d3a9fe5 Mon Sep 17 00:00:00 2001 From: StillHammer Date: Mon, 24 Nov 2025 21:34:16 +0800 Subject: [PATCH] Add infrastructure foundation and intelligent document retrieval MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CMakeLists.txt: build configuration - src/: initial infrastructure structure - config/: application configuration - external/: third-party dependencies - docs/GROVEENGINE_GUIDE.md: GroveEngine reference guide - docs/architecture/intelligent-document-retrieval.md: agentic retrieval for AIAssistantModule 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- CMakeLists.txt | 89 ++ config/notification.json | 6 + config/scheduler.json | 7 + docs/GROVEENGINE_GUIDE.md | 749 ++++++++++ docs/README.md | 8 +- .../intelligent-document-retrieval.md | 1243 +++++++++++++++++ external/GroveEngine | 1 + src/main.cpp | 263 ++++ src/modules/NotificationModule.cpp | 172 +++ src/modules/NotificationModule.h | 96 ++ src/modules/SchedulerModule.cpp | 179 +++ src/modules/SchedulerModule.h | 94 ++ 12 files changed, 2905 insertions(+), 2 deletions(-) create mode 100644 CMakeLists.txt create mode 100644 config/notification.json create mode 100644 config/scheduler.json create mode 100644 docs/GROVEENGINE_GUIDE.md create mode 100644 docs/architecture/intelligent-document-retrieval.md create mode 120000 external/GroveEngine create mode 100644 src/main.cpp create mode 100644 src/modules/NotificationModule.cpp create mode 100644 src/modules/NotificationModule.h create mode 100644 src/modules/SchedulerModule.cpp create mode 100644 src/modules/SchedulerModule.h diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..d70262b --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,89 @@ +cmake_minimum_required(VERSION 3.20) +project(Aissia VERSION 0.1.0 LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Export compile commands for IDE support +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +# ============================================================================ +# GroveEngine Integration +# ============================================================================ +set(GROVE_BUILD_TESTS OFF CACHE BOOL "Disable GroveEngine tests" FORCE) +add_subdirectory(external/GroveEngine) + +# ============================================================================ +# Main Executable +# ============================================================================ +add_executable(aissia + src/main.cpp +) + +target_link_libraries(aissia PRIVATE + GroveEngine::impl + spdlog::spdlog +) + +target_include_directories(aissia PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src +) + +# ============================================================================ +# Hot-Reloadable Modules (.so) +# ============================================================================ + +# SchedulerModule - Gestion du temps et détection hyperfocus +add_library(SchedulerModule SHARED + src/modules/SchedulerModule.cpp +) +target_link_libraries(SchedulerModule PRIVATE + GroveEngine::impl + spdlog::spdlog +) +set_target_properties(SchedulerModule PROPERTIES + PREFIX "lib" + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/modules +) + +# NotificationModule - Alertes système et TTS +add_library(NotificationModule SHARED + src/modules/NotificationModule.cpp +) +target_link_libraries(NotificationModule PRIVATE + GroveEngine::impl + spdlog::spdlog +) +set_target_properties(NotificationModule PROPERTIES + PREFIX "lib" + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/modules +) + +# Futurs modules (décommenter quand implémentés): +# add_library(AIAssistantModule SHARED src/modules/AIAssistantModule.cpp) +# add_library(LanguageLearningModule SHARED src/modules/LanguageLearningModule.cpp) +# add_library(DataModule SHARED src/modules/DataModule.cpp) + +# ============================================================================ +# Copy config files to build directory +# ============================================================================ +file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/config/ + DESTINATION ${CMAKE_BINARY_DIR}/config) + +# ============================================================================ +# Development targets +# ============================================================================ + +# Quick rebuild of modules only (for hot-reload workflow) +add_custom_target(modules + DEPENDS SchedulerModule NotificationModule + COMMENT "Building hot-reloadable modules only" +) + +# Run Aissia +add_custom_target(run + COMMAND $ + DEPENDS aissia modules + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + COMMENT "Running Aissia" +) diff --git a/config/notification.json b/config/notification.json new file mode 100644 index 0000000..788db63 --- /dev/null +++ b/config/notification.json @@ -0,0 +1,6 @@ +{ + "language": "fr", + "silentMode": false, + "ttsEnabled": false, + "maxQueueSize": 50 +} diff --git a/config/scheduler.json b/config/scheduler.json new file mode 100644 index 0000000..b57fa13 --- /dev/null +++ b/config/scheduler.json @@ -0,0 +1,7 @@ +{ + "hyperfocusThresholdMinutes": 120, + "breakReminderIntervalMinutes": 45, + "breakDurationMinutes": 10, + "workdayStartHour": 9, + "workdayEndHour": 18 +} diff --git a/docs/GROVEENGINE_GUIDE.md b/docs/GROVEENGINE_GUIDE.md new file mode 100644 index 0000000..0388694 --- /dev/null +++ b/docs/GROVEENGINE_GUIDE.md @@ -0,0 +1,749 @@ +# GroveEngine User Guide + +GroveEngine is a C++17 hot-reload module system designed for building modular applications with runtime code replacement capabilities. + +## Table of Contents + +1. [Overview](#overview) +2. [Core Concepts](#core-concepts) +3. [Project Setup](#project-setup) +4. [Creating Modules](#creating-modules) +5. [Module Lifecycle](#module-lifecycle) +6. [Inter-Module Communication](#inter-module-communication) +7. [Hot-Reload](#hot-reload) +8. [Configuration Management](#configuration-management) +9. [Task Scheduling](#task-scheduling) +10. [API Reference](#api-reference) + +--- + +## Overview + +GroveEngine provides: + +- **Hot-Reload**: Replace module code at runtime without losing state +- **Modular Architecture**: Self-contained modules with clear interfaces +- **Pub/Sub Communication**: Decoupled inter-module messaging via topics +- **State Preservation**: Automatic state serialization across reloads +- **Configuration Hot-Reload**: Update module configuration without code changes + +### Design Philosophy + +- Modules contain pure business logic (200-300 lines recommended) +- No infrastructure code in modules (threading, networking, persistence) +- All data via `IDataNode` abstraction (backend agnostic) +- Pull-based message processing (modules control when they read messages) + +--- + +## Core Concepts + +### IModule + +The base interface all modules implement. Defines the contract for: +- Processing logic (`process()`) +- Configuration (`setConfiguration()`, `getConfiguration()`) +- State management (`getState()`, `setState()`) +- Lifecycle (`shutdown()`) + +### IDataNode + +Hierarchical data structure for configuration, state, and messages. Supports: +- Typed accessors (`getString()`, `getInt()`, `getDouble()`, `getBool()`) +- Tree navigation (`getChild()`, `getChildNames()`) +- Pattern matching (`getChildrenByNameMatch()`) + +### IIO + +Pub/Sub communication interface: +- `publish()`: Send messages to topics +- `subscribe()`: Listen to topic patterns +- `pullMessage()`: Consume received messages + +### ModuleLoader + +Handles dynamic loading of `.so` files: +- `load()`: Load a module from shared library +- `reload()`: Hot-reload with state preservation +- `unload()`: Clean unload + +--- + +## Project Setup + +### Directory Structure + +``` +MyProject/ +├── CMakeLists.txt +├── external/ +│ └── GroveEngine/ # GroveEngine (submodule or symlink) +├── src/ +│ ├── main.cpp +│ └── modules/ +│ ├── MyModule.h +│ └── MyModule.cpp +└── config/ + └── mymodule.json +``` + +### CMakeLists.txt + +```cmake +cmake_minimum_required(VERSION 3.20) +project(MyProject VERSION 0.1.0 LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +# ============================================================================ +# GroveEngine Integration +# ============================================================================ +set(GROVE_BUILD_TESTS OFF CACHE BOOL "Disable GroveEngine tests" FORCE) +add_subdirectory(external/GroveEngine) + +# ============================================================================ +# Main Executable +# ============================================================================ +add_executable(myapp src/main.cpp) + +target_link_libraries(myapp PRIVATE + GroveEngine::impl + spdlog::spdlog +) + +# ============================================================================ +# Hot-Reloadable Modules (.so) +# ============================================================================ +add_library(MyModule SHARED + src/modules/MyModule.cpp +) + +target_link_libraries(MyModule PRIVATE + GroveEngine::impl + spdlog::spdlog +) + +set_target_properties(MyModule PROPERTIES + PREFIX "lib" + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/modules +) + +# ============================================================================ +# Copy config files to build directory +# ============================================================================ +file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/config/ + DESTINATION ${CMAKE_BINARY_DIR}/config) + +# ============================================================================ +# Convenience targets +# ============================================================================ +add_custom_target(modules + DEPENDS MyModule + COMMENT "Building hot-reloadable modules only" +) +``` + +### Linking GroveEngine + +Option 1: Git submodule +```bash +git submodule add external/GroveEngine +``` + +Option 2: Symlink (local development) +```bash +ln -s /path/to/GroveEngine external/GroveEngine +``` + +--- + +## Creating Modules + +### Module Header + +```cpp +// src/modules/MyModule.h +#pragma once + +#include +#include +#include +#include +#include + +namespace myapp { + +class MyModule : public grove::IModule { +public: + MyModule(); + + // Required IModule interface + void process(const grove::IDataNode& input) override; + void setConfiguration(const grove::IDataNode& configNode, + grove::IIO* io, + grove::ITaskScheduler* scheduler) override; + const grove::IDataNode& getConfiguration() override; + std::unique_ptr getHealthStatus() override; + void shutdown() override; + std::unique_ptr getState() override; + void setState(const grove::IDataNode& state) override; + std::string getType() const override { return "mymodule"; } + bool isIdle() const override { return true; } + +private: + std::shared_ptr m_logger; + std::unique_ptr m_config; + grove::IIO* m_io = nullptr; + + // Module state (will be preserved across hot-reloads) + int m_counter = 0; + std::string m_status; +}; + +} // namespace myapp + +// Required C exports for dynamic loading +extern "C" { + grove::IModule* createModule(); + void destroyModule(grove::IModule* module); +} +``` + +### Module Implementation + +```cpp +// src/modules/MyModule.cpp +#include "MyModule.h" +#include +#include + +namespace myapp { + +MyModule::MyModule() { + m_logger = spdlog::get("MyModule"); + if (!m_logger) { + m_logger = spdlog::stdout_color_mt("MyModule"); + } + m_config = std::make_unique("config"); +} + +void MyModule::setConfiguration(const grove::IDataNode& configNode, + grove::IIO* io, + grove::ITaskScheduler* scheduler) { + m_io = io; + m_config = std::make_unique("config"); + + // Read configuration values with defaults + m_status = configNode.getString("initialStatus", "ready"); + int startCount = configNode.getInt("startCount", 0); + + m_logger->info("MyModule configured: status={}, startCount={}", + m_status, startCount); +} + +const grove::IDataNode& MyModule::getConfiguration() { + return *m_config; +} + +void MyModule::process(const grove::IDataNode& input) { + // Get frame timing from input + double deltaTime = input.getDouble("deltaTime", 0.016); + int frameCount = input.getInt("frameCount", 0); + + // Your processing logic here + m_counter++; + + // Process incoming messages + while (m_io && m_io->hasMessages() > 0) { + auto msg = m_io->pullMessage(); + m_logger->debug("Received message on topic: {}", msg.topic); + // Handle message... + } + + // Publish events if needed + if (m_counter % 100 == 0) { + auto event = std::make_unique("event"); + event->setInt("counter", m_counter); + m_io->publish("mymodule:milestone", std::move(event)); + } +} + +std::unique_ptr MyModule::getHealthStatus() { + auto status = std::make_unique("health"); + status->setString("status", "running"); + status->setInt("counter", m_counter); + return status; +} + +void MyModule::shutdown() { + m_logger->info("MyModule shutting down, counter={}", m_counter); +} + +// ============================================================================ +// State Serialization (Critical for Hot-Reload) +// ============================================================================ + +std::unique_ptr MyModule::getState() { + auto state = std::make_unique("state"); + + // Serialize all state that must survive hot-reload + state->setInt("counter", m_counter); + state->setString("status", m_status); + + m_logger->debug("State saved: counter={}", m_counter); + return state; +} + +void MyModule::setState(const grove::IDataNode& state) { + // Restore state after hot-reload + m_counter = state.getInt("counter", 0); + m_status = state.getString("status", "ready"); + + m_logger->info("State restored: counter={}", m_counter); +} + +} // namespace myapp + +// ============================================================================ +// C Export Functions (Required for dlopen) +// ============================================================================ + +extern "C" { + +grove::IModule* createModule() { + return new myapp::MyModule(); +} + +void destroyModule(grove::IModule* module) { + delete module; +} + +} +``` + +--- + +## Module Lifecycle + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Module Lifecycle │ +└─────────────────────────────────────────────────────────────────┘ + +1. LOAD + ModuleLoader::load(path, name) + │ + ▼ + dlopen() → createModule() + │ + ▼ + setConfiguration(config, io, scheduler) + │ + ▼ + Module Ready + +2. PROCESS (Main Loop) + ┌──────────────────────┐ + │ process(input) │◄────┐ + │ - Read deltaTime │ │ + │ - Update state │ │ + │ - Pull messages │ │ + │ - Publish events │ │ + └──────────────────────┘ │ + │ │ + └────────────────────┘ + +3. HOT-RELOAD + File change detected + │ + ▼ + getState() → Save state + │ + ▼ + unload() → dlclose() + │ + ▼ + load(path, name, isReload=true) + │ + ▼ + setConfiguration(config, io, scheduler) + │ + ▼ + setState(savedState) + │ + ▼ + Module continues with preserved state + +4. SHUTDOWN + shutdown() + │ + ▼ + destroyModule() + │ + ▼ + dlclose() +``` + +--- + +## Inter-Module Communication + +### IIO Pub/Sub System + +Modules communicate via topics using publish/subscribe pattern. + +#### Publishing Messages + +```cpp +void MyModule::process(const grove::IDataNode& input) { + // Create message data + auto data = std::make_unique("data"); + data->setString("event", "player_moved"); + data->setDouble("x", 100.5); + data->setDouble("y", 200.3); + + // Publish to topic + m_io->publish("game:player:position", std::move(data)); +} +``` + +#### Subscribing to Topics + +```cpp +void MyModule::setConfiguration(const grove::IDataNode& configNode, + grove::IIO* io, + grove::ITaskScheduler* scheduler) { + m_io = io; + + // Subscribe to specific topic + m_io->subscribe("game:player:*"); + + // Subscribe with low-frequency batching (for non-critical updates) + grove::SubscriptionConfig config; + config.batchInterval = 1000; // 1 second batches + m_io->subscribeLowFreq("analytics:*", config); +} +``` + +#### Processing Messages + +```cpp +void MyModule::process(const grove::IDataNode& input) { + // Pull-based: module controls when to process messages + while (m_io->hasMessages() > 0) { + grove::Message msg = m_io->pullMessage(); + + if (msg.topic == "game:player:position") { + double x = msg.data->getDouble("x", 0.0); + double y = msg.data->getDouble("y", 0.0); + // Handle position update... + } + } +} +``` + +### Topic Patterns + +Topics support wildcard matching: + +| Pattern | Matches | +|---------|---------| +| `game:player:*` | `game:player:position`, `game:player:health` | +| `economy:*` | `economy:prices`, `economy:trade` | +| `*:error` | `network:error`, `database:error` | + +--- + +## Hot-Reload + +### How It Works + +1. **File Watcher** detects `.so` modification +2. **State Extraction**: `getState()` serializes module state +3. **Unload**: Old library closed with `dlclose()` +4. **Load**: New library loaded with `dlopen()` (cache bypass via temp copy) +5. **Configure**: `setConfiguration()` called with same config +6. **Restore**: `setState()` restores serialized state + +### Implementing Hot-Reload Support + +```cpp +// Critical: Serialize ALL state that must survive reload +std::unique_ptr MyModule::getState() { + auto state = std::make_unique("state"); + + // Primitives + state->setInt("counter", m_counter); + state->setDouble("health", m_health); + state->setString("name", m_name); + state->setBool("active", m_active); + + // Complex state: serialize to JSON child nodes + auto entitiesNode = std::make_unique("entities"); + for (const auto& entity : m_entities) { + auto entityNode = std::make_unique(entity.id); + entityNode->setDouble("x", entity.x); + entityNode->setDouble("y", entity.y); + entitiesNode->setChild(entity.id, std::move(entityNode)); + } + state->setChild("entities", std::move(entitiesNode)); + + return state; +} + +void MyModule::setState(const grove::IDataNode& state) { + // Restore primitives + m_counter = state.getInt("counter", 0); + m_health = state.getDouble("health", 100.0); + m_name = state.getString("name", "default"); + m_active = state.getBool("active", true); + + // Restore complex state + m_entities.clear(); + auto* entitiesNode = state.getChildReadOnly("entities"); + if (entitiesNode) { + for (const auto& name : entitiesNode->getChildNames()) { + auto* entityNode = entitiesNode->getChildReadOnly(name); + if (entityNode) { + Entity e; + e.id = name; + e.x = entityNode->getDouble("x", 0.0); + e.y = entityNode->getDouble("y", 0.0); + m_entities.push_back(e); + } + } + } +} +``` + +### File Watcher Example + +```cpp +class FileWatcher { +public: + void watch(const std::string& path) { + if (fs::exists(path)) { + m_lastModified[path] = fs::last_write_time(path); + } + } + + bool hasChanged(const std::string& path) { + if (!fs::exists(path)) return false; + + auto currentTime = fs::last_write_time(path); + auto it = m_lastModified.find(path); + + if (it == m_lastModified.end()) { + m_lastModified[path] = currentTime; + return false; + } + + if (currentTime != it->second) { + it->second = currentTime; + return true; + } + return false; + } + +private: + std::unordered_map m_lastModified; +}; +``` + +--- + +## Configuration Management + +### JSON Configuration Files + +```json +// config/mymodule.json +{ + "initialStatus": "ready", + "startCount": 0, + "maxItems": 100, + "debugMode": false, + "updateRate": 0.016 +} +``` + +### Loading Configuration + +```cpp +std::unique_ptr loadConfig(const std::string& path) { + if (fs::exists(path)) { + std::ifstream file(path); + nlohmann::json j; + file >> j; + return std::make_unique("config", j); + } + // Return empty config with defaults + return std::make_unique("config"); +} +``` + +### Runtime Config Updates + +Modules can optionally support runtime configuration changes: + +```cpp +bool MyModule::updateConfig(const grove::IDataNode& newConfig) { + // Validate new config + int maxItems = newConfig.getInt("maxItems", 100); + if (maxItems < 1 || maxItems > 10000) { + m_logger->warn("Invalid maxItems: {}", maxItems); + return false; + } + + // Apply changes + m_maxItems = maxItems; + m_debugMode = newConfig.getBool("debugMode", false); + + m_logger->info("Config updated: maxItems={}", m_maxItems); + return true; +} +``` + +--- + +## Task Scheduling + +For computationally expensive operations, delegate to `ITaskScheduler`: + +```cpp +void MyModule::setConfiguration(const grove::IDataNode& configNode, + grove::IIO* io, + grove::ITaskScheduler* scheduler) { + m_scheduler = scheduler; // Store reference +} + +void MyModule::process(const grove::IDataNode& input) { + // Delegate expensive pathfinding calculation + if (needsPathfinding) { + auto taskData = std::make_unique("task"); + taskData->setDouble("startX", unit.x); + taskData->setDouble("startY", unit.y); + taskData->setDouble("targetX", target.x); + taskData->setDouble("targetY", target.y); + taskData->setString("unitId", unit.id); + + m_scheduler->scheduleTask("pathfinding", std::move(taskData)); + } + + // Check for completed tasks + while (m_scheduler->hasCompletedTasks() > 0) { + auto result = m_scheduler->getCompletedTask(); + std::string unitId = result->getString("unitId", ""); + // Apply pathfinding result... + } +} +``` + +--- + +## API Reference + +### IDataNode + +| Method | Description | +|--------|-------------| +| `getString(name, default)` | Get string property | +| `getInt(name, default)` | Get integer property | +| `getDouble(name, default)` | Get double property | +| `getBool(name, default)` | Get boolean property | +| `setString(name, value)` | Set string property | +| `setInt(name, value)` | Set integer property | +| `setDouble(name, value)` | Set double property | +| `setBool(name, value)` | Set boolean property | +| `hasProperty(name)` | Check if property exists | +| `getChild(name)` | Get child node (transfers ownership) | +| `getChildReadOnly(name)` | Get child node (no ownership transfer) | +| `setChild(name, node)` | Add/replace child node | +| `getChildNames()` | Get names of all children | + +### IIO + +| Method | Description | +|--------|-------------| +| `publish(topic, data)` | Publish message to topic | +| `subscribe(pattern, config)` | Subscribe to topic pattern | +| `subscribeLowFreq(pattern, config)` | Subscribe with batching | +| `hasMessages()` | Count of pending messages | +| `pullMessage()` | Consume one message | +| `getHealth()` | Get IO health metrics | + +### IModule + +| Method | Description | +|--------|-------------| +| `process(input)` | Main processing method | +| `setConfiguration(config, io, scheduler)` | Initialize module | +| `getConfiguration()` | Get current config | +| `getState()` | Serialize state for hot-reload | +| `setState(state)` | Restore state after hot-reload | +| `getHealthStatus()` | Get module health report | +| `shutdown()` | Clean shutdown | +| `isIdle()` | Check if safe to hot-reload | +| `getType()` | Get module type identifier | + +### ModuleLoader + +| Method | Description | +|--------|-------------| +| `load(path, name, isReload)` | Load module from .so | +| `reload(module)` | Hot-reload with state preservation | +| `unload()` | Unload current module | +| `isLoaded()` | Check if module is loaded | +| `getLoadedPath()` | Get path of loaded module | + +### IOFactory + +| Method | Description | +|--------|-------------| +| `create(type, instanceId)` | Create IO by type string | +| `create(IOType, instanceId)` | Create IO by enum | +| `createFromConfig(config, instanceId)` | Create IO from config | + +**IO Types:** +- `"intra"` / `IOType::INTRA` - Same-process (development) +- `"local"` / `IOType::LOCAL` - Same-machine (production single-server) +- `"network"` / `IOType::NETWORK` - Distributed (MMO scale) + +--- + +## Building and Running + +```bash +# Configure +cmake -B build + +# Build everything +cmake --build build -j4 + +# Build modules only (for hot-reload workflow) +cmake --build build --target modules + +# Run +./build/myapp +``` + +### Hot-Reload Workflow + +1. Start application: `./build/myapp` +2. Edit module source: `src/modules/MyModule.cpp` +3. Rebuild module: `cmake --build build --target modules` +4. Application detects change and hot-reloads automatically + +--- + +## Best Practices + +1. **Keep modules small**: 200-300 lines of pure business logic +2. **No infrastructure code**: Let GroveEngine handle threading, persistence +3. **Serialize all state**: Everything in `getState()` survives hot-reload +4. **Use typed accessors**: `getInt()`, `getString()` with sensible defaults +5. **Pull-based messaging**: Process messages in `process()`, not callbacks +6. **Validate config**: Check configuration values in `setConfiguration()` +7. **Log appropriately**: Debug for development, Info for production events diff --git a/docs/README.md b/docs/README.md index 3b72e1d..5b84ef9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -47,7 +47,8 @@ docs/ ├── architecture/ # Architecture technique │ ├── architecture-modulaire.md # Interfaces WarFactory (IEngine, IModule, IIO) │ ├── architecture-technique.md # Architecture système complète -│ └── claude-code-integration.md # Intégration Claude Code +│ ├── claude-code-integration.md # Intégration Claude Code +│ └── intelligent-document-retrieval.md # Retrieval agentique pour AIAssistantModule └── implementation/ # Guides d'implémentation ├── CLAUDE-HOT-RELOAD-GUIDE.md # Guide hot-reload (0.4ms) ├── AUTOMATION_GUIDE.md # Automatisation du build @@ -72,7 +73,10 @@ docs/ 2. **[architecture-technique.md](architecture/architecture-technique.md)** → Voir comment AISSIA utilise WarFactory pour ses modules -3. **[CLAUDE-HOT-RELOAD-GUIDE.md](implementation/CLAUDE-HOT-RELOAD-GUIDE.md)** +3. **[intelligent-document-retrieval.md](architecture/intelligent-document-retrieval.md)** + → Architecture retrieval agentique pour AIAssistantModule (gestion documents, context window) + +4. **[CLAUDE-HOT-RELOAD-GUIDE.md](implementation/CLAUDE-HOT-RELOAD-GUIDE.md)** → Workflow de développement avec hot-reload 0.4ms ### 2. Modules AISSIA Prévus diff --git a/docs/architecture/intelligent-document-retrieval.md b/docs/architecture/intelligent-document-retrieval.md new file mode 100644 index 0000000..38c0802 --- /dev/null +++ b/docs/architecture/intelligent-document-retrieval.md @@ -0,0 +1,1243 @@ +# Intelligent Document Retrieval - Architecture Technique + +## Vue d'Ensemble + +Ce document décrit l'architecture pour la récupération intelligente de documents dans AISSIA. 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 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& 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 hybridSearch(const std::string& query) { + auto keywordResults = keywordSearch(query, 20); + auto semanticResults = semanticSearch(query, 20); + + // Fusion avec pondération + std::map 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 parseToolCalls(const json& response) = 0; + + // Formate les résultats des tools pour le prochain message + virtual json formatToolResults(const std::vector& 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 parseToolCalls(const json& response) override { + std::vector 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& 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 parseToolCalls(const json& response) override { + std::vector 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()) + }); + } + } + return calls; + } + + json formatToolResults(const std::vector& 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 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 create(const json& config) { + std::string provider = config.value("provider", "claude"); + + if (provider == "claude") { + return std::make_unique(config["providers"]["claude"]); + } + if (provider == "openai") { + return std::make_unique(config["providers"]["openai"]); + } + if (provider == "ollama") { + return std::make_unique(config["providers"]["ollama"]); + } + + throw std::runtime_error("Unknown LLM provider: " + provider); + } +}; +``` + +### Boucle Agentique Unifiée + +```cpp +json AIAssistantModule::agenticLoop(const std::string& userQuery) { + json messages = json::array(); + messages.push_back({{"role", "user"}, {"content", userQuery}}); + + int iterations = 0; + const int MAX_ITERATIONS = config["max_iterations"].get(); + + while (iterations++ < MAX_ITERATIONS) { + auto response = provider->chat(systemPrompt, messages, tools); + + if (provider->isEndTurn(response)) { + return { + {"response", provider->extractText(response)}, + {"iterations", iterations}, + {"provider", provider->getProviderName()} + }; + } + + auto toolCalls = provider->parseToolCalls(response); + if (toolCalls.empty()) { + return {{"error", "unexpected_state"}}; + } + + // Exécuter les tools (logique commune à tous les providers) + std::vector results; + for (const auto& call : toolCalls) { + auto result = executeTool(call.name, call.input); + results.push_back({call.id, result.dump(), false}); + } + + // Ajouter à l'historique (format dépend du provider) + provider->appendAssistantMessage(messages, response); + + // Gérer la différence de format pour tool results + auto toolResultsMsg = provider->formatToolResults(results); + if (toolResultsMsg.is_array()) { + // OpenAI: plusieurs messages + for (const auto& msg : toolResultsMsg) { + messages.push_back(msg); + } + } else { + // Claude: un seul message + messages.push_back(toolResultsMsg); + } + } + + return {{"error", "max_iterations_reached"}}; +} +``` + +### 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 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 AISSIA +- [claude-code-integration.md](./claude-code-integration.md) - Patterns développement Claude Code diff --git a/external/GroveEngine b/external/GroveEngine new file mode 120000 index 0000000..64e37d5 --- /dev/null +++ b/external/GroveEngine @@ -0,0 +1 @@ +/mnt/e/Users/Alexis Trouvé/Documents/Projets/GroveEngine \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..c4d9c6c --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,263 @@ +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + +// Global flag for clean shutdown +static volatile bool g_running = true; + +void signalHandler(int signal) { + spdlog::info("Signal {} reçu, arrêt en cours...", signal); + g_running = false; +} + +// Simple file watcher for hot-reload +class FileWatcher { +public: + void watch(const std::string& path) { + if (fs::exists(path)) { + m_lastModified[path] = fs::last_write_time(path); + } + } + + bool hasChanged(const std::string& path) { + if (!fs::exists(path)) return false; + + auto currentTime = fs::last_write_time(path); + auto it = m_lastModified.find(path); + + if (it == m_lastModified.end()) { + m_lastModified[path] = currentTime; + return false; + } + + if (currentTime != it->second) { + it->second = currentTime; + return true; + } + return false; + } + +private: + std::unordered_map m_lastModified; +}; + +// Load JSON config file +std::unique_ptr loadConfig(const std::string& path) { + if (fs::exists(path)) { + std::ifstream file(path); + nlohmann::json j; + file >> j; + auto config = std::make_unique("config", j); + spdlog::info("Config chargée: {}", path); + return config; + } else { + spdlog::warn("Config non trouvée: {}, utilisation des défauts", path); + return std::make_unique("config"); + } +} + +// Module entry in our simple manager +struct ModuleEntry { + std::string name; + std::string configFile; + std::string path; + std::unique_ptr loader; + std::unique_ptr io; + grove::IModule* module = nullptr; +}; + +int main(int argc, char* argv[]) { + // Setup logging + auto console = spdlog::stdout_color_mt("Aissia"); + spdlog::set_default_logger(console); + spdlog::set_level(spdlog::level::debug); + spdlog::set_pattern("[%H:%M:%S.%e] [%n] [%^%l%$] %v"); + + spdlog::info("========================================"); + spdlog::info(" AISSIA - Assistant Personnel IA"); + spdlog::info(" Powered by GroveEngine"); + spdlog::info("========================================"); + + // Signal handling + std::signal(SIGINT, signalHandler); + std::signal(SIGTERM, signalHandler); + + // Paths + const std::string modulesDir = "./modules/"; + const std::string configDir = "./config/"; + + // ========================================================================= + // Module Management + // ========================================================================= + std::map modules; + FileWatcher watcher; + + // Liste des modules à charger + std::vector> moduleList = { + {"SchedulerModule", "scheduler.json"}, + {"NotificationModule", "notification.json"}, + // Futurs modules: + // {"AIAssistantModule", "ai_assistant.json"}, + // {"LanguageLearningModule", "language.json"}, + // {"DataModule", "data.json"}, + }; + + // Charger les modules + for (const auto& [moduleName, configFile] : moduleList) { + std::string modulePath = modulesDir + "lib" + moduleName + ".so"; + + if (!fs::exists(modulePath)) { + spdlog::warn("{} non trouvé: {}", moduleName, modulePath); + continue; + } + + ModuleEntry entry; + entry.name = moduleName; + entry.configFile = configFile; + entry.path = modulePath; + entry.loader = std::make_unique(); + entry.io = grove::IOFactory::create("intra", moduleName); + + auto modulePtr = entry.loader->load(modulePath, moduleName); + if (!modulePtr) { + spdlog::error("Échec du chargement: {}", moduleName); + continue; + } + + // Configure + auto config = loadConfig(configDir + configFile); + modulePtr->setConfiguration(*config, entry.io.get(), nullptr); + + entry.module = modulePtr.release(); + watcher.watch(modulePath); + + spdlog::info("{} chargé et configuré", moduleName); + modules[moduleName] = std::move(entry); + } + + if (modules.empty()) { + spdlog::error("Aucun module chargé! Build les modules: cmake --build build --target modules"); + return 1; + } + + // ========================================================================= + // Main Loop + // ========================================================================= + spdlog::info("Démarrage de la boucle principale (Ctrl+C pour quitter)"); + + using Clock = std::chrono::high_resolution_clock; + auto startTime = Clock::now(); + auto lastFrame = Clock::now(); + const auto targetFrameTime = std::chrono::milliseconds(100); // 10 Hz pour un assistant + + grove::JsonDataNode frameInput("frame"); + uint64_t frameCount = 0; + + while (g_running) { + auto frameStart = Clock::now(); + + // Calculate times + auto deltaTime = std::chrono::duration(frameStart - lastFrame).count(); + auto gameTime = std::chrono::duration(frameStart - startTime).count(); + lastFrame = frameStart; + + // Prepare frame input + frameInput.setDouble("deltaTime", deltaTime); + frameInput.setInt("frameCount", frameCount); + frameInput.setDouble("gameTime", gameTime); + + // ===================================================================== + // Hot-Reload Check (every 10 frames = ~1 second) + // ===================================================================== + if (frameCount % 10 == 0) { + for (auto& [name, entry] : modules) { + if (watcher.hasChanged(entry.path)) { + spdlog::info("Modification détectée: {}, hot-reload...", name); + + // Get state before reload + std::unique_ptr state; + if (entry.module) { + state = entry.module->getState(); + } + + // Reload + auto reloaded = entry.loader->load(entry.path, name, true); + if (reloaded) { + auto config = loadConfig(configDir + entry.configFile); + reloaded->setConfiguration(*config, entry.io.get(), nullptr); + + if (state) { + reloaded->setState(*state); + } + + entry.module = reloaded.release(); + spdlog::info("{} rechargé avec succès!", name); + } + } + } + } + + // ===================================================================== + // Process all modules + // ===================================================================== + for (auto& [name, entry] : modules) { + if (entry.module) { + entry.module->process(frameInput); + } + } + + // Process IO messages + for (auto& [name, entry] : modules) { + while (entry.io && entry.io->hasMessages() > 0) { + auto msg = entry.io->pullMessage(); + // Route messages between modules if needed + } + } + + // ===================================================================== + // Frame timing + // ===================================================================== + frameCount++; + + auto frameEnd = Clock::now(); + auto frameDuration = frameEnd - frameStart; + + if (frameDuration < targetFrameTime) { + std::this_thread::sleep_for(targetFrameTime - frameDuration); + } + + // Status log every 30 seconds + if (frameCount % 300 == 0) { + int minutes = static_cast(gameTime / 60.0f); + int seconds = static_cast(gameTime) % 60; + spdlog::debug("Session: {}m{}s, {} modules actifs", + minutes, seconds, modules.size()); + } + } + + // ========================================================================= + // Shutdown + // ========================================================================= + spdlog::info("Arrêt en cours..."); + for (auto& [name, entry] : modules) { + if (entry.module) { + entry.module->shutdown(); + spdlog::info("{} arrêté", name); + } + } + spdlog::info("À bientôt!"); + + return 0; +} diff --git a/src/modules/NotificationModule.cpp b/src/modules/NotificationModule.cpp new file mode 100644 index 0000000..2884ed0 --- /dev/null +++ b/src/modules/NotificationModule.cpp @@ -0,0 +1,172 @@ +#include "NotificationModule.h" +#include + +namespace aissia { + +NotificationModule::NotificationModule() { + m_logger = spdlog::get("NotificationModule"); + if (!m_logger) { + m_logger = spdlog::stdout_color_mt("NotificationModule"); + } + m_config = std::make_unique("config"); +} + +void NotificationModule::setConfiguration(const grove::IDataNode& configNode, + grove::IIO* io, + grove::ITaskScheduler* scheduler) { + m_io = io; + m_config = std::make_unique("config"); + + // Charger la configuration + m_language = configNode.getString("language", "fr"); + m_silentMode = configNode.getBool("silentMode", false); + m_ttsEnabled = configNode.getBool("ttsEnabled", false); + m_maxQueueSize = configNode.getInt("maxQueueSize", 50); + + m_logger->info("NotificationModule configuré: langue={}, silent={}, tts={}", + m_language, m_silentMode, m_ttsEnabled); +} + +const grove::IDataNode& NotificationModule::getConfiguration() { + return *m_config; +} + +void NotificationModule::process(const grove::IDataNode& input) { + // Traiter la file de notifications + processNotificationQueue(); +} + +void NotificationModule::notify(const std::string& title, const std::string& message, Priority priority) { + if (m_silentMode && priority != Priority::URGENT) { + return; // Ignorer les notifications non-urgentes en mode silencieux + } + + Notification notif; + notif.id = std::to_string(++m_notificationCount); + notif.title = title; + notif.message = message; + notif.priority = priority; + notif.language = m_language; + notif.read = false; + notif.timestamp = 0; // Sera mis à jour lors du process + + // Limiter la taille de la queue + while (static_cast(m_pendingNotifications.size()) >= m_maxQueueSize) { + m_pendingNotifications.pop(); + } + + m_pendingNotifications.push(notif); + + if (priority == Priority::URGENT) { + m_urgentCount++; + } +} + +void NotificationModule::processNotificationQueue() { + // Traiter jusqu'à 3 notifications par frame pour éviter le spam + int processed = 0; + while (!m_pendingNotifications.empty() && processed < 3) { + Notification notif = m_pendingNotifications.front(); + m_pendingNotifications.pop(); + + displayNotification(notif); + processed++; + } +} + +void NotificationModule::displayNotification(const Notification& notif) { + std::string emoji = priorityToEmoji(notif.priority); + std::string priorityStr = priorityToString(notif.priority); + + // Affichage console (sera remplacé par Windows toast + TTS) + switch (notif.priority) { + case Priority::URGENT: + m_logger->warn("{} [{}] {}: {}", emoji, priorityStr, notif.title, notif.message); + break; + case Priority::HIGH: + m_logger->info("{} [{}] {}: {}", emoji, priorityStr, notif.title, notif.message); + break; + case Priority::NORMAL: + m_logger->info("{} {}: {}", emoji, notif.title, notif.message); + break; + case Priority::LOW: + m_logger->debug("{} {}: {}", emoji, notif.title, notif.message); + break; + } + + // TODO: Intégrer Windows Toast notifications + // TODO: Intégrer TTS si m_ttsEnabled +} + +std::string NotificationModule::priorityToString(Priority p) { + switch (p) { + case Priority::LOW: return "INFO"; + case Priority::NORMAL: return "RAPPEL"; + case Priority::HIGH: return "IMPORTANT"; + case Priority::URGENT: return "URGENT"; + default: return "?"; + } +} + +std::string NotificationModule::priorityToEmoji(Priority p) { + switch (p) { + case Priority::LOW: return "[i]"; + case Priority::NORMAL: return "[*]"; + case Priority::HIGH: return "[!]"; + case Priority::URGENT: return "[!!!]"; + default: return "[ ]"; + } +} + +std::unique_ptr NotificationModule::getHealthStatus() { + auto status = std::make_unique("status"); + status->setString("status", "running"); + status->setInt("pendingCount", m_pendingNotifications.size()); + status->setInt("totalNotifications", m_notificationCount); + status->setInt("urgentCount", m_urgentCount); + status->setBool("silentMode", m_silentMode); + return status; +} + +void NotificationModule::shutdown() { + // Vider la queue avant arrêt + while (!m_pendingNotifications.empty()) { + displayNotification(m_pendingNotifications.front()); + m_pendingNotifications.pop(); + } + m_logger->info("NotificationModule arrêté. Total: {} notifications ({} urgentes)", + m_notificationCount, m_urgentCount); +} + +std::unique_ptr NotificationModule::getState() { + auto state = std::make_unique("state"); + + state->setInt("notificationCount", m_notificationCount); + state->setInt("urgentCount", m_urgentCount); + state->setInt("pendingCount", m_pendingNotifications.size()); + + m_logger->debug("État sauvegardé: {} notifications", m_notificationCount); + return state; +} + +void NotificationModule::setState(const grove::IDataNode& state) { + m_notificationCount = state.getInt("notificationCount", 0); + m_urgentCount = state.getInt("urgentCount", 0); + // Note: On ne restaure pas la queue - les notifications en attente sont perdues au reload + + m_logger->info("État restauré: {} notifications historiques", m_notificationCount); +} + +} // namespace aissia + +extern "C" { + +grove::IModule* createModule() { + return new aissia::NotificationModule(); +} + +void destroyModule(grove::IModule* module) { + delete module; +} + +} diff --git a/src/modules/NotificationModule.h b/src/modules/NotificationModule.h new file mode 100644 index 0000000..e3e5654 --- /dev/null +++ b/src/modules/NotificationModule.h @@ -0,0 +1,96 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include + +namespace aissia { + +/** + * @brief Notification Module - Alertes système et TTS + * + * Fonctionnalités: + * - Notifications système (console pour l'instant, Windows toast plus tard) + * - File d'attente des notifications avec priorités + * - Support multilingue (FR/EN/JP selon config) + * - Mode silencieux configurable + * + * Souscrit à: + * - "scheduler/hyperfocus_alert" : Alerte hyperfocus -> notification urgente + * - "scheduler/break_reminder" : Rappel pause -> notification normale + * - "ai/suggestion" : Suggestion IA -> notification info + * - "language/correction" : Correction langue -> notification légère + */ +class NotificationModule : public grove::IModule { +public: + enum class Priority { + LOW, // Info, peut être ignorée + NORMAL, // Rappels standards + HIGH, // Alertes importantes + URGENT // Hyperfocus, urgences + }; + + struct Notification { + std::string id; + std::string title; + std::string message; + Priority priority; + std::string language; // "fr", "en", "jp" + bool read; + float timestamp; + }; + + NotificationModule(); + ~NotificationModule() override = default; + + // IModule interface + void process(const grove::IDataNode& input) override; + void setConfiguration(const grove::IDataNode& configNode, grove::IIO* io, + grove::ITaskScheduler* scheduler) override; + const grove::IDataNode& getConfiguration() override; + std::unique_ptr getHealthStatus() override; + void shutdown() override; + std::unique_ptr getState() override; + void setState(const grove::IDataNode& state) override; + std::string getType() const override { return "NotificationModule"; } + bool isIdle() const override { return true; } + int getVersion() const override { return 1; } + + // API publique + void notify(const std::string& title, const std::string& message, Priority priority = Priority::NORMAL); + +private: + // Configuration + std::string m_language = "fr"; + bool m_silentMode = false; + bool m_ttsEnabled = false; // Text-to-Speech (future) + int m_maxQueueSize = 50; + + // État + std::queue m_pendingNotifications; + int m_notificationCount = 0; + int m_urgentCount = 0; + + // Services + grove::IIO* m_io = nullptr; + std::unique_ptr m_config; + std::shared_ptr m_logger; + + // Helpers + void processNotificationQueue(); + void displayNotification(const Notification& notif); + std::string priorityToString(Priority p); + std::string priorityToEmoji(Priority p); +}; + +} // namespace aissia + +extern "C" { + grove::IModule* createModule(); + void destroyModule(grove::IModule* module); +} diff --git a/src/modules/SchedulerModule.cpp b/src/modules/SchedulerModule.cpp new file mode 100644 index 0000000..e2e2aea --- /dev/null +++ b/src/modules/SchedulerModule.cpp @@ -0,0 +1,179 @@ +#include "SchedulerModule.h" +#include + +namespace aissia { + +SchedulerModule::SchedulerModule() { + m_logger = spdlog::get("SchedulerModule"); + if (!m_logger) { + m_logger = spdlog::stdout_color_mt("SchedulerModule"); + } + m_config = std::make_unique("config"); +} + +void SchedulerModule::setConfiguration(const grove::IDataNode& configNode, + grove::IIO* io, + grove::ITaskScheduler* scheduler) { + m_io = io; + m_config = std::make_unique("config"); + + // Charger la configuration + m_hyperfocusThresholdMinutes = configNode.getInt("hyperfocusThresholdMinutes", 120); + m_breakReminderIntervalMinutes = configNode.getInt("breakReminderIntervalMinutes", 45); + m_breakDurationMinutes = configNode.getInt("breakDurationMinutes", 10); + + m_logger->info("SchedulerModule configuré: hyperfocus={}min, break_interval={}min", + m_hyperfocusThresholdMinutes, m_breakReminderIntervalMinutes); +} + +const grove::IDataNode& SchedulerModule::getConfiguration() { + return *m_config; +} + +void SchedulerModule::process(const grove::IDataNode& input) { + float currentTime = input.getDouble("gameTime", 0.0); + float dt = input.getDouble("deltaTime", 0.016); + + // Convertir le temps en minutes pour les calculs + float sessionMinutes = (currentTime - m_sessionStartTime) / 60.0f; + + // Vérifier l'hyperfocus + checkHyperfocus(currentTime); + + // Vérifier les rappels de pause + checkBreakReminder(currentTime); + + // Log périodique (toutes les 5 minutes simulées) + static float lastLog = 0; + if (currentTime - lastLog > 300.0f) { // 300 secondes = 5 minutes + lastLog = currentTime; + m_logger->debug("Session: {:.1f}min, Focus aujourd'hui: {}min, Tâche: {}", + sessionMinutes, m_totalFocusMinutesToday, + m_currentTaskId.empty() ? "(aucune)" : m_currentTaskId); + } +} + +void SchedulerModule::checkHyperfocus(float currentTime) { + if (m_currentTaskId.empty()) return; + + float sessionMinutes = (currentTime - m_sessionStartTime) / 60.0f; + + if (sessionMinutes >= m_hyperfocusThresholdMinutes && !m_hyperfocusAlertSent) { + m_hyperfocusAlertSent = true; + m_logger->warn("HYPERFOCUS DÉTECTÉ! Session de {:.0f} minutes sur '{}'", + sessionMinutes, m_currentTaskId); + + // Publier l'alerte (si IO disponible) + // Note: Dans une version complète, on publierait via m_io + } +} + +void SchedulerModule::checkBreakReminder(float currentTime) { + float timeSinceBreak = (currentTime - m_lastBreakTime) / 60.0f; + + if (timeSinceBreak >= m_breakReminderIntervalMinutes) { + m_lastBreakTime = currentTime; + m_logger->info("RAPPEL: Pause de {} minutes recommandée!", m_breakDurationMinutes); + + // Publier le rappel (si IO disponible) + } +} + +void SchedulerModule::startTask(const std::string& taskId) { + // Compléter la tâche précédente si nécessaire + if (!m_currentTaskId.empty()) { + completeCurrentTask(); + } + + m_currentTaskId = taskId; + m_sessionStartTime = m_lastActivityTime; + m_hyperfocusAlertSent = false; + + Task* task = findTask(taskId); + if (task) { + m_logger->info("Tâche démarrée: {} (estimé: {}min)", task->name, task->estimatedMinutes); + } +} + +void SchedulerModule::completeCurrentTask() { + if (m_currentTaskId.empty()) return; + + Task* task = findTask(m_currentTaskId); + if (task) { + float sessionMinutes = (m_lastActivityTime - m_sessionStartTime) / 60.0f; + task->actualMinutes = static_cast(sessionMinutes); + task->completed = true; + m_totalFocusMinutesToday += task->actualMinutes; + + m_logger->info("Tâche terminée: {} (réel: {}min vs estimé: {}min)", + task->name, task->actualMinutes, task->estimatedMinutes); + } + + m_currentTaskId.clear(); +} + +SchedulerModule::Task* SchedulerModule::findTask(const std::string& taskId) { + for (auto& task : m_tasks) { + if (task.id == taskId) return &task; + } + return nullptr; +} + +std::unique_ptr SchedulerModule::getHealthStatus() { + auto status = std::make_unique("status"); + status->setString("status", "running"); + status->setInt("taskCount", m_tasks.size()); + status->setString("currentTask", m_currentTaskId); + status->setInt("totalFocusMinutesToday", m_totalFocusMinutesToday); + status->setBool("hyperfocusAlertSent", m_hyperfocusAlertSent); + return status; +} + +void SchedulerModule::shutdown() { + if (!m_currentTaskId.empty()) { + completeCurrentTask(); + } + m_logger->info("SchedulerModule arrêté. Focus total: {}min", m_totalFocusMinutesToday); +} + +std::unique_ptr SchedulerModule::getState() { + auto state = std::make_unique("state"); + + state->setString("currentTaskId", m_currentTaskId); + state->setDouble("sessionStartTime", m_sessionStartTime); + state->setDouble("lastBreakTime", m_lastBreakTime); + state->setDouble("lastActivityTime", m_lastActivityTime); + state->setBool("hyperfocusAlertSent", m_hyperfocusAlertSent); + state->setInt("totalFocusMinutesToday", m_totalFocusMinutesToday); + state->setInt("taskCount", m_tasks.size()); + + m_logger->debug("État sauvegardé: {} tâches, focus={}min", m_tasks.size(), m_totalFocusMinutesToday); + return state; +} + +void SchedulerModule::setState(const grove::IDataNode& state) { + m_currentTaskId = state.getString("currentTaskId", ""); + m_sessionStartTime = state.getDouble("sessionStartTime", 0.0); + m_lastBreakTime = state.getDouble("lastBreakTime", 0.0); + m_lastActivityTime = state.getDouble("lastActivityTime", 0.0); + m_hyperfocusAlertSent = state.getBool("hyperfocusAlertSent", false); + m_totalFocusMinutesToday = state.getInt("totalFocusMinutesToday", 0); + + m_logger->info("État restauré: tâche='{}', focus={}min", + m_currentTaskId.empty() ? "(aucune)" : m_currentTaskId, + m_totalFocusMinutesToday); +} + +} // namespace aissia + +extern "C" { + +grove::IModule* createModule() { + return new aissia::SchedulerModule(); +} + +void destroyModule(grove::IModule* module) { + delete module; +} + +} diff --git a/src/modules/SchedulerModule.h b/src/modules/SchedulerModule.h new file mode 100644 index 0000000..40e03ca --- /dev/null +++ b/src/modules/SchedulerModule.h @@ -0,0 +1,94 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace aissia { + +/** + * @brief Scheduler Module - Gestion du temps et détection d'hyperfocus + * + * Fonctionnalités: + * - Planning de tâches avec durées estimées + * - Détection d'hyperfocus (session > seuil configurable) + * - Rappels de pauses automatiques + * - Tracking du temps par tâche + * + * Publie sur: + * - "scheduler/hyperfocus_alert" : Alerte quand hyperfocus détecté + * - "scheduler/break_reminder" : Rappel de pause + * - "scheduler/task_started" : Début de tâche + * - "scheduler/task_completed" : Fin de tâche + * + * Souscrit à: + * - "user/activity" : Activité utilisateur (reset idle) + * - "user/task_switch" : Changement de tâche manuel + */ +class SchedulerModule : public grove::IModule { +public: + struct Task { + std::string id; + std::string name; + std::string category; + int estimatedMinutes; + int actualMinutes; + bool completed; + }; + + SchedulerModule(); + ~SchedulerModule() override = default; + + // IModule interface + void process(const grove::IDataNode& input) override; + void setConfiguration(const grove::IDataNode& configNode, grove::IIO* io, + grove::ITaskScheduler* scheduler) override; + const grove::IDataNode& getConfiguration() override; + std::unique_ptr getHealthStatus() override; + void shutdown() override; + std::unique_ptr getState() override; + void setState(const grove::IDataNode& state) override; + std::string getType() const override { return "SchedulerModule"; } + bool isIdle() const override { return true; } + int getVersion() const override { return 1; } + +private: + // Configuration + int m_hyperfocusThresholdMinutes = 120; // 2 heures par défaut + int m_breakReminderIntervalMinutes = 45; // Rappel toutes les 45 min + int m_breakDurationMinutes = 10; // Pause de 10 min suggérée + + // État + std::vector m_tasks; + std::string m_currentTaskId; + float m_sessionStartTime = 0.0f; // Temps de début de session (gameTime) + float m_lastBreakTime = 0.0f; // Dernier rappel de pause + float m_lastActivityTime = 0.0f; // Dernière activité utilisateur + bool m_hyperfocusAlertSent = false; + int m_totalFocusMinutesToday = 0; + + // Services + grove::IIO* m_io = nullptr; + std::unique_ptr m_config; + std::shared_ptr m_logger; + + // Helpers + void checkHyperfocus(float currentTime); + void checkBreakReminder(float currentTime); + void startTask(const std::string& taskId); + void completeCurrentTask(); + Task* findTask(const std::string& taskId); +}; + +} // namespace aissia + +extern "C" { + grove::IModule* createModule(); + void destroyModule(grove::IModule* module); +}