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:
StillHammer 2025-11-26 11:57:53 +08:00
parent bc3b6cbaba
commit 26a5d3438b
29 changed files with 301838 additions and 756 deletions

1
.gitignore vendored
View File

@ -72,3 +72,4 @@ build/
*.a
*.so
*.dll
external/GroveEngine

View File

@ -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"

View 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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

719
deps/sqlite/sqlite3ext.h vendored Normal file
View 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
View 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

View File

@ -1 +0,0 @@
/mnt/e/Users/Alexis Trouvé/Documents/Projets/GroveEngine

View File

@ -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;
}

View File

@ -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_awaitingResponse = true;
m_logger->info("Sending query: {}", query.substr(0, 50));
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);
m_io->publish("llm:request", std::move(request));
m_totalQueries++;
}
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));
}
}
m_isProcessing = true;
m_logger->info("Processing query: {}", query.substr(0, 50));
void AIModule::handleLLMError(const grove::IDataNode& data) {
std::string conversationId = data.getString("conversationId", "default");
if (conversationId != m_currentConversationId) return;
try {
auto result = agenticLoop(query);
m_awaitingResponse = false;
std::string message = data.getString("message", "Unknown error");
m_logger->error("LLM error: {}", message);
if (result.contains("response")) {
publishResponse(result["response"].get<std::string>());
m_totalQueries++;
} else if (result.contains("error")) {
publishError(result["error"].get<std::string>());
}
} catch (const std::exception& e) {
publishError(e.what());
if (m_io) {
auto event = std::make_unique<grove::JsonDataNode>("error");
event->setString("message", message);
m_io->publish("ai:error", std::move(event));
}
m_isProcessing = false;
}
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}});
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) {
void AIModule::publishSuggestion(const std::string& message, int duration) {
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);
auto event = std::make_unique<grove::JsonDataNode>("suggestion");
event->setString("message", message);
event->setInt("duration", duration);
m_io->publish("ai:suggestion", std::move(event));
}
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);
}

View File

@ -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
* - "ai:suggestion" : Suggestion proactive
* - "ai:error" : Erreur API
* - "llm:request" : Request LLM response
* - "ai:suggestion" : Suggestion proactive
*
* 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

View File

@ -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,70 +64,94 @@ 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;
// Update productivity counters
if (isProductiveApp(m_currentApp)) {
m_totalProductiveSeconds += duration;
} else if (isDistractingApp(m_currentApp)) {
m_totalDistractingSeconds += duration;
}
publishAppChanged(m_currentApp, newApp, 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);
}
m_currentApp = newApp;
m_currentWindowTitle = newTitle;
m_appStartTime = currentTime;
m_logger->debug("App: {} - {}", m_currentApp, m_currentWindowTitle.substr(0, 50));
}
}
void MonitoringModule::checkIdleState(float currentTime) {
bool wasIdle = m_isIdle;
m_isIdle = m_tracker->isUserIdle(m_idleThresholdSeconds);
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 (m_isIdle && !wasIdle) {
m_logger->info("Utilisateur inactif ({}s)", m_idleThresholdSeconds);
if (!oldApp.empty() && duration > 0) {
// Update duration tracking
m_appDurations[oldApp] += duration;
if (m_io) {
auto event = std::make_unique<grove::JsonDataNode>("idle");
event->setString("type", "idle_detected");
event->setInt("idleSeconds", m_tracker->getIdleTimeSeconds());
m_io->publish("monitoring:idle_detected", std::move(event));
// Update productivity counters
bool wasProductive = isProductiveApp(oldApp);
bool wasDistracting = isDistractingApp(oldApp);
if (wasProductive) {
m_totalProductiveSeconds += duration;
} else if (wasDistracting) {
m_totalDistractingSeconds += 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);
}
else if (!m_isIdle && wasIdle) {
m_logger->info("Activite reprise");
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));
}
m_currentApp = newApp;
}
void MonitoringModule::handleWindowInfo(const grove::IDataNode& data) {
std::string appName = data.getString("appName", "");
if (!appName.empty() && appName != m_currentApp) {
m_currentApp = appName;
}
}
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->setInt("idleSeconds", idleSeconds);
m_io->publish("monitoring:idle_detected", std::move(event));
}
}
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");
m_io->publish("monitoring:activity_resumed", std::move(event));
}
}
@ -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);

View File

@ -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

View File

@ -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);
float sessionMinutes = (m_lastActivityTime - m_sessionStartTime) / 60.0f;
if (task) {
float sessionMinutes = (m_lastActivityTime - m_sessionStartTime) / 60.0f;
task->actualMinutes = static_cast<int>(sessionMinutes);
task->completed = true;
m_totalFocusMinutesToday += task->actualMinutes;
m_logger->info("Tâche terminée: {} (réel: {}min vs estimé: {}min)",
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);
}

View File

@ -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 che
* - "scheduler/task_completed" : Fin de 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:

View File

@ -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);
}
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;
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++;
}
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;
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);
}
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

View File

@ -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

View File

@ -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;
}

View File

@ -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
View 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
View 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
View 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

View 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

View 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

View 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

View 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

View 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

View 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