Add infrastructure foundation and intelligent document retrieval
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
f231188880
commit
80f26aea54
89
CMakeLists.txt
Normal file
89
CMakeLists.txt
Normal file
@ -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 $<TARGET_FILE:aissia>
|
||||
DEPENDS aissia modules
|
||||
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||
COMMENT "Running Aissia"
|
||||
)
|
||||
6
config/notification.json
Normal file
6
config/notification.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"language": "fr",
|
||||
"silentMode": false,
|
||||
"ttsEnabled": false,
|
||||
"maxQueueSize": 50
|
||||
}
|
||||
7
config/scheduler.json
Normal file
7
config/scheduler.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"hyperfocusThresholdMinutes": 120,
|
||||
"breakReminderIntervalMinutes": 45,
|
||||
"breakDurationMinutes": 10,
|
||||
"workdayStartHour": 9,
|
||||
"workdayEndHour": 18
|
||||
}
|
||||
749
docs/GROVEENGINE_GUIDE.md
Normal file
749
docs/GROVEENGINE_GUIDE.md
Normal file
@ -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 <grove-engine-repo> 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 <grove/IModule.h>
|
||||
#include <grove/IDataNode.h>
|
||||
#include <grove/IIO.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <memory>
|
||||
|
||||
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<grove::IDataNode> getHealthStatus() override;
|
||||
void shutdown() override;
|
||||
std::unique_ptr<grove::IDataNode> 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<spdlog::logger> m_logger;
|
||||
std::unique_ptr<grove::IDataNode> 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 <grove/JsonDataNode.h>
|
||||
#include <spdlog/sinks/stdout_color_sinks.h>
|
||||
|
||||
namespace myapp {
|
||||
|
||||
MyModule::MyModule() {
|
||||
m_logger = spdlog::get("MyModule");
|
||||
if (!m_logger) {
|
||||
m_logger = spdlog::stdout_color_mt("MyModule");
|
||||
}
|
||||
m_config = std::make_unique<grove::JsonDataNode>("config");
|
||||
}
|
||||
|
||||
void MyModule::setConfiguration(const grove::IDataNode& configNode,
|
||||
grove::IIO* io,
|
||||
grove::ITaskScheduler* scheduler) {
|
||||
m_io = io;
|
||||
m_config = std::make_unique<grove::JsonDataNode>("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<grove::JsonDataNode>("event");
|
||||
event->setInt("counter", m_counter);
|
||||
m_io->publish("mymodule:milestone", std::move(event));
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<grove::IDataNode> MyModule::getHealthStatus() {
|
||||
auto status = std::make_unique<grove::JsonDataNode>("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<grove::IDataNode> MyModule::getState() {
|
||||
auto state = std::make_unique<grove::JsonDataNode>("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<grove::JsonDataNode>("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<grove::IDataNode> MyModule::getState() {
|
||||
auto state = std::make_unique<grove::JsonDataNode>("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<grove::JsonDataNode>("entities");
|
||||
for (const auto& entity : m_entities) {
|
||||
auto entityNode = std::make_unique<grove::JsonDataNode>(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<std::string, fs::file_time_type> 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<grove::JsonDataNode> loadConfig(const std::string& path) {
|
||||
if (fs::exists(path)) {
|
||||
std::ifstream file(path);
|
||||
nlohmann::json j;
|
||||
file >> j;
|
||||
return std::make_unique<grove::JsonDataNode>("config", j);
|
||||
}
|
||||
// Return empty config with defaults
|
||||
return std::make_unique<grove::JsonDataNode>("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<grove::JsonDataNode>("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
|
||||
@ -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
|
||||
|
||||
1243
docs/architecture/intelligent-document-retrieval.md
Normal file
1243
docs/architecture/intelligent-document-retrieval.md
Normal file
File diff suppressed because it is too large
Load Diff
1
external/GroveEngine
vendored
Symbolic link
1
external/GroveEngine
vendored
Symbolic link
@ -0,0 +1 @@
|
||||
/mnt/e/Users/Alexis Trouvé/Documents/Projets/GroveEngine
|
||||
263
src/main.cpp
Normal file
263
src/main.cpp
Normal file
@ -0,0 +1,263 @@
|
||||
#include <grove/ModuleLoader.h>
|
||||
#include <grove/JsonDataNode.h>
|
||||
#include <grove/IOFactory.h>
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <spdlog/sinks/stdout_color_sinks.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include <csignal>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
|
||||
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<std::string, fs::file_time_type> m_lastModified;
|
||||
};
|
||||
|
||||
// Load JSON config file
|
||||
std::unique_ptr<grove::JsonDataNode> loadConfig(const std::string& path) {
|
||||
if (fs::exists(path)) {
|
||||
std::ifstream file(path);
|
||||
nlohmann::json j;
|
||||
file >> j;
|
||||
auto config = std::make_unique<grove::JsonDataNode>("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<grove::JsonDataNode>("config");
|
||||
}
|
||||
}
|
||||
|
||||
// Module entry in our simple manager
|
||||
struct ModuleEntry {
|
||||
std::string name;
|
||||
std::string configFile;
|
||||
std::string path;
|
||||
std::unique_ptr<grove::ModuleLoader> loader;
|
||||
std::unique_ptr<grove::IIO> 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<std::string, ModuleEntry> modules;
|
||||
FileWatcher watcher;
|
||||
|
||||
// Liste des modules à charger
|
||||
std::vector<std::pair<std::string, std::string>> 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<grove::ModuleLoader>();
|
||||
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<float>(frameStart - lastFrame).count();
|
||||
auto gameTime = std::chrono::duration<float>(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<grove::IDataNode> 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<int>(gameTime / 60.0f);
|
||||
int seconds = static_cast<int>(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;
|
||||
}
|
||||
172
src/modules/NotificationModule.cpp
Normal file
172
src/modules/NotificationModule.cpp
Normal file
@ -0,0 +1,172 @@
|
||||
#include "NotificationModule.h"
|
||||
#include <grove/JsonDataNode.h>
|
||||
|
||||
namespace aissia {
|
||||
|
||||
NotificationModule::NotificationModule() {
|
||||
m_logger = spdlog::get("NotificationModule");
|
||||
if (!m_logger) {
|
||||
m_logger = spdlog::stdout_color_mt("NotificationModule");
|
||||
}
|
||||
m_config = std::make_unique<grove::JsonDataNode>("config");
|
||||
}
|
||||
|
||||
void NotificationModule::setConfiguration(const grove::IDataNode& configNode,
|
||||
grove::IIO* io,
|
||||
grove::ITaskScheduler* scheduler) {
|
||||
m_io = io;
|
||||
m_config = std::make_unique<grove::JsonDataNode>("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<int>(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<grove::IDataNode> NotificationModule::getHealthStatus() {
|
||||
auto status = std::make_unique<grove::JsonDataNode>("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<grove::IDataNode> NotificationModule::getState() {
|
||||
auto state = std::make_unique<grove::JsonDataNode>("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;
|
||||
}
|
||||
|
||||
}
|
||||
96
src/modules/NotificationModule.h
Normal file
96
src/modules/NotificationModule.h
Normal file
@ -0,0 +1,96 @@
|
||||
#pragma once
|
||||
|
||||
#include <grove/IModule.h>
|
||||
#include <grove/JsonDataNode.h>
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <spdlog/sinks/stdout_color_sinks.h>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <queue>
|
||||
|
||||
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<grove::IDataNode> getHealthStatus() override;
|
||||
void shutdown() override;
|
||||
std::unique_ptr<grove::IDataNode> 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<Notification> m_pendingNotifications;
|
||||
int m_notificationCount = 0;
|
||||
int m_urgentCount = 0;
|
||||
|
||||
// Services
|
||||
grove::IIO* m_io = nullptr;
|
||||
std::unique_ptr<grove::JsonDataNode> m_config;
|
||||
std::shared_ptr<spdlog::logger> 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);
|
||||
}
|
||||
179
src/modules/SchedulerModule.cpp
Normal file
179
src/modules/SchedulerModule.cpp
Normal file
@ -0,0 +1,179 @@
|
||||
#include "SchedulerModule.h"
|
||||
#include <grove/JsonDataNode.h>
|
||||
|
||||
namespace aissia {
|
||||
|
||||
SchedulerModule::SchedulerModule() {
|
||||
m_logger = spdlog::get("SchedulerModule");
|
||||
if (!m_logger) {
|
||||
m_logger = spdlog::stdout_color_mt("SchedulerModule");
|
||||
}
|
||||
m_config = std::make_unique<grove::JsonDataNode>("config");
|
||||
}
|
||||
|
||||
void SchedulerModule::setConfiguration(const grove::IDataNode& configNode,
|
||||
grove::IIO* io,
|
||||
grove::ITaskScheduler* scheduler) {
|
||||
m_io = io;
|
||||
m_config = std::make_unique<grove::JsonDataNode>("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<int>(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<grove::IDataNode> SchedulerModule::getHealthStatus() {
|
||||
auto status = std::make_unique<grove::JsonDataNode>("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<grove::IDataNode> SchedulerModule::getState() {
|
||||
auto state = std::make_unique<grove::JsonDataNode>("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;
|
||||
}
|
||||
|
||||
}
|
||||
94
src/modules/SchedulerModule.h
Normal file
94
src/modules/SchedulerModule.h
Normal file
@ -0,0 +1,94 @@
|
||||
#pragma once
|
||||
|
||||
#include <grove/IModule.h>
|
||||
#include <grove/JsonDataNode.h>
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <spdlog/sinks/stdout_color_sinks.h>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <chrono>
|
||||
|
||||
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<grove::IDataNode> getHealthStatus() override;
|
||||
void shutdown() override;
|
||||
std::unique_ptr<grove::IDataNode> 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<Task> 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<grove::JsonDataNode> m_config;
|
||||
std::shared_ptr<spdlog::logger> 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);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user