refactor: Services architecture for GroveEngine compliance
- Create 4 infrastructure services (LLM, Storage, Platform, Voice) - Refactor all modules to pure business logic (no HTTP/SQLite/Win32) - Add bundled SQLite amalgamation for MinGW compatibility - Make OpenSSL optional in CMake configuration - Fix topic naming convention (colon format) - Add succession documentation Build status: CMake config needs SQLite C language fix (documented) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
bc3b6cbaba
commit
26a5d3438b
1
.gitignore
vendored
1
.gitignore
vendored
@ -72,3 +72,4 @@ build/
|
||||
*.a
|
||||
*.so
|
||||
*.dll
|
||||
external/GroveEngine
|
||||
|
||||
187
CMakeLists.txt
187
CMakeLists.txt
@ -1,5 +1,5 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(Aissia VERSION 0.1.0 LANGUAGES CXX)
|
||||
project(Aissia VERSION 0.2.0 LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
@ -17,11 +17,28 @@ add_subdirectory(external/GroveEngine)
|
||||
# Dependencies
|
||||
# ============================================================================
|
||||
|
||||
# SQLite3
|
||||
find_package(SQLite3 REQUIRED)
|
||||
# SQLite3 - Use bundled amalgamation for MinGW compatibility
|
||||
enable_language(C) # SQLite is C code
|
||||
add_library(sqlite3 STATIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/deps/sqlite/sqlite3.c
|
||||
)
|
||||
set_target_properties(sqlite3 PROPERTIES LINKER_LANGUAGE C)
|
||||
target_include_directories(sqlite3 PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/deps/sqlite
|
||||
)
|
||||
target_compile_definitions(sqlite3 PRIVATE
|
||||
SQLITE_THREADSAFE=1
|
||||
SQLITE_ENABLE_FTS5
|
||||
SQLITE_ENABLE_JSON1
|
||||
)
|
||||
# Create alias to match find_package naming
|
||||
add_library(SQLite::SQLite3 ALIAS sqlite3)
|
||||
|
||||
# OpenSSL for HTTPS
|
||||
find_package(OpenSSL REQUIRED)
|
||||
# OpenSSL for HTTPS (optional for MinGW)
|
||||
find_package(OpenSSL QUIET)
|
||||
if(NOT OPENSSL_FOUND)
|
||||
message(STATUS "OpenSSL not found - HTTPS features will be disabled")
|
||||
endif()
|
||||
|
||||
# cpp-httplib (header-only HTTP client)
|
||||
include(FetchContent)
|
||||
@ -33,7 +50,86 @@ FetchContent_Declare(
|
||||
FetchContent_MakeAvailable(httplib)
|
||||
|
||||
# ============================================================================
|
||||
# Main Executable
|
||||
# Shared Libraries (used by services in main)
|
||||
# ============================================================================
|
||||
|
||||
# LLM Providers Library
|
||||
add_library(AissiaLLM STATIC
|
||||
src/shared/llm/LLMProviderFactory.cpp
|
||||
src/shared/llm/ClaudeProvider.cpp
|
||||
src/shared/llm/OpenAIProvider.cpp
|
||||
src/shared/llm/ToolRegistry.cpp
|
||||
)
|
||||
target_include_directories(AissiaLLM PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||
${httplib_SOURCE_DIR}
|
||||
)
|
||||
target_link_libraries(AissiaLLM PUBLIC
|
||||
GroveEngine::impl
|
||||
spdlog::spdlog
|
||||
)
|
||||
if(OPENSSL_FOUND)
|
||||
target_link_libraries(AissiaLLM PUBLIC OpenSSL::SSL OpenSSL::Crypto)
|
||||
target_compile_definitions(AissiaLLM PRIVATE CPPHTTPLIB_OPENSSL_SUPPORT)
|
||||
endif()
|
||||
|
||||
# Platform Library (window tracking)
|
||||
add_library(AissiaPlatform STATIC
|
||||
src/shared/platform/WindowTrackerFactory.cpp
|
||||
)
|
||||
target_include_directories(AissiaPlatform PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||
)
|
||||
target_link_libraries(AissiaPlatform PUBLIC
|
||||
spdlog::spdlog
|
||||
)
|
||||
if(WIN32)
|
||||
target_link_libraries(AissiaPlatform PUBLIC psapi)
|
||||
endif()
|
||||
|
||||
# Audio Library (TTS/STT)
|
||||
add_library(AissiaAudio STATIC
|
||||
src/shared/audio/TTSEngineFactory.cpp
|
||||
src/shared/audio/STTEngineFactory.cpp
|
||||
)
|
||||
target_include_directories(AissiaAudio PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||
${httplib_SOURCE_DIR}
|
||||
)
|
||||
target_link_libraries(AissiaAudio PUBLIC
|
||||
spdlog::spdlog
|
||||
)
|
||||
if(OPENSSL_FOUND)
|
||||
target_link_libraries(AissiaAudio PUBLIC OpenSSL::SSL OpenSSL::Crypto)
|
||||
target_compile_definitions(AissiaAudio PRIVATE CPPHTTPLIB_OPENSSL_SUPPORT)
|
||||
endif()
|
||||
if(WIN32)
|
||||
target_link_libraries(AissiaAudio PUBLIC sapi ole32)
|
||||
endif()
|
||||
|
||||
# ============================================================================
|
||||
# Infrastructure Services Library (linked into main)
|
||||
# ============================================================================
|
||||
add_library(AissiaServices STATIC
|
||||
src/services/LLMService.cpp
|
||||
src/services/StorageService.cpp
|
||||
src/services/PlatformService.cpp
|
||||
src/services/VoiceService.cpp
|
||||
)
|
||||
target_include_directories(AissiaServices PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||
)
|
||||
target_link_libraries(AissiaServices PUBLIC
|
||||
GroveEngine::impl
|
||||
spdlog::spdlog
|
||||
AissiaLLM
|
||||
AissiaPlatform
|
||||
AissiaAudio
|
||||
SQLite::SQLite3
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# Main Executable (with services)
|
||||
# ============================================================================
|
||||
add_executable(aissia
|
||||
src/main.cpp
|
||||
@ -42,6 +138,7 @@ add_executable(aissia
|
||||
target_link_libraries(aissia PRIVATE
|
||||
GroveEngine::impl
|
||||
spdlog::spdlog
|
||||
AissiaServices
|
||||
)
|
||||
|
||||
target_include_directories(aissia PRIVATE
|
||||
@ -49,10 +146,10 @@ target_include_directories(aissia PRIVATE
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# Hot-Reloadable Modules (.so)
|
||||
# Hot-Reloadable Modules (.so) - Pure business logic only
|
||||
# ============================================================================
|
||||
|
||||
# SchedulerModule - Gestion du temps et détection hyperfocus
|
||||
# SchedulerModule - Gestion du temps et detection hyperfocus
|
||||
add_library(SchedulerModule SHARED
|
||||
src/modules/SchedulerModule.cpp
|
||||
)
|
||||
@ -65,7 +162,7 @@ set_target_properties(SchedulerModule PROPERTIES
|
||||
LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/modules
|
||||
)
|
||||
|
||||
# NotificationModule - Alertes système et TTS
|
||||
# NotificationModule - Alertes systeme
|
||||
add_library(NotificationModule SHARED
|
||||
src/modules/NotificationModule.cpp
|
||||
)
|
||||
@ -78,67 +175,7 @@ set_target_properties(NotificationModule PROPERTIES
|
||||
LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/modules
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# Shared Libraries (linked into modules)
|
||||
# ============================================================================
|
||||
|
||||
# LLM Providers Library
|
||||
add_library(AissiaLLM STATIC
|
||||
src/shared/llm/LLMProviderFactory.cpp
|
||||
src/shared/llm/ClaudeProvider.cpp
|
||||
src/shared/llm/OpenAIProvider.cpp
|
||||
src/shared/llm/ToolRegistry.cpp
|
||||
)
|
||||
target_include_directories(AissiaLLM PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||
${httplib_SOURCE_DIR}
|
||||
)
|
||||
target_link_libraries(AissiaLLM PRIVATE
|
||||
GroveEngine::impl
|
||||
spdlog::spdlog
|
||||
OpenSSL::SSL
|
||||
OpenSSL::Crypto
|
||||
)
|
||||
target_compile_definitions(AissiaLLM PRIVATE CPPHTTPLIB_OPENSSL_SUPPORT)
|
||||
|
||||
# Platform Library (window tracking)
|
||||
add_library(AissiaPlatform STATIC
|
||||
src/shared/platform/WindowTrackerFactory.cpp
|
||||
)
|
||||
target_include_directories(AissiaPlatform PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||
)
|
||||
target_link_libraries(AissiaPlatform PRIVATE
|
||||
spdlog::spdlog
|
||||
)
|
||||
if(WIN32)
|
||||
target_link_libraries(AissiaPlatform PRIVATE psapi)
|
||||
endif()
|
||||
|
||||
# Audio Library (TTS/STT)
|
||||
add_library(AissiaAudio STATIC
|
||||
src/shared/audio/TTSEngineFactory.cpp
|
||||
src/shared/audio/STTEngineFactory.cpp
|
||||
)
|
||||
target_include_directories(AissiaAudio PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||
${httplib_SOURCE_DIR}
|
||||
)
|
||||
target_link_libraries(AissiaAudio PRIVATE
|
||||
spdlog::spdlog
|
||||
OpenSSL::SSL
|
||||
OpenSSL::Crypto
|
||||
)
|
||||
target_compile_definitions(AissiaAudio PRIVATE CPPHTTPLIB_OPENSSL_SUPPORT)
|
||||
if(WIN32)
|
||||
target_link_libraries(AissiaAudio PRIVATE sapi ole32)
|
||||
endif()
|
||||
|
||||
# ============================================================================
|
||||
# New Modules
|
||||
# ============================================================================
|
||||
|
||||
# StorageModule - SQLite persistence
|
||||
# StorageModule - Pure logic, no SQLite (uses StorageService via IIO)
|
||||
add_library(StorageModule SHARED
|
||||
src/modules/StorageModule.cpp
|
||||
)
|
||||
@ -146,14 +183,13 @@ target_include_directories(StorageModule PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||
target_link_libraries(StorageModule PRIVATE
|
||||
GroveEngine::impl
|
||||
spdlog::spdlog
|
||||
SQLite::SQLite3
|
||||
)
|
||||
set_target_properties(StorageModule PROPERTIES
|
||||
PREFIX "lib"
|
||||
LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/modules
|
||||
)
|
||||
|
||||
# MonitoringModule - Window tracking
|
||||
# MonitoringModule - Pure logic, no Win32 (uses PlatformService via IIO)
|
||||
add_library(MonitoringModule SHARED
|
||||
src/modules/MonitoringModule.cpp
|
||||
)
|
||||
@ -161,14 +197,13 @@ target_include_directories(MonitoringModule PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/
|
||||
target_link_libraries(MonitoringModule PRIVATE
|
||||
GroveEngine::impl
|
||||
spdlog::spdlog
|
||||
AissiaPlatform
|
||||
)
|
||||
set_target_properties(MonitoringModule PROPERTIES
|
||||
PREFIX "lib"
|
||||
LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/modules
|
||||
)
|
||||
|
||||
# AIModule - LLM integration
|
||||
# AIModule - Pure logic, no HTTP (uses LLMService via IIO)
|
||||
add_library(AIModule SHARED
|
||||
src/modules/AIModule.cpp
|
||||
)
|
||||
@ -176,14 +211,13 @@ target_include_directories(AIModule PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
|
||||
target_link_libraries(AIModule PRIVATE
|
||||
GroveEngine::impl
|
||||
spdlog::spdlog
|
||||
AissiaLLM
|
||||
)
|
||||
set_target_properties(AIModule PROPERTIES
|
||||
PREFIX "lib"
|
||||
LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/modules
|
||||
)
|
||||
|
||||
# VoiceModule - TTS/STT
|
||||
# VoiceModule - Pure logic, no TTS (uses VoiceService via IIO)
|
||||
add_library(VoiceModule SHARED
|
||||
src/modules/VoiceModule.cpp
|
||||
)
|
||||
@ -191,7 +225,6 @@ target_include_directories(VoiceModule PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
|
||||
target_link_libraries(VoiceModule PRIVATE
|
||||
GroveEngine::impl
|
||||
spdlog::spdlog
|
||||
AissiaAudio
|
||||
)
|
||||
set_target_properties(VoiceModule PROPERTIES
|
||||
PREFIX "lib"
|
||||
|
||||
340
audits/2025-11-26-engine-compliance-audit.md
Normal file
340
audits/2025-11-26-engine-compliance-audit.md
Normal file
@ -0,0 +1,340 @@
|
||||
# AUDIT DE CONFORMITÉ GROVEENGINE - AISSIA
|
||||
|
||||
**Date** : 2025-11-26
|
||||
**Auditeur** : Claude Code
|
||||
**Version auditée** : Commit bc3b6cb
|
||||
|
||||
---
|
||||
|
||||
## RÉSUMÉ EXÉCUTIF
|
||||
|
||||
**Verdict : Le code contourne massivement les principes de GroveEngine.**
|
||||
|
||||
| Module | Lignes | Conformité Engine | Statut |
|
||||
|--------|--------|-------------------|--------|
|
||||
| AIModule | 306 | VIOLATION | Infrastructure dans module |
|
||||
| MonitoringModule | 222 | VIOLATION | Appels OS dans module |
|
||||
| StorageModule | 273 | VIOLATION | SQLite dans module |
|
||||
| VoiceModule | 209 | VIOLATION | TTS/COM dans module |
|
||||
| SchedulerModule | 179 | CONFORME | Logique métier pure |
|
||||
| NotificationModule | 172 | CONFORME | Logique métier pure |
|
||||
|
||||
**Score global** : 2/6 modules conformes (33%)
|
||||
|
||||
---
|
||||
|
||||
## RAPPEL DES PRINCIPES GROVEENGINE
|
||||
|
||||
Selon `docs/GROVEENGINE_GUIDE.md` :
|
||||
|
||||
1. **Modules = Pure business logic** (200-300 lignes recommandées)
|
||||
2. **No infrastructure code in modules** : threading, networking, persistence
|
||||
3. **All data via IDataNode abstraction** (backend agnostic)
|
||||
4. **Pull-based message processing** via IIO pub/sub
|
||||
5. **Hot-reload ready** : sérialiser tout l'état dans `getState()`
|
||||
|
||||
---
|
||||
|
||||
## VIOLATIONS CRITIQUES
|
||||
|
||||
### 1. AIModule - Networking dans le module
|
||||
|
||||
**Fichier** : `src/modules/AIModule.cpp:146`
|
||||
|
||||
```cpp
|
||||
nlohmann::json AIModule::agenticLoop(const std::string& userQuery) {
|
||||
// ...
|
||||
auto response = m_provider->chat(m_systemPrompt, messages, tools);
|
||||
// Appel HTTP synchrone bloquant !
|
||||
}
|
||||
```
|
||||
|
||||
**Violation** : Appels HTTP synchrones directement dans `process()` via la boucle agentique.
|
||||
|
||||
**Impact** :
|
||||
- Bloque la boucle principale pendant chaque requête LLM (timeout 60s)
|
||||
- `isIdle()` retourne false pendant l'appel, mais le module reste bloquant
|
||||
- Hot-reload impossible pendant une requête en cours
|
||||
- Tous les autres modules sont bloqués
|
||||
|
||||
**Correction requise** : Déléguer les appels LLM à un service infrastructure externe, communication via IIO async.
|
||||
|
||||
---
|
||||
|
||||
### 2. StorageModule - Persistence dans le module
|
||||
|
||||
**Fichier** : `src/modules/StorageModule.cpp:78-91`
|
||||
|
||||
```cpp
|
||||
bool StorageModule::openDatabase() {
|
||||
int rc = sqlite3_open(m_dbPath.c_str(), &m_db);
|
||||
// Handle SQLite directement dans le module
|
||||
}
|
||||
```
|
||||
|
||||
**Violation** : Gestion directe de SQLite. L'engine préconise `IDataNode` abstractions pour la persistence.
|
||||
|
||||
**Impact** :
|
||||
- Hot-reload risqué (handle DB ouvert)
|
||||
- Risque de corruption si reload pendant transaction
|
||||
- Couplage fort avec SQLite
|
||||
|
||||
**Correction requise** : Service StorageService dans main.cpp, modules communiquent via topics `storage:*`.
|
||||
|
||||
---
|
||||
|
||||
### 3. MonitoringModule - Appels OS dans le module
|
||||
|
||||
**Fichier** : `src/modules/MonitoringModule.cpp:78-79`
|
||||
|
||||
```cpp
|
||||
void MonitoringModule::checkCurrentApp(float currentTime) {
|
||||
std::string newApp = m_tracker->getCurrentAppName();
|
||||
// Appelle GetForegroundWindow(), OpenProcess(), etc.
|
||||
}
|
||||
```
|
||||
|
||||
**Violation** : Appels Win32 API dans `process()`. Même encapsulé dans `IWindowTracker`, c'est du code plateforme dans un module hot-reloadable.
|
||||
|
||||
**Impact** :
|
||||
- Dépendance plateforme dans le module
|
||||
- Handles système potentiellement orphelins au reload
|
||||
|
||||
**Correction requise** : Service PlatformService qui publie `monitoring:window_info` périodiquement.
|
||||
|
||||
---
|
||||
|
||||
### 4. VoiceModule - COM/SAPI dans le module
|
||||
|
||||
**Fichier** : `src/modules/VoiceModule.cpp:122`
|
||||
|
||||
```cpp
|
||||
void VoiceModule::speak(const std::string& text) {
|
||||
m_ttsEngine->speak(text, true);
|
||||
// Appel ISpVoice::Speak via COM
|
||||
}
|
||||
```
|
||||
|
||||
**Fichier** : `src/shared/audio/SAPITTSEngine.hpp:26`
|
||||
|
||||
```cpp
|
||||
HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
||||
```
|
||||
|
||||
**Violation** : Initialisation COM et appels SAPI dans le module.
|
||||
|
||||
**Impact** :
|
||||
- `CoInitializeEx` par thread, hot-reload peut causer des fuites
|
||||
- Appels asynchrones SAPI difficiles à gérer au shutdown
|
||||
|
||||
**Correction requise** : Service VoiceService dédié, modules envoient `voice:speak`.
|
||||
|
||||
---
|
||||
|
||||
## PROBLÈMES DE DESIGN
|
||||
|
||||
### 5. Topics incohérents
|
||||
|
||||
**SchedulerModule.h:26** utilise le format slash :
|
||||
```cpp
|
||||
// "scheduler/hyperfocus_alert"
|
||||
```
|
||||
|
||||
**AIModule.cpp:52** utilise le format colon :
|
||||
```cpp
|
||||
m_io->subscribe("scheduler:hyperfocus_alert", subConfig);
|
||||
```
|
||||
|
||||
**Standard GroveEngine** : Format `module:event` (colon)
|
||||
|
||||
**Impact** : Les messages ne seront jamais reçus si les formats ne correspondent pas.
|
||||
|
||||
---
|
||||
|
||||
### 6. SchedulerModule - IIO non utilisé
|
||||
|
||||
**Fichier** : `src/modules/SchedulerModule.cpp:66-68`
|
||||
|
||||
```cpp
|
||||
void SchedulerModule::checkHyperfocus(float currentTime) {
|
||||
// ...
|
||||
// Publier l'alerte (si IO disponible)
|
||||
// Note: Dans une version complète, on publierait via m_io
|
||||
}
|
||||
```
|
||||
|
||||
**Problème** : Le SchedulerModule a `m_io` mais ne publie JAMAIS rien. Les autres modules s'abonnent à `scheduler:*` mais ne recevront rien.
|
||||
|
||||
---
|
||||
|
||||
### 7. État non restaurable - StorageModule
|
||||
|
||||
**Fichier** : `src/modules/StorageModule.cpp:246-258`
|
||||
|
||||
```cpp
|
||||
std::unique_ptr<grove::IDataNode> StorageModule::getState() {
|
||||
state->setBool("isConnected", m_isConnected);
|
||||
// ...
|
||||
}
|
||||
|
||||
void StorageModule::setState(const grove::IDataNode& state) {
|
||||
// NE ROUVRE PAS la connexion DB !
|
||||
m_logger->info("Etat restore...");
|
||||
}
|
||||
```
|
||||
|
||||
**Problème** : `setState()` ne restaure pas la connexion SQLite. Après hot-reload, le module est dans un état incohérent.
|
||||
|
||||
---
|
||||
|
||||
### 8. Libraries statiques dans modules
|
||||
|
||||
**CMakeLists.txt:86-101** :
|
||||
|
||||
```cmake
|
||||
add_library(AissiaLLM STATIC ...)
|
||||
target_link_libraries(AIModule PRIVATE AissiaLLM)
|
||||
```
|
||||
|
||||
**Problème** : Les libs `AissiaLLM`, `AissiaPlatform`, `AissiaAudio` sont compilées en STATIC et linkées dans chaque .so.
|
||||
|
||||
**Impact** :
|
||||
- Code dupliqué dans chaque module
|
||||
- Hot-reload ne rafraîchit pas ces libs
|
||||
- Pas de partage d'état entre modules
|
||||
|
||||
---
|
||||
|
||||
### 9. Dépassement limite de lignes
|
||||
|
||||
| Module | Lignes | Limite recommandée |
|
||||
|--------|--------|-------------------|
|
||||
| AIModule | 306 | 200-300 |
|
||||
|
||||
Le dépassement est mineur mais symptomatique : le module fait trop de choses.
|
||||
|
||||
---
|
||||
|
||||
## CE QUI EST CONFORME
|
||||
|
||||
### SchedulerModule & NotificationModule
|
||||
|
||||
Ces deux modules respectent les principes :
|
||||
- Logique métier pure
|
||||
- Pas d'appels système
|
||||
- État sérialisable
|
||||
- Taille appropriée
|
||||
|
||||
### Structure IModule
|
||||
|
||||
Tous les modules implémentent correctement :
|
||||
- `process()`
|
||||
- `setConfiguration()`
|
||||
- `getState()` / `setState()`
|
||||
- `getHealthStatus()`
|
||||
- `shutdown()`
|
||||
- Exports C `createModule()` / `destroyModule()`
|
||||
|
||||
### main.cpp
|
||||
|
||||
La boucle principale est bien implémentée :
|
||||
- FileWatcher pour hot-reload
|
||||
- Frame timing à 10Hz
|
||||
- Signal handling propre
|
||||
- Chargement/déchargement correct
|
||||
|
||||
---
|
||||
|
||||
## ARCHITECTURE RECOMMANDÉE
|
||||
|
||||
### Actuelle (INCORRECTE)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ main.cpp │
|
||||
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
|
||||
│ │AIModule │ │Storage │ │Monitor │ │Voice │ │
|
||||
│ │ +HTTP │ │ +SQLite │ │ +Win32 │ │ +COM │ │
|
||||
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
Infrastructure DANS les modules = VIOLATION
|
||||
```
|
||||
|
||||
### Corrigée (CONFORME)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ main.cpp │
|
||||
│ │
|
||||
│ ┌─────────────── INFRASTRUCTURE ──────────────────┐ │
|
||||
│ │ LLMService │ StorageService │ PlatformService │ │ │
|
||||
│ │ (async) │ (SQLite) │ (Win32) │ │ │
|
||||
│ └──────────────────────────────────────────────────┘ │
|
||||
│ ↑↓ IIO pub/sub (async, non-bloquant) │
|
||||
│ ┌─────────────── MODULES (hot-reload) ────────────┐ │
|
||||
│ │ AIModule │ StorageModule │ MonitoringModule │ │ │
|
||||
│ │ (logic) │ (logic) │ (logic) │ │ │
|
||||
│ └──────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
Infrastructure HORS modules = CONFORME
|
||||
```
|
||||
|
||||
### Flux de données corrigé
|
||||
|
||||
```
|
||||
User query → voice:transcription → AIModule
|
||||
AIModule → ai:query_request → LLMService (async)
|
||||
LLMService → ai:response → AIModule
|
||||
AIModule → ai:response → VoiceModule → voice:speak
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ACTIONS REQUISES
|
||||
|
||||
### Priorité HAUTE
|
||||
|
||||
1. **Extraire LLM de AIModule**
|
||||
- Créer `LLMService` dans main.cpp ou service dédié
|
||||
- AIModule publie `ai:query_request`, reçoit `ai:response`
|
||||
- Appels HTTP dans thread séparé
|
||||
|
||||
2. **Extraire SQLite de StorageModule**
|
||||
- Créer `StorageService`
|
||||
- Modules publient `storage:save_*`, reçoivent `storage:result`
|
||||
|
||||
3. **Extraire Win32 de MonitoringModule**
|
||||
- Créer `PlatformService`
|
||||
- Publie `platform:window_changed` périodiquement
|
||||
|
||||
4. **Extraire TTS de VoiceModule**
|
||||
- Créer `VoiceService`
|
||||
- Modules publient `voice:speak`
|
||||
|
||||
### Priorité MOYENNE
|
||||
|
||||
5. **Corriger format topics** : Tout en `module:event`
|
||||
6. **Implémenter publish dans SchedulerModule**
|
||||
7. **Corriger setState dans StorageModule**
|
||||
|
||||
### Priorité BASSE
|
||||
|
||||
8. **Refactorer libs STATIC en services**
|
||||
9. **Réduire AIModule sous 300 lignes**
|
||||
|
||||
---
|
||||
|
||||
## CONCLUSION
|
||||
|
||||
Le code actuel **simule** l'utilisation de GroveEngine mais le **contourne** en plaçant l'infrastructure directement dans les modules.
|
||||
|
||||
Les modules ne sont **pas véritablement hot-reloadable** car ils :
|
||||
1. Possèdent des ressources système (DB handles, COM objects)
|
||||
2. Font des appels bloquants (HTTP 60s timeout, TTS)
|
||||
3. Ne communiquent pas correctement via IIO
|
||||
|
||||
**Refactoring majeur requis** pour extraire l'infrastructure des modules vers des services dédiés dans main.cpp.
|
||||
|
||||
---
|
||||
|
||||
*Audit généré automatiquement par Claude Code*
|
||||
29636
deps/sqlite/shell.c
vendored
Normal file
29636
deps/sqlite/shell.c
vendored
Normal file
File diff suppressed because it is too large
Load Diff
255636
deps/sqlite/sqlite3.c
vendored
Normal file
255636
deps/sqlite/sqlite3.c
vendored
Normal file
File diff suppressed because it is too large
Load Diff
13355
deps/sqlite/sqlite3.h
vendored
Normal file
13355
deps/sqlite/sqlite3.h
vendored
Normal file
File diff suppressed because it is too large
Load Diff
719
deps/sqlite/sqlite3ext.h
vendored
Normal file
719
deps/sqlite/sqlite3ext.h
vendored
Normal file
@ -0,0 +1,719 @@
|
||||
/*
|
||||
** 2006 June 7
|
||||
**
|
||||
** The author disclaims copyright to this source code. In place of
|
||||
** a legal notice, here is a blessing:
|
||||
**
|
||||
** May you do good and not evil.
|
||||
** May you find forgiveness for yourself and forgive others.
|
||||
** May you share freely, never taking more than you give.
|
||||
**
|
||||
*************************************************************************
|
||||
** This header file defines the SQLite interface for use by
|
||||
** shared libraries that want to be imported as extensions into
|
||||
** an SQLite instance. Shared libraries that intend to be loaded
|
||||
** as extensions by SQLite should #include this file instead of
|
||||
** sqlite3.h.
|
||||
*/
|
||||
#ifndef SQLITE3EXT_H
|
||||
#define SQLITE3EXT_H
|
||||
#include "sqlite3.h"
|
||||
|
||||
/*
|
||||
** The following structure holds pointers to all of the SQLite API
|
||||
** routines.
|
||||
**
|
||||
** WARNING: In order to maintain backwards compatibility, add new
|
||||
** interfaces to the end of this structure only. If you insert new
|
||||
** interfaces in the middle of this structure, then older different
|
||||
** versions of SQLite will not be able to load each other's shared
|
||||
** libraries!
|
||||
*/
|
||||
struct sqlite3_api_routines {
|
||||
void * (*aggregate_context)(sqlite3_context*,int nBytes);
|
||||
int (*aggregate_count)(sqlite3_context*);
|
||||
int (*bind_blob)(sqlite3_stmt*,int,const void*,int n,void(*)(void*));
|
||||
int (*bind_double)(sqlite3_stmt*,int,double);
|
||||
int (*bind_int)(sqlite3_stmt*,int,int);
|
||||
int (*bind_int64)(sqlite3_stmt*,int,sqlite_int64);
|
||||
int (*bind_null)(sqlite3_stmt*,int);
|
||||
int (*bind_parameter_count)(sqlite3_stmt*);
|
||||
int (*bind_parameter_index)(sqlite3_stmt*,const char*zName);
|
||||
const char * (*bind_parameter_name)(sqlite3_stmt*,int);
|
||||
int (*bind_text)(sqlite3_stmt*,int,const char*,int n,void(*)(void*));
|
||||
int (*bind_text16)(sqlite3_stmt*,int,const void*,int,void(*)(void*));
|
||||
int (*bind_value)(sqlite3_stmt*,int,const sqlite3_value*);
|
||||
int (*busy_handler)(sqlite3*,int(*)(void*,int),void*);
|
||||
int (*busy_timeout)(sqlite3*,int ms);
|
||||
int (*changes)(sqlite3*);
|
||||
int (*close)(sqlite3*);
|
||||
int (*collation_needed)(sqlite3*,void*,void(*)(void*,sqlite3*,
|
||||
int eTextRep,const char*));
|
||||
int (*collation_needed16)(sqlite3*,void*,void(*)(void*,sqlite3*,
|
||||
int eTextRep,const void*));
|
||||
const void * (*column_blob)(sqlite3_stmt*,int iCol);
|
||||
int (*column_bytes)(sqlite3_stmt*,int iCol);
|
||||
int (*column_bytes16)(sqlite3_stmt*,int iCol);
|
||||
int (*column_count)(sqlite3_stmt*pStmt);
|
||||
const char * (*column_database_name)(sqlite3_stmt*,int);
|
||||
const void * (*column_database_name16)(sqlite3_stmt*,int);
|
||||
const char * (*column_decltype)(sqlite3_stmt*,int i);
|
||||
const void * (*column_decltype16)(sqlite3_stmt*,int);
|
||||
double (*column_double)(sqlite3_stmt*,int iCol);
|
||||
int (*column_int)(sqlite3_stmt*,int iCol);
|
||||
sqlite_int64 (*column_int64)(sqlite3_stmt*,int iCol);
|
||||
const char * (*column_name)(sqlite3_stmt*,int);
|
||||
const void * (*column_name16)(sqlite3_stmt*,int);
|
||||
const char * (*column_origin_name)(sqlite3_stmt*,int);
|
||||
const void * (*column_origin_name16)(sqlite3_stmt*,int);
|
||||
const char * (*column_table_name)(sqlite3_stmt*,int);
|
||||
const void * (*column_table_name16)(sqlite3_stmt*,int);
|
||||
const unsigned char * (*column_text)(sqlite3_stmt*,int iCol);
|
||||
const void * (*column_text16)(sqlite3_stmt*,int iCol);
|
||||
int (*column_type)(sqlite3_stmt*,int iCol);
|
||||
sqlite3_value* (*column_value)(sqlite3_stmt*,int iCol);
|
||||
void * (*commit_hook)(sqlite3*,int(*)(void*),void*);
|
||||
int (*complete)(const char*sql);
|
||||
int (*complete16)(const void*sql);
|
||||
int (*create_collation)(sqlite3*,const char*,int,void*,
|
||||
int(*)(void*,int,const void*,int,const void*));
|
||||
int (*create_collation16)(sqlite3*,const void*,int,void*,
|
||||
int(*)(void*,int,const void*,int,const void*));
|
||||
int (*create_function)(sqlite3*,const char*,int,int,void*,
|
||||
void (*xFunc)(sqlite3_context*,int,sqlite3_value**),
|
||||
void (*xStep)(sqlite3_context*,int,sqlite3_value**),
|
||||
void (*xFinal)(sqlite3_context*));
|
||||
int (*create_function16)(sqlite3*,const void*,int,int,void*,
|
||||
void (*xFunc)(sqlite3_context*,int,sqlite3_value**),
|
||||
void (*xStep)(sqlite3_context*,int,sqlite3_value**),
|
||||
void (*xFinal)(sqlite3_context*));
|
||||
int (*create_module)(sqlite3*,const char*,const sqlite3_module*,void*);
|
||||
int (*data_count)(sqlite3_stmt*pStmt);
|
||||
sqlite3 * (*db_handle)(sqlite3_stmt*);
|
||||
int (*declare_vtab)(sqlite3*,const char*);
|
||||
int (*enable_shared_cache)(int);
|
||||
int (*errcode)(sqlite3*db);
|
||||
const char * (*errmsg)(sqlite3*);
|
||||
const void * (*errmsg16)(sqlite3*);
|
||||
int (*exec)(sqlite3*,const char*,sqlite3_callback,void*,char**);
|
||||
int (*expired)(sqlite3_stmt*);
|
||||
int (*finalize)(sqlite3_stmt*pStmt);
|
||||
void (*free)(void*);
|
||||
void (*free_table)(char**result);
|
||||
int (*get_autocommit)(sqlite3*);
|
||||
void * (*get_auxdata)(sqlite3_context*,int);
|
||||
int (*get_table)(sqlite3*,const char*,char***,int*,int*,char**);
|
||||
int (*global_recover)(void);
|
||||
void (*interruptx)(sqlite3*);
|
||||
sqlite_int64 (*last_insert_rowid)(sqlite3*);
|
||||
const char * (*libversion)(void);
|
||||
int (*libversion_number)(void);
|
||||
void *(*malloc)(int);
|
||||
char * (*mprintf)(const char*,...);
|
||||
int (*open)(const char*,sqlite3**);
|
||||
int (*open16)(const void*,sqlite3**);
|
||||
int (*prepare)(sqlite3*,const char*,int,sqlite3_stmt**,const char**);
|
||||
int (*prepare16)(sqlite3*,const void*,int,sqlite3_stmt**,const void**);
|
||||
void * (*profile)(sqlite3*,void(*)(void*,const char*,sqlite_uint64),void*);
|
||||
void (*progress_handler)(sqlite3*,int,int(*)(void*),void*);
|
||||
void *(*realloc)(void*,int);
|
||||
int (*reset)(sqlite3_stmt*pStmt);
|
||||
void (*result_blob)(sqlite3_context*,const void*,int,void(*)(void*));
|
||||
void (*result_double)(sqlite3_context*,double);
|
||||
void (*result_error)(sqlite3_context*,const char*,int);
|
||||
void (*result_error16)(sqlite3_context*,const void*,int);
|
||||
void (*result_int)(sqlite3_context*,int);
|
||||
void (*result_int64)(sqlite3_context*,sqlite_int64);
|
||||
void (*result_null)(sqlite3_context*);
|
||||
void (*result_text)(sqlite3_context*,const char*,int,void(*)(void*));
|
||||
void (*result_text16)(sqlite3_context*,const void*,int,void(*)(void*));
|
||||
void (*result_text16be)(sqlite3_context*,const void*,int,void(*)(void*));
|
||||
void (*result_text16le)(sqlite3_context*,const void*,int,void(*)(void*));
|
||||
void (*result_value)(sqlite3_context*,sqlite3_value*);
|
||||
void * (*rollback_hook)(sqlite3*,void(*)(void*),void*);
|
||||
int (*set_authorizer)(sqlite3*,int(*)(void*,int,const char*,const char*,
|
||||
const char*,const char*),void*);
|
||||
void (*set_auxdata)(sqlite3_context*,int,void*,void (*)(void*));
|
||||
char * (*xsnprintf)(int,char*,const char*,...);
|
||||
int (*step)(sqlite3_stmt*);
|
||||
int (*table_column_metadata)(sqlite3*,const char*,const char*,const char*,
|
||||
char const**,char const**,int*,int*,int*);
|
||||
void (*thread_cleanup)(void);
|
||||
int (*total_changes)(sqlite3*);
|
||||
void * (*trace)(sqlite3*,void(*xTrace)(void*,const char*),void*);
|
||||
int (*transfer_bindings)(sqlite3_stmt*,sqlite3_stmt*);
|
||||
void * (*update_hook)(sqlite3*,void(*)(void*,int ,char const*,char const*,
|
||||
sqlite_int64),void*);
|
||||
void * (*user_data)(sqlite3_context*);
|
||||
const void * (*value_blob)(sqlite3_value*);
|
||||
int (*value_bytes)(sqlite3_value*);
|
||||
int (*value_bytes16)(sqlite3_value*);
|
||||
double (*value_double)(sqlite3_value*);
|
||||
int (*value_int)(sqlite3_value*);
|
||||
sqlite_int64 (*value_int64)(sqlite3_value*);
|
||||
int (*value_numeric_type)(sqlite3_value*);
|
||||
const unsigned char * (*value_text)(sqlite3_value*);
|
||||
const void * (*value_text16)(sqlite3_value*);
|
||||
const void * (*value_text16be)(sqlite3_value*);
|
||||
const void * (*value_text16le)(sqlite3_value*);
|
||||
int (*value_type)(sqlite3_value*);
|
||||
char *(*vmprintf)(const char*,va_list);
|
||||
/* Added ??? */
|
||||
int (*overload_function)(sqlite3*, const char *zFuncName, int nArg);
|
||||
/* Added by 3.3.13 */
|
||||
int (*prepare_v2)(sqlite3*,const char*,int,sqlite3_stmt**,const char**);
|
||||
int (*prepare16_v2)(sqlite3*,const void*,int,sqlite3_stmt**,const void**);
|
||||
int (*clear_bindings)(sqlite3_stmt*);
|
||||
/* Added by 3.4.1 */
|
||||
int (*create_module_v2)(sqlite3*,const char*,const sqlite3_module*,void*,
|
||||
void (*xDestroy)(void *));
|
||||
/* Added by 3.5.0 */
|
||||
int (*bind_zeroblob)(sqlite3_stmt*,int,int);
|
||||
int (*blob_bytes)(sqlite3_blob*);
|
||||
int (*blob_close)(sqlite3_blob*);
|
||||
int (*blob_open)(sqlite3*,const char*,const char*,const char*,sqlite3_int64,
|
||||
int,sqlite3_blob**);
|
||||
int (*blob_read)(sqlite3_blob*,void*,int,int);
|
||||
int (*blob_write)(sqlite3_blob*,const void*,int,int);
|
||||
int (*create_collation_v2)(sqlite3*,const char*,int,void*,
|
||||
int(*)(void*,int,const void*,int,const void*),
|
||||
void(*)(void*));
|
||||
int (*file_control)(sqlite3*,const char*,int,void*);
|
||||
sqlite3_int64 (*memory_highwater)(int);
|
||||
sqlite3_int64 (*memory_used)(void);
|
||||
sqlite3_mutex *(*mutex_alloc)(int);
|
||||
void (*mutex_enter)(sqlite3_mutex*);
|
||||
void (*mutex_free)(sqlite3_mutex*);
|
||||
void (*mutex_leave)(sqlite3_mutex*);
|
||||
int (*mutex_try)(sqlite3_mutex*);
|
||||
int (*open_v2)(const char*,sqlite3**,int,const char*);
|
||||
int (*release_memory)(int);
|
||||
void (*result_error_nomem)(sqlite3_context*);
|
||||
void (*result_error_toobig)(sqlite3_context*);
|
||||
int (*sleep)(int);
|
||||
void (*soft_heap_limit)(int);
|
||||
sqlite3_vfs *(*vfs_find)(const char*);
|
||||
int (*vfs_register)(sqlite3_vfs*,int);
|
||||
int (*vfs_unregister)(sqlite3_vfs*);
|
||||
int (*xthreadsafe)(void);
|
||||
void (*result_zeroblob)(sqlite3_context*,int);
|
||||
void (*result_error_code)(sqlite3_context*,int);
|
||||
int (*test_control)(int, ...);
|
||||
void (*randomness)(int,void*);
|
||||
sqlite3 *(*context_db_handle)(sqlite3_context*);
|
||||
int (*extended_result_codes)(sqlite3*,int);
|
||||
int (*limit)(sqlite3*,int,int);
|
||||
sqlite3_stmt *(*next_stmt)(sqlite3*,sqlite3_stmt*);
|
||||
const char *(*sql)(sqlite3_stmt*);
|
||||
int (*status)(int,int*,int*,int);
|
||||
int (*backup_finish)(sqlite3_backup*);
|
||||
sqlite3_backup *(*backup_init)(sqlite3*,const char*,sqlite3*,const char*);
|
||||
int (*backup_pagecount)(sqlite3_backup*);
|
||||
int (*backup_remaining)(sqlite3_backup*);
|
||||
int (*backup_step)(sqlite3_backup*,int);
|
||||
const char *(*compileoption_get)(int);
|
||||
int (*compileoption_used)(const char*);
|
||||
int (*create_function_v2)(sqlite3*,const char*,int,int,void*,
|
||||
void (*xFunc)(sqlite3_context*,int,sqlite3_value**),
|
||||
void (*xStep)(sqlite3_context*,int,sqlite3_value**),
|
||||
void (*xFinal)(sqlite3_context*),
|
||||
void(*xDestroy)(void*));
|
||||
int (*db_config)(sqlite3*,int,...);
|
||||
sqlite3_mutex *(*db_mutex)(sqlite3*);
|
||||
int (*db_status)(sqlite3*,int,int*,int*,int);
|
||||
int (*extended_errcode)(sqlite3*);
|
||||
void (*log)(int,const char*,...);
|
||||
sqlite3_int64 (*soft_heap_limit64)(sqlite3_int64);
|
||||
const char *(*sourceid)(void);
|
||||
int (*stmt_status)(sqlite3_stmt*,int,int);
|
||||
int (*strnicmp)(const char*,const char*,int);
|
||||
int (*unlock_notify)(sqlite3*,void(*)(void**,int),void*);
|
||||
int (*wal_autocheckpoint)(sqlite3*,int);
|
||||
int (*wal_checkpoint)(sqlite3*,const char*);
|
||||
void *(*wal_hook)(sqlite3*,int(*)(void*,sqlite3*,const char*,int),void*);
|
||||
int (*blob_reopen)(sqlite3_blob*,sqlite3_int64);
|
||||
int (*vtab_config)(sqlite3*,int op,...);
|
||||
int (*vtab_on_conflict)(sqlite3*);
|
||||
/* Version 3.7.16 and later */
|
||||
int (*close_v2)(sqlite3*);
|
||||
const char *(*db_filename)(sqlite3*,const char*);
|
||||
int (*db_readonly)(sqlite3*,const char*);
|
||||
int (*db_release_memory)(sqlite3*);
|
||||
const char *(*errstr)(int);
|
||||
int (*stmt_busy)(sqlite3_stmt*);
|
||||
int (*stmt_readonly)(sqlite3_stmt*);
|
||||
int (*stricmp)(const char*,const char*);
|
||||
int (*uri_boolean)(const char*,const char*,int);
|
||||
sqlite3_int64 (*uri_int64)(const char*,const char*,sqlite3_int64);
|
||||
const char *(*uri_parameter)(const char*,const char*);
|
||||
char *(*xvsnprintf)(int,char*,const char*,va_list);
|
||||
int (*wal_checkpoint_v2)(sqlite3*,const char*,int,int*,int*);
|
||||
/* Version 3.8.7 and later */
|
||||
int (*auto_extension)(void(*)(void));
|
||||
int (*bind_blob64)(sqlite3_stmt*,int,const void*,sqlite3_uint64,
|
||||
void(*)(void*));
|
||||
int (*bind_text64)(sqlite3_stmt*,int,const char*,sqlite3_uint64,
|
||||
void(*)(void*),unsigned char);
|
||||
int (*cancel_auto_extension)(void(*)(void));
|
||||
int (*load_extension)(sqlite3*,const char*,const char*,char**);
|
||||
void *(*malloc64)(sqlite3_uint64);
|
||||
sqlite3_uint64 (*msize)(void*);
|
||||
void *(*realloc64)(void*,sqlite3_uint64);
|
||||
void (*reset_auto_extension)(void);
|
||||
void (*result_blob64)(sqlite3_context*,const void*,sqlite3_uint64,
|
||||
void(*)(void*));
|
||||
void (*result_text64)(sqlite3_context*,const char*,sqlite3_uint64,
|
||||
void(*)(void*), unsigned char);
|
||||
int (*strglob)(const char*,const char*);
|
||||
/* Version 3.8.11 and later */
|
||||
sqlite3_value *(*value_dup)(const sqlite3_value*);
|
||||
void (*value_free)(sqlite3_value*);
|
||||
int (*result_zeroblob64)(sqlite3_context*,sqlite3_uint64);
|
||||
int (*bind_zeroblob64)(sqlite3_stmt*, int, sqlite3_uint64);
|
||||
/* Version 3.9.0 and later */
|
||||
unsigned int (*value_subtype)(sqlite3_value*);
|
||||
void (*result_subtype)(sqlite3_context*,unsigned int);
|
||||
/* Version 3.10.0 and later */
|
||||
int (*status64)(int,sqlite3_int64*,sqlite3_int64*,int);
|
||||
int (*strlike)(const char*,const char*,unsigned int);
|
||||
int (*db_cacheflush)(sqlite3*);
|
||||
/* Version 3.12.0 and later */
|
||||
int (*system_errno)(sqlite3*);
|
||||
/* Version 3.14.0 and later */
|
||||
int (*trace_v2)(sqlite3*,unsigned,int(*)(unsigned,void*,void*,void*),void*);
|
||||
char *(*expanded_sql)(sqlite3_stmt*);
|
||||
/* Version 3.18.0 and later */
|
||||
void (*set_last_insert_rowid)(sqlite3*,sqlite3_int64);
|
||||
/* Version 3.20.0 and later */
|
||||
int (*prepare_v3)(sqlite3*,const char*,int,unsigned int,
|
||||
sqlite3_stmt**,const char**);
|
||||
int (*prepare16_v3)(sqlite3*,const void*,int,unsigned int,
|
||||
sqlite3_stmt**,const void**);
|
||||
int (*bind_pointer)(sqlite3_stmt*,int,void*,const char*,void(*)(void*));
|
||||
void (*result_pointer)(sqlite3_context*,void*,const char*,void(*)(void*));
|
||||
void *(*value_pointer)(sqlite3_value*,const char*);
|
||||
int (*vtab_nochange)(sqlite3_context*);
|
||||
int (*value_nochange)(sqlite3_value*);
|
||||
const char *(*vtab_collation)(sqlite3_index_info*,int);
|
||||
/* Version 3.24.0 and later */
|
||||
int (*keyword_count)(void);
|
||||
int (*keyword_name)(int,const char**,int*);
|
||||
int (*keyword_check)(const char*,int);
|
||||
sqlite3_str *(*str_new)(sqlite3*);
|
||||
char *(*str_finish)(sqlite3_str*);
|
||||
void (*str_appendf)(sqlite3_str*, const char *zFormat, ...);
|
||||
void (*str_vappendf)(sqlite3_str*, const char *zFormat, va_list);
|
||||
void (*str_append)(sqlite3_str*, const char *zIn, int N);
|
||||
void (*str_appendall)(sqlite3_str*, const char *zIn);
|
||||
void (*str_appendchar)(sqlite3_str*, int N, char C);
|
||||
void (*str_reset)(sqlite3_str*);
|
||||
int (*str_errcode)(sqlite3_str*);
|
||||
int (*str_length)(sqlite3_str*);
|
||||
char *(*str_value)(sqlite3_str*);
|
||||
/* Version 3.25.0 and later */
|
||||
int (*create_window_function)(sqlite3*,const char*,int,int,void*,
|
||||
void (*xStep)(sqlite3_context*,int,sqlite3_value**),
|
||||
void (*xFinal)(sqlite3_context*),
|
||||
void (*xValue)(sqlite3_context*),
|
||||
void (*xInv)(sqlite3_context*,int,sqlite3_value**),
|
||||
void(*xDestroy)(void*));
|
||||
/* Version 3.26.0 and later */
|
||||
const char *(*normalized_sql)(sqlite3_stmt*);
|
||||
/* Version 3.28.0 and later */
|
||||
int (*stmt_isexplain)(sqlite3_stmt*);
|
||||
int (*value_frombind)(sqlite3_value*);
|
||||
/* Version 3.30.0 and later */
|
||||
int (*drop_modules)(sqlite3*,const char**);
|
||||
/* Version 3.31.0 and later */
|
||||
sqlite3_int64 (*hard_heap_limit64)(sqlite3_int64);
|
||||
const char *(*uri_key)(const char*,int);
|
||||
const char *(*filename_database)(const char*);
|
||||
const char *(*filename_journal)(const char*);
|
||||
const char *(*filename_wal)(const char*);
|
||||
/* Version 3.32.0 and later */
|
||||
const char *(*create_filename)(const char*,const char*,const char*,
|
||||
int,const char**);
|
||||
void (*free_filename)(const char*);
|
||||
sqlite3_file *(*database_file_object)(const char*);
|
||||
/* Version 3.34.0 and later */
|
||||
int (*txn_state)(sqlite3*,const char*);
|
||||
/* Version 3.36.1 and later */
|
||||
sqlite3_int64 (*changes64)(sqlite3*);
|
||||
sqlite3_int64 (*total_changes64)(sqlite3*);
|
||||
/* Version 3.37.0 and later */
|
||||
int (*autovacuum_pages)(sqlite3*,
|
||||
unsigned int(*)(void*,const char*,unsigned int,unsigned int,unsigned int),
|
||||
void*, void(*)(void*));
|
||||
/* Version 3.38.0 and later */
|
||||
int (*error_offset)(sqlite3*);
|
||||
int (*vtab_rhs_value)(sqlite3_index_info*,int,sqlite3_value**);
|
||||
int (*vtab_distinct)(sqlite3_index_info*);
|
||||
int (*vtab_in)(sqlite3_index_info*,int,int);
|
||||
int (*vtab_in_first)(sqlite3_value*,sqlite3_value**);
|
||||
int (*vtab_in_next)(sqlite3_value*,sqlite3_value**);
|
||||
/* Version 3.39.0 and later */
|
||||
int (*deserialize)(sqlite3*,const char*,unsigned char*,
|
||||
sqlite3_int64,sqlite3_int64,unsigned);
|
||||
unsigned char *(*serialize)(sqlite3*,const char *,sqlite3_int64*,
|
||||
unsigned int);
|
||||
const char *(*db_name)(sqlite3*,int);
|
||||
/* Version 3.40.0 and later */
|
||||
int (*value_encoding)(sqlite3_value*);
|
||||
/* Version 3.41.0 and later */
|
||||
int (*is_interrupted)(sqlite3*);
|
||||
/* Version 3.43.0 and later */
|
||||
int (*stmt_explain)(sqlite3_stmt*,int);
|
||||
/* Version 3.44.0 and later */
|
||||
void *(*get_clientdata)(sqlite3*,const char*);
|
||||
int (*set_clientdata)(sqlite3*, const char*, void*, void(*)(void*));
|
||||
};
|
||||
|
||||
/*
|
||||
** This is the function signature used for all extension entry points. It
|
||||
** is also defined in the file "loadext.c".
|
||||
*/
|
||||
typedef int (*sqlite3_loadext_entry)(
|
||||
sqlite3 *db, /* Handle to the database. */
|
||||
char **pzErrMsg, /* Used to set error string on failure. */
|
||||
const sqlite3_api_routines *pThunk /* Extension API function pointers. */
|
||||
);
|
||||
|
||||
/*
|
||||
** The following macros redefine the API routines so that they are
|
||||
** redirected through the global sqlite3_api structure.
|
||||
**
|
||||
** This header file is also used by the loadext.c source file
|
||||
** (part of the main SQLite library - not an extension) so that
|
||||
** it can get access to the sqlite3_api_routines structure
|
||||
** definition. But the main library does not want to redefine
|
||||
** the API. So the redefinition macros are only valid if the
|
||||
** SQLITE_CORE macros is undefined.
|
||||
*/
|
||||
#if !defined(SQLITE_CORE) && !defined(SQLITE_OMIT_LOAD_EXTENSION)
|
||||
#define sqlite3_aggregate_context sqlite3_api->aggregate_context
|
||||
#ifndef SQLITE_OMIT_DEPRECATED
|
||||
#define sqlite3_aggregate_count sqlite3_api->aggregate_count
|
||||
#endif
|
||||
#define sqlite3_bind_blob sqlite3_api->bind_blob
|
||||
#define sqlite3_bind_double sqlite3_api->bind_double
|
||||
#define sqlite3_bind_int sqlite3_api->bind_int
|
||||
#define sqlite3_bind_int64 sqlite3_api->bind_int64
|
||||
#define sqlite3_bind_null sqlite3_api->bind_null
|
||||
#define sqlite3_bind_parameter_count sqlite3_api->bind_parameter_count
|
||||
#define sqlite3_bind_parameter_index sqlite3_api->bind_parameter_index
|
||||
#define sqlite3_bind_parameter_name sqlite3_api->bind_parameter_name
|
||||
#define sqlite3_bind_text sqlite3_api->bind_text
|
||||
#define sqlite3_bind_text16 sqlite3_api->bind_text16
|
||||
#define sqlite3_bind_value sqlite3_api->bind_value
|
||||
#define sqlite3_busy_handler sqlite3_api->busy_handler
|
||||
#define sqlite3_busy_timeout sqlite3_api->busy_timeout
|
||||
#define sqlite3_changes sqlite3_api->changes
|
||||
#define sqlite3_close sqlite3_api->close
|
||||
#define sqlite3_collation_needed sqlite3_api->collation_needed
|
||||
#define sqlite3_collation_needed16 sqlite3_api->collation_needed16
|
||||
#define sqlite3_column_blob sqlite3_api->column_blob
|
||||
#define sqlite3_column_bytes sqlite3_api->column_bytes
|
||||
#define sqlite3_column_bytes16 sqlite3_api->column_bytes16
|
||||
#define sqlite3_column_count sqlite3_api->column_count
|
||||
#define sqlite3_column_database_name sqlite3_api->column_database_name
|
||||
#define sqlite3_column_database_name16 sqlite3_api->column_database_name16
|
||||
#define sqlite3_column_decltype sqlite3_api->column_decltype
|
||||
#define sqlite3_column_decltype16 sqlite3_api->column_decltype16
|
||||
#define sqlite3_column_double sqlite3_api->column_double
|
||||
#define sqlite3_column_int sqlite3_api->column_int
|
||||
#define sqlite3_column_int64 sqlite3_api->column_int64
|
||||
#define sqlite3_column_name sqlite3_api->column_name
|
||||
#define sqlite3_column_name16 sqlite3_api->column_name16
|
||||
#define sqlite3_column_origin_name sqlite3_api->column_origin_name
|
||||
#define sqlite3_column_origin_name16 sqlite3_api->column_origin_name16
|
||||
#define sqlite3_column_table_name sqlite3_api->column_table_name
|
||||
#define sqlite3_column_table_name16 sqlite3_api->column_table_name16
|
||||
#define sqlite3_column_text sqlite3_api->column_text
|
||||
#define sqlite3_column_text16 sqlite3_api->column_text16
|
||||
#define sqlite3_column_type sqlite3_api->column_type
|
||||
#define sqlite3_column_value sqlite3_api->column_value
|
||||
#define sqlite3_commit_hook sqlite3_api->commit_hook
|
||||
#define sqlite3_complete sqlite3_api->complete
|
||||
#define sqlite3_complete16 sqlite3_api->complete16
|
||||
#define sqlite3_create_collation sqlite3_api->create_collation
|
||||
#define sqlite3_create_collation16 sqlite3_api->create_collation16
|
||||
#define sqlite3_create_function sqlite3_api->create_function
|
||||
#define sqlite3_create_function16 sqlite3_api->create_function16
|
||||
#define sqlite3_create_module sqlite3_api->create_module
|
||||
#define sqlite3_create_module_v2 sqlite3_api->create_module_v2
|
||||
#define sqlite3_data_count sqlite3_api->data_count
|
||||
#define sqlite3_db_handle sqlite3_api->db_handle
|
||||
#define sqlite3_declare_vtab sqlite3_api->declare_vtab
|
||||
#define sqlite3_enable_shared_cache sqlite3_api->enable_shared_cache
|
||||
#define sqlite3_errcode sqlite3_api->errcode
|
||||
#define sqlite3_errmsg sqlite3_api->errmsg
|
||||
#define sqlite3_errmsg16 sqlite3_api->errmsg16
|
||||
#define sqlite3_exec sqlite3_api->exec
|
||||
#ifndef SQLITE_OMIT_DEPRECATED
|
||||
#define sqlite3_expired sqlite3_api->expired
|
||||
#endif
|
||||
#define sqlite3_finalize sqlite3_api->finalize
|
||||
#define sqlite3_free sqlite3_api->free
|
||||
#define sqlite3_free_table sqlite3_api->free_table
|
||||
#define sqlite3_get_autocommit sqlite3_api->get_autocommit
|
||||
#define sqlite3_get_auxdata sqlite3_api->get_auxdata
|
||||
#define sqlite3_get_table sqlite3_api->get_table
|
||||
#ifndef SQLITE_OMIT_DEPRECATED
|
||||
#define sqlite3_global_recover sqlite3_api->global_recover
|
||||
#endif
|
||||
#define sqlite3_interrupt sqlite3_api->interruptx
|
||||
#define sqlite3_last_insert_rowid sqlite3_api->last_insert_rowid
|
||||
#define sqlite3_libversion sqlite3_api->libversion
|
||||
#define sqlite3_libversion_number sqlite3_api->libversion_number
|
||||
#define sqlite3_malloc sqlite3_api->malloc
|
||||
#define sqlite3_mprintf sqlite3_api->mprintf
|
||||
#define sqlite3_open sqlite3_api->open
|
||||
#define sqlite3_open16 sqlite3_api->open16
|
||||
#define sqlite3_prepare sqlite3_api->prepare
|
||||
#define sqlite3_prepare16 sqlite3_api->prepare16
|
||||
#define sqlite3_prepare_v2 sqlite3_api->prepare_v2
|
||||
#define sqlite3_prepare16_v2 sqlite3_api->prepare16_v2
|
||||
#define sqlite3_profile sqlite3_api->profile
|
||||
#define sqlite3_progress_handler sqlite3_api->progress_handler
|
||||
#define sqlite3_realloc sqlite3_api->realloc
|
||||
#define sqlite3_reset sqlite3_api->reset
|
||||
#define sqlite3_result_blob sqlite3_api->result_blob
|
||||
#define sqlite3_result_double sqlite3_api->result_double
|
||||
#define sqlite3_result_error sqlite3_api->result_error
|
||||
#define sqlite3_result_error16 sqlite3_api->result_error16
|
||||
#define sqlite3_result_int sqlite3_api->result_int
|
||||
#define sqlite3_result_int64 sqlite3_api->result_int64
|
||||
#define sqlite3_result_null sqlite3_api->result_null
|
||||
#define sqlite3_result_text sqlite3_api->result_text
|
||||
#define sqlite3_result_text16 sqlite3_api->result_text16
|
||||
#define sqlite3_result_text16be sqlite3_api->result_text16be
|
||||
#define sqlite3_result_text16le sqlite3_api->result_text16le
|
||||
#define sqlite3_result_value sqlite3_api->result_value
|
||||
#define sqlite3_rollback_hook sqlite3_api->rollback_hook
|
||||
#define sqlite3_set_authorizer sqlite3_api->set_authorizer
|
||||
#define sqlite3_set_auxdata sqlite3_api->set_auxdata
|
||||
#define sqlite3_snprintf sqlite3_api->xsnprintf
|
||||
#define sqlite3_step sqlite3_api->step
|
||||
#define sqlite3_table_column_metadata sqlite3_api->table_column_metadata
|
||||
#define sqlite3_thread_cleanup sqlite3_api->thread_cleanup
|
||||
#define sqlite3_total_changes sqlite3_api->total_changes
|
||||
#define sqlite3_trace sqlite3_api->trace
|
||||
#ifndef SQLITE_OMIT_DEPRECATED
|
||||
#define sqlite3_transfer_bindings sqlite3_api->transfer_bindings
|
||||
#endif
|
||||
#define sqlite3_update_hook sqlite3_api->update_hook
|
||||
#define sqlite3_user_data sqlite3_api->user_data
|
||||
#define sqlite3_value_blob sqlite3_api->value_blob
|
||||
#define sqlite3_value_bytes sqlite3_api->value_bytes
|
||||
#define sqlite3_value_bytes16 sqlite3_api->value_bytes16
|
||||
#define sqlite3_value_double sqlite3_api->value_double
|
||||
#define sqlite3_value_int sqlite3_api->value_int
|
||||
#define sqlite3_value_int64 sqlite3_api->value_int64
|
||||
#define sqlite3_value_numeric_type sqlite3_api->value_numeric_type
|
||||
#define sqlite3_value_text sqlite3_api->value_text
|
||||
#define sqlite3_value_text16 sqlite3_api->value_text16
|
||||
#define sqlite3_value_text16be sqlite3_api->value_text16be
|
||||
#define sqlite3_value_text16le sqlite3_api->value_text16le
|
||||
#define sqlite3_value_type sqlite3_api->value_type
|
||||
#define sqlite3_vmprintf sqlite3_api->vmprintf
|
||||
#define sqlite3_vsnprintf sqlite3_api->xvsnprintf
|
||||
#define sqlite3_overload_function sqlite3_api->overload_function
|
||||
#define sqlite3_prepare_v2 sqlite3_api->prepare_v2
|
||||
#define sqlite3_prepare16_v2 sqlite3_api->prepare16_v2
|
||||
#define sqlite3_clear_bindings sqlite3_api->clear_bindings
|
||||
#define sqlite3_bind_zeroblob sqlite3_api->bind_zeroblob
|
||||
#define sqlite3_blob_bytes sqlite3_api->blob_bytes
|
||||
#define sqlite3_blob_close sqlite3_api->blob_close
|
||||
#define sqlite3_blob_open sqlite3_api->blob_open
|
||||
#define sqlite3_blob_read sqlite3_api->blob_read
|
||||
#define sqlite3_blob_write sqlite3_api->blob_write
|
||||
#define sqlite3_create_collation_v2 sqlite3_api->create_collation_v2
|
||||
#define sqlite3_file_control sqlite3_api->file_control
|
||||
#define sqlite3_memory_highwater sqlite3_api->memory_highwater
|
||||
#define sqlite3_memory_used sqlite3_api->memory_used
|
||||
#define sqlite3_mutex_alloc sqlite3_api->mutex_alloc
|
||||
#define sqlite3_mutex_enter sqlite3_api->mutex_enter
|
||||
#define sqlite3_mutex_free sqlite3_api->mutex_free
|
||||
#define sqlite3_mutex_leave sqlite3_api->mutex_leave
|
||||
#define sqlite3_mutex_try sqlite3_api->mutex_try
|
||||
#define sqlite3_open_v2 sqlite3_api->open_v2
|
||||
#define sqlite3_release_memory sqlite3_api->release_memory
|
||||
#define sqlite3_result_error_nomem sqlite3_api->result_error_nomem
|
||||
#define sqlite3_result_error_toobig sqlite3_api->result_error_toobig
|
||||
#define sqlite3_sleep sqlite3_api->sleep
|
||||
#define sqlite3_soft_heap_limit sqlite3_api->soft_heap_limit
|
||||
#define sqlite3_vfs_find sqlite3_api->vfs_find
|
||||
#define sqlite3_vfs_register sqlite3_api->vfs_register
|
||||
#define sqlite3_vfs_unregister sqlite3_api->vfs_unregister
|
||||
#define sqlite3_threadsafe sqlite3_api->xthreadsafe
|
||||
#define sqlite3_result_zeroblob sqlite3_api->result_zeroblob
|
||||
#define sqlite3_result_error_code sqlite3_api->result_error_code
|
||||
#define sqlite3_test_control sqlite3_api->test_control
|
||||
#define sqlite3_randomness sqlite3_api->randomness
|
||||
#define sqlite3_context_db_handle sqlite3_api->context_db_handle
|
||||
#define sqlite3_extended_result_codes sqlite3_api->extended_result_codes
|
||||
#define sqlite3_limit sqlite3_api->limit
|
||||
#define sqlite3_next_stmt sqlite3_api->next_stmt
|
||||
#define sqlite3_sql sqlite3_api->sql
|
||||
#define sqlite3_status sqlite3_api->status
|
||||
#define sqlite3_backup_finish sqlite3_api->backup_finish
|
||||
#define sqlite3_backup_init sqlite3_api->backup_init
|
||||
#define sqlite3_backup_pagecount sqlite3_api->backup_pagecount
|
||||
#define sqlite3_backup_remaining sqlite3_api->backup_remaining
|
||||
#define sqlite3_backup_step sqlite3_api->backup_step
|
||||
#define sqlite3_compileoption_get sqlite3_api->compileoption_get
|
||||
#define sqlite3_compileoption_used sqlite3_api->compileoption_used
|
||||
#define sqlite3_create_function_v2 sqlite3_api->create_function_v2
|
||||
#define sqlite3_db_config sqlite3_api->db_config
|
||||
#define sqlite3_db_mutex sqlite3_api->db_mutex
|
||||
#define sqlite3_db_status sqlite3_api->db_status
|
||||
#define sqlite3_extended_errcode sqlite3_api->extended_errcode
|
||||
#define sqlite3_log sqlite3_api->log
|
||||
#define sqlite3_soft_heap_limit64 sqlite3_api->soft_heap_limit64
|
||||
#define sqlite3_sourceid sqlite3_api->sourceid
|
||||
#define sqlite3_stmt_status sqlite3_api->stmt_status
|
||||
#define sqlite3_strnicmp sqlite3_api->strnicmp
|
||||
#define sqlite3_unlock_notify sqlite3_api->unlock_notify
|
||||
#define sqlite3_wal_autocheckpoint sqlite3_api->wal_autocheckpoint
|
||||
#define sqlite3_wal_checkpoint sqlite3_api->wal_checkpoint
|
||||
#define sqlite3_wal_hook sqlite3_api->wal_hook
|
||||
#define sqlite3_blob_reopen sqlite3_api->blob_reopen
|
||||
#define sqlite3_vtab_config sqlite3_api->vtab_config
|
||||
#define sqlite3_vtab_on_conflict sqlite3_api->vtab_on_conflict
|
||||
/* Version 3.7.16 and later */
|
||||
#define sqlite3_close_v2 sqlite3_api->close_v2
|
||||
#define sqlite3_db_filename sqlite3_api->db_filename
|
||||
#define sqlite3_db_readonly sqlite3_api->db_readonly
|
||||
#define sqlite3_db_release_memory sqlite3_api->db_release_memory
|
||||
#define sqlite3_errstr sqlite3_api->errstr
|
||||
#define sqlite3_stmt_busy sqlite3_api->stmt_busy
|
||||
#define sqlite3_stmt_readonly sqlite3_api->stmt_readonly
|
||||
#define sqlite3_stricmp sqlite3_api->stricmp
|
||||
#define sqlite3_uri_boolean sqlite3_api->uri_boolean
|
||||
#define sqlite3_uri_int64 sqlite3_api->uri_int64
|
||||
#define sqlite3_uri_parameter sqlite3_api->uri_parameter
|
||||
#define sqlite3_uri_vsnprintf sqlite3_api->xvsnprintf
|
||||
#define sqlite3_wal_checkpoint_v2 sqlite3_api->wal_checkpoint_v2
|
||||
/* Version 3.8.7 and later */
|
||||
#define sqlite3_auto_extension sqlite3_api->auto_extension
|
||||
#define sqlite3_bind_blob64 sqlite3_api->bind_blob64
|
||||
#define sqlite3_bind_text64 sqlite3_api->bind_text64
|
||||
#define sqlite3_cancel_auto_extension sqlite3_api->cancel_auto_extension
|
||||
#define sqlite3_load_extension sqlite3_api->load_extension
|
||||
#define sqlite3_malloc64 sqlite3_api->malloc64
|
||||
#define sqlite3_msize sqlite3_api->msize
|
||||
#define sqlite3_realloc64 sqlite3_api->realloc64
|
||||
#define sqlite3_reset_auto_extension sqlite3_api->reset_auto_extension
|
||||
#define sqlite3_result_blob64 sqlite3_api->result_blob64
|
||||
#define sqlite3_result_text64 sqlite3_api->result_text64
|
||||
#define sqlite3_strglob sqlite3_api->strglob
|
||||
/* Version 3.8.11 and later */
|
||||
#define sqlite3_value_dup sqlite3_api->value_dup
|
||||
#define sqlite3_value_free sqlite3_api->value_free
|
||||
#define sqlite3_result_zeroblob64 sqlite3_api->result_zeroblob64
|
||||
#define sqlite3_bind_zeroblob64 sqlite3_api->bind_zeroblob64
|
||||
/* Version 3.9.0 and later */
|
||||
#define sqlite3_value_subtype sqlite3_api->value_subtype
|
||||
#define sqlite3_result_subtype sqlite3_api->result_subtype
|
||||
/* Version 3.10.0 and later */
|
||||
#define sqlite3_status64 sqlite3_api->status64
|
||||
#define sqlite3_strlike sqlite3_api->strlike
|
||||
#define sqlite3_db_cacheflush sqlite3_api->db_cacheflush
|
||||
/* Version 3.12.0 and later */
|
||||
#define sqlite3_system_errno sqlite3_api->system_errno
|
||||
/* Version 3.14.0 and later */
|
||||
#define sqlite3_trace_v2 sqlite3_api->trace_v2
|
||||
#define sqlite3_expanded_sql sqlite3_api->expanded_sql
|
||||
/* Version 3.18.0 and later */
|
||||
#define sqlite3_set_last_insert_rowid sqlite3_api->set_last_insert_rowid
|
||||
/* Version 3.20.0 and later */
|
||||
#define sqlite3_prepare_v3 sqlite3_api->prepare_v3
|
||||
#define sqlite3_prepare16_v3 sqlite3_api->prepare16_v3
|
||||
#define sqlite3_bind_pointer sqlite3_api->bind_pointer
|
||||
#define sqlite3_result_pointer sqlite3_api->result_pointer
|
||||
#define sqlite3_value_pointer sqlite3_api->value_pointer
|
||||
/* Version 3.22.0 and later */
|
||||
#define sqlite3_vtab_nochange sqlite3_api->vtab_nochange
|
||||
#define sqlite3_value_nochange sqlite3_api->value_nochange
|
||||
#define sqlite3_vtab_collation sqlite3_api->vtab_collation
|
||||
/* Version 3.24.0 and later */
|
||||
#define sqlite3_keyword_count sqlite3_api->keyword_count
|
||||
#define sqlite3_keyword_name sqlite3_api->keyword_name
|
||||
#define sqlite3_keyword_check sqlite3_api->keyword_check
|
||||
#define sqlite3_str_new sqlite3_api->str_new
|
||||
#define sqlite3_str_finish sqlite3_api->str_finish
|
||||
#define sqlite3_str_appendf sqlite3_api->str_appendf
|
||||
#define sqlite3_str_vappendf sqlite3_api->str_vappendf
|
||||
#define sqlite3_str_append sqlite3_api->str_append
|
||||
#define sqlite3_str_appendall sqlite3_api->str_appendall
|
||||
#define sqlite3_str_appendchar sqlite3_api->str_appendchar
|
||||
#define sqlite3_str_reset sqlite3_api->str_reset
|
||||
#define sqlite3_str_errcode sqlite3_api->str_errcode
|
||||
#define sqlite3_str_length sqlite3_api->str_length
|
||||
#define sqlite3_str_value sqlite3_api->str_value
|
||||
/* Version 3.25.0 and later */
|
||||
#define sqlite3_create_window_function sqlite3_api->create_window_function
|
||||
/* Version 3.26.0 and later */
|
||||
#define sqlite3_normalized_sql sqlite3_api->normalized_sql
|
||||
/* Version 3.28.0 and later */
|
||||
#define sqlite3_stmt_isexplain sqlite3_api->stmt_isexplain
|
||||
#define sqlite3_value_frombind sqlite3_api->value_frombind
|
||||
/* Version 3.30.0 and later */
|
||||
#define sqlite3_drop_modules sqlite3_api->drop_modules
|
||||
/* Version 3.31.0 and later */
|
||||
#define sqlite3_hard_heap_limit64 sqlite3_api->hard_heap_limit64
|
||||
#define sqlite3_uri_key sqlite3_api->uri_key
|
||||
#define sqlite3_filename_database sqlite3_api->filename_database
|
||||
#define sqlite3_filename_journal sqlite3_api->filename_journal
|
||||
#define sqlite3_filename_wal sqlite3_api->filename_wal
|
||||
/* Version 3.32.0 and later */
|
||||
#define sqlite3_create_filename sqlite3_api->create_filename
|
||||
#define sqlite3_free_filename sqlite3_api->free_filename
|
||||
#define sqlite3_database_file_object sqlite3_api->database_file_object
|
||||
/* Version 3.34.0 and later */
|
||||
#define sqlite3_txn_state sqlite3_api->txn_state
|
||||
/* Version 3.36.1 and later */
|
||||
#define sqlite3_changes64 sqlite3_api->changes64
|
||||
#define sqlite3_total_changes64 sqlite3_api->total_changes64
|
||||
/* Version 3.37.0 and later */
|
||||
#define sqlite3_autovacuum_pages sqlite3_api->autovacuum_pages
|
||||
/* Version 3.38.0 and later */
|
||||
#define sqlite3_error_offset sqlite3_api->error_offset
|
||||
#define sqlite3_vtab_rhs_value sqlite3_api->vtab_rhs_value
|
||||
#define sqlite3_vtab_distinct sqlite3_api->vtab_distinct
|
||||
#define sqlite3_vtab_in sqlite3_api->vtab_in
|
||||
#define sqlite3_vtab_in_first sqlite3_api->vtab_in_first
|
||||
#define sqlite3_vtab_in_next sqlite3_api->vtab_in_next
|
||||
/* Version 3.39.0 and later */
|
||||
#ifndef SQLITE_OMIT_DESERIALIZE
|
||||
#define sqlite3_deserialize sqlite3_api->deserialize
|
||||
#define sqlite3_serialize sqlite3_api->serialize
|
||||
#endif
|
||||
#define sqlite3_db_name sqlite3_api->db_name
|
||||
/* Version 3.40.0 and later */
|
||||
#define sqlite3_value_encoding sqlite3_api->value_encoding
|
||||
/* Version 3.41.0 and later */
|
||||
#define sqlite3_is_interrupted sqlite3_api->is_interrupted
|
||||
/* Version 3.43.0 and later */
|
||||
#define sqlite3_stmt_explain sqlite3_api->stmt_explain
|
||||
/* Version 3.44.0 and later */
|
||||
#define sqlite3_get_clientdata sqlite3_api->get_clientdata
|
||||
#define sqlite3_set_clientdata sqlite3_api->set_clientdata
|
||||
#endif /* !defined(SQLITE_CORE) && !defined(SQLITE_OMIT_LOAD_EXTENSION) */
|
||||
|
||||
#if !defined(SQLITE_CORE) && !defined(SQLITE_OMIT_LOAD_EXTENSION)
|
||||
/* This case when the file really is being compiled as a loadable
|
||||
** extension */
|
||||
# define SQLITE_EXTENSION_INIT1 const sqlite3_api_routines *sqlite3_api=0;
|
||||
# define SQLITE_EXTENSION_INIT2(v) sqlite3_api=v;
|
||||
# define SQLITE_EXTENSION_INIT3 \
|
||||
extern const sqlite3_api_routines *sqlite3_api;
|
||||
#else
|
||||
/* This case when the file is being statically linked into the
|
||||
** application */
|
||||
# define SQLITE_EXTENSION_INIT1 /*no-op*/
|
||||
# define SQLITE_EXTENSION_INIT2(v) (void)v; /* unused parameter */
|
||||
# define SQLITE_EXTENSION_INIT3 /*no-op*/
|
||||
#endif
|
||||
|
||||
#endif /* SQLITE3EXT_H */
|
||||
124
docs/SUCCESSION.md
Normal file
124
docs/SUCCESSION.md
Normal file
@ -0,0 +1,124 @@
|
||||
# Document de Succession - Refactoring AISSIA
|
||||
|
||||
## Contexte
|
||||
|
||||
Refactoring du code AISSIA pour le rendre conforme aux principes GroveEngine (audit initial : 33% conforme, 2/6 modules).
|
||||
|
||||
## Ce qui a été fait
|
||||
|
||||
### 1. Architecture Services (nouveau)
|
||||
|
||||
Créé 4 services dans `src/services/` qui gèrent l'infrastructure :
|
||||
|
||||
| Service | Fichiers | Responsabilité |
|
||||
|---------|----------|----------------|
|
||||
| LLMService | `LLMService.hpp/.cpp` | HTTP vers Claude/OpenAI API |
|
||||
| StorageService | `StorageService.hpp/.cpp` | SQLite persistence |
|
||||
| PlatformService | `PlatformService.hpp/.cpp` | Win32 window tracking |
|
||||
| VoiceService | `VoiceService.hpp/.cpp` | TTS/STT engines |
|
||||
|
||||
Interface commune : `IService.hpp`
|
||||
|
||||
### 2. Modules refactorisés (logique pure)
|
||||
|
||||
| Module | Avant | Après | Changements |
|
||||
|--------|-------|-------|-------------|
|
||||
| AIModule | 306 lignes, HTTP direct | ~170 lignes | Publie `llm:request`, écoute `llm:response` |
|
||||
| StorageModule | 273 lignes, sqlite3 | ~130 lignes | Publie `storage:save_*` |
|
||||
| MonitoringModule | 222 lignes, Win32 | ~190 lignes | Écoute `platform:window_*` |
|
||||
| VoiceModule | 209 lignes, COM/TTS | ~155 lignes | Publie `voice:speak` |
|
||||
| SchedulerModule | - | - | Topics corrigés (`:` au lieu de `/`) |
|
||||
| NotificationModule | - | - | Déjà conforme |
|
||||
|
||||
### 3. main.cpp réécrit
|
||||
|
||||
- Initialise les 4 services avant les modules
|
||||
- `MessageRouter` gère le routage IIO entre services et modules
|
||||
- Services process() avant modules dans la boucle principale
|
||||
|
||||
### 4. CMakeLists.txt
|
||||
|
||||
- SQLite bundled dans `deps/sqlite/` (amalgamation)
|
||||
- OpenSSL rendu optionnel
|
||||
- Nouvelles targets : `AissiaServices`, `AissiaLLM`, `AissiaPlatform`, `AissiaAudio`
|
||||
|
||||
## État du build
|
||||
|
||||
### Problème actuel
|
||||
```
|
||||
CMake Error: Cannot determine link language for target "sqlite3"
|
||||
```
|
||||
|
||||
### Fix appliqué (dans CMakeLists.txt)
|
||||
```cmake
|
||||
enable_language(C) # SQLite is C code
|
||||
add_library(sqlite3 STATIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/deps/sqlite/sqlite3.c
|
||||
)
|
||||
set_target_properties(sqlite3 PROPERTIES LINKER_LANGUAGE C)
|
||||
```
|
||||
|
||||
### Pour terminer le build
|
||||
```bash
|
||||
cd C:\Users\alexi\Documents\projects\aissia
|
||||
rm -rf build # Clean start recommandé
|
||||
cmake -G "MinGW Makefiles" -B build
|
||||
cmake --build build -j4
|
||||
```
|
||||
|
||||
## Fichiers créés/modifiés
|
||||
|
||||
### Nouveaux fichiers
|
||||
- `src/services/IService.hpp`
|
||||
- `src/services/LLMService.hpp` / `.cpp`
|
||||
- `src/services/StorageService.hpp` / `.cpp`
|
||||
- `src/services/PlatformService.hpp` / `.cpp`
|
||||
- `src/services/VoiceService.hpp` / `.cpp`
|
||||
- `deps/sqlite/sqlite3.c` (téléchargé)
|
||||
- `deps/sqlite/sqlite3.h`
|
||||
|
||||
### Fichiers modifiés
|
||||
- `src/main.cpp` - réécrit complètement
|
||||
- `src/modules/AIModule.h` / `.cpp`
|
||||
- `src/modules/StorageModule.h` / `.cpp`
|
||||
- `src/modules/MonitoringModule.h` / `.cpp`
|
||||
- `src/modules/VoiceModule.h` / `.cpp`
|
||||
- `src/modules/SchedulerModule.h` / `.cpp`
|
||||
- `CMakeLists.txt`
|
||||
- `external/GroveEngine/CMakeLists.txt` (OpenSSL optionnel)
|
||||
|
||||
## Communication Inter-Modules (Topics)
|
||||
|
||||
Format : `module:event` (utiliser `:` pas `/`)
|
||||
|
||||
### LLM
|
||||
- `llm:request` - Module -> Service
|
||||
- `llm:response` - Service -> Module
|
||||
- `llm:error` - Service -> Module
|
||||
|
||||
### Storage
|
||||
- `storage:save_session` - Module -> Service
|
||||
- `storage:save_app_usage` - Module -> Service
|
||||
- `storage:session_saved` - Service -> Module
|
||||
- `storage:ready` - Service -> Module
|
||||
|
||||
### Platform
|
||||
- `platform:window_info` - Service -> Module
|
||||
- `platform:window_changed` - Service -> Module
|
||||
- `platform:idle_detected` - Service -> Module
|
||||
|
||||
### Voice
|
||||
- `voice:speak` - Module -> Service
|
||||
- `voice:speaking_started` - Service -> Module
|
||||
|
||||
### Scheduler (existant)
|
||||
- `scheduler:hyperfocus_alert`
|
||||
- `scheduler:break_reminder`
|
||||
- `scheduler:focus_session_started`
|
||||
|
||||
## Prochaines étapes
|
||||
|
||||
1. Terminer le build (fix SQLite C language)
|
||||
2. Tester compilation de tous les modules
|
||||
3. Vérifier hot-reload fonctionne
|
||||
4. Tests d'intégration services <-> modules
|
||||
1
external/GroveEngine
vendored
1
external/GroveEngine
vendored
@ -1 +0,0 @@
|
||||
/mnt/e/Users/Alexis Trouvé/Documents/Projets/GroveEngine
|
||||
217
src/main.cpp
217
src/main.cpp
@ -2,6 +2,11 @@
|
||||
#include <grove/JsonDataNode.h>
|
||||
#include <grove/IOFactory.h>
|
||||
|
||||
#include "services/LLMService.hpp"
|
||||
#include "services/StorageService.hpp"
|
||||
#include "services/PlatformService.hpp"
|
||||
#include "services/VoiceService.hpp"
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <spdlog/sinks/stdout_color_sinks.h>
|
||||
|
||||
@ -18,7 +23,7 @@ namespace fs = std::filesystem;
|
||||
static volatile bool g_running = true;
|
||||
|
||||
void signalHandler(int signal) {
|
||||
spdlog::info("Signal {} reçu, arrêt en cours...", signal);
|
||||
spdlog::info("Signal {} recu, arret en cours...", signal);
|
||||
g_running = false;
|
||||
}
|
||||
|
||||
@ -60,10 +65,10 @@ std::unique_ptr<grove::JsonDataNode> loadConfig(const std::string& path) {
|
||||
nlohmann::json j;
|
||||
file >> j;
|
||||
auto config = std::make_unique<grove::JsonDataNode>("config", j);
|
||||
spdlog::info("Config chargée: {}", path);
|
||||
spdlog::info("Config chargee: {}", path);
|
||||
return config;
|
||||
} else {
|
||||
spdlog::warn("Config non trouvée: {}, utilisation des défauts", path);
|
||||
spdlog::warn("Config non trouvee: {}, utilisation des defauts", path);
|
||||
return std::make_unique<grove::JsonDataNode>("config");
|
||||
}
|
||||
}
|
||||
@ -78,6 +83,63 @@ struct ModuleEntry {
|
||||
grove::IModule* module = nullptr;
|
||||
};
|
||||
|
||||
// Message router between modules and services
|
||||
class MessageRouter {
|
||||
public:
|
||||
void addModuleIO(const std::string& name, grove::IIO* io) {
|
||||
m_moduleIOs[name] = io;
|
||||
}
|
||||
|
||||
void addServiceIO(const std::string& name, grove::IIO* io) {
|
||||
m_serviceIOs[name] = io;
|
||||
}
|
||||
|
||||
void routeMessages() {
|
||||
// Collect all messages from modules and services
|
||||
std::vector<grove::Message> messages;
|
||||
|
||||
for (auto& [name, io] : m_moduleIOs) {
|
||||
if (!io) continue;
|
||||
while (io->hasMessages() > 0) {
|
||||
messages.push_back(io->pullMessage());
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& [name, io] : m_serviceIOs) {
|
||||
if (!io) continue;
|
||||
while (io->hasMessages() > 0) {
|
||||
messages.push_back(io->pullMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Route messages to appropriate destinations
|
||||
for (auto& msg : messages) {
|
||||
// Determine destination based on topic prefix
|
||||
std::string prefix = msg.topic.substr(0, msg.topic.find(':'));
|
||||
|
||||
// Route to services
|
||||
if (prefix == "llm" || prefix == "storage" || prefix == "platform" || prefix == "voice") {
|
||||
for (auto& [name, io] : m_serviceIOs) {
|
||||
if (io && msg.data) {
|
||||
io->publish(msg.topic, msg.data->clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Route to modules (broadcast)
|
||||
for (auto& [name, io] : m_moduleIOs) {
|
||||
if (io && msg.data) {
|
||||
io->publish(msg.topic, msg.data->clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
std::map<std::string, grove::IIO*> m_moduleIOs;
|
||||
std::map<std::string, grove::IIO*> m_serviceIOs;
|
||||
};
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
// Setup logging
|
||||
auto console = spdlog::stdout_color_mt("Aissia");
|
||||
@ -88,6 +150,7 @@ int main(int argc, char* argv[]) {
|
||||
spdlog::info("========================================");
|
||||
spdlog::info(" AISSIA - Assistant Personnel IA");
|
||||
spdlog::info(" Powered by GroveEngine");
|
||||
spdlog::info(" Architecture: Services + Hot-Reload Modules");
|
||||
spdlog::info("========================================");
|
||||
|
||||
// Signal handling
|
||||
@ -99,19 +162,105 @@ int main(int argc, char* argv[]) {
|
||||
const std::string configDir = "./config/";
|
||||
|
||||
// =========================================================================
|
||||
// Module Management
|
||||
// Infrastructure Services (non hot-reloadable)
|
||||
// =========================================================================
|
||||
spdlog::info("Initialisation des services infrastructure...");
|
||||
|
||||
// Create IIO for services
|
||||
auto llmIO = grove::IOFactory::create("intra", "LLMService");
|
||||
auto storageIO = grove::IOFactory::create("intra", "StorageService");
|
||||
auto platformIO = grove::IOFactory::create("intra", "PlatformService");
|
||||
auto voiceIO = grove::IOFactory::create("intra", "VoiceService");
|
||||
|
||||
// LLM Service
|
||||
aissia::LLMService llmService;
|
||||
llmService.initialize(llmIO.get());
|
||||
llmService.loadConfig(configDir + "ai.json");
|
||||
|
||||
// Register default tools
|
||||
llmService.registerTool(
|
||||
"get_current_time",
|
||||
"Obtient l'heure actuelle",
|
||||
{{"type", "object"}, {"properties", nlohmann::json::object()}},
|
||||
[](const nlohmann::json& input) -> nlohmann::json {
|
||||
std::time_t now = std::time(nullptr);
|
||||
std::tm* tm = std::localtime(&now);
|
||||
char buffer[64];
|
||||
std::strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", tm);
|
||||
return {{"time", buffer}};
|
||||
}
|
||||
);
|
||||
|
||||
// Storage Service
|
||||
aissia::StorageService storageService;
|
||||
storageService.initialize(storageIO.get());
|
||||
{
|
||||
auto storageConfig = loadConfig(configDir + "storage.json");
|
||||
std::string dbPath = storageConfig->getString("database_path", "./data/aissia.db");
|
||||
std::string journalMode = storageConfig->getString("journal_mode", "WAL");
|
||||
int busyTimeout = storageConfig->getInt("busy_timeout_ms", 5000);
|
||||
storageService.openDatabase(dbPath, journalMode, busyTimeout);
|
||||
}
|
||||
|
||||
// Platform Service
|
||||
aissia::PlatformService platformService;
|
||||
platformService.initialize(platformIO.get());
|
||||
{
|
||||
auto monitorConfig = loadConfig(configDir + "monitoring.json");
|
||||
int pollInterval = monitorConfig->getInt("poll_interval_ms", 1000);
|
||||
int idleThreshold = monitorConfig->getInt("idle_threshold_seconds", 300);
|
||||
platformService.configure(pollInterval, idleThreshold);
|
||||
}
|
||||
|
||||
// Voice Service
|
||||
aissia::VoiceService voiceService;
|
||||
voiceService.initialize(voiceIO.get());
|
||||
{
|
||||
auto voiceConfig = loadConfig(configDir + "voice.json");
|
||||
auto* ttsNode = voiceConfig->getChildReadOnly("tts");
|
||||
if (ttsNode) {
|
||||
bool enabled = ttsNode->getBool("enabled", true);
|
||||
int rate = ttsNode->getInt("rate", 0);
|
||||
int volume = ttsNode->getInt("volume", 80);
|
||||
voiceService.configureTTS(enabled, rate, volume);
|
||||
}
|
||||
auto* sttNode = voiceConfig->getChildReadOnly("stt");
|
||||
if (sttNode) {
|
||||
bool enabled = sttNode->getBool("enabled", true);
|
||||
std::string language = sttNode->getString("language", "fr");
|
||||
std::string apiKeyEnv = sttNode->getString("api_key_env", "OPENAI_API_KEY");
|
||||
const char* apiKey = std::getenv(apiKeyEnv.c_str());
|
||||
voiceService.configureSTT(enabled, language, apiKey ? apiKey : "");
|
||||
}
|
||||
}
|
||||
|
||||
spdlog::info("Services initialises: LLM={}, Storage={}, Platform={}, Voice={}",
|
||||
llmService.isHealthy() ? "OK" : "FAIL",
|
||||
storageService.isHealthy() ? "OK" : "FAIL",
|
||||
platformService.isHealthy() ? "OK" : "FAIL",
|
||||
voiceService.isHealthy() ? "OK" : "FAIL");
|
||||
|
||||
// =========================================================================
|
||||
// Hot-Reloadable Modules
|
||||
// =========================================================================
|
||||
std::map<std::string, ModuleEntry> modules;
|
||||
FileWatcher watcher;
|
||||
MessageRouter router;
|
||||
|
||||
// Liste des modules à charger
|
||||
// Register service IOs with router
|
||||
router.addServiceIO("LLMService", llmIO.get());
|
||||
router.addServiceIO("StorageService", storageIO.get());
|
||||
router.addServiceIO("PlatformService", platformIO.get());
|
||||
router.addServiceIO("VoiceService", voiceIO.get());
|
||||
|
||||
// Liste des modules a charger (sans infrastructure)
|
||||
std::vector<std::pair<std::string, std::string>> moduleList = {
|
||||
{"StorageModule", "storage.json"}, // Doit être chargé en premier (persistence)
|
||||
{"SchedulerModule", "scheduler.json"},
|
||||
{"NotificationModule", "notification.json"},
|
||||
{"MonitoringModule", "monitoring.json"},
|
||||
{"AIModule", "ai.json"},
|
||||
{"VoiceModule", "voice.json"},
|
||||
{"StorageModule", "storage.json"},
|
||||
};
|
||||
|
||||
// Charger les modules
|
||||
@ -119,7 +268,7 @@ int main(int argc, char* argv[]) {
|
||||
std::string modulePath = modulesDir + "lib" + moduleName + ".so";
|
||||
|
||||
if (!fs::exists(modulePath)) {
|
||||
spdlog::warn("{} non trouvé: {}", moduleName, modulePath);
|
||||
spdlog::warn("{} non trouve: {}", moduleName, modulePath);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -132,7 +281,7 @@ int main(int argc, char* argv[]) {
|
||||
|
||||
auto modulePtr = entry.loader->load(modulePath, moduleName);
|
||||
if (!modulePtr) {
|
||||
spdlog::error("Échec du chargement: {}", moduleName);
|
||||
spdlog::error("Echec du chargement: {}", moduleName);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -143,24 +292,27 @@ int main(int argc, char* argv[]) {
|
||||
entry.module = modulePtr.release();
|
||||
watcher.watch(modulePath);
|
||||
|
||||
spdlog::info("{} chargé et configuré", moduleName);
|
||||
// Register with router
|
||||
router.addModuleIO(moduleName, entry.io.get());
|
||||
|
||||
spdlog::info("{} charge et configure", moduleName);
|
||||
modules[moduleName] = std::move(entry);
|
||||
}
|
||||
|
||||
if (modules.empty()) {
|
||||
spdlog::error("Aucun module chargé! Build les modules: cmake --build build --target modules");
|
||||
spdlog::error("Aucun module charge! Build les modules: cmake --build build --target modules");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Main Loop
|
||||
// =========================================================================
|
||||
spdlog::info("Démarrage de la boucle principale (Ctrl+C pour quitter)");
|
||||
spdlog::info("Demarrage 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
|
||||
const auto targetFrameTime = std::chrono::milliseconds(100); // 10 Hz
|
||||
|
||||
grove::JsonDataNode frameInput("frame");
|
||||
uint64_t frameCount = 0;
|
||||
@ -184,7 +336,7 @@ int main(int argc, char* argv[]) {
|
||||
if (frameCount % 10 == 0) {
|
||||
for (auto& [name, entry] : modules) {
|
||||
if (watcher.hasChanged(entry.path)) {
|
||||
spdlog::info("Modification détectée: {}, hot-reload...", name);
|
||||
spdlog::info("Modification detectee: {}, hot-reload...", name);
|
||||
|
||||
// Get state before reload
|
||||
std::unique_ptr<grove::IDataNode> state;
|
||||
@ -203,14 +355,22 @@ int main(int argc, char* argv[]) {
|
||||
}
|
||||
|
||||
entry.module = reloaded.release();
|
||||
spdlog::info("{} rechargé avec succès!", name);
|
||||
spdlog::info("{} recharge avec succes!", name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Process all modules
|
||||
// Process Services (non hot-reloadable infrastructure)
|
||||
// =====================================================================
|
||||
llmService.process();
|
||||
storageService.process();
|
||||
platformService.process();
|
||||
voiceService.process();
|
||||
|
||||
// =====================================================================
|
||||
// Process Modules (hot-reloadable)
|
||||
// =====================================================================
|
||||
for (auto& [name, entry] : modules) {
|
||||
if (entry.module) {
|
||||
@ -218,13 +378,10 @@ int main(int argc, char* argv[]) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
// =====================================================================
|
||||
// Route messages between modules and services
|
||||
// =====================================================================
|
||||
router.routeMessages();
|
||||
|
||||
// =====================================================================
|
||||
// Frame timing
|
||||
@ -242,7 +399,7 @@ int main(int argc, char* argv[]) {
|
||||
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",
|
||||
spdlog::debug("Session: {}m{}s, {} modules actifs, 4 services",
|
||||
minutes, seconds, modules.size());
|
||||
}
|
||||
}
|
||||
@ -250,14 +407,22 @@ int main(int argc, char* argv[]) {
|
||||
// =========================================================================
|
||||
// Shutdown
|
||||
// =========================================================================
|
||||
spdlog::info("Arrêt en cours...");
|
||||
spdlog::info("Arret en cours...");
|
||||
|
||||
// Shutdown modules first
|
||||
for (auto& [name, entry] : modules) {
|
||||
if (entry.module) {
|
||||
entry.module->shutdown();
|
||||
spdlog::info("{} arrêté", name);
|
||||
spdlog::info("{} arrete", name);
|
||||
}
|
||||
}
|
||||
spdlog::info("À bientôt!");
|
||||
|
||||
// Shutdown services
|
||||
voiceService.shutdown();
|
||||
platformService.shutdown();
|
||||
storageService.shutdown();
|
||||
llmService.shutdown();
|
||||
|
||||
spdlog::info("A bientot!");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
#include "AIModule.h"
|
||||
#include "../shared/llm/LLMProviderFactory.hpp"
|
||||
#include <grove/JsonDataNode.h>
|
||||
#include <fstream>
|
||||
|
||||
namespace aissia {
|
||||
|
||||
@ -11,7 +9,6 @@ AIModule::AIModule() {
|
||||
m_logger = spdlog::stdout_color_mt("AIModule");
|
||||
}
|
||||
m_config = std::make_unique<grove::JsonDataNode>("config");
|
||||
m_conversationHistory = nlohmann::json::array();
|
||||
}
|
||||
|
||||
void AIModule::setConfiguration(const grove::IDataNode& configNode,
|
||||
@ -20,30 +17,12 @@ void AIModule::setConfiguration(const grove::IDataNode& configNode,
|
||||
m_io = io;
|
||||
m_config = std::make_unique<grove::JsonDataNode>("config");
|
||||
|
||||
m_providerName = configNode.getString("provider", "claude");
|
||||
m_maxIterations = configNode.getInt("max_iterations", 10);
|
||||
m_systemPrompt = configNode.getString("system_prompt",
|
||||
"Tu es AISSIA, un assistant personnel specialise dans la gestion du temps et de l'attention. "
|
||||
"Tu aides l'utilisateur a rester productif tout en evitant l'hyperfocus excessif. "
|
||||
"Tu es bienveillant mais ferme quand necessaire pour encourager les pauses.");
|
||||
|
||||
// Load full config from file for LLM provider
|
||||
std::string configPath = configNode.getString("config_path", "./config/ai.json");
|
||||
try {
|
||||
std::ifstream file(configPath);
|
||||
if (file.is_open()) {
|
||||
nlohmann::json fullConfig;
|
||||
file >> fullConfig;
|
||||
m_provider = LLMProviderFactory::create(fullConfig);
|
||||
m_logger->info("AIModule configure: provider={}, model={}",
|
||||
m_providerName, m_provider->getModel());
|
||||
} else {
|
||||
m_logger->warn("Config file not found: {}, using defaults", configPath);
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
m_logger->error("Failed to initialize LLM provider: {}", e.what());
|
||||
}
|
||||
|
||||
// Subscribe to relevant topics
|
||||
if (m_io) {
|
||||
grove::SubscriptionConfig subConfig;
|
||||
@ -51,9 +30,11 @@ void AIModule::setConfiguration(const grove::IDataNode& configNode,
|
||||
m_io->subscribe("voice:transcription", subConfig);
|
||||
m_io->subscribe("scheduler:hyperfocus_alert", subConfig);
|
||||
m_io->subscribe("scheduler:break_reminder", subConfig);
|
||||
m_io->subscribe("llm:response", subConfig);
|
||||
m_io->subscribe("llm:error", subConfig);
|
||||
}
|
||||
|
||||
registerDefaultTools();
|
||||
m_logger->info("AIModule configure (v2 - sans infrastructure)");
|
||||
}
|
||||
|
||||
const grove::IDataNode& AIModule::getConfiguration() {
|
||||
@ -73,15 +54,21 @@ void AIModule::processMessages() {
|
||||
if (msg.topic == "ai:query" && msg.data) {
|
||||
std::string query = msg.data->getString("query", "");
|
||||
if (!query.empty()) {
|
||||
handleQuery(query);
|
||||
sendQuery(query);
|
||||
}
|
||||
}
|
||||
else if (msg.topic == "voice:transcription" && msg.data) {
|
||||
std::string text = msg.data->getString("text", "");
|
||||
if (!text.empty()) {
|
||||
handleQuery(text);
|
||||
sendQuery(text);
|
||||
}
|
||||
}
|
||||
else if (msg.topic == "llm:response" && msg.data) {
|
||||
handleLLMResponse(*msg.data);
|
||||
}
|
||||
else if (msg.topic == "llm:error" && msg.data) {
|
||||
handleLLMError(*msg.data);
|
||||
}
|
||||
else if (msg.topic == "scheduler:hyperfocus_alert" && msg.data) {
|
||||
handleHyperfocusAlert(*msg.data);
|
||||
}
|
||||
@ -91,29 +78,57 @@ void AIModule::processMessages() {
|
||||
}
|
||||
}
|
||||
|
||||
void AIModule::handleQuery(const std::string& query) {
|
||||
if (!m_provider) {
|
||||
publishError("LLM provider not initialized");
|
||||
return;
|
||||
}
|
||||
void AIModule::sendQuery(const std::string& query) {
|
||||
if (!m_io) return;
|
||||
|
||||
m_isProcessing = true;
|
||||
m_logger->info("Processing query: {}", query.substr(0, 50));
|
||||
m_awaitingResponse = true;
|
||||
m_logger->info("Sending query: {}", query.substr(0, 50));
|
||||
|
||||
try {
|
||||
auto result = agenticLoop(query);
|
||||
auto request = std::make_unique<grove::JsonDataNode>("request");
|
||||
request->setString("query", query);
|
||||
request->setString("systemPrompt", m_systemPrompt);
|
||||
request->setString("conversationId", m_currentConversationId);
|
||||
request->setInt("maxIterations", m_maxIterations);
|
||||
|
||||
if (result.contains("response")) {
|
||||
publishResponse(result["response"].get<std::string>());
|
||||
m_io->publish("llm:request", std::move(request));
|
||||
m_totalQueries++;
|
||||
} else if (result.contains("error")) {
|
||||
publishError(result["error"].get<std::string>());
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
publishError(e.what());
|
||||
}
|
||||
|
||||
m_isProcessing = false;
|
||||
void AIModule::handleLLMResponse(const grove::IDataNode& data) {
|
||||
std::string conversationId = data.getString("conversationId", "default");
|
||||
if (conversationId != m_currentConversationId) return;
|
||||
|
||||
m_awaitingResponse = false;
|
||||
std::string text = data.getString("text", "");
|
||||
int tokens = data.getInt("tokens", 0);
|
||||
int iterations = data.getInt("iterations", 1);
|
||||
|
||||
m_totalTokens += tokens;
|
||||
m_logger->info("Response received: {} chars, {} tokens, {} iterations",
|
||||
text.size(), tokens, iterations);
|
||||
|
||||
// Publish response for other modules (VoiceModule, NotificationModule)
|
||||
if (m_io) {
|
||||
auto event = std::make_unique<grove::JsonDataNode>("response");
|
||||
event->setString("text", text);
|
||||
event->setInt("tokens", tokens);
|
||||
m_io->publish("ai:response", std::move(event));
|
||||
}
|
||||
}
|
||||
|
||||
void AIModule::handleLLMError(const grove::IDataNode& data) {
|
||||
std::string conversationId = data.getString("conversationId", "default");
|
||||
if (conversationId != m_currentConversationId) return;
|
||||
|
||||
m_awaitingResponse = false;
|
||||
std::string message = data.getString("message", "Unknown error");
|
||||
m_logger->error("LLM error: {}", message);
|
||||
|
||||
if (m_io) {
|
||||
auto event = std::make_unique<grove::JsonDataNode>("error");
|
||||
event->setString("message", message);
|
||||
m_io->publish("ai:error", std::move(event));
|
||||
}
|
||||
}
|
||||
|
||||
void AIModule::handleHyperfocusAlert(const grove::IDataNode& data) {
|
||||
@ -123,7 +138,7 @@ void AIModule::handleHyperfocusAlert(const grove::IDataNode& data) {
|
||||
std::string query = "L'utilisateur est en hyperfocus depuis " + std::to_string(minutes) +
|
||||
" minutes sur '" + task + "'. Genere une intervention bienveillante mais ferme "
|
||||
"pour l'encourager a faire une pause.";
|
||||
handleQuery(query);
|
||||
sendQuery(query);
|
||||
}
|
||||
|
||||
void AIModule::handleBreakReminder(const grove::IDataNode& data) {
|
||||
@ -131,136 +146,24 @@ void AIModule::handleBreakReminder(const grove::IDataNode& data) {
|
||||
|
||||
std::string query = "Rappelle gentiment a l'utilisateur qu'il est temps de faire une pause de " +
|
||||
std::to_string(breakDuration) + " minutes. Sois encourageant.";
|
||||
handleQuery(query);
|
||||
sendQuery(query);
|
||||
}
|
||||
|
||||
nlohmann::json AIModule::agenticLoop(const std::string& userQuery) {
|
||||
nlohmann::json messages = nlohmann::json::array();
|
||||
messages.push_back({{"role", "user"}, {"content", userQuery}});
|
||||
void AIModule::publishSuggestion(const std::string& message, int duration) {
|
||||
if (!m_io) return;
|
||||
|
||||
nlohmann::json tools = m_toolRegistry.getToolDefinitions();
|
||||
|
||||
for (int iteration = 0; iteration < m_maxIterations; iteration++) {
|
||||
m_logger->debug("Agentic loop iteration {}", iteration + 1);
|
||||
|
||||
auto response = m_provider->chat(m_systemPrompt, messages, tools);
|
||||
|
||||
m_totalTokens += response.input_tokens + response.output_tokens;
|
||||
|
||||
if (response.is_end_turn) {
|
||||
// Add to conversation history
|
||||
m_conversationHistory.push_back({{"role", "user"}, {"content", userQuery}});
|
||||
m_conversationHistory.push_back({{"role", "assistant"}, {"content", response.text}});
|
||||
|
||||
return {
|
||||
{"response", response.text},
|
||||
{"iterations", iteration + 1},
|
||||
{"tokens", response.input_tokens + response.output_tokens}
|
||||
};
|
||||
}
|
||||
|
||||
// Execute tool calls
|
||||
if (!response.tool_calls.empty()) {
|
||||
std::vector<ToolResult> results;
|
||||
|
||||
for (const auto& call : response.tool_calls) {
|
||||
m_logger->debug("Executing tool: {}", call.name);
|
||||
nlohmann::json result = m_toolRegistry.execute(call.name, call.input);
|
||||
results.push_back({call.id, result.dump(), false});
|
||||
}
|
||||
|
||||
// Append assistant message and tool results
|
||||
m_provider->appendAssistantMessage(messages, response);
|
||||
auto toolResultsMsg = m_provider->formatToolResults(results);
|
||||
|
||||
// Handle different provider formats
|
||||
if (toolResultsMsg.is_array()) {
|
||||
for (const auto& msg : toolResultsMsg) {
|
||||
messages.push_back(msg);
|
||||
}
|
||||
} else {
|
||||
messages.push_back(toolResultsMsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {{"error", "max_iterations_reached"}};
|
||||
}
|
||||
|
||||
void AIModule::registerDefaultTools() {
|
||||
// Tool: get_current_time
|
||||
m_toolRegistry.registerTool(
|
||||
"get_current_time",
|
||||
"Obtient l'heure actuelle",
|
||||
{{"type", "object"}, {"properties", nlohmann::json::object()}},
|
||||
[](const nlohmann::json& input) -> nlohmann::json {
|
||||
std::time_t now = std::time(nullptr);
|
||||
std::tm* tm = std::localtime(&now);
|
||||
char buffer[64];
|
||||
std::strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", tm);
|
||||
return {{"time", buffer}};
|
||||
}
|
||||
);
|
||||
|
||||
// Tool: suggest_break
|
||||
m_toolRegistry.registerTool(
|
||||
"suggest_break",
|
||||
"Suggere une pause a l'utilisateur avec un message personnalise",
|
||||
{
|
||||
{"type", "object"},
|
||||
{"properties", {
|
||||
{"message", {{"type", "string"}, {"description", "Message de suggestion"}}},
|
||||
{"duration_minutes", {{"type", "integer"}, {"description", "Duree suggere"}}}
|
||||
}},
|
||||
{"required", nlohmann::json::array({"message"})}
|
||||
},
|
||||
[this](const nlohmann::json& input) -> nlohmann::json {
|
||||
std::string message = input.value("message", "Prends une pause!");
|
||||
int duration = input.value("duration_minutes", 10);
|
||||
|
||||
// Publish suggestion
|
||||
if (m_io) {
|
||||
auto event = std::make_unique<grove::JsonDataNode>("suggestion");
|
||||
event->setString("message", message);
|
||||
event->setInt("duration", duration);
|
||||
m_io->publish("ai:suggestion", std::move(event));
|
||||
}
|
||||
|
||||
return {{"status", "suggestion_sent"}, {"message", message}};
|
||||
}
|
||||
);
|
||||
|
||||
m_logger->info("Registered {} default tools", m_toolRegistry.size());
|
||||
}
|
||||
|
||||
void AIModule::publishResponse(const std::string& response) {
|
||||
if (!m_io) return;
|
||||
|
||||
auto event = std::make_unique<grove::JsonDataNode>("response");
|
||||
event->setString("text", response);
|
||||
event->setString("provider", m_providerName);
|
||||
m_io->publish("ai:response", std::move(event));
|
||||
|
||||
m_logger->info("AI response: {}", response.substr(0, 100));
|
||||
}
|
||||
|
||||
void AIModule::publishError(const std::string& error) {
|
||||
if (!m_io) return;
|
||||
|
||||
auto event = std::make_unique<grove::JsonDataNode>("error");
|
||||
event->setString("message", error);
|
||||
m_io->publish("ai:error", std::move(event));
|
||||
|
||||
m_logger->error("AI error: {}", error);
|
||||
}
|
||||
|
||||
std::unique_ptr<grove::IDataNode> AIModule::getHealthStatus() {
|
||||
auto status = std::make_unique<grove::JsonDataNode>("status");
|
||||
status->setString("status", m_provider ? "ready" : "not_initialized");
|
||||
status->setString("provider", m_providerName);
|
||||
status->setString("status", "ready");
|
||||
status->setInt("totalQueries", m_totalQueries);
|
||||
status->setInt("totalTokens", m_totalTokens);
|
||||
status->setBool("isProcessing", m_isProcessing);
|
||||
status->setBool("awaitingResponse", m_awaitingResponse);
|
||||
return status;
|
||||
}
|
||||
|
||||
@ -270,23 +173,16 @@ void AIModule::shutdown() {
|
||||
|
||||
std::unique_ptr<grove::IDataNode> AIModule::getState() {
|
||||
auto state = std::make_unique<grove::JsonDataNode>("state");
|
||||
state->setString("provider", m_providerName);
|
||||
state->setInt("totalQueries", m_totalQueries);
|
||||
state->setInt("totalTokens", m_totalTokens);
|
||||
state->setString("conversationHistory", m_conversationHistory.dump());
|
||||
state->setString("conversationId", m_currentConversationId);
|
||||
return state;
|
||||
}
|
||||
|
||||
void AIModule::setState(const grove::IDataNode& state) {
|
||||
m_totalQueries = state.getInt("totalQueries", 0);
|
||||
m_totalTokens = state.getInt("totalTokens", 0);
|
||||
|
||||
std::string historyStr = state.getString("conversationHistory", "[]");
|
||||
try {
|
||||
m_conversationHistory = nlohmann::json::parse(historyStr);
|
||||
} catch (...) {
|
||||
m_conversationHistory = nlohmann::json::array();
|
||||
}
|
||||
m_currentConversationId = state.getString("conversationId", "default");
|
||||
|
||||
m_logger->info("Etat restore: queries={}, tokens={}", m_totalQueries, m_totalTokens);
|
||||
}
|
||||
|
||||
@ -2,36 +2,32 @@
|
||||
|
||||
#include <grove/IModule.h>
|
||||
#include <grove/JsonDataNode.h>
|
||||
#include "../shared/llm/ILLMProvider.hpp"
|
||||
#include "../shared/llm/ToolRegistry.hpp"
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <spdlog/sinks/stdout_color_sinks.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace aissia {
|
||||
|
||||
/**
|
||||
* @brief AI Assistant Module - LLM integration agentique
|
||||
* @brief AI Assistant Module - Pure business logic
|
||||
*
|
||||
* Fonctionnalites:
|
||||
* - Boucle agentique avec tools
|
||||
* - Support multi-provider (Claude, OpenAI)
|
||||
* - Interventions proactives
|
||||
* - Gestion contexte conversation
|
||||
* Handles AI-related logic without any infrastructure code.
|
||||
* Communicates with LLMService via IIO pub/sub.
|
||||
*
|
||||
* Publie sur:
|
||||
* - "ai:response" : Reponse finale du LLM
|
||||
* - "ai:thinking" : LLM en cours de reflexion
|
||||
* - "llm:request" : Request LLM response
|
||||
* - "ai:suggestion" : Suggestion proactive
|
||||
* - "ai:error" : Erreur API
|
||||
*
|
||||
* Souscrit a:
|
||||
* - "ai:query" : Requete utilisateur
|
||||
* - "voice:transcription" : Texte transcrit (STT)
|
||||
* - "scheduler:hyperfocus_alert": Generer intervention
|
||||
* - "scheduler:break_reminder" : Generer suggestion pause
|
||||
* - "llm:response" : Response from LLM
|
||||
* - "llm:error" : Error from LLM
|
||||
* - "ai:query" : User query
|
||||
* - "voice:transcription" : STT result
|
||||
* - "scheduler:hyperfocus_alert" : Generate intervention
|
||||
* - "scheduler:break_reminder" : Generate break suggestion
|
||||
*/
|
||||
class AIModule : public grove::IModule {
|
||||
public:
|
||||
@ -48,22 +44,19 @@ public:
|
||||
std::unique_ptr<grove::IDataNode> getState() override;
|
||||
void setState(const grove::IDataNode& state) override;
|
||||
std::string getType() const override { return "AIModule"; }
|
||||
bool isIdle() const override { return !m_isProcessing; }
|
||||
int getVersion() const override { return 1; }
|
||||
bool isIdle() const override { return !m_awaitingResponse; }
|
||||
int getVersion() const override { return 2; }
|
||||
|
||||
private:
|
||||
// Configuration
|
||||
std::string m_providerName = "claude";
|
||||
std::string m_systemPrompt;
|
||||
int m_maxIterations = 10;
|
||||
|
||||
// State
|
||||
std::unique_ptr<ILLMProvider> m_provider;
|
||||
ToolRegistry m_toolRegistry;
|
||||
nlohmann::json m_conversationHistory;
|
||||
int m_totalQueries = 0;
|
||||
int m_totalTokens = 0;
|
||||
bool m_isProcessing = false;
|
||||
bool m_awaitingResponse = false;
|
||||
std::string m_currentConversationId = "default";
|
||||
|
||||
// Services
|
||||
grove::IIO* m_io = nullptr;
|
||||
@ -72,13 +65,12 @@ private:
|
||||
|
||||
// Helpers
|
||||
void processMessages();
|
||||
void handleQuery(const std::string& query);
|
||||
void sendQuery(const std::string& query);
|
||||
void handleLLMResponse(const grove::IDataNode& data);
|
||||
void handleLLMError(const grove::IDataNode& data);
|
||||
void handleHyperfocusAlert(const grove::IDataNode& data);
|
||||
void handleBreakReminder(const grove::IDataNode& data);
|
||||
nlohmann::json agenticLoop(const std::string& userQuery);
|
||||
void registerDefaultTools();
|
||||
void publishResponse(const std::string& response);
|
||||
void publishError(const std::string& error);
|
||||
void publishSuggestion(const std::string& message, int duration);
|
||||
};
|
||||
|
||||
} // namespace aissia
|
||||
|
||||
@ -18,8 +18,6 @@ void MonitoringModule::setConfiguration(const grove::IDataNode& configNode,
|
||||
m_io = io;
|
||||
m_config = std::make_unique<grove::JsonDataNode>("config");
|
||||
|
||||
m_pollIntervalMs = configNode.getInt("poll_interval_ms", 1000);
|
||||
m_idleThresholdSeconds = configNode.getInt("idle_threshold_seconds", 300);
|
||||
m_enabled = configNode.getBool("enabled", true);
|
||||
|
||||
// Load productive apps list
|
||||
@ -49,12 +47,16 @@ void MonitoringModule::setConfiguration(const grove::IDataNode& configNode,
|
||||
"firefox", "chrome", "YouTube"};
|
||||
}
|
||||
|
||||
// Create window tracker
|
||||
m_tracker = WindowTrackerFactory::create();
|
||||
// Subscribe to platform events
|
||||
if (m_io) {
|
||||
grove::SubscriptionConfig subConfig;
|
||||
m_io->subscribe("platform:window_info", subConfig);
|
||||
m_io->subscribe("platform:window_changed", subConfig);
|
||||
m_io->subscribe("platform:idle_detected", subConfig);
|
||||
m_io->subscribe("platform:activity_resumed", subConfig);
|
||||
}
|
||||
|
||||
m_logger->info("MonitoringModule configure: poll={}ms, idle={}s, platform={}",
|
||||
m_pollIntervalMs, m_idleThresholdSeconds,
|
||||
m_tracker ? m_tracker->getPlatformName() : "none");
|
||||
m_logger->info("MonitoringModule configure (v2 - sans infrastructure)");
|
||||
}
|
||||
|
||||
const grove::IDataNode& MonitoringModule::getConfiguration() {
|
||||
@ -62,72 +64,96 @@ const grove::IDataNode& MonitoringModule::getConfiguration() {
|
||||
}
|
||||
|
||||
void MonitoringModule::process(const grove::IDataNode& input) {
|
||||
if (!m_enabled || !m_tracker || !m_tracker->isAvailable()) return;
|
||||
|
||||
float currentTime = input.getDouble("gameTime", 0.0);
|
||||
|
||||
// Poll based on interval
|
||||
float pollIntervalSec = m_pollIntervalMs / 1000.0f;
|
||||
if (currentTime - m_lastPollTime < pollIntervalSec) return;
|
||||
m_lastPollTime = currentTime;
|
||||
|
||||
checkCurrentApp(currentTime);
|
||||
checkIdleState(currentTime);
|
||||
if (!m_enabled) return;
|
||||
processMessages();
|
||||
}
|
||||
|
||||
void MonitoringModule::checkCurrentApp(float currentTime) {
|
||||
std::string newApp = m_tracker->getCurrentAppName();
|
||||
std::string newTitle = m_tracker->getCurrentWindowTitle();
|
||||
void MonitoringModule::processMessages() {
|
||||
if (!m_io) return;
|
||||
|
||||
if (newApp != m_currentApp) {
|
||||
// App changed
|
||||
int duration = static_cast<int>(currentTime - m_appStartTime);
|
||||
while (m_io->hasMessages() > 0) {
|
||||
auto msg = m_io->pullMessage();
|
||||
|
||||
if (!m_currentApp.empty() && duration > 0) {
|
||||
m_appDurations[m_currentApp] += duration;
|
||||
if (msg.topic == "platform:window_changed" && msg.data) {
|
||||
handleWindowChanged(*msg.data);
|
||||
}
|
||||
else if (msg.topic == "platform:window_info" && msg.data) {
|
||||
handleWindowInfo(*msg.data);
|
||||
}
|
||||
else if (msg.topic == "platform:idle_detected" && msg.data) {
|
||||
handleIdleDetected(*msg.data);
|
||||
}
|
||||
else if (msg.topic == "platform:activity_resumed" && msg.data) {
|
||||
handleActivityResumed(*msg.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MonitoringModule::handleWindowChanged(const grove::IDataNode& data) {
|
||||
std::string oldApp = data.getString("oldApp", "");
|
||||
std::string newApp = data.getString("newApp", "");
|
||||
int duration = data.getInt("duration", 0);
|
||||
|
||||
if (!oldApp.empty() && duration > 0) {
|
||||
// Update duration tracking
|
||||
m_appDurations[oldApp] += duration;
|
||||
|
||||
// Update productivity counters
|
||||
if (isProductiveApp(m_currentApp)) {
|
||||
bool wasProductive = isProductiveApp(oldApp);
|
||||
bool wasDistracting = isDistractingApp(oldApp);
|
||||
|
||||
if (wasProductive) {
|
||||
m_totalProductiveSeconds += duration;
|
||||
} else if (isDistractingApp(m_currentApp)) {
|
||||
} else if (wasDistracting) {
|
||||
m_totalDistractingSeconds += duration;
|
||||
}
|
||||
|
||||
publishAppChanged(m_currentApp, newApp, duration);
|
||||
// Publish enriched app change event
|
||||
if (m_io) {
|
||||
auto event = std::make_unique<grove::JsonDataNode>("changed");
|
||||
event->setString("oldApp", oldApp);
|
||||
event->setString("newApp", newApp);
|
||||
event->setInt("duration", duration);
|
||||
event->setBool("wasProductive", wasProductive);
|
||||
event->setBool("wasDistracting", wasDistracting);
|
||||
m_io->publish("monitoring:app_changed", std::move(event));
|
||||
}
|
||||
|
||||
m_logger->debug("App: {} -> {} ({}s, prod={})",
|
||||
oldApp, newApp, duration, wasProductive);
|
||||
}
|
||||
|
||||
m_currentApp = newApp;
|
||||
m_currentWindowTitle = newTitle;
|
||||
m_appStartTime = currentTime;
|
||||
}
|
||||
|
||||
m_logger->debug("App: {} - {}", m_currentApp, m_currentWindowTitle.substr(0, 50));
|
||||
void MonitoringModule::handleWindowInfo(const grove::IDataNode& data) {
|
||||
std::string appName = data.getString("appName", "");
|
||||
if (!appName.empty() && appName != m_currentApp) {
|
||||
m_currentApp = appName;
|
||||
}
|
||||
}
|
||||
|
||||
void MonitoringModule::checkIdleState(float currentTime) {
|
||||
bool wasIdle = m_isIdle;
|
||||
m_isIdle = m_tracker->isUserIdle(m_idleThresholdSeconds);
|
||||
|
||||
if (m_isIdle && !wasIdle) {
|
||||
m_logger->info("Utilisateur inactif ({}s)", m_idleThresholdSeconds);
|
||||
void MonitoringModule::handleIdleDetected(const grove::IDataNode& data) {
|
||||
m_isIdle = true;
|
||||
int idleSeconds = data.getInt("idleSeconds", 0);
|
||||
m_logger->info("User idle detected ({}s)", idleSeconds);
|
||||
|
||||
if (m_io) {
|
||||
auto event = std::make_unique<grove::JsonDataNode>("idle");
|
||||
event->setString("type", "idle_detected");
|
||||
event->setInt("idleSeconds", m_tracker->getIdleTimeSeconds());
|
||||
event->setInt("idleSeconds", idleSeconds);
|
||||
m_io->publish("monitoring:idle_detected", std::move(event));
|
||||
}
|
||||
}
|
||||
else if (!m_isIdle && wasIdle) {
|
||||
m_logger->info("Activite reprise");
|
||||
|
||||
void MonitoringModule::handleActivityResumed(const grove::IDataNode& data) {
|
||||
m_isIdle = false;
|
||||
m_logger->info("User activity resumed");
|
||||
|
||||
if (m_io) {
|
||||
auto event = std::make_unique<grove::JsonDataNode>("active");
|
||||
event->setString("type", "activity_resumed");
|
||||
m_io->publish("monitoring:activity_resumed", std::move(event));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool MonitoringModule::isProductiveApp(const std::string& appName) const {
|
||||
// Check exact match
|
||||
@ -159,18 +185,6 @@ bool MonitoringModule::isDistractingApp(const std::string& appName) const {
|
||||
return false;
|
||||
}
|
||||
|
||||
void MonitoringModule::publishAppChanged(const std::string& oldApp, const std::string& newApp, int duration) {
|
||||
if (!m_io) return;
|
||||
|
||||
auto event = std::make_unique<grove::JsonDataNode>("app_changed");
|
||||
event->setString("oldApp", oldApp);
|
||||
event->setString("newApp", newApp);
|
||||
event->setInt("duration", duration);
|
||||
event->setBool("wasProductive", isProductiveApp(oldApp));
|
||||
event->setBool("wasDistracting", isDistractingApp(oldApp));
|
||||
m_io->publish("monitoring:app_changed", std::move(event));
|
||||
}
|
||||
|
||||
std::unique_ptr<grove::IDataNode> MonitoringModule::getHealthStatus() {
|
||||
auto status = std::make_unique<grove::JsonDataNode>("status");
|
||||
status->setString("status", m_enabled ? "running" : "disabled");
|
||||
@ -178,7 +192,6 @@ std::unique_ptr<grove::IDataNode> MonitoringModule::getHealthStatus() {
|
||||
status->setBool("isIdle", m_isIdle);
|
||||
status->setInt("totalProductiveSeconds", m_totalProductiveSeconds);
|
||||
status->setInt("totalDistractingSeconds", m_totalDistractingSeconds);
|
||||
status->setString("platform", m_tracker ? m_tracker->getPlatformName() : "none");
|
||||
return status;
|
||||
}
|
||||
|
||||
@ -190,7 +203,6 @@ void MonitoringModule::shutdown() {
|
||||
std::unique_ptr<grove::IDataNode> MonitoringModule::getState() {
|
||||
auto state = std::make_unique<grove::JsonDataNode>("state");
|
||||
state->setString("currentApp", m_currentApp);
|
||||
state->setDouble("appStartTime", m_appStartTime);
|
||||
state->setBool("isIdle", m_isIdle);
|
||||
state->setInt("totalProductiveSeconds", m_totalProductiveSeconds);
|
||||
state->setInt("totalDistractingSeconds", m_totalDistractingSeconds);
|
||||
@ -199,7 +211,6 @@ std::unique_ptr<grove::IDataNode> MonitoringModule::getState() {
|
||||
|
||||
void MonitoringModule::setState(const grove::IDataNode& state) {
|
||||
m_currentApp = state.getString("currentApp", "");
|
||||
m_appStartTime = state.getDouble("appStartTime", 0.0);
|
||||
m_isIdle = state.getBool("isIdle", false);
|
||||
m_totalProductiveSeconds = state.getInt("totalProductiveSeconds", 0);
|
||||
m_totalDistractingSeconds = state.getInt("totalDistractingSeconds", 0);
|
||||
|
||||
@ -2,36 +2,31 @@
|
||||
|
||||
#include <grove/IModule.h>
|
||||
#include <grove/JsonDataNode.h>
|
||||
#include "../shared/platform/IWindowTracker.hpp"
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <spdlog/sinks/stdout_color_sinks.h>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <map>
|
||||
|
||||
namespace aissia {
|
||||
|
||||
/**
|
||||
* @brief Monitoring Module - Tracking des applications actives
|
||||
* @brief Monitoring Module - App classification logic only
|
||||
*
|
||||
* Fonctionnalites:
|
||||
* - Detection de l'application au premier plan
|
||||
* - Classification productive/distracting
|
||||
* - Detection d'inactivite utilisateur
|
||||
* - Statistiques par application
|
||||
* Handles app classification without platform-specific tracking.
|
||||
* Receives window info from PlatformService via IIO.
|
||||
*
|
||||
* Publie sur:
|
||||
* - "monitoring:app_changed" : Changement d'application
|
||||
* - "monitoring:idle_detected" : Utilisateur inactif
|
||||
* - "monitoring:activity_resumed" : Retour d'activite
|
||||
* - "monitoring:productivity_update": Mise a jour des stats
|
||||
* - "monitoring:app_changed" : When tracked app changes (with classification)
|
||||
* - "monitoring:productivity_update": Periodic stats
|
||||
*
|
||||
* Souscrit a:
|
||||
* - "scheduler:task_started" : Associer tracking a tache
|
||||
* - "scheduler:task_completed" : Fin tracking tache
|
||||
* - "platform:window_info" : Current window from PlatformService
|
||||
* - "platform:window_changed" : Window change from PlatformService
|
||||
* - "platform:idle_detected" : User idle
|
||||
* - "platform:activity_resumed": User active
|
||||
*/
|
||||
class MonitoringModule : public grove::IModule {
|
||||
public:
|
||||
@ -49,20 +44,16 @@ public:
|
||||
void setState(const grove::IDataNode& state) override;
|
||||
std::string getType() const override { return "MonitoringModule"; }
|
||||
bool isIdle() const override { return true; }
|
||||
int getVersion() const override { return 1; }
|
||||
int getVersion() const override { return 2; }
|
||||
|
||||
private:
|
||||
// Configuration
|
||||
int m_pollIntervalMs = 1000;
|
||||
int m_idleThresholdSeconds = 300;
|
||||
std::set<std::string> m_productiveApps;
|
||||
std::set<std::string> m_distractingApps;
|
||||
bool m_enabled = true;
|
||||
|
||||
// State
|
||||
std::string m_currentApp;
|
||||
std::string m_currentWindowTitle;
|
||||
float m_appStartTime = 0.0f;
|
||||
bool m_isIdle = false;
|
||||
std::map<std::string, int> m_appDurations; // seconds per app
|
||||
int m_totalProductiveSeconds = 0;
|
||||
@ -70,17 +61,17 @@ private:
|
||||
|
||||
// Services
|
||||
grove::IIO* m_io = nullptr;
|
||||
std::unique_ptr<IWindowTracker> m_tracker;
|
||||
std::unique_ptr<grove::JsonDataNode> m_config;
|
||||
std::shared_ptr<spdlog::logger> m_logger;
|
||||
float m_lastPollTime = 0.0f;
|
||||
|
||||
// Helpers
|
||||
void checkCurrentApp(float currentTime);
|
||||
void checkIdleState(float currentTime);
|
||||
void processMessages();
|
||||
void handleWindowChanged(const grove::IDataNode& data);
|
||||
void handleWindowInfo(const grove::IDataNode& data);
|
||||
void handleIdleDetected(const grove::IDataNode& data);
|
||||
void handleActivityResumed(const grove::IDataNode& data);
|
||||
bool isProductiveApp(const std::string& appName) const;
|
||||
bool isDistractingApp(const std::string& appName) const;
|
||||
void publishAppChanged(const std::string& oldApp, const std::string& newApp, int duration);
|
||||
};
|
||||
|
||||
} // namespace aissia
|
||||
|
||||
@ -22,7 +22,16 @@ void SchedulerModule::setConfiguration(const grove::IDataNode& configNode,
|
||||
m_breakReminderIntervalMinutes = configNode.getInt("breakReminderIntervalMinutes", 45);
|
||||
m_breakDurationMinutes = configNode.getInt("breakDurationMinutes", 10);
|
||||
|
||||
m_logger->info("SchedulerModule configuré: hyperfocus={}min, break_interval={}min",
|
||||
// Subscribe to topics
|
||||
if (m_io) {
|
||||
grove::SubscriptionConfig subConfig;
|
||||
m_io->subscribe("user:activity", subConfig);
|
||||
m_io->subscribe("user:task_switch", subConfig);
|
||||
m_io->subscribe("monitoring:idle_detected", subConfig);
|
||||
m_io->subscribe("monitoring:activity_resumed", subConfig);
|
||||
}
|
||||
|
||||
m_logger->info("SchedulerModule configure: hyperfocus={}min, break_interval={}min",
|
||||
m_hyperfocusThresholdMinutes, m_breakReminderIntervalMinutes);
|
||||
}
|
||||
|
||||
@ -32,27 +41,52 @@ const grove::IDataNode& SchedulerModule::getConfiguration() {
|
||||
|
||||
void SchedulerModule::process(const grove::IDataNode& input) {
|
||||
float currentTime = input.getDouble("gameTime", 0.0);
|
||||
float dt = input.getDouble("deltaTime", 0.016);
|
||||
|
||||
// Process incoming messages
|
||||
processMessages();
|
||||
|
||||
// Convertir le temps en minutes pour les calculs
|
||||
float sessionMinutes = (currentTime - m_sessionStartTime) / 60.0f;
|
||||
|
||||
// Vérifier l'hyperfocus
|
||||
// Verifier l'hyperfocus
|
||||
checkHyperfocus(currentTime);
|
||||
|
||||
// Vérifier les rappels de pause
|
||||
// Verifier les rappels de pause
|
||||
checkBreakReminder(currentTime);
|
||||
|
||||
// Log périodique (toutes les 5 minutes simulées)
|
||||
// Log periodique (toutes les 5 minutes simulees)
|
||||
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: {}",
|
||||
m_logger->debug("Session: {:.1f}min, Focus aujourd'hui: {}min, Tache: {}",
|
||||
sessionMinutes, m_totalFocusMinutesToday,
|
||||
m_currentTaskId.empty() ? "(aucune)" : m_currentTaskId);
|
||||
}
|
||||
}
|
||||
|
||||
void SchedulerModule::processMessages() {
|
||||
if (!m_io) return;
|
||||
|
||||
while (m_io->hasMessages() > 0) {
|
||||
auto msg = m_io->pullMessage();
|
||||
|
||||
if (msg.topic == "user:task_switch" && msg.data) {
|
||||
std::string taskId = msg.data->getString("taskId", "");
|
||||
if (!taskId.empty()) {
|
||||
startTask(taskId);
|
||||
}
|
||||
}
|
||||
else if (msg.topic == "monitoring:idle_detected" && msg.data) {
|
||||
// User went idle - pause tracking
|
||||
m_logger->debug("User idle, pausing session tracking");
|
||||
}
|
||||
else if (msg.topic == "monitoring:activity_resumed" && msg.data) {
|
||||
// User returned - resume tracking
|
||||
m_logger->debug("User active, resuming session tracking");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SchedulerModule::checkHyperfocus(float currentTime) {
|
||||
if (m_currentTaskId.empty()) return;
|
||||
|
||||
@ -60,11 +94,17 @@ void SchedulerModule::checkHyperfocus(float currentTime) {
|
||||
|
||||
if (sessionMinutes >= m_hyperfocusThresholdMinutes && !m_hyperfocusAlertSent) {
|
||||
m_hyperfocusAlertSent = true;
|
||||
m_logger->warn("HYPERFOCUS DÉTECTÉ! Session de {:.0f} minutes sur '{}'",
|
||||
m_logger->warn("HYPERFOCUS DETECTE! 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
|
||||
// Publish hyperfocus alert
|
||||
if (m_io) {
|
||||
auto event = std::make_unique<grove::JsonDataNode>("alert");
|
||||
event->setString("type", "hyperfocus");
|
||||
event->setInt("duration_minutes", static_cast<int>(sessionMinutes));
|
||||
event->setString("task", m_currentTaskId);
|
||||
m_io->publish("scheduler:hyperfocus_alert", std::move(event));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,14 +113,21 @@ void SchedulerModule::checkBreakReminder(float currentTime) {
|
||||
|
||||
if (timeSinceBreak >= m_breakReminderIntervalMinutes) {
|
||||
m_lastBreakTime = currentTime;
|
||||
m_logger->info("RAPPEL: Pause de {} minutes recommandée!", m_breakDurationMinutes);
|
||||
m_logger->info("RAPPEL: Pause de {} minutes recommandee!", m_breakDurationMinutes);
|
||||
|
||||
// Publier le rappel (si IO disponible)
|
||||
// Publish break reminder
|
||||
if (m_io) {
|
||||
auto event = std::make_unique<grove::JsonDataNode>("reminder");
|
||||
event->setString("type", "break");
|
||||
event->setInt("break_duration", m_breakDurationMinutes);
|
||||
event->setInt("time_since_last_break", static_cast<int>(timeSinceBreak));
|
||||
m_io->publish("scheduler:break_reminder", std::move(event));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SchedulerModule::startTask(const std::string& taskId) {
|
||||
// Compléter la tâche précédente si nécessaire
|
||||
// Completer la tache precedente si necessaire
|
||||
if (!m_currentTaskId.empty()) {
|
||||
completeCurrentTask();
|
||||
}
|
||||
@ -91,7 +138,15 @@ void SchedulerModule::startTask(const std::string& taskId) {
|
||||
|
||||
Task* task = findTask(taskId);
|
||||
if (task) {
|
||||
m_logger->info("Tâche démarrée: {} (estimé: {}min)", task->name, task->estimatedMinutes);
|
||||
m_logger->info("Tache demarree: {} (estime: {}min)", task->name, task->estimatedMinutes);
|
||||
}
|
||||
|
||||
// Publish task started
|
||||
if (m_io) {
|
||||
auto event = std::make_unique<grove::JsonDataNode>("started");
|
||||
event->setString("taskId", taskId);
|
||||
event->setString("taskName", task ? task->name : taskId);
|
||||
m_io->publish("scheduler:task_started", std::move(event));
|
||||
}
|
||||
}
|
||||
|
||||
@ -99,16 +154,27 @@ void SchedulerModule::completeCurrentTask() {
|
||||
if (m_currentTaskId.empty()) return;
|
||||
|
||||
Task* task = findTask(m_currentTaskId);
|
||||
if (task) {
|
||||
float sessionMinutes = (m_lastActivityTime - m_sessionStartTime) / 60.0f;
|
||||
|
||||
if (task) {
|
||||
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)",
|
||||
m_logger->info("Tache terminee: {} (reel: {}min vs estime: {}min)",
|
||||
task->name, task->actualMinutes, task->estimatedMinutes);
|
||||
}
|
||||
|
||||
// Publish task completed
|
||||
if (m_io) {
|
||||
auto event = std::make_unique<grove::JsonDataNode>("completed");
|
||||
event->setString("taskId", m_currentTaskId);
|
||||
event->setString("taskName", task ? task->name : m_currentTaskId);
|
||||
event->setInt("duration", static_cast<int>(sessionMinutes));
|
||||
event->setBool("hyperfocus", m_hyperfocusAlertSent);
|
||||
m_io->publish("scheduler:task_completed", std::move(event));
|
||||
}
|
||||
|
||||
m_currentTaskId.clear();
|
||||
}
|
||||
|
||||
@ -133,7 +199,7 @@ void SchedulerModule::shutdown() {
|
||||
if (!m_currentTaskId.empty()) {
|
||||
completeCurrentTask();
|
||||
}
|
||||
m_logger->info("SchedulerModule arrêté. Focus total: {}min", m_totalFocusMinutesToday);
|
||||
m_logger->info("SchedulerModule arrete. Focus total: {}min", m_totalFocusMinutesToday);
|
||||
}
|
||||
|
||||
std::unique_ptr<grove::IDataNode> SchedulerModule::getState() {
|
||||
@ -147,7 +213,7 @@ std::unique_ptr<grove::IDataNode> SchedulerModule::getState() {
|
||||
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);
|
||||
m_logger->debug("Etat sauvegarde: {} taches, focus={}min", m_tasks.size(), m_totalFocusMinutesToday);
|
||||
return state;
|
||||
}
|
||||
|
||||
@ -159,7 +225,7 @@ void SchedulerModule::setState(const grove::IDataNode& state) {
|
||||
m_hyperfocusAlertSent = state.getBool("hyperfocusAlertSent", false);
|
||||
m_totalFocusMinutesToday = state.getInt("totalFocusMinutesToday", 0);
|
||||
|
||||
m_logger->info("État restauré: tâche='{}', focus={}min",
|
||||
m_logger->info("Etat restaure: tache='{}', focus={}min",
|
||||
m_currentTaskId.empty() ? "(aucune)" : m_currentTaskId,
|
||||
m_totalFocusMinutesToday);
|
||||
}
|
||||
|
||||
@ -22,14 +22,16 @@ namespace aissia {
|
||||
* - 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
|
||||
* - "scheduler:hyperfocus_alert" : Alerte quand hyperfocus detecte
|
||||
* - "scheduler:break_reminder" : Rappel de pause
|
||||
* - "scheduler:task_started" : Debut de tache
|
||||
* - "scheduler:task_completed" : Fin de tache
|
||||
*
|
||||
* Souscrit à:
|
||||
* - "user/activity" : Activité utilisateur (reset idle)
|
||||
* - "user/task_switch" : Changement de tâche manuel
|
||||
* Souscrit a:
|
||||
* - "user:activity" : Activite utilisateur (reset idle)
|
||||
* - "user:task_switch" : Changement de tache manuel
|
||||
* - "monitoring:idle_detected" : Utilisateur inactif
|
||||
* - "monitoring:activity_resumed": Retour activite
|
||||
*/
|
||||
class SchedulerModule : public grove::IModule {
|
||||
public:
|
||||
|
||||
@ -1,10 +1,5 @@
|
||||
#include "StorageModule.h"
|
||||
#include <grove/JsonDataNode.h>
|
||||
#include <sqlite3.h>
|
||||
#include <filesystem>
|
||||
#include <ctime>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace aissia {
|
||||
|
||||
@ -16,32 +11,22 @@ StorageModule::StorageModule() {
|
||||
m_config = std::make_unique<grove::JsonDataNode>("config");
|
||||
}
|
||||
|
||||
StorageModule::~StorageModule() {
|
||||
closeDatabase();
|
||||
}
|
||||
|
||||
void StorageModule::setConfiguration(const grove::IDataNode& configNode,
|
||||
grove::IIO* io,
|
||||
grove::ITaskScheduler* scheduler) {
|
||||
m_io = io;
|
||||
m_config = std::make_unique<grove::JsonDataNode>("config");
|
||||
|
||||
m_dbPath = configNode.getString("database_path", "./data/aissia.db");
|
||||
m_journalMode = configNode.getString("journal_mode", "WAL");
|
||||
m_busyTimeoutMs = configNode.getInt("busy_timeout_ms", 5000);
|
||||
|
||||
// Ensure data directory exists
|
||||
fs::path dbPath(m_dbPath);
|
||||
if (dbPath.has_parent_path()) {
|
||||
fs::create_directories(dbPath.parent_path());
|
||||
// Subscribe to topics
|
||||
if (m_io) {
|
||||
grove::SubscriptionConfig subConfig;
|
||||
m_io->subscribe("scheduler:task_completed", subConfig);
|
||||
m_io->subscribe("monitoring:app_changed", subConfig);
|
||||
m_io->subscribe("storage:session_saved", subConfig);
|
||||
m_io->subscribe("storage:error", subConfig);
|
||||
}
|
||||
|
||||
if (openDatabase()) {
|
||||
initializeSchema();
|
||||
m_logger->info("StorageModule configure: db={}, journal={}", m_dbPath, m_journalMode);
|
||||
} else {
|
||||
m_logger->error("Echec ouverture base de donnees: {}", m_dbPath);
|
||||
}
|
||||
m_logger->info("StorageModule configure (v2 - sans infrastructure)");
|
||||
}
|
||||
|
||||
const grove::IDataNode& StorageModule::getConfiguration() {
|
||||
@ -49,7 +34,6 @@ const grove::IDataNode& StorageModule::getConfiguration() {
|
||||
}
|
||||
|
||||
void StorageModule::process(const grove::IDataNode& input) {
|
||||
if (!m_isConnected) return;
|
||||
processMessages();
|
||||
}
|
||||
|
||||
@ -60,202 +44,94 @@ void StorageModule::processMessages() {
|
||||
auto msg = m_io->pullMessage();
|
||||
|
||||
if (msg.topic == "scheduler:task_completed" && msg.data) {
|
||||
std::string taskName = msg.data->getString("taskName", "unknown");
|
||||
int duration = msg.data->getInt("duration", 0);
|
||||
bool hyperfocus = msg.data->getBool("hyperfocus", false);
|
||||
saveWorkSession(taskName, duration, hyperfocus);
|
||||
handleTaskCompleted(*msg.data);
|
||||
}
|
||||
else if (msg.topic == "monitoring:app_changed" && msg.data) {
|
||||
std::string appName = msg.data->getString("appName", "");
|
||||
int duration = msg.data->getInt("duration", 0);
|
||||
bool productive = msg.data->getBool("productive", false);
|
||||
saveAppUsage(m_lastSessionId, appName, duration, productive);
|
||||
handleAppChanged(*msg.data);
|
||||
}
|
||||
else if (msg.topic == "storage:session_saved" && msg.data) {
|
||||
handleSessionSaved(*msg.data);
|
||||
}
|
||||
else if (msg.topic == "storage:error" && msg.data) {
|
||||
handleStorageError(*msg.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool StorageModule::openDatabase() {
|
||||
int rc = sqlite3_open(m_dbPath.c_str(), &m_db);
|
||||
if (rc != SQLITE_OK) {
|
||||
m_logger->error("SQLite open error: {}", sqlite3_errmsg(m_db));
|
||||
return false;
|
||||
}
|
||||
void StorageModule::handleTaskCompleted(const grove::IDataNode& data) {
|
||||
std::string taskName = data.getString("taskName", "unknown");
|
||||
int duration = data.getInt("duration", 0);
|
||||
bool hyperfocus = data.getBool("hyperfocus", false);
|
||||
|
||||
// Set pragmas
|
||||
std::string pragmas = "PRAGMA journal_mode=" + m_journalMode + ";"
|
||||
"PRAGMA busy_timeout=" + std::to_string(m_busyTimeoutMs) + ";"
|
||||
"PRAGMA foreign_keys=ON;";
|
||||
executeSQL(pragmas);
|
||||
m_logger->debug("Task completed: {} ({}min), publishing save request", taskName, duration);
|
||||
|
||||
m_isConnected = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void StorageModule::closeDatabase() {
|
||||
if (m_db) {
|
||||
sqlite3_close(m_db);
|
||||
m_db = nullptr;
|
||||
m_isConnected = false;
|
||||
if (m_io) {
|
||||
auto request = std::make_unique<grove::JsonDataNode>("save");
|
||||
request->setString("taskName", taskName);
|
||||
request->setInt("durationMinutes", duration);
|
||||
request->setBool("hyperfocus", hyperfocus);
|
||||
m_io->publish("storage:save_session", std::move(request));
|
||||
m_pendingSaves++;
|
||||
}
|
||||
}
|
||||
|
||||
bool StorageModule::initializeSchema() {
|
||||
const char* schema = R"SQL(
|
||||
CREATE TABLE IF NOT EXISTS work_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
task_name TEXT,
|
||||
start_time INTEGER,
|
||||
end_time INTEGER,
|
||||
duration_minutes INTEGER,
|
||||
hyperfocus_detected BOOLEAN DEFAULT 0,
|
||||
created_at INTEGER DEFAULT (strftime('%s', 'now'))
|
||||
);
|
||||
void StorageModule::handleAppChanged(const grove::IDataNode& data) {
|
||||
std::string appName = data.getString("oldApp", "");
|
||||
int duration = data.getInt("duration", 0);
|
||||
bool productive = data.getBool("wasProductive", false);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS app_usage (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_id INTEGER,
|
||||
app_name TEXT,
|
||||
duration_seconds INTEGER,
|
||||
is_productive BOOLEAN,
|
||||
created_at INTEGER DEFAULT (strftime('%s', 'now')),
|
||||
FOREIGN KEY (session_id) REFERENCES work_sessions(id)
|
||||
);
|
||||
if (appName.empty() || duration <= 0) return;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS conversations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
role TEXT,
|
||||
content TEXT,
|
||||
provider TEXT,
|
||||
model TEXT,
|
||||
tokens_used INTEGER,
|
||||
created_at INTEGER DEFAULT (strftime('%s', 'now'))
|
||||
);
|
||||
m_logger->debug("App usage: {} ({}s), publishing save request", appName, duration);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS daily_metrics (
|
||||
date TEXT PRIMARY KEY,
|
||||
total_focus_minutes INTEGER DEFAULT 0,
|
||||
total_breaks INTEGER DEFAULT 0,
|
||||
hyperfocus_count INTEGER DEFAULT 0,
|
||||
updated_at INTEGER DEFAULT (strftime('%s', 'now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_date ON work_sessions(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_app_usage_session ON app_usage(session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_conversations_date ON conversations(created_at);
|
||||
)SQL";
|
||||
|
||||
return executeSQL(schema);
|
||||
if (m_io) {
|
||||
auto request = std::make_unique<grove::JsonDataNode>("save");
|
||||
request->setInt("sessionId", m_lastSessionId);
|
||||
request->setString("appName", appName);
|
||||
request->setInt("durationSeconds", duration);
|
||||
request->setBool("productive", productive);
|
||||
m_io->publish("storage:save_app_usage", std::move(request));
|
||||
m_pendingSaves++;
|
||||
}
|
||||
}
|
||||
|
||||
bool StorageModule::executeSQL(const std::string& sql) {
|
||||
char* errMsg = nullptr;
|
||||
int rc = sqlite3_exec(m_db, sql.c_str(), nullptr, nullptr, &errMsg);
|
||||
|
||||
if (rc != SQLITE_OK) {
|
||||
m_logger->error("SQL error: {}", errMsg ? errMsg : "unknown");
|
||||
sqlite3_free(errMsg);
|
||||
return false;
|
||||
void StorageModule::handleSessionSaved(const grove::IDataNode& data) {
|
||||
m_lastSessionId = data.getInt("sessionId", 0);
|
||||
m_pendingSaves--;
|
||||
m_totalSaved++;
|
||||
m_logger->debug("Session saved: id={}", m_lastSessionId);
|
||||
}
|
||||
|
||||
m_totalQueries++;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool StorageModule::saveWorkSession(const std::string& taskName, int durationMinutes, bool hyperfocusDetected) {
|
||||
if (!m_isConnected) return false;
|
||||
|
||||
std::time_t now = std::time(nullptr);
|
||||
std::time_t startTime = now - (durationMinutes * 60);
|
||||
|
||||
std::string sql = "INSERT INTO work_sessions (task_name, start_time, end_time, duration_minutes, hyperfocus_detected) "
|
||||
"VALUES ('" + taskName + "', " + std::to_string(startTime) + ", " +
|
||||
std::to_string(now) + ", " + std::to_string(durationMinutes) + ", " +
|
||||
(hyperfocusDetected ? "1" : "0") + ");";
|
||||
|
||||
if (executeSQL(sql)) {
|
||||
m_lastSessionId = static_cast<int>(sqlite3_last_insert_rowid(m_db));
|
||||
m_logger->debug("Session sauvegardee: {} ({}min)", taskName, durationMinutes);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool StorageModule::saveAppUsage(int sessionId, const std::string& appName, int durationSeconds, bool productive) {
|
||||
if (!m_isConnected) return false;
|
||||
|
||||
std::string sql = "INSERT INTO app_usage (session_id, app_name, duration_seconds, is_productive) "
|
||||
"VALUES (" + std::to_string(sessionId) + ", '" + appName + "', " +
|
||||
std::to_string(durationSeconds) + ", " + (productive ? "1" : "0") + ");";
|
||||
|
||||
return executeSQL(sql);
|
||||
}
|
||||
|
||||
bool StorageModule::saveConversation(const std::string& role, const std::string& content,
|
||||
const std::string& provider, const std::string& model, int tokensUsed) {
|
||||
if (!m_isConnected) return false;
|
||||
|
||||
// Escape single quotes in content
|
||||
std::string escapedContent = content;
|
||||
size_t pos = 0;
|
||||
while ((pos = escapedContent.find("'", pos)) != std::string::npos) {
|
||||
escapedContent.replace(pos, 1, "''");
|
||||
pos += 2;
|
||||
}
|
||||
|
||||
std::string sql = "INSERT INTO conversations (role, content, provider, model, tokens_used) "
|
||||
"VALUES ('" + role + "', '" + escapedContent + "', '" + provider + "', '" +
|
||||
model + "', " + std::to_string(tokensUsed) + ");";
|
||||
|
||||
return executeSQL(sql);
|
||||
}
|
||||
|
||||
bool StorageModule::updateDailyMetrics(int focusMinutes, int breaks, int hyperfocusCount) {
|
||||
if (!m_isConnected) return false;
|
||||
|
||||
std::time_t now = std::time(nullptr);
|
||||
std::tm* tm = std::localtime(&now);
|
||||
char dateStr[11];
|
||||
std::strftime(dateStr, sizeof(dateStr), "%Y-%m-%d", tm);
|
||||
|
||||
std::string sql = "INSERT INTO daily_metrics (date, total_focus_minutes, total_breaks, hyperfocus_count) "
|
||||
"VALUES ('" + std::string(dateStr) + "', " + std::to_string(focusMinutes) + ", " +
|
||||
std::to_string(breaks) + ", " + std::to_string(hyperfocusCount) + ") "
|
||||
"ON CONFLICT(date) DO UPDATE SET "
|
||||
"total_focus_minutes = total_focus_minutes + " + std::to_string(focusMinutes) + ", "
|
||||
"total_breaks = total_breaks + " + std::to_string(breaks) + ", "
|
||||
"hyperfocus_count = hyperfocus_count + " + std::to_string(hyperfocusCount) + ", "
|
||||
"updated_at = strftime('%s', 'now');";
|
||||
|
||||
return executeSQL(sql);
|
||||
void StorageModule::handleStorageError(const grove::IDataNode& data) {
|
||||
std::string message = data.getString("message", "Unknown error");
|
||||
m_pendingSaves--;
|
||||
m_logger->error("Storage error: {}", message);
|
||||
}
|
||||
|
||||
std::unique_ptr<grove::IDataNode> StorageModule::getHealthStatus() {
|
||||
auto status = std::make_unique<grove::JsonDataNode>("status");
|
||||
status->setString("status", m_isConnected ? "connected" : "disconnected");
|
||||
status->setString("database", m_dbPath);
|
||||
status->setInt("totalQueries", m_totalQueries);
|
||||
status->setString("status", "running");
|
||||
status->setInt("lastSessionId", m_lastSessionId);
|
||||
status->setInt("pendingSaves", m_pendingSaves);
|
||||
status->setInt("totalSaved", m_totalSaved);
|
||||
return status;
|
||||
}
|
||||
|
||||
void StorageModule::shutdown() {
|
||||
closeDatabase();
|
||||
m_logger->info("StorageModule arrete. Total queries: {}", m_totalQueries);
|
||||
m_logger->info("StorageModule arrete. Total saved: {}", m_totalSaved);
|
||||
}
|
||||
|
||||
std::unique_ptr<grove::IDataNode> StorageModule::getState() {
|
||||
auto state = std::make_unique<grove::JsonDataNode>("state");
|
||||
state->setString("dbPath", m_dbPath);
|
||||
state->setBool("isConnected", m_isConnected);
|
||||
state->setInt("totalQueries", m_totalQueries);
|
||||
state->setInt("lastSessionId", m_lastSessionId);
|
||||
state->setInt("totalSaved", m_totalSaved);
|
||||
return state;
|
||||
}
|
||||
|
||||
void StorageModule::setState(const grove::IDataNode& state) {
|
||||
m_totalQueries = state.getInt("totalQueries", 0);
|
||||
m_lastSessionId = state.getInt("lastSessionId", 0);
|
||||
m_logger->info("Etat restore: queries={}, lastSession={}", m_totalQueries, m_lastSessionId);
|
||||
m_totalSaved = state.getInt("totalSaved", 0);
|
||||
m_logger->info("Etat restore: lastSession={}, saved={}", m_lastSessionId, m_totalSaved);
|
||||
}
|
||||
|
||||
} // namespace aissia
|
||||
|
||||
@ -8,34 +8,30 @@
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
struct sqlite3;
|
||||
|
||||
namespace aissia {
|
||||
|
||||
/**
|
||||
* @brief Storage Module - SQLite persistence locale
|
||||
* @brief Storage Module - Pure business logic for data persistence
|
||||
*
|
||||
* Fonctionnalites:
|
||||
* - Persistance des sessions de travail
|
||||
* - Stockage des conversations IA
|
||||
* - Metriques journalieres
|
||||
* - Historique d'utilisation des apps
|
||||
* Handles data persistence logic without direct database access.
|
||||
* Communicates with StorageService via IIO pub/sub.
|
||||
*
|
||||
* Publie sur:
|
||||
* - "storage:ready" : DB initialisee
|
||||
* - "storage:error" : Erreur DB
|
||||
* - "storage:query_result" : Resultat de requete
|
||||
* - "storage:save_session" : Save work session
|
||||
* - "storage:save_app_usage" : Save app usage record
|
||||
* - "storage:save_conversation" : Save conversation
|
||||
* - "storage:update_metrics" : Update daily metrics
|
||||
*
|
||||
* Souscrit a:
|
||||
* - "storage:save_session" : Sauvegarder session
|
||||
* - "storage:save_conversation" : Sauvegarder conversation
|
||||
* - "scheduler:task_completed" : Logger completion tache
|
||||
* - "monitoring:app_changed" : Logger changement app
|
||||
* - "scheduler:task_completed" : Log task completion
|
||||
* - "monitoring:app_changed" : Log app change
|
||||
* - "storage:session_saved" : Session saved confirmation
|
||||
* - "storage:error" : Storage error
|
||||
*/
|
||||
class StorageModule : public grove::IModule {
|
||||
public:
|
||||
StorageModule();
|
||||
~StorageModule() override;
|
||||
~StorageModule() override = default;
|
||||
|
||||
// IModule interface
|
||||
void process(const grove::IDataNode& input) override;
|
||||
@ -48,26 +44,13 @@ public:
|
||||
void setState(const grove::IDataNode& state) override;
|
||||
std::string getType() const override { return "StorageModule"; }
|
||||
bool isIdle() const override { return true; }
|
||||
int getVersion() const override { return 1; }
|
||||
|
||||
// Public API for other modules
|
||||
bool saveWorkSession(const std::string& taskName, int durationMinutes, bool hyperfocusDetected);
|
||||
bool saveAppUsage(int sessionId, const std::string& appName, int durationSeconds, bool productive);
|
||||
bool saveConversation(const std::string& role, const std::string& content,
|
||||
const std::string& provider, const std::string& model, int tokensUsed);
|
||||
bool updateDailyMetrics(int focusMinutes, int breaks, int hyperfocusCount);
|
||||
int getVersion() const override { return 2; }
|
||||
|
||||
private:
|
||||
// Configuration
|
||||
std::string m_dbPath = "./data/aissia.db";
|
||||
std::string m_journalMode = "WAL";
|
||||
int m_busyTimeoutMs = 5000;
|
||||
|
||||
// State
|
||||
sqlite3* m_db = nullptr;
|
||||
bool m_isConnected = false;
|
||||
int m_totalQueries = 0;
|
||||
int m_lastSessionId = 0;
|
||||
int m_pendingSaves = 0;
|
||||
int m_totalSaved = 0;
|
||||
|
||||
// Services
|
||||
grove::IIO* m_io = nullptr;
|
||||
@ -75,11 +58,11 @@ private:
|
||||
std::shared_ptr<spdlog::logger> m_logger;
|
||||
|
||||
// Helpers
|
||||
bool openDatabase();
|
||||
void closeDatabase();
|
||||
bool initializeSchema();
|
||||
bool executeSQL(const std::string& sql);
|
||||
void processMessages();
|
||||
void handleTaskCompleted(const grove::IDataNode& data);
|
||||
void handleAppChanged(const grove::IDataNode& data);
|
||||
void handleSessionSaved(const grove::IDataNode& data);
|
||||
void handleStorageError(const grove::IDataNode& data);
|
||||
};
|
||||
|
||||
} // namespace aissia
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
#include "VoiceModule.h"
|
||||
#include <grove/JsonDataNode.h>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace aissia {
|
||||
|
||||
@ -22,47 +21,28 @@ void VoiceModule::setConfiguration(const grove::IDataNode& configNode,
|
||||
auto* ttsNode = configNode.getChildReadOnly("tts");
|
||||
if (ttsNode) {
|
||||
m_ttsEnabled = ttsNode->getBool("enabled", true);
|
||||
m_ttsRate = ttsNode->getInt("rate", 0);
|
||||
m_ttsVolume = ttsNode->getInt("volume", 80);
|
||||
}
|
||||
|
||||
// STT config
|
||||
auto* sttNode = configNode.getChildReadOnly("stt");
|
||||
std::string sttApiKey;
|
||||
if (sttNode) {
|
||||
m_sttEnabled = sttNode->getBool("enabled", true);
|
||||
m_language = sttNode->getString("language", "fr");
|
||||
std::string apiKeyEnv = sttNode->getString("api_key_env", "OPENAI_API_KEY");
|
||||
const char* key = std::getenv(apiKeyEnv.c_str());
|
||||
if (key) sttApiKey = key;
|
||||
}
|
||||
|
||||
// Create TTS engine
|
||||
m_ttsEngine = TTSEngineFactory::create();
|
||||
if (m_ttsEngine && m_ttsEngine->isAvailable()) {
|
||||
m_ttsEngine->setRate(m_ttsRate);
|
||||
m_ttsEngine->setVolume(m_ttsVolume);
|
||||
}
|
||||
|
||||
// Create STT engine
|
||||
m_sttEngine = STTEngineFactory::create(sttApiKey);
|
||||
if (m_sttEngine) {
|
||||
m_sttEngine->setLanguage(m_language);
|
||||
}
|
||||
|
||||
// Subscribe to topics
|
||||
if (m_io) {
|
||||
grove::SubscriptionConfig subConfig;
|
||||
m_io->subscribe("voice:speak", subConfig);
|
||||
m_io->subscribe("voice:listen", subConfig);
|
||||
m_io->subscribe("ai:response", subConfig);
|
||||
m_io->subscribe("ai:suggestion", subConfig);
|
||||
m_io->subscribe("notification:speak", subConfig);
|
||||
m_io->subscribe("voice:speaking_started", subConfig);
|
||||
m_io->subscribe("voice:speaking_ended", subConfig);
|
||||
m_io->subscribe("voice:transcription", subConfig);
|
||||
}
|
||||
|
||||
m_logger->info("VoiceModule configure: TTS={} ({}), STT={} ({})",
|
||||
m_ttsEnabled, m_ttsEngine ? m_ttsEngine->getEngineName() : "none",
|
||||
m_sttEnabled, m_sttEngine ? m_sttEngine->getEngineName() : "none");
|
||||
m_logger->info("VoiceModule configure (v2 - sans infrastructure): TTS={}, STT={}",
|
||||
m_ttsEnabled, m_sttEnabled);
|
||||
}
|
||||
|
||||
const grove::IDataNode& VoiceModule::getConfiguration() {
|
||||
@ -71,7 +51,6 @@ const grove::IDataNode& VoiceModule::getConfiguration() {
|
||||
|
||||
void VoiceModule::process(const grove::IDataNode& input) {
|
||||
processMessages();
|
||||
processSpeakQueue();
|
||||
}
|
||||
|
||||
void VoiceModule::processMessages() {
|
||||
@ -80,64 +59,37 @@ void VoiceModule::processMessages() {
|
||||
while (m_io->hasMessages() > 0) {
|
||||
auto msg = m_io->pullMessage();
|
||||
|
||||
if (msg.topic == "voice:speak" && msg.data) {
|
||||
handleSpeakRequest(*msg.data);
|
||||
}
|
||||
else if (msg.topic == "ai:response" && msg.data) {
|
||||
if (msg.topic == "ai:response" && msg.data) {
|
||||
handleAIResponse(*msg.data);
|
||||
}
|
||||
else if (msg.topic == "ai:suggestion" && msg.data) {
|
||||
handleSuggestion(*msg.data);
|
||||
}
|
||||
else if (msg.topic == "notification:speak" && msg.data) {
|
||||
std::string text = msg.data->getString("message", "");
|
||||
if (!text.empty()) {
|
||||
m_speakQueue.push(text);
|
||||
}
|
||||
handleNotificationSpeak(*msg.data);
|
||||
}
|
||||
else if (msg.topic == "voice:speaking_started" && msg.data) {
|
||||
handleSpeakingStarted(*msg.data);
|
||||
}
|
||||
else if (msg.topic == "voice:speaking_ended" && msg.data) {
|
||||
handleSpeakingEnded(*msg.data);
|
||||
}
|
||||
else if (msg.topic == "voice:transcription" && msg.data) {
|
||||
handleTranscription(*msg.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VoiceModule::processSpeakQueue() {
|
||||
if (!m_ttsEnabled || !m_ttsEngine || m_speakQueue.empty()) return;
|
||||
void VoiceModule::requestSpeak(const std::string& text, bool priority) {
|
||||
if (!m_io || !m_ttsEnabled || text.empty()) return;
|
||||
|
||||
// Only speak if not currently speaking
|
||||
if (!m_ttsEngine->isSpeaking() && !m_speakQueue.empty()) {
|
||||
std::string text = m_speakQueue.front();
|
||||
m_speakQueue.pop();
|
||||
speak(text);
|
||||
}
|
||||
}
|
||||
auto request = std::make_unique<grove::JsonDataNode>("speak");
|
||||
request->setString("text", text);
|
||||
request->setBool("priority", priority);
|
||||
m_io->publish("voice:speak", std::move(request));
|
||||
|
||||
void VoiceModule::speak(const std::string& text) {
|
||||
if (!m_ttsEngine || !m_ttsEnabled) return;
|
||||
|
||||
// Publish speaking started
|
||||
if (m_io) {
|
||||
auto event = std::make_unique<grove::JsonDataNode>("event");
|
||||
event->setString("text", text.substr(0, 100));
|
||||
m_io->publish("voice:speaking_started", std::move(event));
|
||||
}
|
||||
|
||||
m_ttsEngine->speak(text, true);
|
||||
m_totalSpoken++;
|
||||
|
||||
m_logger->debug("Speaking: {}", text.substr(0, 50));
|
||||
}
|
||||
|
||||
void VoiceModule::handleSpeakRequest(const grove::IDataNode& data) {
|
||||
std::string text = data.getString("text", "");
|
||||
bool priority = data.getBool("priority", false);
|
||||
|
||||
if (text.empty()) return;
|
||||
|
||||
if (priority) {
|
||||
// Clear queue and speak immediately
|
||||
while (!m_speakQueue.empty()) m_speakQueue.pop();
|
||||
if (m_ttsEngine) m_ttsEngine->stop();
|
||||
}
|
||||
|
||||
m_speakQueue.push(text);
|
||||
m_logger->debug("Speak request: {} (priority={})",
|
||||
text.size() > 50 ? text.substr(0, 50) + "..." : text, priority);
|
||||
}
|
||||
|
||||
void VoiceModule::handleAIResponse(const grove::IDataNode& data) {
|
||||
@ -145,7 +97,7 @@ void VoiceModule::handleAIResponse(const grove::IDataNode& data) {
|
||||
|
||||
std::string text = data.getString("text", "");
|
||||
if (!text.empty()) {
|
||||
m_speakQueue.push(text);
|
||||
requestSpeak(text, false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -154,10 +106,47 @@ void VoiceModule::handleSuggestion(const grove::IDataNode& data) {
|
||||
|
||||
std::string message = data.getString("message", "");
|
||||
if (!message.empty()) {
|
||||
// Priority for suggestions
|
||||
if (m_ttsEngine) m_ttsEngine->stop();
|
||||
while (!m_speakQueue.empty()) m_speakQueue.pop();
|
||||
m_speakQueue.push(message);
|
||||
// Suggestions are priority messages
|
||||
requestSpeak(message, true);
|
||||
}
|
||||
}
|
||||
|
||||
void VoiceModule::handleNotificationSpeak(const grove::IDataNode& data) {
|
||||
if (!m_ttsEnabled) return;
|
||||
|
||||
std::string message = data.getString("message", "");
|
||||
if (!message.empty()) {
|
||||
requestSpeak(message, false);
|
||||
}
|
||||
}
|
||||
|
||||
void VoiceModule::handleSpeakingStarted(const grove::IDataNode& data) {
|
||||
m_isSpeaking = true;
|
||||
m_totalSpoken++;
|
||||
std::string text = data.getString("text", "");
|
||||
m_logger->debug("Speaking started: {}", text.size() > 30 ? text.substr(0, 30) + "..." : text);
|
||||
}
|
||||
|
||||
void VoiceModule::handleSpeakingEnded(const grove::IDataNode& data) {
|
||||
m_isSpeaking = false;
|
||||
m_logger->debug("Speaking ended");
|
||||
}
|
||||
|
||||
void VoiceModule::handleTranscription(const grove::IDataNode& data) {
|
||||
std::string text = data.getString("text", "");
|
||||
float confidence = data.getDouble("confidence", 0.0);
|
||||
|
||||
if (!text.empty()) {
|
||||
m_totalTranscribed++;
|
||||
m_logger->info("Transcription: {} (conf={:.2f})", text, confidence);
|
||||
|
||||
// Forward to AI module
|
||||
if (m_io) {
|
||||
auto event = std::make_unique<grove::JsonDataNode>("query");
|
||||
event->setString("query", text);
|
||||
event->setString("source", "voice");
|
||||
m_io->publish("ai:query", std::move(event));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -166,25 +155,21 @@ std::unique_ptr<grove::IDataNode> VoiceModule::getHealthStatus() {
|
||||
status->setString("status", "running");
|
||||
status->setBool("ttsEnabled", m_ttsEnabled);
|
||||
status->setBool("sttEnabled", m_sttEnabled);
|
||||
status->setString("ttsEngine", m_ttsEngine ? m_ttsEngine->getEngineName() : "none");
|
||||
status->setString("sttEngine", m_sttEngine ? m_sttEngine->getEngineName() : "none");
|
||||
status->setInt("queueSize", m_speakQueue.size());
|
||||
status->setBool("isSpeaking", m_isSpeaking);
|
||||
status->setInt("totalSpoken", m_totalSpoken);
|
||||
status->setInt("totalTranscribed", m_totalTranscribed);
|
||||
return status;
|
||||
}
|
||||
|
||||
void VoiceModule::shutdown() {
|
||||
if (m_ttsEngine) {
|
||||
m_ttsEngine->stop();
|
||||
}
|
||||
m_logger->info("VoiceModule arrete. Total spoken: {}", m_totalSpoken);
|
||||
m_logger->info("VoiceModule arrete. Spoken: {}, Transcribed: {}",
|
||||
m_totalSpoken, m_totalTranscribed);
|
||||
}
|
||||
|
||||
std::unique_ptr<grove::IDataNode> VoiceModule::getState() {
|
||||
auto state = std::make_unique<grove::JsonDataNode>("state");
|
||||
state->setInt("totalSpoken", m_totalSpoken);
|
||||
state->setInt("totalTranscribed", m_totalTranscribed);
|
||||
state->setInt("queueSize", m_speakQueue.size());
|
||||
return state;
|
||||
}
|
||||
|
||||
|
||||
@ -2,8 +2,6 @@
|
||||
|
||||
#include <grove/IModule.h>
|
||||
#include <grove/JsonDataNode.h>
|
||||
#include "../shared/audio/ITTSEngine.hpp"
|
||||
#include "../shared/audio/ISTTEngine.hpp"
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <spdlog/sinks/stdout_color_sinks.h>
|
||||
@ -14,25 +12,22 @@
|
||||
namespace aissia {
|
||||
|
||||
/**
|
||||
* @brief Voice Module - TTS and STT coordination
|
||||
* @brief Voice Module - Voice interaction logic only
|
||||
*
|
||||
* Fonctionnalites:
|
||||
* - Text-to-Speech via SAPI (Windows) ou espeak (Linux)
|
||||
* - Speech-to-Text via OpenAI Whisper API
|
||||
* - File d'attente de messages a parler
|
||||
* - Integration avec les autres modules
|
||||
* Handles voice interaction logic without platform-specific TTS/STT.
|
||||
* Communicates with VoiceService via IIO pub/sub.
|
||||
*
|
||||
* Publie sur:
|
||||
* - "voice:transcription" : Texte transcrit (STT)
|
||||
* - "voice:speaking_started" : TTS commence
|
||||
* - "voice:speaking_ended" : TTS termine
|
||||
* - "voice:speak" : Request TTS
|
||||
* - "voice:listen" : Request STT
|
||||
*
|
||||
* Souscrit a:
|
||||
* - "voice:speak" : Demande TTS
|
||||
* - "voice:listen" : Demande STT
|
||||
* - "ai:response" : Reponse IA a lire
|
||||
* - "notification:speak" : Notification a lire
|
||||
* - "ai:suggestion" : Suggestion a lire
|
||||
* - "ai:response" : AI response to speak
|
||||
* - "ai:suggestion" : AI suggestion to speak (priority)
|
||||
* - "notification:speak" : Notification to speak
|
||||
* - "voice:speaking_started" : TTS started
|
||||
* - "voice:speaking_ended" : TTS ended
|
||||
* - "voice:transcription" : STT result
|
||||
*/
|
||||
class VoiceModule : public grove::IModule {
|
||||
public:
|
||||
@ -49,21 +44,17 @@ public:
|
||||
std::unique_ptr<grove::IDataNode> getState() override;
|
||||
void setState(const grove::IDataNode& state) override;
|
||||
std::string getType() const override { return "VoiceModule"; }
|
||||
bool isIdle() const override { return m_speakQueue.empty(); }
|
||||
int getVersion() const override { return 1; }
|
||||
bool isIdle() const override { return !m_isSpeaking; }
|
||||
int getVersion() const override { return 2; }
|
||||
|
||||
private:
|
||||
// Configuration
|
||||
bool m_ttsEnabled = true;
|
||||
bool m_sttEnabled = true;
|
||||
int m_ttsRate = 0;
|
||||
int m_ttsVolume = 80;
|
||||
std::string m_language = "fr";
|
||||
|
||||
// State
|
||||
std::unique_ptr<ITTSEngine> m_ttsEngine;
|
||||
std::unique_ptr<ISTTEngine> m_sttEngine;
|
||||
std::queue<std::string> m_speakQueue;
|
||||
bool m_isSpeaking = false;
|
||||
int m_totalSpoken = 0;
|
||||
int m_totalTranscribed = 0;
|
||||
|
||||
@ -74,11 +65,13 @@ private:
|
||||
|
||||
// Helpers
|
||||
void processMessages();
|
||||
void processSpeakQueue();
|
||||
void speak(const std::string& text);
|
||||
void handleSpeakRequest(const grove::IDataNode& data);
|
||||
void requestSpeak(const std::string& text, bool priority = false);
|
||||
void handleAIResponse(const grove::IDataNode& data);
|
||||
void handleSuggestion(const grove::IDataNode& data);
|
||||
void handleNotificationSpeak(const grove::IDataNode& data);
|
||||
void handleSpeakingStarted(const grove::IDataNode& data);
|
||||
void handleSpeakingEnded(const grove::IDataNode& data);
|
||||
void handleTranscription(const grove::IDataNode& data);
|
||||
};
|
||||
|
||||
} // namespace aissia
|
||||
|
||||
39
src/services/IService.hpp
Normal file
39
src/services/IService.hpp
Normal file
@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include <grove/IIO.h>
|
||||
#include <string>
|
||||
|
||||
namespace aissia {
|
||||
|
||||
/**
|
||||
* @brief Interface for infrastructure services
|
||||
*
|
||||
* Services handle non-hot-reloadable infrastructure:
|
||||
* - LLM HTTP calls
|
||||
* - SQLite database
|
||||
* - Platform APIs (Win32/X11)
|
||||
* - TTS/STT engines
|
||||
*
|
||||
* Services communicate with modules via IIO pub/sub.
|
||||
*/
|
||||
class IService {
|
||||
public:
|
||||
virtual ~IService() = default;
|
||||
|
||||
/// Initialize the service with IIO for pub/sub
|
||||
virtual bool initialize(grove::IIO* io) = 0;
|
||||
|
||||
/// Process pending work (called each frame from main loop)
|
||||
virtual void process() = 0;
|
||||
|
||||
/// Clean shutdown
|
||||
virtual void shutdown() = 0;
|
||||
|
||||
/// Service name for logging
|
||||
virtual std::string getName() const = 0;
|
||||
|
||||
/// Check if service is healthy
|
||||
virtual bool isHealthy() const = 0;
|
||||
};
|
||||
|
||||
} // namespace aissia
|
||||
270
src/services/LLMService.cpp
Normal file
270
src/services/LLMService.cpp
Normal file
@ -0,0 +1,270 @@
|
||||
#include "LLMService.hpp"
|
||||
#include "../shared/llm/LLMProviderFactory.hpp"
|
||||
|
||||
#include <spdlog/sinks/stdout_color_sinks.h>
|
||||
#include <fstream>
|
||||
|
||||
namespace aissia {
|
||||
|
||||
LLMService::LLMService() {
|
||||
m_logger = spdlog::get("LLMService");
|
||||
if (!m_logger) {
|
||||
m_logger = spdlog::stdout_color_mt("LLMService");
|
||||
}
|
||||
}
|
||||
|
||||
LLMService::~LLMService() {
|
||||
shutdown();
|
||||
}
|
||||
|
||||
bool LLMService::initialize(grove::IIO* io) {
|
||||
m_io = io;
|
||||
|
||||
if (m_io) {
|
||||
grove::SubscriptionConfig config;
|
||||
m_io->subscribe("llm:request", config);
|
||||
}
|
||||
|
||||
// Start worker thread
|
||||
m_running = true;
|
||||
m_workerThread = std::thread(&LLMService::workerLoop, this);
|
||||
|
||||
m_logger->info("LLMService initialized");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LLMService::loadConfig(const std::string& configPath) {
|
||||
try {
|
||||
std::ifstream file(configPath);
|
||||
if (!file.is_open()) {
|
||||
m_logger->warn("Config file not found: {}", configPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
nlohmann::json config;
|
||||
file >> config;
|
||||
|
||||
m_provider = LLMProviderFactory::create(config);
|
||||
if (!m_provider) {
|
||||
m_logger->error("Failed to create LLM provider");
|
||||
return false;
|
||||
}
|
||||
|
||||
m_providerName = config.value("provider", "claude");
|
||||
m_maxIterations = config.value("max_iterations", 10);
|
||||
m_defaultSystemPrompt = config.value("system_prompt",
|
||||
"Tu es AISSIA, un assistant personnel intelligent.");
|
||||
|
||||
m_logger->info("LLM provider loaded: {} ({})", m_providerName, m_provider->getModel());
|
||||
return true;
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
m_logger->error("Failed to load config: {}", e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void LLMService::registerTool(const std::string& name, const std::string& description,
|
||||
const nlohmann::json& schema,
|
||||
std::function<nlohmann::json(const nlohmann::json&)> handler) {
|
||||
m_toolRegistry.registerTool(name, description, schema, handler);
|
||||
m_logger->debug("Tool registered: {}", name);
|
||||
}
|
||||
|
||||
void LLMService::process() {
|
||||
processIncomingMessages();
|
||||
publishResponses();
|
||||
}
|
||||
|
||||
void LLMService::processIncomingMessages() {
|
||||
if (!m_io) return;
|
||||
|
||||
while (m_io->hasMessages() > 0) {
|
||||
auto msg = m_io->pullMessage();
|
||||
|
||||
if (msg.topic == "llm:request" && msg.data) {
|
||||
Request req;
|
||||
req.query = msg.data->getString("query", "");
|
||||
req.systemPrompt = msg.data->getString("systemPrompt", m_defaultSystemPrompt);
|
||||
req.conversationId = msg.data->getString("conversationId", "default");
|
||||
req.maxIterations = msg.data->getInt("maxIterations", m_maxIterations);
|
||||
|
||||
// Get tools from message or use registered tools
|
||||
auto* toolsNode = msg.data->getChildReadOnly("tools");
|
||||
if (toolsNode) {
|
||||
// Custom tools from message
|
||||
// (would need to parse from IDataNode)
|
||||
}
|
||||
|
||||
if (!req.query.empty()) {
|
||||
std::lock_guard<std::mutex> lock(m_requestMutex);
|
||||
m_requestQueue.push(std::move(req));
|
||||
m_requestCV.notify_one();
|
||||
m_logger->debug("Request queued: {}", req.query.substr(0, 50));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LLMService::publishResponses() {
|
||||
if (!m_io) return;
|
||||
|
||||
std::lock_guard<std::mutex> lock(m_responseMutex);
|
||||
while (!m_responseQueue.empty()) {
|
||||
auto resp = std::move(m_responseQueue.front());
|
||||
m_responseQueue.pop();
|
||||
|
||||
if (resp.isError) {
|
||||
auto event = std::make_unique<grove::JsonDataNode>("error");
|
||||
event->setString("message", resp.text);
|
||||
event->setString("conversationId", resp.conversationId);
|
||||
m_io->publish("llm:error", std::move(event));
|
||||
} else {
|
||||
auto event = std::make_unique<grove::JsonDataNode>("response");
|
||||
event->setString("text", resp.text);
|
||||
event->setString("conversationId", resp.conversationId);
|
||||
event->setInt("tokens", resp.tokens);
|
||||
event->setInt("iterations", resp.iterations);
|
||||
m_io->publish("llm:response", std::move(event));
|
||||
|
||||
m_logger->info("Response published: {} chars", resp.text.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LLMService::workerLoop() {
|
||||
m_logger->debug("Worker thread started");
|
||||
|
||||
while (m_running) {
|
||||
Request req;
|
||||
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_requestMutex);
|
||||
m_requestCV.wait_for(lock, std::chrono::milliseconds(100), [this] {
|
||||
return !m_requestQueue.empty() || !m_running;
|
||||
});
|
||||
|
||||
if (!m_running) break;
|
||||
if (m_requestQueue.empty()) continue;
|
||||
|
||||
req = std::move(m_requestQueue.front());
|
||||
m_requestQueue.pop();
|
||||
}
|
||||
|
||||
// Process request (HTTP calls happen here)
|
||||
auto resp = processRequest(req);
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_responseMutex);
|
||||
m_responseQueue.push(std::move(resp));
|
||||
}
|
||||
}
|
||||
|
||||
m_logger->debug("Worker thread stopped");
|
||||
}
|
||||
|
||||
LLMService::Response LLMService::processRequest(const Request& request) {
|
||||
Response resp;
|
||||
resp.conversationId = request.conversationId;
|
||||
|
||||
if (!m_provider) {
|
||||
resp.text = "LLM provider not initialized";
|
||||
resp.isError = true;
|
||||
return resp;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get or create conversation history
|
||||
auto& history = m_conversations[request.conversationId];
|
||||
if (history.is_null()) {
|
||||
history = nlohmann::json::array();
|
||||
}
|
||||
|
||||
// Add user message
|
||||
history.push_back({{"role", "user"}, {"content", request.query}});
|
||||
|
||||
// Get tool definitions
|
||||
nlohmann::json tools = m_toolRegistry.getToolDefinitions();
|
||||
|
||||
// Run agentic loop
|
||||
auto result = agenticLoop(request.query, request.systemPrompt,
|
||||
history, tools, request.maxIterations);
|
||||
|
||||
if (result.contains("error")) {
|
||||
resp.text = result["error"].get<std::string>();
|
||||
resp.isError = true;
|
||||
} else {
|
||||
resp.text = result["response"].get<std::string>();
|
||||
resp.tokens = result.value("tokens", 0);
|
||||
resp.iterations = result.value("iterations", 1);
|
||||
|
||||
// Add assistant response to history
|
||||
history.push_back({{"role", "assistant"}, {"content", resp.text}});
|
||||
}
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
resp.text = e.what();
|
||||
resp.isError = true;
|
||||
}
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
nlohmann::json LLMService::agenticLoop(const std::string& query, const std::string& systemPrompt,
|
||||
nlohmann::json& messages, const nlohmann::json& tools,
|
||||
int maxIterations) {
|
||||
int totalTokens = 0;
|
||||
|
||||
for (int iteration = 0; iteration < maxIterations; iteration++) {
|
||||
m_logger->debug("Agentic loop iteration {}", iteration + 1);
|
||||
|
||||
auto response = m_provider->chat(systemPrompt, messages, tools);
|
||||
totalTokens += response.input_tokens + response.output_tokens;
|
||||
|
||||
if (response.is_end_turn) {
|
||||
return {
|
||||
{"response", response.text},
|
||||
{"iterations", iteration + 1},
|
||||
{"tokens", totalTokens}
|
||||
};
|
||||
}
|
||||
|
||||
// Execute tool calls
|
||||
if (!response.tool_calls.empty()) {
|
||||
std::vector<ToolResult> results;
|
||||
|
||||
for (const auto& call : response.tool_calls) {
|
||||
m_logger->debug("Executing tool: {}", call.name);
|
||||
nlohmann::json result = m_toolRegistry.execute(call.name, call.input);
|
||||
results.push_back({call.id, result.dump(), false});
|
||||
}
|
||||
|
||||
// Append assistant message and tool results
|
||||
m_provider->appendAssistantMessage(messages, response);
|
||||
auto toolResultsMsg = m_provider->formatToolResults(results);
|
||||
|
||||
if (toolResultsMsg.is_array()) {
|
||||
for (const auto& msg : toolResultsMsg) {
|
||||
messages.push_back(msg);
|
||||
}
|
||||
} else {
|
||||
messages.push_back(toolResultsMsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {{"error", "max_iterations_reached"}};
|
||||
}
|
||||
|
||||
void LLMService::shutdown() {
|
||||
m_running = false;
|
||||
m_requestCV.notify_all();
|
||||
|
||||
if (m_workerThread.joinable()) {
|
||||
m_workerThread.join();
|
||||
}
|
||||
|
||||
m_logger->info("LLMService shutdown");
|
||||
}
|
||||
|
||||
} // namespace aissia
|
||||
106
src/services/LLMService.hpp
Normal file
106
src/services/LLMService.hpp
Normal file
@ -0,0 +1,106 @@
|
||||
#pragma once
|
||||
|
||||
#include "IService.hpp"
|
||||
#include "../shared/llm/ILLMProvider.hpp"
|
||||
#include "../shared/llm/ToolRegistry.hpp"
|
||||
|
||||
#include <grove/IIO.h>
|
||||
#include <grove/JsonDataNode.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <queue>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
|
||||
namespace aissia {
|
||||
|
||||
/**
|
||||
* @brief LLM Service - Async HTTP calls to LLM providers
|
||||
*
|
||||
* Handles all LLM API calls in a background thread.
|
||||
* Modules communicate via IIO:
|
||||
*
|
||||
* Subscribes to:
|
||||
* - "llm:request" : { query, systemPrompt?, tools?, conversationId? }
|
||||
*
|
||||
* Publishes:
|
||||
* - "llm:response" : { text, conversationId, tokens, iterations }
|
||||
* - "llm:error" : { message, conversationId }
|
||||
* - "llm:thinking" : { conversationId } (during agentic loop)
|
||||
*/
|
||||
class LLMService : public IService {
|
||||
public:
|
||||
LLMService();
|
||||
~LLMService() override;
|
||||
|
||||
bool initialize(grove::IIO* io) override;
|
||||
void process() override;
|
||||
void shutdown() override;
|
||||
std::string getName() const override { return "LLMService"; }
|
||||
bool isHealthy() const override { return m_provider != nullptr; }
|
||||
|
||||
/// Load provider from config file
|
||||
bool loadConfig(const std::string& configPath);
|
||||
|
||||
/// Register a tool that can be called by the LLM
|
||||
void registerTool(const std::string& name, const std::string& description,
|
||||
const nlohmann::json& schema,
|
||||
std::function<nlohmann::json(const nlohmann::json&)> handler);
|
||||
|
||||
private:
|
||||
struct Request {
|
||||
std::string query;
|
||||
std::string systemPrompt;
|
||||
std::string conversationId;
|
||||
nlohmann::json tools;
|
||||
int maxIterations = 10;
|
||||
};
|
||||
|
||||
struct Response {
|
||||
std::string text;
|
||||
std::string conversationId;
|
||||
int tokens = 0;
|
||||
int iterations = 0;
|
||||
bool isError = false;
|
||||
};
|
||||
|
||||
// Configuration
|
||||
std::string m_providerName = "claude";
|
||||
std::string m_defaultSystemPrompt;
|
||||
int m_maxIterations = 10;
|
||||
|
||||
// State
|
||||
std::unique_ptr<ILLMProvider> m_provider;
|
||||
ToolRegistry m_toolRegistry;
|
||||
std::map<std::string, nlohmann::json> m_conversations; // conversationId -> history
|
||||
|
||||
// Threading
|
||||
std::thread m_workerThread;
|
||||
std::atomic<bool> m_running{false};
|
||||
std::queue<Request> m_requestQueue;
|
||||
std::queue<Response> m_responseQueue;
|
||||
std::mutex m_requestMutex;
|
||||
std::mutex m_responseMutex;
|
||||
std::condition_variable m_requestCV;
|
||||
|
||||
// Services
|
||||
grove::IIO* m_io = nullptr;
|
||||
std::shared_ptr<spdlog::logger> m_logger;
|
||||
|
||||
// Worker thread
|
||||
void workerLoop();
|
||||
Response processRequest(const Request& request);
|
||||
nlohmann::json agenticLoop(const std::string& query, const std::string& systemPrompt,
|
||||
nlohmann::json& messages, const nlohmann::json& tools,
|
||||
int maxIterations);
|
||||
|
||||
// Message handling
|
||||
void processIncomingMessages();
|
||||
void publishResponses();
|
||||
};
|
||||
|
||||
} // namespace aissia
|
||||
139
src/services/PlatformService.cpp
Normal file
139
src/services/PlatformService.cpp
Normal file
@ -0,0 +1,139 @@
|
||||
#include "PlatformService.hpp"
|
||||
|
||||
#include <spdlog/sinks/stdout_color_sinks.h>
|
||||
|
||||
namespace aissia {
|
||||
|
||||
PlatformService::PlatformService() {
|
||||
m_logger = spdlog::get("PlatformService");
|
||||
if (!m_logger) {
|
||||
m_logger = spdlog::stdout_color_mt("PlatformService");
|
||||
}
|
||||
}
|
||||
|
||||
bool PlatformService::initialize(grove::IIO* io) {
|
||||
m_io = io;
|
||||
|
||||
// Create platform-specific window tracker
|
||||
m_tracker = WindowTrackerFactory::create();
|
||||
|
||||
if (!m_tracker || !m_tracker->isAvailable()) {
|
||||
m_logger->warn("Window tracker not available on this platform");
|
||||
return true; // Non-fatal, module can work without tracking
|
||||
}
|
||||
|
||||
if (m_io) {
|
||||
grove::SubscriptionConfig config;
|
||||
m_io->subscribe("platform:query_window", config);
|
||||
}
|
||||
|
||||
m_logger->info("PlatformService initialized: {}", m_tracker->getPlatformName());
|
||||
return true;
|
||||
}
|
||||
|
||||
void PlatformService::configure(int pollIntervalMs, int idleThresholdSeconds) {
|
||||
m_pollIntervalMs = pollIntervalMs;
|
||||
m_idleThresholdSeconds = idleThresholdSeconds;
|
||||
m_logger->debug("Configured: poll={}ms, idle={}s", pollIntervalMs, idleThresholdSeconds);
|
||||
}
|
||||
|
||||
void PlatformService::process() {
|
||||
if (!m_tracker || !m_tracker->isAvailable()) return;
|
||||
|
||||
// Use monotonic clock for timing
|
||||
static auto startTime = std::chrono::steady_clock::now();
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
float currentTime = std::chrono::duration<float>(now - startTime).count();
|
||||
|
||||
float pollIntervalSec = m_pollIntervalMs / 1000.0f;
|
||||
if (currentTime - m_lastPollTime >= pollIntervalSec) {
|
||||
m_lastPollTime = currentTime;
|
||||
pollWindowInfo(currentTime);
|
||||
}
|
||||
|
||||
// Handle query requests
|
||||
if (m_io) {
|
||||
while (m_io->hasMessages() > 0) {
|
||||
auto msg = m_io->pullMessage();
|
||||
if (msg.topic == "platform:query_window") {
|
||||
publishWindowInfo();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PlatformService::pollWindowInfo(float currentTime) {
|
||||
std::string newApp = m_tracker->getCurrentAppName();
|
||||
std::string newTitle = m_tracker->getCurrentWindowTitle();
|
||||
|
||||
// Check for app change
|
||||
if (newApp != m_currentApp) {
|
||||
int duration = static_cast<int>(currentTime - m_appStartTime);
|
||||
|
||||
if (!m_currentApp.empty() && duration > 0) {
|
||||
publishWindowChanged(m_currentApp, newApp, duration);
|
||||
}
|
||||
|
||||
m_currentApp = newApp;
|
||||
m_currentWindowTitle = newTitle;
|
||||
m_appStartTime = currentTime;
|
||||
|
||||
m_logger->debug("App: {} - {}", m_currentApp,
|
||||
m_currentWindowTitle.size() > 50 ?
|
||||
m_currentWindowTitle.substr(0, 50) + "..." : m_currentWindowTitle);
|
||||
}
|
||||
|
||||
// Check idle state
|
||||
bool isIdle = m_tracker->isUserIdle(m_idleThresholdSeconds);
|
||||
|
||||
if (isIdle && !m_wasIdle) {
|
||||
m_logger->info("User idle detected ({}s)", m_idleThresholdSeconds);
|
||||
if (m_io) {
|
||||
auto event = std::make_unique<grove::JsonDataNode>("idle");
|
||||
event->setInt("idleSeconds", m_tracker->getIdleTimeSeconds());
|
||||
m_io->publish("platform:idle_detected", std::move(event));
|
||||
}
|
||||
}
|
||||
else if (!isIdle && m_wasIdle) {
|
||||
m_logger->info("User activity resumed");
|
||||
if (m_io) {
|
||||
auto event = std::make_unique<grove::JsonDataNode>("active");
|
||||
m_io->publish("platform:activity_resumed", std::move(event));
|
||||
}
|
||||
}
|
||||
|
||||
m_wasIdle = isIdle;
|
||||
|
||||
// Publish periodic window info
|
||||
publishWindowInfo();
|
||||
}
|
||||
|
||||
void PlatformService::publishWindowInfo() {
|
||||
if (!m_io || !m_tracker) return;
|
||||
|
||||
auto event = std::make_unique<grove::JsonDataNode>("window");
|
||||
event->setString("appName", m_currentApp);
|
||||
event->setString("windowTitle", m_currentWindowTitle);
|
||||
event->setBool("isIdle", m_wasIdle);
|
||||
event->setInt("idleSeconds", m_tracker->getIdleTimeSeconds());
|
||||
m_io->publish("platform:window_info", std::move(event));
|
||||
}
|
||||
|
||||
void PlatformService::publishWindowChanged(const std::string& oldApp,
|
||||
const std::string& newApp,
|
||||
int duration) {
|
||||
if (!m_io) return;
|
||||
|
||||
auto event = std::make_unique<grove::JsonDataNode>("changed");
|
||||
event->setString("oldApp", oldApp);
|
||||
event->setString("newApp", newApp);
|
||||
event->setInt("duration", duration);
|
||||
m_io->publish("platform:window_changed", std::move(event));
|
||||
}
|
||||
|
||||
void PlatformService::shutdown() {
|
||||
m_tracker.reset();
|
||||
m_logger->info("PlatformService shutdown");
|
||||
}
|
||||
|
||||
} // namespace aissia
|
||||
67
src/services/PlatformService.hpp
Normal file
67
src/services/PlatformService.hpp
Normal file
@ -0,0 +1,67 @@
|
||||
#pragma once
|
||||
|
||||
#include "IService.hpp"
|
||||
#include "../shared/platform/IWindowTracker.hpp"
|
||||
|
||||
#include <grove/IIO.h>
|
||||
#include <grove/JsonDataNode.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace aissia {
|
||||
|
||||
/**
|
||||
* @brief Platform Service - OS-specific APIs (window tracking, etc.)
|
||||
*
|
||||
* Handles platform-specific operations that can't be in hot-reload modules.
|
||||
* Polls foreground window at configurable interval.
|
||||
*
|
||||
* Subscribes to:
|
||||
* - "platform:query_window" : Request current window info
|
||||
*
|
||||
* Publishes:
|
||||
* - "platform:window_info" : { appName, windowTitle, isIdle, idleSeconds }
|
||||
* - "platform:window_changed" : { oldApp, newApp, duration }
|
||||
* - "platform:idle_detected" : { idleSeconds }
|
||||
* - "platform:activity_resumed" : {}
|
||||
*/
|
||||
class PlatformService : public IService {
|
||||
public:
|
||||
PlatformService();
|
||||
~PlatformService() override = default;
|
||||
|
||||
bool initialize(grove::IIO* io) override;
|
||||
void process() override;
|
||||
void shutdown() override;
|
||||
std::string getName() const override { return "PlatformService"; }
|
||||
bool isHealthy() const override { return m_tracker != nullptr && m_tracker->isAvailable(); }
|
||||
|
||||
/// Configure polling interval and idle threshold
|
||||
void configure(int pollIntervalMs = 1000, int idleThresholdSeconds = 300);
|
||||
|
||||
private:
|
||||
// Configuration
|
||||
int m_pollIntervalMs = 1000;
|
||||
int m_idleThresholdSeconds = 300;
|
||||
|
||||
// State
|
||||
std::unique_ptr<IWindowTracker> m_tracker;
|
||||
std::string m_currentApp;
|
||||
std::string m_currentWindowTitle;
|
||||
float m_appStartTime = 0.0f;
|
||||
float m_lastPollTime = 0.0f;
|
||||
bool m_wasIdle = false;
|
||||
|
||||
// Services
|
||||
grove::IIO* m_io = nullptr;
|
||||
std::shared_ptr<spdlog::logger> m_logger;
|
||||
|
||||
// Helpers
|
||||
void pollWindowInfo(float currentTime);
|
||||
void publishWindowInfo();
|
||||
void publishWindowChanged(const std::string& oldApp, const std::string& newApp, int duration);
|
||||
};
|
||||
|
||||
} // namespace aissia
|
||||
348
src/services/StorageService.cpp
Normal file
348
src/services/StorageService.cpp
Normal file
@ -0,0 +1,348 @@
|
||||
#include "StorageService.hpp"
|
||||
|
||||
#include <spdlog/sinks/stdout_color_sinks.h>
|
||||
#include <sqlite3.h>
|
||||
#include <filesystem>
|
||||
#include <ctime>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace aissia {
|
||||
|
||||
StorageService::StorageService() {
|
||||
m_logger = spdlog::get("StorageService");
|
||||
if (!m_logger) {
|
||||
m_logger = spdlog::stdout_color_mt("StorageService");
|
||||
}
|
||||
}
|
||||
|
||||
StorageService::~StorageService() {
|
||||
shutdown();
|
||||
}
|
||||
|
||||
bool StorageService::initialize(grove::IIO* io) {
|
||||
m_io = io;
|
||||
|
||||
if (m_io) {
|
||||
grove::SubscriptionConfig config;
|
||||
m_io->subscribe("storage:save_session", config);
|
||||
m_io->subscribe("storage:save_app_usage", config);
|
||||
m_io->subscribe("storage:save_conversation", config);
|
||||
m_io->subscribe("storage:update_metrics", config);
|
||||
}
|
||||
|
||||
m_logger->info("StorageService initialized");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool StorageService::openDatabase(const std::string& dbPath,
|
||||
const std::string& journalMode,
|
||||
int busyTimeoutMs) {
|
||||
m_dbPath = dbPath;
|
||||
|
||||
// Ensure directory exists
|
||||
fs::path path(dbPath);
|
||||
if (path.has_parent_path()) {
|
||||
fs::create_directories(path.parent_path());
|
||||
}
|
||||
|
||||
int rc = sqlite3_open(dbPath.c_str(), &m_db);
|
||||
if (rc != SQLITE_OK) {
|
||||
m_logger->error("SQLite open error: {}", sqlite3_errmsg(m_db));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set pragmas
|
||||
std::string pragmas = "PRAGMA journal_mode=" + journalMode + ";"
|
||||
"PRAGMA busy_timeout=" + std::to_string(busyTimeoutMs) + ";"
|
||||
"PRAGMA foreign_keys=ON;";
|
||||
if (!executeSQL(pragmas)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!initializeSchema()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!prepareStatements()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_isConnected = true;
|
||||
|
||||
// Publish ready event
|
||||
if (m_io) {
|
||||
auto event = std::make_unique<grove::JsonDataNode>("ready");
|
||||
event->setString("database", dbPath);
|
||||
m_io->publish("storage:ready", std::move(event));
|
||||
}
|
||||
|
||||
m_logger->info("Database opened: {}", dbPath);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool StorageService::initializeSchema() {
|
||||
const char* schema = R"SQL(
|
||||
CREATE TABLE IF NOT EXISTS work_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
task_name TEXT,
|
||||
start_time INTEGER,
|
||||
end_time INTEGER,
|
||||
duration_minutes INTEGER,
|
||||
hyperfocus_detected BOOLEAN DEFAULT 0,
|
||||
created_at INTEGER DEFAULT (strftime('%s', 'now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS app_usage (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_id INTEGER,
|
||||
app_name TEXT,
|
||||
duration_seconds INTEGER,
|
||||
is_productive BOOLEAN,
|
||||
created_at INTEGER DEFAULT (strftime('%s', 'now')),
|
||||
FOREIGN KEY (session_id) REFERENCES work_sessions(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS conversations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
role TEXT,
|
||||
content TEXT,
|
||||
provider TEXT,
|
||||
model TEXT,
|
||||
tokens_used INTEGER,
|
||||
created_at INTEGER DEFAULT (strftime('%s', 'now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS daily_metrics (
|
||||
date TEXT PRIMARY KEY,
|
||||
total_focus_minutes INTEGER DEFAULT 0,
|
||||
total_breaks INTEGER DEFAULT 0,
|
||||
hyperfocus_count INTEGER DEFAULT 0,
|
||||
updated_at INTEGER DEFAULT (strftime('%s', 'now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_date ON work_sessions(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_app_usage_session ON app_usage(session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_conversations_date ON conversations(created_at);
|
||||
)SQL";
|
||||
|
||||
return executeSQL(schema);
|
||||
}
|
||||
|
||||
bool StorageService::prepareStatements() {
|
||||
int rc;
|
||||
|
||||
// Save session statement
|
||||
const char* sqlSession = "INSERT INTO work_sessions "
|
||||
"(task_name, start_time, end_time, duration_minutes, hyperfocus_detected) "
|
||||
"VALUES (?, ?, ?, ?, ?)";
|
||||
rc = sqlite3_prepare_v2(m_db, sqlSession, -1, &m_stmtSaveSession, nullptr);
|
||||
if (rc != SQLITE_OK) {
|
||||
m_logger->error("Failed to prepare save_session: {}", sqlite3_errmsg(m_db));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Save app usage statement
|
||||
const char* sqlAppUsage = "INSERT INTO app_usage "
|
||||
"(session_id, app_name, duration_seconds, is_productive) "
|
||||
"VALUES (?, ?, ?, ?)";
|
||||
rc = sqlite3_prepare_v2(m_db, sqlAppUsage, -1, &m_stmtSaveAppUsage, nullptr);
|
||||
if (rc != SQLITE_OK) {
|
||||
m_logger->error("Failed to prepare save_app_usage: {}", sqlite3_errmsg(m_db));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Save conversation statement
|
||||
const char* sqlConv = "INSERT INTO conversations "
|
||||
"(role, content, provider, model, tokens_used) "
|
||||
"VALUES (?, ?, ?, ?, ?)";
|
||||
rc = sqlite3_prepare_v2(m_db, sqlConv, -1, &m_stmtSaveConversation, nullptr);
|
||||
if (rc != SQLITE_OK) {
|
||||
m_logger->error("Failed to prepare save_conversation: {}", sqlite3_errmsg(m_db));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update metrics statement
|
||||
const char* sqlMetrics = "INSERT INTO daily_metrics "
|
||||
"(date, total_focus_minutes, total_breaks, hyperfocus_count) "
|
||||
"VALUES (?, ?, ?, ?) "
|
||||
"ON CONFLICT(date) DO UPDATE SET "
|
||||
"total_focus_minutes = total_focus_minutes + excluded.total_focus_minutes, "
|
||||
"total_breaks = total_breaks + excluded.total_breaks, "
|
||||
"hyperfocus_count = hyperfocus_count + excluded.hyperfocus_count, "
|
||||
"updated_at = strftime('%s', 'now')";
|
||||
rc = sqlite3_prepare_v2(m_db, sqlMetrics, -1, &m_stmtUpdateMetrics, nullptr);
|
||||
if (rc != SQLITE_OK) {
|
||||
m_logger->error("Failed to prepare update_metrics: {}", sqlite3_errmsg(m_db));
|
||||
return false;
|
||||
}
|
||||
|
||||
m_logger->debug("Prepared statements created");
|
||||
return true;
|
||||
}
|
||||
|
||||
void StorageService::finalizeStatements() {
|
||||
if (m_stmtSaveSession) { sqlite3_finalize(m_stmtSaveSession); m_stmtSaveSession = nullptr; }
|
||||
if (m_stmtSaveAppUsage) { sqlite3_finalize(m_stmtSaveAppUsage); m_stmtSaveAppUsage = nullptr; }
|
||||
if (m_stmtSaveConversation) { sqlite3_finalize(m_stmtSaveConversation); m_stmtSaveConversation = nullptr; }
|
||||
if (m_stmtUpdateMetrics) { sqlite3_finalize(m_stmtUpdateMetrics); m_stmtUpdateMetrics = nullptr; }
|
||||
}
|
||||
|
||||
void StorageService::process() {
|
||||
processMessages();
|
||||
}
|
||||
|
||||
void StorageService::processMessages() {
|
||||
if (!m_io || !m_isConnected) return;
|
||||
|
||||
while (m_io->hasMessages() > 0) {
|
||||
auto msg = m_io->pullMessage();
|
||||
|
||||
if (msg.topic == "storage:save_session" && msg.data) {
|
||||
handleSaveSession(*msg.data);
|
||||
}
|
||||
else if (msg.topic == "storage:save_app_usage" && msg.data) {
|
||||
handleSaveAppUsage(*msg.data);
|
||||
}
|
||||
else if (msg.topic == "storage:save_conversation" && msg.data) {
|
||||
handleSaveConversation(*msg.data);
|
||||
}
|
||||
else if (msg.topic == "storage:update_metrics" && msg.data) {
|
||||
handleUpdateMetrics(*msg.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void StorageService::handleSaveSession(const grove::IDataNode& data) {
|
||||
std::string taskName = data.getString("taskName", "unknown");
|
||||
int durationMinutes = data.getInt("durationMinutes", 0);
|
||||
bool hyperfocus = data.getBool("hyperfocus", false);
|
||||
|
||||
std::time_t now = std::time(nullptr);
|
||||
std::time_t startTime = now - (durationMinutes * 60);
|
||||
|
||||
sqlite3_reset(m_stmtSaveSession);
|
||||
sqlite3_bind_text(m_stmtSaveSession, 1, taskName.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_int64(m_stmtSaveSession, 2, startTime);
|
||||
sqlite3_bind_int64(m_stmtSaveSession, 3, now);
|
||||
sqlite3_bind_int(m_stmtSaveSession, 4, durationMinutes);
|
||||
sqlite3_bind_int(m_stmtSaveSession, 5, hyperfocus ? 1 : 0);
|
||||
|
||||
int rc = sqlite3_step(m_stmtSaveSession);
|
||||
if (rc == SQLITE_DONE) {
|
||||
m_lastSessionId = static_cast<int>(sqlite3_last_insert_rowid(m_db));
|
||||
m_totalQueries++;
|
||||
m_logger->debug("Session saved: {} ({}min), id={}", taskName, durationMinutes, m_lastSessionId);
|
||||
|
||||
if (m_io) {
|
||||
auto event = std::make_unique<grove::JsonDataNode>("saved");
|
||||
event->setInt("sessionId", m_lastSessionId);
|
||||
m_io->publish("storage:session_saved", std::move(event));
|
||||
}
|
||||
} else {
|
||||
publishError(sqlite3_errmsg(m_db));
|
||||
}
|
||||
}
|
||||
|
||||
void StorageService::handleSaveAppUsage(const grove::IDataNode& data) {
|
||||
int sessionId = data.getInt("sessionId", m_lastSessionId);
|
||||
std::string appName = data.getString("appName", "");
|
||||
int durationSeconds = data.getInt("durationSeconds", 0);
|
||||
bool productive = data.getBool("productive", false);
|
||||
|
||||
sqlite3_reset(m_stmtSaveAppUsage);
|
||||
sqlite3_bind_int(m_stmtSaveAppUsage, 1, sessionId);
|
||||
sqlite3_bind_text(m_stmtSaveAppUsage, 2, appName.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_int(m_stmtSaveAppUsage, 3, durationSeconds);
|
||||
sqlite3_bind_int(m_stmtSaveAppUsage, 4, productive ? 1 : 0);
|
||||
|
||||
int rc = sqlite3_step(m_stmtSaveAppUsage);
|
||||
if (rc == SQLITE_DONE) {
|
||||
m_totalQueries++;
|
||||
} else {
|
||||
publishError(sqlite3_errmsg(m_db));
|
||||
}
|
||||
}
|
||||
|
||||
void StorageService::handleSaveConversation(const grove::IDataNode& data) {
|
||||
std::string role = data.getString("role", "");
|
||||
std::string content = data.getString("content", "");
|
||||
std::string provider = data.getString("provider", "");
|
||||
std::string model = data.getString("model", "");
|
||||
int tokens = data.getInt("tokens", 0);
|
||||
|
||||
sqlite3_reset(m_stmtSaveConversation);
|
||||
sqlite3_bind_text(m_stmtSaveConversation, 1, role.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(m_stmtSaveConversation, 2, content.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(m_stmtSaveConversation, 3, provider.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(m_stmtSaveConversation, 4, model.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_int(m_stmtSaveConversation, 5, tokens);
|
||||
|
||||
int rc = sqlite3_step(m_stmtSaveConversation);
|
||||
if (rc == SQLITE_DONE) {
|
||||
m_totalQueries++;
|
||||
} else {
|
||||
publishError(sqlite3_errmsg(m_db));
|
||||
}
|
||||
}
|
||||
|
||||
void StorageService::handleUpdateMetrics(const grove::IDataNode& data) {
|
||||
int focusMinutes = data.getInt("focusMinutes", 0);
|
||||
int breaks = data.getInt("breaks", 0);
|
||||
int hyperfocusCount = data.getInt("hyperfocusCount", 0);
|
||||
|
||||
std::time_t now = std::time(nullptr);
|
||||
std::tm* tm = std::localtime(&now);
|
||||
char dateStr[11];
|
||||
std::strftime(dateStr, sizeof(dateStr), "%Y-%m-%d", tm);
|
||||
|
||||
sqlite3_reset(m_stmtUpdateMetrics);
|
||||
sqlite3_bind_text(m_stmtUpdateMetrics, 1, dateStr, -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_int(m_stmtUpdateMetrics, 2, focusMinutes);
|
||||
sqlite3_bind_int(m_stmtUpdateMetrics, 3, breaks);
|
||||
sqlite3_bind_int(m_stmtUpdateMetrics, 4, hyperfocusCount);
|
||||
|
||||
int rc = sqlite3_step(m_stmtUpdateMetrics);
|
||||
if (rc == SQLITE_DONE) {
|
||||
m_totalQueries++;
|
||||
} else {
|
||||
publishError(sqlite3_errmsg(m_db));
|
||||
}
|
||||
}
|
||||
|
||||
bool StorageService::executeSQL(const std::string& sql) {
|
||||
char* errMsg = nullptr;
|
||||
int rc = sqlite3_exec(m_db, sql.c_str(), nullptr, nullptr, &errMsg);
|
||||
|
||||
if (rc != SQLITE_OK) {
|
||||
m_logger->error("SQL error: {}", errMsg ? errMsg : "unknown");
|
||||
sqlite3_free(errMsg);
|
||||
return false;
|
||||
}
|
||||
|
||||
m_totalQueries++;
|
||||
return true;
|
||||
}
|
||||
|
||||
void StorageService::publishError(const std::string& message) {
|
||||
m_logger->error("Storage error: {}", message);
|
||||
if (m_io) {
|
||||
auto event = std::make_unique<grove::JsonDataNode>("error");
|
||||
event->setString("message", message);
|
||||
m_io->publish("storage:error", std::move(event));
|
||||
}
|
||||
}
|
||||
|
||||
void StorageService::shutdown() {
|
||||
finalizeStatements();
|
||||
|
||||
if (m_db) {
|
||||
sqlite3_close(m_db);
|
||||
m_db = nullptr;
|
||||
m_isConnected = false;
|
||||
}
|
||||
|
||||
m_logger->info("StorageService shutdown. Total queries: {}", m_totalQueries);
|
||||
}
|
||||
|
||||
} // namespace aissia
|
||||
91
src/services/StorageService.hpp
Normal file
91
src/services/StorageService.hpp
Normal file
@ -0,0 +1,91 @@
|
||||
#pragma once
|
||||
|
||||
#include "IService.hpp"
|
||||
|
||||
#include <grove/IIO.h>
|
||||
#include <grove/JsonDataNode.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <queue>
|
||||
#include <mutex>
|
||||
|
||||
struct sqlite3;
|
||||
struct sqlite3_stmt;
|
||||
|
||||
namespace aissia {
|
||||
|
||||
/**
|
||||
* @brief Storage Service - SQLite persistence
|
||||
*
|
||||
* Handles all database operations synchronously in main thread.
|
||||
* Uses prepared statements to prevent SQL injection.
|
||||
*
|
||||
* Subscribes to:
|
||||
* - "storage:save_session" : { taskName, durationMinutes, hyperfocus }
|
||||
* - "storage:save_app_usage" : { sessionId, appName, durationSeconds, productive }
|
||||
* - "storage:save_conversation" : { role, content, provider, model, tokens }
|
||||
* - "storage:update_metrics" : { focusMinutes, breaks, hyperfocusCount }
|
||||
* - "storage:query" : { sql, params[] }
|
||||
*
|
||||
* Publishes:
|
||||
* - "storage:ready" : Database initialized
|
||||
* - "storage:session_saved": { sessionId }
|
||||
* - "storage:error" : { message }
|
||||
*/
|
||||
class StorageService : public IService {
|
||||
public:
|
||||
StorageService();
|
||||
~StorageService() override;
|
||||
|
||||
bool initialize(grove::IIO* io) override;
|
||||
void process() override;
|
||||
void shutdown() override;
|
||||
std::string getName() const override { return "StorageService"; }
|
||||
bool isHealthy() const override { return m_isConnected; }
|
||||
|
||||
/// Open database with config
|
||||
bool openDatabase(const std::string& dbPath,
|
||||
const std::string& journalMode = "WAL",
|
||||
int busyTimeoutMs = 5000);
|
||||
|
||||
/// Get last inserted session ID
|
||||
int getLastSessionId() const { return m_lastSessionId; }
|
||||
|
||||
private:
|
||||
// Database
|
||||
sqlite3* m_db = nullptr;
|
||||
std::string m_dbPath;
|
||||
bool m_isConnected = false;
|
||||
int m_lastSessionId = 0;
|
||||
int m_totalQueries = 0;
|
||||
|
||||
// Prepared statements
|
||||
sqlite3_stmt* m_stmtSaveSession = nullptr;
|
||||
sqlite3_stmt* m_stmtSaveAppUsage = nullptr;
|
||||
sqlite3_stmt* m_stmtSaveConversation = nullptr;
|
||||
sqlite3_stmt* m_stmtUpdateMetrics = nullptr;
|
||||
|
||||
// Services
|
||||
grove::IIO* m_io = nullptr;
|
||||
std::shared_ptr<spdlog::logger> m_logger;
|
||||
|
||||
// Database operations
|
||||
bool initializeSchema();
|
||||
bool prepareStatements();
|
||||
void finalizeStatements();
|
||||
|
||||
// Message handlers
|
||||
void processMessages();
|
||||
void handleSaveSession(const grove::IDataNode& data);
|
||||
void handleSaveAppUsage(const grove::IDataNode& data);
|
||||
void handleSaveConversation(const grove::IDataNode& data);
|
||||
void handleUpdateMetrics(const grove::IDataNode& data);
|
||||
|
||||
// Helpers
|
||||
bool executeSQL(const std::string& sql);
|
||||
void publishError(const std::string& message);
|
||||
};
|
||||
|
||||
} // namespace aissia
|
||||
143
src/services/VoiceService.cpp
Normal file
143
src/services/VoiceService.cpp
Normal file
@ -0,0 +1,143 @@
|
||||
#include "VoiceService.hpp"
|
||||
|
||||
#include <spdlog/sinks/stdout_color_sinks.h>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace aissia {
|
||||
|
||||
VoiceService::VoiceService() {
|
||||
m_logger = spdlog::get("VoiceService");
|
||||
if (!m_logger) {
|
||||
m_logger = spdlog::stdout_color_mt("VoiceService");
|
||||
}
|
||||
}
|
||||
|
||||
bool VoiceService::initialize(grove::IIO* io) {
|
||||
m_io = io;
|
||||
|
||||
// Create TTS engine
|
||||
m_ttsEngine = TTSEngineFactory::create();
|
||||
if (m_ttsEngine && m_ttsEngine->isAvailable()) {
|
||||
m_ttsEngine->setRate(m_ttsRate);
|
||||
m_ttsEngine->setVolume(m_ttsVolume);
|
||||
m_logger->info("TTS engine: {}", m_ttsEngine->getEngineName());
|
||||
} else {
|
||||
m_logger->warn("TTS engine not available");
|
||||
}
|
||||
|
||||
if (m_io) {
|
||||
grove::SubscriptionConfig config;
|
||||
m_io->subscribe("voice:speak", config);
|
||||
m_io->subscribe("voice:stop", config);
|
||||
m_io->subscribe("voice:listen", config);
|
||||
}
|
||||
|
||||
m_logger->info("VoiceService initialized");
|
||||
return true;
|
||||
}
|
||||
|
||||
void VoiceService::configureTTS(bool enabled, int rate, int volume) {
|
||||
m_ttsEnabled = enabled;
|
||||
m_ttsRate = rate;
|
||||
m_ttsVolume = volume;
|
||||
|
||||
if (m_ttsEngine) {
|
||||
m_ttsEngine->setRate(rate);
|
||||
m_ttsEngine->setVolume(volume);
|
||||
}
|
||||
}
|
||||
|
||||
void VoiceService::configureSTT(bool enabled, const std::string& language,
|
||||
const std::string& apiKey) {
|
||||
m_sttEnabled = enabled;
|
||||
m_language = language;
|
||||
|
||||
if (!apiKey.empty()) {
|
||||
m_sttEngine = STTEngineFactory::create(apiKey);
|
||||
if (m_sttEngine) {
|
||||
m_sttEngine->setLanguage(language);
|
||||
m_logger->info("STT engine: {}", m_sttEngine->getEngineName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VoiceService::process() {
|
||||
processMessages();
|
||||
processSpeakQueue();
|
||||
}
|
||||
|
||||
void VoiceService::processMessages() {
|
||||
if (!m_io) return;
|
||||
|
||||
while (m_io->hasMessages() > 0) {
|
||||
auto msg = m_io->pullMessage();
|
||||
|
||||
if (msg.topic == "voice:speak" && msg.data) {
|
||||
handleSpeakRequest(*msg.data);
|
||||
}
|
||||
else if (msg.topic == "voice:stop") {
|
||||
if (m_ttsEngine) {
|
||||
m_ttsEngine->stop();
|
||||
}
|
||||
// Clear queue
|
||||
while (!m_speakQueue.empty()) m_speakQueue.pop();
|
||||
}
|
||||
else if (msg.topic == "voice:listen" && m_sttEnabled && m_sttEngine) {
|
||||
// STT would be handled here
|
||||
// For now just log
|
||||
m_logger->debug("STT listen requested");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VoiceService::handleSpeakRequest(const grove::IDataNode& data) {
|
||||
std::string text = data.getString("text", "");
|
||||
bool priority = data.getBool("priority", false);
|
||||
|
||||
if (text.empty()) return;
|
||||
|
||||
if (priority) {
|
||||
// Clear queue and stop current speech
|
||||
while (!m_speakQueue.empty()) m_speakQueue.pop();
|
||||
if (m_ttsEngine) m_ttsEngine->stop();
|
||||
}
|
||||
|
||||
m_speakQueue.push(text);
|
||||
}
|
||||
|
||||
void VoiceService::processSpeakQueue() {
|
||||
if (!m_ttsEnabled || !m_ttsEngine || m_speakQueue.empty()) return;
|
||||
|
||||
// Only speak if not currently speaking
|
||||
if (!m_ttsEngine->isSpeaking() && !m_speakQueue.empty()) {
|
||||
std::string text = m_speakQueue.front();
|
||||
m_speakQueue.pop();
|
||||
speak(text);
|
||||
}
|
||||
}
|
||||
|
||||
void VoiceService::speak(const std::string& text) {
|
||||
if (!m_ttsEngine || !m_ttsEnabled) return;
|
||||
|
||||
// Publish speaking started
|
||||
if (m_io) {
|
||||
auto event = std::make_unique<grove::JsonDataNode>("event");
|
||||
event->setString("text", text.size() > 100 ? text.substr(0, 100) + "..." : text);
|
||||
m_io->publish("voice:speaking_started", std::move(event));
|
||||
}
|
||||
|
||||
m_ttsEngine->speak(text, true);
|
||||
m_totalSpoken++;
|
||||
|
||||
m_logger->debug("Speaking: {}", text.size() > 50 ? text.substr(0, 50) + "..." : text);
|
||||
}
|
||||
|
||||
void VoiceService::shutdown() {
|
||||
if (m_ttsEngine) {
|
||||
m_ttsEngine->stop();
|
||||
}
|
||||
|
||||
m_logger->info("VoiceService shutdown. Total spoken: {}", m_totalSpoken);
|
||||
}
|
||||
|
||||
} // namespace aissia
|
||||
76
src/services/VoiceService.hpp
Normal file
76
src/services/VoiceService.hpp
Normal file
@ -0,0 +1,76 @@
|
||||
#pragma once
|
||||
|
||||
#include "IService.hpp"
|
||||
#include "../shared/audio/ITTSEngine.hpp"
|
||||
#include "../shared/audio/ISTTEngine.hpp"
|
||||
|
||||
#include <grove/IIO.h>
|
||||
#include <grove/JsonDataNode.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <queue>
|
||||
|
||||
namespace aissia {
|
||||
|
||||
/**
|
||||
* @brief Voice Service - TTS and STT engines
|
||||
*
|
||||
* Handles platform-specific audio engines (SAPI on Windows, espeak on Linux).
|
||||
* Manages speak queue and processes TTS/STT requests.
|
||||
*
|
||||
* Subscribes to:
|
||||
* - "voice:speak" : { text, priority? }
|
||||
* - "voice:stop" : Stop current speech
|
||||
* - "voice:listen" : Start STT recording
|
||||
*
|
||||
* Publishes:
|
||||
* - "voice:speaking_started" : { text }
|
||||
* - "voice:speaking_ended" : {}
|
||||
* - "voice:transcription" : { text, confidence }
|
||||
*/
|
||||
class VoiceService : public IService {
|
||||
public:
|
||||
VoiceService();
|
||||
~VoiceService() override = default;
|
||||
|
||||
bool initialize(grove::IIO* io) override;
|
||||
void process() override;
|
||||
void shutdown() override;
|
||||
std::string getName() const override { return "VoiceService"; }
|
||||
bool isHealthy() const override { return m_ttsEngine != nullptr; }
|
||||
|
||||
/// Configure TTS settings
|
||||
void configureTTS(bool enabled = true, int rate = 0, int volume = 80);
|
||||
|
||||
/// Configure STT settings
|
||||
void configureSTT(bool enabled = true, const std::string& language = "fr",
|
||||
const std::string& apiKey = "");
|
||||
|
||||
private:
|
||||
// Configuration
|
||||
bool m_ttsEnabled = true;
|
||||
bool m_sttEnabled = true;
|
||||
int m_ttsRate = 0;
|
||||
int m_ttsVolume = 80;
|
||||
std::string m_language = "fr";
|
||||
|
||||
// State
|
||||
std::unique_ptr<ITTSEngine> m_ttsEngine;
|
||||
std::unique_ptr<ISTTEngine> m_sttEngine;
|
||||
std::queue<std::string> m_speakQueue;
|
||||
int m_totalSpoken = 0;
|
||||
|
||||
// Services
|
||||
grove::IIO* m_io = nullptr;
|
||||
std::shared_ptr<spdlog::logger> m_logger;
|
||||
|
||||
// Helpers
|
||||
void processMessages();
|
||||
void processSpeakQueue();
|
||||
void speak(const std::string& text);
|
||||
void handleSpeakRequest(const grove::IDataNode& data);
|
||||
};
|
||||
|
||||
} // namespace aissia
|
||||
Loading…
Reference in New Issue
Block a user