Migration Gitea - sauvegarde locale 2025-12-04 18:58
This commit is contained in:
parent
cb938500cd
commit
ce2b25a599
6
.gitmodules
vendored
6
.gitmodules
vendored
@ -1,3 +1,3 @@
|
|||||||
[submodule "external/whisper.cpp"]
|
[submodule "external/whisper.cpp"]
|
||||||
path = external/whisper.cpp
|
path = external/whisper.cpp
|
||||||
url = https://github.com/ggerganov/whisper.cpp
|
url = https://github.com/ggerganov/whisper.cpp
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,340 +1,340 @@
|
|||||||
# AUDIT DE CONFORMITÉ GROVEENGINE - AISSIA
|
# AUDIT DE CONFORMITÉ GROVEENGINE - AISSIA
|
||||||
|
|
||||||
**Date** : 2025-11-26
|
**Date** : 2025-11-26
|
||||||
**Auditeur** : Claude Code
|
**Auditeur** : Claude Code
|
||||||
**Version auditée** : Commit bc3b6cb
|
**Version auditée** : Commit bc3b6cb
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## RÉSUMÉ EXÉCUTIF
|
## RÉSUMÉ EXÉCUTIF
|
||||||
|
|
||||||
**Verdict : Le code contourne massivement les principes de GroveEngine.**
|
**Verdict : Le code contourne massivement les principes de GroveEngine.**
|
||||||
|
|
||||||
| Module | Lignes | Conformité Engine | Statut |
|
| Module | Lignes | Conformité Engine | Statut |
|
||||||
|--------|--------|-------------------|--------|
|
|--------|--------|-------------------|--------|
|
||||||
| AIModule | 306 | VIOLATION | Infrastructure dans module |
|
| AIModule | 306 | VIOLATION | Infrastructure dans module |
|
||||||
| MonitoringModule | 222 | VIOLATION | Appels OS dans module |
|
| MonitoringModule | 222 | VIOLATION | Appels OS dans module |
|
||||||
| StorageModule | 273 | VIOLATION | SQLite dans module |
|
| StorageModule | 273 | VIOLATION | SQLite dans module |
|
||||||
| VoiceModule | 209 | VIOLATION | TTS/COM dans module |
|
| VoiceModule | 209 | VIOLATION | TTS/COM dans module |
|
||||||
| SchedulerModule | 179 | CONFORME | Logique métier pure |
|
| SchedulerModule | 179 | CONFORME | Logique métier pure |
|
||||||
| NotificationModule | 172 | CONFORME | Logique métier pure |
|
| NotificationModule | 172 | CONFORME | Logique métier pure |
|
||||||
|
|
||||||
**Score global** : 2/6 modules conformes (33%)
|
**Score global** : 2/6 modules conformes (33%)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## RAPPEL DES PRINCIPES GROVEENGINE
|
## RAPPEL DES PRINCIPES GROVEENGINE
|
||||||
|
|
||||||
Selon `docs/GROVEENGINE_GUIDE.md` :
|
Selon `docs/GROVEENGINE_GUIDE.md` :
|
||||||
|
|
||||||
1. **Modules = Pure business logic** (200-300 lignes recommandées)
|
1. **Modules = Pure business logic** (200-300 lignes recommandées)
|
||||||
2. **No infrastructure code in modules** : threading, networking, persistence
|
2. **No infrastructure code in modules** : threading, networking, persistence
|
||||||
3. **All data via IDataNode abstraction** (backend agnostic)
|
3. **All data via IDataNode abstraction** (backend agnostic)
|
||||||
4. **Pull-based message processing** via IIO pub/sub
|
4. **Pull-based message processing** via IIO pub/sub
|
||||||
5. **Hot-reload ready** : sérialiser tout l'état dans `getState()`
|
5. **Hot-reload ready** : sérialiser tout l'état dans `getState()`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## VIOLATIONS CRITIQUES
|
## VIOLATIONS CRITIQUES
|
||||||
|
|
||||||
### 1. AIModule - Networking dans le module
|
### 1. AIModule - Networking dans le module
|
||||||
|
|
||||||
**Fichier** : `src/modules/AIModule.cpp:146`
|
**Fichier** : `src/modules/AIModule.cpp:146`
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
nlohmann::json AIModule::agenticLoop(const std::string& userQuery) {
|
nlohmann::json AIModule::agenticLoop(const std::string& userQuery) {
|
||||||
// ...
|
// ...
|
||||||
auto response = m_provider->chat(m_systemPrompt, messages, tools);
|
auto response = m_provider->chat(m_systemPrompt, messages, tools);
|
||||||
// Appel HTTP synchrone bloquant !
|
// Appel HTTP synchrone bloquant !
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Violation** : Appels HTTP synchrones directement dans `process()` via la boucle agentique.
|
**Violation** : Appels HTTP synchrones directement dans `process()` via la boucle agentique.
|
||||||
|
|
||||||
**Impact** :
|
**Impact** :
|
||||||
- Bloque la boucle principale pendant chaque requête LLM (timeout 60s)
|
- Bloque la boucle principale pendant chaque requête LLM (timeout 60s)
|
||||||
- `isIdle()` retourne false pendant l'appel, mais le module reste bloquant
|
- `isIdle()` retourne false pendant l'appel, mais le module reste bloquant
|
||||||
- Hot-reload impossible pendant une requête en cours
|
- Hot-reload impossible pendant une requête en cours
|
||||||
- Tous les autres modules sont bloqués
|
- Tous les autres modules sont bloqués
|
||||||
|
|
||||||
**Correction requise** : Déléguer les appels LLM à un service infrastructure externe, communication via IIO async.
|
**Correction requise** : Déléguer les appels LLM à un service infrastructure externe, communication via IIO async.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 2. StorageModule - Persistence dans le module
|
### 2. StorageModule - Persistence dans le module
|
||||||
|
|
||||||
**Fichier** : `src/modules/StorageModule.cpp:78-91`
|
**Fichier** : `src/modules/StorageModule.cpp:78-91`
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
bool StorageModule::openDatabase() {
|
bool StorageModule::openDatabase() {
|
||||||
int rc = sqlite3_open(m_dbPath.c_str(), &m_db);
|
int rc = sqlite3_open(m_dbPath.c_str(), &m_db);
|
||||||
// Handle SQLite directement dans le module
|
// Handle SQLite directement dans le module
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Violation** : Gestion directe de SQLite. L'engine préconise `IDataNode` abstractions pour la persistence.
|
**Violation** : Gestion directe de SQLite. L'engine préconise `IDataNode` abstractions pour la persistence.
|
||||||
|
|
||||||
**Impact** :
|
**Impact** :
|
||||||
- Hot-reload risqué (handle DB ouvert)
|
- Hot-reload risqué (handle DB ouvert)
|
||||||
- Risque de corruption si reload pendant transaction
|
- Risque de corruption si reload pendant transaction
|
||||||
- Couplage fort avec SQLite
|
- Couplage fort avec SQLite
|
||||||
|
|
||||||
**Correction requise** : Service StorageService dans main.cpp, modules communiquent via topics `storage:*`.
|
**Correction requise** : Service StorageService dans main.cpp, modules communiquent via topics `storage:*`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3. MonitoringModule - Appels OS dans le module
|
### 3. MonitoringModule - Appels OS dans le module
|
||||||
|
|
||||||
**Fichier** : `src/modules/MonitoringModule.cpp:78-79`
|
**Fichier** : `src/modules/MonitoringModule.cpp:78-79`
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
void MonitoringModule::checkCurrentApp(float currentTime) {
|
void MonitoringModule::checkCurrentApp(float currentTime) {
|
||||||
std::string newApp = m_tracker->getCurrentAppName();
|
std::string newApp = m_tracker->getCurrentAppName();
|
||||||
// Appelle GetForegroundWindow(), OpenProcess(), etc.
|
// 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.
|
**Violation** : Appels Win32 API dans `process()`. Même encapsulé dans `IWindowTracker`, c'est du code plateforme dans un module hot-reloadable.
|
||||||
|
|
||||||
**Impact** :
|
**Impact** :
|
||||||
- Dépendance plateforme dans le module
|
- Dépendance plateforme dans le module
|
||||||
- Handles système potentiellement orphelins au reload
|
- Handles système potentiellement orphelins au reload
|
||||||
|
|
||||||
**Correction requise** : Service PlatformService qui publie `monitoring:window_info` périodiquement.
|
**Correction requise** : Service PlatformService qui publie `monitoring:window_info` périodiquement.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 4. VoiceModule - COM/SAPI dans le module
|
### 4. VoiceModule - COM/SAPI dans le module
|
||||||
|
|
||||||
**Fichier** : `src/modules/VoiceModule.cpp:122`
|
**Fichier** : `src/modules/VoiceModule.cpp:122`
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
void VoiceModule::speak(const std::string& text) {
|
void VoiceModule::speak(const std::string& text) {
|
||||||
m_ttsEngine->speak(text, true);
|
m_ttsEngine->speak(text, true);
|
||||||
// Appel ISpVoice::Speak via COM
|
// Appel ISpVoice::Speak via COM
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Fichier** : `src/shared/audio/SAPITTSEngine.hpp:26`
|
**Fichier** : `src/shared/audio/SAPITTSEngine.hpp:26`
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
||||||
```
|
```
|
||||||
|
|
||||||
**Violation** : Initialisation COM et appels SAPI dans le module.
|
**Violation** : Initialisation COM et appels SAPI dans le module.
|
||||||
|
|
||||||
**Impact** :
|
**Impact** :
|
||||||
- `CoInitializeEx` par thread, hot-reload peut causer des fuites
|
- `CoInitializeEx` par thread, hot-reload peut causer des fuites
|
||||||
- Appels asynchrones SAPI difficiles à gérer au shutdown
|
- Appels asynchrones SAPI difficiles à gérer au shutdown
|
||||||
|
|
||||||
**Correction requise** : Service VoiceService dédié, modules envoient `voice:speak`.
|
**Correction requise** : Service VoiceService dédié, modules envoient `voice:speak`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## PROBLÈMES DE DESIGN
|
## PROBLÈMES DE DESIGN
|
||||||
|
|
||||||
### 5. Topics incohérents
|
### 5. Topics incohérents
|
||||||
|
|
||||||
**SchedulerModule.h:26** utilise le format slash :
|
**SchedulerModule.h:26** utilise le format slash :
|
||||||
```cpp
|
```cpp
|
||||||
// "scheduler/hyperfocus_alert"
|
// "scheduler/hyperfocus_alert"
|
||||||
```
|
```
|
||||||
|
|
||||||
**AIModule.cpp:52** utilise le format colon :
|
**AIModule.cpp:52** utilise le format colon :
|
||||||
```cpp
|
```cpp
|
||||||
m_io->subscribe("scheduler:hyperfocus_alert", subConfig);
|
m_io->subscribe("scheduler:hyperfocus_alert", subConfig);
|
||||||
```
|
```
|
||||||
|
|
||||||
**Standard GroveEngine** : Format `module:event` (colon)
|
**Standard GroveEngine** : Format `module:event` (colon)
|
||||||
|
|
||||||
**Impact** : Les messages ne seront jamais reçus si les formats ne correspondent pas.
|
**Impact** : Les messages ne seront jamais reçus si les formats ne correspondent pas.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 6. SchedulerModule - IIO non utilisé
|
### 6. SchedulerModule - IIO non utilisé
|
||||||
|
|
||||||
**Fichier** : `src/modules/SchedulerModule.cpp:66-68`
|
**Fichier** : `src/modules/SchedulerModule.cpp:66-68`
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
void SchedulerModule::checkHyperfocus(float currentTime) {
|
void SchedulerModule::checkHyperfocus(float currentTime) {
|
||||||
// ...
|
// ...
|
||||||
// Publier l'alerte (si IO disponible)
|
// Publier l'alerte (si IO disponible)
|
||||||
// Note: Dans une version complète, on publierait via m_io
|
// 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.
|
**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
|
### 7. État non restaurable - StorageModule
|
||||||
|
|
||||||
**Fichier** : `src/modules/StorageModule.cpp:246-258`
|
**Fichier** : `src/modules/StorageModule.cpp:246-258`
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
std::unique_ptr<grove::IDataNode> StorageModule::getState() {
|
std::unique_ptr<grove::IDataNode> StorageModule::getState() {
|
||||||
state->setBool("isConnected", m_isConnected);
|
state->setBool("isConnected", m_isConnected);
|
||||||
// ...
|
// ...
|
||||||
}
|
}
|
||||||
|
|
||||||
void StorageModule::setState(const grove::IDataNode& state) {
|
void StorageModule::setState(const grove::IDataNode& state) {
|
||||||
// NE ROUVRE PAS la connexion DB !
|
// NE ROUVRE PAS la connexion DB !
|
||||||
m_logger->info("Etat restore...");
|
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.
|
**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
|
### 8. Libraries statiques dans modules
|
||||||
|
|
||||||
**CMakeLists.txt:86-101** :
|
**CMakeLists.txt:86-101** :
|
||||||
|
|
||||||
```cmake
|
```cmake
|
||||||
add_library(AissiaLLM STATIC ...)
|
add_library(AissiaLLM STATIC ...)
|
||||||
target_link_libraries(AIModule PRIVATE AissiaLLM)
|
target_link_libraries(AIModule PRIVATE AissiaLLM)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Problème** : Les libs `AissiaLLM`, `AissiaPlatform`, `AissiaAudio` sont compilées en STATIC et linkées dans chaque .so.
|
**Problème** : Les libs `AissiaLLM`, `AissiaPlatform`, `AissiaAudio` sont compilées en STATIC et linkées dans chaque .so.
|
||||||
|
|
||||||
**Impact** :
|
**Impact** :
|
||||||
- Code dupliqué dans chaque module
|
- Code dupliqué dans chaque module
|
||||||
- Hot-reload ne rafraîchit pas ces libs
|
- Hot-reload ne rafraîchit pas ces libs
|
||||||
- Pas de partage d'état entre modules
|
- Pas de partage d'état entre modules
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 9. Dépassement limite de lignes
|
### 9. Dépassement limite de lignes
|
||||||
|
|
||||||
| Module | Lignes | Limite recommandée |
|
| Module | Lignes | Limite recommandée |
|
||||||
|--------|--------|-------------------|
|
|--------|--------|-------------------|
|
||||||
| AIModule | 306 | 200-300 |
|
| AIModule | 306 | 200-300 |
|
||||||
|
|
||||||
Le dépassement est mineur mais symptomatique : le module fait trop de choses.
|
Le dépassement est mineur mais symptomatique : le module fait trop de choses.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## CE QUI EST CONFORME
|
## CE QUI EST CONFORME
|
||||||
|
|
||||||
### SchedulerModule & NotificationModule
|
### SchedulerModule & NotificationModule
|
||||||
|
|
||||||
Ces deux modules respectent les principes :
|
Ces deux modules respectent les principes :
|
||||||
- Logique métier pure
|
- Logique métier pure
|
||||||
- Pas d'appels système
|
- Pas d'appels système
|
||||||
- État sérialisable
|
- État sérialisable
|
||||||
- Taille appropriée
|
- Taille appropriée
|
||||||
|
|
||||||
### Structure IModule
|
### Structure IModule
|
||||||
|
|
||||||
Tous les modules implémentent correctement :
|
Tous les modules implémentent correctement :
|
||||||
- `process()`
|
- `process()`
|
||||||
- `setConfiguration()`
|
- `setConfiguration()`
|
||||||
- `getState()` / `setState()`
|
- `getState()` / `setState()`
|
||||||
- `getHealthStatus()`
|
- `getHealthStatus()`
|
||||||
- `shutdown()`
|
- `shutdown()`
|
||||||
- Exports C `createModule()` / `destroyModule()`
|
- Exports C `createModule()` / `destroyModule()`
|
||||||
|
|
||||||
### main.cpp
|
### main.cpp
|
||||||
|
|
||||||
La boucle principale est bien implémentée :
|
La boucle principale est bien implémentée :
|
||||||
- FileWatcher pour hot-reload
|
- FileWatcher pour hot-reload
|
||||||
- Frame timing à 10Hz
|
- Frame timing à 10Hz
|
||||||
- Signal handling propre
|
- Signal handling propre
|
||||||
- Chargement/déchargement correct
|
- Chargement/déchargement correct
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ARCHITECTURE RECOMMANDÉE
|
## ARCHITECTURE RECOMMANDÉE
|
||||||
|
|
||||||
### Actuelle (INCORRECTE)
|
### Actuelle (INCORRECTE)
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────────────────────────────┐
|
┌─────────────────────────────────────────────────────────┐
|
||||||
│ main.cpp │
|
│ main.cpp │
|
||||||
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
|
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
|
||||||
│ │AIModule │ │Storage │ │Monitor │ │Voice │ │
|
│ │AIModule │ │Storage │ │Monitor │ │Voice │ │
|
||||||
│ │ +HTTP │ │ +SQLite │ │ +Win32 │ │ +COM │ │
|
│ │ +HTTP │ │ +SQLite │ │ +Win32 │ │ +COM │ │
|
||||||
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
|
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
|
||||||
└─────────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────────┘
|
||||||
Infrastructure DANS les modules = VIOLATION
|
Infrastructure DANS les modules = VIOLATION
|
||||||
```
|
```
|
||||||
|
|
||||||
### Corrigée (CONFORME)
|
### Corrigée (CONFORME)
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────────────────────────────┐
|
┌─────────────────────────────────────────────────────────┐
|
||||||
│ main.cpp │
|
│ main.cpp │
|
||||||
│ │
|
│ │
|
||||||
│ ┌─────────────── INFRASTRUCTURE ──────────────────┐ │
|
│ ┌─────────────── INFRASTRUCTURE ──────────────────┐ │
|
||||||
│ │ LLMService │ StorageService │ PlatformService │ │ │
|
│ │ LLMService │ StorageService │ PlatformService │ │ │
|
||||||
│ │ (async) │ (SQLite) │ (Win32) │ │ │
|
│ │ (async) │ (SQLite) │ (Win32) │ │ │
|
||||||
│ └──────────────────────────────────────────────────┘ │
|
│ └──────────────────────────────────────────────────┘ │
|
||||||
│ ↑↓ IIO pub/sub (async, non-bloquant) │
|
│ ↑↓ IIO pub/sub (async, non-bloquant) │
|
||||||
│ ┌─────────────── MODULES (hot-reload) ────────────┐ │
|
│ ┌─────────────── MODULES (hot-reload) ────────────┐ │
|
||||||
│ │ AIModule │ StorageModule │ MonitoringModule │ │ │
|
│ │ AIModule │ StorageModule │ MonitoringModule │ │ │
|
||||||
│ │ (logic) │ (logic) │ (logic) │ │ │
|
│ │ (logic) │ (logic) │ (logic) │ │ │
|
||||||
│ └──────────────────────────────────────────────────┘ │
|
│ └──────────────────────────────────────────────────┘ │
|
||||||
└─────────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────────┘
|
||||||
Infrastructure HORS modules = CONFORME
|
Infrastructure HORS modules = CONFORME
|
||||||
```
|
```
|
||||||
|
|
||||||
### Flux de données corrigé
|
### Flux de données corrigé
|
||||||
|
|
||||||
```
|
```
|
||||||
User query → voice:transcription → AIModule
|
User query → voice:transcription → AIModule
|
||||||
AIModule → ai:query_request → LLMService (async)
|
AIModule → ai:query_request → LLMService (async)
|
||||||
LLMService → ai:response → AIModule
|
LLMService → ai:response → AIModule
|
||||||
AIModule → ai:response → VoiceModule → voice:speak
|
AIModule → ai:response → VoiceModule → voice:speak
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ACTIONS REQUISES
|
## ACTIONS REQUISES
|
||||||
|
|
||||||
### Priorité HAUTE
|
### Priorité HAUTE
|
||||||
|
|
||||||
1. **Extraire LLM de AIModule**
|
1. **Extraire LLM de AIModule**
|
||||||
- Créer `LLMService` dans main.cpp ou service dédié
|
- Créer `LLMService` dans main.cpp ou service dédié
|
||||||
- AIModule publie `ai:query_request`, reçoit `ai:response`
|
- AIModule publie `ai:query_request`, reçoit `ai:response`
|
||||||
- Appels HTTP dans thread séparé
|
- Appels HTTP dans thread séparé
|
||||||
|
|
||||||
2. **Extraire SQLite de StorageModule**
|
2. **Extraire SQLite de StorageModule**
|
||||||
- Créer `StorageService`
|
- Créer `StorageService`
|
||||||
- Modules publient `storage:save_*`, reçoivent `storage:result`
|
- Modules publient `storage:save_*`, reçoivent `storage:result`
|
||||||
|
|
||||||
3. **Extraire Win32 de MonitoringModule**
|
3. **Extraire Win32 de MonitoringModule**
|
||||||
- Créer `PlatformService`
|
- Créer `PlatformService`
|
||||||
- Publie `platform:window_changed` périodiquement
|
- Publie `platform:window_changed` périodiquement
|
||||||
|
|
||||||
4. **Extraire TTS de VoiceModule**
|
4. **Extraire TTS de VoiceModule**
|
||||||
- Créer `VoiceService`
|
- Créer `VoiceService`
|
||||||
- Modules publient `voice:speak`
|
- Modules publient `voice:speak`
|
||||||
|
|
||||||
### Priorité MOYENNE
|
### Priorité MOYENNE
|
||||||
|
|
||||||
5. **Corriger format topics** : Tout en `module:event`
|
5. **Corriger format topics** : Tout en `module:event`
|
||||||
6. **Implémenter publish dans SchedulerModule**
|
6. **Implémenter publish dans SchedulerModule**
|
||||||
7. **Corriger setState dans StorageModule**
|
7. **Corriger setState dans StorageModule**
|
||||||
|
|
||||||
### Priorité BASSE
|
### Priorité BASSE
|
||||||
|
|
||||||
8. **Refactorer libs STATIC en services**
|
8. **Refactorer libs STATIC en services**
|
||||||
9. **Réduire AIModule sous 300 lignes**
|
9. **Réduire AIModule sous 300 lignes**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## CONCLUSION
|
## CONCLUSION
|
||||||
|
|
||||||
Le code actuel **simule** l'utilisation de GroveEngine mais le **contourne** en plaçant l'infrastructure directement dans les modules.
|
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 :
|
Les modules ne sont **pas véritablement hot-reloadable** car ils :
|
||||||
1. Possèdent des ressources système (DB handles, COM objects)
|
1. Possèdent des ressources système (DB handles, COM objects)
|
||||||
2. Font des appels bloquants (HTTP 60s timeout, TTS)
|
2. Font des appels bloquants (HTTP 60s timeout, TTS)
|
||||||
3. Ne communiquent pas correctement via IIO
|
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.
|
**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*
|
*Audit généré automatiquement par Claude Code*
|
||||||
|
|||||||
@ -1,305 +1,305 @@
|
|||||||
# AISSIA MCP Configuration for Claude Code
|
# AISSIA MCP Configuration for Claude Code
|
||||||
|
|
||||||
This directory contains an example MCP (Model Context Protocol) configuration for integrating AISSIA with Claude Code.
|
This directory contains an example MCP (Model Context Protocol) configuration for integrating AISSIA with Claude Code.
|
||||||
|
|
||||||
## Quick Setup
|
## Quick Setup
|
||||||
|
|
||||||
### 1. Locate Claude Code MCP Settings
|
### 1. Locate Claude Code MCP Settings
|
||||||
|
|
||||||
The MCP configuration file location depends on your operating system:
|
The MCP configuration file location depends on your operating system:
|
||||||
|
|
||||||
**Windows**:
|
**Windows**:
|
||||||
```
|
```
|
||||||
%APPDATA%\Code\User\globalStorage\saoudrizwan.claude-dev\settings\cline_mcp_settings.json
|
%APPDATA%\Code\User\globalStorage\saoudrizwan.claude-dev\settings\cline_mcp_settings.json
|
||||||
```
|
```
|
||||||
|
|
||||||
Full path example:
|
Full path example:
|
||||||
```
|
```
|
||||||
C:\Users\YourUsername\AppData\Roaming\Code\User\globalStorage\saoudrizwan.claude-dev\settings\cline_mcp_settings.json
|
C:\Users\YourUsername\AppData\Roaming\Code\User\globalStorage\saoudrizwan.claude-dev\settings\cline_mcp_settings.json
|
||||||
```
|
```
|
||||||
|
|
||||||
**macOS**:
|
**macOS**:
|
||||||
```
|
```
|
||||||
~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json
|
~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json
|
||||||
```
|
```
|
||||||
|
|
||||||
**Linux**:
|
**Linux**:
|
||||||
```
|
```
|
||||||
~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json
|
~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Copy Configuration
|
### 2. Copy Configuration
|
||||||
|
|
||||||
Copy the contents of `claude_code_mcp_config.json` to the Claude Code MCP settings file.
|
Copy the contents of `claude_code_mcp_config.json` to the Claude Code MCP settings file.
|
||||||
|
|
||||||
**Important**: Update the `command` path to point to your actual AISSIA executable:
|
**Important**: Update the `command` path to point to your actual AISSIA executable:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"aissia": {
|
"aissia": {
|
||||||
"command": "C:\\path\\to\\your\\aissia\\build\\aissia.exe",
|
"command": "C:\\path\\to\\your\\aissia\\build\\aissia.exe",
|
||||||
"args": ["--mcp-server"],
|
"args": ["--mcp-server"],
|
||||||
"disabled": false
|
"disabled": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Restart Claude Code
|
### 3. Restart Claude Code
|
||||||
|
|
||||||
Restart VS Code (or reload window: `Ctrl+Shift+P` → "Developer: Reload Window") to apply the changes.
|
Restart VS Code (or reload window: `Ctrl+Shift+P` → "Developer: Reload Window") to apply the changes.
|
||||||
|
|
||||||
### 4. Verify Integration
|
### 4. Verify Integration
|
||||||
|
|
||||||
Open Claude Code and check that AISSIA tools are available:
|
Open Claude Code and check that AISSIA tools are available:
|
||||||
|
|
||||||
```
|
```
|
||||||
You: Can you list the available MCP servers?
|
You: Can you list the available MCP servers?
|
||||||
Claude: I have access to the following MCP servers:
|
Claude: I have access to the following MCP servers:
|
||||||
- aissia: 13 tools available
|
- aissia: 13 tools available
|
||||||
```
|
```
|
||||||
|
|
||||||
## Available Tools
|
## Available Tools
|
||||||
|
|
||||||
Once configured, Claude will have access to these 13 AISSIA tools:
|
Once configured, Claude will have access to these 13 AISSIA tools:
|
||||||
|
|
||||||
### AISSIA Core (5 tools)
|
### AISSIA Core (5 tools)
|
||||||
1. **chat_with_aissia** ⭐ - Dialogue with AISSIA's AI assistant (Claude Sonnet 4)
|
1. **chat_with_aissia** ⭐ - Dialogue with AISSIA's AI assistant (Claude Sonnet 4)
|
||||||
2. **transcribe_audio** - Transcribe audio files to text
|
2. **transcribe_audio** - Transcribe audio files to text
|
||||||
3. **text_to_speech** - Convert text to speech audio files
|
3. **text_to_speech** - Convert text to speech audio files
|
||||||
4. **save_memory** - Save notes to AISSIA's persistent storage
|
4. **save_memory** - Save notes to AISSIA's persistent storage
|
||||||
5. **search_memories** - Search through saved memories
|
5. **search_memories** - Search through saved memories
|
||||||
|
|
||||||
### File System (8 tools)
|
### File System (8 tools)
|
||||||
6. **read_file** - Read file contents
|
6. **read_file** - Read file contents
|
||||||
7. **write_file** - Write content to files
|
7. **write_file** - Write content to files
|
||||||
8. **list_directory** - List files in a directory
|
8. **list_directory** - List files in a directory
|
||||||
9. **search_files** - Search for files by pattern
|
9. **search_files** - Search for files by pattern
|
||||||
10. **file_exists** - Check if a file exists
|
10. **file_exists** - Check if a file exists
|
||||||
11. **create_directory** - Create directories
|
11. **create_directory** - Create directories
|
||||||
12. **delete_file** - Delete files
|
12. **delete_file** - Delete files
|
||||||
13. **move_file** - Move or rename files
|
13. **move_file** - Move or rename files
|
||||||
|
|
||||||
## Configuration Options
|
## Configuration Options
|
||||||
|
|
||||||
### Basic Configuration
|
### Basic Configuration
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"aissia": {
|
"aissia": {
|
||||||
"command": "path/to/aissia.exe",
|
"command": "path/to/aissia.exe",
|
||||||
"args": ["--mcp-server"],
|
"args": ["--mcp-server"],
|
||||||
"disabled": false
|
"disabled": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### With Auto-Approval
|
### With Auto-Approval
|
||||||
|
|
||||||
To skip confirmation prompts for specific tools:
|
To skip confirmation prompts for specific tools:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"aissia": {
|
"aissia": {
|
||||||
"command": "path/to/aissia.exe",
|
"command": "path/to/aissia.exe",
|
||||||
"args": ["--mcp-server"],
|
"args": ["--mcp-server"],
|
||||||
"disabled": false,
|
"disabled": false,
|
||||||
"alwaysAllow": ["chat_with_aissia", "read_file", "write_file"]
|
"alwaysAllow": ["chat_with_aissia", "read_file", "write_file"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Disable Server
|
### Disable Server
|
||||||
|
|
||||||
To temporarily disable AISSIA without removing the configuration:
|
To temporarily disable AISSIA without removing the configuration:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"aissia": {
|
"aissia": {
|
||||||
"command": "path/to/aissia.exe",
|
"command": "path/to/aissia.exe",
|
||||||
"args": ["--mcp-server"],
|
"args": ["--mcp-server"],
|
||||||
"disabled": true // <-- Set to true
|
"disabled": true // <-- Set to true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
Before running AISSIA in MCP server mode, ensure these config files exist:
|
Before running AISSIA in MCP server mode, ensure these config files exist:
|
||||||
|
|
||||||
### config/ai.json
|
### config/ai.json
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"provider": "claude",
|
"provider": "claude",
|
||||||
"api_key": "sk-ant-api03-...",
|
"api_key": "sk-ant-api03-...",
|
||||||
"model": "claude-sonnet-4-20250514",
|
"model": "claude-sonnet-4-20250514",
|
||||||
"max_iterations": 10,
|
"max_iterations": 10,
|
||||||
"system_prompt": "Tu es AISSIA, un assistant personnel intelligent..."
|
"system_prompt": "Tu es AISSIA, un assistant personnel intelligent..."
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### config/storage.json
|
### config/storage.json
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"database_path": "./data/aissia.db",
|
"database_path": "./data/aissia.db",
|
||||||
"journal_mode": "WAL",
|
"journal_mode": "WAL",
|
||||||
"busy_timeout_ms": 5000
|
"busy_timeout_ms": 5000
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### config/voice.json (optional)
|
### config/voice.json (optional)
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"tts": {
|
"tts": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"rate": 0,
|
"rate": 0,
|
||||||
"volume": 80
|
"volume": 80
|
||||||
},
|
},
|
||||||
"stt": {
|
"stt": {
|
||||||
"active_mode": {
|
"active_mode": {
|
||||||
"enabled": false
|
"enabled": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Testing MCP Server
|
## Testing MCP Server
|
||||||
|
|
||||||
You can test the MCP server independently before integrating with Claude Code:
|
You can test the MCP server independently before integrating with Claude Code:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Test tools/list
|
# Test tools/list
|
||||||
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | ./build/aissia.exe --mcp-server
|
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | ./build/aissia.exe --mcp-server
|
||||||
|
|
||||||
# Test chat_with_aissia tool
|
# Test chat_with_aissia tool
|
||||||
echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"chat_with_aissia","arguments":{"message":"What time is it?"}}}' | ./build/aissia.exe --mcp-server
|
echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"chat_with_aissia","arguments":{"message":"What time is it?"}}}' | ./build/aissia.exe --mcp-server
|
||||||
```
|
```
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### "Server not found" or "Connection failed"
|
### "Server not found" or "Connection failed"
|
||||||
|
|
||||||
1. Verify the `command` path is correct and points to `aissia.exe`
|
1. Verify the `command` path is correct and points to `aissia.exe`
|
||||||
2. Make sure AISSIA compiles successfully: `cmake --build build`
|
2. Make sure AISSIA compiles successfully: `cmake --build build`
|
||||||
3. Test running `./build/aissia.exe --mcp-server` manually
|
3. Test running `./build/aissia.exe --mcp-server` manually
|
||||||
|
|
||||||
### "LLMService not initialized"
|
### "LLMService not initialized"
|
||||||
|
|
||||||
AISSIA requires `config/ai.json` with a valid Claude API key. Check:
|
AISSIA requires `config/ai.json` with a valid Claude API key. Check:
|
||||||
1. File exists: `config/ai.json`
|
1. File exists: `config/ai.json`
|
||||||
2. API key is valid: `"api_key": "sk-ant-api03-..."`
|
2. API key is valid: `"api_key": "sk-ant-api03-..."`
|
||||||
3. Provider is set: `"provider": "claude"`
|
3. Provider is set: `"provider": "claude"`
|
||||||
|
|
||||||
### "Tool execution failed"
|
### "Tool execution failed"
|
||||||
|
|
||||||
Some tools have limited functionality in Phase 8 MVP:
|
Some tools have limited functionality in Phase 8 MVP:
|
||||||
- `transcribe_audio` - Not fully implemented yet (STT file support needed)
|
- `transcribe_audio` - Not fully implemented yet (STT file support needed)
|
||||||
- `text_to_speech` - Not fully implemented yet (TTS file output needed)
|
- `text_to_speech` - Not fully implemented yet (TTS file output needed)
|
||||||
- `save_memory` - Not fully implemented yet (Storage sync methods needed)
|
- `save_memory` - Not fully implemented yet (Storage sync methods needed)
|
||||||
- `search_memories` - Not fully implemented yet (Storage sync methods needed)
|
- `search_memories` - Not fully implemented yet (Storage sync methods needed)
|
||||||
|
|
||||||
These will be completed in Phase 8.1 and 8.2.
|
These will be completed in Phase 8.1 and 8.2.
|
||||||
|
|
||||||
### Server starts but tools don't appear
|
### Server starts but tools don't appear
|
||||||
|
|
||||||
1. Check Claude Code logs: `Ctrl+Shift+P` → "Developer: Open Extension Logs"
|
1. Check Claude Code logs: `Ctrl+Shift+P` → "Developer: Open Extension Logs"
|
||||||
2. Look for MCP server initialization errors
|
2. Look for MCP server initialization errors
|
||||||
3. Verify JSON syntax in the MCP configuration file
|
3. Verify JSON syntax in the MCP configuration file
|
||||||
|
|
||||||
## Example Use Cases
|
## Example Use Cases
|
||||||
|
|
||||||
### 1. Ask AISSIA for Help
|
### 1. Ask AISSIA for Help
|
||||||
|
|
||||||
```
|
```
|
||||||
You: Use chat_with_aissia to ask "What are my top productivity patterns?"
|
You: Use chat_with_aissia to ask "What are my top productivity patterns?"
|
||||||
Claude: [calls chat_with_aissia tool]
|
Claude: [calls chat_with_aissia tool]
|
||||||
AISSIA: Based on your activity data, your most productive hours are 9-11 AM...
|
AISSIA: Based on your activity data, your most productive hours are 9-11 AM...
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. File Operations + AI
|
### 2. File Operations + AI
|
||||||
|
|
||||||
```
|
```
|
||||||
You: Read my TODO.md file and ask AISSIA to prioritize the tasks
|
You: Read my TODO.md file and ask AISSIA to prioritize the tasks
|
||||||
Claude: [calls read_file("TODO.md")]
|
Claude: [calls read_file("TODO.md")]
|
||||||
Claude: [calls chat_with_aissia with task list]
|
Claude: [calls chat_with_aissia with task list]
|
||||||
AISSIA: Here's a prioritized version based on urgency and dependencies...
|
AISSIA: Here's a prioritized version based on urgency and dependencies...
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Voice Transcription (future)
|
### 3. Voice Transcription (future)
|
||||||
|
|
||||||
```
|
```
|
||||||
You: Transcribe meeting-notes.wav to text
|
You: Transcribe meeting-notes.wav to text
|
||||||
Claude: [calls transcribe_audio("meeting-notes.wav")]
|
Claude: [calls transcribe_audio("meeting-notes.wav")]
|
||||||
Result: "Welcome to the team meeting. Today we're discussing..."
|
Result: "Welcome to the team meeting. Today we're discussing..."
|
||||||
```
|
```
|
||||||
|
|
||||||
## Advanced Configuration
|
## Advanced Configuration
|
||||||
|
|
||||||
### Multiple MCP Servers
|
### Multiple MCP Servers
|
||||||
|
|
||||||
You can configure multiple MCP servers alongside AISSIA:
|
You can configure multiple MCP servers alongside AISSIA:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"aissia": {
|
"aissia": {
|
||||||
"command": "C:\\path\\to\\aissia\\build\\aissia.exe",
|
"command": "C:\\path\\to\\aissia\\build\\aissia.exe",
|
||||||
"args": ["--mcp-server"],
|
"args": ["--mcp-server"],
|
||||||
"disabled": false
|
"disabled": false
|
||||||
},
|
},
|
||||||
"filesystem": {
|
"filesystem": {
|
||||||
"command": "npx",
|
"command": "npx",
|
||||||
"args": ["-y", "@modelcontextprotocol/server-filesystem", "C:\\Users"],
|
"args": ["-y", "@modelcontextprotocol/server-filesystem", "C:\\Users"],
|
||||||
"disabled": false
|
"disabled": false
|
||||||
},
|
},
|
||||||
"brave-search": {
|
"brave-search": {
|
||||||
"command": "npx",
|
"command": "npx",
|
||||||
"args": ["-y", "@modelcontextprotocol/server-brave-search"],
|
"args": ["-y", "@modelcontextprotocol/server-brave-search"],
|
||||||
"disabled": false,
|
"disabled": false,
|
||||||
"env": {
|
"env": {
|
||||||
"BRAVE_API_KEY": "your-brave-api-key"
|
"BRAVE_API_KEY": "your-brave-api-key"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Environment Variables
|
### Environment Variables
|
||||||
|
|
||||||
Pass environment variables to AISSIA:
|
Pass environment variables to AISSIA:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"aissia": {
|
"aissia": {
|
||||||
"command": "C:\\path\\to\\aissia\\build\\aissia.exe",
|
"command": "C:\\path\\to\\aissia\\build\\aissia.exe",
|
||||||
"args": ["--mcp-server"],
|
"args": ["--mcp-server"],
|
||||||
"disabled": false,
|
"disabled": false,
|
||||||
"env": {
|
"env": {
|
||||||
"AISSIA_LOG_LEVEL": "debug",
|
"AISSIA_LOG_LEVEL": "debug",
|
||||||
"CLAUDE_API_KEY": "sk-ant-api03-..."
|
"CLAUDE_API_KEY": "sk-ant-api03-..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- **Full Documentation**: `docs/CLAUDE_CODE_INTEGRATION.md`
|
- **Full Documentation**: `docs/CLAUDE_CODE_INTEGRATION.md`
|
||||||
- **MCP Specification**: https://github.com/anthropics/mcp
|
- **MCP Specification**: https://github.com/anthropics/mcp
|
||||||
- **Claude Code Extension**: https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev
|
- **Claude Code Extension**: https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev
|
||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
For issues or questions:
|
For issues or questions:
|
||||||
1. Check the full documentation: `docs/CLAUDE_CODE_INTEGRATION.md`
|
1. Check the full documentation: `docs/CLAUDE_CODE_INTEGRATION.md`
|
||||||
2. Review logs: AISSIA writes to stderr in MCP mode
|
2. Review logs: AISSIA writes to stderr in MCP mode
|
||||||
3. Test manually: `./build/aissia.exe --mcp-server` and send JSON-RPC requests
|
3. Test manually: `./build/aissia.exe --mcp-server` and send JSON-RPC requests
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"aissia": {
|
"aissia": {
|
||||||
"command": "C:\\Users\\alexi\\Documents\\projects\\aissia\\build\\aissia.exe",
|
"command": "C:\\Users\\alexi\\Documents\\projects\\aissia\\build\\aissia.exe",
|
||||||
"args": ["--mcp-server"],
|
"args": ["--mcp-server"],
|
||||||
"disabled": false,
|
"disabled": false,
|
||||||
"alwaysAllow": []
|
"alwaysAllow": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,35 +1,35 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Generate test audio WAV file for STT testing"""
|
"""Generate test audio WAV file for STT testing"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from gtts import gTTS
|
from gtts import gTTS
|
||||||
import os
|
import os
|
||||||
from pydub import AudioSegment
|
from pydub import AudioSegment
|
||||||
|
|
||||||
# Generate French test audio
|
# Generate French test audio
|
||||||
text = "Bonjour, ceci est un test de reconnaissance vocale."
|
text = "Bonjour, ceci est un test de reconnaissance vocale."
|
||||||
print(f"Generating audio: '{text}'")
|
print(f"Generating audio: '{text}'")
|
||||||
|
|
||||||
# Create TTS
|
# Create TTS
|
||||||
tts = gTTS(text=text, lang='fr', slow=False)
|
tts = gTTS(text=text, lang='fr', slow=False)
|
||||||
tts.save("test_audio_temp.mp3")
|
tts.save("test_audio_temp.mp3")
|
||||||
print("✓ Generated MP3")
|
print("✓ Generated MP3")
|
||||||
|
|
||||||
# Convert to WAV (16kHz, mono, 16-bit PCM)
|
# Convert to WAV (16kHz, mono, 16-bit PCM)
|
||||||
audio = AudioSegment.from_mp3("test_audio_temp.mp3")
|
audio = AudioSegment.from_mp3("test_audio_temp.mp3")
|
||||||
audio = audio.set_frame_rate(16000).set_channels(1).set_sample_width(2)
|
audio = audio.set_frame_rate(16000).set_channels(1).set_sample_width(2)
|
||||||
audio.export("test_audio.wav", format="wav")
|
audio.export("test_audio.wav", format="wav")
|
||||||
print("✓ Converted to WAV (16kHz, mono, 16-bit)")
|
print("✓ Converted to WAV (16kHz, mono, 16-bit)")
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
os.remove("test_audio_temp.mp3")
|
os.remove("test_audio_temp.mp3")
|
||||||
print("✓ Saved as test_audio.wav")
|
print("✓ Saved as test_audio.wav")
|
||||||
print(f"Duration: {len(audio)/1000:.1f}s")
|
print(f"Duration: {len(audio)/1000:.1f}s")
|
||||||
|
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
print(f"Missing dependency: {e}")
|
print(f"Missing dependency: {e}")
|
||||||
print("\nInstall with: pip install gtts pydub")
|
print("\nInstall with: pip install gtts pydub")
|
||||||
print("Note: pydub also requires ffmpeg")
|
print("Note: pydub also requires ffmpeg")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|||||||
@ -1,38 +1,38 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Generate simple test audio WAV file using only stdlib"""
|
"""Generate simple test audio WAV file using only stdlib"""
|
||||||
|
|
||||||
import wave
|
import wave
|
||||||
import struct
|
import struct
|
||||||
import math
|
import math
|
||||||
|
|
||||||
# WAV parameters
|
# WAV parameters
|
||||||
sample_rate = 16000
|
sample_rate = 16000
|
||||||
duration = 2 # seconds
|
duration = 2 # seconds
|
||||||
frequency = 440 # Hz (A4 note)
|
frequency = 440 # Hz (A4 note)
|
||||||
|
|
||||||
# Generate sine wave samples
|
# Generate sine wave samples
|
||||||
samples = []
|
samples = []
|
||||||
for i in range(int(sample_rate * duration)):
|
for i in range(int(sample_rate * duration)):
|
||||||
# Sine wave value (-1.0 to 1.0)
|
# Sine wave value (-1.0 to 1.0)
|
||||||
value = math.sin(2.0 * math.pi * frequency * i / sample_rate)
|
value = math.sin(2.0 * math.pi * frequency * i / sample_rate)
|
||||||
|
|
||||||
# Convert to 16-bit PCM (-32768 to 32767)
|
# Convert to 16-bit PCM (-32768 to 32767)
|
||||||
sample = int(value * 32767)
|
sample = int(value * 32767)
|
||||||
samples.append(sample)
|
samples.append(sample)
|
||||||
|
|
||||||
# Write WAV file
|
# Write WAV file
|
||||||
with wave.open("test_audio.wav", "w") as wav_file:
|
with wave.open("test_audio.wav", "w") as wav_file:
|
||||||
# Set parameters (1 channel, 2 bytes per sample, 16kHz)
|
# Set parameters (1 channel, 2 bytes per sample, 16kHz)
|
||||||
wav_file.setnchannels(1)
|
wav_file.setnchannels(1)
|
||||||
wav_file.setsampwidth(2)
|
wav_file.setsampwidth(2)
|
||||||
wav_file.setframerate(sample_rate)
|
wav_file.setframerate(sample_rate)
|
||||||
|
|
||||||
# Write frames
|
# Write frames
|
||||||
for sample in samples:
|
for sample in samples:
|
||||||
wav_file.writeframes(struct.pack('<h', sample))
|
wav_file.writeframes(struct.pack('<h', sample))
|
||||||
|
|
||||||
print(f"[OK] Generated test_audio.wav")
|
print(f"[OK] Generated test_audio.wav")
|
||||||
print(f" - Format: 16kHz, mono, 16-bit PCM")
|
print(f" - Format: 16kHz, mono, 16-bit PCM")
|
||||||
print(f" - Duration: {duration}s")
|
print(f" - Duration: {duration}s")
|
||||||
print(f" - Frequency: {frequency}Hz (A4 tone)")
|
print(f" - Frequency: {frequency}Hz (A4 tone)")
|
||||||
print(f" - Samples: {len(samples)}")
|
print(f" - Samples: {len(samples)}")
|
||||||
|
|||||||
@ -1,449 +1,449 @@
|
|||||||
# AISSIA - Claude Code Integration (Phase 8)
|
# AISSIA - Claude Code Integration (Phase 8)
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
AISSIA can now be exposed as an **MCP Server** (Model Context Protocol) to integrate with Claude Code and other MCP-compatible clients. This allows Claude to use AISSIA's capabilities as tools during conversations.
|
AISSIA can now be exposed as an **MCP Server** (Model Context Protocol) to integrate with Claude Code and other MCP-compatible clients. This allows Claude to use AISSIA's capabilities as tools during conversations.
|
||||||
|
|
||||||
**Mode MCP Server**: `./aissia --mcp-server`
|
**Mode MCP Server**: `./aissia --mcp-server`
|
||||||
|
|
||||||
This mode exposes AISSIA's services via JSON-RPC 2.0 over stdio, following the MCP specification.
|
This mode exposes AISSIA's services via JSON-RPC 2.0 over stdio, following the MCP specification.
|
||||||
|
|
||||||
## Available Tools
|
## Available Tools
|
||||||
|
|
||||||
AISSIA exposes **13 tools** total:
|
AISSIA exposes **13 tools** total:
|
||||||
|
|
||||||
### 1. AISSIA Core Tools (Priority)
|
### 1. AISSIA Core Tools (Priority)
|
||||||
|
|
||||||
#### `chat_with_aissia` ⭐ **PRIORITY**
|
#### `chat_with_aissia` ⭐ **PRIORITY**
|
||||||
Dialogue with AISSIA's built-in AI assistant (Claude Sonnet 4). Send a message and get an intelligent response with access to AISSIA's knowledge and capabilities.
|
Dialogue with AISSIA's built-in AI assistant (Claude Sonnet 4). Send a message and get an intelligent response with access to AISSIA's knowledge and capabilities.
|
||||||
|
|
||||||
**Input**:
|
**Input**:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"message": "string (required) - Message to send to AISSIA",
|
"message": "string (required) - Message to send to AISSIA",
|
||||||
"conversation_id": "string (optional) - Conversation ID for continuity",
|
"conversation_id": "string (optional) - Conversation ID for continuity",
|
||||||
"system_prompt": "string (optional) - Custom system prompt"
|
"system_prompt": "string (optional) - Custom system prompt"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Output**:
|
**Output**:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"response": "AISSIA's response text",
|
"response": "AISSIA's response text",
|
||||||
"conversation_id": "conversation-id",
|
"conversation_id": "conversation-id",
|
||||||
"tokens": 1234,
|
"tokens": 1234,
|
||||||
"iterations": 2
|
"iterations": 2
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Example use case**: "Hey AISSIA, can you analyze my focus patterns this week?"
|
**Example use case**: "Hey AISSIA, can you analyze my focus patterns this week?"
|
||||||
|
|
||||||
#### `transcribe_audio`
|
#### `transcribe_audio`
|
||||||
Transcribe audio file to text using Speech-to-Text engines (Whisper.cpp, OpenAI Whisper API, Google Speech).
|
Transcribe audio file to text using Speech-to-Text engines (Whisper.cpp, OpenAI Whisper API, Google Speech).
|
||||||
|
|
||||||
**Input**:
|
**Input**:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"file_path": "string (required) - Path to audio file",
|
"file_path": "string (required) - Path to audio file",
|
||||||
"language": "string (optional) - Language code (e.g., 'fr', 'en'). Default: 'fr'"
|
"language": "string (optional) - Language code (e.g., 'fr', 'en'). Default: 'fr'"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Output**:
|
**Output**:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"text": "Transcribed text from audio",
|
"text": "Transcribed text from audio",
|
||||||
"file": "/path/to/audio.wav",
|
"file": "/path/to/audio.wav",
|
||||||
"language": "fr"
|
"language": "fr"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Status**: ⚠️ Not yet implemented - requires STT service file transcription support
|
**Status**: ⚠️ Not yet implemented - requires STT service file transcription support
|
||||||
|
|
||||||
#### `text_to_speech`
|
#### `text_to_speech`
|
||||||
Convert text to speech audio file using Text-to-Speech synthesis. Generates audio in WAV format.
|
Convert text to speech audio file using Text-to-Speech synthesis. Generates audio in WAV format.
|
||||||
|
|
||||||
**Input**:
|
**Input**:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"text": "string (required) - Text to synthesize",
|
"text": "string (required) - Text to synthesize",
|
||||||
"output_file": "string (required) - Output audio file path (WAV)",
|
"output_file": "string (required) - Output audio file path (WAV)",
|
||||||
"voice": "string (optional) - Voice identifier (e.g., 'fr-fr', 'en-us'). Default: 'fr-fr'"
|
"voice": "string (optional) - Voice identifier (e.g., 'fr-fr', 'en-us'). Default: 'fr-fr'"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Output**:
|
**Output**:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"success": true,
|
"success": true,
|
||||||
"file": "/path/to/output.wav",
|
"file": "/path/to/output.wav",
|
||||||
"voice": "fr-fr"
|
"voice": "fr-fr"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Status**: ⚠️ Not yet implemented - requires TTS engine file output support
|
**Status**: ⚠️ Not yet implemented - requires TTS engine file output support
|
||||||
|
|
||||||
#### `save_memory`
|
#### `save_memory`
|
||||||
Save a note or memory to AISSIA's persistent storage. Memories can be tagged and searched later.
|
Save a note or memory to AISSIA's persistent storage. Memories can be tagged and searched later.
|
||||||
|
|
||||||
**Input**:
|
**Input**:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"title": "string (required) - Memory title",
|
"title": "string (required) - Memory title",
|
||||||
"content": "string (required) - Memory content",
|
"content": "string (required) - Memory content",
|
||||||
"tags": ["array of strings (optional) - Tags for categorization"]
|
"tags": ["array of strings (optional) - Tags for categorization"]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Output**:
|
**Output**:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"id": "memory-uuid",
|
"id": "memory-uuid",
|
||||||
"title": "Meeting notes",
|
"title": "Meeting notes",
|
||||||
"timestamp": "2025-01-30T10:00:00Z"
|
"timestamp": "2025-01-30T10:00:00Z"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Status**: ⚠️ Not yet implemented - requires StorageService sync methods
|
**Status**: ⚠️ Not yet implemented - requires StorageService sync methods
|
||||||
|
|
||||||
#### `search_memories`
|
#### `search_memories`
|
||||||
Search through saved memories and notes in AISSIA's storage. Returns matching memories with relevance scores.
|
Search through saved memories and notes in AISSIA's storage. Returns matching memories with relevance scores.
|
||||||
|
|
||||||
**Input**:
|
**Input**:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"query": "string (required) - Search query",
|
"query": "string (required) - Search query",
|
||||||
"limit": "integer (optional) - Maximum results to return. Default: 10"
|
"limit": "integer (optional) - Maximum results to return. Default: 10"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Output**:
|
**Output**:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"results": [
|
"results": [
|
||||||
{
|
{
|
||||||
"id": "memory-uuid",
|
"id": "memory-uuid",
|
||||||
"title": "Meeting notes",
|
"title": "Meeting notes",
|
||||||
"content": "...",
|
"content": "...",
|
||||||
"score": 0.85,
|
"score": 0.85,
|
||||||
"tags": ["work", "meeting"]
|
"tags": ["work", "meeting"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"count": 5
|
"count": 5
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Status**: ⚠️ Not yet implemented - requires StorageService sync methods
|
**Status**: ⚠️ Not yet implemented - requires StorageService sync methods
|
||||||
|
|
||||||
### 2. File System Tools (8 tools)
|
### 2. File System Tools (8 tools)
|
||||||
|
|
||||||
- `read_file` - Read a file from the filesystem
|
- `read_file` - Read a file from the filesystem
|
||||||
- `write_file` - Write content to a file
|
- `write_file` - Write content to a file
|
||||||
- `list_directory` - List files in a directory
|
- `list_directory` - List files in a directory
|
||||||
- `search_files` - Search for files by pattern
|
- `search_files` - Search for files by pattern
|
||||||
- `file_exists` - Check if a file exists
|
- `file_exists` - Check if a file exists
|
||||||
- `create_directory` - Create a new directory
|
- `create_directory` - Create a new directory
|
||||||
- `delete_file` - Delete a file
|
- `delete_file` - Delete a file
|
||||||
- `move_file` - Move or rename a file
|
- `move_file` - Move or rename a file
|
||||||
|
|
||||||
These tools provide Claude with direct filesystem access to work with files on your system.
|
These tools provide Claude with direct filesystem access to work with files on your system.
|
||||||
|
|
||||||
## Installation for Claude Code
|
## Installation for Claude Code
|
||||||
|
|
||||||
### 1. Configure Claude Code MCP
|
### 1. Configure Claude Code MCP
|
||||||
|
|
||||||
Create or edit your Claude Code MCP configuration file:
|
Create or edit your Claude Code MCP configuration file:
|
||||||
|
|
||||||
**Windows**: `%APPDATA%\Code\User\globalStorage\saoudrizwan.claude-dev\settings\cline_mcp_settings.json`
|
**Windows**: `%APPDATA%\Code\User\globalStorage\saoudrizwan.claude-dev\settings\cline_mcp_settings.json`
|
||||||
**macOS/Linux**: `~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json`
|
**macOS/Linux**: `~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json`
|
||||||
|
|
||||||
Add AISSIA as an MCP server:
|
Add AISSIA as an MCP server:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"aissia": {
|
"aissia": {
|
||||||
"command": "C:\\path\\to\\aissia\\build\\aissia.exe",
|
"command": "C:\\path\\to\\aissia\\build\\aissia.exe",
|
||||||
"args": ["--mcp-server"],
|
"args": ["--mcp-server"],
|
||||||
"disabled": false
|
"disabled": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Note**: Replace `C:\\path\\to\\aissia\\build\\aissia.exe` with the actual path to your compiled AISSIA executable.
|
**Note**: Replace `C:\\path\\to\\aissia\\build\\aissia.exe` with the actual path to your compiled AISSIA executable.
|
||||||
|
|
||||||
### 2. Verify Configuration
|
### 2. Verify Configuration
|
||||||
|
|
||||||
Restart Claude Code (or VS Code) to reload the MCP configuration.
|
Restart Claude Code (or VS Code) to reload the MCP configuration.
|
||||||
|
|
||||||
Claude should now have access to all 13 AISSIA tools during conversations.
|
Claude should now have access to all 13 AISSIA tools during conversations.
|
||||||
|
|
||||||
### 3. Test Integration
|
### 3. Test Integration
|
||||||
|
|
||||||
In Claude Code, try:
|
In Claude Code, try:
|
||||||
|
|
||||||
```
|
```
|
||||||
"Can you use the chat_with_aissia tool to ask AISSIA what time it is?"
|
"Can you use the chat_with_aissia tool to ask AISSIA what time it is?"
|
||||||
```
|
```
|
||||||
|
|
||||||
Claude will call the `chat_with_aissia` tool, which internally uses AISSIA's LLM service to process the query.
|
Claude will call the `chat_with_aissia` tool, which internally uses AISSIA's LLM service to process the query.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
### Synchronous Mode (MCP Server)
|
### Synchronous Mode (MCP Server)
|
||||||
|
|
||||||
When running as an MCP server, AISSIA uses **synchronous blocking calls** instead of the async pub/sub architecture used in normal mode:
|
When running as an MCP server, AISSIA uses **synchronous blocking calls** instead of the async pub/sub architecture used in normal mode:
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
// Normal mode (async)
|
// Normal mode (async)
|
||||||
io->publish("llm:request", data);
|
io->publish("llm:request", data);
|
||||||
// ... wait for response on "llm:response" topic
|
// ... wait for response on "llm:response" topic
|
||||||
|
|
||||||
// MCP mode (sync)
|
// MCP mode (sync)
|
||||||
auto response = llmService->sendMessageSync(message, conversationId);
|
auto response = llmService->sendMessageSync(message, conversationId);
|
||||||
// immediate result
|
// immediate result
|
||||||
```
|
```
|
||||||
|
|
||||||
This is necessary because:
|
This is necessary because:
|
||||||
1. MCP protocol expects immediate JSON-RPC responses
|
1. MCP protocol expects immediate JSON-RPC responses
|
||||||
2. No event loop in MCP server mode (stdin/stdout blocking I/O)
|
2. No event loop in MCP server mode (stdin/stdout blocking I/O)
|
||||||
3. Simplifies integration with external tools
|
3. Simplifies integration with external tools
|
||||||
|
|
||||||
### Service Integration
|
### Service Integration
|
||||||
|
|
||||||
```
|
```
|
||||||
MCPServer (stdio JSON-RPC)
|
MCPServer (stdio JSON-RPC)
|
||||||
↓
|
↓
|
||||||
MCPServerTools (tool handlers)
|
MCPServerTools (tool handlers)
|
||||||
↓
|
↓
|
||||||
Services (sync methods)
|
Services (sync methods)
|
||||||
├── LLMService::sendMessageSync()
|
├── LLMService::sendMessageSync()
|
||||||
├── VoiceService::transcribeFileSync()
|
├── VoiceService::transcribeFileSync()
|
||||||
├── VoiceService::textToSpeechSync()
|
├── VoiceService::textToSpeechSync()
|
||||||
└── StorageService (stub implementations)
|
└── StorageService (stub implementations)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Tool Registry
|
### Tool Registry
|
||||||
|
|
||||||
All tools are registered in a central `ToolRegistry`:
|
All tools are registered in a central `ToolRegistry`:
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
ToolRegistry registry;
|
ToolRegistry registry;
|
||||||
|
|
||||||
// 1. Internal tools (get_current_time)
|
// 1. Internal tools (get_current_time)
|
||||||
registry.registerTool("get_current_time", ...);
|
registry.registerTool("get_current_time", ...);
|
||||||
|
|
||||||
// 2. FileSystem tools (8 tools)
|
// 2. FileSystem tools (8 tools)
|
||||||
for (auto& toolDef : FileSystemTools::getToolDefinitions()) {
|
for (auto& toolDef : FileSystemTools::getToolDefinitions()) {
|
||||||
registry.registerTool(toolDef);
|
registry.registerTool(toolDef);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. AISSIA tools (5 tools)
|
// 3. AISSIA tools (5 tools)
|
||||||
MCPServerTools aissiaTools(llmService, storageService, voiceService);
|
MCPServerTools aissiaTools(llmService, storageService, voiceService);
|
||||||
for (const auto& toolDef : aissiaTools.getToolDefinitions()) {
|
for (const auto& toolDef : aissiaTools.getToolDefinitions()) {
|
||||||
registry.registerTool(toolDef);
|
registry.registerTool(toolDef);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Total: **13 tools**
|
Total: **13 tools**
|
||||||
|
|
||||||
## Configuration Files
|
## Configuration Files
|
||||||
|
|
||||||
AISSIA MCP Server requires these config files (same as normal mode):
|
AISSIA MCP Server requires these config files (same as normal mode):
|
||||||
|
|
||||||
- `config/ai.json` - LLM provider configuration (Claude API key)
|
- `config/ai.json` - LLM provider configuration (Claude API key)
|
||||||
- `config/storage.json` - Database path and settings
|
- `config/storage.json` - Database path and settings
|
||||||
- `config/voice.json` - TTS/STT engine settings
|
- `config/voice.json` - TTS/STT engine settings
|
||||||
|
|
||||||
**Important**: Make sure these files are present before running `--mcp-server` mode.
|
**Important**: Make sure these files are present before running `--mcp-server` mode.
|
||||||
|
|
||||||
## Limitations (Phase 8 MVP)
|
## Limitations (Phase 8 MVP)
|
||||||
|
|
||||||
1. **STT/TTS file operations**: `transcribe_audio` and `text_to_speech` are not fully implemented yet
|
1. **STT/TTS file operations**: `transcribe_audio` and `text_to_speech` are not fully implemented yet
|
||||||
- STT service needs file transcription support (currently only streaming)
|
- STT service needs file transcription support (currently only streaming)
|
||||||
- TTS engine needs file output support (currently only direct playback)
|
- TTS engine needs file output support (currently only direct playback)
|
||||||
|
|
||||||
2. **Storage sync methods**: `save_memory` and `search_memories` return "not implemented" errors
|
2. **Storage sync methods**: `save_memory` and `search_memories` return "not implemented" errors
|
||||||
- StorageService needs `saveMemorySync()` and `searchMemoriesSync()` methods
|
- StorageService needs `saveMemorySync()` and `searchMemoriesSync()` methods
|
||||||
- Current storage only works via async pub/sub
|
- Current storage only works via async pub/sub
|
||||||
|
|
||||||
3. **No hot-reload**: MCP server mode doesn't load hot-reloadable modules
|
3. **No hot-reload**: MCP server mode doesn't load hot-reloadable modules
|
||||||
- Only services and tools are available
|
- Only services and tools are available
|
||||||
- No SchedulerModule, MonitoringModule, etc.
|
- No SchedulerModule, MonitoringModule, etc.
|
||||||
|
|
||||||
4. **Single-threaded**: MCP server runs synchronously on main thread
|
4. **Single-threaded**: MCP server runs synchronously on main thread
|
||||||
- LLMService worker thread still runs for agentic loops
|
- LLMService worker thread still runs for agentic loops
|
||||||
- But overall server is blocking on stdin
|
- But overall server is blocking on stdin
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
### Phase 8.1 - Complete STT/TTS Sync Methods
|
### Phase 8.1 - Complete STT/TTS Sync Methods
|
||||||
- [ ] Implement `VoiceService::transcribeFileSync()` using STT engines
|
- [ ] Implement `VoiceService::transcribeFileSync()` using STT engines
|
||||||
- [ ] Implement `VoiceService::textToSpeechSync()` with file output
|
- [ ] Implement `VoiceService::textToSpeechSync()` with file output
|
||||||
- [ ] Test audio file transcription via MCP
|
- [ ] Test audio file transcription via MCP
|
||||||
|
|
||||||
### Phase 8.2 - Storage Sync Methods
|
### Phase 8.2 - Storage Sync Methods
|
||||||
- [ ] Implement `StorageService::saveMemorySync()`
|
- [ ] Implement `StorageService::saveMemorySync()`
|
||||||
- [ ] Implement `StorageService::searchMemoriesSync()`
|
- [ ] Implement `StorageService::searchMemoriesSync()`
|
||||||
- [ ] Add vector embeddings for semantic search
|
- [ ] Add vector embeddings for semantic search
|
||||||
|
|
||||||
### Phase 8.3 - Advanced Tools
|
### Phase 8.3 - Advanced Tools
|
||||||
- [ ] `schedule_task` - Add tasks to AISSIA's scheduler
|
- [ ] `schedule_task` - Add tasks to AISSIA's scheduler
|
||||||
- [ ] `get_focus_stats` - Retrieve hyperfocus detection stats
|
- [ ] `get_focus_stats` - Retrieve hyperfocus detection stats
|
||||||
- [ ] `list_active_apps` - Get current monitored applications
|
- [ ] `list_active_apps` - Get current monitored applications
|
||||||
- [ ] `send_notification` - Trigger system notifications
|
- [ ] `send_notification` - Trigger system notifications
|
||||||
|
|
||||||
### Phase 8.4 - Multi-Modal Support
|
### Phase 8.4 - Multi-Modal Support
|
||||||
- [ ] Image input for LLM (Claude vision)
|
- [ ] Image input for LLM (Claude vision)
|
||||||
- [ ] PDF/document parsing tools
|
- [ ] PDF/document parsing tools
|
||||||
- [ ] Web scraping integration
|
- [ ] Web scraping integration
|
||||||
|
|
||||||
## Use Cases
|
## Use Cases
|
||||||
|
|
||||||
### 1. AI Assistant Collaboration
|
### 1. AI Assistant Collaboration
|
||||||
|
|
||||||
Claude Code can delegate complex reasoning tasks to AISSIA:
|
Claude Code can delegate complex reasoning tasks to AISSIA:
|
||||||
|
|
||||||
```
|
```
|
||||||
Claude: "I need to analyze user behavior patterns. Let me ask AISSIA."
|
Claude: "I need to analyze user behavior patterns. Let me ask AISSIA."
|
||||||
→ calls chat_with_aissia("Analyze recent focus patterns")
|
→ calls chat_with_aissia("Analyze recent focus patterns")
|
||||||
AISSIA: "Based on monitoring data, user has 3 hyperfocus sessions daily averaging 2.5 hours..."
|
AISSIA: "Based on monitoring data, user has 3 hyperfocus sessions daily averaging 2.5 hours..."
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Voice Transcription Workflow
|
### 2. Voice Transcription Workflow
|
||||||
|
|
||||||
```
|
```
|
||||||
Claude: "Transcribe meeting-2025-01-30.wav"
|
Claude: "Transcribe meeting-2025-01-30.wav"
|
||||||
→ calls transcribe_audio(file_path="meeting-2025-01-30.wav", language="en")
|
→ calls transcribe_audio(file_path="meeting-2025-01-30.wav", language="en")
|
||||||
→ calls write_file(path="transcript.txt", content=result)
|
→ calls write_file(path="transcript.txt", content=result)
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Knowledge Management
|
### 3. Knowledge Management
|
||||||
|
|
||||||
```
|
```
|
||||||
Claude: "Save this important insight to AISSIA's memory"
|
Claude: "Save this important insight to AISSIA's memory"
|
||||||
→ calls save_memory(
|
→ calls save_memory(
|
||||||
title="Project architecture decision",
|
title="Project architecture decision",
|
||||||
content="We decided to use hot-reload modules for business logic...",
|
content="We decided to use hot-reload modules for business logic...",
|
||||||
tags=["architecture", "project"]
|
tags=["architecture", "project"]
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. File + AI Operations
|
### 4. File + AI Operations
|
||||||
|
|
||||||
```
|
```
|
||||||
Claude: "Read todos.md, ask AISSIA to prioritize tasks, update file"
|
Claude: "Read todos.md, ask AISSIA to prioritize tasks, update file"
|
||||||
→ calls read_file("todos.md")
|
→ calls read_file("todos.md")
|
||||||
→ calls chat_with_aissia("Prioritize these tasks: ...")
|
→ calls chat_with_aissia("Prioritize these tasks: ...")
|
||||||
→ calls write_file("todos-prioritized.md", content=...)
|
→ calls write_file("todos-prioritized.md", content=...)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
### Adding New Tools
|
### Adding New Tools
|
||||||
|
|
||||||
1. **Declare tool in MCPServerTools.hpp**:
|
1. **Declare tool in MCPServerTools.hpp**:
|
||||||
```cpp
|
```cpp
|
||||||
json handleNewTool(const json& input);
|
json handleNewTool(const json& input);
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Implement in MCPServerTools.cpp**:
|
2. **Implement in MCPServerTools.cpp**:
|
||||||
```cpp
|
```cpp
|
||||||
json MCPServerTools::handleNewTool(const json& input) {
|
json MCPServerTools::handleNewTool(const json& input) {
|
||||||
// Extract input parameters
|
// Extract input parameters
|
||||||
std::string param = input["param"];
|
std::string param = input["param"];
|
||||||
|
|
||||||
// Call service
|
// Call service
|
||||||
auto result = m_someService->doSomethingSync(param);
|
auto result = m_someService->doSomethingSync(param);
|
||||||
|
|
||||||
// Return JSON result
|
// Return JSON result
|
||||||
return {
|
return {
|
||||||
{"output", result},
|
{"output", result},
|
||||||
{"status", "success"}
|
{"status", "success"}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Register in getToolDefinitions()**:
|
3. **Register in getToolDefinitions()**:
|
||||||
```cpp
|
```cpp
|
||||||
tools.push_back({
|
tools.push_back({
|
||||||
"new_tool",
|
"new_tool",
|
||||||
"Description of what this tool does",
|
"Description of what this tool does",
|
||||||
{
|
{
|
||||||
{"type", "object"},
|
{"type", "object"},
|
||||||
{"properties", {
|
{"properties", {
|
||||||
{"param", {
|
{"param", {
|
||||||
{"type", "string"},
|
{"type", "string"},
|
||||||
{"description", "Parameter description"}
|
{"description", "Parameter description"}
|
||||||
}}
|
}}
|
||||||
}},
|
}},
|
||||||
{"required", json::array({"param"})}
|
{"required", json::array({"param"})}
|
||||||
},
|
},
|
||||||
[this](const json& input) { return handleNewTool(input); }
|
[this](const json& input) { return handleNewTool(input); }
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
4. **Add to execute() switch**:
|
4. **Add to execute() switch**:
|
||||||
```cpp
|
```cpp
|
||||||
if (toolName == "new_tool") {
|
if (toolName == "new_tool") {
|
||||||
return handleNewTool(input);
|
return handleNewTool(input);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Testing MCP Server
|
### Testing MCP Server
|
||||||
|
|
||||||
Test with `nc` or `socat`:
|
Test with `nc` or `socat`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Send tools/list request
|
# Send tools/list request
|
||||||
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | ./build/aissia.exe --mcp-server
|
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | ./build/aissia.exe --mcp-server
|
||||||
|
|
||||||
# Send tool call
|
# Send tool call
|
||||||
echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"chat_with_aissia","arguments":{"message":"Hello AISSIA"}}}' | ./build/aissia.exe --mcp-server
|
echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"chat_with_aissia","arguments":{"message":"Hello AISSIA"}}}' | ./build/aissia.exe --mcp-server
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected output format:
|
Expected output format:
|
||||||
```json
|
```json
|
||||||
{"jsonrpc":"2.0","id":1,"result":{"tools":[{"name":"chat_with_aissia","description":"...","inputSchema":{...}}]}}
|
{"jsonrpc":"2.0","id":1,"result":{"tools":[{"name":"chat_with_aissia","description":"...","inputSchema":{...}}]}}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### "LLMService not initialized"
|
### "LLMService not initialized"
|
||||||
|
|
||||||
Make sure `config/ai.json` exists with valid API key:
|
Make sure `config/ai.json` exists with valid API key:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"provider": "claude",
|
"provider": "claude",
|
||||||
"api_key": "sk-ant-...",
|
"api_key": "sk-ant-...",
|
||||||
"model": "claude-sonnet-4-20250514"
|
"model": "claude-sonnet-4-20250514"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### "VoiceService not available"
|
### "VoiceService not available"
|
||||||
|
|
||||||
Voice tools are optional. If you don't need STT/TTS, this is normal.
|
Voice tools are optional. If you don't need STT/TTS, this is normal.
|
||||||
|
|
||||||
### "StorageService not available"
|
### "StorageService not available"
|
||||||
|
|
||||||
Make sure `config/storage.json` exists:
|
Make sure `config/storage.json` exists:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"database_path": "./data/aissia.db",
|
"database_path": "./data/aissia.db",
|
||||||
"journal_mode": "WAL",
|
"journal_mode": "WAL",
|
||||||
"busy_timeout_ms": 5000
|
"busy_timeout_ms": 5000
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### "Tool not found"
|
### "Tool not found"
|
||||||
|
|
||||||
Check `tools/list` output to see which tools are actually registered.
|
Check `tools/list` output to see which tools are actually registered.
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- **MCP Specification**: https://github.com/anthropics/mcp
|
- **MCP Specification**: https://github.com/anthropics/mcp
|
||||||
- **AISSIA Architecture**: `docs/project-overview.md`
|
- **AISSIA Architecture**: `docs/project-overview.md`
|
||||||
- **GroveEngine Guide**: `docs/GROVEENGINE_GUIDE.md`
|
- **GroveEngine Guide**: `docs/GROVEENGINE_GUIDE.md`
|
||||||
- **LLM Service**: `src/services/LLMService.hpp`
|
- **LLM Service**: `src/services/LLMService.hpp`
|
||||||
- **MCPServer**: `src/shared/mcp/MCPServer.hpp`
|
- **MCPServer**: `src/shared/mcp/MCPServer.hpp`
|
||||||
|
|||||||
@ -1,268 +1,268 @@
|
|||||||
# Speech-to-Text (STT) Setup Guide - Windows
|
# Speech-to-Text (STT) Setup Guide - Windows
|
||||||
|
|
||||||
Guide pour configurer les moteurs de reconnaissance vocale STT sur Windows.
|
Guide pour configurer les moteurs de reconnaissance vocale STT sur Windows.
|
||||||
|
|
||||||
## État Actuel
|
## État Actuel
|
||||||
|
|
||||||
AISSIA supporte **5 moteurs STT** avec priorités automatiques :
|
AISSIA supporte **5 moteurs STT** avec priorités automatiques :
|
||||||
|
|
||||||
| Moteur | Type | Status | Requis |
|
| Moteur | Type | Status | Requis |
|
||||||
|--------|------|--------|--------|
|
|--------|------|--------|--------|
|
||||||
| **Whisper.cpp** | Local | ✅ Configuré | Modèle téléchargé |
|
| **Whisper.cpp** | Local | ✅ Configuré | Modèle téléchargé |
|
||||||
| **OpenAI Whisper API** | Cloud | ✅ Configuré | API key dans .env |
|
| **OpenAI Whisper API** | Cloud | ✅ Configuré | API key dans .env |
|
||||||
| **Google Speech** | Cloud | ✅ Configuré | API key dans .env |
|
| **Google Speech** | Cloud | ✅ Configuré | API key dans .env |
|
||||||
| **Azure STT** | Cloud | ⚠️ Optionnel | API key manquante |
|
| **Azure STT** | Cloud | ⚠️ Optionnel | API key manquante |
|
||||||
| **Deepgram** | Cloud | ⚠️ Optionnel | API key manquante |
|
| **Deepgram** | Cloud | ⚠️ Optionnel | API key manquante |
|
||||||
|
|
||||||
**3 moteurs sont déjà fonctionnels** (Whisper.cpp, OpenAI, Google) ✅
|
**3 moteurs sont déjà fonctionnels** (Whisper.cpp, OpenAI, Google) ✅
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Whisper.cpp (Local, Offline) ✅
|
## 1. Whisper.cpp (Local, Offline) ✅
|
||||||
|
|
||||||
### Avantages
|
### Avantages
|
||||||
- ✅ Complètement offline (pas d'internet requis)
|
- ✅ Complètement offline (pas d'internet requis)
|
||||||
- ✅ Excellente précision (qualité OpenAI Whisper)
|
- ✅ Excellente précision (qualité OpenAI Whisper)
|
||||||
- ✅ Gratuit, pas de limite d'utilisation
|
- ✅ Gratuit, pas de limite d'utilisation
|
||||||
- ✅ Support multilingue (99 langues)
|
- ✅ Support multilingue (99 langues)
|
||||||
- ❌ Plus lent que les APIs cloud (temps réel difficile)
|
- ❌ Plus lent que les APIs cloud (temps réel difficile)
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
**Modèle téléchargé** : `models/ggml-base.bin` (142MB)
|
**Modèle téléchargé** : `models/ggml-base.bin` (142MB)
|
||||||
|
|
||||||
Autres modèles disponibles :
|
Autres modèles disponibles :
|
||||||
```bash
|
```bash
|
||||||
cd models/
|
cd models/
|
||||||
|
|
||||||
# Tiny (75MB) - Rapide mais moins précis
|
# Tiny (75MB) - Rapide mais moins précis
|
||||||
curl -L -o ggml-tiny.bin https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-tiny.bin
|
curl -L -o ggml-tiny.bin https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-tiny.bin
|
||||||
|
|
||||||
# Small (466MB) - Bon compromis
|
# Small (466MB) - Bon compromis
|
||||||
curl -L -o ggml-small.bin https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-small.bin
|
curl -L -o ggml-small.bin https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-small.bin
|
||||||
|
|
||||||
# Medium (1.5GB) - Très bonne qualité
|
# Medium (1.5GB) - Très bonne qualité
|
||||||
curl -L -o ggml-medium.bin https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-medium.bin
|
curl -L -o ggml-medium.bin https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-medium.bin
|
||||||
|
|
||||||
# Large (2.9GB) - Meilleure qualité
|
# Large (2.9GB) - Meilleure qualité
|
||||||
curl -L -o ggml-large-v3.bin https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-large-v3.bin
|
curl -L -o ggml-large-v3.bin https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-large-v3.bin
|
||||||
```
|
```
|
||||||
|
|
||||||
**Recommandé** : `base` ou `small` pour la plupart des usages.
|
**Recommandé** : `base` ou `small` pour la plupart des usages.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. OpenAI Whisper API ✅
|
## 2. OpenAI Whisper API ✅
|
||||||
|
|
||||||
### Avantages
|
### Avantages
|
||||||
- ✅ Très rapide (temps réel)
|
- ✅ Très rapide (temps réel)
|
||||||
- ✅ Excellente précision
|
- ✅ Excellente précision
|
||||||
- ✅ Support multilingue
|
- ✅ Support multilingue
|
||||||
- ❌ Requiert internet
|
- ❌ Requiert internet
|
||||||
- ❌ Coût : $0.006/minute ($0.36/heure)
|
- ❌ Coût : $0.006/minute ($0.36/heure)
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
1. Obtenir une clé API OpenAI : https://platform.openai.com/api-keys
|
1. Obtenir une clé API OpenAI : https://platform.openai.com/api-keys
|
||||||
2. Ajouter à `.env` :
|
2. Ajouter à `.env` :
|
||||||
```bash
|
```bash
|
||||||
OPENAI_API_KEY=sk-proj-...
|
OPENAI_API_KEY=sk-proj-...
|
||||||
```
|
```
|
||||||
|
|
||||||
**Status** : ✅ Déjà configuré
|
**Status** : ✅ Déjà configuré
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Google Speech-to-Text ✅
|
## 3. Google Speech-to-Text ✅
|
||||||
|
|
||||||
### Avantages
|
### Avantages
|
||||||
- ✅ Très rapide
|
- ✅ Très rapide
|
||||||
- ✅ Bonne précision
|
- ✅ Bonne précision
|
||||||
- ✅ Support multilingue (125+ langues)
|
- ✅ Support multilingue (125+ langues)
|
||||||
- ❌ Requiert internet
|
- ❌ Requiert internet
|
||||||
- ❌ Coût : $0.006/15s ($1.44/heure)
|
- ❌ Coût : $0.006/15s ($1.44/heure)
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
1. Activer l'API : https://console.cloud.google.com/apis/library/speech.googleapis.com
|
1. Activer l'API : https://console.cloud.google.com/apis/library/speech.googleapis.com
|
||||||
2. Créer une clé API
|
2. Créer une clé API
|
||||||
3. Ajouter à `.env` :
|
3. Ajouter à `.env` :
|
||||||
```bash
|
```bash
|
||||||
GOOGLE_API_KEY=AIzaSy...
|
GOOGLE_API_KEY=AIzaSy...
|
||||||
```
|
```
|
||||||
|
|
||||||
**Status** : ✅ Déjà configuré
|
**Status** : ✅ Déjà configuré
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Azure Speech-to-Text (Optionnel)
|
## 4. Azure Speech-to-Text (Optionnel)
|
||||||
|
|
||||||
### Avantages
|
### Avantages
|
||||||
- ✅ Excellente précision
|
- ✅ Excellente précision
|
||||||
- ✅ Support multilingue
|
- ✅ Support multilingue
|
||||||
- ✅ Free tier : 5h/mois gratuit
|
- ✅ Free tier : 5h/mois gratuit
|
||||||
- ❌ Requiert internet
|
- ❌ Requiert internet
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
1. Créer une ressource Azure Speech : https://portal.azure.com
|
1. Créer une ressource Azure Speech : https://portal.azure.com
|
||||||
2. Copier la clé et la région
|
2. Copier la clé et la région
|
||||||
3. Ajouter à `.env` :
|
3. Ajouter à `.env` :
|
||||||
```bash
|
```bash
|
||||||
AZURE_SPEECH_KEY=votre_cle_azure
|
AZURE_SPEECH_KEY=votre_cle_azure
|
||||||
AZURE_SPEECH_REGION=westeurope # ou votre région
|
AZURE_SPEECH_REGION=westeurope # ou votre région
|
||||||
```
|
```
|
||||||
|
|
||||||
**Status** : ⚠️ Optionnel (non configuré)
|
**Status** : ⚠️ Optionnel (non configuré)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. Deepgram (Optionnel)
|
## 5. Deepgram (Optionnel)
|
||||||
|
|
||||||
### Avantages
|
### Avantages
|
||||||
- ✅ Très rapide (streaming temps réel)
|
- ✅ Très rapide (streaming temps réel)
|
||||||
- ✅ Bonne précision
|
- ✅ Bonne précision
|
||||||
- ✅ Free tier : $200 crédit / 45,000 minutes
|
- ✅ Free tier : $200 crédit / 45,000 minutes
|
||||||
- ❌ Requiert internet
|
- ❌ Requiert internet
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
1. Créer un compte : https://console.deepgram.com
|
1. Créer un compte : https://console.deepgram.com
|
||||||
2. Créer une API key
|
2. Créer une API key
|
||||||
3. Ajouter à `.env` :
|
3. Ajouter à `.env` :
|
||||||
```bash
|
```bash
|
||||||
DEEPGRAM_API_KEY=votre_cle_deepgram
|
DEEPGRAM_API_KEY=votre_cle_deepgram
|
||||||
```
|
```
|
||||||
|
|
||||||
**Status** : ⚠️ Optionnel (non configuré)
|
**Status** : ⚠️ Optionnel (non configuré)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Tester les Moteurs STT
|
## Tester les Moteurs STT
|
||||||
|
|
||||||
### Option 1 : Test avec fichier audio
|
### Option 1 : Test avec fichier audio
|
||||||
|
|
||||||
1. Générer un fichier audio de test :
|
1. Générer un fichier audio de test :
|
||||||
```bash
|
```bash
|
||||||
python create_test_audio_simple.py
|
python create_test_audio_simple.py
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Lancer le test (quand compilé) :
|
2. Lancer le test (quand compilé) :
|
||||||
```bash
|
```bash
|
||||||
./build/test_stt_live test_audio.wav
|
./build/test_stt_live test_audio.wav
|
||||||
```
|
```
|
||||||
|
|
||||||
Ceci testera automatiquement tous les moteurs disponibles.
|
Ceci testera automatiquement tous les moteurs disponibles.
|
||||||
|
|
||||||
### Option 2 : Test depuis AISSIA
|
### Option 2 : Test depuis AISSIA
|
||||||
|
|
||||||
Les moteurs STT sont intégrés dans `VoiceModule` et accessibles via :
|
Les moteurs STT sont intégrés dans `VoiceModule` et accessibles via :
|
||||||
- `voice:start_listening` (pub/sub)
|
- `voice:start_listening` (pub/sub)
|
||||||
- `voice:stop_listening`
|
- `voice:stop_listening`
|
||||||
- `voice:transcribe` (avec fichier audio)
|
- `voice:transcribe` (avec fichier audio)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Configuration Recommandée
|
## Configuration Recommandée
|
||||||
|
|
||||||
Pour un usage optimal, voici l'ordre de priorité recommandé :
|
Pour un usage optimal, voici l'ordre de priorité recommandé :
|
||||||
|
|
||||||
### Pour développement/tests locaux
|
### Pour développement/tests locaux
|
||||||
1. **Whisper.cpp** (`ggml-base.bin`) - Offline, gratuit
|
1. **Whisper.cpp** (`ggml-base.bin`) - Offline, gratuit
|
||||||
2. **OpenAI Whisper API** - Si internet disponible
|
2. **OpenAI Whisper API** - Si internet disponible
|
||||||
3. **Google Speech** - Fallback
|
3. **Google Speech** - Fallback
|
||||||
|
|
||||||
### Pour production/temps réel
|
### Pour production/temps réel
|
||||||
1. **Deepgram** - Meilleur streaming temps réel
|
1. **Deepgram** - Meilleur streaming temps réel
|
||||||
2. **Azure STT** - Bonne qualité, free tier
|
2. **Azure STT** - Bonne qualité, free tier
|
||||||
3. **Whisper.cpp** (`ggml-small.bin`) - Offline fallback
|
3. **Whisper.cpp** (`ggml-small.bin`) - Offline fallback
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Fichiers de Configuration
|
## Fichiers de Configuration
|
||||||
|
|
||||||
### .env (API Keys)
|
### .env (API Keys)
|
||||||
```bash
|
```bash
|
||||||
# OpenAI Whisper API (✅ configuré)
|
# OpenAI Whisper API (✅ configuré)
|
||||||
OPENAI_API_KEY=sk-proj-...
|
OPENAI_API_KEY=sk-proj-...
|
||||||
|
|
||||||
# Google Speech (✅ configuré)
|
# Google Speech (✅ configuré)
|
||||||
GOOGLE_API_KEY=AIzaSy...
|
GOOGLE_API_KEY=AIzaSy...
|
||||||
|
|
||||||
# Azure STT (optionnel)
|
# Azure STT (optionnel)
|
||||||
#AZURE_SPEECH_KEY=votre_cle
|
#AZURE_SPEECH_KEY=votre_cle
|
||||||
#AZURE_SPEECH_REGION=westeurope
|
#AZURE_SPEECH_REGION=westeurope
|
||||||
|
|
||||||
# Deepgram (optionnel)
|
# Deepgram (optionnel)
|
||||||
#DEEPGRAM_API_KEY=votre_cle
|
#DEEPGRAM_API_KEY=votre_cle
|
||||||
```
|
```
|
||||||
|
|
||||||
### config/voice.json
|
### config/voice.json
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"stt": {
|
"stt": {
|
||||||
"active_mode": {
|
"active_mode": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"engine": "whisper_cpp",
|
"engine": "whisper_cpp",
|
||||||
"model_path": "./models/ggml-base.bin",
|
"model_path": "./models/ggml-base.bin",
|
||||||
"language": "fr",
|
"language": "fr",
|
||||||
"fallback_engine": "whisper_api"
|
"fallback_engine": "whisper_api"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Dépendances
|
## Dépendances
|
||||||
|
|
||||||
### Whisper.cpp
|
### Whisper.cpp
|
||||||
- ✅ Intégré dans le build (external/whisper.cpp)
|
- ✅ Intégré dans le build (external/whisper.cpp)
|
||||||
- ✅ Lié statiquement à AissiaAudio
|
- ✅ Lié statiquement à AissiaAudio
|
||||||
- ❌ Modèle requis : téléchargé dans `models/`
|
- ❌ Modèle requis : téléchargé dans `models/`
|
||||||
|
|
||||||
### APIs Cloud
|
### APIs Cloud
|
||||||
- ✅ Httplib pour requêtes HTTP (déjà dans le projet)
|
- ✅ Httplib pour requêtes HTTP (déjà dans le projet)
|
||||||
- ✅ nlohmann/json pour sérialisation (déjà dans le projet)
|
- ✅ nlohmann/json pour sérialisation (déjà dans le projet)
|
||||||
- ❌ OpenSSL désactivé (HTTP-only mode OK)
|
- ❌ OpenSSL désactivé (HTTP-only mode OK)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### "Whisper model not found"
|
### "Whisper model not found"
|
||||||
```bash
|
```bash
|
||||||
cd models/
|
cd models/
|
||||||
curl -L -o ggml-base.bin https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.bin
|
curl -L -o ggml-base.bin https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.bin
|
||||||
```
|
```
|
||||||
|
|
||||||
### "API key not found"
|
### "API key not found"
|
||||||
Vérifier que `.env` contient les clés et est chargé :
|
Vérifier que `.env` contient les clés et est chargé :
|
||||||
```bash
|
```bash
|
||||||
cat .env | grep -E "OPENAI|GOOGLE|AZURE|DEEPGRAM"
|
cat .env | grep -E "OPENAI|GOOGLE|AZURE|DEEPGRAM"
|
||||||
```
|
```
|
||||||
|
|
||||||
### "Transcription failed"
|
### "Transcription failed"
|
||||||
1. Vérifier le format audio : 16kHz, mono, 16-bit PCM WAV
|
1. Vérifier le format audio : 16kHz, mono, 16-bit PCM WAV
|
||||||
2. Générer un test : `python create_test_audio_simple.py`
|
2. Générer un test : `python create_test_audio_simple.py`
|
||||||
3. Activer les logs : `spdlog::set_level(spdlog::level::debug)`
|
3. Activer les logs : `spdlog::set_level(spdlog::level::debug)`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Prochaines Étapes
|
## Prochaines Étapes
|
||||||
|
|
||||||
1. ✅ Whisper.cpp configuré et fonctionnel
|
1. ✅ Whisper.cpp configuré et fonctionnel
|
||||||
2. ✅ OpenAI + Google APIs configurées
|
2. ✅ OpenAI + Google APIs configurées
|
||||||
3. ⚠️ Optionnel : Ajouter Azure ou Deepgram pour redondance
|
3. ⚠️ Optionnel : Ajouter Azure ou Deepgram pour redondance
|
||||||
4. 🔜 Tester avec `./build/test_stt_live test_audio.wav`
|
4. 🔜 Tester avec `./build/test_stt_live test_audio.wav`
|
||||||
5. 🔜 Intégrer dans VoiceModule via pub/sub
|
5. 🔜 Intégrer dans VoiceModule via pub/sub
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Références
|
## Références
|
||||||
|
|
||||||
- [Whisper.cpp GitHub](https://github.com/ggerganov/whisper.cpp)
|
- [Whisper.cpp GitHub](https://github.com/ggerganov/whisper.cpp)
|
||||||
- [OpenAI Whisper API](https://platform.openai.com/docs/guides/speech-to-text)
|
- [OpenAI Whisper API](https://platform.openai.com/docs/guides/speech-to-text)
|
||||||
- [Google Speech-to-Text](https://cloud.google.com/speech-to-text)
|
- [Google Speech-to-Text](https://cloud.google.com/speech-to-text)
|
||||||
- [Azure Speech](https://azure.microsoft.com/en-us/services/cognitive-services/speech-to-text/)
|
- [Azure Speech](https://azure.microsoft.com/en-us/services/cognitive-services/speech-to-text/)
|
||||||
- [Deepgram](https://developers.deepgram.com/)
|
- [Deepgram](https://developers.deepgram.com/)
|
||||||
|
|||||||
@ -1,227 +1,227 @@
|
|||||||
# Document de Succession - AISSIA
|
# Document de Succession - AISSIA
|
||||||
|
|
||||||
## Contexte
|
## Contexte
|
||||||
|
|
||||||
AISSIA = Assistant vocal agentique basé sur GroveEngine (C++17 hot-reload). Architecture "Claude Code en vocal" avec tools internes + FileSystem + MCP.
|
AISSIA = Assistant vocal agentique basé sur GroveEngine (C++17 hot-reload). Architecture "Claude Code en vocal" avec tools internes + FileSystem + MCP.
|
||||||
|
|
||||||
**Dernier commit** : `37b62b5`
|
**Dernier commit** : `37b62b5`
|
||||||
|
|
||||||
## État Actuel
|
## État Actuel
|
||||||
|
|
||||||
### Ce qui fonctionne
|
### Ce qui fonctionne
|
||||||
|
|
||||||
✅ **Build complet** - `cmake -B build && cmake --build build -j4`
|
✅ **Build complet** - `cmake -B build && cmake --build build -j4`
|
||||||
✅ **6 modules hot-reload** - Scheduler, Notification, Monitoring, AI, Voice, Storage
|
✅ **6 modules hot-reload** - Scheduler, Notification, Monitoring, AI, Voice, Storage
|
||||||
✅ **4 services** - LLMService, StorageService, PlatformService, VoiceService
|
✅ **4 services** - LLMService, StorageService, PlatformService, VoiceService
|
||||||
✅ **17 tools pour l'agent** :
|
✅ **17 tools pour l'agent** :
|
||||||
- 11 tools internes (via IIO pub/sub)
|
- 11 tools internes (via IIO pub/sub)
|
||||||
- 6 FileSystem tools (read/write/edit/list/glob/grep)
|
- 6 FileSystem tools (read/write/edit/list/glob/grep)
|
||||||
- MCP tools (désactivés par défaut)
|
- MCP tools (désactivés par défaut)
|
||||||
✅ **Tests** - 67/75 tests modules+types passent
|
✅ **Tests** - 67/75 tests modules+types passent
|
||||||
|
|
||||||
### Lancement
|
### Lancement
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Build
|
# Build
|
||||||
cmake -B build && cmake --build build -j4
|
cmake -B build && cmake --build build -j4
|
||||||
|
|
||||||
# Run (depuis racine ou build/)
|
# Run (depuis racine ou build/)
|
||||||
./build/aissia
|
./build/aissia
|
||||||
|
|
||||||
# Mode MCP Server (expose les tools via JSON-RPC stdio)
|
# Mode MCP Server (expose les tools via JSON-RPC stdio)
|
||||||
./build/aissia --mcp-server
|
./build/aissia --mcp-server
|
||||||
|
|
||||||
# Tests
|
# Tests
|
||||||
cmake -B build -DBUILD_TESTING=ON
|
cmake -B build -DBUILD_TESTING=ON
|
||||||
./build/tests/aissia_tests "[scheduler],[notification]" # Modules
|
./build/tests/aissia_tests "[scheduler],[notification]" # Modules
|
||||||
./build/tests/aissia_tests "[types]" # MCP types
|
./build/tests/aissia_tests "[types]" # MCP types
|
||||||
```
|
```
|
||||||
|
|
||||||
### Variables d'Environnement
|
### Variables d'Environnement
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export ANTHROPIC_API_KEY="sk-ant-..." # Requis pour Claude API
|
export ANTHROPIC_API_KEY="sk-ant-..." # Requis pour Claude API
|
||||||
```
|
```
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────────────────────────────────┐
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
│ LLMService │
|
│ LLMService │
|
||||||
│ (Agentic Loop) │
|
│ (Agentic Loop) │
|
||||||
├─────────────────────────────────────────────────────────────┤
|
├─────────────────────────────────────────────────────────────┤
|
||||||
│ ToolRegistry │
|
│ ToolRegistry │
|
||||||
│ ├── InternalTools (11) ─────► IIO pub/sub │
|
│ ├── InternalTools (11) ─────► IIO pub/sub │
|
||||||
│ ├── FileSystemTools (6) ────► Direct C++ (read/write/edit) │
|
│ ├── FileSystemTools (6) ────► Direct C++ (read/write/edit) │
|
||||||
│ └── MCPClient (optionnel) ──► stdio JSON-RPC │
|
│ └── MCPClient (optionnel) ──► stdio JSON-RPC │
|
||||||
└─────────────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────────────┘
|
||||||
│
|
│
|
||||||
┌──────────────┬─────┴──────┬──────────────┐
|
┌──────────────┬─────┴──────┬──────────────┐
|
||||||
Scheduler Monitoring Storage Voice
|
Scheduler Monitoring Storage Voice
|
||||||
Module Module Module Module
|
Module Module Module Module
|
||||||
```
|
```
|
||||||
|
|
||||||
### Tools Disponibles
|
### Tools Disponibles
|
||||||
|
|
||||||
| Catégorie | Tools | Communication |
|
| Catégorie | Tools | Communication |
|
||||||
|-----------|-------|---------------|
|
|-----------|-------|---------------|
|
||||||
| Scheduler | get_current_task, list_tasks, start_task, complete_task, start_break | IIO |
|
| Scheduler | get_current_task, list_tasks, start_task, complete_task, start_break | IIO |
|
||||||
| Monitoring | get_focus_stats, get_current_app | IIO |
|
| Monitoring | get_focus_stats, get_current_app | IIO |
|
||||||
| Storage | save_note, query_notes, get_session_history | IIO |
|
| Storage | save_note, query_notes, get_session_history | IIO |
|
||||||
| Voice | speak | IIO |
|
| Voice | speak | IIO |
|
||||||
| FileSystem | read_file, write_file, edit_file, list_directory, glob_files, grep_files | Direct C++ |
|
| FileSystem | read_file, write_file, edit_file, list_directory, glob_files, grep_files | Direct C++ |
|
||||||
|
|
||||||
### FileSystem Tools (Nouveau)
|
### FileSystem Tools (Nouveau)
|
||||||
|
|
||||||
Implémentés dans `src/shared/tools/FileSystemTools.*` :
|
Implémentés dans `src/shared/tools/FileSystemTools.*` :
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
// Lecture avec numéros de ligne
|
// Lecture avec numéros de ligne
|
||||||
FileSystemTools::execute("read_file", {{"path", "/path/to/file"}, {"limit", 10}});
|
FileSystemTools::execute("read_file", {{"path", "/path/to/file"}, {"limit", 10}});
|
||||||
|
|
||||||
// Édition style Claude Code
|
// Édition style Claude Code
|
||||||
FileSystemTools::execute("edit_file", {
|
FileSystemTools::execute("edit_file", {
|
||||||
{"path", "/path/to/file"},
|
{"path", "/path/to/file"},
|
||||||
{"old_string", "foo"},
|
{"old_string", "foo"},
|
||||||
{"new_string", "bar"}
|
{"new_string", "bar"}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Recherche
|
// Recherche
|
||||||
FileSystemTools::execute("glob_files", {{"pattern", "**/*.cpp"}});
|
FileSystemTools::execute("glob_files", {{"pattern", "**/*.cpp"}});
|
||||||
FileSystemTools::execute("grep_files", {{"pattern", "TODO"}, {"path", "./src"}});
|
FileSystemTools::execute("grep_files", {{"pattern", "TODO"}, {"path", "./src"}});
|
||||||
```
|
```
|
||||||
|
|
||||||
**Sécurité** :
|
**Sécurité** :
|
||||||
- Chemins autorisés configurables
|
- Chemins autorisés configurables
|
||||||
- Patterns bloqués : `*.env`, `*.key`, `*credentials*`
|
- Patterns bloqués : `*.env`, `*.key`, `*credentials*`
|
||||||
- Limites : 1MB lecture, 10MB écriture
|
- Limites : 1MB lecture, 10MB écriture
|
||||||
|
|
||||||
## Fichiers Clés
|
## Fichiers Clés
|
||||||
|
|
||||||
### Nouveaux (Session actuelle)
|
### Nouveaux (Session actuelle)
|
||||||
```
|
```
|
||||||
src/shared/tools/FileSystemTools.hpp
|
src/shared/tools/FileSystemTools.hpp
|
||||||
src/shared/tools/FileSystemTools.cpp
|
src/shared/tools/FileSystemTools.cpp
|
||||||
PLAN_FILESYSTEM_TOOLS.md
|
PLAN_FILESYSTEM_TOOLS.md
|
||||||
```
|
```
|
||||||
|
|
||||||
### Services
|
### Services
|
||||||
```
|
```
|
||||||
src/services/LLMService.* # Agentic loop, tools registry
|
src/services/LLMService.* # Agentic loop, tools registry
|
||||||
src/services/StorageService.* # SQLite persistence
|
src/services/StorageService.* # SQLite persistence
|
||||||
src/services/PlatformService.* # Window tracking
|
src/services/PlatformService.* # Window tracking
|
||||||
src/services/VoiceService.* # TTS/STT
|
src/services/VoiceService.* # TTS/STT
|
||||||
```
|
```
|
||||||
|
|
||||||
### Modules (Hot-Reload)
|
### Modules (Hot-Reload)
|
||||||
```
|
```
|
||||||
src/modules/SchedulerModule.*
|
src/modules/SchedulerModule.*
|
||||||
src/modules/NotificationModule.*
|
src/modules/NotificationModule.*
|
||||||
src/modules/MonitoringModule.*
|
src/modules/MonitoringModule.*
|
||||||
src/modules/AIModule.*
|
src/modules/AIModule.*
|
||||||
src/modules/VoiceModule.*
|
src/modules/VoiceModule.*
|
||||||
src/modules/StorageModule.*
|
src/modules/StorageModule.*
|
||||||
```
|
```
|
||||||
|
|
||||||
### MCP
|
### MCP
|
||||||
```
|
```
|
||||||
src/shared/mcp/MCPTypes.hpp
|
src/shared/mcp/MCPTypes.hpp
|
||||||
src/shared/mcp/MCPClient.* # Client MCP (consomme des serveurs externes)
|
src/shared/mcp/MCPClient.* # Client MCP (consomme des serveurs externes)
|
||||||
src/shared/mcp/MCPServer.* # Serveur MCP (expose AISSIA comme serveur)
|
src/shared/mcp/MCPServer.* # Serveur MCP (expose AISSIA comme serveur)
|
||||||
src/shared/mcp/StdioTransport.*
|
src/shared/mcp/StdioTransport.*
|
||||||
config/mcp.json
|
config/mcp.json
|
||||||
```
|
```
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Build avec tests
|
# Build avec tests
|
||||||
cmake -B build -DBUILD_TESTING=ON && cmake --build build -j4
|
cmake -B build -DBUILD_TESTING=ON && cmake --build build -j4
|
||||||
|
|
||||||
# Par catégorie
|
# Par catégorie
|
||||||
./build/tests/aissia_tests "[scheduler]" # 10 tests
|
./build/tests/aissia_tests "[scheduler]" # 10 tests
|
||||||
./build/tests/aissia_tests "[notification]" # 10 tests
|
./build/tests/aissia_tests "[notification]" # 10 tests
|
||||||
./build/tests/aissia_tests "[types]" # 15 tests MCP
|
./build/tests/aissia_tests "[types]" # 15 tests MCP
|
||||||
|
|
||||||
# Tous les modules
|
# Tous les modules
|
||||||
./build/tests/aissia_tests "[scheduler],[notification],[monitoring],[ai],[voice],[storage]"
|
./build/tests/aissia_tests "[scheduler],[notification],[monitoring],[ai],[voice],[storage]"
|
||||||
```
|
```
|
||||||
|
|
||||||
**Résultats actuels** :
|
**Résultats actuels** :
|
||||||
- Modules : 52/60 (87%)
|
- Modules : 52/60 (87%)
|
||||||
- MCP Types : 15/15 (100%)
|
- MCP Types : 15/15 (100%)
|
||||||
- MCP Transport/Client : Nécessite fix serveurs Python
|
- MCP Transport/Client : Nécessite fix serveurs Python
|
||||||
|
|
||||||
## Prochaines Étapes
|
## Prochaines Étapes
|
||||||
|
|
||||||
### Priorité Haute
|
### Priorité Haute
|
||||||
1. **Tester avec API key** - Vérifier la boucle agentique complète
|
1. **Tester avec API key** - Vérifier la boucle agentique complète
|
||||||
2. **Activer MCP filesystem** - Pour tests end-to-end avec tools externes
|
2. **Activer MCP filesystem** - Pour tests end-to-end avec tools externes
|
||||||
|
|
||||||
### Priorité Moyenne
|
### Priorité Moyenne
|
||||||
3. **Fixer tests MCP Transport** - Les serveurs Python reçoivent EOF
|
3. **Fixer tests MCP Transport** - Les serveurs Python reçoivent EOF
|
||||||
4. **Ajouter plus de tools** - add_task, set_reminder, etc.
|
4. **Ajouter plus de tools** - add_task, set_reminder, etc.
|
||||||
5. **Streaming responses** - Feedback temps réel pendant génération
|
5. **Streaming responses** - Feedback temps réel pendant génération
|
||||||
|
|
||||||
### Priorité Basse
|
### Priorité Basse
|
||||||
6. **Tests end-to-end** - Flux complet inter-modules
|
6. **Tests end-to-end** - Flux complet inter-modules
|
||||||
7. **CI/CD** - GitHub Actions
|
7. **CI/CD** - GitHub Actions
|
||||||
8. **Documentation API** - Doxygen
|
8. **Documentation API** - Doxygen
|
||||||
|
|
||||||
## MCP Server Mode
|
## MCP Server Mode
|
||||||
|
|
||||||
AISSIA peut fonctionner comme **serveur MCP**, exposant ses tools à des clients externes via JSON-RPC sur stdio.
|
AISSIA peut fonctionner comme **serveur MCP**, exposant ses tools à des clients externes via JSON-RPC sur stdio.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./build/aissia --mcp-server
|
./build/aissia --mcp-server
|
||||||
```
|
```
|
||||||
|
|
||||||
### Protocole
|
### Protocole
|
||||||
|
|
||||||
Communication JSON-RPC 2.0 sur stdin/stdout :
|
Communication JSON-RPC 2.0 sur stdin/stdout :
|
||||||
|
|
||||||
```json
|
```json
|
||||||
// Client → AISSIA
|
// Client → AISSIA
|
||||||
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"client","version":"1.0"}}}
|
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"client","version":"1.0"}}}
|
||||||
|
|
||||||
// AISSIA → Client
|
// AISSIA → Client
|
||||||
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","serverInfo":{"name":"aissia","version":"0.2.0"},...}}
|
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","serverInfo":{"name":"aissia","version":"0.2.0"},...}}
|
||||||
|
|
||||||
// Lister les tools
|
// Lister les tools
|
||||||
{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}
|
{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}
|
||||||
|
|
||||||
// Appeler un tool
|
// Appeler un tool
|
||||||
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"list_directory","arguments":{"path":"."}}}
|
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"list_directory","arguments":{"path":"."}}}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Utilisation avec Claude Code
|
### Utilisation avec Claude Code
|
||||||
|
|
||||||
Ajouter dans la config MCP :
|
Ajouter dans la config MCP :
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"servers": {
|
"servers": {
|
||||||
"aissia": {
|
"aissia": {
|
||||||
"command": "/chemin/vers/build/aissia",
|
"command": "/chemin/vers/build/aissia",
|
||||||
"args": ["--mcp-server"]
|
"args": ["--mcp-server"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Tools Exposés (actuellement)
|
### Tools Exposés (actuellement)
|
||||||
|
|
||||||
6 FileSystem tools. TODO: exposer les tools internes (scheduler, voice, etc.).
|
6 FileSystem tools. TODO: exposer les tools internes (scheduler, voice, etc.).
|
||||||
|
|
||||||
## Notes Techniques
|
## Notes Techniques
|
||||||
|
|
||||||
### WSL
|
### WSL
|
||||||
- Window tracker non disponible (stub utilisé)
|
- Window tracker non disponible (stub utilisé)
|
||||||
- espeak non installé (TTS stub)
|
- espeak non installé (TTS stub)
|
||||||
- Tout le reste fonctionne
|
- Tout le reste fonctionne
|
||||||
|
|
||||||
### Hot-Reload
|
### Hot-Reload
|
||||||
Les modules sont des `.so` chargés dynamiquement. Pour recompiler un module :
|
Les modules sont des `.so` chargés dynamiquement. Pour recompiler un module :
|
||||||
```bash
|
```bash
|
||||||
cmake --build build --target SchedulerModule
|
cmake --build build --target SchedulerModule
|
||||||
# Le module sera rechargé au prochain cycle si modifié
|
# Le module sera rechargé au prochain cycle si modifié
|
||||||
```
|
```
|
||||||
|
|||||||
@ -1,16 +1,16 @@
|
|||||||
$ErrorActionPreference = "Continue"
|
$ErrorActionPreference = "Continue"
|
||||||
cd "C:\Users\alexi\Documents\projects\aissia"
|
cd "C:\Users\alexi\Documents\projects\aissia"
|
||||||
|
|
||||||
Write-Host "=== Running aissia_tests.exe ===" -ForegroundColor Cyan
|
Write-Host "=== Running aissia_tests.exe ===" -ForegroundColor Cyan
|
||||||
& ".\build\tests\aissia_tests.exe" 2>&1 | Tee-Object -FilePath "test_output.txt"
|
& ".\build\tests\aissia_tests.exe" 2>&1 | Tee-Object -FilePath "test_output.txt"
|
||||||
$testExitCode = $LASTEXITCODE
|
$testExitCode = $LASTEXITCODE
|
||||||
Write-Host "`nTest exit code: $testExitCode" -ForegroundColor $(if ($testExitCode -eq 0) { "Green" } else { "Red" })
|
Write-Host "`nTest exit code: $testExitCode" -ForegroundColor $(if ($testExitCode -eq 0) { "Green" } else { "Red" })
|
||||||
|
|
||||||
Write-Host "`n=== Running test_stt_engines.exe ===" -ForegroundColor Cyan
|
Write-Host "`n=== Running test_stt_engines.exe ===" -ForegroundColor Cyan
|
||||||
& ".\build\test_stt_engines.exe" 2>&1 | Tee-Object -FilePath "stt_test_output.txt" -Append
|
& ".\build\test_stt_engines.exe" 2>&1 | Tee-Object -FilePath "stt_test_output.txt" -Append
|
||||||
$sttExitCode = $LASTEXITCODE
|
$sttExitCode = $LASTEXITCODE
|
||||||
Write-Host "`nSTT Test exit code: $sttExitCode" -ForegroundColor $(if ($sttExitCode -eq 0) { "Green" } else { "Red" })
|
Write-Host "`nSTT Test exit code: $sttExitCode" -ForegroundColor $(if ($sttExitCode -eq 0) { "Green" } else { "Red" })
|
||||||
|
|
||||||
Write-Host "`n=== Test Summary ===" -ForegroundColor Cyan
|
Write-Host "`n=== Test Summary ===" -ForegroundColor Cyan
|
||||||
Write-Host "aissia_tests: $(if ($testExitCode -eq 0) { 'PASSED' } else { 'FAILED' })"
|
Write-Host "aissia_tests: $(if ($testExitCode -eq 0) { 'PASSED' } else { 'FAILED' })"
|
||||||
Write-Host "test_stt_engines: $(if ($sttExitCode -eq 0) { 'PASSED' } else { 'FAILED' })"
|
Write-Host "test_stt_engines: $(if ($sttExitCode -eq 0) { 'PASSED' } else { 'FAILED' })"
|
||||||
|
|||||||
@ -1,39 +1,39 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <grove/IIO.h>
|
#include <grove/IIO.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
namespace aissia {
|
namespace aissia {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Interface for infrastructure services
|
* @brief Interface for infrastructure services
|
||||||
*
|
*
|
||||||
* Services handle non-hot-reloadable infrastructure:
|
* Services handle non-hot-reloadable infrastructure:
|
||||||
* - LLM HTTP calls
|
* - LLM HTTP calls
|
||||||
* - SQLite database
|
* - SQLite database
|
||||||
* - Platform APIs (Win32/X11)
|
* - Platform APIs (Win32/X11)
|
||||||
* - TTS/STT engines
|
* - TTS/STT engines
|
||||||
*
|
*
|
||||||
* Services communicate with modules via IIO pub/sub.
|
* Services communicate with modules via IIO pub/sub.
|
||||||
*/
|
*/
|
||||||
class IService {
|
class IService {
|
||||||
public:
|
public:
|
||||||
virtual ~IService() = default;
|
virtual ~IService() = default;
|
||||||
|
|
||||||
/// Initialize the service with IIO for pub/sub
|
/// Initialize the service with IIO for pub/sub
|
||||||
virtual bool initialize(grove::IIO* io) = 0;
|
virtual bool initialize(grove::IIO* io) = 0;
|
||||||
|
|
||||||
/// Process pending work (called each frame from main loop)
|
/// Process pending work (called each frame from main loop)
|
||||||
virtual void process() = 0;
|
virtual void process() = 0;
|
||||||
|
|
||||||
/// Clean shutdown
|
/// Clean shutdown
|
||||||
virtual void shutdown() = 0;
|
virtual void shutdown() = 0;
|
||||||
|
|
||||||
/// Service name for logging
|
/// Service name for logging
|
||||||
virtual std::string getName() const = 0;
|
virtual std::string getName() const = 0;
|
||||||
|
|
||||||
/// Check if service is healthy
|
/// Check if service is healthy
|
||||||
virtual bool isHealthy() const = 0;
|
virtual bool isHealthy() const = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace aissia
|
} // namespace aissia
|
||||||
|
|||||||
@ -1,375 +1,375 @@
|
|||||||
#include "LLMService.hpp"
|
#include "LLMService.hpp"
|
||||||
#include "../shared/llm/LLMProviderFactory.hpp"
|
#include "../shared/llm/LLMProviderFactory.hpp"
|
||||||
|
|
||||||
#include <spdlog/sinks/stdout_color_sinks.h>
|
#include <spdlog/sinks/stdout_color_sinks.h>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
|
||||||
namespace aissia {
|
namespace aissia {
|
||||||
|
|
||||||
LLMService::LLMService() {
|
LLMService::LLMService() {
|
||||||
m_logger = spdlog::get("LLMService");
|
m_logger = spdlog::get("LLMService");
|
||||||
if (!m_logger) {
|
if (!m_logger) {
|
||||||
m_logger = spdlog::stdout_color_mt("LLMService");
|
m_logger = spdlog::stdout_color_mt("LLMService");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LLMService::~LLMService() {
|
LLMService::~LLMService() {
|
||||||
shutdown();
|
shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool LLMService::initialize(grove::IIO* io) {
|
bool LLMService::initialize(grove::IIO* io) {
|
||||||
m_io = io;
|
m_io = io;
|
||||||
|
|
||||||
if (m_io) {
|
if (m_io) {
|
||||||
grove::SubscriptionConfig config;
|
grove::SubscriptionConfig config;
|
||||||
m_io->subscribe("llm:request", config);
|
m_io->subscribe("llm:request", config);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start worker thread
|
// Start worker thread
|
||||||
m_running = true;
|
m_running = true;
|
||||||
m_workerThread = std::thread(&LLMService::workerLoop, this);
|
m_workerThread = std::thread(&LLMService::workerLoop, this);
|
||||||
|
|
||||||
m_logger->info("LLMService initialized");
|
m_logger->info("LLMService initialized");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool LLMService::loadConfig(const std::string& configPath) {
|
bool LLMService::loadConfig(const std::string& configPath) {
|
||||||
try {
|
try {
|
||||||
std::ifstream file(configPath);
|
std::ifstream file(configPath);
|
||||||
if (!file.is_open()) {
|
if (!file.is_open()) {
|
||||||
m_logger->warn("Config file not found: {}", configPath);
|
m_logger->warn("Config file not found: {}", configPath);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
nlohmann::json config;
|
nlohmann::json config;
|
||||||
file >> config;
|
file >> config;
|
||||||
|
|
||||||
m_provider = LLMProviderFactory::create(config);
|
m_provider = LLMProviderFactory::create(config);
|
||||||
if (!m_provider) {
|
if (!m_provider) {
|
||||||
m_logger->error("Failed to create LLM provider");
|
m_logger->error("Failed to create LLM provider");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_providerName = config.value("provider", "claude");
|
m_providerName = config.value("provider", "claude");
|
||||||
m_maxIterations = config.value("max_iterations", 10);
|
m_maxIterations = config.value("max_iterations", 10);
|
||||||
m_defaultSystemPrompt = config.value("system_prompt",
|
m_defaultSystemPrompt = config.value("system_prompt",
|
||||||
"Tu es AISSIA, un assistant personnel intelligent. "
|
"Tu es AISSIA, un assistant personnel intelligent. "
|
||||||
"Tu peux utiliser des tools pour accomplir des taches: "
|
"Tu peux utiliser des tools pour accomplir des taches: "
|
||||||
"gerer le planning, verifier le focus, sauvegarder des notes, "
|
"gerer le planning, verifier le focus, sauvegarder des notes, "
|
||||||
"lire des fichiers, faire des recherches web, etc.");
|
"lire des fichiers, faire des recherches web, etc.");
|
||||||
|
|
||||||
m_logger->info("LLM provider loaded: {} ({})", m_providerName, m_provider->getModel());
|
m_logger->info("LLM provider loaded: {} ({})", m_providerName, m_provider->getModel());
|
||||||
|
|
||||||
// Initialize tools after provider is ready
|
// Initialize tools after provider is ready
|
||||||
initializeTools();
|
initializeTools();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
m_logger->error("Failed to load config: {}", e.what());
|
m_logger->error("Failed to load config: {}", e.what());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void LLMService::initializeTools() {
|
void LLMService::initializeTools() {
|
||||||
m_logger->info("Initializing tools...");
|
m_logger->info("Initializing tools...");
|
||||||
|
|
||||||
// 1. Internal tools (via GroveEngine IIO)
|
// 1. Internal tools (via GroveEngine IIO)
|
||||||
if (m_io) {
|
if (m_io) {
|
||||||
m_internalTools = std::make_unique<InternalTools>(m_io);
|
m_internalTools = std::make_unique<InternalTools>(m_io);
|
||||||
for (const auto& tool : m_internalTools->getTools()) {
|
for (const auto& tool : m_internalTools->getTools()) {
|
||||||
m_toolRegistry.registerTool(tool);
|
m_toolRegistry.registerTool(tool);
|
||||||
}
|
}
|
||||||
m_logger->info("Registered {} internal tools", m_internalTools->size());
|
m_logger->info("Registered {} internal tools", m_internalTools->size());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. FileSystem tools (direct C++ execution)
|
// 2. FileSystem tools (direct C++ execution)
|
||||||
for (const auto& toolDef : tools::FileSystemTools::getToolDefinitions()) {
|
for (const auto& toolDef : tools::FileSystemTools::getToolDefinitions()) {
|
||||||
std::string toolName = toolDef["name"].get<std::string>();
|
std::string toolName = toolDef["name"].get<std::string>();
|
||||||
m_toolRegistry.registerTool(
|
m_toolRegistry.registerTool(
|
||||||
toolName,
|
toolName,
|
||||||
toolDef["description"].get<std::string>(),
|
toolDef["description"].get<std::string>(),
|
||||||
toolDef["input_schema"],
|
toolDef["input_schema"],
|
||||||
[toolName](const nlohmann::json& input) -> nlohmann::json {
|
[toolName](const nlohmann::json& input) -> nlohmann::json {
|
||||||
return tools::FileSystemTools::execute(toolName, input);
|
return tools::FileSystemTools::execute(toolName, input);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
m_logger->info("Registered {} filesystem tools", tools::FileSystemTools::getToolDefinitions().size());
|
m_logger->info("Registered {} filesystem tools", tools::FileSystemTools::getToolDefinitions().size());
|
||||||
|
|
||||||
// 3. MCP tools (via external servers)
|
// 3. MCP tools (via external servers)
|
||||||
m_mcpClient = std::make_unique<mcp::MCPClient>();
|
m_mcpClient = std::make_unique<mcp::MCPClient>();
|
||||||
if (loadMCPConfig("config/mcp.json")) {
|
if (loadMCPConfig("config/mcp.json")) {
|
||||||
int connected = m_mcpClient->connectAll();
|
int connected = m_mcpClient->connectAll();
|
||||||
if (connected > 0) {
|
if (connected > 0) {
|
||||||
for (const auto& tool : m_mcpClient->listAllTools()) {
|
for (const auto& tool : m_mcpClient->listAllTools()) {
|
||||||
// Convert MCP tool to our ToolDefinition format
|
// Convert MCP tool to our ToolDefinition format
|
||||||
m_toolRegistry.registerTool(
|
m_toolRegistry.registerTool(
|
||||||
tool.name,
|
tool.name,
|
||||||
tool.description,
|
tool.description,
|
||||||
tool.inputSchema,
|
tool.inputSchema,
|
||||||
[this, toolName = tool.name](const nlohmann::json& input) -> nlohmann::json {
|
[this, toolName = tool.name](const nlohmann::json& input) -> nlohmann::json {
|
||||||
auto result = m_mcpClient->callTool(toolName, input);
|
auto result = m_mcpClient->callTool(toolName, input);
|
||||||
// Convert MCP result to simple JSON
|
// Convert MCP result to simple JSON
|
||||||
if (result.isError) {
|
if (result.isError) {
|
||||||
return {{"error", true}, {"content", result.content}};
|
return {{"error", true}, {"content", result.content}};
|
||||||
}
|
}
|
||||||
// Extract text content
|
// Extract text content
|
||||||
std::string text;
|
std::string text;
|
||||||
for (const auto& content : result.content) {
|
for (const auto& content : result.content) {
|
||||||
if (content.contains("text")) {
|
if (content.contains("text")) {
|
||||||
text += content["text"].get<std::string>();
|
text += content["text"].get<std::string>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {{"content", text}};
|
return {{"content", text}};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
m_logger->info("Registered {} MCP tools from {} servers",
|
m_logger->info("Registered {} MCP tools from {} servers",
|
||||||
m_mcpClient->toolCount(), connected);
|
m_mcpClient->toolCount(), connected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m_logger->info("Total tools available: {}", m_toolRegistry.size());
|
m_logger->info("Total tools available: {}", m_toolRegistry.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
bool LLMService::loadMCPConfig(const std::string& configPath) {
|
bool LLMService::loadMCPConfig(const std::string& configPath) {
|
||||||
return m_mcpClient->loadConfig(configPath);
|
return m_mcpClient->loadConfig(configPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
void LLMService::registerTool(const std::string& name, const std::string& description,
|
void LLMService::registerTool(const std::string& name, const std::string& description,
|
||||||
const nlohmann::json& schema,
|
const nlohmann::json& schema,
|
||||||
std::function<nlohmann::json(const nlohmann::json&)> handler) {
|
std::function<nlohmann::json(const nlohmann::json&)> handler) {
|
||||||
m_toolRegistry.registerTool(name, description, schema, handler);
|
m_toolRegistry.registerTool(name, description, schema, handler);
|
||||||
m_logger->debug("Tool registered: {}", name);
|
m_logger->debug("Tool registered: {}", name);
|
||||||
}
|
}
|
||||||
|
|
||||||
void LLMService::process() {
|
void LLMService::process() {
|
||||||
processIncomingMessages();
|
processIncomingMessages();
|
||||||
publishResponses();
|
publishResponses();
|
||||||
}
|
}
|
||||||
|
|
||||||
void LLMService::processIncomingMessages() {
|
void LLMService::processIncomingMessages() {
|
||||||
if (!m_io) return;
|
if (!m_io) return;
|
||||||
|
|
||||||
while (m_io->hasMessages() > 0) {
|
while (m_io->hasMessages() > 0) {
|
||||||
auto msg = m_io->pullMessage();
|
auto msg = m_io->pullMessage();
|
||||||
|
|
||||||
if (msg.topic == "llm:request" && msg.data) {
|
if (msg.topic == "llm:request" && msg.data) {
|
||||||
Request req;
|
Request req;
|
||||||
req.query = msg.data->getString("query", "");
|
req.query = msg.data->getString("query", "");
|
||||||
req.systemPrompt = msg.data->getString("systemPrompt", m_defaultSystemPrompt);
|
req.systemPrompt = msg.data->getString("systemPrompt", m_defaultSystemPrompt);
|
||||||
req.conversationId = msg.data->getString("conversationId", "default");
|
req.conversationId = msg.data->getString("conversationId", "default");
|
||||||
req.maxIterations = msg.data->getInt("maxIterations", m_maxIterations);
|
req.maxIterations = msg.data->getInt("maxIterations", m_maxIterations);
|
||||||
|
|
||||||
// Get tools from message or use registered tools
|
// Get tools from message or use registered tools
|
||||||
auto* toolsNode = msg.data->getChildReadOnly("tools");
|
auto* toolsNode = msg.data->getChildReadOnly("tools");
|
||||||
if (toolsNode) {
|
if (toolsNode) {
|
||||||
// Custom tools from message
|
// Custom tools from message
|
||||||
// (would need to parse from IDataNode)
|
// (would need to parse from IDataNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!req.query.empty()) {
|
if (!req.query.empty()) {
|
||||||
std::lock_guard<std::mutex> lock(m_requestMutex);
|
std::lock_guard<std::mutex> lock(m_requestMutex);
|
||||||
m_requestQueue.push(std::move(req));
|
m_requestQueue.push(std::move(req));
|
||||||
m_requestCV.notify_one();
|
m_requestCV.notify_one();
|
||||||
m_logger->debug("Request queued: {}", req.query.substr(0, 50));
|
m_logger->debug("Request queued: {}", req.query.substr(0, 50));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void LLMService::publishResponses() {
|
void LLMService::publishResponses() {
|
||||||
if (!m_io) return;
|
if (!m_io) return;
|
||||||
|
|
||||||
std::lock_guard<std::mutex> lock(m_responseMutex);
|
std::lock_guard<std::mutex> lock(m_responseMutex);
|
||||||
while (!m_responseQueue.empty()) {
|
while (!m_responseQueue.empty()) {
|
||||||
auto resp = std::move(m_responseQueue.front());
|
auto resp = std::move(m_responseQueue.front());
|
||||||
m_responseQueue.pop();
|
m_responseQueue.pop();
|
||||||
|
|
||||||
if (resp.isError) {
|
if (resp.isError) {
|
||||||
auto event = std::make_unique<grove::JsonDataNode>("error");
|
auto event = std::make_unique<grove::JsonDataNode>("error");
|
||||||
event->setString("message", resp.text);
|
event->setString("message", resp.text);
|
||||||
event->setString("conversationId", resp.conversationId);
|
event->setString("conversationId", resp.conversationId);
|
||||||
m_io->publish("llm:error", std::move(event));
|
m_io->publish("llm:error", std::move(event));
|
||||||
} else {
|
} else {
|
||||||
auto event = std::make_unique<grove::JsonDataNode>("response");
|
auto event = std::make_unique<grove::JsonDataNode>("response");
|
||||||
event->setString("text", resp.text);
|
event->setString("text", resp.text);
|
||||||
event->setString("conversationId", resp.conversationId);
|
event->setString("conversationId", resp.conversationId);
|
||||||
event->setInt("tokens", resp.tokens);
|
event->setInt("tokens", resp.tokens);
|
||||||
event->setInt("iterations", resp.iterations);
|
event->setInt("iterations", resp.iterations);
|
||||||
m_io->publish("llm:response", std::move(event));
|
m_io->publish("llm:response", std::move(event));
|
||||||
|
|
||||||
m_logger->info("Response published: {} chars", resp.text.size());
|
m_logger->info("Response published: {} chars", resp.text.size());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void LLMService::workerLoop() {
|
void LLMService::workerLoop() {
|
||||||
m_logger->debug("Worker thread started");
|
m_logger->debug("Worker thread started");
|
||||||
|
|
||||||
while (m_running) {
|
while (m_running) {
|
||||||
Request req;
|
Request req;
|
||||||
|
|
||||||
{
|
{
|
||||||
std::unique_lock<std::mutex> lock(m_requestMutex);
|
std::unique_lock<std::mutex> lock(m_requestMutex);
|
||||||
m_requestCV.wait_for(lock, std::chrono::milliseconds(100), [this] {
|
m_requestCV.wait_for(lock, std::chrono::milliseconds(100), [this] {
|
||||||
return !m_requestQueue.empty() || !m_running;
|
return !m_requestQueue.empty() || !m_running;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!m_running) break;
|
if (!m_running) break;
|
||||||
if (m_requestQueue.empty()) continue;
|
if (m_requestQueue.empty()) continue;
|
||||||
|
|
||||||
req = std::move(m_requestQueue.front());
|
req = std::move(m_requestQueue.front());
|
||||||
m_requestQueue.pop();
|
m_requestQueue.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process request (HTTP calls happen here)
|
// Process request (HTTP calls happen here)
|
||||||
auto resp = processRequest(req);
|
auto resp = processRequest(req);
|
||||||
|
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(m_responseMutex);
|
std::lock_guard<std::mutex> lock(m_responseMutex);
|
||||||
m_responseQueue.push(std::move(resp));
|
m_responseQueue.push(std::move(resp));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m_logger->debug("Worker thread stopped");
|
m_logger->debug("Worker thread stopped");
|
||||||
}
|
}
|
||||||
|
|
||||||
LLMService::Response LLMService::processRequest(const Request& request) {
|
LLMService::Response LLMService::processRequest(const Request& request) {
|
||||||
Response resp;
|
Response resp;
|
||||||
resp.conversationId = request.conversationId;
|
resp.conversationId = request.conversationId;
|
||||||
|
|
||||||
if (!m_provider) {
|
if (!m_provider) {
|
||||||
resp.text = "LLM provider not initialized";
|
resp.text = "LLM provider not initialized";
|
||||||
resp.isError = true;
|
resp.isError = true;
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get or create conversation history
|
// Get or create conversation history
|
||||||
auto& history = m_conversations[request.conversationId];
|
auto& history = m_conversations[request.conversationId];
|
||||||
if (history.is_null()) {
|
if (history.is_null()) {
|
||||||
history = nlohmann::json::array();
|
history = nlohmann::json::array();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add user message
|
// Add user message
|
||||||
history.push_back({{"role", "user"}, {"content", request.query}});
|
history.push_back({{"role", "user"}, {"content", request.query}});
|
||||||
|
|
||||||
// Get tool definitions
|
// Get tool definitions
|
||||||
nlohmann::json tools = m_toolRegistry.getToolDefinitions();
|
nlohmann::json tools = m_toolRegistry.getToolDefinitions();
|
||||||
|
|
||||||
// Run agentic loop
|
// Run agentic loop
|
||||||
auto result = agenticLoop(request.query, request.systemPrompt,
|
auto result = agenticLoop(request.query, request.systemPrompt,
|
||||||
history, tools, request.maxIterations);
|
history, tools, request.maxIterations);
|
||||||
|
|
||||||
if (result.contains("error")) {
|
if (result.contains("error")) {
|
||||||
resp.text = result["error"].get<std::string>();
|
resp.text = result["error"].get<std::string>();
|
||||||
resp.isError = true;
|
resp.isError = true;
|
||||||
} else {
|
} else {
|
||||||
resp.text = result["response"].get<std::string>();
|
resp.text = result["response"].get<std::string>();
|
||||||
resp.tokens = result.value("tokens", 0);
|
resp.tokens = result.value("tokens", 0);
|
||||||
resp.iterations = result.value("iterations", 1);
|
resp.iterations = result.value("iterations", 1);
|
||||||
|
|
||||||
// Add assistant response to history
|
// Add assistant response to history
|
||||||
history.push_back({{"role", "assistant"}, {"content", resp.text}});
|
history.push_back({{"role", "assistant"}, {"content", resp.text}});
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
resp.text = e.what();
|
resp.text = e.what();
|
||||||
resp.isError = true;
|
resp.isError = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
nlohmann::json LLMService::agenticLoop(const std::string& query, const std::string& systemPrompt,
|
nlohmann::json LLMService::agenticLoop(const std::string& query, const std::string& systemPrompt,
|
||||||
nlohmann::json& messages, const nlohmann::json& tools,
|
nlohmann::json& messages, const nlohmann::json& tools,
|
||||||
int maxIterations) {
|
int maxIterations) {
|
||||||
int totalTokens = 0;
|
int totalTokens = 0;
|
||||||
|
|
||||||
for (int iteration = 0; iteration < maxIterations; iteration++) {
|
for (int iteration = 0; iteration < maxIterations; iteration++) {
|
||||||
m_logger->debug("Agentic loop iteration {}", iteration + 1);
|
m_logger->debug("Agentic loop iteration {}", iteration + 1);
|
||||||
|
|
||||||
auto response = m_provider->chat(systemPrompt, messages, tools);
|
auto response = m_provider->chat(systemPrompt, messages, tools);
|
||||||
totalTokens += response.input_tokens + response.output_tokens;
|
totalTokens += response.input_tokens + response.output_tokens;
|
||||||
|
|
||||||
if (response.is_end_turn) {
|
if (response.is_end_turn) {
|
||||||
return {
|
return {
|
||||||
{"response", response.text},
|
{"response", response.text},
|
||||||
{"iterations", iteration + 1},
|
{"iterations", iteration + 1},
|
||||||
{"tokens", totalTokens}
|
{"tokens", totalTokens}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute tool calls
|
// Execute tool calls
|
||||||
if (!response.tool_calls.empty()) {
|
if (!response.tool_calls.empty()) {
|
||||||
std::vector<ToolResult> results;
|
std::vector<ToolResult> results;
|
||||||
|
|
||||||
for (const auto& call : response.tool_calls) {
|
for (const auto& call : response.tool_calls) {
|
||||||
m_logger->debug("Executing tool: {}", call.name);
|
m_logger->debug("Executing tool: {}", call.name);
|
||||||
nlohmann::json result = m_toolRegistry.execute(call.name, call.input);
|
nlohmann::json result = m_toolRegistry.execute(call.name, call.input);
|
||||||
results.push_back({call.id, result.dump(), false});
|
results.push_back({call.id, result.dump(), false});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append assistant message and tool results
|
// Append assistant message and tool results
|
||||||
m_provider->appendAssistantMessage(messages, response);
|
m_provider->appendAssistantMessage(messages, response);
|
||||||
auto toolResultsMsg = m_provider->formatToolResults(results);
|
auto toolResultsMsg = m_provider->formatToolResults(results);
|
||||||
|
|
||||||
if (toolResultsMsg.is_array()) {
|
if (toolResultsMsg.is_array()) {
|
||||||
for (const auto& msg : toolResultsMsg) {
|
for (const auto& msg : toolResultsMsg) {
|
||||||
messages.push_back(msg);
|
messages.push_back(msg);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
messages.push_back(toolResultsMsg);
|
messages.push_back(toolResultsMsg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {{"error", "max_iterations_reached"}};
|
return {{"error", "max_iterations_reached"}};
|
||||||
}
|
}
|
||||||
|
|
||||||
void LLMService::shutdown() {
|
void LLMService::shutdown() {
|
||||||
m_running = false;
|
m_running = false;
|
||||||
m_requestCV.notify_all();
|
m_requestCV.notify_all();
|
||||||
|
|
||||||
if (m_workerThread.joinable()) {
|
if (m_workerThread.joinable()) {
|
||||||
m_workerThread.join();
|
m_workerThread.join();
|
||||||
}
|
}
|
||||||
|
|
||||||
m_logger->info("LLMService shutdown");
|
m_logger->info("LLMService shutdown");
|
||||||
}
|
}
|
||||||
|
|
||||||
LLMService::SyncResponse LLMService::sendMessageSync(
|
LLMService::SyncResponse LLMService::sendMessageSync(
|
||||||
const std::string& message,
|
const std::string& message,
|
||||||
const std::string& conversationId,
|
const std::string& conversationId,
|
||||||
const std::string& systemPrompt
|
const std::string& systemPrompt
|
||||||
) {
|
) {
|
||||||
SyncResponse syncResp;
|
SyncResponse syncResp;
|
||||||
|
|
||||||
// Create request (same as async mode)
|
// Create request (same as async mode)
|
||||||
Request request;
|
Request request;
|
||||||
request.query = message;
|
request.query = message;
|
||||||
request.conversationId = conversationId.empty() ? "mcp-session" : conversationId;
|
request.conversationId = conversationId.empty() ? "mcp-session" : conversationId;
|
||||||
request.systemPrompt = systemPrompt.empty() ? m_defaultSystemPrompt : systemPrompt;
|
request.systemPrompt = systemPrompt.empty() ? m_defaultSystemPrompt : systemPrompt;
|
||||||
request.maxIterations = m_maxIterations;
|
request.maxIterations = m_maxIterations;
|
||||||
|
|
||||||
// Process synchronously (blocking call)
|
// Process synchronously (blocking call)
|
||||||
auto response = processRequest(request);
|
auto response = processRequest(request);
|
||||||
|
|
||||||
// Convert to SyncResponse
|
// Convert to SyncResponse
|
||||||
if (!response.isError) {
|
if (!response.isError) {
|
||||||
syncResp.text = response.text;
|
syncResp.text = response.text;
|
||||||
syncResp.tokens = response.tokens;
|
syncResp.tokens = response.tokens;
|
||||||
syncResp.iterations = response.iterations;
|
syncResp.iterations = response.iterations;
|
||||||
} else {
|
} else {
|
||||||
// On error, return error in text
|
// On error, return error in text
|
||||||
syncResp.text = "Error: " + response.text;
|
syncResp.text = "Error: " + response.text;
|
||||||
syncResp.tokens = 0;
|
syncResp.tokens = 0;
|
||||||
syncResp.iterations = 0;
|
syncResp.iterations = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return syncResp;
|
return syncResp;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace aissia
|
} // namespace aissia
|
||||||
|
|||||||
@ -1,140 +1,140 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "IService.hpp"
|
#include "IService.hpp"
|
||||||
#include "../shared/llm/ILLMProvider.hpp"
|
#include "../shared/llm/ILLMProvider.hpp"
|
||||||
#include "../shared/llm/ToolRegistry.hpp"
|
#include "../shared/llm/ToolRegistry.hpp"
|
||||||
#include "../shared/tools/InternalTools.hpp"
|
#include "../shared/tools/InternalTools.hpp"
|
||||||
#include "../shared/tools/FileSystemTools.hpp"
|
#include "../shared/tools/FileSystemTools.hpp"
|
||||||
#include "../shared/mcp/MCPClient.hpp"
|
#include "../shared/mcp/MCPClient.hpp"
|
||||||
|
|
||||||
#include <grove/IIO.h>
|
#include <grove/IIO.h>
|
||||||
#include <grove/JsonDataNode.h>
|
#include <grove/JsonDataNode.h>
|
||||||
#include <spdlog/spdlog.h>
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <queue>
|
#include <queue>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <condition_variable>
|
#include <condition_variable>
|
||||||
|
|
||||||
namespace aissia {
|
namespace aissia {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief LLM Service - Async HTTP calls to LLM providers
|
* @brief LLM Service - Async HTTP calls to LLM providers
|
||||||
*
|
*
|
||||||
* Handles all LLM API calls in a background thread.
|
* Handles all LLM API calls in a background thread.
|
||||||
* Modules communicate via IIO:
|
* Modules communicate via IIO:
|
||||||
*
|
*
|
||||||
* Subscribes to:
|
* Subscribes to:
|
||||||
* - "llm:request" : { query, systemPrompt?, tools?, conversationId? }
|
* - "llm:request" : { query, systemPrompt?, tools?, conversationId? }
|
||||||
*
|
*
|
||||||
* Publishes:
|
* Publishes:
|
||||||
* - "llm:response" : { text, conversationId, tokens, iterations }
|
* - "llm:response" : { text, conversationId, tokens, iterations }
|
||||||
* - "llm:error" : { message, conversationId }
|
* - "llm:error" : { message, conversationId }
|
||||||
* - "llm:thinking" : { conversationId } (during agentic loop)
|
* - "llm:thinking" : { conversationId } (during agentic loop)
|
||||||
*/
|
*/
|
||||||
class LLMService : public IService {
|
class LLMService : public IService {
|
||||||
public:
|
public:
|
||||||
LLMService();
|
LLMService();
|
||||||
~LLMService() override;
|
~LLMService() override;
|
||||||
|
|
||||||
bool initialize(grove::IIO* io) override;
|
bool initialize(grove::IIO* io) override;
|
||||||
void process() override;
|
void process() override;
|
||||||
void shutdown() override;
|
void shutdown() override;
|
||||||
std::string getName() const override { return "LLMService"; }
|
std::string getName() const override { return "LLMService"; }
|
||||||
bool isHealthy() const override { return m_provider != nullptr; }
|
bool isHealthy() const override { return m_provider != nullptr; }
|
||||||
|
|
||||||
/// Load provider from config file
|
/// Load provider from config file
|
||||||
bool loadConfig(const std::string& configPath);
|
bool loadConfig(const std::string& configPath);
|
||||||
|
|
||||||
/// Register a tool that can be called by the LLM
|
/// Register a tool that can be called by the LLM
|
||||||
void registerTool(const std::string& name, const std::string& description,
|
void registerTool(const std::string& name, const std::string& description,
|
||||||
const nlohmann::json& schema,
|
const nlohmann::json& schema,
|
||||||
std::function<nlohmann::json(const nlohmann::json&)> handler);
|
std::function<nlohmann::json(const nlohmann::json&)> handler);
|
||||||
|
|
||||||
/// Load and initialize all tools (internal + MCP)
|
/// Load and initialize all tools (internal + MCP)
|
||||||
void initializeTools();
|
void initializeTools();
|
||||||
|
|
||||||
/// Load MCP server configurations
|
/// Load MCP server configurations
|
||||||
bool loadMCPConfig(const std::string& configPath);
|
bool loadMCPConfig(const std::string& configPath);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Synchronous response structure for MCP Server mode
|
* @brief Synchronous response structure for MCP Server mode
|
||||||
*/
|
*/
|
||||||
struct SyncResponse {
|
struct SyncResponse {
|
||||||
std::string text;
|
std::string text;
|
||||||
int tokens = 0;
|
int tokens = 0;
|
||||||
int iterations = 0;
|
int iterations = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Send message synchronously (blocking, for MCP Server mode)
|
* @brief Send message synchronously (blocking, for MCP Server mode)
|
||||||
*
|
*
|
||||||
* @param message User message
|
* @param message User message
|
||||||
* @param conversationId Conversation ID (optional)
|
* @param conversationId Conversation ID (optional)
|
||||||
* @param systemPrompt Custom system prompt (optional)
|
* @param systemPrompt Custom system prompt (optional)
|
||||||
* @return Sync response with text, tokens, iterations
|
* @return Sync response with text, tokens, iterations
|
||||||
*/
|
*/
|
||||||
SyncResponse sendMessageSync(
|
SyncResponse sendMessageSync(
|
||||||
const std::string& message,
|
const std::string& message,
|
||||||
const std::string& conversationId = "",
|
const std::string& conversationId = "",
|
||||||
const std::string& systemPrompt = ""
|
const std::string& systemPrompt = ""
|
||||||
);
|
);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct Request {
|
struct Request {
|
||||||
std::string query;
|
std::string query;
|
||||||
std::string systemPrompt;
|
std::string systemPrompt;
|
||||||
std::string conversationId;
|
std::string conversationId;
|
||||||
nlohmann::json tools;
|
nlohmann::json tools;
|
||||||
int maxIterations = 10;
|
int maxIterations = 10;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Response {
|
struct Response {
|
||||||
std::string text;
|
std::string text;
|
||||||
std::string conversationId;
|
std::string conversationId;
|
||||||
int tokens = 0;
|
int tokens = 0;
|
||||||
int iterations = 0;
|
int iterations = 0;
|
||||||
bool isError = false;
|
bool isError = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Configuration
|
// Configuration
|
||||||
std::string m_providerName = "claude";
|
std::string m_providerName = "claude";
|
||||||
std::string m_defaultSystemPrompt;
|
std::string m_defaultSystemPrompt;
|
||||||
int m_maxIterations = 10;
|
int m_maxIterations = 10;
|
||||||
|
|
||||||
// State
|
// State
|
||||||
std::unique_ptr<ILLMProvider> m_provider;
|
std::unique_ptr<ILLMProvider> m_provider;
|
||||||
ToolRegistry m_toolRegistry;
|
ToolRegistry m_toolRegistry;
|
||||||
std::unique_ptr<InternalTools> m_internalTools;
|
std::unique_ptr<InternalTools> m_internalTools;
|
||||||
std::unique_ptr<mcp::MCPClient> m_mcpClient;
|
std::unique_ptr<mcp::MCPClient> m_mcpClient;
|
||||||
std::map<std::string, nlohmann::json> m_conversations; // conversationId -> history
|
std::map<std::string, nlohmann::json> m_conversations; // conversationId -> history
|
||||||
|
|
||||||
// Threading
|
// Threading
|
||||||
std::thread m_workerThread;
|
std::thread m_workerThread;
|
||||||
std::atomic<bool> m_running{false};
|
std::atomic<bool> m_running{false};
|
||||||
std::queue<Request> m_requestQueue;
|
std::queue<Request> m_requestQueue;
|
||||||
std::queue<Response> m_responseQueue;
|
std::queue<Response> m_responseQueue;
|
||||||
std::mutex m_requestMutex;
|
std::mutex m_requestMutex;
|
||||||
std::mutex m_responseMutex;
|
std::mutex m_responseMutex;
|
||||||
std::condition_variable m_requestCV;
|
std::condition_variable m_requestCV;
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
grove::IIO* m_io = nullptr;
|
grove::IIO* m_io = nullptr;
|
||||||
std::shared_ptr<spdlog::logger> m_logger;
|
std::shared_ptr<spdlog::logger> m_logger;
|
||||||
|
|
||||||
// Worker thread
|
// Worker thread
|
||||||
void workerLoop();
|
void workerLoop();
|
||||||
Response processRequest(const Request& request);
|
Response processRequest(const Request& request);
|
||||||
nlohmann::json agenticLoop(const std::string& query, const std::string& systemPrompt,
|
nlohmann::json agenticLoop(const std::string& query, const std::string& systemPrompt,
|
||||||
nlohmann::json& messages, const nlohmann::json& tools,
|
nlohmann::json& messages, const nlohmann::json& tools,
|
||||||
int maxIterations);
|
int maxIterations);
|
||||||
|
|
||||||
// Message handling
|
// Message handling
|
||||||
void processIncomingMessages();
|
void processIncomingMessages();
|
||||||
void publishResponses();
|
void publishResponses();
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace aissia
|
} // namespace aissia
|
||||||
|
|||||||
@ -1,139 +1,139 @@
|
|||||||
#include "PlatformService.hpp"
|
#include "PlatformService.hpp"
|
||||||
|
|
||||||
#include <spdlog/sinks/stdout_color_sinks.h>
|
#include <spdlog/sinks/stdout_color_sinks.h>
|
||||||
|
|
||||||
namespace aissia {
|
namespace aissia {
|
||||||
|
|
||||||
PlatformService::PlatformService() {
|
PlatformService::PlatformService() {
|
||||||
m_logger = spdlog::get("PlatformService");
|
m_logger = spdlog::get("PlatformService");
|
||||||
if (!m_logger) {
|
if (!m_logger) {
|
||||||
m_logger = spdlog::stdout_color_mt("PlatformService");
|
m_logger = spdlog::stdout_color_mt("PlatformService");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PlatformService::initialize(grove::IIO* io) {
|
bool PlatformService::initialize(grove::IIO* io) {
|
||||||
m_io = io;
|
m_io = io;
|
||||||
|
|
||||||
// Create platform-specific window tracker
|
// Create platform-specific window tracker
|
||||||
m_tracker = WindowTrackerFactory::create();
|
m_tracker = WindowTrackerFactory::create();
|
||||||
|
|
||||||
if (!m_tracker || !m_tracker->isAvailable()) {
|
if (!m_tracker || !m_tracker->isAvailable()) {
|
||||||
m_logger->warn("Window tracker not available on this platform");
|
m_logger->warn("Window tracker not available on this platform");
|
||||||
return true; // Non-fatal, module can work without tracking
|
return true; // Non-fatal, module can work without tracking
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_io) {
|
if (m_io) {
|
||||||
grove::SubscriptionConfig config;
|
grove::SubscriptionConfig config;
|
||||||
m_io->subscribe("platform:query_window", config);
|
m_io->subscribe("platform:query_window", config);
|
||||||
}
|
}
|
||||||
|
|
||||||
m_logger->info("PlatformService initialized: {}", m_tracker->getPlatformName());
|
m_logger->info("PlatformService initialized: {}", m_tracker->getPlatformName());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlatformService::configure(int pollIntervalMs, int idleThresholdSeconds) {
|
void PlatformService::configure(int pollIntervalMs, int idleThresholdSeconds) {
|
||||||
m_pollIntervalMs = pollIntervalMs;
|
m_pollIntervalMs = pollIntervalMs;
|
||||||
m_idleThresholdSeconds = idleThresholdSeconds;
|
m_idleThresholdSeconds = idleThresholdSeconds;
|
||||||
m_logger->debug("Configured: poll={}ms, idle={}s", pollIntervalMs, idleThresholdSeconds);
|
m_logger->debug("Configured: poll={}ms, idle={}s", pollIntervalMs, idleThresholdSeconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlatformService::process() {
|
void PlatformService::process() {
|
||||||
if (!m_tracker || !m_tracker->isAvailable()) return;
|
if (!m_tracker || !m_tracker->isAvailable()) return;
|
||||||
|
|
||||||
// Use monotonic clock for timing
|
// Use monotonic clock for timing
|
||||||
static auto startTime = std::chrono::steady_clock::now();
|
static auto startTime = std::chrono::steady_clock::now();
|
||||||
auto now = std::chrono::steady_clock::now();
|
auto now = std::chrono::steady_clock::now();
|
||||||
float currentTime = std::chrono::duration<float>(now - startTime).count();
|
float currentTime = std::chrono::duration<float>(now - startTime).count();
|
||||||
|
|
||||||
float pollIntervalSec = m_pollIntervalMs / 1000.0f;
|
float pollIntervalSec = m_pollIntervalMs / 1000.0f;
|
||||||
if (currentTime - m_lastPollTime >= pollIntervalSec) {
|
if (currentTime - m_lastPollTime >= pollIntervalSec) {
|
||||||
m_lastPollTime = currentTime;
|
m_lastPollTime = currentTime;
|
||||||
pollWindowInfo(currentTime);
|
pollWindowInfo(currentTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle query requests
|
// Handle query requests
|
||||||
if (m_io) {
|
if (m_io) {
|
||||||
while (m_io->hasMessages() > 0) {
|
while (m_io->hasMessages() > 0) {
|
||||||
auto msg = m_io->pullMessage();
|
auto msg = m_io->pullMessage();
|
||||||
if (msg.topic == "platform:query_window") {
|
if (msg.topic == "platform:query_window") {
|
||||||
publishWindowInfo();
|
publishWindowInfo();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlatformService::pollWindowInfo(float currentTime) {
|
void PlatformService::pollWindowInfo(float currentTime) {
|
||||||
std::string newApp = m_tracker->getCurrentAppName();
|
std::string newApp = m_tracker->getCurrentAppName();
|
||||||
std::string newTitle = m_tracker->getCurrentWindowTitle();
|
std::string newTitle = m_tracker->getCurrentWindowTitle();
|
||||||
|
|
||||||
// Check for app change
|
// Check for app change
|
||||||
if (newApp != m_currentApp) {
|
if (newApp != m_currentApp) {
|
||||||
int duration = static_cast<int>(currentTime - m_appStartTime);
|
int duration = static_cast<int>(currentTime - m_appStartTime);
|
||||||
|
|
||||||
if (!m_currentApp.empty() && duration > 0) {
|
if (!m_currentApp.empty() && duration > 0) {
|
||||||
publishWindowChanged(m_currentApp, newApp, duration);
|
publishWindowChanged(m_currentApp, newApp, duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
m_currentApp = newApp;
|
m_currentApp = newApp;
|
||||||
m_currentWindowTitle = newTitle;
|
m_currentWindowTitle = newTitle;
|
||||||
m_appStartTime = currentTime;
|
m_appStartTime = currentTime;
|
||||||
|
|
||||||
m_logger->debug("App: {} - {}", m_currentApp,
|
m_logger->debug("App: {} - {}", m_currentApp,
|
||||||
m_currentWindowTitle.size() > 50 ?
|
m_currentWindowTitle.size() > 50 ?
|
||||||
m_currentWindowTitle.substr(0, 50) + "..." : m_currentWindowTitle);
|
m_currentWindowTitle.substr(0, 50) + "..." : m_currentWindowTitle);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check idle state
|
// Check idle state
|
||||||
bool isIdle = m_tracker->isUserIdle(m_idleThresholdSeconds);
|
bool isIdle = m_tracker->isUserIdle(m_idleThresholdSeconds);
|
||||||
|
|
||||||
if (isIdle && !m_wasIdle) {
|
if (isIdle && !m_wasIdle) {
|
||||||
m_logger->info("User idle detected ({}s)", m_idleThresholdSeconds);
|
m_logger->info("User idle detected ({}s)", m_idleThresholdSeconds);
|
||||||
if (m_io) {
|
if (m_io) {
|
||||||
auto event = std::make_unique<grove::JsonDataNode>("idle");
|
auto event = std::make_unique<grove::JsonDataNode>("idle");
|
||||||
event->setInt("idleSeconds", m_tracker->getIdleTimeSeconds());
|
event->setInt("idleSeconds", m_tracker->getIdleTimeSeconds());
|
||||||
m_io->publish("platform:idle_detected", std::move(event));
|
m_io->publish("platform:idle_detected", std::move(event));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (!isIdle && m_wasIdle) {
|
else if (!isIdle && m_wasIdle) {
|
||||||
m_logger->info("User activity resumed");
|
m_logger->info("User activity resumed");
|
||||||
if (m_io) {
|
if (m_io) {
|
||||||
auto event = std::make_unique<grove::JsonDataNode>("active");
|
auto event = std::make_unique<grove::JsonDataNode>("active");
|
||||||
m_io->publish("platform:activity_resumed", std::move(event));
|
m_io->publish("platform:activity_resumed", std::move(event));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m_wasIdle = isIdle;
|
m_wasIdle = isIdle;
|
||||||
|
|
||||||
// Publish periodic window info
|
// Publish periodic window info
|
||||||
publishWindowInfo();
|
publishWindowInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlatformService::publishWindowInfo() {
|
void PlatformService::publishWindowInfo() {
|
||||||
if (!m_io || !m_tracker) return;
|
if (!m_io || !m_tracker) return;
|
||||||
|
|
||||||
auto event = std::make_unique<grove::JsonDataNode>("window");
|
auto event = std::make_unique<grove::JsonDataNode>("window");
|
||||||
event->setString("appName", m_currentApp);
|
event->setString("appName", m_currentApp);
|
||||||
event->setString("windowTitle", m_currentWindowTitle);
|
event->setString("windowTitle", m_currentWindowTitle);
|
||||||
event->setBool("isIdle", m_wasIdle);
|
event->setBool("isIdle", m_wasIdle);
|
||||||
event->setInt("idleSeconds", m_tracker->getIdleTimeSeconds());
|
event->setInt("idleSeconds", m_tracker->getIdleTimeSeconds());
|
||||||
m_io->publish("platform:window_info", std::move(event));
|
m_io->publish("platform:window_info", std::move(event));
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlatformService::publishWindowChanged(const std::string& oldApp,
|
void PlatformService::publishWindowChanged(const std::string& oldApp,
|
||||||
const std::string& newApp,
|
const std::string& newApp,
|
||||||
int duration) {
|
int duration) {
|
||||||
if (!m_io) return;
|
if (!m_io) return;
|
||||||
|
|
||||||
auto event = std::make_unique<grove::JsonDataNode>("changed");
|
auto event = std::make_unique<grove::JsonDataNode>("changed");
|
||||||
event->setString("oldApp", oldApp);
|
event->setString("oldApp", oldApp);
|
||||||
event->setString("newApp", newApp);
|
event->setString("newApp", newApp);
|
||||||
event->setInt("duration", duration);
|
event->setInt("duration", duration);
|
||||||
m_io->publish("platform:window_changed", std::move(event));
|
m_io->publish("platform:window_changed", std::move(event));
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlatformService::shutdown() {
|
void PlatformService::shutdown() {
|
||||||
m_tracker.reset();
|
m_tracker.reset();
|
||||||
m_logger->info("PlatformService shutdown");
|
m_logger->info("PlatformService shutdown");
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace aissia
|
} // namespace aissia
|
||||||
|
|||||||
@ -1,67 +1,67 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "IService.hpp"
|
#include "IService.hpp"
|
||||||
#include "../shared/platform/IWindowTracker.hpp"
|
#include "../shared/platform/IWindowTracker.hpp"
|
||||||
|
|
||||||
#include <grove/IIO.h>
|
#include <grove/IIO.h>
|
||||||
#include <grove/JsonDataNode.h>
|
#include <grove/JsonDataNode.h>
|
||||||
#include <spdlog/spdlog.h>
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
namespace aissia {
|
namespace aissia {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Platform Service - OS-specific APIs (window tracking, etc.)
|
* @brief Platform Service - OS-specific APIs (window tracking, etc.)
|
||||||
*
|
*
|
||||||
* Handles platform-specific operations that can't be in hot-reload modules.
|
* Handles platform-specific operations that can't be in hot-reload modules.
|
||||||
* Polls foreground window at configurable interval.
|
* Polls foreground window at configurable interval.
|
||||||
*
|
*
|
||||||
* Subscribes to:
|
* Subscribes to:
|
||||||
* - "platform:query_window" : Request current window info
|
* - "platform:query_window" : Request current window info
|
||||||
*
|
*
|
||||||
* Publishes:
|
* Publishes:
|
||||||
* - "platform:window_info" : { appName, windowTitle, isIdle, idleSeconds }
|
* - "platform:window_info" : { appName, windowTitle, isIdle, idleSeconds }
|
||||||
* - "platform:window_changed" : { oldApp, newApp, duration }
|
* - "platform:window_changed" : { oldApp, newApp, duration }
|
||||||
* - "platform:idle_detected" : { idleSeconds }
|
* - "platform:idle_detected" : { idleSeconds }
|
||||||
* - "platform:activity_resumed" : {}
|
* - "platform:activity_resumed" : {}
|
||||||
*/
|
*/
|
||||||
class PlatformService : public IService {
|
class PlatformService : public IService {
|
||||||
public:
|
public:
|
||||||
PlatformService();
|
PlatformService();
|
||||||
~PlatformService() override = default;
|
~PlatformService() override = default;
|
||||||
|
|
||||||
bool initialize(grove::IIO* io) override;
|
bool initialize(grove::IIO* io) override;
|
||||||
void process() override;
|
void process() override;
|
||||||
void shutdown() override;
|
void shutdown() override;
|
||||||
std::string getName() const override { return "PlatformService"; }
|
std::string getName() const override { return "PlatformService"; }
|
||||||
bool isHealthy() const override { return m_tracker != nullptr && m_tracker->isAvailable(); }
|
bool isHealthy() const override { return m_tracker != nullptr && m_tracker->isAvailable(); }
|
||||||
|
|
||||||
/// Configure polling interval and idle threshold
|
/// Configure polling interval and idle threshold
|
||||||
void configure(int pollIntervalMs = 1000, int idleThresholdSeconds = 300);
|
void configure(int pollIntervalMs = 1000, int idleThresholdSeconds = 300);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Configuration
|
// Configuration
|
||||||
int m_pollIntervalMs = 1000;
|
int m_pollIntervalMs = 1000;
|
||||||
int m_idleThresholdSeconds = 300;
|
int m_idleThresholdSeconds = 300;
|
||||||
|
|
||||||
// State
|
// State
|
||||||
std::unique_ptr<IWindowTracker> m_tracker;
|
std::unique_ptr<IWindowTracker> m_tracker;
|
||||||
std::string m_currentApp;
|
std::string m_currentApp;
|
||||||
std::string m_currentWindowTitle;
|
std::string m_currentWindowTitle;
|
||||||
float m_appStartTime = 0.0f;
|
float m_appStartTime = 0.0f;
|
||||||
float m_lastPollTime = 0.0f;
|
float m_lastPollTime = 0.0f;
|
||||||
bool m_wasIdle = false;
|
bool m_wasIdle = false;
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
grove::IIO* m_io = nullptr;
|
grove::IIO* m_io = nullptr;
|
||||||
std::shared_ptr<spdlog::logger> m_logger;
|
std::shared_ptr<spdlog::logger> m_logger;
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
void pollWindowInfo(float currentTime);
|
void pollWindowInfo(float currentTime);
|
||||||
void publishWindowInfo();
|
void publishWindowInfo();
|
||||||
void publishWindowChanged(const std::string& oldApp, const std::string& newApp, int duration);
|
void publishWindowChanged(const std::string& oldApp, const std::string& newApp, int duration);
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace aissia
|
} // namespace aissia
|
||||||
|
|||||||
@ -1,189 +1,189 @@
|
|||||||
// CRITICAL ORDER: Include system headers first
|
// CRITICAL ORDER: Include system headers first
|
||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
// Include local headers before spdlog
|
// Include local headers before spdlog
|
||||||
#include "STTService.hpp"
|
#include "STTService.hpp"
|
||||||
#include "../shared/audio/ISTTEngine.hpp"
|
#include "../shared/audio/ISTTEngine.hpp"
|
||||||
|
|
||||||
// Include spdlog after local headers
|
// Include spdlog after local headers
|
||||||
#include <spdlog/spdlog.h>
|
#include <spdlog/spdlog.h>
|
||||||
#include <spdlog/sinks/stdout_color_sinks.h>
|
#include <spdlog/sinks/stdout_color_sinks.h>
|
||||||
|
|
||||||
namespace aissia {
|
namespace aissia {
|
||||||
|
|
||||||
STTService::STTService(const nlohmann::json& config)
|
STTService::STTService(const nlohmann::json& config)
|
||||||
: m_config(config)
|
: m_config(config)
|
||||||
{
|
{
|
||||||
m_logger = spdlog::get("STTService");
|
m_logger = spdlog::get("STTService");
|
||||||
if (!m_logger) {
|
if (!m_logger) {
|
||||||
m_logger = spdlog::stdout_color_mt("STTService");
|
m_logger = spdlog::stdout_color_mt("STTService");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract language from config
|
// Extract language from config
|
||||||
if (config.contains("active_mode") && config["active_mode"].contains("language")) {
|
if (config.contains("active_mode") && config["active_mode"].contains("language")) {
|
||||||
m_language = config["active_mode"]["language"].get<std::string>();
|
m_language = config["active_mode"]["language"].get<std::string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
m_logger->info("STTService created");
|
m_logger->info("STTService created");
|
||||||
}
|
}
|
||||||
|
|
||||||
STTService::~STTService() {
|
STTService::~STTService() {
|
||||||
stop();
|
stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool STTService::start() {
|
bool STTService::start() {
|
||||||
m_logger->info("Starting STT service");
|
m_logger->info("Starting STT service");
|
||||||
|
|
||||||
loadEngines();
|
loadEngines();
|
||||||
|
|
||||||
if (!m_activeEngine || !m_activeEngine->isAvailable()) {
|
if (!m_activeEngine || !m_activeEngine->isAvailable()) {
|
||||||
m_logger->error("No active STT engine available");
|
m_logger->error("No active STT engine available");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_logger->info("STT service started");
|
m_logger->info("STT service started");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void STTService::stop() {
|
void STTService::stop() {
|
||||||
m_logger->info("Stopping STT service");
|
m_logger->info("Stopping STT service");
|
||||||
stopListening();
|
stopListening();
|
||||||
m_activeEngine.reset();
|
m_activeEngine.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
void STTService::setMode(STTMode mode) {
|
void STTService::setMode(STTMode mode) {
|
||||||
if (m_currentMode == mode) {
|
if (m_currentMode == mode) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_logger->info("Switching STT mode");
|
m_logger->info("Switching STT mode");
|
||||||
m_currentMode = mode;
|
m_currentMode = mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string STTService::transcribeFile(const std::string& filePath) {
|
std::string STTService::transcribeFile(const std::string& filePath) {
|
||||||
if (!m_activeEngine || !m_activeEngine->isAvailable()) {
|
if (!m_activeEngine || !m_activeEngine->isAvailable()) {
|
||||||
m_logger->warn("No STT engine available for transcription");
|
m_logger->warn("No STT engine available for transcription");
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
m_logger->info("Transcribing file");
|
m_logger->info("Transcribing file");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
std::string result = m_activeEngine->transcribeFile(filePath);
|
std::string result = m_activeEngine->transcribeFile(filePath);
|
||||||
m_logger->info("Transcription complete");
|
m_logger->info("Transcription complete");
|
||||||
return result;
|
return result;
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
m_logger->error("Transcription failed");
|
m_logger->error("Transcription failed");
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string STTService::transcribe(const std::vector<float>& audioData) {
|
std::string STTService::transcribe(const std::vector<float>& audioData) {
|
||||||
if (!m_activeEngine || !m_activeEngine->isAvailable()) {
|
if (!m_activeEngine || !m_activeEngine->isAvailable()) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (audioData.empty()) {
|
if (audioData.empty()) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
std::string result = m_activeEngine->transcribe(audioData);
|
std::string result = m_activeEngine->transcribe(audioData);
|
||||||
|
|
||||||
if (!result.empty() && m_listening && m_onTranscription) {
|
if (!result.empty() && m_listening && m_onTranscription) {
|
||||||
m_onTranscription(result, m_currentMode);
|
m_onTranscription(result, m_currentMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
m_logger->error("Transcription failed");
|
m_logger->error("Transcription failed");
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void STTService::startListening(TranscriptionCallback onTranscription,
|
void STTService::startListening(TranscriptionCallback onTranscription,
|
||||||
KeywordCallback onKeyword) {
|
KeywordCallback onKeyword) {
|
||||||
m_logger->info("Start listening");
|
m_logger->info("Start listening");
|
||||||
|
|
||||||
m_onTranscription = onTranscription;
|
m_onTranscription = onTranscription;
|
||||||
m_onKeyword = onKeyword;
|
m_onKeyword = onKeyword;
|
||||||
m_listening = true;
|
m_listening = true;
|
||||||
|
|
||||||
m_logger->warn("Streaming microphone capture not yet implemented");
|
m_logger->warn("Streaming microphone capture not yet implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
void STTService::stopListening() {
|
void STTService::stopListening() {
|
||||||
if (!m_listening) {
|
if (!m_listening) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_logger->info("Stop listening");
|
m_logger->info("Stop listening");
|
||||||
m_listening = false;
|
m_listening = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void STTService::setLanguage(const std::string& language) {
|
void STTService::setLanguage(const std::string& language) {
|
||||||
m_logger->info("Setting language");
|
m_logger->info("Setting language");
|
||||||
m_language = language;
|
m_language = language;
|
||||||
|
|
||||||
if (m_activeEngine) {
|
if (m_activeEngine) {
|
||||||
m_activeEngine->setLanguage(language);
|
m_activeEngine->setLanguage(language);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool STTService::isAvailable() const {
|
bool STTService::isAvailable() const {
|
||||||
return m_activeEngine && m_activeEngine->isAvailable();
|
return m_activeEngine && m_activeEngine->isAvailable();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string STTService::getCurrentEngine() const {
|
std::string STTService::getCurrentEngine() const {
|
||||||
if (m_activeEngine) {
|
if (m_activeEngine) {
|
||||||
return m_activeEngine->getEngineName();
|
return m_activeEngine->getEngineName();
|
||||||
}
|
}
|
||||||
return "none";
|
return "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
void STTService::loadEngines() {
|
void STTService::loadEngines() {
|
||||||
m_logger->info("Loading STT engines");
|
m_logger->info("Loading STT engines");
|
||||||
|
|
||||||
std::string engineType = "auto";
|
std::string engineType = "auto";
|
||||||
if (m_config.contains("active_mode")) {
|
if (m_config.contains("active_mode")) {
|
||||||
const auto& activeMode = m_config["active_mode"];
|
const auto& activeMode = m_config["active_mode"];
|
||||||
if (activeMode.contains("engine")) {
|
if (activeMode.contains("engine")) {
|
||||||
engineType = activeMode["engine"];
|
engineType = activeMode["engine"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string modelPath;
|
std::string modelPath;
|
||||||
if (m_config.contains("active_mode")) {
|
if (m_config.contains("active_mode")) {
|
||||||
const auto& activeMode = m_config["active_mode"];
|
const auto& activeMode = m_config["active_mode"];
|
||||||
if (activeMode.contains("model_path")) {
|
if (activeMode.contains("model_path")) {
|
||||||
modelPath = activeMode["model_path"];
|
modelPath = activeMode["model_path"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string apiKey;
|
std::string apiKey;
|
||||||
if (m_config.contains("whisper_api")) {
|
if (m_config.contains("whisper_api")) {
|
||||||
const auto& whisperApi = m_config["whisper_api"];
|
const auto& whisperApi = m_config["whisper_api"];
|
||||||
std::string apiKeyEnv = "OPENAI_API_KEY";
|
std::string apiKeyEnv = "OPENAI_API_KEY";
|
||||||
if (whisperApi.contains("api_key_env")) {
|
if (whisperApi.contains("api_key_env")) {
|
||||||
apiKeyEnv = whisperApi["api_key_env"];
|
apiKeyEnv = whisperApi["api_key_env"];
|
||||||
}
|
}
|
||||||
const char* envVal = std::getenv(apiKeyEnv.c_str());
|
const char* envVal = std::getenv(apiKeyEnv.c_str());
|
||||||
if (envVal) {
|
if (envVal) {
|
||||||
apiKey = envVal;
|
apiKey = envVal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m_activeEngine = STTEngineFactory::create(engineType, modelPath, apiKey);
|
m_activeEngine = STTEngineFactory::create(engineType, modelPath, apiKey);
|
||||||
|
|
||||||
if (m_activeEngine && m_activeEngine->isAvailable()) {
|
if (m_activeEngine && m_activeEngine->isAvailable()) {
|
||||||
m_activeEngine->setLanguage(m_language);
|
m_activeEngine->setLanguage(m_language);
|
||||||
m_logger->info("STT engine loaded successfully");
|
m_logger->info("STT engine loaded successfully");
|
||||||
} else {
|
} else {
|
||||||
m_logger->warn("No active STT engine available");
|
m_logger->warn("No active STT engine available");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace aissia
|
} // namespace aissia
|
||||||
|
|||||||
@ -1,348 +1,348 @@
|
|||||||
#include "StorageService.hpp"
|
#include "StorageService.hpp"
|
||||||
|
|
||||||
#include <spdlog/sinks/stdout_color_sinks.h>
|
#include <spdlog/sinks/stdout_color_sinks.h>
|
||||||
#include <sqlite3.h>
|
#include <sqlite3.h>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
|
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
namespace aissia {
|
namespace aissia {
|
||||||
|
|
||||||
StorageService::StorageService() {
|
StorageService::StorageService() {
|
||||||
m_logger = spdlog::get("StorageService");
|
m_logger = spdlog::get("StorageService");
|
||||||
if (!m_logger) {
|
if (!m_logger) {
|
||||||
m_logger = spdlog::stdout_color_mt("StorageService");
|
m_logger = spdlog::stdout_color_mt("StorageService");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StorageService::~StorageService() {
|
StorageService::~StorageService() {
|
||||||
shutdown();
|
shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool StorageService::initialize(grove::IIO* io) {
|
bool StorageService::initialize(grove::IIO* io) {
|
||||||
m_io = io;
|
m_io = io;
|
||||||
|
|
||||||
if (m_io) {
|
if (m_io) {
|
||||||
grove::SubscriptionConfig config;
|
grove::SubscriptionConfig config;
|
||||||
m_io->subscribe("storage:save_session", config);
|
m_io->subscribe("storage:save_session", config);
|
||||||
m_io->subscribe("storage:save_app_usage", config);
|
m_io->subscribe("storage:save_app_usage", config);
|
||||||
m_io->subscribe("storage:save_conversation", config);
|
m_io->subscribe("storage:save_conversation", config);
|
||||||
m_io->subscribe("storage:update_metrics", config);
|
m_io->subscribe("storage:update_metrics", config);
|
||||||
}
|
}
|
||||||
|
|
||||||
m_logger->info("StorageService initialized");
|
m_logger->info("StorageService initialized");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool StorageService::openDatabase(const std::string& dbPath,
|
bool StorageService::openDatabase(const std::string& dbPath,
|
||||||
const std::string& journalMode,
|
const std::string& journalMode,
|
||||||
int busyTimeoutMs) {
|
int busyTimeoutMs) {
|
||||||
m_dbPath = dbPath;
|
m_dbPath = dbPath;
|
||||||
|
|
||||||
// Ensure directory exists
|
// Ensure directory exists
|
||||||
fs::path path(dbPath);
|
fs::path path(dbPath);
|
||||||
if (path.has_parent_path()) {
|
if (path.has_parent_path()) {
|
||||||
fs::create_directories(path.parent_path());
|
fs::create_directories(path.parent_path());
|
||||||
}
|
}
|
||||||
|
|
||||||
int rc = sqlite3_open(dbPath.c_str(), &m_db);
|
int rc = sqlite3_open(dbPath.c_str(), &m_db);
|
||||||
if (rc != SQLITE_OK) {
|
if (rc != SQLITE_OK) {
|
||||||
m_logger->error("SQLite open error: {}", sqlite3_errmsg(m_db));
|
m_logger->error("SQLite open error: {}", sqlite3_errmsg(m_db));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set pragmas
|
// Set pragmas
|
||||||
std::string pragmas = "PRAGMA journal_mode=" + journalMode + ";"
|
std::string pragmas = "PRAGMA journal_mode=" + journalMode + ";"
|
||||||
"PRAGMA busy_timeout=" + std::to_string(busyTimeoutMs) + ";"
|
"PRAGMA busy_timeout=" + std::to_string(busyTimeoutMs) + ";"
|
||||||
"PRAGMA foreign_keys=ON;";
|
"PRAGMA foreign_keys=ON;";
|
||||||
if (!executeSQL(pragmas)) {
|
if (!executeSQL(pragmas)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!initializeSchema()) {
|
if (!initializeSchema()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!prepareStatements()) {
|
if (!prepareStatements()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_isConnected = true;
|
m_isConnected = true;
|
||||||
|
|
||||||
// Publish ready event
|
// Publish ready event
|
||||||
if (m_io) {
|
if (m_io) {
|
||||||
auto event = std::make_unique<grove::JsonDataNode>("ready");
|
auto event = std::make_unique<grove::JsonDataNode>("ready");
|
||||||
event->setString("database", dbPath);
|
event->setString("database", dbPath);
|
||||||
m_io->publish("storage:ready", std::move(event));
|
m_io->publish("storage:ready", std::move(event));
|
||||||
}
|
}
|
||||||
|
|
||||||
m_logger->info("Database opened: {}", dbPath);
|
m_logger->info("Database opened: {}", dbPath);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool StorageService::initializeSchema() {
|
bool StorageService::initializeSchema() {
|
||||||
const char* schema = R"SQL(
|
const char* schema = R"SQL(
|
||||||
CREATE TABLE IF NOT EXISTS work_sessions (
|
CREATE TABLE IF NOT EXISTS work_sessions (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
task_name TEXT,
|
task_name TEXT,
|
||||||
start_time INTEGER,
|
start_time INTEGER,
|
||||||
end_time INTEGER,
|
end_time INTEGER,
|
||||||
duration_minutes INTEGER,
|
duration_minutes INTEGER,
|
||||||
hyperfocus_detected BOOLEAN DEFAULT 0,
|
hyperfocus_detected BOOLEAN DEFAULT 0,
|
||||||
created_at INTEGER DEFAULT (strftime('%s', 'now'))
|
created_at INTEGER DEFAULT (strftime('%s', 'now'))
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS app_usage (
|
CREATE TABLE IF NOT EXISTS app_usage (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
session_id INTEGER,
|
session_id INTEGER,
|
||||||
app_name TEXT,
|
app_name TEXT,
|
||||||
duration_seconds INTEGER,
|
duration_seconds INTEGER,
|
||||||
is_productive BOOLEAN,
|
is_productive BOOLEAN,
|
||||||
created_at INTEGER DEFAULT (strftime('%s', 'now')),
|
created_at INTEGER DEFAULT (strftime('%s', 'now')),
|
||||||
FOREIGN KEY (session_id) REFERENCES work_sessions(id)
|
FOREIGN KEY (session_id) REFERENCES work_sessions(id)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS conversations (
|
CREATE TABLE IF NOT EXISTS conversations (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
role TEXT,
|
role TEXT,
|
||||||
content TEXT,
|
content TEXT,
|
||||||
provider TEXT,
|
provider TEXT,
|
||||||
model TEXT,
|
model TEXT,
|
||||||
tokens_used INTEGER,
|
tokens_used INTEGER,
|
||||||
created_at INTEGER DEFAULT (strftime('%s', 'now'))
|
created_at INTEGER DEFAULT (strftime('%s', 'now'))
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS daily_metrics (
|
CREATE TABLE IF NOT EXISTS daily_metrics (
|
||||||
date TEXT PRIMARY KEY,
|
date TEXT PRIMARY KEY,
|
||||||
total_focus_minutes INTEGER DEFAULT 0,
|
total_focus_minutes INTEGER DEFAULT 0,
|
||||||
total_breaks INTEGER DEFAULT 0,
|
total_breaks INTEGER DEFAULT 0,
|
||||||
hyperfocus_count INTEGER DEFAULT 0,
|
hyperfocus_count INTEGER DEFAULT 0,
|
||||||
updated_at INTEGER DEFAULT (strftime('%s', 'now'))
|
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_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_app_usage_session ON app_usage(session_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_conversations_date ON conversations(created_at);
|
CREATE INDEX IF NOT EXISTS idx_conversations_date ON conversations(created_at);
|
||||||
)SQL";
|
)SQL";
|
||||||
|
|
||||||
return executeSQL(schema);
|
return executeSQL(schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool StorageService::prepareStatements() {
|
bool StorageService::prepareStatements() {
|
||||||
int rc;
|
int rc;
|
||||||
|
|
||||||
// Save session statement
|
// Save session statement
|
||||||
const char* sqlSession = "INSERT INTO work_sessions "
|
const char* sqlSession = "INSERT INTO work_sessions "
|
||||||
"(task_name, start_time, end_time, duration_minutes, hyperfocus_detected) "
|
"(task_name, start_time, end_time, duration_minutes, hyperfocus_detected) "
|
||||||
"VALUES (?, ?, ?, ?, ?)";
|
"VALUES (?, ?, ?, ?, ?)";
|
||||||
rc = sqlite3_prepare_v2(m_db, sqlSession, -1, &m_stmtSaveSession, nullptr);
|
rc = sqlite3_prepare_v2(m_db, sqlSession, -1, &m_stmtSaveSession, nullptr);
|
||||||
if (rc != SQLITE_OK) {
|
if (rc != SQLITE_OK) {
|
||||||
m_logger->error("Failed to prepare save_session: {}", sqlite3_errmsg(m_db));
|
m_logger->error("Failed to prepare save_session: {}", sqlite3_errmsg(m_db));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save app usage statement
|
// Save app usage statement
|
||||||
const char* sqlAppUsage = "INSERT INTO app_usage "
|
const char* sqlAppUsage = "INSERT INTO app_usage "
|
||||||
"(session_id, app_name, duration_seconds, is_productive) "
|
"(session_id, app_name, duration_seconds, is_productive) "
|
||||||
"VALUES (?, ?, ?, ?)";
|
"VALUES (?, ?, ?, ?)";
|
||||||
rc = sqlite3_prepare_v2(m_db, sqlAppUsage, -1, &m_stmtSaveAppUsage, nullptr);
|
rc = sqlite3_prepare_v2(m_db, sqlAppUsage, -1, &m_stmtSaveAppUsage, nullptr);
|
||||||
if (rc != SQLITE_OK) {
|
if (rc != SQLITE_OK) {
|
||||||
m_logger->error("Failed to prepare save_app_usage: {}", sqlite3_errmsg(m_db));
|
m_logger->error("Failed to prepare save_app_usage: {}", sqlite3_errmsg(m_db));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save conversation statement
|
// Save conversation statement
|
||||||
const char* sqlConv = "INSERT INTO conversations "
|
const char* sqlConv = "INSERT INTO conversations "
|
||||||
"(role, content, provider, model, tokens_used) "
|
"(role, content, provider, model, tokens_used) "
|
||||||
"VALUES (?, ?, ?, ?, ?)";
|
"VALUES (?, ?, ?, ?, ?)";
|
||||||
rc = sqlite3_prepare_v2(m_db, sqlConv, -1, &m_stmtSaveConversation, nullptr);
|
rc = sqlite3_prepare_v2(m_db, sqlConv, -1, &m_stmtSaveConversation, nullptr);
|
||||||
if (rc != SQLITE_OK) {
|
if (rc != SQLITE_OK) {
|
||||||
m_logger->error("Failed to prepare save_conversation: {}", sqlite3_errmsg(m_db));
|
m_logger->error("Failed to prepare save_conversation: {}", sqlite3_errmsg(m_db));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update metrics statement
|
// Update metrics statement
|
||||||
const char* sqlMetrics = "INSERT INTO daily_metrics "
|
const char* sqlMetrics = "INSERT INTO daily_metrics "
|
||||||
"(date, total_focus_minutes, total_breaks, hyperfocus_count) "
|
"(date, total_focus_minutes, total_breaks, hyperfocus_count) "
|
||||||
"VALUES (?, ?, ?, ?) "
|
"VALUES (?, ?, ?, ?) "
|
||||||
"ON CONFLICT(date) DO UPDATE SET "
|
"ON CONFLICT(date) DO UPDATE SET "
|
||||||
"total_focus_minutes = total_focus_minutes + excluded.total_focus_minutes, "
|
"total_focus_minutes = total_focus_minutes + excluded.total_focus_minutes, "
|
||||||
"total_breaks = total_breaks + excluded.total_breaks, "
|
"total_breaks = total_breaks + excluded.total_breaks, "
|
||||||
"hyperfocus_count = hyperfocus_count + excluded.hyperfocus_count, "
|
"hyperfocus_count = hyperfocus_count + excluded.hyperfocus_count, "
|
||||||
"updated_at = strftime('%s', 'now')";
|
"updated_at = strftime('%s', 'now')";
|
||||||
rc = sqlite3_prepare_v2(m_db, sqlMetrics, -1, &m_stmtUpdateMetrics, nullptr);
|
rc = sqlite3_prepare_v2(m_db, sqlMetrics, -1, &m_stmtUpdateMetrics, nullptr);
|
||||||
if (rc != SQLITE_OK) {
|
if (rc != SQLITE_OK) {
|
||||||
m_logger->error("Failed to prepare update_metrics: {}", sqlite3_errmsg(m_db));
|
m_logger->error("Failed to prepare update_metrics: {}", sqlite3_errmsg(m_db));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_logger->debug("Prepared statements created");
|
m_logger->debug("Prepared statements created");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void StorageService::finalizeStatements() {
|
void StorageService::finalizeStatements() {
|
||||||
if (m_stmtSaveSession) { sqlite3_finalize(m_stmtSaveSession); m_stmtSaveSession = nullptr; }
|
if (m_stmtSaveSession) { sqlite3_finalize(m_stmtSaveSession); m_stmtSaveSession = nullptr; }
|
||||||
if (m_stmtSaveAppUsage) { sqlite3_finalize(m_stmtSaveAppUsage); m_stmtSaveAppUsage = nullptr; }
|
if (m_stmtSaveAppUsage) { sqlite3_finalize(m_stmtSaveAppUsage); m_stmtSaveAppUsage = nullptr; }
|
||||||
if (m_stmtSaveConversation) { sqlite3_finalize(m_stmtSaveConversation); m_stmtSaveConversation = nullptr; }
|
if (m_stmtSaveConversation) { sqlite3_finalize(m_stmtSaveConversation); m_stmtSaveConversation = nullptr; }
|
||||||
if (m_stmtUpdateMetrics) { sqlite3_finalize(m_stmtUpdateMetrics); m_stmtUpdateMetrics = nullptr; }
|
if (m_stmtUpdateMetrics) { sqlite3_finalize(m_stmtUpdateMetrics); m_stmtUpdateMetrics = nullptr; }
|
||||||
}
|
}
|
||||||
|
|
||||||
void StorageService::process() {
|
void StorageService::process() {
|
||||||
processMessages();
|
processMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
void StorageService::processMessages() {
|
void StorageService::processMessages() {
|
||||||
if (!m_io || !m_isConnected) return;
|
if (!m_io || !m_isConnected) return;
|
||||||
|
|
||||||
while (m_io->hasMessages() > 0) {
|
while (m_io->hasMessages() > 0) {
|
||||||
auto msg = m_io->pullMessage();
|
auto msg = m_io->pullMessage();
|
||||||
|
|
||||||
if (msg.topic == "storage:save_session" && msg.data) {
|
if (msg.topic == "storage:save_session" && msg.data) {
|
||||||
handleSaveSession(*msg.data);
|
handleSaveSession(*msg.data);
|
||||||
}
|
}
|
||||||
else if (msg.topic == "storage:save_app_usage" && msg.data) {
|
else if (msg.topic == "storage:save_app_usage" && msg.data) {
|
||||||
handleSaveAppUsage(*msg.data);
|
handleSaveAppUsage(*msg.data);
|
||||||
}
|
}
|
||||||
else if (msg.topic == "storage:save_conversation" && msg.data) {
|
else if (msg.topic == "storage:save_conversation" && msg.data) {
|
||||||
handleSaveConversation(*msg.data);
|
handleSaveConversation(*msg.data);
|
||||||
}
|
}
|
||||||
else if (msg.topic == "storage:update_metrics" && msg.data) {
|
else if (msg.topic == "storage:update_metrics" && msg.data) {
|
||||||
handleUpdateMetrics(*msg.data);
|
handleUpdateMetrics(*msg.data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void StorageService::handleSaveSession(const grove::IDataNode& data) {
|
void StorageService::handleSaveSession(const grove::IDataNode& data) {
|
||||||
std::string taskName = data.getString("taskName", "unknown");
|
std::string taskName = data.getString("taskName", "unknown");
|
||||||
int durationMinutes = data.getInt("durationMinutes", 0);
|
int durationMinutes = data.getInt("durationMinutes", 0);
|
||||||
bool hyperfocus = data.getBool("hyperfocus", false);
|
bool hyperfocus = data.getBool("hyperfocus", false);
|
||||||
|
|
||||||
std::time_t now = std::time(nullptr);
|
std::time_t now = std::time(nullptr);
|
||||||
std::time_t startTime = now - (durationMinutes * 60);
|
std::time_t startTime = now - (durationMinutes * 60);
|
||||||
|
|
||||||
sqlite3_reset(m_stmtSaveSession);
|
sqlite3_reset(m_stmtSaveSession);
|
||||||
sqlite3_bind_text(m_stmtSaveSession, 1, taskName.c_str(), -1, SQLITE_TRANSIENT);
|
sqlite3_bind_text(m_stmtSaveSession, 1, taskName.c_str(), -1, SQLITE_TRANSIENT);
|
||||||
sqlite3_bind_int64(m_stmtSaveSession, 2, startTime);
|
sqlite3_bind_int64(m_stmtSaveSession, 2, startTime);
|
||||||
sqlite3_bind_int64(m_stmtSaveSession, 3, now);
|
sqlite3_bind_int64(m_stmtSaveSession, 3, now);
|
||||||
sqlite3_bind_int(m_stmtSaveSession, 4, durationMinutes);
|
sqlite3_bind_int(m_stmtSaveSession, 4, durationMinutes);
|
||||||
sqlite3_bind_int(m_stmtSaveSession, 5, hyperfocus ? 1 : 0);
|
sqlite3_bind_int(m_stmtSaveSession, 5, hyperfocus ? 1 : 0);
|
||||||
|
|
||||||
int rc = sqlite3_step(m_stmtSaveSession);
|
int rc = sqlite3_step(m_stmtSaveSession);
|
||||||
if (rc == SQLITE_DONE) {
|
if (rc == SQLITE_DONE) {
|
||||||
m_lastSessionId = static_cast<int>(sqlite3_last_insert_rowid(m_db));
|
m_lastSessionId = static_cast<int>(sqlite3_last_insert_rowid(m_db));
|
||||||
m_totalQueries++;
|
m_totalQueries++;
|
||||||
m_logger->debug("Session saved: {} ({}min), id={}", taskName, durationMinutes, m_lastSessionId);
|
m_logger->debug("Session saved: {} ({}min), id={}", taskName, durationMinutes, m_lastSessionId);
|
||||||
|
|
||||||
if (m_io) {
|
if (m_io) {
|
||||||
auto event = std::make_unique<grove::JsonDataNode>("saved");
|
auto event = std::make_unique<grove::JsonDataNode>("saved");
|
||||||
event->setInt("sessionId", m_lastSessionId);
|
event->setInt("sessionId", m_lastSessionId);
|
||||||
m_io->publish("storage:session_saved", std::move(event));
|
m_io->publish("storage:session_saved", std::move(event));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
publishError(sqlite3_errmsg(m_db));
|
publishError(sqlite3_errmsg(m_db));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void StorageService::handleSaveAppUsage(const grove::IDataNode& data) {
|
void StorageService::handleSaveAppUsage(const grove::IDataNode& data) {
|
||||||
int sessionId = data.getInt("sessionId", m_lastSessionId);
|
int sessionId = data.getInt("sessionId", m_lastSessionId);
|
||||||
std::string appName = data.getString("appName", "");
|
std::string appName = data.getString("appName", "");
|
||||||
int durationSeconds = data.getInt("durationSeconds", 0);
|
int durationSeconds = data.getInt("durationSeconds", 0);
|
||||||
bool productive = data.getBool("productive", false);
|
bool productive = data.getBool("productive", false);
|
||||||
|
|
||||||
sqlite3_reset(m_stmtSaveAppUsage);
|
sqlite3_reset(m_stmtSaveAppUsage);
|
||||||
sqlite3_bind_int(m_stmtSaveAppUsage, 1, sessionId);
|
sqlite3_bind_int(m_stmtSaveAppUsage, 1, sessionId);
|
||||||
sqlite3_bind_text(m_stmtSaveAppUsage, 2, appName.c_str(), -1, SQLITE_TRANSIENT);
|
sqlite3_bind_text(m_stmtSaveAppUsage, 2, appName.c_str(), -1, SQLITE_TRANSIENT);
|
||||||
sqlite3_bind_int(m_stmtSaveAppUsage, 3, durationSeconds);
|
sqlite3_bind_int(m_stmtSaveAppUsage, 3, durationSeconds);
|
||||||
sqlite3_bind_int(m_stmtSaveAppUsage, 4, productive ? 1 : 0);
|
sqlite3_bind_int(m_stmtSaveAppUsage, 4, productive ? 1 : 0);
|
||||||
|
|
||||||
int rc = sqlite3_step(m_stmtSaveAppUsage);
|
int rc = sqlite3_step(m_stmtSaveAppUsage);
|
||||||
if (rc == SQLITE_DONE) {
|
if (rc == SQLITE_DONE) {
|
||||||
m_totalQueries++;
|
m_totalQueries++;
|
||||||
} else {
|
} else {
|
||||||
publishError(sqlite3_errmsg(m_db));
|
publishError(sqlite3_errmsg(m_db));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void StorageService::handleSaveConversation(const grove::IDataNode& data) {
|
void StorageService::handleSaveConversation(const grove::IDataNode& data) {
|
||||||
std::string role = data.getString("role", "");
|
std::string role = data.getString("role", "");
|
||||||
std::string content = data.getString("content", "");
|
std::string content = data.getString("content", "");
|
||||||
std::string provider = data.getString("provider", "");
|
std::string provider = data.getString("provider", "");
|
||||||
std::string model = data.getString("model", "");
|
std::string model = data.getString("model", "");
|
||||||
int tokens = data.getInt("tokens", 0);
|
int tokens = data.getInt("tokens", 0);
|
||||||
|
|
||||||
sqlite3_reset(m_stmtSaveConversation);
|
sqlite3_reset(m_stmtSaveConversation);
|
||||||
sqlite3_bind_text(m_stmtSaveConversation, 1, role.c_str(), -1, SQLITE_TRANSIENT);
|
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, 2, content.c_str(), -1, SQLITE_TRANSIENT);
|
||||||
sqlite3_bind_text(m_stmtSaveConversation, 3, provider.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_text(m_stmtSaveConversation, 4, model.c_str(), -1, SQLITE_TRANSIENT);
|
||||||
sqlite3_bind_int(m_stmtSaveConversation, 5, tokens);
|
sqlite3_bind_int(m_stmtSaveConversation, 5, tokens);
|
||||||
|
|
||||||
int rc = sqlite3_step(m_stmtSaveConversation);
|
int rc = sqlite3_step(m_stmtSaveConversation);
|
||||||
if (rc == SQLITE_DONE) {
|
if (rc == SQLITE_DONE) {
|
||||||
m_totalQueries++;
|
m_totalQueries++;
|
||||||
} else {
|
} else {
|
||||||
publishError(sqlite3_errmsg(m_db));
|
publishError(sqlite3_errmsg(m_db));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void StorageService::handleUpdateMetrics(const grove::IDataNode& data) {
|
void StorageService::handleUpdateMetrics(const grove::IDataNode& data) {
|
||||||
int focusMinutes = data.getInt("focusMinutes", 0);
|
int focusMinutes = data.getInt("focusMinutes", 0);
|
||||||
int breaks = data.getInt("breaks", 0);
|
int breaks = data.getInt("breaks", 0);
|
||||||
int hyperfocusCount = data.getInt("hyperfocusCount", 0);
|
int hyperfocusCount = data.getInt("hyperfocusCount", 0);
|
||||||
|
|
||||||
std::time_t now = std::time(nullptr);
|
std::time_t now = std::time(nullptr);
|
||||||
std::tm* tm = std::localtime(&now);
|
std::tm* tm = std::localtime(&now);
|
||||||
char dateStr[11];
|
char dateStr[11];
|
||||||
std::strftime(dateStr, sizeof(dateStr), "%Y-%m-%d", tm);
|
std::strftime(dateStr, sizeof(dateStr), "%Y-%m-%d", tm);
|
||||||
|
|
||||||
sqlite3_reset(m_stmtUpdateMetrics);
|
sqlite3_reset(m_stmtUpdateMetrics);
|
||||||
sqlite3_bind_text(m_stmtUpdateMetrics, 1, dateStr, -1, SQLITE_TRANSIENT);
|
sqlite3_bind_text(m_stmtUpdateMetrics, 1, dateStr, -1, SQLITE_TRANSIENT);
|
||||||
sqlite3_bind_int(m_stmtUpdateMetrics, 2, focusMinutes);
|
sqlite3_bind_int(m_stmtUpdateMetrics, 2, focusMinutes);
|
||||||
sqlite3_bind_int(m_stmtUpdateMetrics, 3, breaks);
|
sqlite3_bind_int(m_stmtUpdateMetrics, 3, breaks);
|
||||||
sqlite3_bind_int(m_stmtUpdateMetrics, 4, hyperfocusCount);
|
sqlite3_bind_int(m_stmtUpdateMetrics, 4, hyperfocusCount);
|
||||||
|
|
||||||
int rc = sqlite3_step(m_stmtUpdateMetrics);
|
int rc = sqlite3_step(m_stmtUpdateMetrics);
|
||||||
if (rc == SQLITE_DONE) {
|
if (rc == SQLITE_DONE) {
|
||||||
m_totalQueries++;
|
m_totalQueries++;
|
||||||
} else {
|
} else {
|
||||||
publishError(sqlite3_errmsg(m_db));
|
publishError(sqlite3_errmsg(m_db));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool StorageService::executeSQL(const std::string& sql) {
|
bool StorageService::executeSQL(const std::string& sql) {
|
||||||
char* errMsg = nullptr;
|
char* errMsg = nullptr;
|
||||||
int rc = sqlite3_exec(m_db, sql.c_str(), nullptr, nullptr, &errMsg);
|
int rc = sqlite3_exec(m_db, sql.c_str(), nullptr, nullptr, &errMsg);
|
||||||
|
|
||||||
if (rc != SQLITE_OK) {
|
if (rc != SQLITE_OK) {
|
||||||
m_logger->error("SQL error: {}", errMsg ? errMsg : "unknown");
|
m_logger->error("SQL error: {}", errMsg ? errMsg : "unknown");
|
||||||
sqlite3_free(errMsg);
|
sqlite3_free(errMsg);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_totalQueries++;
|
m_totalQueries++;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void StorageService::publishError(const std::string& message) {
|
void StorageService::publishError(const std::string& message) {
|
||||||
m_logger->error("Storage error: {}", message);
|
m_logger->error("Storage error: {}", message);
|
||||||
if (m_io) {
|
if (m_io) {
|
||||||
auto event = std::make_unique<grove::JsonDataNode>("error");
|
auto event = std::make_unique<grove::JsonDataNode>("error");
|
||||||
event->setString("message", message);
|
event->setString("message", message);
|
||||||
m_io->publish("storage:error", std::move(event));
|
m_io->publish("storage:error", std::move(event));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void StorageService::shutdown() {
|
void StorageService::shutdown() {
|
||||||
finalizeStatements();
|
finalizeStatements();
|
||||||
|
|
||||||
if (m_db) {
|
if (m_db) {
|
||||||
sqlite3_close(m_db);
|
sqlite3_close(m_db);
|
||||||
m_db = nullptr;
|
m_db = nullptr;
|
||||||
m_isConnected = false;
|
m_isConnected = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_logger->info("StorageService shutdown. Total queries: {}", m_totalQueries);
|
m_logger->info("StorageService shutdown. Total queries: {}", m_totalQueries);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace aissia
|
} // namespace aissia
|
||||||
|
|||||||
@ -1,91 +1,91 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "IService.hpp"
|
#include "IService.hpp"
|
||||||
|
|
||||||
#include <grove/IIO.h>
|
#include <grove/IIO.h>
|
||||||
#include <grove/JsonDataNode.h>
|
#include <grove/JsonDataNode.h>
|
||||||
#include <spdlog/spdlog.h>
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <queue>
|
#include <queue>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
|
||||||
struct sqlite3;
|
struct sqlite3;
|
||||||
struct sqlite3_stmt;
|
struct sqlite3_stmt;
|
||||||
|
|
||||||
namespace aissia {
|
namespace aissia {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Storage Service - SQLite persistence
|
* @brief Storage Service - SQLite persistence
|
||||||
*
|
*
|
||||||
* Handles all database operations synchronously in main thread.
|
* Handles all database operations synchronously in main thread.
|
||||||
* Uses prepared statements to prevent SQL injection.
|
* Uses prepared statements to prevent SQL injection.
|
||||||
*
|
*
|
||||||
* Subscribes to:
|
* Subscribes to:
|
||||||
* - "storage:save_session" : { taskName, durationMinutes, hyperfocus }
|
* - "storage:save_session" : { taskName, durationMinutes, hyperfocus }
|
||||||
* - "storage:save_app_usage" : { sessionId, appName, durationSeconds, productive }
|
* - "storage:save_app_usage" : { sessionId, appName, durationSeconds, productive }
|
||||||
* - "storage:save_conversation" : { role, content, provider, model, tokens }
|
* - "storage:save_conversation" : { role, content, provider, model, tokens }
|
||||||
* - "storage:update_metrics" : { focusMinutes, breaks, hyperfocusCount }
|
* - "storage:update_metrics" : { focusMinutes, breaks, hyperfocusCount }
|
||||||
* - "storage:query" : { sql, params[] }
|
* - "storage:query" : { sql, params[] }
|
||||||
*
|
*
|
||||||
* Publishes:
|
* Publishes:
|
||||||
* - "storage:ready" : Database initialized
|
* - "storage:ready" : Database initialized
|
||||||
* - "storage:session_saved": { sessionId }
|
* - "storage:session_saved": { sessionId }
|
||||||
* - "storage:error" : { message }
|
* - "storage:error" : { message }
|
||||||
*/
|
*/
|
||||||
class StorageService : public IService {
|
class StorageService : public IService {
|
||||||
public:
|
public:
|
||||||
StorageService();
|
StorageService();
|
||||||
~StorageService() override;
|
~StorageService() override;
|
||||||
|
|
||||||
bool initialize(grove::IIO* io) override;
|
bool initialize(grove::IIO* io) override;
|
||||||
void process() override;
|
void process() override;
|
||||||
void shutdown() override;
|
void shutdown() override;
|
||||||
std::string getName() const override { return "StorageService"; }
|
std::string getName() const override { return "StorageService"; }
|
||||||
bool isHealthy() const override { return m_isConnected; }
|
bool isHealthy() const override { return m_isConnected; }
|
||||||
|
|
||||||
/// Open database with config
|
/// Open database with config
|
||||||
bool openDatabase(const std::string& dbPath,
|
bool openDatabase(const std::string& dbPath,
|
||||||
const std::string& journalMode = "WAL",
|
const std::string& journalMode = "WAL",
|
||||||
int busyTimeoutMs = 5000);
|
int busyTimeoutMs = 5000);
|
||||||
|
|
||||||
/// Get last inserted session ID
|
/// Get last inserted session ID
|
||||||
int getLastSessionId() const { return m_lastSessionId; }
|
int getLastSessionId() const { return m_lastSessionId; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Database
|
// Database
|
||||||
sqlite3* m_db = nullptr;
|
sqlite3* m_db = nullptr;
|
||||||
std::string m_dbPath;
|
std::string m_dbPath;
|
||||||
bool m_isConnected = false;
|
bool m_isConnected = false;
|
||||||
int m_lastSessionId = 0;
|
int m_lastSessionId = 0;
|
||||||
int m_totalQueries = 0;
|
int m_totalQueries = 0;
|
||||||
|
|
||||||
// Prepared statements
|
// Prepared statements
|
||||||
sqlite3_stmt* m_stmtSaveSession = nullptr;
|
sqlite3_stmt* m_stmtSaveSession = nullptr;
|
||||||
sqlite3_stmt* m_stmtSaveAppUsage = nullptr;
|
sqlite3_stmt* m_stmtSaveAppUsage = nullptr;
|
||||||
sqlite3_stmt* m_stmtSaveConversation = nullptr;
|
sqlite3_stmt* m_stmtSaveConversation = nullptr;
|
||||||
sqlite3_stmt* m_stmtUpdateMetrics = nullptr;
|
sqlite3_stmt* m_stmtUpdateMetrics = nullptr;
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
grove::IIO* m_io = nullptr;
|
grove::IIO* m_io = nullptr;
|
||||||
std::shared_ptr<spdlog::logger> m_logger;
|
std::shared_ptr<spdlog::logger> m_logger;
|
||||||
|
|
||||||
// Database operations
|
// Database operations
|
||||||
bool initializeSchema();
|
bool initializeSchema();
|
||||||
bool prepareStatements();
|
bool prepareStatements();
|
||||||
void finalizeStatements();
|
void finalizeStatements();
|
||||||
|
|
||||||
// Message handlers
|
// Message handlers
|
||||||
void processMessages();
|
void processMessages();
|
||||||
void handleSaveSession(const grove::IDataNode& data);
|
void handleSaveSession(const grove::IDataNode& data);
|
||||||
void handleSaveAppUsage(const grove::IDataNode& data);
|
void handleSaveAppUsage(const grove::IDataNode& data);
|
||||||
void handleSaveConversation(const grove::IDataNode& data);
|
void handleSaveConversation(const grove::IDataNode& data);
|
||||||
void handleUpdateMetrics(const grove::IDataNode& data);
|
void handleUpdateMetrics(const grove::IDataNode& data);
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
bool executeSQL(const std::string& sql);
|
bool executeSQL(const std::string& sql);
|
||||||
void publishError(const std::string& message);
|
void publishError(const std::string& message);
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace aissia
|
} // namespace aissia
|
||||||
|
|||||||
@ -1,294 +1,294 @@
|
|||||||
// CRITICAL ORDER: Include system headers before local headers to avoid macro conflicts
|
// CRITICAL ORDER: Include system headers before local headers to avoid macro conflicts
|
||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <queue>
|
#include <queue>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
|
||||||
// Include VoiceService.hpp BEFORE spdlog to avoid logger macro conflicts
|
// Include VoiceService.hpp BEFORE spdlog to avoid logger macro conflicts
|
||||||
#include "VoiceService.hpp"
|
#include "VoiceService.hpp"
|
||||||
#include "STTService.hpp"
|
#include "STTService.hpp"
|
||||||
|
|
||||||
// Include spdlog after VoiceService.hpp
|
// Include spdlog after VoiceService.hpp
|
||||||
#include <spdlog/sinks/stdout_color_sinks.h>
|
#include <spdlog/sinks/stdout_color_sinks.h>
|
||||||
|
|
||||||
namespace aissia {
|
namespace aissia {
|
||||||
|
|
||||||
VoiceService::VoiceService() {
|
VoiceService::VoiceService() {
|
||||||
m_logger = spdlog::get("VoiceService");
|
m_logger = spdlog::get("VoiceService");
|
||||||
if (!m_logger) {
|
if (!m_logger) {
|
||||||
m_logger = spdlog::stdout_color_mt("VoiceService");
|
m_logger = spdlog::stdout_color_mt("VoiceService");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool VoiceService::initialize(grove::IIO* io) {
|
bool VoiceService::initialize(grove::IIO* io) {
|
||||||
m_io = io;
|
m_io = io;
|
||||||
|
|
||||||
// Create TTS engine
|
// Create TTS engine
|
||||||
m_ttsEngine = TTSEngineFactory::create();
|
m_ttsEngine = TTSEngineFactory::create();
|
||||||
if (m_ttsEngine && m_ttsEngine->isAvailable()) {
|
if (m_ttsEngine && m_ttsEngine->isAvailable()) {
|
||||||
m_ttsEngine->setRate(m_ttsRate);
|
m_ttsEngine->setRate(m_ttsRate);
|
||||||
m_ttsEngine->setVolume(m_ttsVolume);
|
m_ttsEngine->setVolume(m_ttsVolume);
|
||||||
m_logger->info("TTS engine initialized");
|
m_logger->info("TTS engine initialized");
|
||||||
} else {
|
} else {
|
||||||
m_logger->warn("TTS engine not available");
|
m_logger->warn("TTS engine not available");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_io) {
|
if (m_io) {
|
||||||
grove::SubscriptionConfig config;
|
grove::SubscriptionConfig config;
|
||||||
m_io->subscribe("voice:speak", config);
|
m_io->subscribe("voice:speak", config);
|
||||||
m_io->subscribe("voice:stop", config);
|
m_io->subscribe("voice:stop", config);
|
||||||
m_io->subscribe("voice:listen", config);
|
m_io->subscribe("voice:listen", config);
|
||||||
}
|
}
|
||||||
|
|
||||||
m_logger->info("VoiceService initialized");
|
m_logger->info("VoiceService initialized");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void VoiceService::configureTTS(bool enabled, int rate, int volume) {
|
void VoiceService::configureTTS(bool enabled, int rate, int volume) {
|
||||||
m_ttsEnabled = enabled;
|
m_ttsEnabled = enabled;
|
||||||
m_ttsRate = rate;
|
m_ttsRate = rate;
|
||||||
m_ttsVolume = volume;
|
m_ttsVolume = volume;
|
||||||
|
|
||||||
if (m_ttsEngine) {
|
if (m_ttsEngine) {
|
||||||
m_ttsEngine->setRate(rate);
|
m_ttsEngine->setRate(rate);
|
||||||
m_ttsEngine->setVolume(volume);
|
m_ttsEngine->setVolume(volume);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void VoiceService::configureSTT(bool enabled, const std::string& language,
|
void VoiceService::configureSTT(bool enabled, const std::string& language,
|
||||||
const std::string& apiKey) {
|
const std::string& apiKey) {
|
||||||
m_sttEnabled = enabled;
|
m_sttEnabled = enabled;
|
||||||
m_language = language;
|
m_language = language;
|
||||||
|
|
||||||
if (!apiKey.empty()) {
|
if (!apiKey.empty()) {
|
||||||
m_sttEngine = STTEngineFactory::create(apiKey);
|
m_sttEngine = STTEngineFactory::create(apiKey);
|
||||||
if (m_sttEngine) {
|
if (m_sttEngine) {
|
||||||
m_sttEngine->setLanguage(language);
|
m_sttEngine->setLanguage(language);
|
||||||
m_logger->info("STT engine configured");
|
m_logger->info("STT engine configured");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void VoiceService::process() {
|
void VoiceService::process() {
|
||||||
processMessages();
|
processMessages();
|
||||||
processSpeakQueue();
|
processSpeakQueue();
|
||||||
}
|
}
|
||||||
|
|
||||||
void VoiceService::processMessages() {
|
void VoiceService::processMessages() {
|
||||||
if (!m_io) return;
|
if (!m_io) return;
|
||||||
|
|
||||||
while (m_io->hasMessages() > 0) {
|
while (m_io->hasMessages() > 0) {
|
||||||
auto msg = m_io->pullMessage();
|
auto msg = m_io->pullMessage();
|
||||||
|
|
||||||
if (msg.topic == "voice:speak" && msg.data) {
|
if (msg.topic == "voice:speak" && msg.data) {
|
||||||
handleSpeakRequest(*msg.data);
|
handleSpeakRequest(*msg.data);
|
||||||
}
|
}
|
||||||
else if (msg.topic == "voice:stop") {
|
else if (msg.topic == "voice:stop") {
|
||||||
if (m_ttsEngine) {
|
if (m_ttsEngine) {
|
||||||
m_ttsEngine->stop();
|
m_ttsEngine->stop();
|
||||||
}
|
}
|
||||||
// Clear queue
|
// Clear queue
|
||||||
while (!m_speakQueue.empty()) m_speakQueue.pop();
|
while (!m_speakQueue.empty()) m_speakQueue.pop();
|
||||||
}
|
}
|
||||||
else if (msg.topic == "voice:listen" && m_sttEnabled && m_sttEngine) {
|
else if (msg.topic == "voice:listen" && m_sttEnabled && m_sttEngine) {
|
||||||
// STT would be handled here
|
// STT would be handled here
|
||||||
// For now just log
|
// For now just log
|
||||||
m_logger->debug("STT listen requested");
|
m_logger->debug("STT listen requested");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void VoiceService::handleSpeakRequest(const grove::IDataNode& data) {
|
void VoiceService::handleSpeakRequest(const grove::IDataNode& data) {
|
||||||
std::string text = data.getString("text", "");
|
std::string text = data.getString("text", "");
|
||||||
bool priority = data.getBool("priority", false);
|
bool priority = data.getBool("priority", false);
|
||||||
|
|
||||||
if (text.empty()) return;
|
if (text.empty()) return;
|
||||||
|
|
||||||
if (priority) {
|
if (priority) {
|
||||||
// Clear queue and stop current speech
|
// Clear queue and stop current speech
|
||||||
while (!m_speakQueue.empty()) m_speakQueue.pop();
|
while (!m_speakQueue.empty()) m_speakQueue.pop();
|
||||||
if (m_ttsEngine) m_ttsEngine->stop();
|
if (m_ttsEngine) m_ttsEngine->stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
m_speakQueue.push(text);
|
m_speakQueue.push(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
void VoiceService::processSpeakQueue() {
|
void VoiceService::processSpeakQueue() {
|
||||||
if (!m_ttsEnabled || !m_ttsEngine || m_speakQueue.empty()) return;
|
if (!m_ttsEnabled || !m_ttsEngine || m_speakQueue.empty()) return;
|
||||||
|
|
||||||
// Only speak if not currently speaking
|
// Only speak if not currently speaking
|
||||||
if (!m_ttsEngine->isSpeaking() && !m_speakQueue.empty()) {
|
if (!m_ttsEngine->isSpeaking() && !m_speakQueue.empty()) {
|
||||||
std::string text = m_speakQueue.front();
|
std::string text = m_speakQueue.front();
|
||||||
m_speakQueue.pop();
|
m_speakQueue.pop();
|
||||||
speak(text);
|
speak(text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void VoiceService::speak(const std::string& text) {
|
void VoiceService::speak(const std::string& text) {
|
||||||
if (!m_ttsEngine || !m_ttsEnabled) return;
|
if (!m_ttsEngine || !m_ttsEnabled) return;
|
||||||
|
|
||||||
// Publish speaking started
|
// Publish speaking started
|
||||||
if (m_io) {
|
if (m_io) {
|
||||||
auto event = std::unique_ptr<grove::IDataNode>(
|
auto event = std::unique_ptr<grove::IDataNode>(
|
||||||
new grove::JsonDataNode("event")
|
new grove::JsonDataNode("event")
|
||||||
);
|
);
|
||||||
event->setString("text", text.size() > 100 ? text.substr(0, 100) + "..." : text);
|
event->setString("text", text.size() > 100 ? text.substr(0, 100) + "..." : text);
|
||||||
m_io->publish("voice:speaking_started", std::move(event));
|
m_io->publish("voice:speaking_started", std::move(event));
|
||||||
}
|
}
|
||||||
|
|
||||||
m_ttsEngine->speak(text, true);
|
m_ttsEngine->speak(text, true);
|
||||||
m_totalSpoken++;
|
m_totalSpoken++;
|
||||||
|
|
||||||
m_logger->debug("Speaking");
|
m_logger->debug("Speaking");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 7: New STT configuration with full config support
|
// Phase 7: New STT configuration with full config support
|
||||||
void VoiceService::configureSTT(const nlohmann::json& sttConfig) {
|
void VoiceService::configureSTT(const nlohmann::json& sttConfig) {
|
||||||
m_logger->info("[VoiceService] Configuring STT service (Phase 7)");
|
m_logger->info("[VoiceService] Configuring STT service (Phase 7)");
|
||||||
|
|
||||||
// Extract enabled flag
|
// Extract enabled flag
|
||||||
bool enabled = false;
|
bool enabled = false;
|
||||||
if (sttConfig.contains("active_mode")) {
|
if (sttConfig.contains("active_mode")) {
|
||||||
const auto& activeMode = sttConfig["active_mode"];
|
const auto& activeMode = sttConfig["active_mode"];
|
||||||
enabled = activeMode.value("enabled", true);
|
enabled = activeMode.value("enabled", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
m_sttEnabled = enabled;
|
m_sttEnabled = enabled;
|
||||||
|
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
m_logger->info("[VoiceService] STT disabled in config");
|
m_logger->info("[VoiceService] STT disabled in config");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create and start STT service
|
// Create and start STT service
|
||||||
m_sttService = std::make_unique<STTService>(sttConfig);
|
m_sttService = std::make_unique<STTService>(sttConfig);
|
||||||
|
|
||||||
if (!m_sttService->start()) {
|
if (!m_sttService->start()) {
|
||||||
m_logger->error("[VoiceService] Failed to start STT service");
|
m_logger->error("[VoiceService] Failed to start STT service");
|
||||||
m_sttService.reset();
|
m_sttService.reset();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_logger->info("[VoiceService] STT service started");
|
m_logger->info("[VoiceService] STT service started");
|
||||||
|
|
||||||
// Setup callbacks for transcription events
|
// Setup callbacks for transcription events
|
||||||
// Note: For MVP Milestone 1, we don't start streaming yet
|
// Note: For MVP Milestone 1, we don't start streaming yet
|
||||||
// This will be implemented in Milestone 2 (passive mode)
|
// This will be implemented in Milestone 2 (passive mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
// STT event handlers (Phase 7)
|
// STT event handlers (Phase 7)
|
||||||
void VoiceService::handleKeyword(const std::string& keyword) {
|
void VoiceService::handleKeyword(const std::string& keyword) {
|
||||||
m_logger->info("[VoiceService] Keyword detected");
|
m_logger->info("[VoiceService] Keyword detected");
|
||||||
|
|
||||||
// Publish keyword detection event
|
// Publish keyword detection event
|
||||||
if (m_io) {
|
if (m_io) {
|
||||||
auto event = std::unique_ptr<grove::IDataNode>(
|
auto event = std::unique_ptr<grove::IDataNode>(
|
||||||
new grove::JsonDataNode("event")
|
new grove::JsonDataNode("event")
|
||||||
);
|
);
|
||||||
event->setString("keyword", keyword);
|
event->setString("keyword", keyword);
|
||||||
event->setInt("timestamp", static_cast<int>(std::time(nullptr)));
|
event->setInt("timestamp", static_cast<int>(std::time(nullptr)));
|
||||||
m_io->publish("voice:keyword_detected", std::move(event));
|
m_io->publish("voice:keyword_detected", std::move(event));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-switch to active mode (Phase 7.2)
|
// Auto-switch to active mode (Phase 7.2)
|
||||||
if (m_sttService) {
|
if (m_sttService) {
|
||||||
m_sttService->setMode(STTMode::ACTIVE);
|
m_sttService->setMode(STTMode::ACTIVE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void VoiceService::handleTranscription(const std::string& text, STTMode mode) {
|
void VoiceService::handleTranscription(const std::string& text, STTMode mode) {
|
||||||
m_logger->info("[VoiceService] Transcription received");
|
m_logger->info("[VoiceService] Transcription received");
|
||||||
|
|
||||||
// Publish transcription event
|
// Publish transcription event
|
||||||
if (m_io) {
|
if (m_io) {
|
||||||
std::string modeStr = (mode == STTMode::PASSIVE ? "passive" : "active");
|
std::string modeStr = (mode == STTMode::PASSIVE ? "passive" : "active");
|
||||||
auto event = std::unique_ptr<grove::IDataNode>(
|
auto event = std::unique_ptr<grove::IDataNode>(
|
||||||
new grove::JsonDataNode("event")
|
new grove::JsonDataNode("event")
|
||||||
);
|
);
|
||||||
event->setString("text", text);
|
event->setString("text", text);
|
||||||
event->setString("mode", modeStr);
|
event->setString("mode", modeStr);
|
||||||
event->setInt("timestamp", static_cast<int>(std::time(nullptr)));
|
event->setInt("timestamp", static_cast<int>(std::time(nullptr)));
|
||||||
m_io->publish("voice:transcription", std::move(event));
|
m_io->publish("voice:transcription", std::move(event));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void VoiceService::shutdown() {
|
void VoiceService::shutdown() {
|
||||||
if (m_ttsEngine) {
|
if (m_ttsEngine) {
|
||||||
m_ttsEngine->stop();
|
m_ttsEngine->stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_sttService) {
|
if (m_sttService) {
|
||||||
m_sttService->stop();
|
m_sttService->stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
m_logger->info("[VoiceService] Shutdown");
|
m_logger->info("[VoiceService] Shutdown");
|
||||||
}
|
}
|
||||||
|
|
||||||
bool VoiceService::loadConfig(const std::string& configPath) {
|
bool VoiceService::loadConfig(const std::string& configPath) {
|
||||||
try {
|
try {
|
||||||
std::ifstream file(configPath);
|
std::ifstream file(configPath);
|
||||||
if (!file.is_open()) {
|
if (!file.is_open()) {
|
||||||
m_logger->warn("[VoiceService] Config file not found: {}", configPath);
|
m_logger->warn("[VoiceService] Config file not found: {}", configPath);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
nlohmann::json config;
|
nlohmann::json config;
|
||||||
file >> config;
|
file >> config;
|
||||||
|
|
||||||
// Load TTS config
|
// Load TTS config
|
||||||
if (config.contains("tts")) {
|
if (config.contains("tts")) {
|
||||||
const auto& ttsConfig = config["tts"];
|
const auto& ttsConfig = config["tts"];
|
||||||
m_ttsEnabled = ttsConfig.value("enabled", true);
|
m_ttsEnabled = ttsConfig.value("enabled", true);
|
||||||
m_ttsRate = ttsConfig.value("rate", 0);
|
m_ttsRate = ttsConfig.value("rate", 0);
|
||||||
m_ttsVolume = ttsConfig.value("volume", 80);
|
m_ttsVolume = ttsConfig.value("volume", 80);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load STT config (Phase 7 format)
|
// Load STT config (Phase 7 format)
|
||||||
if (config.contains("stt")) {
|
if (config.contains("stt")) {
|
||||||
configureSTT(config["stt"]);
|
configureSTT(config["stt"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
m_logger->info("[VoiceService] Config loaded from {}", configPath);
|
m_logger->info("[VoiceService] Config loaded from {}", configPath);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
m_logger->error("[VoiceService] Failed to load config: {}", e.what());
|
m_logger->error("[VoiceService] Failed to load config: {}", e.what());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string VoiceService::transcribeFileSync(
|
std::string VoiceService::transcribeFileSync(
|
||||||
const std::string& filePath,
|
const std::string& filePath,
|
||||||
const std::string& language
|
const std::string& language
|
||||||
) {
|
) {
|
||||||
m_logger->info("[VoiceService] transcribeFileSync: {}", filePath);
|
m_logger->info("[VoiceService] transcribeFileSync: {}", filePath);
|
||||||
|
|
||||||
if (!m_sttService) {
|
if (!m_sttService) {
|
||||||
throw std::runtime_error("STT service not initialized");
|
throw std::runtime_error("STT service not initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use STT service to transcribe file synchronously
|
// Use STT service to transcribe file synchronously
|
||||||
// Note: This requires STT service to support file transcription
|
// Note: This requires STT service to support file transcription
|
||||||
// For MVP, we'll throw not implemented
|
// For MVP, we'll throw not implemented
|
||||||
throw std::runtime_error("transcribeFileSync not yet implemented - STT service needs file transcription support");
|
throw std::runtime_error("transcribeFileSync not yet implemented - STT service needs file transcription support");
|
||||||
}
|
}
|
||||||
|
|
||||||
bool VoiceService::textToSpeechSync(
|
bool VoiceService::textToSpeechSync(
|
||||||
const std::string& text,
|
const std::string& text,
|
||||||
const std::string& outputFile,
|
const std::string& outputFile,
|
||||||
const std::string& voice
|
const std::string& voice
|
||||||
) {
|
) {
|
||||||
m_logger->info("[VoiceService] textToSpeechSync: {} -> {}", text.substr(0, 50), outputFile);
|
m_logger->info("[VoiceService] textToSpeechSync: {} -> {}", text.substr(0, 50), outputFile);
|
||||||
|
|
||||||
if (!m_ttsEngine) {
|
if (!m_ttsEngine) {
|
||||||
throw std::runtime_error("TTS engine not initialized");
|
throw std::runtime_error("TTS engine not initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
// For MVP, we don't support saving to file yet
|
// For MVP, we don't support saving to file yet
|
||||||
// The TTS engine currently only speaks directly
|
// The TTS engine currently only speaks directly
|
||||||
throw std::runtime_error("textToSpeechSync file output not yet implemented - TTS engine needs file output support");
|
throw std::runtime_error("textToSpeechSync file output not yet implemented - TTS engine needs file output support");
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace aissia
|
} // namespace aissia
|
||||||
|
|||||||
@ -1,117 +1,117 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
// Include nlohmann/json BEFORE grove headers to avoid macro conflicts
|
// Include nlohmann/json BEFORE grove headers to avoid macro conflicts
|
||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
#include "IService.hpp"
|
#include "IService.hpp"
|
||||||
#include "ISTTService.hpp"
|
#include "ISTTService.hpp"
|
||||||
#include "../shared/audio/ITTSEngine.hpp"
|
#include "../shared/audio/ITTSEngine.hpp"
|
||||||
#include "../shared/audio/ISTTEngine.hpp"
|
#include "../shared/audio/ISTTEngine.hpp"
|
||||||
|
|
||||||
#include <grove/IIO.h>
|
#include <grove/IIO.h>
|
||||||
#include <grove/JsonDataNode.h>
|
#include <grove/JsonDataNode.h>
|
||||||
#include <spdlog/spdlog.h>
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <queue>
|
#include <queue>
|
||||||
|
|
||||||
namespace aissia {
|
namespace aissia {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Voice Service - TTS and STT engines
|
* @brief Voice Service - TTS and STT engines
|
||||||
*
|
*
|
||||||
* Handles platform-specific audio engines (SAPI on Windows, espeak on Linux).
|
* Handles platform-specific audio engines (SAPI on Windows, espeak on Linux).
|
||||||
* Manages speak queue and processes TTS/STT requests.
|
* Manages speak queue and processes TTS/STT requests.
|
||||||
*
|
*
|
||||||
* Subscribes to:
|
* Subscribes to:
|
||||||
* - "voice:speak" : { text, priority? }
|
* - "voice:speak" : { text, priority? }
|
||||||
* - "voice:stop" : Stop current speech
|
* - "voice:stop" : Stop current speech
|
||||||
* - "voice:listen" : Start STT recording
|
* - "voice:listen" : Start STT recording
|
||||||
*
|
*
|
||||||
* Publishes:
|
* Publishes:
|
||||||
* - "voice:speaking_started" : { text }
|
* - "voice:speaking_started" : { text }
|
||||||
* - "voice:speaking_ended" : {}
|
* - "voice:speaking_ended" : {}
|
||||||
* - "voice:transcription" : { text, confidence }
|
* - "voice:transcription" : { text, confidence }
|
||||||
*/
|
*/
|
||||||
class VoiceService : public IService {
|
class VoiceService : public IService {
|
||||||
public:
|
public:
|
||||||
VoiceService();
|
VoiceService();
|
||||||
~VoiceService() override = default;
|
~VoiceService() override = default;
|
||||||
|
|
||||||
bool initialize(grove::IIO* io) override;
|
bool initialize(grove::IIO* io) override;
|
||||||
void process() override;
|
void process() override;
|
||||||
void shutdown() override;
|
void shutdown() override;
|
||||||
std::string getName() const override { return "VoiceService"; }
|
std::string getName() const override { return "VoiceService"; }
|
||||||
bool isHealthy() const override { return m_ttsEngine != nullptr; }
|
bool isHealthy() const override { return m_ttsEngine != nullptr; }
|
||||||
|
|
||||||
/// Configure TTS settings
|
/// Configure TTS settings
|
||||||
void configureTTS(bool enabled = true, int rate = 0, int volume = 80);
|
void configureTTS(bool enabled = true, int rate = 0, int volume = 80);
|
||||||
|
|
||||||
/// Configure STT settings (legacy API)
|
/// Configure STT settings (legacy API)
|
||||||
void configureSTT(bool enabled = true, const std::string& language = "fr",
|
void configureSTT(bool enabled = true, const std::string& language = "fr",
|
||||||
const std::string& apiKey = "");
|
const std::string& apiKey = "");
|
||||||
|
|
||||||
/// Configure STT with full config (Phase 7)
|
/// Configure STT with full config (Phase 7)
|
||||||
void configureSTT(const nlohmann::json& sttConfig);
|
void configureSTT(const nlohmann::json& sttConfig);
|
||||||
|
|
||||||
/// Load configuration from JSON file
|
/// Load configuration from JSON file
|
||||||
bool loadConfig(const std::string& configPath);
|
bool loadConfig(const std::string& configPath);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Transcribe audio file synchronously (for MCP Server mode)
|
* @brief Transcribe audio file synchronously (for MCP Server mode)
|
||||||
*
|
*
|
||||||
* @param filePath Path to audio file
|
* @param filePath Path to audio file
|
||||||
* @param language Language code (e.g., "fr", "en")
|
* @param language Language code (e.g., "fr", "en")
|
||||||
* @return Transcribed text
|
* @return Transcribed text
|
||||||
*/
|
*/
|
||||||
std::string transcribeFileSync(
|
std::string transcribeFileSync(
|
||||||
const std::string& filePath,
|
const std::string& filePath,
|
||||||
const std::string& language = "fr"
|
const std::string& language = "fr"
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Convert text to speech synchronously (for MCP Server mode)
|
* @brief Convert text to speech synchronously (for MCP Server mode)
|
||||||
*
|
*
|
||||||
* @param text Text to synthesize
|
* @param text Text to synthesize
|
||||||
* @param outputFile Output audio file path
|
* @param outputFile Output audio file path
|
||||||
* @param voice Voice identifier (e.g., "fr-fr")
|
* @param voice Voice identifier (e.g., "fr-fr")
|
||||||
* @return true if successful
|
* @return true if successful
|
||||||
*/
|
*/
|
||||||
bool textToSpeechSync(
|
bool textToSpeechSync(
|
||||||
const std::string& text,
|
const std::string& text,
|
||||||
const std::string& outputFile,
|
const std::string& outputFile,
|
||||||
const std::string& voice = "fr-fr"
|
const std::string& voice = "fr-fr"
|
||||||
);
|
);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Configuration
|
// Configuration
|
||||||
bool m_ttsEnabled = true;
|
bool m_ttsEnabled = true;
|
||||||
bool m_sttEnabled = true;
|
bool m_sttEnabled = true;
|
||||||
int m_ttsRate = 0;
|
int m_ttsRate = 0;
|
||||||
int m_ttsVolume = 80;
|
int m_ttsVolume = 80;
|
||||||
std::string m_language = "fr";
|
std::string m_language = "fr";
|
||||||
|
|
||||||
// State
|
// State
|
||||||
std::unique_ptr<ITTSEngine> m_ttsEngine;
|
std::unique_ptr<ITTSEngine> m_ttsEngine;
|
||||||
std::unique_ptr<ISTTEngine> m_sttEngine; // Legacy direct engine (deprecated)
|
std::unique_ptr<ISTTEngine> m_sttEngine; // Legacy direct engine (deprecated)
|
||||||
std::unique_ptr<ISTTService> m_sttService; // Phase 7: New STT service layer
|
std::unique_ptr<ISTTService> m_sttService; // Phase 7: New STT service layer
|
||||||
std::queue<std::string> m_speakQueue;
|
std::queue<std::string> m_speakQueue;
|
||||||
int m_totalSpoken = 0;
|
int m_totalSpoken = 0;
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
grove::IIO* m_io = nullptr;
|
grove::IIO* m_io = nullptr;
|
||||||
std::shared_ptr<spdlog::logger> m_logger;
|
std::shared_ptr<spdlog::logger> m_logger;
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
void processMessages();
|
void processMessages();
|
||||||
void processSpeakQueue();
|
void processSpeakQueue();
|
||||||
void speak(const std::string& text);
|
void speak(const std::string& text);
|
||||||
void handleSpeakRequest(const grove::IDataNode& data);
|
void handleSpeakRequest(const grove::IDataNode& data);
|
||||||
|
|
||||||
// STT handlers (Phase 7)
|
// STT handlers (Phase 7)
|
||||||
void handleTranscription(const std::string& text, STTMode mode);
|
void handleTranscription(const std::string& text, STTMode mode);
|
||||||
void handleKeyword(const std::string& keyword);
|
void handleKeyword(const std::string& keyword);
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace aissia
|
} // namespace aissia
|
||||||
|
|||||||
@ -1,310 +1,310 @@
|
|||||||
#include "MCPServerTools.hpp"
|
#include "MCPServerTools.hpp"
|
||||||
#include "../../services/LLMService.hpp"
|
#include "../../services/LLMService.hpp"
|
||||||
#include "../../services/StorageService.hpp"
|
#include "../../services/StorageService.hpp"
|
||||||
#include "../../services/VoiceService.hpp"
|
#include "../../services/VoiceService.hpp"
|
||||||
|
|
||||||
#include <spdlog/spdlog.h>
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
namespace aissia::tools {
|
namespace aissia::tools {
|
||||||
|
|
||||||
MCPServerTools::MCPServerTools(
|
MCPServerTools::MCPServerTools(
|
||||||
LLMService* llm,
|
LLMService* llm,
|
||||||
StorageService* storage,
|
StorageService* storage,
|
||||||
VoiceService* voice
|
VoiceService* voice
|
||||||
) : m_llmService(llm),
|
) : m_llmService(llm),
|
||||||
m_storageService(storage),
|
m_storageService(storage),
|
||||||
m_voiceService(voice)
|
m_voiceService(voice)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<ToolDefinition> MCPServerTools::getToolDefinitions() {
|
std::vector<ToolDefinition> MCPServerTools::getToolDefinitions() {
|
||||||
std::vector<ToolDefinition> tools;
|
std::vector<ToolDefinition> tools;
|
||||||
|
|
||||||
// Tool 1: chat_with_aissia (PRIORITÉ)
|
// Tool 1: chat_with_aissia (PRIORITÉ)
|
||||||
if (m_llmService) {
|
if (m_llmService) {
|
||||||
tools.push_back({
|
tools.push_back({
|
||||||
"chat_with_aissia",
|
"chat_with_aissia",
|
||||||
"Dialogue with AISSIA assistant (Claude Sonnet 4). Send a message and get an intelligent response with access to AISSIA's knowledge and capabilities.",
|
"Dialogue with AISSIA assistant (Claude Sonnet 4). Send a message and get an intelligent response with access to AISSIA's knowledge and capabilities.",
|
||||||
{
|
{
|
||||||
{"type", "object"},
|
{"type", "object"},
|
||||||
{"properties", {
|
{"properties", {
|
||||||
{"message", {
|
{"message", {
|
||||||
{"type", "string"},
|
{"type", "string"},
|
||||||
{"description", "Message to send to AISSIA"}
|
{"description", "Message to send to AISSIA"}
|
||||||
}},
|
}},
|
||||||
{"conversation_id", {
|
{"conversation_id", {
|
||||||
{"type", "string"},
|
{"type", "string"},
|
||||||
{"description", "Conversation ID for continuity (optional)"}
|
{"description", "Conversation ID for continuity (optional)"}
|
||||||
}},
|
}},
|
||||||
{"system_prompt", {
|
{"system_prompt", {
|
||||||
{"type", "string"},
|
{"type", "string"},
|
||||||
{"description", "Custom system prompt (optional)"}
|
{"description", "Custom system prompt (optional)"}
|
||||||
}}
|
}}
|
||||||
}},
|
}},
|
||||||
{"required", json::array({"message"})}
|
{"required", json::array({"message"})}
|
||||||
},
|
},
|
||||||
[this](const json& input) { return handleChatWithAissia(input); }
|
[this](const json& input) { return handleChatWithAissia(input); }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tool 2: transcribe_audio
|
// Tool 2: transcribe_audio
|
||||||
if (m_voiceService) {
|
if (m_voiceService) {
|
||||||
tools.push_back({
|
tools.push_back({
|
||||||
"transcribe_audio",
|
"transcribe_audio",
|
||||||
"Transcribe audio file to text using Speech-to-Text (Whisper.cpp, OpenAI Whisper API, or Google Speech). Supports WAV, MP3, and other common audio formats.",
|
"Transcribe audio file to text using Speech-to-Text (Whisper.cpp, OpenAI Whisper API, or Google Speech). Supports WAV, MP3, and other common audio formats.",
|
||||||
{
|
{
|
||||||
{"type", "object"},
|
{"type", "object"},
|
||||||
{"properties", {
|
{"properties", {
|
||||||
{"file_path", {
|
{"file_path", {
|
||||||
{"type", "string"},
|
{"type", "string"},
|
||||||
{"description", "Path to audio file"}
|
{"description", "Path to audio file"}
|
||||||
}},
|
}},
|
||||||
{"language", {
|
{"language", {
|
||||||
{"type", "string"},
|
{"type", "string"},
|
||||||
{"description", "Language code (e.g., 'fr', 'en'). Default: 'fr'"}
|
{"description", "Language code (e.g., 'fr', 'en'). Default: 'fr'"}
|
||||||
}}
|
}}
|
||||||
}},
|
}},
|
||||||
{"required", json::array({"file_path"})}
|
{"required", json::array({"file_path"})}
|
||||||
},
|
},
|
||||||
[this](const json& input) { return handleTranscribeAudio(input); }
|
[this](const json& input) { return handleTranscribeAudio(input); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// Tool 3: text_to_speech
|
// Tool 3: text_to_speech
|
||||||
tools.push_back({
|
tools.push_back({
|
||||||
"text_to_speech",
|
"text_to_speech",
|
||||||
"Convert text to speech audio file using Text-to-Speech synthesis. Generates audio in WAV format.",
|
"Convert text to speech audio file using Text-to-Speech synthesis. Generates audio in WAV format.",
|
||||||
{
|
{
|
||||||
{"type", "object"},
|
{"type", "object"},
|
||||||
{"properties", {
|
{"properties", {
|
||||||
{"text", {
|
{"text", {
|
||||||
{"type", "string"},
|
{"type", "string"},
|
||||||
{"description", "Text to synthesize"}
|
{"description", "Text to synthesize"}
|
||||||
}},
|
}},
|
||||||
{"output_file", {
|
{"output_file", {
|
||||||
{"type", "string"},
|
{"type", "string"},
|
||||||
{"description", "Output audio file path (WAV)"}
|
{"description", "Output audio file path (WAV)"}
|
||||||
}},
|
}},
|
||||||
{"voice", {
|
{"voice", {
|
||||||
{"type", "string"},
|
{"type", "string"},
|
||||||
{"description", "Voice identifier (e.g., 'fr-fr', 'en-us'). Default: 'fr-fr'"}
|
{"description", "Voice identifier (e.g., 'fr-fr', 'en-us'). Default: 'fr-fr'"}
|
||||||
}}
|
}}
|
||||||
}},
|
}},
|
||||||
{"required", json::array({"text", "output_file"})}
|
{"required", json::array({"text", "output_file"})}
|
||||||
},
|
},
|
||||||
[this](const json& input) { return handleTextToSpeech(input); }
|
[this](const json& input) { return handleTextToSpeech(input); }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tool 4: save_memory
|
// Tool 4: save_memory
|
||||||
if (m_storageService) {
|
if (m_storageService) {
|
||||||
tools.push_back({
|
tools.push_back({
|
||||||
"save_memory",
|
"save_memory",
|
||||||
"Save a note or memory to AISSIA's persistent storage. Memories can be tagged and searched later.",
|
"Save a note or memory to AISSIA's persistent storage. Memories can be tagged and searched later.",
|
||||||
{
|
{
|
||||||
{"type", "object"},
|
{"type", "object"},
|
||||||
{"properties", {
|
{"properties", {
|
||||||
{"title", {
|
{"title", {
|
||||||
{"type", "string"},
|
{"type", "string"},
|
||||||
{"description", "Memory title"}
|
{"description", "Memory title"}
|
||||||
}},
|
}},
|
||||||
{"content", {
|
{"content", {
|
||||||
{"type", "string"},
|
{"type", "string"},
|
||||||
{"description", "Memory content"}
|
{"description", "Memory content"}
|
||||||
}},
|
}},
|
||||||
{"tags", {
|
{"tags", {
|
||||||
{"type", "array"},
|
{"type", "array"},
|
||||||
{"items", {{"type", "string"}}},
|
{"items", {{"type", "string"}}},
|
||||||
{"description", "Tags for categorization (optional)"}
|
{"description", "Tags for categorization (optional)"}
|
||||||
}}
|
}}
|
||||||
}},
|
}},
|
||||||
{"required", json::array({"title", "content"})}
|
{"required", json::array({"title", "content"})}
|
||||||
},
|
},
|
||||||
[this](const json& input) { return handleSaveMemory(input); }
|
[this](const json& input) { return handleSaveMemory(input); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// Tool 5: search_memories
|
// Tool 5: search_memories
|
||||||
tools.push_back({
|
tools.push_back({
|
||||||
"search_memories",
|
"search_memories",
|
||||||
"Search through saved memories and notes in AISSIA's storage. Returns matching memories with relevance scores.",
|
"Search through saved memories and notes in AISSIA's storage. Returns matching memories with relevance scores.",
|
||||||
{
|
{
|
||||||
{"type", "object"},
|
{"type", "object"},
|
||||||
{"properties", {
|
{"properties", {
|
||||||
{"query", {
|
{"query", {
|
||||||
{"type", "string"},
|
{"type", "string"},
|
||||||
{"description", "Search query"}
|
{"description", "Search query"}
|
||||||
}},
|
}},
|
||||||
{"limit", {
|
{"limit", {
|
||||||
{"type", "integer"},
|
{"type", "integer"},
|
||||||
{"description", "Maximum results to return. Default: 10"}
|
{"description", "Maximum results to return. Default: 10"}
|
||||||
}}
|
}}
|
||||||
}},
|
}},
|
||||||
{"required", json::array({"query"})}
|
{"required", json::array({"query"})}
|
||||||
},
|
},
|
||||||
[this](const json& input) { return handleSearchMemories(input); }
|
[this](const json& input) { return handleSearchMemories(input); }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return tools;
|
return tools;
|
||||||
}
|
}
|
||||||
|
|
||||||
json MCPServerTools::execute(const std::string& toolName, const json& input) {
|
json MCPServerTools::execute(const std::string& toolName, const json& input) {
|
||||||
if (toolName == "chat_with_aissia") {
|
if (toolName == "chat_with_aissia") {
|
||||||
return handleChatWithAissia(input);
|
return handleChatWithAissia(input);
|
||||||
} else if (toolName == "transcribe_audio") {
|
} else if (toolName == "transcribe_audio") {
|
||||||
return handleTranscribeAudio(input);
|
return handleTranscribeAudio(input);
|
||||||
} else if (toolName == "text_to_speech") {
|
} else if (toolName == "text_to_speech") {
|
||||||
return handleTextToSpeech(input);
|
return handleTextToSpeech(input);
|
||||||
} else if (toolName == "save_memory") {
|
} else if (toolName == "save_memory") {
|
||||||
return handleSaveMemory(input);
|
return handleSaveMemory(input);
|
||||||
} else if (toolName == "search_memories") {
|
} else if (toolName == "search_memories") {
|
||||||
return handleSearchMemories(input);
|
return handleSearchMemories(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
{"error", "Unknown tool: " + toolName}
|
{"error", "Unknown tool: " + toolName}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Tool Handlers
|
// Tool Handlers
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
json MCPServerTools::handleChatWithAissia(const json& input) {
|
json MCPServerTools::handleChatWithAissia(const json& input) {
|
||||||
if (!m_llmService) {
|
if (!m_llmService) {
|
||||||
return {{"error", "LLMService not available"}};
|
return {{"error", "LLMService not available"}};
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
std::string message = input["message"];
|
std::string message = input["message"];
|
||||||
std::string conversationId = input.value("conversation_id", "");
|
std::string conversationId = input.value("conversation_id", "");
|
||||||
std::string systemPrompt = input.value("system_prompt", "");
|
std::string systemPrompt = input.value("system_prompt", "");
|
||||||
|
|
||||||
spdlog::info("[chat_with_aissia] Message: {}", message.substr(0, 100));
|
spdlog::info("[chat_with_aissia] Message: {}", message.substr(0, 100));
|
||||||
|
|
||||||
// Call synchronous LLM method
|
// Call synchronous LLM method
|
||||||
auto response = m_llmService->sendMessageSync(message, conversationId, systemPrompt);
|
auto response = m_llmService->sendMessageSync(message, conversationId, systemPrompt);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
{"response", response.text},
|
{"response", response.text},
|
||||||
{"conversation_id", conversationId},
|
{"conversation_id", conversationId},
|
||||||
{"tokens", response.tokens},
|
{"tokens", response.tokens},
|
||||||
{"iterations", response.iterations}
|
{"iterations", response.iterations}
|
||||||
};
|
};
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
spdlog::error("[chat_with_aissia] Error: {}", e.what());
|
spdlog::error("[chat_with_aissia] Error: {}", e.what());
|
||||||
return {{"error", e.what()}};
|
return {{"error", e.what()}};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
json MCPServerTools::handleTranscribeAudio(const json& input) {
|
json MCPServerTools::handleTranscribeAudio(const json& input) {
|
||||||
if (!m_voiceService) {
|
if (!m_voiceService) {
|
||||||
return {{"error", "VoiceService not available"}};
|
return {{"error", "VoiceService not available"}};
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
std::string filePath = input["file_path"];
|
std::string filePath = input["file_path"];
|
||||||
std::string language = input.value("language", "fr");
|
std::string language = input.value("language", "fr");
|
||||||
|
|
||||||
spdlog::info("[transcribe_audio] File: {}, Language: {}", filePath, language);
|
spdlog::info("[transcribe_audio] File: {}, Language: {}", filePath, language);
|
||||||
|
|
||||||
// Call synchronous STT method
|
// Call synchronous STT method
|
||||||
std::string text = m_voiceService->transcribeFileSync(filePath, language);
|
std::string text = m_voiceService->transcribeFileSync(filePath, language);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
{"text", text},
|
{"text", text},
|
||||||
{"file", filePath},
|
{"file", filePath},
|
||||||
{"language", language}
|
{"language", language}
|
||||||
};
|
};
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
spdlog::error("[transcribe_audio] Error: {}", e.what());
|
spdlog::error("[transcribe_audio] Error: {}", e.what());
|
||||||
return {{"error", e.what()}};
|
return {{"error", e.what()}};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
json MCPServerTools::handleTextToSpeech(const json& input) {
|
json MCPServerTools::handleTextToSpeech(const json& input) {
|
||||||
if (!m_voiceService) {
|
if (!m_voiceService) {
|
||||||
return {{"error", "VoiceService not available"}};
|
return {{"error", "VoiceService not available"}};
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
std::string text = input["text"];
|
std::string text = input["text"];
|
||||||
std::string outputFile = input["output_file"];
|
std::string outputFile = input["output_file"];
|
||||||
std::string voice = input.value("voice", "fr-fr");
|
std::string voice = input.value("voice", "fr-fr");
|
||||||
|
|
||||||
spdlog::info("[text_to_speech] Text: {}, Output: {}", text.substr(0, 50), outputFile);
|
spdlog::info("[text_to_speech] Text: {}, Output: {}", text.substr(0, 50), outputFile);
|
||||||
|
|
||||||
// Call synchronous TTS method
|
// Call synchronous TTS method
|
||||||
bool success = m_voiceService->textToSpeechSync(text, outputFile, voice);
|
bool success = m_voiceService->textToSpeechSync(text, outputFile, voice);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
return {
|
return {
|
||||||
{"success", true},
|
{"success", true},
|
||||||
{"file", outputFile},
|
{"file", outputFile},
|
||||||
{"voice", voice}
|
{"voice", voice}
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {{"error", "TTS generation failed"}};
|
return {{"error", "TTS generation failed"}};
|
||||||
}
|
}
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
spdlog::error("[text_to_speech] Error: {}", e.what());
|
spdlog::error("[text_to_speech] Error: {}", e.what());
|
||||||
return {{"error", e.what()}};
|
return {{"error", e.what()}};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
json MCPServerTools::handleSaveMemory(const json& input) {
|
json MCPServerTools::handleSaveMemory(const json& input) {
|
||||||
if (!m_storageService) {
|
if (!m_storageService) {
|
||||||
return {{"error", "StorageService not available"}};
|
return {{"error", "StorageService not available"}};
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
std::string title = input["title"];
|
std::string title = input["title"];
|
||||||
std::string content = input["content"];
|
std::string content = input["content"];
|
||||||
std::vector<std::string> tags;
|
std::vector<std::string> tags;
|
||||||
|
|
||||||
if (input.contains("tags") && input["tags"].is_array()) {
|
if (input.contains("tags") && input["tags"].is_array()) {
|
||||||
for (const auto& tag : input["tags"]) {
|
for (const auto& tag : input["tags"]) {
|
||||||
tags.push_back(tag.get<std::string>());
|
tags.push_back(tag.get<std::string>());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
spdlog::info("[save_memory] Title: {}", title);
|
spdlog::info("[save_memory] Title: {}", title);
|
||||||
|
|
||||||
// TODO: Implement saveMemorySync in StorageService
|
// TODO: Implement saveMemorySync in StorageService
|
||||||
// For now, return not implemented
|
// For now, return not implemented
|
||||||
return json({
|
return json({
|
||||||
{"error", "save_memory not yet implemented"},
|
{"error", "save_memory not yet implemented"},
|
||||||
{"note", "StorageService sync methods need to be added"},
|
{"note", "StorageService sync methods need to be added"},
|
||||||
{"title", title}
|
{"title", title}
|
||||||
});
|
});
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
spdlog::error("[save_memory] Error: {}", e.what());
|
spdlog::error("[save_memory] Error: {}", e.what());
|
||||||
return {{"error", e.what()}};
|
return {{"error", e.what()}};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
json MCPServerTools::handleSearchMemories(const json& input) {
|
json MCPServerTools::handleSearchMemories(const json& input) {
|
||||||
if (!m_storageService) {
|
if (!m_storageService) {
|
||||||
return {{"error", "StorageService not available"}};
|
return {{"error", "StorageService not available"}};
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
std::string query = input["query"];
|
std::string query = input["query"];
|
||||||
int limit = input.value("limit", 10);
|
int limit = input.value("limit", 10);
|
||||||
|
|
||||||
spdlog::info("[search_memories] Query: {}, Limit: {}", query, limit);
|
spdlog::info("[search_memories] Query: {}, Limit: {}", query, limit);
|
||||||
|
|
||||||
// TODO: Implement searchMemoriesSync in StorageService
|
// TODO: Implement searchMemoriesSync in StorageService
|
||||||
// For now, return not implemented
|
// For now, return not implemented
|
||||||
return json({
|
return json({
|
||||||
{"error", "search_memories not yet implemented"},
|
{"error", "search_memories not yet implemented"},
|
||||||
{"note", "StorageService sync methods need to be added"},
|
{"note", "StorageService sync methods need to be added"},
|
||||||
{"query", query},
|
{"query", query},
|
||||||
{"limit", limit}
|
{"limit", limit}
|
||||||
});
|
});
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
spdlog::error("[search_memories] Error: {}", e.what());
|
spdlog::error("[search_memories] Error: {}", e.what());
|
||||||
return {{"error", e.what()}};
|
return {{"error", e.what()}};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace aissia::tools
|
} // namespace aissia::tools
|
||||||
|
|||||||
@ -1,76 +1,76 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "../llm/ToolRegistry.hpp"
|
#include "../llm/ToolRegistry.hpp"
|
||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
// Forward declarations
|
// Forward declarations
|
||||||
namespace aissia {
|
namespace aissia {
|
||||||
class LLMService;
|
class LLMService;
|
||||||
class StorageService;
|
class StorageService;
|
||||||
class VoiceService;
|
class VoiceService;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace aissia::tools {
|
namespace aissia::tools {
|
||||||
|
|
||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief MCP Server Tools - Bridge between MCP Server and AISSIA services
|
* @brief MCP Server Tools - Bridge between MCP Server and AISSIA services
|
||||||
*
|
*
|
||||||
* Provides tool definitions for AISSIA modules exposed via MCP Server:
|
* Provides tool definitions for AISSIA modules exposed via MCP Server:
|
||||||
* - chat_with_aissia: Dialogue with AISSIA (Claude Sonnet 4)
|
* - chat_with_aissia: Dialogue with AISSIA (Claude Sonnet 4)
|
||||||
* - transcribe_audio: Speech-to-text (Whisper.cpp/OpenAI/Google)
|
* - transcribe_audio: Speech-to-text (Whisper.cpp/OpenAI/Google)
|
||||||
* - text_to_speech: Text-to-speech synthesis
|
* - text_to_speech: Text-to-speech synthesis
|
||||||
* - save_memory: Save note/memory to storage
|
* - save_memory: Save note/memory to storage
|
||||||
* - search_memories: Search stored memories
|
* - search_memories: Search stored memories
|
||||||
*
|
*
|
||||||
* Note: These tools run in synchronous mode (no IIO pub/sub, no main loop)
|
* Note: These tools run in synchronous mode (no IIO pub/sub, no main loop)
|
||||||
*/
|
*/
|
||||||
class MCPServerTools {
|
class MCPServerTools {
|
||||||
public:
|
public:
|
||||||
/**
|
/**
|
||||||
* @brief Construct MCP server tools with service dependencies
|
* @brief Construct MCP server tools with service dependencies
|
||||||
*
|
*
|
||||||
* @param llm LLMService for chat_with_aissia (can be nullptr)
|
* @param llm LLMService for chat_with_aissia (can be nullptr)
|
||||||
* @param storage StorageService for save/search memories (can be nullptr)
|
* @param storage StorageService for save/search memories (can be nullptr)
|
||||||
* @param voice VoiceService for TTS/STT (can be nullptr)
|
* @param voice VoiceService for TTS/STT (can be nullptr)
|
||||||
*/
|
*/
|
||||||
MCPServerTools(
|
MCPServerTools(
|
||||||
LLMService* llm,
|
LLMService* llm,
|
||||||
StorageService* storage,
|
StorageService* storage,
|
||||||
VoiceService* voice
|
VoiceService* voice
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get all tool definitions for registration
|
* @brief Get all tool definitions for registration
|
||||||
*
|
*
|
||||||
* @return Vector of ToolDefinition structs
|
* @return Vector of ToolDefinition structs
|
||||||
*/
|
*/
|
||||||
std::vector<ToolDefinition> getToolDefinitions();
|
std::vector<ToolDefinition> getToolDefinitions();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Execute a tool by name
|
* @brief Execute a tool by name
|
||||||
*
|
*
|
||||||
* @param toolName Tool to execute
|
* @param toolName Tool to execute
|
||||||
* @param input Tool arguments (JSON)
|
* @param input Tool arguments (JSON)
|
||||||
* @return Tool result (JSON)
|
* @return Tool result (JSON)
|
||||||
*/
|
*/
|
||||||
json execute(const std::string& toolName, const json& input);
|
json execute(const std::string& toolName, const json& input);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Tool handlers
|
// Tool handlers
|
||||||
json handleChatWithAissia(const json& input);
|
json handleChatWithAissia(const json& input);
|
||||||
json handleTranscribeAudio(const json& input);
|
json handleTranscribeAudio(const json& input);
|
||||||
json handleTextToSpeech(const json& input);
|
json handleTextToSpeech(const json& input);
|
||||||
json handleSaveMemory(const json& input);
|
json handleSaveMemory(const json& input);
|
||||||
json handleSearchMemories(const json& input);
|
json handleSearchMemories(const json& input);
|
||||||
|
|
||||||
// Service references (nullable)
|
// Service references (nullable)
|
||||||
LLMService* m_llmService;
|
LLMService* m_llmService;
|
||||||
StorageService* m_storageService;
|
StorageService* m_storageService;
|
||||||
VoiceService* m_voiceService;
|
VoiceService* m_voiceService;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace aissia::tools
|
} // namespace aissia::tools
|
||||||
|
|||||||
@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"environment": {
|
"environment": {
|
||||||
"platform": "linux",
|
"platform": "linux",
|
||||||
"testDirectory": "tests/integration"
|
"testDirectory": "tests/integration"
|
||||||
},
|
},
|
||||||
"summary": {
|
"summary": {
|
||||||
"failed": 0,
|
"failed": 0,
|
||||||
"passed": 0,
|
"passed": 0,
|
||||||
"skipped": 0,
|
"skipped": 0,
|
||||||
"successRate": 0.0,
|
"successRate": 0.0,
|
||||||
"total": 0,
|
"total": 0,
|
||||||
"totalDurationMs": 0
|
"totalDurationMs": 0
|
||||||
},
|
},
|
||||||
"tests": [],
|
"tests": [],
|
||||||
"timestamp": "2025-11-29T09:01:38Z"
|
"timestamp": "2025-11-29T09:01:38Z"
|
||||||
}
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -a
|
set -a
|
||||||
source .env
|
source .env
|
||||||
set +a
|
set +a
|
||||||
echo "Quelle heure est-il ?" | timeout 30 ./build/aissia --interactive
|
echo "Quelle heure est-il ?" | timeout 30 ./build/aissia --interactive
|
||||||
|
|||||||
BIN
test_output.txt
Normal file
BIN
test_output.txt
Normal file
Binary file not shown.
@ -1,237 +1,237 @@
|
|||||||
/**
|
/**
|
||||||
* @file test_stt_live.cpp
|
* @file test_stt_live.cpp
|
||||||
* @brief Live STT testing tool - Test all 4 engines
|
* @brief Live STT testing tool - Test all 4 engines
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "src/shared/audio/ISTTEngine.hpp"
|
#include "src/shared/audio/ISTTEngine.hpp"
|
||||||
#include <spdlog/spdlog.h>
|
#include <spdlog/spdlog.h>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
|
||||||
using namespace aissia;
|
using namespace aissia;
|
||||||
|
|
||||||
// Helper: Load .env file
|
// Helper: Load .env file
|
||||||
void loadEnv(const std::string& path = ".env") {
|
void loadEnv(const std::string& path = ".env") {
|
||||||
std::ifstream file(path);
|
std::ifstream file(path);
|
||||||
if (!file.is_open()) {
|
if (!file.is_open()) {
|
||||||
spdlog::warn("No .env file found at: {}", path);
|
spdlog::warn("No .env file found at: {}", path);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string line;
|
std::string line;
|
||||||
while (std::getline(file, line)) {
|
while (std::getline(file, line)) {
|
||||||
if (line.empty() || line[0] == '#') continue;
|
if (line.empty() || line[0] == '#') continue;
|
||||||
|
|
||||||
auto pos = line.find('=');
|
auto pos = line.find('=');
|
||||||
if (pos != std::string::npos) {
|
if (pos != std::string::npos) {
|
||||||
std::string key = line.substr(0, pos);
|
std::string key = line.substr(0, pos);
|
||||||
std::string value = line.substr(pos + 1);
|
std::string value = line.substr(pos + 1);
|
||||||
|
|
||||||
// Remove quotes
|
// Remove quotes
|
||||||
if (!value.empty() && value.front() == '"' && value.back() == '"') {
|
if (!value.empty() && value.front() == '"' && value.back() == '"') {
|
||||||
value = value.substr(1, value.length() - 2);
|
value = value.substr(1, value.length() - 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
_putenv_s(key.c_str(), value.c_str());
|
_putenv_s(key.c_str(), value.c_str());
|
||||||
#else
|
#else
|
||||||
setenv(key.c_str(), value.c_str(), 1);
|
setenv(key.c_str(), value.c_str(), 1);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
spdlog::info("Loaded environment from {}", path);
|
spdlog::info("Loaded environment from {}", path);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper: Get API key from env
|
// Helper: Get API key from env
|
||||||
std::string getEnvVar(const std::string& name) {
|
std::string getEnvVar(const std::string& name) {
|
||||||
const char* val = std::getenv(name.c_str());
|
const char* val = std::getenv(name.c_str());
|
||||||
return val ? std::string(val) : "";
|
return val ? std::string(val) : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper: Load audio file as WAV (simplified - assumes 16-bit PCM)
|
// Helper: Load audio file as WAV (simplified - assumes 16-bit PCM)
|
||||||
std::vector<float> loadWavFile(const std::string& path) {
|
std::vector<float> loadWavFile(const std::string& path) {
|
||||||
std::ifstream file(path, std::ios::binary);
|
std::ifstream file(path, std::ios::binary);
|
||||||
if (!file.is_open()) {
|
if (!file.is_open()) {
|
||||||
spdlog::error("Failed to open audio file: {}", path);
|
spdlog::error("Failed to open audio file: {}", path);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip WAV header (44 bytes)
|
// Skip WAV header (44 bytes)
|
||||||
file.seekg(44);
|
file.seekg(44);
|
||||||
|
|
||||||
// Read 16-bit PCM samples
|
// Read 16-bit PCM samples
|
||||||
std::vector<int16_t> samples;
|
std::vector<int16_t> samples;
|
||||||
int16_t sample;
|
int16_t sample;
|
||||||
while (file.read(reinterpret_cast<char*>(&sample), sizeof(sample))) {
|
while (file.read(reinterpret_cast<char*>(&sample), sizeof(sample))) {
|
||||||
samples.push_back(sample);
|
samples.push_back(sample);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to float [-1.0, 1.0]
|
// Convert to float [-1.0, 1.0]
|
||||||
std::vector<float> audioData;
|
std::vector<float> audioData;
|
||||||
audioData.reserve(samples.size());
|
audioData.reserve(samples.size());
|
||||||
for (int16_t s : samples) {
|
for (int16_t s : samples) {
|
||||||
audioData.push_back(static_cast<float>(s) / 32768.0f);
|
audioData.push_back(static_cast<float>(s) / 32768.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
spdlog::info("Loaded {} samples from {}", audioData.size(), path);
|
spdlog::info("Loaded {} samples from {}", audioData.size(), path);
|
||||||
return audioData;
|
return audioData;
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(int argc, char* argv[]) {
|
int main(int argc, char* argv[]) {
|
||||||
spdlog::set_level(spdlog::level::info);
|
spdlog::set_level(spdlog::level::info);
|
||||||
spdlog::info("=== AISSIA STT Live Test ===");
|
spdlog::info("=== AISSIA STT Live Test ===");
|
||||||
|
|
||||||
// Load environment variables
|
// Load environment variables
|
||||||
loadEnv();
|
loadEnv();
|
||||||
|
|
||||||
// Check command line
|
// Check command line
|
||||||
if (argc < 2) {
|
if (argc < 2) {
|
||||||
std::cout << "Usage: " << argv[0] << " <audio.wav>\n";
|
std::cout << "Usage: " << argv[0] << " <audio.wav>\n";
|
||||||
std::cout << "\nAvailable engines:\n";
|
std::cout << "\nAvailable engines:\n";
|
||||||
std::cout << " 1. Whisper.cpp (local, requires models/ggml-base.bin)\n";
|
std::cout << " 1. Whisper.cpp (local, requires models/ggml-base.bin)\n";
|
||||||
std::cout << " 2. Whisper API (requires OPENAI_API_KEY)\n";
|
std::cout << " 2. Whisper API (requires OPENAI_API_KEY)\n";
|
||||||
std::cout << " 3. Google Speech (requires GOOGLE_API_KEY)\n";
|
std::cout << " 3. Google Speech (requires GOOGLE_API_KEY)\n";
|
||||||
std::cout << " 4. Azure STT (requires AZURE_SPEECH_KEY + AZURE_SPEECH_REGION)\n";
|
std::cout << " 4. Azure STT (requires AZURE_SPEECH_KEY + AZURE_SPEECH_REGION)\n";
|
||||||
std::cout << " 5. Deepgram (requires DEEPGRAM_API_KEY)\n";
|
std::cout << " 5. Deepgram (requires DEEPGRAM_API_KEY)\n";
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string audioFile = argv[1];
|
std::string audioFile = argv[1];
|
||||||
|
|
||||||
// Load audio
|
// Load audio
|
||||||
std::vector<float> audioData = loadWavFile(audioFile);
|
std::vector<float> audioData = loadWavFile(audioFile);
|
||||||
if (audioData.empty()) {
|
if (audioData.empty()) {
|
||||||
spdlog::error("Failed to load audio data");
|
spdlog::error("Failed to load audio data");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test each engine
|
// Test each engine
|
||||||
std::cout << "\n========================================\n";
|
std::cout << "\n========================================\n";
|
||||||
std::cout << "Testing STT Engines\n";
|
std::cout << "Testing STT Engines\n";
|
||||||
std::cout << "========================================\n\n";
|
std::cout << "========================================\n\n";
|
||||||
|
|
||||||
// 1. Whisper.cpp (local)
|
// 1. Whisper.cpp (local)
|
||||||
{
|
{
|
||||||
std::cout << "[1/5] Whisper.cpp (local)\n";
|
std::cout << "[1/5] Whisper.cpp (local)\n";
|
||||||
std::cout << "----------------------------\n";
|
std::cout << "----------------------------\n";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
auto engine = STTEngineFactory::create("whisper_cpp", "models/ggml-base.bin");
|
auto engine = STTEngineFactory::create("whisper_cpp", "models/ggml-base.bin");
|
||||||
if (engine && engine->isAvailable()) {
|
if (engine && engine->isAvailable()) {
|
||||||
engine->setLanguage("fr");
|
engine->setLanguage("fr");
|
||||||
std::string result = engine->transcribe(audioData);
|
std::string result = engine->transcribe(audioData);
|
||||||
std::cout << "✅ Result: " << result << "\n\n";
|
std::cout << "✅ Result: " << result << "\n\n";
|
||||||
} else {
|
} else {
|
||||||
std::cout << "❌ Not available (model missing?)\n\n";
|
std::cout << "❌ Not available (model missing?)\n\n";
|
||||||
}
|
}
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
std::cout << "❌ Error: " << e.what() << "\n\n";
|
std::cout << "❌ Error: " << e.what() << "\n\n";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Whisper API
|
// 2. Whisper API
|
||||||
{
|
{
|
||||||
std::cout << "[2/5] OpenAI Whisper API\n";
|
std::cout << "[2/5] OpenAI Whisper API\n";
|
||||||
std::cout << "----------------------------\n";
|
std::cout << "----------------------------\n";
|
||||||
|
|
||||||
std::string apiKey = getEnvVar("OPENAI_API_KEY");
|
std::string apiKey = getEnvVar("OPENAI_API_KEY");
|
||||||
if (apiKey.empty()) {
|
if (apiKey.empty()) {
|
||||||
std::cout << "❌ OPENAI_API_KEY not set\n\n";
|
std::cout << "❌ OPENAI_API_KEY not set\n\n";
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
auto engine = STTEngineFactory::create("whisper_api", "", apiKey);
|
auto engine = STTEngineFactory::create("whisper_api", "", apiKey);
|
||||||
if (engine && engine->isAvailable()) {
|
if (engine && engine->isAvailable()) {
|
||||||
engine->setLanguage("fr");
|
engine->setLanguage("fr");
|
||||||
std::string result = engine->transcribeFile(audioFile);
|
std::string result = engine->transcribeFile(audioFile);
|
||||||
std::cout << "✅ Result: " << result << "\n\n";
|
std::cout << "✅ Result: " << result << "\n\n";
|
||||||
} else {
|
} else {
|
||||||
std::cout << "❌ Not available\n\n";
|
std::cout << "❌ Not available\n\n";
|
||||||
}
|
}
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
std::cout << "❌ Error: " << e.what() << "\n\n";
|
std::cout << "❌ Error: " << e.what() << "\n\n";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Google Speech
|
// 3. Google Speech
|
||||||
{
|
{
|
||||||
std::cout << "[3/5] Google Speech-to-Text\n";
|
std::cout << "[3/5] Google Speech-to-Text\n";
|
||||||
std::cout << "----------------------------\n";
|
std::cout << "----------------------------\n";
|
||||||
|
|
||||||
std::string apiKey = getEnvVar("GOOGLE_API_KEY");
|
std::string apiKey = getEnvVar("GOOGLE_API_KEY");
|
||||||
if (apiKey.empty()) {
|
if (apiKey.empty()) {
|
||||||
std::cout << "❌ GOOGLE_API_KEY not set\n\n";
|
std::cout << "❌ GOOGLE_API_KEY not set\n\n";
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
auto engine = STTEngineFactory::create("google", "", apiKey);
|
auto engine = STTEngineFactory::create("google", "", apiKey);
|
||||||
if (engine && engine->isAvailable()) {
|
if (engine && engine->isAvailable()) {
|
||||||
engine->setLanguage("fr");
|
engine->setLanguage("fr");
|
||||||
std::string result = engine->transcribeFile(audioFile);
|
std::string result = engine->transcribeFile(audioFile);
|
||||||
std::cout << "✅ Result: " << result << "\n\n";
|
std::cout << "✅ Result: " << result << "\n\n";
|
||||||
} else {
|
} else {
|
||||||
std::cout << "❌ Not available\n\n";
|
std::cout << "❌ Not available\n\n";
|
||||||
}
|
}
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
std::cout << "❌ Error: " << e.what() << "\n\n";
|
std::cout << "❌ Error: " << e.what() << "\n\n";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Azure Speech
|
// 4. Azure Speech
|
||||||
{
|
{
|
||||||
std::cout << "[4/5] Azure Speech-to-Text\n";
|
std::cout << "[4/5] Azure Speech-to-Text\n";
|
||||||
std::cout << "----------------------------\n";
|
std::cout << "----------------------------\n";
|
||||||
|
|
||||||
std::string apiKey = getEnvVar("AZURE_SPEECH_KEY");
|
std::string apiKey = getEnvVar("AZURE_SPEECH_KEY");
|
||||||
std::string region = getEnvVar("AZURE_SPEECH_REGION");
|
std::string region = getEnvVar("AZURE_SPEECH_REGION");
|
||||||
|
|
||||||
if (apiKey.empty() || region.empty()) {
|
if (apiKey.empty() || region.empty()) {
|
||||||
std::cout << "❌ AZURE_SPEECH_KEY or AZURE_SPEECH_REGION not set\n\n";
|
std::cout << "❌ AZURE_SPEECH_KEY or AZURE_SPEECH_REGION not set\n\n";
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
auto engine = STTEngineFactory::create("azure", region, apiKey);
|
auto engine = STTEngineFactory::create("azure", region, apiKey);
|
||||||
if (engine && engine->isAvailable()) {
|
if (engine && engine->isAvailable()) {
|
||||||
engine->setLanguage("fr");
|
engine->setLanguage("fr");
|
||||||
std::string result = engine->transcribeFile(audioFile);
|
std::string result = engine->transcribeFile(audioFile);
|
||||||
std::cout << "✅ Result: " << result << "\n\n";
|
std::cout << "✅ Result: " << result << "\n\n";
|
||||||
} else {
|
} else {
|
||||||
std::cout << "❌ Not available\n\n";
|
std::cout << "❌ Not available\n\n";
|
||||||
}
|
}
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
std::cout << "❌ Error: " << e.what() << "\n\n";
|
std::cout << "❌ Error: " << e.what() << "\n\n";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Deepgram
|
// 5. Deepgram
|
||||||
{
|
{
|
||||||
std::cout << "[5/5] Deepgram\n";
|
std::cout << "[5/5] Deepgram\n";
|
||||||
std::cout << "----------------------------\n";
|
std::cout << "----------------------------\n";
|
||||||
|
|
||||||
std::string apiKey = getEnvVar("DEEPGRAM_API_KEY");
|
std::string apiKey = getEnvVar("DEEPGRAM_API_KEY");
|
||||||
if (apiKey.empty()) {
|
if (apiKey.empty()) {
|
||||||
std::cout << "❌ DEEPGRAM_API_KEY not set\n\n";
|
std::cout << "❌ DEEPGRAM_API_KEY not set\n\n";
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
auto engine = STTEngineFactory::create("deepgram", "", apiKey);
|
auto engine = STTEngineFactory::create("deepgram", "", apiKey);
|
||||||
if (engine && engine->isAvailable()) {
|
if (engine && engine->isAvailable()) {
|
||||||
engine->setLanguage("fr");
|
engine->setLanguage("fr");
|
||||||
std::string result = engine->transcribeFile(audioFile);
|
std::string result = engine->transcribeFile(audioFile);
|
||||||
std::cout << "✅ Result: " << result << "\n\n";
|
std::cout << "✅ Result: " << result << "\n\n";
|
||||||
} else {
|
} else {
|
||||||
std::cout << "❌ Not available\n\n";
|
std::cout << "❌ Not available\n\n";
|
||||||
}
|
}
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
std::cout << "❌ Error: " << e.what() << "\n\n";
|
std::cout << "❌ Error: " << e.what() << "\n\n";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::cout << "========================================\n";
|
std::cout << "========================================\n";
|
||||||
std::cout << "Testing complete!\n";
|
std::cout << "Testing complete!\n";
|
||||||
std::cout << "========================================\n";
|
std::cout << "========================================\n";
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,176 +1,176 @@
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
# AISSIA Integration Tests
|
# AISSIA Integration Tests
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
# Fetch Catch2
|
# Fetch Catch2
|
||||||
include(FetchContent)
|
include(FetchContent)
|
||||||
FetchContent_Declare(
|
FetchContent_Declare(
|
||||||
Catch2
|
Catch2
|
||||||
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
|
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
|
||||||
GIT_TAG v3.4.0
|
GIT_TAG v3.4.0
|
||||||
)
|
)
|
||||||
FetchContent_MakeAvailable(Catch2)
|
FetchContent_MakeAvailable(Catch2)
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Test executable
|
# Test executable
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
add_executable(aissia_tests
|
add_executable(aissia_tests
|
||||||
main.cpp
|
main.cpp
|
||||||
|
|
||||||
# Mocks
|
# Mocks
|
||||||
mocks/MockIO.cpp
|
mocks/MockIO.cpp
|
||||||
|
|
||||||
# Module sources (needed for testing)
|
# Module sources (needed for testing)
|
||||||
${CMAKE_SOURCE_DIR}/src/modules/SchedulerModule.cpp
|
${CMAKE_SOURCE_DIR}/src/modules/SchedulerModule.cpp
|
||||||
${CMAKE_SOURCE_DIR}/src/modules/NotificationModule.cpp
|
${CMAKE_SOURCE_DIR}/src/modules/NotificationModule.cpp
|
||||||
${CMAKE_SOURCE_DIR}/src/modules/MonitoringModule.cpp
|
${CMAKE_SOURCE_DIR}/src/modules/MonitoringModule.cpp
|
||||||
${CMAKE_SOURCE_DIR}/src/modules/AIModule.cpp
|
${CMAKE_SOURCE_DIR}/src/modules/AIModule.cpp
|
||||||
${CMAKE_SOURCE_DIR}/src/modules/VoiceModule.cpp
|
${CMAKE_SOURCE_DIR}/src/modules/VoiceModule.cpp
|
||||||
${CMAKE_SOURCE_DIR}/src/modules/StorageModule.cpp
|
${CMAKE_SOURCE_DIR}/src/modules/StorageModule.cpp
|
||||||
${CMAKE_SOURCE_DIR}/src/modules/WebModule.cpp
|
${CMAKE_SOURCE_DIR}/src/modules/WebModule.cpp
|
||||||
|
|
||||||
# Module tests (70 TI)
|
# Module tests (70 TI)
|
||||||
modules/SchedulerModuleTests.cpp
|
modules/SchedulerModuleTests.cpp
|
||||||
modules/NotificationModuleTests.cpp
|
modules/NotificationModuleTests.cpp
|
||||||
modules/MonitoringModuleTests.cpp
|
modules/MonitoringModuleTests.cpp
|
||||||
modules/AIModuleTests.cpp
|
modules/AIModuleTests.cpp
|
||||||
modules/VoiceModuleTests.cpp
|
modules/VoiceModuleTests.cpp
|
||||||
modules/StorageModuleTests.cpp
|
modules/StorageModuleTests.cpp
|
||||||
modules/WebModuleTests.cpp
|
modules/WebModuleTests.cpp
|
||||||
|
|
||||||
# MCP tests (50 TI)
|
# MCP tests (50 TI)
|
||||||
mcp/MCPTypesTests.cpp
|
mcp/MCPTypesTests.cpp
|
||||||
mcp/StdioTransportTests.cpp
|
mcp/StdioTransportTests.cpp
|
||||||
mcp/MCPClientTests.cpp
|
mcp/MCPClientTests.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(aissia_tests PRIVATE
|
target_link_libraries(aissia_tests PRIVATE
|
||||||
Catch2::Catch2WithMain
|
Catch2::Catch2WithMain
|
||||||
GroveEngine::impl
|
GroveEngine::impl
|
||||||
AissiaTools
|
AissiaTools
|
||||||
spdlog::spdlog
|
spdlog::spdlog
|
||||||
)
|
)
|
||||||
|
|
||||||
# WebModule needs httplib and OpenSSL
|
# WebModule needs httplib and OpenSSL
|
||||||
target_include_directories(aissia_tests PRIVATE
|
target_include_directories(aissia_tests PRIVATE
|
||||||
${httplib_SOURCE_DIR}
|
${httplib_SOURCE_DIR}
|
||||||
)
|
)
|
||||||
# Link Winsock for httplib on Windows
|
# Link Winsock for httplib on Windows
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
target_link_libraries(aissia_tests PRIVATE ws2_32)
|
target_link_libraries(aissia_tests PRIVATE ws2_32)
|
||||||
endif()
|
endif()
|
||||||
if(OPENSSL_FOUND)
|
if(OPENSSL_FOUND)
|
||||||
target_link_libraries(aissia_tests PRIVATE OpenSSL::SSL OpenSSL::Crypto)
|
target_link_libraries(aissia_tests PRIVATE OpenSSL::SSL OpenSSL::Crypto)
|
||||||
target_compile_definitions(aissia_tests PRIVATE CPPHTTPLIB_OPENSSL_SUPPORT)
|
target_compile_definitions(aissia_tests PRIVATE CPPHTTPLIB_OPENSSL_SUPPORT)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Disable module factory functions during testing
|
# Disable module factory functions during testing
|
||||||
target_compile_definitions(aissia_tests PRIVATE AISSIA_TEST_BUILD)
|
target_compile_definitions(aissia_tests PRIVATE AISSIA_TEST_BUILD)
|
||||||
|
|
||||||
target_include_directories(aissia_tests PRIVATE
|
target_include_directories(aissia_tests PRIVATE
|
||||||
${CMAKE_SOURCE_DIR}/src
|
${CMAKE_SOURCE_DIR}/src
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}
|
${CMAKE_CURRENT_SOURCE_DIR}
|
||||||
)
|
)
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Copy test fixtures to build directory
|
# Copy test fixtures to build directory
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/fixtures/
|
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/fixtures/
|
||||||
DESTINATION ${CMAKE_BINARY_DIR}/tests/fixtures)
|
DESTINATION ${CMAKE_BINARY_DIR}/tests/fixtures)
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# CTest integration
|
# CTest integration
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
include(CTest)
|
include(CTest)
|
||||||
# Note: catch_discover_tests requires running the exe at build time
|
# Note: catch_discover_tests requires running the exe at build time
|
||||||
# which can fail due to missing DLLs. Use manual test registration instead.
|
# which can fail due to missing DLLs. Use manual test registration instead.
|
||||||
add_test(NAME aissia_tests COMMAND aissia_tests)
|
add_test(NAME aissia_tests COMMAND aissia_tests)
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Custom targets
|
# Custom targets
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
# Run all tests
|
# Run all tests
|
||||||
add_custom_target(test_all
|
add_custom_target(test_all
|
||||||
COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure
|
COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure
|
||||||
DEPENDS aissia_tests
|
DEPENDS aissia_tests
|
||||||
COMMENT "Running all integration tests"
|
COMMENT "Running all integration tests"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Run module tests only
|
# Run module tests only
|
||||||
add_custom_target(test_modules
|
add_custom_target(test_modules
|
||||||
COMMAND $<TARGET_FILE:aissia_tests> "[scheduler],[notification],[monitoring],[ai],[voice],[storage],[web]"
|
COMMAND $<TARGET_FILE:aissia_tests> "[scheduler],[notification],[monitoring],[ai],[voice],[storage],[web]"
|
||||||
DEPENDS aissia_tests
|
DEPENDS aissia_tests
|
||||||
COMMENT "Running module integration tests"
|
COMMENT "Running module integration tests"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Run MCP tests only
|
# Run MCP tests only
|
||||||
add_custom_target(test_mcp
|
add_custom_target(test_mcp
|
||||||
COMMAND $<TARGET_FILE:aissia_tests> "[mcp]"
|
COMMAND $<TARGET_FILE:aissia_tests> "[mcp]"
|
||||||
DEPENDS aissia_tests
|
DEPENDS aissia_tests
|
||||||
COMMENT "Running MCP integration tests"
|
COMMENT "Running MCP integration tests"
|
||||||
)
|
)
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Integration Test Modules (Dynamic .so files)
|
# Integration Test Modules (Dynamic .so files)
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
# Helper macro to create integration test modules
|
# Helper macro to create integration test modules
|
||||||
macro(add_integration_test TEST_NAME)
|
macro(add_integration_test TEST_NAME)
|
||||||
add_library(${TEST_NAME} SHARED
|
add_library(${TEST_NAME} SHARED
|
||||||
integration/${TEST_NAME}.cpp
|
integration/${TEST_NAME}.cpp
|
||||||
)
|
)
|
||||||
target_include_directories(${TEST_NAME} PRIVATE
|
target_include_directories(${TEST_NAME} PRIVATE
|
||||||
${CMAKE_SOURCE_DIR}/src
|
${CMAKE_SOURCE_DIR}/src
|
||||||
)
|
)
|
||||||
target_link_libraries(${TEST_NAME} PRIVATE
|
target_link_libraries(${TEST_NAME} PRIVATE
|
||||||
GroveEngine::impl
|
GroveEngine::impl
|
||||||
spdlog::spdlog
|
spdlog::spdlog
|
||||||
)
|
)
|
||||||
set_target_properties(${TEST_NAME} PROPERTIES
|
set_target_properties(${TEST_NAME} PROPERTIES
|
||||||
PREFIX ""
|
PREFIX ""
|
||||||
LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/tests/integration
|
LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/tests/integration
|
||||||
)
|
)
|
||||||
endmacro()
|
endmacro()
|
||||||
|
|
||||||
# Individual integration test modules (will be added as we create them)
|
# Individual integration test modules (will be added as we create them)
|
||||||
# Phase 2: MCP Tests
|
# Phase 2: MCP Tests
|
||||||
add_integration_test(IT_001_GetCurrentTime)
|
add_integration_test(IT_001_GetCurrentTime)
|
||||||
add_integration_test(IT_002_FileSystemWrite)
|
add_integration_test(IT_002_FileSystemWrite)
|
||||||
add_integration_test(IT_003_FileSystemRead)
|
add_integration_test(IT_003_FileSystemRead)
|
||||||
add_integration_test(IT_004_MCPToolsList)
|
add_integration_test(IT_004_MCPToolsList)
|
||||||
|
|
||||||
# Phase 3: Flow Tests
|
# Phase 3: Flow Tests
|
||||||
add_integration_test(IT_005_VoiceToAI)
|
add_integration_test(IT_005_VoiceToAI)
|
||||||
add_integration_test(IT_006_AIToLLM)
|
add_integration_test(IT_006_AIToLLM)
|
||||||
add_integration_test(IT_007_StorageWrite)
|
add_integration_test(IT_007_StorageWrite)
|
||||||
add_integration_test(IT_008_StorageRead)
|
add_integration_test(IT_008_StorageRead)
|
||||||
|
|
||||||
# Phase 4: End-to-End Test
|
# Phase 4: End-to-End Test
|
||||||
add_integration_test(IT_009_FullConversationLoop)
|
add_integration_test(IT_009_FullConversationLoop)
|
||||||
|
|
||||||
# Phase 5: Module Tests
|
# Phase 5: Module Tests
|
||||||
add_integration_test(IT_010_SchedulerHyperfocus)
|
add_integration_test(IT_010_SchedulerHyperfocus)
|
||||||
add_integration_test(IT_011_NotificationAlert)
|
add_integration_test(IT_011_NotificationAlert)
|
||||||
add_integration_test(IT_012_MonitoringActivity)
|
add_integration_test(IT_012_MonitoringActivity)
|
||||||
add_integration_test(IT_013_WebRequest)
|
add_integration_test(IT_013_WebRequest)
|
||||||
|
|
||||||
# Custom target to build all integration tests
|
# Custom target to build all integration tests
|
||||||
add_custom_target(integration_tests
|
add_custom_target(integration_tests
|
||||||
DEPENDS
|
DEPENDS
|
||||||
IT_001_GetCurrentTime
|
IT_001_GetCurrentTime
|
||||||
IT_002_FileSystemWrite
|
IT_002_FileSystemWrite
|
||||||
IT_003_FileSystemRead
|
IT_003_FileSystemRead
|
||||||
IT_004_MCPToolsList
|
IT_004_MCPToolsList
|
||||||
IT_005_VoiceToAI
|
IT_005_VoiceToAI
|
||||||
IT_006_AIToLLM
|
IT_006_AIToLLM
|
||||||
IT_007_StorageWrite
|
IT_007_StorageWrite
|
||||||
IT_008_StorageRead
|
IT_008_StorageRead
|
||||||
IT_009_FullConversationLoop
|
IT_009_FullConversationLoop
|
||||||
IT_010_SchedulerHyperfocus
|
IT_010_SchedulerHyperfocus
|
||||||
IT_011_NotificationAlert
|
IT_011_NotificationAlert
|
||||||
IT_012_MonitoringActivity
|
IT_012_MonitoringActivity
|
||||||
IT_013_WebRequest
|
IT_013_WebRequest
|
||||||
COMMENT "Building all integration test modules"
|
COMMENT "Building all integration test modules"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
68
tests/fixtures/echo_server.py
vendored
68
tests/fixtures/echo_server.py
vendored
@ -1,34 +1,34 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Simple JSON-RPC echo server for testing StdioTransport.
|
Simple JSON-RPC echo server for testing StdioTransport.
|
||||||
Echoes back the params of any request as the result.
|
Echoes back the params of any request as the result.
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
line = sys.stdin.readline()
|
line = sys.stdin.readline()
|
||||||
if not line:
|
if not line:
|
||||||
break
|
break
|
||||||
|
|
||||||
request = json.loads(line.strip())
|
request = json.loads(line.strip())
|
||||||
response = {
|
response = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": request.get("id"),
|
"id": request.get("id"),
|
||||||
"result": request.get("params", {})
|
"result": request.get("params", {})
|
||||||
}
|
}
|
||||||
sys.stdout.write(json.dumps(response) + "\n")
|
sys.stdout.write(json.dumps(response) + "\n")
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
# Invalid JSON, ignore
|
# Invalid JSON, ignore
|
||||||
pass
|
pass
|
||||||
except Exception:
|
except Exception:
|
||||||
# Other errors, continue
|
# Other errors, continue
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
38
tests/fixtures/mock_mcp.json
vendored
38
tests/fixtures/mock_mcp.json
vendored
@ -1,19 +1,19 @@
|
|||||||
{
|
{
|
||||||
"servers": {
|
"servers": {
|
||||||
"mock_server": {
|
"mock_server": {
|
||||||
"command": "C:\\Users\\alexi\\AppData\\Local\\Programs\\Python\\Python312\\python.exe",
|
"command": "C:\\Users\\alexi\\AppData\\Local\\Programs\\Python\\Python312\\python.exe",
|
||||||
"args": ["-u", "tests/fixtures/mock_mcp_server.py"],
|
"args": ["-u", "tests/fixtures/mock_mcp_server.py"],
|
||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
"disabled_server": {
|
"disabled_server": {
|
||||||
"command": "nonexistent_command",
|
"command": "nonexistent_command",
|
||||||
"args": [],
|
"args": [],
|
||||||
"enabled": false
|
"enabled": false
|
||||||
},
|
},
|
||||||
"echo_server": {
|
"echo_server": {
|
||||||
"command": "C:\\Users\\alexi\\AppData\\Local\\Programs\\Python\\Python312\\python.exe",
|
"command": "C:\\Users\\alexi\\AppData\\Local\\Programs\\Python\\Python312\\python.exe",
|
||||||
"args": ["-u", "tests/fixtures/echo_server.py"],
|
"args": ["-u", "tests/fixtures/echo_server.py"],
|
||||||
"enabled": true
|
"enabled": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
272
tests/fixtures/mock_mcp_server.py
vendored
272
tests/fixtures/mock_mcp_server.py
vendored
@ -1,136 +1,136 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Mock MCP server for integration testing.
|
Mock MCP server for integration testing.
|
||||||
Implements the MCP protocol (initialize, tools/list, tools/call).
|
Implements the MCP protocol (initialize, tools/list, tools/call).
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
|
||||||
# Tools exposed by this mock server
|
# Tools exposed by this mock server
|
||||||
TOOLS = [
|
TOOLS = [
|
||||||
{
|
{
|
||||||
"name": "test_tool",
|
"name": "test_tool",
|
||||||
"description": "A test tool that echoes its input",
|
"description": "A test tool that echoes its input",
|
||||||
"inputSchema": {
|
"inputSchema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"message": {"type": "string", "description": "Message to echo"}
|
"message": {"type": "string", "description": "Message to echo"}
|
||||||
},
|
},
|
||||||
"required": ["message"]
|
"required": ["message"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "get_time",
|
"name": "get_time",
|
||||||
"description": "Returns the current server time",
|
"description": "Returns the current server time",
|
||||||
"inputSchema": {
|
"inputSchema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {}
|
"properties": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def handle_initialize(params):
|
def handle_initialize(params):
|
||||||
"""Handle initialize request"""
|
"""Handle initialize request"""
|
||||||
return {
|
return {
|
||||||
"protocolVersion": "2024-11-05",
|
"protocolVersion": "2024-11-05",
|
||||||
"capabilities": {
|
"capabilities": {
|
||||||
"tools": {}
|
"tools": {}
|
||||||
},
|
},
|
||||||
"serverInfo": {
|
"serverInfo": {
|
||||||
"name": "MockMCPServer",
|
"name": "MockMCPServer",
|
||||||
"version": "1.0.0"
|
"version": "1.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def handle_tools_list(params):
|
def handle_tools_list(params):
|
||||||
"""Handle tools/list request"""
|
"""Handle tools/list request"""
|
||||||
return {"tools": TOOLS}
|
return {"tools": TOOLS}
|
||||||
|
|
||||||
|
|
||||||
def handle_tools_call(params):
|
def handle_tools_call(params):
|
||||||
"""Handle tools/call request"""
|
"""Handle tools/call request"""
|
||||||
tool_name = params.get("name", "")
|
tool_name = params.get("name", "")
|
||||||
arguments = params.get("arguments", {})
|
arguments = params.get("arguments", {})
|
||||||
|
|
||||||
if tool_name == "test_tool":
|
if tool_name == "test_tool":
|
||||||
message = arguments.get("message", "no message")
|
message = arguments.get("message", "no message")
|
||||||
return {
|
return {
|
||||||
"content": [
|
"content": [
|
||||||
{"type": "text", "text": f"Echo: {message}"}
|
{"type": "text", "text": f"Echo: {message}"}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
elif tool_name == "get_time":
|
elif tool_name == "get_time":
|
||||||
import datetime
|
import datetime
|
||||||
return {
|
return {
|
||||||
"content": [
|
"content": [
|
||||||
{"type": "text", "text": datetime.datetime.now().isoformat()}
|
{"type": "text", "text": datetime.datetime.now().isoformat()}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
return {
|
return {
|
||||||
"content": [
|
"content": [
|
||||||
{"type": "text", "text": f"Unknown tool: {tool_name}"}
|
{"type": "text", "text": f"Unknown tool: {tool_name}"}
|
||||||
],
|
],
|
||||||
"isError": True
|
"isError": True
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def handle_request(request):
|
def handle_request(request):
|
||||||
"""Route request to appropriate handler"""
|
"""Route request to appropriate handler"""
|
||||||
method = request.get("method", "")
|
method = request.get("method", "")
|
||||||
|
|
||||||
handlers = {
|
handlers = {
|
||||||
"initialize": handle_initialize,
|
"initialize": handle_initialize,
|
||||||
"tools/list": handle_tools_list,
|
"tools/list": handle_tools_list,
|
||||||
"tools/call": handle_tools_call,
|
"tools/call": handle_tools_call,
|
||||||
}
|
}
|
||||||
|
|
||||||
handler = handlers.get(method)
|
handler = handlers.get(method)
|
||||||
if handler:
|
if handler:
|
||||||
return handler(request.get("params", {}))
|
return handler(request.get("params", {}))
|
||||||
else:
|
else:
|
||||||
return {"error": {"code": -32601, "message": f"Method not found: {method}"}}
|
return {"error": {"code": -32601, "message": f"Method not found: {method}"}}
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
line = sys.stdin.readline()
|
line = sys.stdin.readline()
|
||||||
if not line:
|
if not line:
|
||||||
break
|
break
|
||||||
|
|
||||||
request = json.loads(line.strip())
|
request = json.loads(line.strip())
|
||||||
result = handle_request(request)
|
result = handle_request(request)
|
||||||
|
|
||||||
response = {
|
response = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": request.get("id")
|
"id": request.get("id")
|
||||||
}
|
}
|
||||||
|
|
||||||
if "error" in result:
|
if "error" in result:
|
||||||
response["error"] = result["error"]
|
response["error"] = result["error"]
|
||||||
else:
|
else:
|
||||||
response["result"] = result
|
response["result"] = result
|
||||||
|
|
||||||
sys.stdout.write(json.dumps(response) + "\n")
|
sys.stdout.write(json.dumps(response) + "\n")
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
error_response = {
|
error_response = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": None,
|
"id": None,
|
||||||
"error": {"code": -32700, "message": f"Parse error: {str(e)}"}
|
"error": {"code": -32700, "message": f"Parse error: {str(e)}"}
|
||||||
}
|
}
|
||||||
sys.stdout.write(json.dumps(error_response) + "\n")
|
sys.stdout.write(json.dumps(error_response) + "\n")
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Log to stderr for debugging
|
# Log to stderr for debugging
|
||||||
sys.stderr.write(f"Error: {str(e)}\n")
|
sys.stderr.write(f"Error: {str(e)}\n")
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
// AISSIA Integration Tests - Entry Point
|
// AISSIA Integration Tests - Entry Point
|
||||||
// Using Catch2 v3 with main provided by Catch2::Catch2WithMain
|
// Using Catch2 v3 with main provided by Catch2::Catch2WithMain
|
||||||
|
|
||||||
// This file is intentionally minimal.
|
// This file is intentionally minimal.
|
||||||
// Catch2WithMain provides the main() function automatically.
|
// Catch2WithMain provides the main() function automatically.
|
||||||
|
|
||||||
// Include common test utilities
|
// Include common test utilities
|
||||||
#include "utils/TestHelpers.hpp"
|
#include "utils/TestHelpers.hpp"
|
||||||
|
|||||||
@ -1,392 +1,392 @@
|
|||||||
/**
|
/**
|
||||||
* @file MCPClientTests.cpp
|
* @file MCPClientTests.cpp
|
||||||
* @brief Integration tests for MCPClient (15 TI)
|
* @brief Integration tests for MCPClient (15 TI)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <catch2/catch_test_macros.hpp>
|
#include <catch2/catch_test_macros.hpp>
|
||||||
#include "shared/mcp/MCPClient.hpp"
|
#include "shared/mcp/MCPClient.hpp"
|
||||||
#include "mocks/MockTransport.hpp"
|
#include "mocks/MockTransport.hpp"
|
||||||
|
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
|
||||||
using namespace aissia::mcp;
|
using namespace aissia::mcp;
|
||||||
using namespace aissia::tests;
|
using namespace aissia::tests;
|
||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Helper: Create test config file
|
// Helper: Create test config file
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
std::string createTestConfigFile(const json& config) {
|
std::string createTestConfigFile(const json& config) {
|
||||||
std::string path = "test_mcp_config.json";
|
std::string path = "test_mcp_config.json";
|
||||||
std::ofstream file(path);
|
std::ofstream file(path);
|
||||||
file << config.dump(2);
|
file << config.dump(2);
|
||||||
file.close();
|
file.close();
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
void cleanupTestConfigFile(const std::string& path) {
|
void cleanupTestConfigFile(const std::string& path) {
|
||||||
std::filesystem::remove(path);
|
std::filesystem::remove(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_CLIENT_001: Load config valid
|
// TI_CLIENT_001: Load config valid
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_CLIENT_001_LoadConfigValid", "[mcp][client]") {
|
TEST_CASE("TI_CLIENT_001_LoadConfigValid", "[mcp][client]") {
|
||||||
json config = {
|
json config = {
|
||||||
{"servers", {
|
{"servers", {
|
||||||
{"test_server", {
|
{"test_server", {
|
||||||
{"command", "C:\\Users\\alexi\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"},
|
{"command", "C:\\Users\\alexi\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"},
|
||||||
{"args", json::array({"server.py"})},
|
{"args", json::array({"server.py"})},
|
||||||
{"enabled", true}
|
{"enabled", true}
|
||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
};
|
};
|
||||||
auto path = createTestConfigFile(config);
|
auto path = createTestConfigFile(config);
|
||||||
|
|
||||||
MCPClient client;
|
MCPClient client;
|
||||||
bool loaded = client.loadConfig(path);
|
bool loaded = client.loadConfig(path);
|
||||||
|
|
||||||
REQUIRE(loaded == true);
|
REQUIRE(loaded == true);
|
||||||
|
|
||||||
cleanupTestConfigFile(path);
|
cleanupTestConfigFile(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_CLIENT_002: Load config invalid
|
// TI_CLIENT_002: Load config invalid
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_CLIENT_002_LoadConfigInvalid", "[mcp][client]") {
|
TEST_CASE("TI_CLIENT_002_LoadConfigInvalid", "[mcp][client]") {
|
||||||
// Create file with invalid JSON
|
// Create file with invalid JSON
|
||||||
std::string path = "invalid_config.json";
|
std::string path = "invalid_config.json";
|
||||||
std::ofstream file(path);
|
std::ofstream file(path);
|
||||||
file << "{ invalid json }";
|
file << "{ invalid json }";
|
||||||
file.close();
|
file.close();
|
||||||
|
|
||||||
MCPClient client;
|
MCPClient client;
|
||||||
bool loaded = client.loadConfig(path);
|
bool loaded = client.loadConfig(path);
|
||||||
|
|
||||||
REQUIRE(loaded == false);
|
REQUIRE(loaded == false);
|
||||||
|
|
||||||
cleanupTestConfigFile(path);
|
cleanupTestConfigFile(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_CLIENT_003: Load config missing file
|
// TI_CLIENT_003: Load config missing file
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_CLIENT_003_LoadConfigMissingFile", "[mcp][client]") {
|
TEST_CASE("TI_CLIENT_003_LoadConfigMissingFile", "[mcp][client]") {
|
||||||
MCPClient client;
|
MCPClient client;
|
||||||
bool loaded = client.loadConfig("nonexistent_file.json");
|
bool loaded = client.loadConfig("nonexistent_file.json");
|
||||||
|
|
||||||
REQUIRE(loaded == false);
|
REQUIRE(loaded == false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_CLIENT_004: ConnectAll starts servers
|
// TI_CLIENT_004: ConnectAll starts servers
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_CLIENT_004_ConnectAllStartsServers", "[mcp][client]") {
|
TEST_CASE("TI_CLIENT_004_ConnectAllStartsServers", "[mcp][client]") {
|
||||||
// Use the real mock MCP server fixture
|
// Use the real mock MCP server fixture
|
||||||
MCPClient client;
|
MCPClient client;
|
||||||
bool loaded = client.loadConfig("tests/fixtures/mock_mcp.json");
|
bool loaded = client.loadConfig("tests/fixtures/mock_mcp.json");
|
||||||
|
|
||||||
if (loaded) {
|
if (loaded) {
|
||||||
int connected = client.connectAll();
|
int connected = client.connectAll();
|
||||||
// Should connect to enabled servers
|
// Should connect to enabled servers
|
||||||
REQUIRE(connected >= 0);
|
REQUIRE(connected >= 0);
|
||||||
client.disconnectAll();
|
client.disconnectAll();
|
||||||
} else {
|
} else {
|
||||||
// Skip if fixture not available
|
// Skip if fixture not available
|
||||||
SUCCEED();
|
SUCCEED();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_CLIENT_005: ConnectAll skips disabled
|
// TI_CLIENT_005: ConnectAll skips disabled
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_CLIENT_005_ConnectAllSkipsDisabled", "[mcp][client]") {
|
TEST_CASE("TI_CLIENT_005_ConnectAllSkipsDisabled", "[mcp][client]") {
|
||||||
json config = {
|
json config = {
|
||||||
{"servers", {
|
{"servers", {
|
||||||
{"enabled_server", {
|
{"enabled_server", {
|
||||||
{"command", "C:\\Users\\alexi\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"},
|
{"command", "C:\\Users\\alexi\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"},
|
||||||
{"args", json::array({"tests/fixtures/echo_server.py"})},
|
{"args", json::array({"tests/fixtures/echo_server.py"})},
|
||||||
{"enabled", true}
|
{"enabled", true}
|
||||||
}},
|
}},
|
||||||
{"disabled_server", {
|
{"disabled_server", {
|
||||||
{"command", "nonexistent"},
|
{"command", "nonexistent"},
|
||||||
{"enabled", false}
|
{"enabled", false}
|
||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
};
|
};
|
||||||
auto path = createTestConfigFile(config);
|
auto path = createTestConfigFile(config);
|
||||||
|
|
||||||
MCPClient client;
|
MCPClient client;
|
||||||
client.loadConfig(path);
|
client.loadConfig(path);
|
||||||
int connected = client.connectAll();
|
int connected = client.connectAll();
|
||||||
|
|
||||||
// disabled_server should not be connected
|
// disabled_server should not be connected
|
||||||
REQUIRE(client.isConnected("disabled_server") == false);
|
REQUIRE(client.isConnected("disabled_server") == false);
|
||||||
|
|
||||||
client.disconnectAll();
|
client.disconnectAll();
|
||||||
cleanupTestConfigFile(path);
|
cleanupTestConfigFile(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_CLIENT_006: Connect single server
|
// TI_CLIENT_006: Connect single server
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_CLIENT_006_ConnectSingleServer", "[mcp][client]") {
|
TEST_CASE("TI_CLIENT_006_ConnectSingleServer", "[mcp][client]") {
|
||||||
json config = {
|
json config = {
|
||||||
{"servers", {
|
{"servers", {
|
||||||
{"server1", {
|
{"server1", {
|
||||||
{"command", "C:\\Users\\alexi\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"},
|
{"command", "C:\\Users\\alexi\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"},
|
||||||
{"args", json::array({"tests/fixtures/echo_server.py"})},
|
{"args", json::array({"tests/fixtures/echo_server.py"})},
|
||||||
{"enabled", true}
|
{"enabled", true}
|
||||||
}},
|
}},
|
||||||
{"server2", {
|
{"server2", {
|
||||||
{"command", "C:\\Users\\alexi\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"},
|
{"command", "C:\\Users\\alexi\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"},
|
||||||
{"args", json::array({"tests/fixtures/echo_server.py"})},
|
{"args", json::array({"tests/fixtures/echo_server.py"})},
|
||||||
{"enabled", true}
|
{"enabled", true}
|
||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
};
|
};
|
||||||
auto path = createTestConfigFile(config);
|
auto path = createTestConfigFile(config);
|
||||||
|
|
||||||
MCPClient client;
|
MCPClient client;
|
||||||
client.loadConfig(path);
|
client.loadConfig(path);
|
||||||
|
|
||||||
// Connect only server1
|
// Connect only server1
|
||||||
bool connected = client.connect("server1");
|
bool connected = client.connect("server1");
|
||||||
|
|
||||||
REQUIRE(connected == true);
|
REQUIRE(connected == true);
|
||||||
REQUIRE(client.isConnected("server1") == true);
|
REQUIRE(client.isConnected("server1") == true);
|
||||||
REQUIRE(client.isConnected("server2") == false);
|
REQUIRE(client.isConnected("server2") == false);
|
||||||
|
|
||||||
client.disconnectAll();
|
client.disconnectAll();
|
||||||
cleanupTestConfigFile(path);
|
cleanupTestConfigFile(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_CLIENT_007: Disconnect single server
|
// TI_CLIENT_007: Disconnect single server
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_CLIENT_007_DisconnectSingleServer", "[mcp][client]") {
|
TEST_CASE("TI_CLIENT_007_DisconnectSingleServer", "[mcp][client]") {
|
||||||
json config = {
|
json config = {
|
||||||
{"servers", {
|
{"servers", {
|
||||||
{"server1", {
|
{"server1", {
|
||||||
{"command", "C:\\Users\\alexi\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"},
|
{"command", "C:\\Users\\alexi\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"},
|
||||||
{"args", json::array({"tests/fixtures/echo_server.py"})},
|
{"args", json::array({"tests/fixtures/echo_server.py"})},
|
||||||
{"enabled", true}
|
{"enabled", true}
|
||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
};
|
};
|
||||||
auto path = createTestConfigFile(config);
|
auto path = createTestConfigFile(config);
|
||||||
|
|
||||||
MCPClient client;
|
MCPClient client;
|
||||||
client.loadConfig(path);
|
client.loadConfig(path);
|
||||||
client.connect("server1");
|
client.connect("server1");
|
||||||
REQUIRE(client.isConnected("server1") == true);
|
REQUIRE(client.isConnected("server1") == true);
|
||||||
|
|
||||||
client.disconnect("server1");
|
client.disconnect("server1");
|
||||||
REQUIRE(client.isConnected("server1") == false);
|
REQUIRE(client.isConnected("server1") == false);
|
||||||
|
|
||||||
cleanupTestConfigFile(path);
|
cleanupTestConfigFile(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_CLIENT_008: DisconnectAll cleans up
|
// TI_CLIENT_008: DisconnectAll cleans up
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_CLIENT_008_DisconnectAllCleansUp", "[mcp][client]") {
|
TEST_CASE("TI_CLIENT_008_DisconnectAllCleansUp", "[mcp][client]") {
|
||||||
json config = {
|
json config = {
|
||||||
{"servers", {
|
{"servers", {
|
||||||
{"server1", {
|
{"server1", {
|
||||||
{"command", "C:\\Users\\alexi\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"},
|
{"command", "C:\\Users\\alexi\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"},
|
||||||
{"args", json::array({"tests/fixtures/echo_server.py"})},
|
{"args", json::array({"tests/fixtures/echo_server.py"})},
|
||||||
{"enabled", true}
|
{"enabled", true}
|
||||||
}},
|
}},
|
||||||
{"server2", {
|
{"server2", {
|
||||||
{"command", "C:\\Users\\alexi\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"},
|
{"command", "C:\\Users\\alexi\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"},
|
||||||
{"args", json::array({"tests/fixtures/echo_server.py"})},
|
{"args", json::array({"tests/fixtures/echo_server.py"})},
|
||||||
{"enabled", true}
|
{"enabled", true}
|
||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
};
|
};
|
||||||
auto path = createTestConfigFile(config);
|
auto path = createTestConfigFile(config);
|
||||||
|
|
||||||
MCPClient client;
|
MCPClient client;
|
||||||
client.loadConfig(path);
|
client.loadConfig(path);
|
||||||
client.connectAll();
|
client.connectAll();
|
||||||
|
|
||||||
client.disconnectAll();
|
client.disconnectAll();
|
||||||
|
|
||||||
REQUIRE(client.isConnected("server1") == false);
|
REQUIRE(client.isConnected("server1") == false);
|
||||||
REQUIRE(client.isConnected("server2") == false);
|
REQUIRE(client.isConnected("server2") == false);
|
||||||
REQUIRE(client.getConnectedServers().empty() == true);
|
REQUIRE(client.getConnectedServers().empty() == true);
|
||||||
|
|
||||||
cleanupTestConfigFile(path);
|
cleanupTestConfigFile(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_CLIENT_009: ListAllTools aggregates
|
// TI_CLIENT_009: ListAllTools aggregates
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_CLIENT_009_ListAllToolsAggregates", "[mcp][client]") {
|
TEST_CASE("TI_CLIENT_009_ListAllToolsAggregates", "[mcp][client]") {
|
||||||
MCPClient client;
|
MCPClient client;
|
||||||
bool loaded = client.loadConfig("tests/fixtures/mock_mcp.json");
|
bool loaded = client.loadConfig("tests/fixtures/mock_mcp.json");
|
||||||
|
|
||||||
if (loaded) {
|
if (loaded) {
|
||||||
client.connectAll();
|
client.connectAll();
|
||||||
auto tools = client.listAllTools();
|
auto tools = client.listAllTools();
|
||||||
|
|
||||||
// Should have tools from mock server
|
// Should have tools from mock server
|
||||||
REQUIRE(tools.size() >= 0);
|
REQUIRE(tools.size() >= 0);
|
||||||
|
|
||||||
client.disconnectAll();
|
client.disconnectAll();
|
||||||
} else {
|
} else {
|
||||||
SUCCEED();
|
SUCCEED();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_CLIENT_010: Tool name prefixed
|
// TI_CLIENT_010: Tool name prefixed
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_CLIENT_010_ToolNamePrefixed", "[mcp][client]") {
|
TEST_CASE("TI_CLIENT_010_ToolNamePrefixed", "[mcp][client]") {
|
||||||
MCPClient client;
|
MCPClient client;
|
||||||
bool loaded = client.loadConfig("tests/fixtures/mock_mcp.json");
|
bool loaded = client.loadConfig("tests/fixtures/mock_mcp.json");
|
||||||
|
|
||||||
if (loaded) {
|
if (loaded) {
|
||||||
client.connectAll();
|
client.connectAll();
|
||||||
auto tools = client.listAllTools();
|
auto tools = client.listAllTools();
|
||||||
|
|
||||||
bool hasPrefix = false;
|
bool hasPrefix = false;
|
||||||
for (const auto& tool : tools) {
|
for (const auto& tool : tools) {
|
||||||
if (tool.name.find(":") != std::string::npos) {
|
if (tool.name.find(":") != std::string::npos) {
|
||||||
hasPrefix = true;
|
hasPrefix = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tools.empty()) {
|
if (!tools.empty()) {
|
||||||
REQUIRE(hasPrefix == true);
|
REQUIRE(hasPrefix == true);
|
||||||
}
|
}
|
||||||
|
|
||||||
client.disconnectAll();
|
client.disconnectAll();
|
||||||
} else {
|
} else {
|
||||||
SUCCEED();
|
SUCCEED();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_CLIENT_011: CallTool routes to server
|
// TI_CLIENT_011: CallTool routes to server
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_CLIENT_011_CallToolRoutesToServer", "[mcp][client]") {
|
TEST_CASE("TI_CLIENT_011_CallToolRoutesToServer", "[mcp][client]") {
|
||||||
MCPClient client;
|
MCPClient client;
|
||||||
bool loaded = client.loadConfig("tests/fixtures/mock_mcp.json");
|
bool loaded = client.loadConfig("tests/fixtures/mock_mcp.json");
|
||||||
|
|
||||||
if (loaded) {
|
if (loaded) {
|
||||||
client.connectAll();
|
client.connectAll();
|
||||||
auto tools = client.listAllTools();
|
auto tools = client.listAllTools();
|
||||||
|
|
||||||
if (!tools.empty()) {
|
if (!tools.empty()) {
|
||||||
// Call the first available tool
|
// Call the first available tool
|
||||||
auto result = client.callTool(tools[0].name, json::object());
|
auto result = client.callTool(tools[0].name, json::object());
|
||||||
// Should get some result (success or error)
|
// Should get some result (success or error)
|
||||||
REQUIRE(result.content.size() >= 0);
|
REQUIRE(result.content.size() >= 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
client.disconnectAll();
|
client.disconnectAll();
|
||||||
} else {
|
} else {
|
||||||
SUCCEED();
|
SUCCEED();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_CLIENT_012: CallTool invalid name
|
// TI_CLIENT_012: CallTool invalid name
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_CLIENT_012_CallToolInvalidName", "[mcp][client]") {
|
TEST_CASE("TI_CLIENT_012_CallToolInvalidName", "[mcp][client]") {
|
||||||
MCPClient client;
|
MCPClient client;
|
||||||
client.loadConfig("tests/fixtures/mock_mcp.json");
|
client.loadConfig("tests/fixtures/mock_mcp.json");
|
||||||
client.connectAll();
|
client.connectAll();
|
||||||
|
|
||||||
auto result = client.callTool("nonexistent:tool", json::object());
|
auto result = client.callTool("nonexistent:tool", json::object());
|
||||||
|
|
||||||
REQUIRE(result.isError == true);
|
REQUIRE(result.isError == true);
|
||||||
|
|
||||||
client.disconnectAll();
|
client.disconnectAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_CLIENT_013: CallTool disconnected server
|
// TI_CLIENT_013: CallTool disconnected server
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_CLIENT_013_CallToolDisconnectedServer", "[mcp][client]") {
|
TEST_CASE("TI_CLIENT_013_CallToolDisconnectedServer", "[mcp][client]") {
|
||||||
MCPClient client;
|
MCPClient client;
|
||||||
// Don't connect any servers
|
// Don't connect any servers
|
||||||
|
|
||||||
auto result = client.callTool("server:tool", json::object());
|
auto result = client.callTool("server:tool", json::object());
|
||||||
|
|
||||||
REQUIRE(result.isError == true);
|
REQUIRE(result.isError == true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_CLIENT_014: ToolCount accurate
|
// TI_CLIENT_014: ToolCount accurate
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_CLIENT_014_ToolCountAccurate", "[mcp][client]") {
|
TEST_CASE("TI_CLIENT_014_ToolCountAccurate", "[mcp][client]") {
|
||||||
MCPClient client;
|
MCPClient client;
|
||||||
bool loaded = client.loadConfig("tests/fixtures/mock_mcp.json");
|
bool loaded = client.loadConfig("tests/fixtures/mock_mcp.json");
|
||||||
|
|
||||||
if (loaded) {
|
if (loaded) {
|
||||||
client.connectAll();
|
client.connectAll();
|
||||||
|
|
||||||
size_t count = client.toolCount();
|
size_t count = client.toolCount();
|
||||||
auto tools = client.listAllTools();
|
auto tools = client.listAllTools();
|
||||||
|
|
||||||
REQUIRE(count == tools.size());
|
REQUIRE(count == tools.size());
|
||||||
|
|
||||||
client.disconnectAll();
|
client.disconnectAll();
|
||||||
} else {
|
} else {
|
||||||
SUCCEED();
|
SUCCEED();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_CLIENT_015: IsConnected accurate
|
// TI_CLIENT_015: IsConnected accurate
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_CLIENT_015_IsConnectedAccurate", "[mcp][client]") {
|
TEST_CASE("TI_CLIENT_015_IsConnectedAccurate", "[mcp][client]") {
|
||||||
json config = {
|
json config = {
|
||||||
{"servers", {
|
{"servers", {
|
||||||
{"test_server", {
|
{"test_server", {
|
||||||
{"command", "C:\\Users\\alexi\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"},
|
{"command", "C:\\Users\\alexi\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"},
|
||||||
{"args", json::array({"tests/fixtures/echo_server.py"})},
|
{"args", json::array({"tests/fixtures/echo_server.py"})},
|
||||||
{"enabled", true}
|
{"enabled", true}
|
||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
};
|
};
|
||||||
auto path = createTestConfigFile(config);
|
auto path = createTestConfigFile(config);
|
||||||
|
|
||||||
MCPClient client;
|
MCPClient client;
|
||||||
client.loadConfig(path);
|
client.loadConfig(path);
|
||||||
|
|
||||||
// Not connected yet
|
// Not connected yet
|
||||||
REQUIRE(client.isConnected("test_server") == false);
|
REQUIRE(client.isConnected("test_server") == false);
|
||||||
|
|
||||||
// Connect
|
// Connect
|
||||||
client.connect("test_server");
|
client.connect("test_server");
|
||||||
REQUIRE(client.isConnected("test_server") == true);
|
REQUIRE(client.isConnected("test_server") == true);
|
||||||
|
|
||||||
// Disconnect
|
// Disconnect
|
||||||
client.disconnect("test_server");
|
client.disconnect("test_server");
|
||||||
REQUIRE(client.isConnected("test_server") == false);
|
REQUIRE(client.isConnected("test_server") == false);
|
||||||
|
|
||||||
cleanupTestConfigFile(path);
|
cleanupTestConfigFile(path);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,298 +1,298 @@
|
|||||||
/**
|
/**
|
||||||
* @file MCPTypesTests.cpp
|
* @file MCPTypesTests.cpp
|
||||||
* @brief Integration tests for MCP Types (15 TI)
|
* @brief Integration tests for MCP Types (15 TI)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <catch2/catch_test_macros.hpp>
|
#include <catch2/catch_test_macros.hpp>
|
||||||
#include "shared/mcp/MCPTypes.hpp"
|
#include "shared/mcp/MCPTypes.hpp"
|
||||||
|
|
||||||
using namespace aissia::mcp;
|
using namespace aissia::mcp;
|
||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_TYPES_001: MCPTool toJson
|
// TI_TYPES_001: MCPTool toJson
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_TYPES_001_MCPToolToJson", "[mcp][types]") {
|
TEST_CASE("TI_TYPES_001_MCPToolToJson", "[mcp][types]") {
|
||||||
MCPTool tool;
|
MCPTool tool;
|
||||||
tool.name = "read_file";
|
tool.name = "read_file";
|
||||||
tool.description = "Read a file from the filesystem";
|
tool.description = "Read a file from the filesystem";
|
||||||
tool.inputSchema = {
|
tool.inputSchema = {
|
||||||
{"type", "object"},
|
{"type", "object"},
|
||||||
{"properties", {
|
{"properties", {
|
||||||
{"path", {{"type", "string"}}}
|
{"path", {{"type", "string"}}}
|
||||||
}},
|
}},
|
||||||
{"required", json::array({"path"})}
|
{"required", json::array({"path"})}
|
||||||
};
|
};
|
||||||
|
|
||||||
json j = tool.toJson();
|
json j = tool.toJson();
|
||||||
|
|
||||||
REQUIRE(j["name"] == "read_file");
|
REQUIRE(j["name"] == "read_file");
|
||||||
REQUIRE(j["description"] == "Read a file from the filesystem");
|
REQUIRE(j["description"] == "Read a file from the filesystem");
|
||||||
REQUIRE(j["inputSchema"]["type"] == "object");
|
REQUIRE(j["inputSchema"]["type"] == "object");
|
||||||
REQUIRE(j["inputSchema"]["properties"]["path"]["type"] == "string");
|
REQUIRE(j["inputSchema"]["properties"]["path"]["type"] == "string");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_TYPES_002: MCPTool fromJson
|
// TI_TYPES_002: MCPTool fromJson
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_TYPES_002_MCPToolFromJson", "[mcp][types]") {
|
TEST_CASE("TI_TYPES_002_MCPToolFromJson", "[mcp][types]") {
|
||||||
json j = {
|
json j = {
|
||||||
{"name", "write_file"},
|
{"name", "write_file"},
|
||||||
{"description", "Write content to a file"},
|
{"description", "Write content to a file"},
|
||||||
{"inputSchema", {
|
{"inputSchema", {
|
||||||
{"type", "object"},
|
{"type", "object"},
|
||||||
{"properties", {
|
{"properties", {
|
||||||
{"path", {{"type", "string"}}},
|
{"path", {{"type", "string"}}},
|
||||||
{"content", {{"type", "string"}}}
|
{"content", {{"type", "string"}}}
|
||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
};
|
};
|
||||||
|
|
||||||
auto tool = MCPTool::fromJson(j);
|
auto tool = MCPTool::fromJson(j);
|
||||||
|
|
||||||
REQUIRE(tool.name == "write_file");
|
REQUIRE(tool.name == "write_file");
|
||||||
REQUIRE(tool.description == "Write content to a file");
|
REQUIRE(tool.description == "Write content to a file");
|
||||||
REQUIRE(tool.inputSchema["type"] == "object");
|
REQUIRE(tool.inputSchema["type"] == "object");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_TYPES_003: MCPTool fromJson with missing fields
|
// TI_TYPES_003: MCPTool fromJson with missing fields
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_TYPES_003_MCPToolFromJsonMissingFields", "[mcp][types]") {
|
TEST_CASE("TI_TYPES_003_MCPToolFromJsonMissingFields", "[mcp][types]") {
|
||||||
json j = {{"name", "minimal_tool"}};
|
json j = {{"name", "minimal_tool"}};
|
||||||
|
|
||||||
auto tool = MCPTool::fromJson(j);
|
auto tool = MCPTool::fromJson(j);
|
||||||
|
|
||||||
REQUIRE(tool.name == "minimal_tool");
|
REQUIRE(tool.name == "minimal_tool");
|
||||||
REQUIRE(tool.description == "");
|
REQUIRE(tool.description == "");
|
||||||
REQUIRE(tool.inputSchema.is_object());
|
REQUIRE(tool.inputSchema.is_object());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_TYPES_004: MCPResource fromJson
|
// TI_TYPES_004: MCPResource fromJson
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_TYPES_004_MCPResourceFromJson", "[mcp][types]") {
|
TEST_CASE("TI_TYPES_004_MCPResourceFromJson", "[mcp][types]") {
|
||||||
json j = {
|
json j = {
|
||||||
{"uri", "file:///home/user/doc.txt"},
|
{"uri", "file:///home/user/doc.txt"},
|
||||||
{"name", "Document"},
|
{"name", "Document"},
|
||||||
{"description", "A text document"},
|
{"description", "A text document"},
|
||||||
{"mimeType", "text/plain"}
|
{"mimeType", "text/plain"}
|
||||||
};
|
};
|
||||||
|
|
||||||
auto resource = MCPResource::fromJson(j);
|
auto resource = MCPResource::fromJson(j);
|
||||||
|
|
||||||
REQUIRE(resource.uri == "file:///home/user/doc.txt");
|
REQUIRE(resource.uri == "file:///home/user/doc.txt");
|
||||||
REQUIRE(resource.name == "Document");
|
REQUIRE(resource.name == "Document");
|
||||||
REQUIRE(resource.description == "A text document");
|
REQUIRE(resource.description == "A text document");
|
||||||
REQUIRE(resource.mimeType == "text/plain");
|
REQUIRE(resource.mimeType == "text/plain");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_TYPES_005: MCPToolResult toJson
|
// TI_TYPES_005: MCPToolResult toJson
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_TYPES_005_MCPToolResultToJson", "[mcp][types]") {
|
TEST_CASE("TI_TYPES_005_MCPToolResultToJson", "[mcp][types]") {
|
||||||
MCPToolResult result;
|
MCPToolResult result;
|
||||||
result.content = {
|
result.content = {
|
||||||
{{"type", "text"}, {"text", "File contents here"}},
|
{{"type", "text"}, {"text", "File contents here"}},
|
||||||
{{"type", "text"}, {"text", "More content"}}
|
{{"type", "text"}, {"text", "More content"}}
|
||||||
};
|
};
|
||||||
result.isError = false;
|
result.isError = false;
|
||||||
|
|
||||||
json j = result.toJson();
|
json j = result.toJson();
|
||||||
|
|
||||||
REQUIRE(j["content"].size() == 2);
|
REQUIRE(j["content"].size() == 2);
|
||||||
REQUIRE(j["content"][0]["type"] == "text");
|
REQUIRE(j["content"][0]["type"] == "text");
|
||||||
REQUIRE(j["isError"] == false);
|
REQUIRE(j["isError"] == false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_TYPES_006: MCPCapabilities fromJson
|
// TI_TYPES_006: MCPCapabilities fromJson
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_TYPES_006_MCPCapabilitiesFromJson", "[mcp][types]") {
|
TEST_CASE("TI_TYPES_006_MCPCapabilitiesFromJson", "[mcp][types]") {
|
||||||
json j = {
|
json j = {
|
||||||
{"tools", json::object()},
|
{"tools", json::object()},
|
||||||
{"resources", {{"subscribe", true}}},
|
{"resources", {{"subscribe", true}}},
|
||||||
{"prompts", json::object()}
|
{"prompts", json::object()}
|
||||||
};
|
};
|
||||||
|
|
||||||
auto caps = MCPCapabilities::fromJson(j);
|
auto caps = MCPCapabilities::fromJson(j);
|
||||||
|
|
||||||
REQUIRE(caps.hasTools == true);
|
REQUIRE(caps.hasTools == true);
|
||||||
REQUIRE(caps.hasResources == true);
|
REQUIRE(caps.hasResources == true);
|
||||||
REQUIRE(caps.hasPrompts == true);
|
REQUIRE(caps.hasPrompts == true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_TYPES_007: MCPCapabilities empty
|
// TI_TYPES_007: MCPCapabilities empty
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_TYPES_007_MCPCapabilitiesEmpty", "[mcp][types]") {
|
TEST_CASE("TI_TYPES_007_MCPCapabilitiesEmpty", "[mcp][types]") {
|
||||||
json j = json::object();
|
json j = json::object();
|
||||||
|
|
||||||
auto caps = MCPCapabilities::fromJson(j);
|
auto caps = MCPCapabilities::fromJson(j);
|
||||||
|
|
||||||
REQUIRE(caps.hasTools == false);
|
REQUIRE(caps.hasTools == false);
|
||||||
REQUIRE(caps.hasResources == false);
|
REQUIRE(caps.hasResources == false);
|
||||||
REQUIRE(caps.hasPrompts == false);
|
REQUIRE(caps.hasPrompts == false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_TYPES_008: MCPServerInfo fromJson
|
// TI_TYPES_008: MCPServerInfo fromJson
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_TYPES_008_MCPServerInfoFromJson", "[mcp][types]") {
|
TEST_CASE("TI_TYPES_008_MCPServerInfoFromJson", "[mcp][types]") {
|
||||||
json j = {
|
json j = {
|
||||||
{"name", "filesystem-server"},
|
{"name", "filesystem-server"},
|
||||||
{"version", "1.2.3"},
|
{"version", "1.2.3"},
|
||||||
{"capabilities", {
|
{"capabilities", {
|
||||||
{"tools", json::object()}
|
{"tools", json::object()}
|
||||||
}}
|
}}
|
||||||
};
|
};
|
||||||
|
|
||||||
auto info = MCPServerInfo::fromJson(j);
|
auto info = MCPServerInfo::fromJson(j);
|
||||||
|
|
||||||
REQUIRE(info.name == "filesystem-server");
|
REQUIRE(info.name == "filesystem-server");
|
||||||
REQUIRE(info.version == "1.2.3");
|
REQUIRE(info.version == "1.2.3");
|
||||||
REQUIRE(info.capabilities.hasTools == true);
|
REQUIRE(info.capabilities.hasTools == true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_TYPES_009: JsonRpcRequest toJson
|
// TI_TYPES_009: JsonRpcRequest toJson
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_TYPES_009_JsonRpcRequestToJson", "[mcp][types]") {
|
TEST_CASE("TI_TYPES_009_JsonRpcRequestToJson", "[mcp][types]") {
|
||||||
JsonRpcRequest request;
|
JsonRpcRequest request;
|
||||||
request.id = 42;
|
request.id = 42;
|
||||||
request.method = "tools/call";
|
request.method = "tools/call";
|
||||||
request.params = {{"name", "read_file"}, {"arguments", {{"path", "/tmp/test"}}}};
|
request.params = {{"name", "read_file"}, {"arguments", {{"path", "/tmp/test"}}}};
|
||||||
|
|
||||||
json j = request.toJson();
|
json j = request.toJson();
|
||||||
|
|
||||||
REQUIRE(j["jsonrpc"] == "2.0");
|
REQUIRE(j["jsonrpc"] == "2.0");
|
||||||
REQUIRE(j["id"] == 42);
|
REQUIRE(j["id"] == 42);
|
||||||
REQUIRE(j["method"] == "tools/call");
|
REQUIRE(j["method"] == "tools/call");
|
||||||
REQUIRE(j["params"]["name"] == "read_file");
|
REQUIRE(j["params"]["name"] == "read_file");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_TYPES_010: JsonRpcResponse fromJson
|
// TI_TYPES_010: JsonRpcResponse fromJson
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_TYPES_010_JsonRpcResponseFromJson", "[mcp][types]") {
|
TEST_CASE("TI_TYPES_010_JsonRpcResponseFromJson", "[mcp][types]") {
|
||||||
json j = {
|
json j = {
|
||||||
{"jsonrpc", "2.0"},
|
{"jsonrpc", "2.0"},
|
||||||
{"id", 42},
|
{"id", 42},
|
||||||
{"result", {{"tools", json::array()}}}
|
{"result", {{"tools", json::array()}}}
|
||||||
};
|
};
|
||||||
|
|
||||||
auto response = JsonRpcResponse::fromJson(j);
|
auto response = JsonRpcResponse::fromJson(j);
|
||||||
|
|
||||||
REQUIRE(response.jsonrpc == "2.0");
|
REQUIRE(response.jsonrpc == "2.0");
|
||||||
REQUIRE(response.id == 42);
|
REQUIRE(response.id == 42);
|
||||||
REQUIRE(response.result.has_value());
|
REQUIRE(response.result.has_value());
|
||||||
REQUIRE(response.result.value()["tools"].is_array());
|
REQUIRE(response.result.value()["tools"].is_array());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_TYPES_011: JsonRpcResponse isError
|
// TI_TYPES_011: JsonRpcResponse isError
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_TYPES_011_JsonRpcResponseIsError", "[mcp][types]") {
|
TEST_CASE("TI_TYPES_011_JsonRpcResponseIsError", "[mcp][types]") {
|
||||||
json errorJson = {
|
json errorJson = {
|
||||||
{"jsonrpc", "2.0"},
|
{"jsonrpc", "2.0"},
|
||||||
{"id", 1},
|
{"id", 1},
|
||||||
{"error", {{"code", -32600}, {"message", "Invalid Request"}}}
|
{"error", {{"code", -32600}, {"message", "Invalid Request"}}}
|
||||||
};
|
};
|
||||||
|
|
||||||
auto response = JsonRpcResponse::fromJson(errorJson);
|
auto response = JsonRpcResponse::fromJson(errorJson);
|
||||||
|
|
||||||
REQUIRE(response.isError() == true);
|
REQUIRE(response.isError() == true);
|
||||||
REQUIRE(response.error.has_value());
|
REQUIRE(response.error.has_value());
|
||||||
REQUIRE(response.error.value()["code"] == -32600);
|
REQUIRE(response.error.value()["code"] == -32600);
|
||||||
REQUIRE(response.error.value()["message"] == "Invalid Request");
|
REQUIRE(response.error.value()["message"] == "Invalid Request");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_TYPES_012: MCPServerConfig fromJson
|
// TI_TYPES_012: MCPServerConfig fromJson
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_TYPES_012_MCPServerConfigFromJson", "[mcp][types]") {
|
TEST_CASE("TI_TYPES_012_MCPServerConfigFromJson", "[mcp][types]") {
|
||||||
json j = {
|
json j = {
|
||||||
{"command", "mcp-server-filesystem"},
|
{"command", "mcp-server-filesystem"},
|
||||||
{"args", json::array({"--root", "/home"})},
|
{"args", json::array({"--root", "/home"})},
|
||||||
{"env", {{"DEBUG", "true"}}},
|
{"env", {{"DEBUG", "true"}}},
|
||||||
{"enabled", true}
|
{"enabled", true}
|
||||||
};
|
};
|
||||||
|
|
||||||
auto config = MCPServerConfig::fromJson("filesystem", j);
|
auto config = MCPServerConfig::fromJson("filesystem", j);
|
||||||
|
|
||||||
REQUIRE(config.name == "filesystem");
|
REQUIRE(config.name == "filesystem");
|
||||||
REQUIRE(config.command == "mcp-server-filesystem");
|
REQUIRE(config.command == "mcp-server-filesystem");
|
||||||
REQUIRE(config.args.size() == 2);
|
REQUIRE(config.args.size() == 2);
|
||||||
REQUIRE(config.args[0] == "--root");
|
REQUIRE(config.args[0] == "--root");
|
||||||
REQUIRE(config.args[1] == "/home");
|
REQUIRE(config.args[1] == "/home");
|
||||||
REQUIRE(config.env["DEBUG"] == "true");
|
REQUIRE(config.env["DEBUG"] == "true");
|
||||||
REQUIRE(config.enabled == true);
|
REQUIRE(config.enabled == true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_TYPES_013: MCPServerConfig env expansion
|
// TI_TYPES_013: MCPServerConfig env expansion
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_TYPES_013_MCPServerConfigEnvExpansion", "[mcp][types]") {
|
TEST_CASE("TI_TYPES_013_MCPServerConfigEnvExpansion", "[mcp][types]") {
|
||||||
json j = {
|
json j = {
|
||||||
{"command", "mcp-server"},
|
{"command", "mcp-server"},
|
||||||
{"env", {{"API_KEY", "${MY_API_KEY}"}}}
|
{"env", {{"API_KEY", "${MY_API_KEY}"}}}
|
||||||
};
|
};
|
||||||
|
|
||||||
auto config = MCPServerConfig::fromJson("test", j);
|
auto config = MCPServerConfig::fromJson("test", j);
|
||||||
|
|
||||||
// Note: Actual env expansion happens in MCPClient, not in fromJson
|
// Note: Actual env expansion happens in MCPClient, not in fromJson
|
||||||
// This test verifies the raw value is stored
|
// This test verifies the raw value is stored
|
||||||
REQUIRE(config.env["API_KEY"] == "${MY_API_KEY}");
|
REQUIRE(config.env["API_KEY"] == "${MY_API_KEY}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_TYPES_014: MCPServerConfig disabled
|
// TI_TYPES_014: MCPServerConfig disabled
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_TYPES_014_MCPServerConfigDisabled", "[mcp][types]") {
|
TEST_CASE("TI_TYPES_014_MCPServerConfigDisabled", "[mcp][types]") {
|
||||||
json j = {
|
json j = {
|
||||||
{"command", "some-server"},
|
{"command", "some-server"},
|
||||||
{"enabled", false}
|
{"enabled", false}
|
||||||
};
|
};
|
||||||
|
|
||||||
auto config = MCPServerConfig::fromJson("disabled_server", j);
|
auto config = MCPServerConfig::fromJson("disabled_server", j);
|
||||||
|
|
||||||
REQUIRE(config.enabled == false);
|
REQUIRE(config.enabled == false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_TYPES_015: JsonRpcRequest ID increment
|
// TI_TYPES_015: JsonRpcRequest ID increment
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_TYPES_015_JsonRpcRequestIdIncrement", "[mcp][types]") {
|
TEST_CASE("TI_TYPES_015_JsonRpcRequestIdIncrement", "[mcp][types]") {
|
||||||
JsonRpcRequest req1;
|
JsonRpcRequest req1;
|
||||||
req1.id = 1;
|
req1.id = 1;
|
||||||
req1.method = "test";
|
req1.method = "test";
|
||||||
|
|
||||||
JsonRpcRequest req2;
|
JsonRpcRequest req2;
|
||||||
req2.id = 2;
|
req2.id = 2;
|
||||||
req2.method = "test";
|
req2.method = "test";
|
||||||
|
|
||||||
// IDs should be different
|
// IDs should be different
|
||||||
REQUIRE(req1.id != req2.id);
|
REQUIRE(req1.id != req2.id);
|
||||||
|
|
||||||
// Both should serialize correctly
|
// Both should serialize correctly
|
||||||
json j1 = req1.toJson();
|
json j1 = req1.toJson();
|
||||||
json j2 = req2.toJson();
|
json j2 = req2.toJson();
|
||||||
|
|
||||||
REQUIRE(j1["id"] == 1);
|
REQUIRE(j1["id"] == 1);
|
||||||
REQUIRE(j2["id"] == 2);
|
REQUIRE(j2["id"] == 2);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,445 +1,445 @@
|
|||||||
/**
|
/**
|
||||||
* @file StdioTransportTests.cpp
|
* @file StdioTransportTests.cpp
|
||||||
* @brief Integration tests for StdioTransport (20 TI)
|
* @brief Integration tests for StdioTransport (20 TI)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <catch2/catch_test_macros.hpp>
|
#include <catch2/catch_test_macros.hpp>
|
||||||
#include "shared/mcp/StdioTransport.hpp"
|
#include "shared/mcp/StdioTransport.hpp"
|
||||||
#include "shared/mcp/MCPTypes.hpp"
|
#include "shared/mcp/MCPTypes.hpp"
|
||||||
|
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
|
||||||
using namespace aissia::mcp;
|
using namespace aissia::mcp;
|
||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Helper: Create config for echo server
|
// Helper: Create config for echo server
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
MCPServerConfig makeEchoServerConfig() {
|
MCPServerConfig makeEchoServerConfig() {
|
||||||
MCPServerConfig config;
|
MCPServerConfig config;
|
||||||
config.name = "echo";
|
config.name = "echo";
|
||||||
config.command = "C:\\Users\\alexi\\AppData\\Local\\Programs\\Python\\Python312\\python.exe";
|
config.command = "C:\\Users\\alexi\\AppData\\Local\\Programs\\Python\\Python312\\python.exe";
|
||||||
config.args = {"tests/fixtures/echo_server.py"};
|
config.args = {"tests/fixtures/echo_server.py"};
|
||||||
config.enabled = true;
|
config.enabled = true;
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
MCPServerConfig makeMockMCPServerConfig() {
|
MCPServerConfig makeMockMCPServerConfig() {
|
||||||
MCPServerConfig config;
|
MCPServerConfig config;
|
||||||
config.name = "mock_mcp";
|
config.name = "mock_mcp";
|
||||||
config.command = "C:\\Users\\alexi\\AppData\\Local\\Programs\\Python\\Python312\\python.exe";
|
config.command = "C:\\Users\\alexi\\AppData\\Local\\Programs\\Python\\Python312\\python.exe";
|
||||||
config.args = {"tests/fixtures/mock_mcp_server.py"};
|
config.args = {"tests/fixtures/mock_mcp_server.py"};
|
||||||
config.enabled = true;
|
config.enabled = true;
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_TRANSPORT_001: Start spawns process
|
// TI_TRANSPORT_001: Start spawns process
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_TRANSPORT_001_StartSpawnsProcess", "[mcp][transport]") {
|
TEST_CASE("TI_TRANSPORT_001_StartSpawnsProcess", "[mcp][transport]") {
|
||||||
auto config = makeEchoServerConfig();
|
auto config = makeEchoServerConfig();
|
||||||
StdioTransport transport(config);
|
StdioTransport transport(config);
|
||||||
|
|
||||||
bool started = transport.start();
|
bool started = transport.start();
|
||||||
|
|
||||||
REQUIRE(started == true);
|
REQUIRE(started == true);
|
||||||
REQUIRE(transport.isRunning() == true);
|
REQUIRE(transport.isRunning() == true);
|
||||||
|
|
||||||
transport.stop();
|
transport.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_TRANSPORT_002: Start fails with invalid command
|
// TI_TRANSPORT_002: Start fails with invalid command
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_TRANSPORT_002_StartFailsInvalidCommand", "[mcp][transport]") {
|
TEST_CASE("TI_TRANSPORT_002_StartFailsInvalidCommand", "[mcp][transport]") {
|
||||||
MCPServerConfig config;
|
MCPServerConfig config;
|
||||||
config.name = "invalid";
|
config.name = "invalid";
|
||||||
config.command = "nonexistent_command_xyz";
|
config.command = "nonexistent_command_xyz";
|
||||||
config.enabled = true;
|
config.enabled = true;
|
||||||
|
|
||||||
StdioTransport transport(config);
|
StdioTransport transport(config);
|
||||||
|
|
||||||
bool started = transport.start();
|
bool started = transport.start();
|
||||||
|
|
||||||
REQUIRE(started == false);
|
REQUIRE(started == false);
|
||||||
REQUIRE(transport.isRunning() == false);
|
REQUIRE(transport.isRunning() == false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_TRANSPORT_003: Stop kills process
|
// TI_TRANSPORT_003: Stop kills process
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_TRANSPORT_003_StopKillsProcess", "[mcp][transport]") {
|
TEST_CASE("TI_TRANSPORT_003_StopKillsProcess", "[mcp][transport]") {
|
||||||
auto config = makeEchoServerConfig();
|
auto config = makeEchoServerConfig();
|
||||||
StdioTransport transport(config);
|
StdioTransport transport(config);
|
||||||
|
|
||||||
transport.start();
|
transport.start();
|
||||||
REQUIRE(transport.isRunning() == true);
|
REQUIRE(transport.isRunning() == true);
|
||||||
|
|
||||||
transport.stop();
|
transport.stop();
|
||||||
REQUIRE(transport.isRunning() == false);
|
REQUIRE(transport.isRunning() == false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_TRANSPORT_004: IsRunning reflects state
|
// TI_TRANSPORT_004: IsRunning reflects state
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_TRANSPORT_004_IsRunningReflectsState", "[mcp][transport]") {
|
TEST_CASE("TI_TRANSPORT_004_IsRunningReflectsState", "[mcp][transport]") {
|
||||||
auto config = makeEchoServerConfig();
|
auto config = makeEchoServerConfig();
|
||||||
StdioTransport transport(config);
|
StdioTransport transport(config);
|
||||||
|
|
||||||
REQUIRE(transport.isRunning() == false);
|
REQUIRE(transport.isRunning() == false);
|
||||||
|
|
||||||
transport.start();
|
transport.start();
|
||||||
REQUIRE(transport.isRunning() == true);
|
REQUIRE(transport.isRunning() == true);
|
||||||
|
|
||||||
transport.stop();
|
transport.stop();
|
||||||
REQUIRE(transport.isRunning() == false);
|
REQUIRE(transport.isRunning() == false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_TRANSPORT_005: SendRequest writes to stdin
|
// TI_TRANSPORT_005: SendRequest writes to stdin
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_TRANSPORT_005_SendRequestWritesToStdin", "[mcp][transport]") {
|
TEST_CASE("TI_TRANSPORT_005_SendRequestWritesToStdin", "[mcp][transport]") {
|
||||||
auto config = makeEchoServerConfig();
|
auto config = makeEchoServerConfig();
|
||||||
StdioTransport transport(config);
|
StdioTransport transport(config);
|
||||||
transport.start();
|
transport.start();
|
||||||
|
|
||||||
JsonRpcRequest request;
|
JsonRpcRequest request;
|
||||||
request.id = 1;
|
request.id = 1;
|
||||||
request.method = "test";
|
request.method = "test";
|
||||||
request.params = {{"message", "hello"}};
|
request.params = {{"message", "hello"}};
|
||||||
|
|
||||||
// Echo server will echo back params as result
|
// Echo server will echo back params as result
|
||||||
auto response = transport.sendRequest(request, 5000);
|
auto response = transport.sendRequest(request, 5000);
|
||||||
|
|
||||||
// If we got a response, the request was written
|
// If we got a response, the request was written
|
||||||
REQUIRE(response.isError() == false);
|
REQUIRE(response.isError() == false);
|
||||||
|
|
||||||
transport.stop();
|
transport.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_TRANSPORT_006: SendRequest reads response
|
// TI_TRANSPORT_006: SendRequest reads response
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_TRANSPORT_006_SendRequestReadsResponse", "[mcp][transport]") {
|
TEST_CASE("TI_TRANSPORT_006_SendRequestReadsResponse", "[mcp][transport]") {
|
||||||
auto config = makeEchoServerConfig();
|
auto config = makeEchoServerConfig();
|
||||||
StdioTransport transport(config);
|
StdioTransport transport(config);
|
||||||
transport.start();
|
transport.start();
|
||||||
|
|
||||||
JsonRpcRequest request;
|
JsonRpcRequest request;
|
||||||
request.id = 42;
|
request.id = 42;
|
||||||
request.method = "echo";
|
request.method = "echo";
|
||||||
request.params = {{"value", 123}};
|
request.params = {{"value", 123}};
|
||||||
|
|
||||||
auto response = transport.sendRequest(request, 5000);
|
auto response = transport.sendRequest(request, 5000);
|
||||||
|
|
||||||
REQUIRE(response.isError() == false);
|
REQUIRE(response.isError() == false);
|
||||||
REQUIRE(response.id == 42);
|
REQUIRE(response.id == 42);
|
||||||
REQUIRE(response.result.has_value());
|
REQUIRE(response.result.has_value());
|
||||||
REQUIRE(response.result.value()["value"] == 123);
|
REQUIRE(response.result.value()["value"] == 123);
|
||||||
|
|
||||||
transport.stop();
|
transport.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_TRANSPORT_007: SendRequest timeout
|
// TI_TRANSPORT_007: SendRequest timeout
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_TRANSPORT_007_SendRequestTimeout", "[mcp][transport]") {
|
TEST_CASE("TI_TRANSPORT_007_SendRequestTimeout", "[mcp][transport]") {
|
||||||
// Use cat which doesn't respond to JSON-RPC
|
// Use cat which doesn't respond to JSON-RPC
|
||||||
MCPServerConfig config;
|
MCPServerConfig config;
|
||||||
config.name = "cat";
|
config.name = "cat";
|
||||||
config.command = "cat";
|
config.command = "cat";
|
||||||
config.enabled = true;
|
config.enabled = true;
|
||||||
|
|
||||||
StdioTransport transport(config);
|
StdioTransport transport(config);
|
||||||
transport.start();
|
transport.start();
|
||||||
|
|
||||||
JsonRpcRequest request;
|
JsonRpcRequest request;
|
||||||
request.id = 1;
|
request.id = 1;
|
||||||
request.method = "test";
|
request.method = "test";
|
||||||
|
|
||||||
// Very short timeout
|
// Very short timeout
|
||||||
auto response = transport.sendRequest(request, 100);
|
auto response = transport.sendRequest(request, 100);
|
||||||
|
|
||||||
// Should timeout and return error
|
// Should timeout and return error
|
||||||
REQUIRE(response.isError() == true);
|
REQUIRE(response.isError() == true);
|
||||||
|
|
||||||
transport.stop();
|
transport.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_TRANSPORT_008: SendRequest ID matching
|
// TI_TRANSPORT_008: SendRequest ID matching
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_TRANSPORT_008_SendRequestIdMatching", "[mcp][transport]") {
|
TEST_CASE("TI_TRANSPORT_008_SendRequestIdMatching", "[mcp][transport]") {
|
||||||
auto config = makeEchoServerConfig();
|
auto config = makeEchoServerConfig();
|
||||||
StdioTransport transport(config);
|
StdioTransport transport(config);
|
||||||
transport.start();
|
transport.start();
|
||||||
|
|
||||||
// Send request with specific ID
|
// Send request with specific ID
|
||||||
JsonRpcRequest request;
|
JsonRpcRequest request;
|
||||||
request.id = 999;
|
request.id = 999;
|
||||||
request.method = "test";
|
request.method = "test";
|
||||||
|
|
||||||
auto response = transport.sendRequest(request, 5000);
|
auto response = transport.sendRequest(request, 5000);
|
||||||
|
|
||||||
// Response ID should match request ID
|
// Response ID should match request ID
|
||||||
REQUIRE(response.id == 999);
|
REQUIRE(response.id == 999);
|
||||||
|
|
||||||
transport.stop();
|
transport.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_TRANSPORT_009: Concurrent requests
|
// TI_TRANSPORT_009: Concurrent requests
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_TRANSPORT_009_ConcurrentRequests", "[mcp][transport]") {
|
TEST_CASE("TI_TRANSPORT_009_ConcurrentRequests", "[mcp][transport]") {
|
||||||
auto config = makeEchoServerConfig();
|
auto config = makeEchoServerConfig();
|
||||||
StdioTransport transport(config);
|
StdioTransport transport(config);
|
||||||
transport.start();
|
transport.start();
|
||||||
|
|
||||||
std::vector<std::thread> threads;
|
std::vector<std::thread> threads;
|
||||||
std::vector<bool> results(5, false);
|
std::vector<bool> results(5, false);
|
||||||
|
|
||||||
for (int i = 0; i < 5; i++) {
|
for (int i = 0; i < 5; i++) {
|
||||||
threads.emplace_back([&transport, &results, i]() {
|
threads.emplace_back([&transport, &results, i]() {
|
||||||
JsonRpcRequest request;
|
JsonRpcRequest request;
|
||||||
request.id = 100 + i;
|
request.id = 100 + i;
|
||||||
request.method = "test";
|
request.method = "test";
|
||||||
request.params = {{"index", i}};
|
request.params = {{"index", i}};
|
||||||
|
|
||||||
auto response = transport.sendRequest(request, 5000);
|
auto response = transport.sendRequest(request, 5000);
|
||||||
results[i] = !response.isError() && response.id == 100 + i;
|
results[i] = !response.isError() && response.id == 100 + i;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto& t : threads) {
|
for (auto& t : threads) {
|
||||||
t.join();
|
t.join();
|
||||||
}
|
}
|
||||||
|
|
||||||
// All requests should succeed
|
// All requests should succeed
|
||||||
for (bool result : results) {
|
for (bool result : results) {
|
||||||
REQUIRE(result == true);
|
REQUIRE(result == true);
|
||||||
}
|
}
|
||||||
|
|
||||||
transport.stop();
|
transport.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_TRANSPORT_010: SendNotification no response
|
// TI_TRANSPORT_010: SendNotification no response
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_TRANSPORT_010_SendNotificationNoResponse", "[mcp][transport]") {
|
TEST_CASE("TI_TRANSPORT_010_SendNotificationNoResponse", "[mcp][transport]") {
|
||||||
auto config = makeEchoServerConfig();
|
auto config = makeEchoServerConfig();
|
||||||
StdioTransport transport(config);
|
StdioTransport transport(config);
|
||||||
transport.start();
|
transport.start();
|
||||||
|
|
||||||
// Should not block or throw
|
// Should not block or throw
|
||||||
REQUIRE_NOTHROW(transport.sendNotification("notification/test", {{"data", "value"}}));
|
REQUIRE_NOTHROW(transport.sendNotification("notification/test", {{"data", "value"}}));
|
||||||
|
|
||||||
transport.stop();
|
transport.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_TRANSPORT_011: Reader thread starts on start
|
// TI_TRANSPORT_011: Reader thread starts on start
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_TRANSPORT_011_ReaderThreadStartsOnStart", "[mcp][transport]") {
|
TEST_CASE("TI_TRANSPORT_011_ReaderThreadStartsOnStart", "[mcp][transport]") {
|
||||||
auto config = makeEchoServerConfig();
|
auto config = makeEchoServerConfig();
|
||||||
StdioTransport transport(config);
|
StdioTransport transport(config);
|
||||||
|
|
||||||
transport.start();
|
transport.start();
|
||||||
|
|
||||||
// If reader thread didn't start, sendRequest would hang
|
// If reader thread didn't start, sendRequest would hang
|
||||||
JsonRpcRequest request;
|
JsonRpcRequest request;
|
||||||
request.id = 1;
|
request.id = 1;
|
||||||
request.method = "test";
|
request.method = "test";
|
||||||
|
|
||||||
auto response = transport.sendRequest(request, 1000);
|
auto response = transport.sendRequest(request, 1000);
|
||||||
|
|
||||||
// Got response means reader thread is working
|
// Got response means reader thread is working
|
||||||
REQUIRE(response.isError() == false);
|
REQUIRE(response.isError() == false);
|
||||||
|
|
||||||
transport.stop();
|
transport.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_TRANSPORT_012: Reader thread stops on stop
|
// TI_TRANSPORT_012: Reader thread stops on stop
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_TRANSPORT_012_ReaderThreadStopsOnStop", "[mcp][transport]") {
|
TEST_CASE("TI_TRANSPORT_012_ReaderThreadStopsOnStop", "[mcp][transport]") {
|
||||||
auto config = makeEchoServerConfig();
|
auto config = makeEchoServerConfig();
|
||||||
StdioTransport transport(config);
|
StdioTransport transport(config);
|
||||||
|
|
||||||
transport.start();
|
transport.start();
|
||||||
transport.stop();
|
transport.stop();
|
||||||
|
|
||||||
// Should not hang or crash on destruction
|
// Should not hang or crash on destruction
|
||||||
SUCCEED();
|
SUCCEED();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_TRANSPORT_013: JSON parse error handled
|
// TI_TRANSPORT_013: JSON parse error handled
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_TRANSPORT_013_JsonParseErrorHandled", "[mcp][transport]") {
|
TEST_CASE("TI_TRANSPORT_013_JsonParseErrorHandled", "[mcp][transport]") {
|
||||||
auto config = makeEchoServerConfig();
|
auto config = makeEchoServerConfig();
|
||||||
StdioTransport transport(config);
|
StdioTransport transport(config);
|
||||||
transport.start();
|
transport.start();
|
||||||
|
|
||||||
// Send valid request - server will respond with valid JSON
|
// Send valid request - server will respond with valid JSON
|
||||||
JsonRpcRequest request;
|
JsonRpcRequest request;
|
||||||
request.id = 1;
|
request.id = 1;
|
||||||
request.method = "test";
|
request.method = "test";
|
||||||
|
|
||||||
// Should not crash even if server sends invalid JSON
|
// Should not crash even if server sends invalid JSON
|
||||||
REQUIRE_NOTHROW(transport.sendRequest(request, 1000));
|
REQUIRE_NOTHROW(transport.sendRequest(request, 1000));
|
||||||
|
|
||||||
transport.stop();
|
transport.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_TRANSPORT_014: Process crash detected
|
// TI_TRANSPORT_014: Process crash detected
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_TRANSPORT_014_ProcessCrashDetected", "[mcp][transport]") {
|
TEST_CASE("TI_TRANSPORT_014_ProcessCrashDetected", "[mcp][transport]") {
|
||||||
// TODO: Need a server that crashes to test this
|
// TODO: Need a server that crashes to test this
|
||||||
// For now, just verify we can handle stop
|
// For now, just verify we can handle stop
|
||||||
auto config = makeEchoServerConfig();
|
auto config = makeEchoServerConfig();
|
||||||
StdioTransport transport(config);
|
StdioTransport transport(config);
|
||||||
|
|
||||||
transport.start();
|
transport.start();
|
||||||
transport.stop();
|
transport.stop();
|
||||||
|
|
||||||
REQUIRE(transport.isRunning() == false);
|
REQUIRE(transport.isRunning() == false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_TRANSPORT_015: Large message handling
|
// TI_TRANSPORT_015: Large message handling
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_TRANSPORT_015_LargeMessageHandling", "[mcp][transport]") {
|
TEST_CASE("TI_TRANSPORT_015_LargeMessageHandling", "[mcp][transport]") {
|
||||||
auto config = makeEchoServerConfig();
|
auto config = makeEchoServerConfig();
|
||||||
StdioTransport transport(config);
|
StdioTransport transport(config);
|
||||||
transport.start();
|
transport.start();
|
||||||
|
|
||||||
// Create large params
|
// Create large params
|
||||||
std::string largeString(10000, 'x');
|
std::string largeString(10000, 'x');
|
||||||
JsonRpcRequest request;
|
JsonRpcRequest request;
|
||||||
request.id = 1;
|
request.id = 1;
|
||||||
request.method = "test";
|
request.method = "test";
|
||||||
request.params = {{"data", largeString}};
|
request.params = {{"data", largeString}};
|
||||||
|
|
||||||
auto response = transport.sendRequest(request, 10000);
|
auto response = transport.sendRequest(request, 10000);
|
||||||
|
|
||||||
REQUIRE(response.isError() == false);
|
REQUIRE(response.isError() == false);
|
||||||
REQUIRE(response.result.value()["data"] == largeString);
|
REQUIRE(response.result.value()["data"] == largeString);
|
||||||
|
|
||||||
transport.stop();
|
transport.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_TRANSPORT_016: Multiline JSON handling
|
// TI_TRANSPORT_016: Multiline JSON handling
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_TRANSPORT_016_MultilineJsonHandling", "[mcp][transport]") {
|
TEST_CASE("TI_TRANSPORT_016_MultilineJsonHandling", "[mcp][transport]") {
|
||||||
auto config = makeEchoServerConfig();
|
auto config = makeEchoServerConfig();
|
||||||
StdioTransport transport(config);
|
StdioTransport transport(config);
|
||||||
transport.start();
|
transport.start();
|
||||||
|
|
||||||
// JSON with newlines in strings should work
|
// JSON with newlines in strings should work
|
||||||
JsonRpcRequest request;
|
JsonRpcRequest request;
|
||||||
request.id = 1;
|
request.id = 1;
|
||||||
request.method = "test";
|
request.method = "test";
|
||||||
request.params = {{"text", "line1\nline2\nline3"}};
|
request.params = {{"text", "line1\nline2\nline3"}};
|
||||||
|
|
||||||
auto response = transport.sendRequest(request, 5000);
|
auto response = transport.sendRequest(request, 5000);
|
||||||
|
|
||||||
REQUIRE(response.isError() == false);
|
REQUIRE(response.isError() == false);
|
||||||
|
|
||||||
transport.stop();
|
transport.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_TRANSPORT_017: Env variables passed to process
|
// TI_TRANSPORT_017: Env variables passed to process
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_TRANSPORT_017_EnvVariablesPassedToProcess", "[mcp][transport]") {
|
TEST_CASE("TI_TRANSPORT_017_EnvVariablesPassedToProcess", "[mcp][transport]") {
|
||||||
auto config = makeEchoServerConfig();
|
auto config = makeEchoServerConfig();
|
||||||
config.env["TEST_VAR"] = "test_value";
|
config.env["TEST_VAR"] = "test_value";
|
||||||
|
|
||||||
StdioTransport transport(config);
|
StdioTransport transport(config);
|
||||||
bool started = transport.start();
|
bool started = transport.start();
|
||||||
|
|
||||||
REQUIRE(started == true);
|
REQUIRE(started == true);
|
||||||
|
|
||||||
transport.stop();
|
transport.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_TRANSPORT_018: Args passed to process
|
// TI_TRANSPORT_018: Args passed to process
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_TRANSPORT_018_ArgsPassedToProcess", "[mcp][transport]") {
|
TEST_CASE("TI_TRANSPORT_018_ArgsPassedToProcess", "[mcp][transport]") {
|
||||||
auto config = makeMockMCPServerConfig();
|
auto config = makeMockMCPServerConfig();
|
||||||
// Args are already set in the helper function
|
// Args are already set in the helper function
|
||||||
|
|
||||||
StdioTransport transport(config);
|
StdioTransport transport(config);
|
||||||
bool started = transport.start();
|
bool started = transport.start();
|
||||||
|
|
||||||
REQUIRE(started == true);
|
REQUIRE(started == true);
|
||||||
|
|
||||||
transport.stop();
|
transport.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_TRANSPORT_019: Destructor cleans up
|
// TI_TRANSPORT_019: Destructor cleans up
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_TRANSPORT_019_DestructorCleansUp", "[mcp][transport]") {
|
TEST_CASE("TI_TRANSPORT_019_DestructorCleansUp", "[mcp][transport]") {
|
||||||
{
|
{
|
||||||
auto config = makeEchoServerConfig();
|
auto config = makeEchoServerConfig();
|
||||||
StdioTransport transport(config);
|
StdioTransport transport(config);
|
||||||
transport.start();
|
transport.start();
|
||||||
// Destructor called here
|
// Destructor called here
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should not leak resources or hang
|
// Should not leak resources or hang
|
||||||
SUCCEED();
|
SUCCEED();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_TRANSPORT_020: Restart after stop
|
// TI_TRANSPORT_020: Restart after stop
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_TRANSPORT_020_RestartAfterStop", "[mcp][transport]") {
|
TEST_CASE("TI_TRANSPORT_020_RestartAfterStop", "[mcp][transport]") {
|
||||||
auto config = makeEchoServerConfig();
|
auto config = makeEchoServerConfig();
|
||||||
StdioTransport transport(config);
|
StdioTransport transport(config);
|
||||||
|
|
||||||
// First start/stop
|
// First start/stop
|
||||||
transport.start();
|
transport.start();
|
||||||
transport.stop();
|
transport.stop();
|
||||||
REQUIRE(transport.isRunning() == false);
|
REQUIRE(transport.isRunning() == false);
|
||||||
|
|
||||||
// Second start
|
// Second start
|
||||||
bool restarted = transport.start();
|
bool restarted = transport.start();
|
||||||
REQUIRE(restarted == true);
|
REQUIRE(restarted == true);
|
||||||
REQUIRE(transport.isRunning() == true);
|
REQUIRE(transport.isRunning() == true);
|
||||||
|
|
||||||
// Verify it works
|
// Verify it works
|
||||||
JsonRpcRequest request;
|
JsonRpcRequest request;
|
||||||
request.id = 1;
|
request.id = 1;
|
||||||
request.method = "test";
|
request.method = "test";
|
||||||
auto response = transport.sendRequest(request, 5000);
|
auto response = transport.sendRequest(request, 5000);
|
||||||
REQUIRE(response.isError() == false);
|
REQUIRE(response.isError() == false);
|
||||||
|
|
||||||
transport.stop();
|
transport.stop();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,88 +1,88 @@
|
|||||||
#include "MockIO.hpp"
|
#include "MockIO.hpp"
|
||||||
|
|
||||||
namespace aissia::tests {
|
namespace aissia::tests {
|
||||||
|
|
||||||
void MockIO::publish(const std::string& topic, std::unique_ptr<grove::IDataNode> data) {
|
void MockIO::publish(const std::string& topic, std::unique_ptr<grove::IDataNode> data) {
|
||||||
// Convert IDataNode to JSON for easy verification
|
// Convert IDataNode to JSON for easy verification
|
||||||
json jsonData;
|
json jsonData;
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
// Try to extract JSON from JsonDataNode
|
// Try to extract JSON from JsonDataNode
|
||||||
auto* jsonNode = dynamic_cast<grove::JsonDataNode*>(data.get());
|
auto* jsonNode = dynamic_cast<grove::JsonDataNode*>(data.get());
|
||||||
if (jsonNode) {
|
if (jsonNode) {
|
||||||
jsonData = jsonNode->getJsonData();
|
jsonData = jsonNode->getJsonData();
|
||||||
} else {
|
} else {
|
||||||
// Fallback: create basic JSON from IDataNode interface
|
// Fallback: create basic JSON from IDataNode interface
|
||||||
jsonData = json::object();
|
jsonData = json::object();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m_publishedMessages.emplace_back(topic, jsonData);
|
m_publishedMessages.emplace_back(topic, jsonData);
|
||||||
}
|
}
|
||||||
|
|
||||||
grove::Message MockIO::pullMessage() {
|
grove::Message MockIO::pullMessage() {
|
||||||
if (m_incomingMessages.empty()) {
|
if (m_incomingMessages.empty()) {
|
||||||
throw std::runtime_error("No messages available");
|
throw std::runtime_error("No messages available");
|
||||||
}
|
}
|
||||||
|
|
||||||
grove::Message msg = std::move(m_incomingMessages.front());
|
grove::Message msg = std::move(m_incomingMessages.front());
|
||||||
m_incomingMessages.pop();
|
m_incomingMessages.pop();
|
||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
void MockIO::injectMessage(const std::string& topic, const json& data) {
|
void MockIO::injectMessage(const std::string& topic, const json& data) {
|
||||||
grove::Message message;
|
grove::Message message;
|
||||||
message.topic = topic;
|
message.topic = topic;
|
||||||
message.data = std::make_unique<grove::JsonDataNode>("data", data);
|
message.data = std::make_unique<grove::JsonDataNode>("data", data);
|
||||||
message.timestamp = 0;
|
message.timestamp = 0;
|
||||||
m_incomingMessages.push(std::move(message));
|
m_incomingMessages.push(std::move(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
void MockIO::injectMessages(const std::vector<std::pair<std::string, json>>& messages) {
|
void MockIO::injectMessages(const std::vector<std::pair<std::string, json>>& messages) {
|
||||||
for (const auto& [topic, data] : messages) {
|
for (const auto& [topic, data] : messages) {
|
||||||
injectMessage(topic, data);
|
injectMessage(topic, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MockIO::wasPublished(const std::string& topic) const {
|
bool MockIO::wasPublished(const std::string& topic) const {
|
||||||
return std::any_of(m_publishedMessages.begin(), m_publishedMessages.end(),
|
return std::any_of(m_publishedMessages.begin(), m_publishedMessages.end(),
|
||||||
[&topic](const auto& msg) { return msg.first == topic; });
|
[&topic](const auto& msg) { return msg.first == topic; });
|
||||||
}
|
}
|
||||||
|
|
||||||
json MockIO::getLastPublished(const std::string& topic) const {
|
json MockIO::getLastPublished(const std::string& topic) const {
|
||||||
for (auto it = m_publishedMessages.rbegin(); it != m_publishedMessages.rend(); ++it) {
|
for (auto it = m_publishedMessages.rbegin(); it != m_publishedMessages.rend(); ++it) {
|
||||||
if (it->first == topic) {
|
if (it->first == topic) {
|
||||||
return it->second;
|
return it->second;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return json::object();
|
return json::object();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<json> MockIO::getAllPublished(const std::string& topic) const {
|
std::vector<json> MockIO::getAllPublished(const std::string& topic) const {
|
||||||
std::vector<json> result;
|
std::vector<json> result;
|
||||||
for (const auto& [t, data] : m_publishedMessages) {
|
for (const auto& [t, data] : m_publishedMessages) {
|
||||||
if (t == topic) {
|
if (t == topic) {
|
||||||
result.push_back(data);
|
result.push_back(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t MockIO::countPublished(const std::string& topic) const {
|
size_t MockIO::countPublished(const std::string& topic) const {
|
||||||
return std::count_if(m_publishedMessages.begin(), m_publishedMessages.end(),
|
return std::count_if(m_publishedMessages.begin(), m_publishedMessages.end(),
|
||||||
[&topic](const auto& msg) { return msg.first == topic; });
|
[&topic](const auto& msg) { return msg.first == topic; });
|
||||||
}
|
}
|
||||||
|
|
||||||
void MockIO::clear() {
|
void MockIO::clear() {
|
||||||
m_publishedMessages.clear();
|
m_publishedMessages.clear();
|
||||||
while (!m_incomingMessages.empty()) {
|
while (!m_incomingMessages.empty()) {
|
||||||
m_incomingMessages.pop();
|
m_incomingMessages.pop();
|
||||||
}
|
}
|
||||||
m_subscriptions.clear();
|
m_subscriptions.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MockIO::clearPublished() {
|
void MockIO::clearPublished() {
|
||||||
m_publishedMessages.clear();
|
m_publishedMessages.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace aissia::tests
|
} // namespace aissia::tests
|
||||||
|
|||||||
@ -1,129 +1,129 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <grove/IIO.h>
|
#include <grove/IIO.h>
|
||||||
#include <grove/JsonDataNode.h>
|
#include <grove/JsonDataNode.h>
|
||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <queue>
|
#include <queue>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
namespace aissia::tests {
|
namespace aissia::tests {
|
||||||
|
|
||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Mock implementation of grove::IIO for testing
|
* @brief Mock implementation of grove::IIO for testing
|
||||||
*
|
*
|
||||||
* Captures published messages and allows injecting incoming messages.
|
* Captures published messages and allows injecting incoming messages.
|
||||||
*/
|
*/
|
||||||
class MockIO : public grove::IIO {
|
class MockIO : public grove::IIO {
|
||||||
public:
|
public:
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
// IIO Interface Implementation
|
// IIO Interface Implementation
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
void publish(const std::string& topic, std::unique_ptr<grove::IDataNode> data) override;
|
void publish(const std::string& topic, std::unique_ptr<grove::IDataNode> data) override;
|
||||||
|
|
||||||
void subscribe(const std::string& topicPattern, const grove::SubscriptionConfig& config = {}) override {
|
void subscribe(const std::string& topicPattern, const grove::SubscriptionConfig& config = {}) override {
|
||||||
// Mock: just record subscription
|
// Mock: just record subscription
|
||||||
m_subscriptions.push_back(topicPattern);
|
m_subscriptions.push_back(topicPattern);
|
||||||
}
|
}
|
||||||
|
|
||||||
void subscribeLowFreq(const std::string& topicPattern, const grove::SubscriptionConfig& config = {}) override {
|
void subscribeLowFreq(const std::string& topicPattern, const grove::SubscriptionConfig& config = {}) override {
|
||||||
// Mock: same as subscribe
|
// Mock: same as subscribe
|
||||||
m_subscriptions.push_back(topicPattern);
|
m_subscriptions.push_back(topicPattern);
|
||||||
}
|
}
|
||||||
|
|
||||||
int hasMessages() const override {
|
int hasMessages() const override {
|
||||||
return static_cast<int>(m_incomingMessages.size());
|
return static_cast<int>(m_incomingMessages.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
grove::Message pullMessage() override;
|
grove::Message pullMessage() override;
|
||||||
|
|
||||||
grove::IOHealth getHealth() const override {
|
grove::IOHealth getHealth() const override {
|
||||||
return grove::IOHealth{
|
return grove::IOHealth{
|
||||||
.queueSize = static_cast<int>(m_incomingMessages.size()),
|
.queueSize = static_cast<int>(m_incomingMessages.size()),
|
||||||
.maxQueueSize = 1000,
|
.maxQueueSize = 1000,
|
||||||
.dropping = false,
|
.dropping = false,
|
||||||
.averageProcessingRate = 100.0f,
|
.averageProcessingRate = 100.0f,
|
||||||
.droppedMessageCount = 0
|
.droppedMessageCount = 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
grove::IOType getType() const override {
|
grove::IOType getType() const override {
|
||||||
return grove::IOType::INTRA;
|
return grove::IOType::INTRA;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
// Test Helpers - Message Injection
|
// Test Helpers - Message Injection
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Inject a message to be received by the module under test
|
* @brief Inject a message to be received by the module under test
|
||||||
*/
|
*/
|
||||||
void injectMessage(const std::string& topic, const json& data);
|
void injectMessage(const std::string& topic, const json& data);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Inject multiple messages at once
|
* @brief Inject multiple messages at once
|
||||||
*/
|
*/
|
||||||
void injectMessages(const std::vector<std::pair<std::string, json>>& messages);
|
void injectMessages(const std::vector<std::pair<std::string, json>>& messages);
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
// Test Helpers - Verification
|
// Test Helpers - Verification
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Check if a message was published to a specific topic
|
* @brief Check if a message was published to a specific topic
|
||||||
*/
|
*/
|
||||||
bool wasPublished(const std::string& topic) const;
|
bool wasPublished(const std::string& topic) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get the last message published to a topic
|
* @brief Get the last message published to a topic
|
||||||
*/
|
*/
|
||||||
json getLastPublished(const std::string& topic) const;
|
json getLastPublished(const std::string& topic) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get all messages published to a topic
|
* @brief Get all messages published to a topic
|
||||||
*/
|
*/
|
||||||
std::vector<json> getAllPublished(const std::string& topic) const;
|
std::vector<json> getAllPublished(const std::string& topic) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Count messages published to a topic
|
* @brief Count messages published to a topic
|
||||||
*/
|
*/
|
||||||
size_t countPublished(const std::string& topic) const;
|
size_t countPublished(const std::string& topic) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get all published messages (topic -> data pairs)
|
* @brief Get all published messages (topic -> data pairs)
|
||||||
*/
|
*/
|
||||||
const std::vector<std::pair<std::string, json>>& getPublishedMessages() const {
|
const std::vector<std::pair<std::string, json>>& getPublishedMessages() const {
|
||||||
return m_publishedMessages;
|
return m_publishedMessages;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Clear all captured and pending messages
|
* @brief Clear all captured and pending messages
|
||||||
*/
|
*/
|
||||||
void clear();
|
void clear();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Clear only published messages (keep incoming queue)
|
* @brief Clear only published messages (keep incoming queue)
|
||||||
*/
|
*/
|
||||||
void clearPublished();
|
void clearPublished();
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
// Test State
|
// Test State
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
/// All messages published by the module under test
|
/// All messages published by the module under test
|
||||||
std::vector<std::pair<std::string, json>> m_publishedMessages;
|
std::vector<std::pair<std::string, json>> m_publishedMessages;
|
||||||
|
|
||||||
/// Messages waiting to be received by the module
|
/// Messages waiting to be received by the module
|
||||||
std::queue<grove::Message> m_incomingMessages;
|
std::queue<grove::Message> m_incomingMessages;
|
||||||
|
|
||||||
/// Subscribed topic patterns (for verification)
|
/// Subscribed topic patterns (for verification)
|
||||||
std::vector<std::string> m_subscriptions;
|
std::vector<std::string> m_subscriptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace aissia::tests
|
} // namespace aissia::tests
|
||||||
|
|||||||
@ -1,192 +1,192 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "shared/mcp/MCPTransport.hpp"
|
#include "shared/mcp/MCPTransport.hpp"
|
||||||
#include "shared/mcp/MCPTypes.hpp"
|
#include "shared/mcp/MCPTypes.hpp"
|
||||||
|
|
||||||
#include <queue>
|
#include <queue>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
namespace aissia::tests {
|
namespace aissia::tests {
|
||||||
|
|
||||||
using namespace aissia::mcp;
|
using namespace aissia::mcp;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Mock implementation of IMCPTransport for testing MCPClient
|
* @brief Mock implementation of IMCPTransport for testing MCPClient
|
||||||
*/
|
*/
|
||||||
class MockTransport : public IMCPTransport {
|
class MockTransport : public IMCPTransport {
|
||||||
public:
|
public:
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
// IMCPTransport Interface
|
// IMCPTransport Interface
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
bool start() override {
|
bool start() override {
|
||||||
if (m_startShouldFail) {
|
if (m_startShouldFail) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
m_running = true;
|
m_running = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void stop() override {
|
void stop() override {
|
||||||
m_running = false;
|
m_running = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isRunning() const override {
|
bool isRunning() const override {
|
||||||
return m_running;
|
return m_running;
|
||||||
}
|
}
|
||||||
|
|
||||||
JsonRpcResponse sendRequest(const JsonRpcRequest& request, int timeoutMs = 30000) override {
|
JsonRpcResponse sendRequest(const JsonRpcRequest& request, int timeoutMs = 30000) override {
|
||||||
m_sentRequests.push_back(request);
|
m_sentRequests.push_back(request);
|
||||||
|
|
||||||
// If we have a custom handler, use it
|
// If we have a custom handler, use it
|
||||||
if (m_requestHandler) {
|
if (m_requestHandler) {
|
||||||
return m_requestHandler(request);
|
return m_requestHandler(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, use prepared responses
|
// Otherwise, use prepared responses
|
||||||
if (!m_preparedResponses.empty()) {
|
if (!m_preparedResponses.empty()) {
|
||||||
auto response = m_preparedResponses.front();
|
auto response = m_preparedResponses.front();
|
||||||
m_preparedResponses.pop();
|
m_preparedResponses.pop();
|
||||||
response.id = request.id; // Match the request ID
|
response.id = request.id; // Match the request ID
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default: return error
|
// Default: return error
|
||||||
JsonRpcResponse errorResponse;
|
JsonRpcResponse errorResponse;
|
||||||
errorResponse.id = request.id;
|
errorResponse.id = request.id;
|
||||||
errorResponse.error = json{{"code", -32603}, {"message", "No prepared response"}};
|
errorResponse.error = json{{"code", -32603}, {"message", "No prepared response"}};
|
||||||
return errorResponse;
|
return errorResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
void sendNotification(const std::string& method, const json& params) override {
|
void sendNotification(const std::string& method, const json& params) override {
|
||||||
m_sentNotifications.emplace_back(method, params);
|
m_sentNotifications.emplace_back(method, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
// Test Configuration
|
// Test Configuration
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Make start() fail
|
* @brief Make start() fail
|
||||||
*/
|
*/
|
||||||
void setStartShouldFail(bool fail) {
|
void setStartShouldFail(bool fail) {
|
||||||
m_startShouldFail = fail;
|
m_startShouldFail = fail;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Add a response to be returned on next sendRequest
|
* @brief Add a response to be returned on next sendRequest
|
||||||
*/
|
*/
|
||||||
void prepareResponse(const JsonRpcResponse& response) {
|
void prepareResponse(const JsonRpcResponse& response) {
|
||||||
m_preparedResponses.push(response);
|
m_preparedResponses.push(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Prepare a successful response with result
|
* @brief Prepare a successful response with result
|
||||||
*/
|
*/
|
||||||
void prepareSuccessResponse(const json& result) {
|
void prepareSuccessResponse(const json& result) {
|
||||||
JsonRpcResponse response;
|
JsonRpcResponse response;
|
||||||
response.result = result;
|
response.result = result;
|
||||||
m_preparedResponses.push(response);
|
m_preparedResponses.push(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Prepare an error response
|
* @brief Prepare an error response
|
||||||
*/
|
*/
|
||||||
void prepareErrorResponse(int code, const std::string& message) {
|
void prepareErrorResponse(int code, const std::string& message) {
|
||||||
JsonRpcResponse response;
|
JsonRpcResponse response;
|
||||||
response.error = json{{"code", code}, {"message", message}};
|
response.error = json{{"code", code}, {"message", message}};
|
||||||
m_preparedResponses.push(response);
|
m_preparedResponses.push(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Set a custom handler for all requests
|
* @brief Set a custom handler for all requests
|
||||||
*/
|
*/
|
||||||
void setRequestHandler(std::function<JsonRpcResponse(const JsonRpcRequest&)> handler) {
|
void setRequestHandler(std::function<JsonRpcResponse(const JsonRpcRequest&)> handler) {
|
||||||
m_requestHandler = std::move(handler);
|
m_requestHandler = std::move(handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Simulate MCP server with initialize and tools/list
|
* @brief Simulate MCP server with initialize and tools/list
|
||||||
*/
|
*/
|
||||||
void setupAsMCPServer(const std::string& serverName, const std::vector<MCPTool>& tools) {
|
void setupAsMCPServer(const std::string& serverName, const std::vector<MCPTool>& tools) {
|
||||||
m_requestHandler = [serverName, tools](const JsonRpcRequest& req) -> JsonRpcResponse {
|
m_requestHandler = [serverName, tools](const JsonRpcRequest& req) -> JsonRpcResponse {
|
||||||
JsonRpcResponse resp;
|
JsonRpcResponse resp;
|
||||||
resp.id = req.id;
|
resp.id = req.id;
|
||||||
|
|
||||||
if (req.method == "initialize") {
|
if (req.method == "initialize") {
|
||||||
resp.result = json{
|
resp.result = json{
|
||||||
{"protocolVersion", "2024-11-05"},
|
{"protocolVersion", "2024-11-05"},
|
||||||
{"capabilities", {{"tools", json::object()}}},
|
{"capabilities", {{"tools", json::object()}}},
|
||||||
{"serverInfo", {{"name", serverName}, {"version", "1.0.0"}}}
|
{"serverInfo", {{"name", serverName}, {"version", "1.0.0"}}}
|
||||||
};
|
};
|
||||||
} else if (req.method == "tools/list") {
|
} else if (req.method == "tools/list") {
|
||||||
json toolsJson = json::array();
|
json toolsJson = json::array();
|
||||||
for (const auto& tool : tools) {
|
for (const auto& tool : tools) {
|
||||||
toolsJson.push_back(tool.toJson());
|
toolsJson.push_back(tool.toJson());
|
||||||
}
|
}
|
||||||
resp.result = json{{"tools", toolsJson}};
|
resp.result = json{{"tools", toolsJson}};
|
||||||
} else if (req.method == "tools/call") {
|
} else if (req.method == "tools/call") {
|
||||||
resp.result = json{
|
resp.result = json{
|
||||||
{"content", json::array({{{"type", "text"}, {"text", "Tool executed"}}})}
|
{"content", json::array({{{"type", "text"}, {"text", "Tool executed"}}})}
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
resp.error = json{{"code", -32601}, {"message", "Method not found"}};
|
resp.error = json{{"code", -32601}, {"message", "Method not found"}};
|
||||||
}
|
}
|
||||||
|
|
||||||
return resp;
|
return resp;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
// Test Verification
|
// Test Verification
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get all sent requests
|
* @brief Get all sent requests
|
||||||
*/
|
*/
|
||||||
const std::vector<JsonRpcRequest>& getSentRequests() const {
|
const std::vector<JsonRpcRequest>& getSentRequests() const {
|
||||||
return m_sentRequests;
|
return m_sentRequests;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Check if a method was called
|
* @brief Check if a method was called
|
||||||
*/
|
*/
|
||||||
bool wasMethodCalled(const std::string& method) const {
|
bool wasMethodCalled(const std::string& method) const {
|
||||||
return std::any_of(m_sentRequests.begin(), m_sentRequests.end(),
|
return std::any_of(m_sentRequests.begin(), m_sentRequests.end(),
|
||||||
[&method](const auto& req) { return req.method == method; });
|
[&method](const auto& req) { return req.method == method; });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get count of calls to a method
|
* @brief Get count of calls to a method
|
||||||
*/
|
*/
|
||||||
size_t countMethodCalls(const std::string& method) const {
|
size_t countMethodCalls(const std::string& method) const {
|
||||||
return std::count_if(m_sentRequests.begin(), m_sentRequests.end(),
|
return std::count_if(m_sentRequests.begin(), m_sentRequests.end(),
|
||||||
[&method](const auto& req) { return req.method == method; });
|
[&method](const auto& req) { return req.method == method; });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Clear all state
|
* @brief Clear all state
|
||||||
*/
|
*/
|
||||||
void clear() {
|
void clear() {
|
||||||
m_sentRequests.clear();
|
m_sentRequests.clear();
|
||||||
m_sentNotifications.clear();
|
m_sentNotifications.clear();
|
||||||
while (!m_preparedResponses.empty()) {
|
while (!m_preparedResponses.empty()) {
|
||||||
m_preparedResponses.pop();
|
m_preparedResponses.pop();
|
||||||
}
|
}
|
||||||
m_requestHandler = nullptr;
|
m_requestHandler = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
// Test State
|
// Test State
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
bool m_running = false;
|
bool m_running = false;
|
||||||
bool m_startShouldFail = false;
|
bool m_startShouldFail = false;
|
||||||
std::vector<JsonRpcRequest> m_sentRequests;
|
std::vector<JsonRpcRequest> m_sentRequests;
|
||||||
std::vector<std::pair<std::string, json>> m_sentNotifications;
|
std::vector<std::pair<std::string, json>> m_sentNotifications;
|
||||||
std::queue<JsonRpcResponse> m_preparedResponses;
|
std::queue<JsonRpcResponse> m_preparedResponses;
|
||||||
std::function<JsonRpcResponse(const JsonRpcRequest&)> m_requestHandler;
|
std::function<JsonRpcResponse(const JsonRpcRequest&)> m_requestHandler;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace aissia::tests
|
} // namespace aissia::tests
|
||||||
|
|||||||
@ -1,293 +1,293 @@
|
|||||||
/**
|
/**
|
||||||
* @file AIModuleTests.cpp
|
* @file AIModuleTests.cpp
|
||||||
* @brief Integration tests for AIModule (10 TI)
|
* @brief Integration tests for AIModule (10 TI)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <catch2/catch_test_macros.hpp>
|
#include <catch2/catch_test_macros.hpp>
|
||||||
#include "mocks/MockIO.hpp"
|
#include "mocks/MockIO.hpp"
|
||||||
#include "utils/TimeSimulator.hpp"
|
#include "utils/TimeSimulator.hpp"
|
||||||
#include "utils/TestHelpers.hpp"
|
#include "utils/TestHelpers.hpp"
|
||||||
|
|
||||||
#include "modules/AIModule.h"
|
#include "modules/AIModule.h"
|
||||||
#include <grove/JsonDataNode.h>
|
#include <grove/JsonDataNode.h>
|
||||||
|
|
||||||
using namespace aissia;
|
using namespace aissia;
|
||||||
using namespace aissia::tests;
|
using namespace aissia::tests;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Test Fixture
|
// Test Fixture
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
class AITestFixture {
|
class AITestFixture {
|
||||||
public:
|
public:
|
||||||
MockIO io;
|
MockIO io;
|
||||||
TimeSimulator time;
|
TimeSimulator time;
|
||||||
AIModule module;
|
AIModule module;
|
||||||
|
|
||||||
void configure(const json& config = json::object()) {
|
void configure(const json& config = json::object()) {
|
||||||
json fullConfig = {
|
json fullConfig = {
|
||||||
{"system_prompt", "Tu es un assistant personnel intelligent."},
|
{"system_prompt", "Tu es un assistant personnel intelligent."},
|
||||||
{"max_iterations", 10}
|
{"max_iterations", 10}
|
||||||
};
|
};
|
||||||
fullConfig.merge_patch(config);
|
fullConfig.merge_patch(config);
|
||||||
|
|
||||||
grove::JsonDataNode configNode("config", fullConfig);
|
grove::JsonDataNode configNode("config", fullConfig);
|
||||||
module.setConfiguration(configNode, &io, nullptr);
|
module.setConfiguration(configNode, &io, nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
void process() {
|
void process() {
|
||||||
grove::JsonDataNode input("input", time.createInput());
|
grove::JsonDataNode input("input", time.createInput());
|
||||||
module.process(input);
|
module.process(input);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_AI_001: Query Sends LLM Request
|
// TI_AI_001: Query Sends LLM Request
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_AI_001_QuerySendsLLMRequest", "[ai][integration]") {
|
TEST_CASE("TI_AI_001_QuerySendsLLMRequest", "[ai][integration]") {
|
||||||
AITestFixture f;
|
AITestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Send query
|
// Send query
|
||||||
f.io.injectMessage("ai:query", {{"query", "Quelle heure est-il?"}});
|
f.io.injectMessage("ai:query", {{"query", "Quelle heure est-il?"}});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
// Verify LLM request published
|
// Verify LLM request published
|
||||||
REQUIRE(f.io.wasPublished("llm:request"));
|
REQUIRE(f.io.wasPublished("llm:request"));
|
||||||
auto msg = f.io.getLastPublished("llm:request");
|
auto msg = f.io.getLastPublished("llm:request");
|
||||||
REQUIRE(msg["query"] == "Quelle heure est-il?");
|
REQUIRE(msg["query"] == "Quelle heure est-il?");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_AI_002: Voice Transcription Triggers Query
|
// TI_AI_002: Voice Transcription Triggers Query
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_AI_002_VoiceTranscriptionTriggersQuery", "[ai][integration]") {
|
TEST_CASE("TI_AI_002_VoiceTranscriptionTriggersQuery", "[ai][integration]") {
|
||||||
AITestFixture f;
|
AITestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Send voice transcription
|
// Send voice transcription
|
||||||
f.io.injectMessage("voice:transcription", {
|
f.io.injectMessage("voice:transcription", {
|
||||||
{"text", "Aide-moi avec mon code"},
|
{"text", "Aide-moi avec mon code"},
|
||||||
{"confidence", 0.95}
|
{"confidence", 0.95}
|
||||||
});
|
});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
// Verify LLM request
|
// Verify LLM request
|
||||||
REQUIRE(f.io.wasPublished("llm:request"));
|
REQUIRE(f.io.wasPublished("llm:request"));
|
||||||
auto msg = f.io.getLastPublished("llm:request");
|
auto msg = f.io.getLastPublished("llm:request");
|
||||||
REQUIRE(msg["query"] == "Aide-moi avec mon code");
|
REQUIRE(msg["query"] == "Aide-moi avec mon code");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_AI_003: LLM Response Handled
|
// TI_AI_003: LLM Response Handled
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_AI_003_LLMResponseHandled", "[ai][integration]") {
|
TEST_CASE("TI_AI_003_LLMResponseHandled", "[ai][integration]") {
|
||||||
AITestFixture f;
|
AITestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Send query to set awaiting state
|
// Send query to set awaiting state
|
||||||
f.io.injectMessage("ai:query", {{"query", "Test"}});
|
f.io.injectMessage("ai:query", {{"query", "Test"}});
|
||||||
f.process();
|
f.process();
|
||||||
REQUIRE(f.module.isIdle() == false);
|
REQUIRE(f.module.isIdle() == false);
|
||||||
|
|
||||||
// Receive response
|
// Receive response
|
||||||
f.io.injectMessage("llm:response", {
|
f.io.injectMessage("llm:response", {
|
||||||
{"text", "Voici la reponse"},
|
{"text", "Voici la reponse"},
|
||||||
{"tokens", 100},
|
{"tokens", 100},
|
||||||
{"conversationId", "default"}
|
{"conversationId", "default"}
|
||||||
});
|
});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
// Verify no longer awaiting
|
// Verify no longer awaiting
|
||||||
REQUIRE(f.module.isIdle() == true);
|
REQUIRE(f.module.isIdle() == true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_AI_004: LLM Error Handled
|
// TI_AI_004: LLM Error Handled
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_AI_004_LLMErrorHandled", "[ai][integration]") {
|
TEST_CASE("TI_AI_004_LLMErrorHandled", "[ai][integration]") {
|
||||||
AITestFixture f;
|
AITestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Send query
|
// Send query
|
||||||
f.io.injectMessage("ai:query", {{"query", "Test"}});
|
f.io.injectMessage("ai:query", {{"query", "Test"}});
|
||||||
f.process();
|
f.process();
|
||||||
REQUIRE(f.module.isIdle() == false);
|
REQUIRE(f.module.isIdle() == false);
|
||||||
|
|
||||||
// Receive error
|
// Receive error
|
||||||
f.io.injectMessage("llm:error", {
|
f.io.injectMessage("llm:error", {
|
||||||
{"message", "API rate limit exceeded"},
|
{"message", "API rate limit exceeded"},
|
||||||
{"conversationId", "default"}
|
{"conversationId", "default"}
|
||||||
});
|
});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
// Should no longer be awaiting
|
// Should no longer be awaiting
|
||||||
REQUIRE(f.module.isIdle() == true);
|
REQUIRE(f.module.isIdle() == true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_AI_005: Hyperfocus Alert Generates Suggestion
|
// TI_AI_005: Hyperfocus Alert Generates Suggestion
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_AI_005_HyperfocusAlertGeneratesSuggestion", "[ai][integration]") {
|
TEST_CASE("TI_AI_005_HyperfocusAlertGeneratesSuggestion", "[ai][integration]") {
|
||||||
AITestFixture f;
|
AITestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Receive hyperfocus alert
|
// Receive hyperfocus alert
|
||||||
f.io.injectMessage("scheduler:hyperfocus_alert", {
|
f.io.injectMessage("scheduler:hyperfocus_alert", {
|
||||||
{"sessionMinutes", 130},
|
{"sessionMinutes", 130},
|
||||||
{"task", "coding"}
|
{"task", "coding"}
|
||||||
});
|
});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
// Verify LLM request published
|
// Verify LLM request published
|
||||||
REQUIRE(f.io.wasPublished("llm:request"));
|
REQUIRE(f.io.wasPublished("llm:request"));
|
||||||
auto req = f.io.getLastPublished("llm:request");
|
auto req = f.io.getLastPublished("llm:request");
|
||||||
std::string convId = req["conversationId"];
|
std::string convId = req["conversationId"];
|
||||||
|
|
||||||
// Simulate LLM response
|
// Simulate LLM response
|
||||||
f.io.injectMessage("llm:response", {
|
f.io.injectMessage("llm:response", {
|
||||||
{"text", "Time to take a break!"},
|
{"text", "Time to take a break!"},
|
||||||
{"conversationId", convId}
|
{"conversationId", convId}
|
||||||
});
|
});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
// Verify suggestion published
|
// Verify suggestion published
|
||||||
REQUIRE(f.io.wasPublished("ai:suggestion"));
|
REQUIRE(f.io.wasPublished("ai:suggestion"));
|
||||||
auto msg = f.io.getLastPublished("ai:suggestion");
|
auto msg = f.io.getLastPublished("ai:suggestion");
|
||||||
REQUIRE(msg.contains("message"));
|
REQUIRE(msg.contains("message"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_AI_006: Break Reminder Generates Suggestion
|
// TI_AI_006: Break Reminder Generates Suggestion
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_AI_006_BreakReminderGeneratesSuggestion", "[ai][integration]") {
|
TEST_CASE("TI_AI_006_BreakReminderGeneratesSuggestion", "[ai][integration]") {
|
||||||
AITestFixture f;
|
AITestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Receive break reminder
|
// Receive break reminder
|
||||||
f.io.injectMessage("scheduler:break_reminder", {
|
f.io.injectMessage("scheduler:break_reminder", {
|
||||||
{"workMinutes", 45}
|
{"workMinutes", 45}
|
||||||
});
|
});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
// Verify LLM request published
|
// Verify LLM request published
|
||||||
REQUIRE(f.io.wasPublished("llm:request"));
|
REQUIRE(f.io.wasPublished("llm:request"));
|
||||||
auto req = f.io.getLastPublished("llm:request");
|
auto req = f.io.getLastPublished("llm:request");
|
||||||
std::string convId = req["conversationId"];
|
std::string convId = req["conversationId"];
|
||||||
|
|
||||||
// Simulate LLM response
|
// Simulate LLM response
|
||||||
f.io.injectMessage("llm:response", {
|
f.io.injectMessage("llm:response", {
|
||||||
{"text", "Take a short break now!"},
|
{"text", "Take a short break now!"},
|
||||||
{"conversationId", convId}
|
{"conversationId", convId}
|
||||||
});
|
});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
// Verify suggestion
|
// Verify suggestion
|
||||||
REQUIRE(f.io.wasPublished("ai:suggestion"));
|
REQUIRE(f.io.wasPublished("ai:suggestion"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_AI_007: System Prompt In Request
|
// TI_AI_007: System Prompt In Request
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_AI_007_SystemPromptInRequest", "[ai][integration]") {
|
TEST_CASE("TI_AI_007_SystemPromptInRequest", "[ai][integration]") {
|
||||||
AITestFixture f;
|
AITestFixture f;
|
||||||
f.configure({{"system_prompt", "Custom prompt here"}});
|
f.configure({{"system_prompt", "Custom prompt here"}});
|
||||||
|
|
||||||
f.io.injectMessage("ai:query", {{"query", "Test"}});
|
f.io.injectMessage("ai:query", {{"query", "Test"}});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
REQUIRE(f.io.wasPublished("llm:request"));
|
REQUIRE(f.io.wasPublished("llm:request"));
|
||||||
auto msg = f.io.getLastPublished("llm:request");
|
auto msg = f.io.getLastPublished("llm:request");
|
||||||
REQUIRE(msg["systemPrompt"] == "Custom prompt here");
|
REQUIRE(msg["systemPrompt"] == "Custom prompt here");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_AI_008: Conversation ID Tracking
|
// TI_AI_008: Conversation ID Tracking
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_AI_008_ConversationIdTracking", "[ai][integration]") {
|
TEST_CASE("TI_AI_008_ConversationIdTracking", "[ai][integration]") {
|
||||||
AITestFixture f;
|
AITestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// First query
|
// First query
|
||||||
f.io.injectMessage("ai:query", {{"query", "Question 1"}});
|
f.io.injectMessage("ai:query", {{"query", "Question 1"}});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
auto msg1 = f.io.getLastPublished("llm:request");
|
auto msg1 = f.io.getLastPublished("llm:request");
|
||||||
std::string convId = msg1["conversationId"];
|
std::string convId = msg1["conversationId"];
|
||||||
REQUIRE(!convId.empty());
|
REQUIRE(!convId.empty());
|
||||||
|
|
||||||
// Simulate response
|
// Simulate response
|
||||||
f.io.injectMessage("llm:response", {{"text", "Response"}, {"conversationId", convId}});
|
f.io.injectMessage("llm:response", {{"text", "Response"}, {"conversationId", convId}});
|
||||||
f.process();
|
f.process();
|
||||||
f.io.clearPublished();
|
f.io.clearPublished();
|
||||||
|
|
||||||
// Second query should use same conversation
|
// Second query should use same conversation
|
||||||
f.io.injectMessage("ai:query", {{"query", "Question 2"}});
|
f.io.injectMessage("ai:query", {{"query", "Question 2"}});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
auto msg2 = f.io.getLastPublished("llm:request");
|
auto msg2 = f.io.getLastPublished("llm:request");
|
||||||
REQUIRE(msg2["conversationId"] == convId);
|
REQUIRE(msg2["conversationId"] == convId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_AI_009: Token Counting Accumulates
|
// TI_AI_009: Token Counting Accumulates
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_AI_009_TokenCountingAccumulates", "[ai][integration]") {
|
TEST_CASE("TI_AI_009_TokenCountingAccumulates", "[ai][integration]") {
|
||||||
AITestFixture f;
|
AITestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Query 1
|
// Query 1
|
||||||
f.io.injectMessage("ai:query", {{"query", "Q1"}});
|
f.io.injectMessage("ai:query", {{"query", "Q1"}});
|
||||||
f.process();
|
f.process();
|
||||||
f.io.injectMessage("llm:response", {{"text", "R1"}, {"tokens", 50}});
|
f.io.injectMessage("llm:response", {{"text", "R1"}, {"tokens", 50}});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
// Query 2
|
// Query 2
|
||||||
f.io.injectMessage("ai:query", {{"query", "Q2"}});
|
f.io.injectMessage("ai:query", {{"query", "Q2"}});
|
||||||
f.process();
|
f.process();
|
||||||
f.io.injectMessage("llm:response", {{"text", "R2"}, {"tokens", 75}});
|
f.io.injectMessage("llm:response", {{"text", "R2"}, {"tokens", 75}});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
// Verify total
|
// Verify total
|
||||||
auto state = f.module.getState();
|
auto state = f.module.getState();
|
||||||
// TODO: Verify totalTokens == 125
|
// TODO: Verify totalTokens == 125
|
||||||
SUCCEED(); // Placeholder
|
SUCCEED(); // Placeholder
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_AI_010: State Serialization
|
// TI_AI_010: State Serialization
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_AI_010_StateSerialization", "[ai][integration]") {
|
TEST_CASE("TI_AI_010_StateSerialization", "[ai][integration]") {
|
||||||
AITestFixture f;
|
AITestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Build state
|
// Build state
|
||||||
f.io.injectMessage("ai:query", {{"query", "Test"}});
|
f.io.injectMessage("ai:query", {{"query", "Test"}});
|
||||||
f.process();
|
f.process();
|
||||||
f.io.injectMessage("llm:response", {{"text", "Response"}, {"tokens", 100}});
|
f.io.injectMessage("llm:response", {{"text", "Response"}, {"tokens", 100}});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
// Get state
|
// Get state
|
||||||
auto state = f.module.getState();
|
auto state = f.module.getState();
|
||||||
REQUIRE(state != nullptr);
|
REQUIRE(state != nullptr);
|
||||||
|
|
||||||
// Restore
|
// Restore
|
||||||
AIModule module2;
|
AIModule module2;
|
||||||
grove::JsonDataNode configNode2("config", json::object());
|
grove::JsonDataNode configNode2("config", json::object());
|
||||||
module2.setConfiguration(configNode2, &f.io, nullptr);
|
module2.setConfiguration(configNode2, &f.io, nullptr);
|
||||||
module2.setState(*state);
|
module2.setState(*state);
|
||||||
|
|
||||||
auto state2 = module2.getState();
|
auto state2 = module2.getState();
|
||||||
REQUIRE(state2 != nullptr);
|
REQUIRE(state2 != nullptr);
|
||||||
SUCCEED(); // Placeholder
|
SUCCEED(); // Placeholder
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,285 +1,285 @@
|
|||||||
/**
|
/**
|
||||||
* @file MonitoringModuleTests.cpp
|
* @file MonitoringModuleTests.cpp
|
||||||
* @brief Integration tests for MonitoringModule (10 TI)
|
* @brief Integration tests for MonitoringModule (10 TI)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <catch2/catch_test_macros.hpp>
|
#include <catch2/catch_test_macros.hpp>
|
||||||
#include "mocks/MockIO.hpp"
|
#include "mocks/MockIO.hpp"
|
||||||
#include "utils/TimeSimulator.hpp"
|
#include "utils/TimeSimulator.hpp"
|
||||||
#include "utils/TestHelpers.hpp"
|
#include "utils/TestHelpers.hpp"
|
||||||
|
|
||||||
#include "modules/MonitoringModule.h"
|
#include "modules/MonitoringModule.h"
|
||||||
#include <grove/JsonDataNode.h>
|
#include <grove/JsonDataNode.h>
|
||||||
|
|
||||||
using namespace aissia;
|
using namespace aissia;
|
||||||
using namespace aissia::tests;
|
using namespace aissia::tests;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Test Fixture
|
// Test Fixture
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
class MonitoringTestFixture {
|
class MonitoringTestFixture {
|
||||||
public:
|
public:
|
||||||
MockIO io;
|
MockIO io;
|
||||||
TimeSimulator time;
|
TimeSimulator time;
|
||||||
MonitoringModule module;
|
MonitoringModule module;
|
||||||
|
|
||||||
void configure(const json& config = json::object()) {
|
void configure(const json& config = json::object()) {
|
||||||
json fullConfig = {
|
json fullConfig = {
|
||||||
{"enabled", true},
|
{"enabled", true},
|
||||||
{"productive_apps", json::array({"Code", "CLion", "Visual Studio"})},
|
{"productive_apps", json::array({"Code", "CLion", "Visual Studio"})},
|
||||||
{"distracting_apps", json::array({"Discord", "Steam", "YouTube"})}
|
{"distracting_apps", json::array({"Discord", "Steam", "YouTube"})}
|
||||||
};
|
};
|
||||||
fullConfig.merge_patch(config);
|
fullConfig.merge_patch(config);
|
||||||
|
|
||||||
grove::JsonDataNode configNode("config", fullConfig);
|
grove::JsonDataNode configNode("config", fullConfig);
|
||||||
module.setConfiguration(configNode, &io, nullptr);
|
module.setConfiguration(configNode, &io, nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
void process() {
|
void process() {
|
||||||
grove::JsonDataNode input("input", time.createInput());
|
grove::JsonDataNode input("input", time.createInput());
|
||||||
module.process(input);
|
module.process(input);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_MONITOR_001: App Changed
|
// TI_MONITOR_001: App Changed
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_MONITOR_001_AppChanged", "[monitoring][integration]") {
|
TEST_CASE("TI_MONITOR_001_AppChanged", "[monitoring][integration]") {
|
||||||
MonitoringTestFixture f;
|
MonitoringTestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Inject window change
|
// Inject window change
|
||||||
f.io.injectMessage("platform:window_changed", {
|
f.io.injectMessage("platform:window_changed", {
|
||||||
{"oldApp", ""},
|
{"oldApp", ""},
|
||||||
{"newApp", "Code"},
|
{"newApp", "Code"},
|
||||||
{"duration", 0}
|
{"duration", 0}
|
||||||
});
|
});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
// Verify app_changed published
|
// Verify app_changed published
|
||||||
REQUIRE(f.io.wasPublished("monitoring:app_changed"));
|
REQUIRE(f.io.wasPublished("monitoring:app_changed"));
|
||||||
auto msg = f.io.getLastPublished("monitoring:app_changed");
|
auto msg = f.io.getLastPublished("monitoring:app_changed");
|
||||||
REQUIRE(msg["appName"] == "Code");
|
REQUIRE(msg["appName"] == "Code");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_MONITOR_002: Productive App Classification
|
// TI_MONITOR_002: Productive App Classification
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_MONITOR_002_ProductiveAppClassification", "[monitoring][integration]") {
|
TEST_CASE("TI_MONITOR_002_ProductiveAppClassification", "[monitoring][integration]") {
|
||||||
MonitoringTestFixture f;
|
MonitoringTestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
f.io.injectMessage("platform:window_changed", {
|
f.io.injectMessage("platform:window_changed", {
|
||||||
{"oldApp", ""},
|
{"oldApp", ""},
|
||||||
{"newApp", "Code"},
|
{"newApp", "Code"},
|
||||||
{"duration", 0}
|
{"duration", 0}
|
||||||
});
|
});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
REQUIRE(f.io.wasPublished("monitoring:app_changed"));
|
REQUIRE(f.io.wasPublished("monitoring:app_changed"));
|
||||||
auto msg = f.io.getLastPublished("monitoring:app_changed");
|
auto msg = f.io.getLastPublished("monitoring:app_changed");
|
||||||
REQUIRE(msg["classification"] == "productive");
|
REQUIRE(msg["classification"] == "productive");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_MONITOR_003: Distracting App Classification
|
// TI_MONITOR_003: Distracting App Classification
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_MONITOR_003_DistractingAppClassification", "[monitoring][integration]") {
|
TEST_CASE("TI_MONITOR_003_DistractingAppClassification", "[monitoring][integration]") {
|
||||||
MonitoringTestFixture f;
|
MonitoringTestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
f.io.injectMessage("platform:window_changed", {
|
f.io.injectMessage("platform:window_changed", {
|
||||||
{"oldApp", ""},
|
{"oldApp", ""},
|
||||||
{"newApp", "Discord"},
|
{"newApp", "Discord"},
|
||||||
{"duration", 0}
|
{"duration", 0}
|
||||||
});
|
});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
REQUIRE(f.io.wasPublished("monitoring:app_changed"));
|
REQUIRE(f.io.wasPublished("monitoring:app_changed"));
|
||||||
auto msg = f.io.getLastPublished("monitoring:app_changed");
|
auto msg = f.io.getLastPublished("monitoring:app_changed");
|
||||||
REQUIRE(msg["classification"] == "distracting");
|
REQUIRE(msg["classification"] == "distracting");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_MONITOR_004: Neutral App Classification
|
// TI_MONITOR_004: Neutral App Classification
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_MONITOR_004_NeutralAppClassification", "[monitoring][integration]") {
|
TEST_CASE("TI_MONITOR_004_NeutralAppClassification", "[monitoring][integration]") {
|
||||||
MonitoringTestFixture f;
|
MonitoringTestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
f.io.injectMessage("platform:window_changed", {
|
f.io.injectMessage("platform:window_changed", {
|
||||||
{"oldApp", ""},
|
{"oldApp", ""},
|
||||||
{"newApp", "Notepad"},
|
{"newApp", "Notepad"},
|
||||||
{"duration", 0}
|
{"duration", 0}
|
||||||
});
|
});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
REQUIRE(f.io.wasPublished("monitoring:app_changed"));
|
REQUIRE(f.io.wasPublished("monitoring:app_changed"));
|
||||||
auto msg = f.io.getLastPublished("monitoring:app_changed");
|
auto msg = f.io.getLastPublished("monitoring:app_changed");
|
||||||
REQUIRE(msg["classification"] == "neutral");
|
REQUIRE(msg["classification"] == "neutral");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_MONITOR_005: Duration Tracking
|
// TI_MONITOR_005: Duration Tracking
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_MONITOR_005_DurationTracking", "[monitoring][integration]") {
|
TEST_CASE("TI_MONITOR_005_DurationTracking", "[monitoring][integration]") {
|
||||||
MonitoringTestFixture f;
|
MonitoringTestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Start with Code
|
// Start with Code
|
||||||
f.io.injectMessage("platform:window_changed", {
|
f.io.injectMessage("platform:window_changed", {
|
||||||
{"oldApp", ""},
|
{"oldApp", ""},
|
||||||
{"newApp", "Code"},
|
{"newApp", "Code"},
|
||||||
{"duration", 0}
|
{"duration", 0}
|
||||||
});
|
});
|
||||||
f.process();
|
f.process();
|
||||||
f.io.clearPublished();
|
f.io.clearPublished();
|
||||||
|
|
||||||
// Switch after 60 seconds
|
// Switch after 60 seconds
|
||||||
f.io.injectMessage("platform:window_changed", {
|
f.io.injectMessage("platform:window_changed", {
|
||||||
{"oldApp", "Code"},
|
{"oldApp", "Code"},
|
||||||
{"newApp", "Discord"},
|
{"newApp", "Discord"},
|
||||||
{"duration", 60}
|
{"duration", 60}
|
||||||
});
|
});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
// Verify duration tracked
|
// Verify duration tracked
|
||||||
auto state = f.module.getState();
|
auto state = f.module.getState();
|
||||||
// TODO: Verify appDurations["Code"] == 60
|
// TODO: Verify appDurations["Code"] == 60
|
||||||
SUCCEED(); // Placeholder
|
SUCCEED(); // Placeholder
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_MONITOR_006: Idle Detected Pauses Tracking
|
// TI_MONITOR_006: Idle Detected Pauses Tracking
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_MONITOR_006_IdleDetectedPausesTracking", "[monitoring][integration]") {
|
TEST_CASE("TI_MONITOR_006_IdleDetectedPausesTracking", "[monitoring][integration]") {
|
||||||
MonitoringTestFixture f;
|
MonitoringTestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Start tracking
|
// Start tracking
|
||||||
f.io.injectMessage("platform:window_changed", {
|
f.io.injectMessage("platform:window_changed", {
|
||||||
{"oldApp", ""},
|
{"oldApp", ""},
|
||||||
{"newApp", "Code"},
|
{"newApp", "Code"},
|
||||||
{"duration", 0}
|
{"duration", 0}
|
||||||
});
|
});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
// Go idle
|
// Go idle
|
||||||
f.io.injectMessage("platform:idle_detected", {{"idleSeconds", 300}});
|
f.io.injectMessage("platform:idle_detected", {{"idleSeconds", 300}});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
// Verify idle state
|
// Verify idle state
|
||||||
auto state = f.module.getState();
|
auto state = f.module.getState();
|
||||||
// TODO: Verify isIdle == true
|
// TODO: Verify isIdle == true
|
||||||
SUCCEED(); // Placeholder
|
SUCCEED(); // Placeholder
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_MONITOR_007: Activity Resumed Resumes Tracking
|
// TI_MONITOR_007: Activity Resumed Resumes Tracking
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_MONITOR_007_ActivityResumedResumesTracking", "[monitoring][integration]") {
|
TEST_CASE("TI_MONITOR_007_ActivityResumedResumesTracking", "[monitoring][integration]") {
|
||||||
MonitoringTestFixture f;
|
MonitoringTestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Setup idle state
|
// Setup idle state
|
||||||
f.io.injectMessage("platform:window_changed", {{"oldApp", ""}, {"newApp", "Code"}, {"duration", 0}});
|
f.io.injectMessage("platform:window_changed", {{"oldApp", ""}, {"newApp", "Code"}, {"duration", 0}});
|
||||||
f.process();
|
f.process();
|
||||||
f.io.injectMessage("platform:idle_detected", {});
|
f.io.injectMessage("platform:idle_detected", {});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
// Resume
|
// Resume
|
||||||
f.io.injectMessage("platform:activity_resumed", {});
|
f.io.injectMessage("platform:activity_resumed", {});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
// Verify not idle
|
// Verify not idle
|
||||||
auto state = f.module.getState();
|
auto state = f.module.getState();
|
||||||
// TODO: Verify isIdle == false
|
// TODO: Verify isIdle == false
|
||||||
SUCCEED(); // Placeholder
|
SUCCEED(); // Placeholder
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_MONITOR_008: Productivity Stats
|
// TI_MONITOR_008: Productivity Stats
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_MONITOR_008_ProductivityStats", "[monitoring][integration]") {
|
TEST_CASE("TI_MONITOR_008_ProductivityStats", "[monitoring][integration]") {
|
||||||
MonitoringTestFixture f;
|
MonitoringTestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Use productive app for 60s
|
// Use productive app for 60s
|
||||||
f.io.injectMessage("platform:window_changed", {{"oldApp", ""}, {"newApp", "Code"}, {"duration", 0}});
|
f.io.injectMessage("platform:window_changed", {{"oldApp", ""}, {"newApp", "Code"}, {"duration", 0}});
|
||||||
f.process();
|
f.process();
|
||||||
f.io.injectMessage("platform:window_changed", {{"oldApp", "Code"}, {"newApp", "Discord"}, {"duration", 60}});
|
f.io.injectMessage("platform:window_changed", {{"oldApp", "Code"}, {"newApp", "Discord"}, {"duration", 60}});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
// Use distracting app for 30s
|
// Use distracting app for 30s
|
||||||
f.io.injectMessage("platform:window_changed", {{"oldApp", "Discord"}, {"newApp", "Code"}, {"duration", 30}});
|
f.io.injectMessage("platform:window_changed", {{"oldApp", "Discord"}, {"newApp", "Code"}, {"duration", 30}});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
// Verify stats
|
// Verify stats
|
||||||
auto state = f.module.getState();
|
auto state = f.module.getState();
|
||||||
// TODO: Verify totalProductiveSeconds == 60, totalDistractingSeconds == 30
|
// TODO: Verify totalProductiveSeconds == 60, totalDistractingSeconds == 30
|
||||||
SUCCEED(); // Placeholder
|
SUCCEED(); // Placeholder
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_MONITOR_009: Tool Query Get Current App
|
// TI_MONITOR_009: Tool Query Get Current App
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_MONITOR_009_ToolQueryGetCurrentApp", "[monitoring][integration]") {
|
TEST_CASE("TI_MONITOR_009_ToolQueryGetCurrentApp", "[monitoring][integration]") {
|
||||||
MonitoringTestFixture f;
|
MonitoringTestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Set current app
|
// Set current app
|
||||||
f.io.injectMessage("platform:window_changed", {{"oldApp", ""}, {"newApp", "Code"}, {"duration", 0}});
|
f.io.injectMessage("platform:window_changed", {{"oldApp", ""}, {"newApp", "Code"}, {"duration", 0}});
|
||||||
f.process();
|
f.process();
|
||||||
f.io.clearPublished();
|
f.io.clearPublished();
|
||||||
|
|
||||||
// Query
|
// Query
|
||||||
f.io.injectMessage("monitoring:query", {
|
f.io.injectMessage("monitoring:query", {
|
||||||
{"action", "get_current_app"},
|
{"action", "get_current_app"},
|
||||||
{"correlation_id", "test-456"}
|
{"correlation_id", "test-456"}
|
||||||
});
|
});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
// Verify response
|
// Verify response
|
||||||
REQUIRE(f.io.wasPublished("monitoring:response"));
|
REQUIRE(f.io.wasPublished("monitoring:response"));
|
||||||
auto resp = f.io.getLastPublished("monitoring:response");
|
auto resp = f.io.getLastPublished("monitoring:response");
|
||||||
REQUIRE(resp["correlation_id"] == "test-456");
|
REQUIRE(resp["correlation_id"] == "test-456");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_MONITOR_010: State Serialization
|
// TI_MONITOR_010: State Serialization
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_MONITOR_010_StateSerialization", "[monitoring][integration]") {
|
TEST_CASE("TI_MONITOR_010_StateSerialization", "[monitoring][integration]") {
|
||||||
MonitoringTestFixture f;
|
MonitoringTestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Build up some state
|
// Build up some state
|
||||||
f.io.injectMessage("platform:window_changed", {{"oldApp", ""}, {"newApp", "Code"}, {"duration", 0}});
|
f.io.injectMessage("platform:window_changed", {{"oldApp", ""}, {"newApp", "Code"}, {"duration", 0}});
|
||||||
f.process();
|
f.process();
|
||||||
f.io.injectMessage("platform:window_changed", {{"oldApp", "Code"}, {"newApp", "Discord"}, {"duration", 120}});
|
f.io.injectMessage("platform:window_changed", {{"oldApp", "Code"}, {"newApp", "Discord"}, {"duration", 120}});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
// Get state
|
// Get state
|
||||||
auto state = f.module.getState();
|
auto state = f.module.getState();
|
||||||
REQUIRE(state != nullptr);
|
REQUIRE(state != nullptr);
|
||||||
|
|
||||||
// Restore to new module
|
// Restore to new module
|
||||||
MonitoringModule module2;
|
MonitoringModule module2;
|
||||||
grove::JsonDataNode configNode2("config", json::object());
|
grove::JsonDataNode configNode2("config", json::object());
|
||||||
module2.setConfiguration(configNode2, &f.io, nullptr);
|
module2.setConfiguration(configNode2, &f.io, nullptr);
|
||||||
module2.setState(*state);
|
module2.setState(*state);
|
||||||
|
|
||||||
auto state2 = module2.getState();
|
auto state2 = module2.getState();
|
||||||
REQUIRE(state2 != nullptr);
|
REQUIRE(state2 != nullptr);
|
||||||
SUCCEED(); // Placeholder
|
SUCCEED(); // Placeholder
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,303 +1,303 @@
|
|||||||
/**
|
/**
|
||||||
* @file NotificationModuleTests.cpp
|
* @file NotificationModuleTests.cpp
|
||||||
* @brief Integration tests for NotificationModule (10 TI)
|
* @brief Integration tests for NotificationModule (10 TI)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <catch2/catch_test_macros.hpp>
|
#include <catch2/catch_test_macros.hpp>
|
||||||
#include "mocks/MockIO.hpp"
|
#include "mocks/MockIO.hpp"
|
||||||
#include "utils/TimeSimulator.hpp"
|
#include "utils/TimeSimulator.hpp"
|
||||||
#include "utils/TestHelpers.hpp"
|
#include "utils/TestHelpers.hpp"
|
||||||
|
|
||||||
#include "modules/NotificationModule.h"
|
#include "modules/NotificationModule.h"
|
||||||
#include <grove/JsonDataNode.h>
|
#include <grove/JsonDataNode.h>
|
||||||
|
|
||||||
using namespace aissia;
|
using namespace aissia;
|
||||||
using namespace aissia::tests;
|
using namespace aissia::tests;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Test Fixture
|
// Test Fixture
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
class NotificationTestFixture {
|
class NotificationTestFixture {
|
||||||
public:
|
public:
|
||||||
MockIO io;
|
MockIO io;
|
||||||
TimeSimulator time;
|
TimeSimulator time;
|
||||||
NotificationModule module;
|
NotificationModule module;
|
||||||
|
|
||||||
void configure(const json& config = json::object()) {
|
void configure(const json& config = json::object()) {
|
||||||
json fullConfig = {
|
json fullConfig = {
|
||||||
{"language", "fr"},
|
{"language", "fr"},
|
||||||
{"silentMode", false},
|
{"silentMode", false},
|
||||||
{"ttsEnabled", false},
|
{"ttsEnabled", false},
|
||||||
{"maxQueueSize", 50}
|
{"maxQueueSize", 50}
|
||||||
};
|
};
|
||||||
fullConfig.merge_patch(config);
|
fullConfig.merge_patch(config);
|
||||||
|
|
||||||
grove::JsonDataNode configNode("config", fullConfig);
|
grove::JsonDataNode configNode("config", fullConfig);
|
||||||
module.setConfiguration(configNode, &io, nullptr);
|
module.setConfiguration(configNode, &io, nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
void process() {
|
void process() {
|
||||||
grove::JsonDataNode input("input", time.createInput());
|
grove::JsonDataNode input("input", time.createInput());
|
||||||
module.process(input);
|
module.process(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
int getPendingCount() {
|
int getPendingCount() {
|
||||||
auto state = module.getState();
|
auto state = module.getState();
|
||||||
return state ? state->getInt("pendingCount", -1) : -1;
|
return state ? state->getInt("pendingCount", -1) : -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
int getNotificationCount() {
|
int getNotificationCount() {
|
||||||
auto state = module.getState();
|
auto state = module.getState();
|
||||||
return state ? state->getInt("notificationCount", -1) : -1;
|
return state ? state->getInt("notificationCount", -1) : -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
int getUrgentCount() {
|
int getUrgentCount() {
|
||||||
auto state = module.getState();
|
auto state = module.getState();
|
||||||
return state ? state->getInt("urgentCount", -1) : -1;
|
return state ? state->getInt("urgentCount", -1) : -1;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_NOTIF_001: Queue Notification
|
// TI_NOTIF_001: Queue Notification
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_NOTIF_001_QueueNotification", "[notification][integration]") {
|
TEST_CASE("TI_NOTIF_001_QueueNotification", "[notification][integration]") {
|
||||||
NotificationTestFixture f;
|
NotificationTestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Add notification
|
// Add notification
|
||||||
f.module.notify("Test Title", "Test Message", NotificationModule::Priority::NORMAL);
|
f.module.notify("Test Title", "Test Message", NotificationModule::Priority::NORMAL);
|
||||||
|
|
||||||
// Verify queue has 1 item (before processing)
|
// Verify queue has 1 item (before processing)
|
||||||
REQUIRE(f.getPendingCount() == 1);
|
REQUIRE(f.getPendingCount() == 1);
|
||||||
|
|
||||||
// Verify notification count incremented
|
// Verify notification count incremented
|
||||||
REQUIRE(f.getNotificationCount() == 1);
|
REQUIRE(f.getNotificationCount() == 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_NOTIF_002: Process Queue (max 3 per frame)
|
// TI_NOTIF_002: Process Queue (max 3 per frame)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_NOTIF_002_ProcessQueue", "[notification][integration]") {
|
TEST_CASE("TI_NOTIF_002_ProcessQueue", "[notification][integration]") {
|
||||||
NotificationTestFixture f;
|
NotificationTestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Add 5 notifications
|
// Add 5 notifications
|
||||||
for (int i = 0; i < 5; i++) {
|
for (int i = 0; i < 5; i++) {
|
||||||
f.module.notify("Title", "Message " + std::to_string(i), NotificationModule::Priority::NORMAL);
|
f.module.notify("Title", "Message " + std::to_string(i), NotificationModule::Priority::NORMAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify 5 pending before process
|
// Verify 5 pending before process
|
||||||
REQUIRE(f.getPendingCount() == 5);
|
REQUIRE(f.getPendingCount() == 5);
|
||||||
|
|
||||||
// Process one frame (should handle max 3)
|
// Process one frame (should handle max 3)
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
// Verify 2 remaining in queue
|
// Verify 2 remaining in queue
|
||||||
REQUIRE(f.getPendingCount() == 2);
|
REQUIRE(f.getPendingCount() == 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_NOTIF_003: Priority Ordering
|
// TI_NOTIF_003: Priority Ordering
|
||||||
// NOTE: Current implementation uses FIFO queue without priority sorting.
|
// NOTE: Current implementation uses FIFO queue without priority sorting.
|
||||||
// This test verifies that URGENT notifications can still be added
|
// This test verifies that URGENT notifications can still be added
|
||||||
// alongside other priorities. True priority ordering would require
|
// alongside other priorities. True priority ordering would require
|
||||||
// a priority queue implementation.
|
// a priority queue implementation.
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_NOTIF_003_PriorityOrdering", "[notification][integration]") {
|
TEST_CASE("TI_NOTIF_003_PriorityOrdering", "[notification][integration]") {
|
||||||
NotificationTestFixture f;
|
NotificationTestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Add notifications in reverse priority order
|
// Add notifications in reverse priority order
|
||||||
f.module.notify("Low", "Low priority", NotificationModule::Priority::LOW);
|
f.module.notify("Low", "Low priority", NotificationModule::Priority::LOW);
|
||||||
f.module.notify("Urgent", "Urgent priority", NotificationModule::Priority::URGENT);
|
f.module.notify("Urgent", "Urgent priority", NotificationModule::Priority::URGENT);
|
||||||
f.module.notify("Normal", "Normal priority", NotificationModule::Priority::NORMAL);
|
f.module.notify("Normal", "Normal priority", NotificationModule::Priority::NORMAL);
|
||||||
|
|
||||||
// Verify all 3 are queued
|
// Verify all 3 are queued
|
||||||
REQUIRE(f.getPendingCount() == 3);
|
REQUIRE(f.getPendingCount() == 3);
|
||||||
|
|
||||||
// Verify urgent count is tracked
|
// Verify urgent count is tracked
|
||||||
REQUIRE(f.getUrgentCount() == 1);
|
REQUIRE(f.getUrgentCount() == 1);
|
||||||
|
|
||||||
// Process - verify all are processed
|
// Process - verify all are processed
|
||||||
f.process();
|
f.process();
|
||||||
REQUIRE(f.getPendingCount() == 0);
|
REQUIRE(f.getPendingCount() == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_NOTIF_004: Silent Mode Blocks Non-Urgent
|
// TI_NOTIF_004: Silent Mode Blocks Non-Urgent
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_NOTIF_004_SilentModeBlocksNonUrgent", "[notification][integration]") {
|
TEST_CASE("TI_NOTIF_004_SilentModeBlocksNonUrgent", "[notification][integration]") {
|
||||||
NotificationTestFixture f;
|
NotificationTestFixture f;
|
||||||
f.configure({{"silentMode", true}});
|
f.configure({{"silentMode", true}});
|
||||||
|
|
||||||
// Add non-urgent notifications
|
// Add non-urgent notifications
|
||||||
f.module.notify("Low", "Should be blocked", NotificationModule::Priority::LOW);
|
f.module.notify("Low", "Should be blocked", NotificationModule::Priority::LOW);
|
||||||
f.module.notify("Normal", "Should be blocked", NotificationModule::Priority::NORMAL);
|
f.module.notify("Normal", "Should be blocked", NotificationModule::Priority::NORMAL);
|
||||||
f.module.notify("High", "Should be blocked", NotificationModule::Priority::HIGH);
|
f.module.notify("High", "Should be blocked", NotificationModule::Priority::HIGH);
|
||||||
|
|
||||||
// Verify all were blocked (queue empty)
|
// Verify all were blocked (queue empty)
|
||||||
REQUIRE(f.getPendingCount() == 0);
|
REQUIRE(f.getPendingCount() == 0);
|
||||||
|
|
||||||
// Verify notification count was NOT incremented for blocked notifications
|
// Verify notification count was NOT incremented for blocked notifications
|
||||||
// Note: Current implementation increments count before checking silentMode
|
// Note: Current implementation increments count before checking silentMode
|
||||||
// So count will be 0 (notify returns early before incrementing)
|
// So count will be 0 (notify returns early before incrementing)
|
||||||
REQUIRE(f.getNotificationCount() == 0);
|
REQUIRE(f.getNotificationCount() == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_NOTIF_005: Silent Mode Allows Urgent
|
// TI_NOTIF_005: Silent Mode Allows Urgent
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_NOTIF_005_SilentModeAllowsUrgent", "[notification][integration]") {
|
TEST_CASE("TI_NOTIF_005_SilentModeAllowsUrgent", "[notification][integration]") {
|
||||||
NotificationTestFixture f;
|
NotificationTestFixture f;
|
||||||
f.configure({{"silentMode", true}});
|
f.configure({{"silentMode", true}});
|
||||||
|
|
||||||
// Add urgent notification
|
// Add urgent notification
|
||||||
f.module.notify("Urgent", "Should pass", NotificationModule::Priority::URGENT);
|
f.module.notify("Urgent", "Should pass", NotificationModule::Priority::URGENT);
|
||||||
|
|
||||||
// Verify URGENT notification was queued
|
// Verify URGENT notification was queued
|
||||||
REQUIRE(f.getPendingCount() == 1);
|
REQUIRE(f.getPendingCount() == 1);
|
||||||
|
|
||||||
// Verify counts
|
// Verify counts
|
||||||
REQUIRE(f.getNotificationCount() == 1);
|
REQUIRE(f.getNotificationCount() == 1);
|
||||||
REQUIRE(f.getUrgentCount() == 1);
|
REQUIRE(f.getUrgentCount() == 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_NOTIF_006: Max Queue Size
|
// TI_NOTIF_006: Max Queue Size
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_NOTIF_006_MaxQueueSize", "[notification][integration]") {
|
TEST_CASE("TI_NOTIF_006_MaxQueueSize", "[notification][integration]") {
|
||||||
NotificationTestFixture f;
|
NotificationTestFixture f;
|
||||||
f.configure({{"maxQueueSize", 5}});
|
f.configure({{"maxQueueSize", 5}});
|
||||||
|
|
||||||
// Add more than max (10 notifications)
|
// Add more than max (10 notifications)
|
||||||
for (int i = 0; i < 10; i++) {
|
for (int i = 0; i < 10; i++) {
|
||||||
f.module.notify("Title", "Message " + std::to_string(i), NotificationModule::Priority::NORMAL);
|
f.module.notify("Title", "Message " + std::to_string(i), NotificationModule::Priority::NORMAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify queue is capped at maxQueueSize
|
// Verify queue is capped at maxQueueSize
|
||||||
REQUIRE(f.getPendingCount() <= 5);
|
REQUIRE(f.getPendingCount() <= 5);
|
||||||
|
|
||||||
// Notification count should still reflect all attempts
|
// Notification count should still reflect all attempts
|
||||||
REQUIRE(f.getNotificationCount() == 10);
|
REQUIRE(f.getNotificationCount() == 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_NOTIF_007: Language Config
|
// TI_NOTIF_007: Language Config
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_NOTIF_007_LanguageConfig", "[notification][integration]") {
|
TEST_CASE("TI_NOTIF_007_LanguageConfig", "[notification][integration]") {
|
||||||
NotificationTestFixture f;
|
NotificationTestFixture f;
|
||||||
f.configure({{"language", "en"}});
|
f.configure({{"language", "en"}});
|
||||||
|
|
||||||
// Verify module accepted configuration (no crash)
|
// Verify module accepted configuration (no crash)
|
||||||
// The language is stored internally and used for notification display
|
// The language is stored internally and used for notification display
|
||||||
// We can verify via getHealthStatus which doesn't expose language directly
|
// We can verify via getHealthStatus which doesn't expose language directly
|
||||||
auto health = f.module.getHealthStatus();
|
auto health = f.module.getHealthStatus();
|
||||||
REQUIRE(health != nullptr);
|
REQUIRE(health != nullptr);
|
||||||
REQUIRE(health->getString("status", "") == "running");
|
REQUIRE(health->getString("status", "") == "running");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_NOTIF_008: Notification Count Tracking
|
// TI_NOTIF_008: Notification Count Tracking
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_NOTIF_008_NotificationCountTracking", "[notification][integration]") {
|
TEST_CASE("TI_NOTIF_008_NotificationCountTracking", "[notification][integration]") {
|
||||||
NotificationTestFixture f;
|
NotificationTestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Add various notifications
|
// Add various notifications
|
||||||
f.module.notify("Normal1", "msg", NotificationModule::Priority::NORMAL);
|
f.module.notify("Normal1", "msg", NotificationModule::Priority::NORMAL);
|
||||||
f.module.notify("Urgent1", "msg", NotificationModule::Priority::URGENT);
|
f.module.notify("Urgent1", "msg", NotificationModule::Priority::URGENT);
|
||||||
f.module.notify("Urgent2", "msg", NotificationModule::Priority::URGENT);
|
f.module.notify("Urgent2", "msg", NotificationModule::Priority::URGENT);
|
||||||
f.module.notify("Low1", "msg", NotificationModule::Priority::LOW);
|
f.module.notify("Low1", "msg", NotificationModule::Priority::LOW);
|
||||||
|
|
||||||
// Verify counts
|
// Verify counts
|
||||||
REQUIRE(f.getNotificationCount() == 4);
|
REQUIRE(f.getNotificationCount() == 4);
|
||||||
REQUIRE(f.getUrgentCount() == 2);
|
REQUIRE(f.getUrgentCount() == 2);
|
||||||
REQUIRE(f.getPendingCount() == 4);
|
REQUIRE(f.getPendingCount() == 4);
|
||||||
|
|
||||||
// Process all
|
// Process all
|
||||||
f.process(); // processes 3
|
f.process(); // processes 3
|
||||||
f.process(); // processes 1
|
f.process(); // processes 1
|
||||||
|
|
||||||
// Verify queue empty but counts preserved
|
// Verify queue empty but counts preserved
|
||||||
REQUIRE(f.getPendingCount() == 0);
|
REQUIRE(f.getPendingCount() == 0);
|
||||||
REQUIRE(f.getNotificationCount() == 4);
|
REQUIRE(f.getNotificationCount() == 4);
|
||||||
REQUIRE(f.getUrgentCount() == 2);
|
REQUIRE(f.getUrgentCount() == 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_NOTIF_009: State Serialization
|
// TI_NOTIF_009: State Serialization
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_NOTIF_009_StateSerialization", "[notification][integration]") {
|
TEST_CASE("TI_NOTIF_009_StateSerialization", "[notification][integration]") {
|
||||||
NotificationTestFixture f;
|
NotificationTestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Create some state
|
// Create some state
|
||||||
f.module.notify("Test1", "msg", NotificationModule::Priority::NORMAL);
|
f.module.notify("Test1", "msg", NotificationModule::Priority::NORMAL);
|
||||||
f.module.notify("Test2", "msg", NotificationModule::Priority::URGENT);
|
f.module.notify("Test2", "msg", NotificationModule::Priority::URGENT);
|
||||||
f.process(); // Process some
|
f.process(); // Process some
|
||||||
|
|
||||||
// Get state
|
// Get state
|
||||||
auto state = f.module.getState();
|
auto state = f.module.getState();
|
||||||
REQUIRE(state != nullptr);
|
REQUIRE(state != nullptr);
|
||||||
|
|
||||||
// Verify state contains expected fields
|
// Verify state contains expected fields
|
||||||
REQUIRE(state->getInt("notificationCount", -1) == 2);
|
REQUIRE(state->getInt("notificationCount", -1) == 2);
|
||||||
REQUIRE(state->getInt("urgentCount", -1) == 1);
|
REQUIRE(state->getInt("urgentCount", -1) == 1);
|
||||||
|
|
||||||
// Create new module and restore
|
// Create new module and restore
|
||||||
NotificationModule module2;
|
NotificationModule module2;
|
||||||
MockIO io2;
|
MockIO io2;
|
||||||
grove::JsonDataNode configNode("config", json::object());
|
grove::JsonDataNode configNode("config", json::object());
|
||||||
module2.setConfiguration(configNode, &io2, nullptr);
|
module2.setConfiguration(configNode, &io2, nullptr);
|
||||||
module2.setState(*state);
|
module2.setState(*state);
|
||||||
|
|
||||||
// Verify counters were restored
|
// Verify counters were restored
|
||||||
auto state2 = module2.getState();
|
auto state2 = module2.getState();
|
||||||
REQUIRE(state2 != nullptr);
|
REQUIRE(state2 != nullptr);
|
||||||
REQUIRE(state2->getInt("notificationCount", -1) == 2);
|
REQUIRE(state2->getInt("notificationCount", -1) == 2);
|
||||||
REQUIRE(state2->getInt("urgentCount", -1) == 1);
|
REQUIRE(state2->getInt("urgentCount", -1) == 1);
|
||||||
// Note: pending queue is NOT restored (documented behavior)
|
// Note: pending queue is NOT restored (documented behavior)
|
||||||
REQUIRE(state2->getInt("pendingCount", -1) == 0);
|
REQUIRE(state2->getInt("pendingCount", -1) == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_NOTIF_010: Multiple Frame Processing
|
// TI_NOTIF_010: Multiple Frame Processing
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_NOTIF_010_MultipleFrameProcessing", "[notification][integration]") {
|
TEST_CASE("TI_NOTIF_010_MultipleFrameProcessing", "[notification][integration]") {
|
||||||
NotificationTestFixture f;
|
NotificationTestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Add 7 notifications (needs 3 frames to process at 3/frame)
|
// Add 7 notifications (needs 3 frames to process at 3/frame)
|
||||||
for (int i = 0; i < 7; i++) {
|
for (int i = 0; i < 7; i++) {
|
||||||
f.module.notify("Title", "Message " + std::to_string(i), NotificationModule::Priority::NORMAL);
|
f.module.notify("Title", "Message " + std::to_string(i), NotificationModule::Priority::NORMAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify initial count
|
// Verify initial count
|
||||||
REQUIRE(f.getPendingCount() == 7);
|
REQUIRE(f.getPendingCount() == 7);
|
||||||
|
|
||||||
// Frame 1: 3 processed, 4 remaining
|
// Frame 1: 3 processed, 4 remaining
|
||||||
f.process();
|
f.process();
|
||||||
REQUIRE(f.getPendingCount() == 4);
|
REQUIRE(f.getPendingCount() == 4);
|
||||||
|
|
||||||
// Frame 2: 3 processed, 1 remaining
|
// Frame 2: 3 processed, 1 remaining
|
||||||
f.process();
|
f.process();
|
||||||
REQUIRE(f.getPendingCount() == 1);
|
REQUIRE(f.getPendingCount() == 1);
|
||||||
|
|
||||||
// Frame 3: 1 processed, 0 remaining
|
// Frame 3: 1 processed, 0 remaining
|
||||||
f.process();
|
f.process();
|
||||||
REQUIRE(f.getPendingCount() == 0);
|
REQUIRE(f.getPendingCount() == 0);
|
||||||
|
|
||||||
// Total notification count should be unchanged
|
// Total notification count should be unchanged
|
||||||
REQUIRE(f.getNotificationCount() == 7);
|
REQUIRE(f.getNotificationCount() == 7);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,315 +1,315 @@
|
|||||||
/**
|
/**
|
||||||
* @file SchedulerModuleTests.cpp
|
* @file SchedulerModuleTests.cpp
|
||||||
* @brief Integration tests for SchedulerModule (10 TI)
|
* @brief Integration tests for SchedulerModule (10 TI)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <catch2/catch_test_macros.hpp>
|
#include <catch2/catch_test_macros.hpp>
|
||||||
#include "mocks/MockIO.hpp"
|
#include "mocks/MockIO.hpp"
|
||||||
#include "utils/TimeSimulator.hpp"
|
#include "utils/TimeSimulator.hpp"
|
||||||
#include "utils/TestHelpers.hpp"
|
#include "utils/TestHelpers.hpp"
|
||||||
|
|
||||||
#include "modules/SchedulerModule.h"
|
#include "modules/SchedulerModule.h"
|
||||||
#include <grove/JsonDataNode.h>
|
#include <grove/JsonDataNode.h>
|
||||||
|
|
||||||
using namespace aissia;
|
using namespace aissia;
|
||||||
using namespace aissia::tests;
|
using namespace aissia::tests;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Test Fixture
|
// Test Fixture
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
class SchedulerTestFixture {
|
class SchedulerTestFixture {
|
||||||
public:
|
public:
|
||||||
MockIO io;
|
MockIO io;
|
||||||
TimeSimulator time;
|
TimeSimulator time;
|
||||||
SchedulerModule module;
|
SchedulerModule module;
|
||||||
|
|
||||||
void configure(const json& config = json::object()) {
|
void configure(const json& config = json::object()) {
|
||||||
json fullConfig = {
|
json fullConfig = {
|
||||||
{"hyperfocusThresholdMinutes", 120},
|
{"hyperfocusThresholdMinutes", 120},
|
||||||
{"breakReminderIntervalMinutes", 45},
|
{"breakReminderIntervalMinutes", 45},
|
||||||
{"breakDurationMinutes", 10}
|
{"breakDurationMinutes", 10}
|
||||||
};
|
};
|
||||||
fullConfig.merge_patch(config);
|
fullConfig.merge_patch(config);
|
||||||
|
|
||||||
grove::JsonDataNode configNode("config", fullConfig);
|
grove::JsonDataNode configNode("config", fullConfig);
|
||||||
module.setConfiguration(configNode, &io, nullptr);
|
module.setConfiguration(configNode, &io, nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
void process() {
|
void process() {
|
||||||
grove::JsonDataNode input("input", time.createInput());
|
grove::JsonDataNode input("input", time.createInput());
|
||||||
module.process(input);
|
module.process(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
void processWithTime(float gameTime) {
|
void processWithTime(float gameTime) {
|
||||||
time.setTime(gameTime);
|
time.setTime(gameTime);
|
||||||
grove::JsonDataNode input("input", time.createInput(0.1f));
|
grove::JsonDataNode input("input", time.createInput(0.1f));
|
||||||
module.process(input);
|
module.process(input);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_SCHEDULER_001: Start Task
|
// TI_SCHEDULER_001: Start Task
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_SCHEDULER_001_StartTask", "[scheduler][integration]") {
|
TEST_CASE("TI_SCHEDULER_001_StartTask", "[scheduler][integration]") {
|
||||||
SchedulerTestFixture f;
|
SchedulerTestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Inject task switch message
|
// Inject task switch message
|
||||||
f.io.injectMessage("user:task_switch", {{"taskId", "task-1"}});
|
f.io.injectMessage("user:task_switch", {{"taskId", "task-1"}});
|
||||||
|
|
||||||
// Process
|
// Process
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
// Verify task_started was published
|
// Verify task_started was published
|
||||||
REQUIRE(f.io.wasPublished("scheduler:task_started"));
|
REQUIRE(f.io.wasPublished("scheduler:task_started"));
|
||||||
auto msg = f.io.getLastPublished("scheduler:task_started");
|
auto msg = f.io.getLastPublished("scheduler:task_started");
|
||||||
REQUIRE(msg["taskId"] == "task-1");
|
REQUIRE(msg["taskId"] == "task-1");
|
||||||
REQUIRE(msg.contains("taskName"));
|
REQUIRE(msg.contains("taskName"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_SCHEDULER_002: Complete Task
|
// TI_SCHEDULER_002: Complete Task
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_SCHEDULER_002_CompleteTask", "[scheduler][integration]") {
|
TEST_CASE("TI_SCHEDULER_002_CompleteTask", "[scheduler][integration]") {
|
||||||
SchedulerTestFixture f;
|
SchedulerTestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Start a task at time 0
|
// Start a task at time 0
|
||||||
f.io.injectMessage("user:task_switch", {{"taskId", "task-1"}});
|
f.io.injectMessage("user:task_switch", {{"taskId", "task-1"}});
|
||||||
f.processWithTime(0.0f);
|
f.processWithTime(0.0f);
|
||||||
f.io.clearPublished();
|
f.io.clearPublished();
|
||||||
|
|
||||||
// Advance time 30 minutes (1800 seconds)
|
// Advance time 30 minutes (1800 seconds)
|
||||||
f.time.setTime(1800.0f);
|
f.time.setTime(1800.0f);
|
||||||
|
|
||||||
// Switch to another task (completes current task implicitly)
|
// Switch to another task (completes current task implicitly)
|
||||||
f.io.injectMessage("user:task_switch", {{"taskId", "task-2"}});
|
f.io.injectMessage("user:task_switch", {{"taskId", "task-2"}});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
// Verify task_completed was published with duration
|
// Verify task_completed was published with duration
|
||||||
REQUIRE(f.io.wasPublished("scheduler:task_completed"));
|
REQUIRE(f.io.wasPublished("scheduler:task_completed"));
|
||||||
auto msg = f.io.getLastPublished("scheduler:task_completed");
|
auto msg = f.io.getLastPublished("scheduler:task_completed");
|
||||||
REQUIRE(msg["taskId"] == "task-1");
|
REQUIRE(msg["taskId"] == "task-1");
|
||||||
REQUIRE(msg.contains("duration"));
|
REQUIRE(msg.contains("duration"));
|
||||||
// Duration should be around 30 minutes
|
// Duration should be around 30 minutes
|
||||||
int duration = msg["duration"].get<int>();
|
int duration = msg["duration"].get<int>();
|
||||||
REQUIRE(duration >= 29);
|
REQUIRE(duration >= 29);
|
||||||
REQUIRE(duration <= 31);
|
REQUIRE(duration <= 31);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_SCHEDULER_003: Hyperfocus Detection
|
// TI_SCHEDULER_003: Hyperfocus Detection
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_SCHEDULER_003_HyperfocusDetection", "[scheduler][integration]") {
|
TEST_CASE("TI_SCHEDULER_003_HyperfocusDetection", "[scheduler][integration]") {
|
||||||
SchedulerTestFixture f;
|
SchedulerTestFixture f;
|
||||||
f.configure({{"hyperfocusThresholdMinutes", 120}});
|
f.configure({{"hyperfocusThresholdMinutes", 120}});
|
||||||
|
|
||||||
// Start a task at time 0
|
// Start a task at time 0
|
||||||
f.io.injectMessage("user:task_switch", {{"taskId", "task-1"}});
|
f.io.injectMessage("user:task_switch", {{"taskId", "task-1"}});
|
||||||
f.processWithTime(0.0f);
|
f.processWithTime(0.0f);
|
||||||
f.io.clearPublished();
|
f.io.clearPublished();
|
||||||
|
|
||||||
// Advance time past threshold (121 minutes = 7260 seconds)
|
// Advance time past threshold (121 minutes = 7260 seconds)
|
||||||
f.processWithTime(7260.0f);
|
f.processWithTime(7260.0f);
|
||||||
|
|
||||||
// Verify hyperfocus alert
|
// Verify hyperfocus alert
|
||||||
REQUIRE(f.io.wasPublished("scheduler:hyperfocus_alert"));
|
REQUIRE(f.io.wasPublished("scheduler:hyperfocus_alert"));
|
||||||
auto msg = f.io.getLastPublished("scheduler:hyperfocus_alert");
|
auto msg = f.io.getLastPublished("scheduler:hyperfocus_alert");
|
||||||
REQUIRE(msg["type"] == "hyperfocus");
|
REQUIRE(msg["type"] == "hyperfocus");
|
||||||
REQUIRE(msg["task"] == "task-1");
|
REQUIRE(msg["task"] == "task-1");
|
||||||
REQUIRE(msg["duration_minutes"].get<int>() >= 120);
|
REQUIRE(msg["duration_minutes"].get<int>() >= 120);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_SCHEDULER_004: Hyperfocus Alert Only Once
|
// TI_SCHEDULER_004: Hyperfocus Alert Only Once
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_SCHEDULER_004_HyperfocusAlertOnce", "[scheduler][integration]") {
|
TEST_CASE("TI_SCHEDULER_004_HyperfocusAlertOnce", "[scheduler][integration]") {
|
||||||
SchedulerTestFixture f;
|
SchedulerTestFixture f;
|
||||||
f.configure({{"hyperfocusThresholdMinutes", 120}});
|
f.configure({{"hyperfocusThresholdMinutes", 120}});
|
||||||
|
|
||||||
// Start task
|
// Start task
|
||||||
f.io.injectMessage("user:task_switch", {{"taskId", "task-1"}});
|
f.io.injectMessage("user:task_switch", {{"taskId", "task-1"}});
|
||||||
f.processWithTime(0.0f);
|
f.processWithTime(0.0f);
|
||||||
|
|
||||||
// Trigger hyperfocus (121 min)
|
// Trigger hyperfocus (121 min)
|
||||||
f.processWithTime(7260.0f);
|
f.processWithTime(7260.0f);
|
||||||
|
|
||||||
// Count first alert
|
// Count first alert
|
||||||
size_t alertCount = f.io.countPublished("scheduler:hyperfocus_alert");
|
size_t alertCount = f.io.countPublished("scheduler:hyperfocus_alert");
|
||||||
REQUIRE(alertCount == 1);
|
REQUIRE(alertCount == 1);
|
||||||
|
|
||||||
// Continue processing (130 min, 140 min)
|
// Continue processing (130 min, 140 min)
|
||||||
f.processWithTime(7800.0f);
|
f.processWithTime(7800.0f);
|
||||||
f.processWithTime(8400.0f);
|
f.processWithTime(8400.0f);
|
||||||
|
|
||||||
// Should still be only 1 alert
|
// Should still be only 1 alert
|
||||||
REQUIRE(f.io.countPublished("scheduler:hyperfocus_alert") == 1);
|
REQUIRE(f.io.countPublished("scheduler:hyperfocus_alert") == 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_SCHEDULER_005: Break Reminder
|
// TI_SCHEDULER_005: Break Reminder
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_SCHEDULER_005_BreakReminder", "[scheduler][integration]") {
|
TEST_CASE("TI_SCHEDULER_005_BreakReminder", "[scheduler][integration]") {
|
||||||
SchedulerTestFixture f;
|
SchedulerTestFixture f;
|
||||||
f.configure({{"breakReminderIntervalMinutes", 45}});
|
f.configure({{"breakReminderIntervalMinutes", 45}});
|
||||||
|
|
||||||
// Process at time 0 (sets lastBreakTime)
|
// Process at time 0 (sets lastBreakTime)
|
||||||
f.processWithTime(0.0f);
|
f.processWithTime(0.0f);
|
||||||
f.io.clearPublished();
|
f.io.clearPublished();
|
||||||
|
|
||||||
// Advance past break reminder interval (46 minutes = 2760 seconds)
|
// Advance past break reminder interval (46 minutes = 2760 seconds)
|
||||||
f.processWithTime(2760.0f);
|
f.processWithTime(2760.0f);
|
||||||
|
|
||||||
// Verify break reminder
|
// Verify break reminder
|
||||||
REQUIRE(f.io.wasPublished("scheduler:break_reminder"));
|
REQUIRE(f.io.wasPublished("scheduler:break_reminder"));
|
||||||
auto msg = f.io.getLastPublished("scheduler:break_reminder");
|
auto msg = f.io.getLastPublished("scheduler:break_reminder");
|
||||||
REQUIRE(msg["type"] == "break");
|
REQUIRE(msg["type"] == "break");
|
||||||
REQUIRE(msg.contains("break_duration"));
|
REQUIRE(msg.contains("break_duration"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_SCHEDULER_006: Idle Pauses Session
|
// TI_SCHEDULER_006: Idle Pauses Session
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_SCHEDULER_006_IdlePausesSession", "[scheduler][integration]") {
|
TEST_CASE("TI_SCHEDULER_006_IdlePausesSession", "[scheduler][integration]") {
|
||||||
SchedulerTestFixture f;
|
SchedulerTestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Start task
|
// Start task
|
||||||
f.io.injectMessage("user:task_switch", {{"taskId", "task-1"}});
|
f.io.injectMessage("user:task_switch", {{"taskId", "task-1"}});
|
||||||
f.processWithTime(0.0f);
|
f.processWithTime(0.0f);
|
||||||
|
|
||||||
// Go idle
|
// Go idle
|
||||||
f.io.injectMessage("monitoring:idle_detected", {{"idleSeconds", 300}});
|
f.io.injectMessage("monitoring:idle_detected", {{"idleSeconds", 300}});
|
||||||
f.processWithTime(60.0f);
|
f.processWithTime(60.0f);
|
||||||
|
|
||||||
// Verify module received and processed the idle message
|
// Verify module received and processed the idle message
|
||||||
// (Module logs "User idle" - we can verify via state)
|
// (Module logs "User idle" - we can verify via state)
|
||||||
auto state = f.module.getState();
|
auto state = f.module.getState();
|
||||||
REQUIRE(state != nullptr);
|
REQUIRE(state != nullptr);
|
||||||
// Task should still be tracked (idle doesn't clear it)
|
// Task should still be tracked (idle doesn't clear it)
|
||||||
REQUIRE(state->getString("currentTaskId", "") == "task-1");
|
REQUIRE(state->getString("currentTaskId", "") == "task-1");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_SCHEDULER_007: Activity Resumes Session
|
// TI_SCHEDULER_007: Activity Resumes Session
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_SCHEDULER_007_ActivityResumesSession", "[scheduler][integration]") {
|
TEST_CASE("TI_SCHEDULER_007_ActivityResumesSession", "[scheduler][integration]") {
|
||||||
SchedulerTestFixture f;
|
SchedulerTestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Start task, go idle, resume
|
// Start task, go idle, resume
|
||||||
f.io.injectMessage("user:task_switch", {{"taskId", "task-1"}});
|
f.io.injectMessage("user:task_switch", {{"taskId", "task-1"}});
|
||||||
f.processWithTime(0.0f);
|
f.processWithTime(0.0f);
|
||||||
f.io.injectMessage("monitoring:idle_detected", {});
|
f.io.injectMessage("monitoring:idle_detected", {});
|
||||||
f.processWithTime(60.0f);
|
f.processWithTime(60.0f);
|
||||||
f.io.injectMessage("monitoring:activity_resumed", {});
|
f.io.injectMessage("monitoring:activity_resumed", {});
|
||||||
f.processWithTime(120.0f);
|
f.processWithTime(120.0f);
|
||||||
|
|
||||||
// Verify session continues - task still active
|
// Verify session continues - task still active
|
||||||
auto state = f.module.getState();
|
auto state = f.module.getState();
|
||||||
REQUIRE(state != nullptr);
|
REQUIRE(state != nullptr);
|
||||||
REQUIRE(state->getString("currentTaskId", "") == "task-1");
|
REQUIRE(state->getString("currentTaskId", "") == "task-1");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_SCHEDULER_008: Tool Query Get Current Task
|
// TI_SCHEDULER_008: Tool Query Get Current Task
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_SCHEDULER_008_ToolQueryGetCurrentTask", "[scheduler][integration]") {
|
TEST_CASE("TI_SCHEDULER_008_ToolQueryGetCurrentTask", "[scheduler][integration]") {
|
||||||
SchedulerTestFixture f;
|
SchedulerTestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Start a task
|
// Start a task
|
||||||
f.io.injectMessage("user:task_switch", {{"taskId", "task-1"}});
|
f.io.injectMessage("user:task_switch", {{"taskId", "task-1"}});
|
||||||
f.processWithTime(0.0f);
|
f.processWithTime(0.0f);
|
||||||
f.io.clearPublished();
|
f.io.clearPublished();
|
||||||
|
|
||||||
// Query current task
|
// Query current task
|
||||||
f.io.injectMessage("scheduler:query", {
|
f.io.injectMessage("scheduler:query", {
|
||||||
{"action", "get_current_task"},
|
{"action", "get_current_task"},
|
||||||
{"correlation_id", "test-123"}
|
{"correlation_id", "test-123"}
|
||||||
});
|
});
|
||||||
f.processWithTime(60.0f);
|
f.processWithTime(60.0f);
|
||||||
|
|
||||||
// Verify response
|
// Verify response
|
||||||
REQUIRE(f.io.wasPublished("scheduler:response"));
|
REQUIRE(f.io.wasPublished("scheduler:response"));
|
||||||
auto resp = f.io.getLastPublished("scheduler:response");
|
auto resp = f.io.getLastPublished("scheduler:response");
|
||||||
REQUIRE(resp["correlation_id"] == "test-123");
|
REQUIRE(resp["correlation_id"] == "test-123");
|
||||||
REQUIRE(resp["task_id"] == "task-1");
|
REQUIRE(resp["task_id"] == "task-1");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_SCHEDULER_009: Tool Command Start Break
|
// TI_SCHEDULER_009: Tool Command Start Break
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_SCHEDULER_009_ToolCommandStartBreak", "[scheduler][integration]") {
|
TEST_CASE("TI_SCHEDULER_009_ToolCommandStartBreak", "[scheduler][integration]") {
|
||||||
SchedulerTestFixture f;
|
SchedulerTestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Start task
|
// Start task
|
||||||
f.io.injectMessage("user:task_switch", {{"taskId", "task-1"}});
|
f.io.injectMessage("user:task_switch", {{"taskId", "task-1"}});
|
||||||
f.processWithTime(0.0f);
|
f.processWithTime(0.0f);
|
||||||
f.io.clearPublished();
|
f.io.clearPublished();
|
||||||
|
|
||||||
// Command to start break
|
// Command to start break
|
||||||
f.io.injectMessage("scheduler:command", {
|
f.io.injectMessage("scheduler:command", {
|
||||||
{"action", "start_break"},
|
{"action", "start_break"},
|
||||||
{"duration_minutes", 15},
|
{"duration_minutes", 15},
|
||||||
{"reason", "test break"}
|
{"reason", "test break"}
|
||||||
});
|
});
|
||||||
f.processWithTime(60.0f);
|
f.processWithTime(60.0f);
|
||||||
|
|
||||||
// Verify break started was published
|
// Verify break started was published
|
||||||
REQUIRE(f.io.wasPublished("scheduler:break_started"));
|
REQUIRE(f.io.wasPublished("scheduler:break_started"));
|
||||||
auto msg = f.io.getLastPublished("scheduler:break_started");
|
auto msg = f.io.getLastPublished("scheduler:break_started");
|
||||||
REQUIRE(msg["duration"] == 15);
|
REQUIRE(msg["duration"] == 15);
|
||||||
REQUIRE(msg["reason"] == "test break");
|
REQUIRE(msg["reason"] == "test break");
|
||||||
|
|
||||||
// Verify response was also published
|
// Verify response was also published
|
||||||
REQUIRE(f.io.wasPublished("scheduler:response"));
|
REQUIRE(f.io.wasPublished("scheduler:response"));
|
||||||
auto resp = f.io.getLastPublished("scheduler:response");
|
auto resp = f.io.getLastPublished("scheduler:response");
|
||||||
REQUIRE(resp["success"] == true);
|
REQUIRE(resp["success"] == true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_SCHEDULER_010: State Serialization
|
// TI_SCHEDULER_010: State Serialization
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_SCHEDULER_010_StateSerialization", "[scheduler][integration]") {
|
TEST_CASE("TI_SCHEDULER_010_StateSerialization", "[scheduler][integration]") {
|
||||||
SchedulerTestFixture f;
|
SchedulerTestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Setup some state
|
// Setup some state
|
||||||
f.io.injectMessage("user:task_switch", {{"taskId", "task-1"}});
|
f.io.injectMessage("user:task_switch", {{"taskId", "task-1"}});
|
||||||
f.processWithTime(0.0f);
|
f.processWithTime(0.0f);
|
||||||
f.processWithTime(1800.0f); // 30 minutes
|
f.processWithTime(1800.0f); // 30 minutes
|
||||||
|
|
||||||
// Get state
|
// Get state
|
||||||
auto state = f.module.getState();
|
auto state = f.module.getState();
|
||||||
REQUIRE(state != nullptr);
|
REQUIRE(state != nullptr);
|
||||||
|
|
||||||
// Verify state content
|
// Verify state content
|
||||||
REQUIRE(state->getString("currentTaskId", "") == "task-1");
|
REQUIRE(state->getString("currentTaskId", "") == "task-1");
|
||||||
REQUIRE(state->getBool("hyperfocusAlertSent", true) == false);
|
REQUIRE(state->getBool("hyperfocusAlertSent", true) == false);
|
||||||
|
|
||||||
// Create new module and restore state
|
// Create new module and restore state
|
||||||
SchedulerModule module2;
|
SchedulerModule module2;
|
||||||
MockIO io2;
|
MockIO io2;
|
||||||
grove::JsonDataNode configNode("config", json::object());
|
grove::JsonDataNode configNode("config", json::object());
|
||||||
module2.setConfiguration(configNode, &io2, nullptr);
|
module2.setConfiguration(configNode, &io2, nullptr);
|
||||||
module2.setState(*state);
|
module2.setState(*state);
|
||||||
|
|
||||||
// Verify state was restored
|
// Verify state was restored
|
||||||
auto state2 = module2.getState();
|
auto state2 = module2.getState();
|
||||||
REQUIRE(state2 != nullptr);
|
REQUIRE(state2 != nullptr);
|
||||||
REQUIRE(state2->getString("currentTaskId", "") == "task-1");
|
REQUIRE(state2->getString("currentTaskId", "") == "task-1");
|
||||||
REQUIRE(state2->getBool("hyperfocusAlertSent", true) == false);
|
REQUIRE(state2->getBool("hyperfocusAlertSent", true) == false);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,293 +1,293 @@
|
|||||||
/**
|
/**
|
||||||
* @file StorageModuleTests.cpp
|
* @file StorageModuleTests.cpp
|
||||||
* @brief Integration tests for StorageModule (10 TI)
|
* @brief Integration tests for StorageModule (10 TI)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <catch2/catch_test_macros.hpp>
|
#include <catch2/catch_test_macros.hpp>
|
||||||
#include "mocks/MockIO.hpp"
|
#include "mocks/MockIO.hpp"
|
||||||
#include "utils/TimeSimulator.hpp"
|
#include "utils/TimeSimulator.hpp"
|
||||||
#include "utils/TestHelpers.hpp"
|
#include "utils/TestHelpers.hpp"
|
||||||
|
|
||||||
#include "modules/StorageModule.h"
|
#include "modules/StorageModule.h"
|
||||||
#include <grove/JsonDataNode.h>
|
#include <grove/JsonDataNode.h>
|
||||||
|
|
||||||
using namespace aissia;
|
using namespace aissia;
|
||||||
using namespace aissia::tests;
|
using namespace aissia::tests;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Test Fixture
|
// Test Fixture
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
class StorageTestFixture {
|
class StorageTestFixture {
|
||||||
public:
|
public:
|
||||||
MockIO io;
|
MockIO io;
|
||||||
TimeSimulator time;
|
TimeSimulator time;
|
||||||
StorageModule module;
|
StorageModule module;
|
||||||
|
|
||||||
void configure(const json& config = json::object()) {
|
void configure(const json& config = json::object()) {
|
||||||
json fullConfig = json::object();
|
json fullConfig = json::object();
|
||||||
fullConfig.merge_patch(config);
|
fullConfig.merge_patch(config);
|
||||||
|
|
||||||
grove::JsonDataNode configNode("config", fullConfig);
|
grove::JsonDataNode configNode("config", fullConfig);
|
||||||
module.setConfiguration(configNode, &io, nullptr);
|
module.setConfiguration(configNode, &io, nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
void process() {
|
void process() {
|
||||||
grove::JsonDataNode input("input", time.createInput());
|
grove::JsonDataNode input("input", time.createInput());
|
||||||
module.process(input);
|
module.process(input);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_STORAGE_001: Task Completed Saves Session
|
// TI_STORAGE_001: Task Completed Saves Session
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_STORAGE_001_TaskCompletedSavesSession", "[storage][integration]") {
|
TEST_CASE("TI_STORAGE_001_TaskCompletedSavesSession", "[storage][integration]") {
|
||||||
StorageTestFixture f;
|
StorageTestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Receive task completed
|
// Receive task completed
|
||||||
f.io.injectMessage("scheduler:task_completed", {
|
f.io.injectMessage("scheduler:task_completed", {
|
||||||
{"taskId", "task-1"},
|
{"taskId", "task-1"},
|
||||||
{"taskName", "Coding session"},
|
{"taskName", "Coding session"},
|
||||||
{"durationMinutes", 45},
|
{"durationMinutes", 45},
|
||||||
{"hyperfocus", false}
|
{"hyperfocus", false}
|
||||||
});
|
});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
// Verify save_session published
|
// Verify save_session published
|
||||||
REQUIRE(f.io.wasPublished("storage:save_session"));
|
REQUIRE(f.io.wasPublished("storage:save_session"));
|
||||||
auto msg = f.io.getLastPublished("storage:save_session");
|
auto msg = f.io.getLastPublished("storage:save_session");
|
||||||
REQUIRE(msg["taskName"] == "Coding session");
|
REQUIRE(msg["taskName"] == "Coding session");
|
||||||
REQUIRE(msg["durationMinutes"] == 45);
|
REQUIRE(msg["durationMinutes"] == 45);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_STORAGE_002: App Changed Saves Usage
|
// TI_STORAGE_002: App Changed Saves Usage
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_STORAGE_002_AppChangedSavesUsage", "[storage][integration]") {
|
TEST_CASE("TI_STORAGE_002_AppChangedSavesUsage", "[storage][integration]") {
|
||||||
StorageTestFixture f;
|
StorageTestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Receive app changed with duration
|
// Receive app changed with duration
|
||||||
f.io.injectMessage("monitoring:app_changed", {
|
f.io.injectMessage("monitoring:app_changed", {
|
||||||
{"appName", "Code"},
|
{"appName", "Code"},
|
||||||
{"oldApp", "Discord"},
|
{"oldApp", "Discord"},
|
||||||
{"duration", 120},
|
{"duration", 120},
|
||||||
{"classification", "productive"}
|
{"classification", "productive"}
|
||||||
});
|
});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
// Verify save_app_usage published
|
// Verify save_app_usage published
|
||||||
REQUIRE(f.io.wasPublished("storage:save_app_usage"));
|
REQUIRE(f.io.wasPublished("storage:save_app_usage"));
|
||||||
auto msg = f.io.getLastPublished("storage:save_app_usage");
|
auto msg = f.io.getLastPublished("storage:save_app_usage");
|
||||||
REQUIRE(msg["appName"] == "Discord"); // Old app that ended
|
REQUIRE(msg["appName"] == "Discord"); // Old app that ended
|
||||||
REQUIRE(msg["durationSeconds"] == 120);
|
REQUIRE(msg["durationSeconds"] == 120);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_STORAGE_003: Session Saved Updates Last ID
|
// TI_STORAGE_003: Session Saved Updates Last ID
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_STORAGE_003_SessionSavedUpdatesLastId", "[storage][integration]") {
|
TEST_CASE("TI_STORAGE_003_SessionSavedUpdatesLastId", "[storage][integration]") {
|
||||||
StorageTestFixture f;
|
StorageTestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Receive session saved confirmation
|
// Receive session saved confirmation
|
||||||
f.io.injectMessage("storage:session_saved", {
|
f.io.injectMessage("storage:session_saved", {
|
||||||
{"sessionId", 42}
|
{"sessionId", 42}
|
||||||
});
|
});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
// Verify state updated
|
// Verify state updated
|
||||||
auto state = f.module.getState();
|
auto state = f.module.getState();
|
||||||
// TODO: Verify lastSessionId == 42
|
// TODO: Verify lastSessionId == 42
|
||||||
SUCCEED(); // Placeholder
|
SUCCEED(); // Placeholder
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_STORAGE_004: Storage Error Handled
|
// TI_STORAGE_004: Storage Error Handled
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_STORAGE_004_StorageErrorHandled", "[storage][integration]") {
|
TEST_CASE("TI_STORAGE_004_StorageErrorHandled", "[storage][integration]") {
|
||||||
StorageTestFixture f;
|
StorageTestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Receive storage error
|
// Receive storage error
|
||||||
f.io.injectMessage("storage:error", {
|
f.io.injectMessage("storage:error", {
|
||||||
{"message", "Database locked"}
|
{"message", "Database locked"}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Should not throw
|
// Should not throw
|
||||||
REQUIRE_NOTHROW(f.process());
|
REQUIRE_NOTHROW(f.process());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_STORAGE_005: Pending Saves Tracking
|
// TI_STORAGE_005: Pending Saves Tracking
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_STORAGE_005_PendingSavesTracking", "[storage][integration]") {
|
TEST_CASE("TI_STORAGE_005_PendingSavesTracking", "[storage][integration]") {
|
||||||
StorageTestFixture f;
|
StorageTestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Trigger save
|
// Trigger save
|
||||||
f.io.injectMessage("scheduler:task_completed", {
|
f.io.injectMessage("scheduler:task_completed", {
|
||||||
{"taskId", "t1"},
|
{"taskId", "t1"},
|
||||||
{"taskName", "Task"},
|
{"taskName", "Task"},
|
||||||
{"durationMinutes", 10}
|
{"durationMinutes", 10}
|
||||||
});
|
});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
// Verify pending incremented
|
// Verify pending incremented
|
||||||
auto state = f.module.getState();
|
auto state = f.module.getState();
|
||||||
// TODO: Verify pendingSaves == 1
|
// TODO: Verify pendingSaves == 1
|
||||||
SUCCEED(); // Placeholder
|
SUCCEED(); // Placeholder
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_STORAGE_006: Total Saved Tracking
|
// TI_STORAGE_006: Total Saved Tracking
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_STORAGE_006_TotalSavedTracking", "[storage][integration]") {
|
TEST_CASE("TI_STORAGE_006_TotalSavedTracking", "[storage][integration]") {
|
||||||
StorageTestFixture f;
|
StorageTestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Save and confirm multiple times
|
// Save and confirm multiple times
|
||||||
for (int i = 0; i < 3; i++) {
|
for (int i = 0; i < 3; i++) {
|
||||||
f.io.injectMessage("scheduler:task_completed", {
|
f.io.injectMessage("scheduler:task_completed", {
|
||||||
{"taskId", "t" + std::to_string(i)},
|
{"taskId", "t" + std::to_string(i)},
|
||||||
{"taskName", "Task"},
|
{"taskName", "Task"},
|
||||||
{"durationMinutes", 10}
|
{"durationMinutes", 10}
|
||||||
});
|
});
|
||||||
f.process();
|
f.process();
|
||||||
f.io.injectMessage("storage:session_saved", {{"sessionId", i}});
|
f.io.injectMessage("storage:session_saved", {{"sessionId", i}});
|
||||||
f.process();
|
f.process();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify total
|
// Verify total
|
||||||
auto state = f.module.getState();
|
auto state = f.module.getState();
|
||||||
// TODO: Verify totalSaved == 3
|
// TODO: Verify totalSaved == 3
|
||||||
SUCCEED(); // Placeholder
|
SUCCEED(); // Placeholder
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_STORAGE_007: Tool Query Notes
|
// TI_STORAGE_007: Tool Query Notes
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_STORAGE_007_ToolQueryNotes", "[storage][integration]") {
|
TEST_CASE("TI_STORAGE_007_ToolQueryNotes", "[storage][integration]") {
|
||||||
StorageTestFixture f;
|
StorageTestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Add a note first
|
// Add a note first
|
||||||
f.io.injectMessage("storage:command", {
|
f.io.injectMessage("storage:command", {
|
||||||
{"action", "save_note"},
|
{"action", "save_note"},
|
||||||
{"content", "Test note"},
|
{"content", "Test note"},
|
||||||
{"tags", json::array({"test", "important"})}
|
{"tags", json::array({"test", "important"})}
|
||||||
});
|
});
|
||||||
f.process();
|
f.process();
|
||||||
f.io.clearPublished();
|
f.io.clearPublished();
|
||||||
|
|
||||||
// Query notes
|
// Query notes
|
||||||
f.io.injectMessage("storage:query", {
|
f.io.injectMessage("storage:query", {
|
||||||
{"action", "query_notes"},
|
{"action", "query_notes"},
|
||||||
{"correlation_id", "query-1"}
|
{"correlation_id", "query-1"}
|
||||||
});
|
});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
// Verify response
|
// Verify response
|
||||||
REQUIRE(f.io.wasPublished("storage:response"));
|
REQUIRE(f.io.wasPublished("storage:response"));
|
||||||
auto resp = f.io.getLastPublished("storage:response");
|
auto resp = f.io.getLastPublished("storage:response");
|
||||||
REQUIRE(resp["correlation_id"] == "query-1");
|
REQUIRE(resp["correlation_id"] == "query-1");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_STORAGE_008: Tool Command Save Note
|
// TI_STORAGE_008: Tool Command Save Note
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_STORAGE_008_ToolCommandSaveNote", "[storage][integration]") {
|
TEST_CASE("TI_STORAGE_008_ToolCommandSaveNote", "[storage][integration]") {
|
||||||
StorageTestFixture f;
|
StorageTestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Save note
|
// Save note
|
||||||
f.io.injectMessage("storage:command", {
|
f.io.injectMessage("storage:command", {
|
||||||
{"action", "save_note"},
|
{"action", "save_note"},
|
||||||
{"content", "Remember to check logs"},
|
{"content", "Remember to check logs"},
|
||||||
{"tags", json::array({"reminder"})}
|
{"tags", json::array({"reminder"})}
|
||||||
});
|
});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
// Verify note added to state
|
// Verify note added to state
|
||||||
auto state = f.module.getState();
|
auto state = f.module.getState();
|
||||||
// TODO: Verify notes contains the new note
|
// TODO: Verify notes contains the new note
|
||||||
SUCCEED(); // Placeholder
|
SUCCEED(); // Placeholder
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_STORAGE_009: Note Tags Filtering
|
// TI_STORAGE_009: Note Tags Filtering
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_STORAGE_009_NoteTagsFiltering", "[storage][integration]") {
|
TEST_CASE("TI_STORAGE_009_NoteTagsFiltering", "[storage][integration]") {
|
||||||
StorageTestFixture f;
|
StorageTestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Add notes with different tags
|
// Add notes with different tags
|
||||||
f.io.injectMessage("storage:command", {
|
f.io.injectMessage("storage:command", {
|
||||||
{"action", "save_note"},
|
{"action", "save_note"},
|
||||||
{"content", "Work note"},
|
{"content", "Work note"},
|
||||||
{"tags", json::array({"work"})}
|
{"tags", json::array({"work"})}
|
||||||
});
|
});
|
||||||
f.process();
|
f.process();
|
||||||
f.io.injectMessage("storage:command", {
|
f.io.injectMessage("storage:command", {
|
||||||
{"action", "save_note"},
|
{"action", "save_note"},
|
||||||
{"content", "Personal note"},
|
{"content", "Personal note"},
|
||||||
{"tags", json::array({"personal"})}
|
{"tags", json::array({"personal"})}
|
||||||
});
|
});
|
||||||
f.process();
|
f.process();
|
||||||
f.io.clearPublished();
|
f.io.clearPublished();
|
||||||
|
|
||||||
// Query with tag filter
|
// Query with tag filter
|
||||||
f.io.injectMessage("storage:query", {
|
f.io.injectMessage("storage:query", {
|
||||||
{"action", "query_notes"},
|
{"action", "query_notes"},
|
||||||
{"tags", json::array({"work"})},
|
{"tags", json::array({"work"})},
|
||||||
{"correlation_id", "filter-1"}
|
{"correlation_id", "filter-1"}
|
||||||
});
|
});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
// Verify filtered response
|
// Verify filtered response
|
||||||
REQUIRE(f.io.wasPublished("storage:response"));
|
REQUIRE(f.io.wasPublished("storage:response"));
|
||||||
auto resp = f.io.getLastPublished("storage:response");
|
auto resp = f.io.getLastPublished("storage:response");
|
||||||
// TODO: Verify only work notes returned
|
// TODO: Verify only work notes returned
|
||||||
SUCCEED(); // Placeholder
|
SUCCEED(); // Placeholder
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_STORAGE_010: State Serialization
|
// TI_STORAGE_010: State Serialization
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_STORAGE_010_StateSerialization", "[storage][integration]") {
|
TEST_CASE("TI_STORAGE_010_StateSerialization", "[storage][integration]") {
|
||||||
StorageTestFixture f;
|
StorageTestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Build state with notes
|
// Build state with notes
|
||||||
f.io.injectMessage("storage:command", {
|
f.io.injectMessage("storage:command", {
|
||||||
{"action", "save_note"},
|
{"action", "save_note"},
|
||||||
{"content", "Test note for serialization"},
|
{"content", "Test note for serialization"},
|
||||||
{"tags", json::array({"test"})}
|
{"tags", json::array({"test"})}
|
||||||
});
|
});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
// Get state
|
// Get state
|
||||||
auto state = f.module.getState();
|
auto state = f.module.getState();
|
||||||
REQUIRE(state != nullptr);
|
REQUIRE(state != nullptr);
|
||||||
|
|
||||||
// Restore
|
// Restore
|
||||||
StorageModule module2;
|
StorageModule module2;
|
||||||
grove::JsonDataNode configNode2("config", json::object());
|
grove::JsonDataNode configNode2("config", json::object());
|
||||||
module2.setConfiguration(configNode2, &f.io, nullptr);
|
module2.setConfiguration(configNode2, &f.io, nullptr);
|
||||||
module2.setState(*state);
|
module2.setState(*state);
|
||||||
|
|
||||||
auto state2 = module2.getState();
|
auto state2 = module2.getState();
|
||||||
REQUIRE(state2 != nullptr);
|
REQUIRE(state2 != nullptr);
|
||||||
SUCCEED(); // Placeholder
|
SUCCEED(); // Placeholder
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,258 +1,258 @@
|
|||||||
/**
|
/**
|
||||||
* @file VoiceModuleTests.cpp
|
* @file VoiceModuleTests.cpp
|
||||||
* @brief Integration tests for VoiceModule (10 TI)
|
* @brief Integration tests for VoiceModule (10 TI)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <catch2/catch_test_macros.hpp>
|
#include <catch2/catch_test_macros.hpp>
|
||||||
#include "mocks/MockIO.hpp"
|
#include "mocks/MockIO.hpp"
|
||||||
#include "utils/TimeSimulator.hpp"
|
#include "utils/TimeSimulator.hpp"
|
||||||
#include "utils/TestHelpers.hpp"
|
#include "utils/TestHelpers.hpp"
|
||||||
|
|
||||||
#include "modules/VoiceModule.h"
|
#include "modules/VoiceModule.h"
|
||||||
#include <grove/JsonDataNode.h>
|
#include <grove/JsonDataNode.h>
|
||||||
|
|
||||||
using namespace aissia;
|
using namespace aissia;
|
||||||
using namespace aissia::tests;
|
using namespace aissia::tests;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Test Fixture
|
// Test Fixture
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
class VoiceTestFixture {
|
class VoiceTestFixture {
|
||||||
public:
|
public:
|
||||||
MockIO io;
|
MockIO io;
|
||||||
TimeSimulator time;
|
TimeSimulator time;
|
||||||
VoiceModule module;
|
VoiceModule module;
|
||||||
|
|
||||||
void configure(const json& config = json::object()) {
|
void configure(const json& config = json::object()) {
|
||||||
json fullConfig = {
|
json fullConfig = {
|
||||||
{"ttsEnabled", true},
|
{"ttsEnabled", true},
|
||||||
{"sttEnabled", true},
|
{"sttEnabled", true},
|
||||||
{"language", "fr"}
|
{"language", "fr"}
|
||||||
};
|
};
|
||||||
fullConfig.merge_patch(config);
|
fullConfig.merge_patch(config);
|
||||||
|
|
||||||
grove::JsonDataNode configNode("config", fullConfig);
|
grove::JsonDataNode configNode("config", fullConfig);
|
||||||
module.setConfiguration(configNode, &io, nullptr);
|
module.setConfiguration(configNode, &io, nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
void process() {
|
void process() {
|
||||||
grove::JsonDataNode input("input", time.createInput());
|
grove::JsonDataNode input("input", time.createInput());
|
||||||
module.process(input);
|
module.process(input);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_VOICE_001: AI Response Triggers Speak
|
// TI_VOICE_001: AI Response Triggers Speak
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_VOICE_001_AIResponseTriggersSpeak", "[voice][integration]") {
|
TEST_CASE("TI_VOICE_001_AIResponseTriggersSpeak", "[voice][integration]") {
|
||||||
VoiceTestFixture f;
|
VoiceTestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Receive AI response
|
// Receive AI response
|
||||||
f.io.injectMessage("ai:response", {
|
f.io.injectMessage("ai:response", {
|
||||||
{"text", "Voici la reponse a ta question"}
|
{"text", "Voici la reponse a ta question"}
|
||||||
});
|
});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
// Verify speak request
|
// Verify speak request
|
||||||
REQUIRE(f.io.wasPublished("voice:speak"));
|
REQUIRE(f.io.wasPublished("voice:speak"));
|
||||||
auto msg = f.io.getLastPublished("voice:speak");
|
auto msg = f.io.getLastPublished("voice:speak");
|
||||||
REQUIRE(msg["text"] == "Voici la reponse a ta question");
|
REQUIRE(msg["text"] == "Voici la reponse a ta question");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_VOICE_002: Suggestion Priority Speak
|
// TI_VOICE_002: Suggestion Priority Speak
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_VOICE_002_SuggestionPrioritySpeak", "[voice][integration]") {
|
TEST_CASE("TI_VOICE_002_SuggestionPrioritySpeak", "[voice][integration]") {
|
||||||
VoiceTestFixture f;
|
VoiceTestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Receive suggestion (should be priority)
|
// Receive suggestion (should be priority)
|
||||||
f.io.injectMessage("ai:suggestion", {
|
f.io.injectMessage("ai:suggestion", {
|
||||||
{"message", "Tu devrais faire une pause"},
|
{"message", "Tu devrais faire une pause"},
|
||||||
{"duration", 5}
|
{"duration", 5}
|
||||||
});
|
});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
// Verify speak with priority
|
// Verify speak with priority
|
||||||
REQUIRE(f.io.wasPublished("voice:speak"));
|
REQUIRE(f.io.wasPublished("voice:speak"));
|
||||||
auto msg = f.io.getLastPublished("voice:speak");
|
auto msg = f.io.getLastPublished("voice:speak");
|
||||||
REQUIRE(msg["priority"] == true);
|
REQUIRE(msg["priority"] == true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_VOICE_003: Speaking Started Updates State
|
// TI_VOICE_003: Speaking Started Updates State
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_VOICE_003_SpeakingStartedUpdatesState", "[voice][integration]") {
|
TEST_CASE("TI_VOICE_003_SpeakingStartedUpdatesState", "[voice][integration]") {
|
||||||
VoiceTestFixture f;
|
VoiceTestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Initially idle
|
// Initially idle
|
||||||
REQUIRE(f.module.isIdle() == true);
|
REQUIRE(f.module.isIdle() == true);
|
||||||
|
|
||||||
// Receive speaking started
|
// Receive speaking started
|
||||||
f.io.injectMessage("voice:speaking_started", {{"text", "Hello"}});
|
f.io.injectMessage("voice:speaking_started", {{"text", "Hello"}});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
// Should be speaking
|
// Should be speaking
|
||||||
REQUIRE(f.module.isIdle() == false);
|
REQUIRE(f.module.isIdle() == false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_VOICE_004: Speaking Ended Updates State
|
// TI_VOICE_004: Speaking Ended Updates State
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_VOICE_004_SpeakingEndedUpdatesState", "[voice][integration]") {
|
TEST_CASE("TI_VOICE_004_SpeakingEndedUpdatesState", "[voice][integration]") {
|
||||||
VoiceTestFixture f;
|
VoiceTestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Start speaking
|
// Start speaking
|
||||||
f.io.injectMessage("voice:speaking_started", {{"text", "Hello"}});
|
f.io.injectMessage("voice:speaking_started", {{"text", "Hello"}});
|
||||||
f.process();
|
f.process();
|
||||||
REQUIRE(f.module.isIdle() == false);
|
REQUIRE(f.module.isIdle() == false);
|
||||||
|
|
||||||
// End speaking
|
// End speaking
|
||||||
f.io.injectMessage("voice:speaking_ended", {});
|
f.io.injectMessage("voice:speaking_ended", {});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
// Should be idle
|
// Should be idle
|
||||||
REQUIRE(f.module.isIdle() == true);
|
REQUIRE(f.module.isIdle() == true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_VOICE_005: IsIdle Reflects Speaking
|
// TI_VOICE_005: IsIdle Reflects Speaking
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_VOICE_005_IsIdleReflectsSpeaking", "[voice][integration]") {
|
TEST_CASE("TI_VOICE_005_IsIdleReflectsSpeaking", "[voice][integration]") {
|
||||||
VoiceTestFixture f;
|
VoiceTestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Not speaking = idle
|
// Not speaking = idle
|
||||||
REQUIRE(f.module.isIdle() == true);
|
REQUIRE(f.module.isIdle() == true);
|
||||||
|
|
||||||
// Start speaking
|
// Start speaking
|
||||||
f.io.injectMessage("voice:speaking_started", {});
|
f.io.injectMessage("voice:speaking_started", {});
|
||||||
f.process();
|
f.process();
|
||||||
REQUIRE(f.module.isIdle() == false);
|
REQUIRE(f.module.isIdle() == false);
|
||||||
|
|
||||||
// Stop speaking
|
// Stop speaking
|
||||||
f.io.injectMessage("voice:speaking_ended", {});
|
f.io.injectMessage("voice:speaking_ended", {});
|
||||||
f.process();
|
f.process();
|
||||||
REQUIRE(f.module.isIdle() == true);
|
REQUIRE(f.module.isIdle() == true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_VOICE_006: Transcription Forwarded (No Re-publish)
|
// TI_VOICE_006: Transcription Forwarded (No Re-publish)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_VOICE_006_TranscriptionForwarded", "[voice][integration]") {
|
TEST_CASE("TI_VOICE_006_TranscriptionForwarded", "[voice][integration]") {
|
||||||
VoiceTestFixture f;
|
VoiceTestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Receive transcription
|
// Receive transcription
|
||||||
f.io.injectMessage("voice:transcription", {
|
f.io.injectMessage("voice:transcription", {
|
||||||
{"text", "Test transcription"},
|
{"text", "Test transcription"},
|
||||||
{"confidence", 0.9}
|
{"confidence", 0.9}
|
||||||
});
|
});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
// VoiceModule should NOT re-publish transcription
|
// VoiceModule should NOT re-publish transcription
|
||||||
// It just updates internal state
|
// It just updates internal state
|
||||||
REQUIRE(f.io.countPublished("voice:transcription") == 0);
|
REQUIRE(f.io.countPublished("voice:transcription") == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_VOICE_007: Total Spoken Incremented
|
// TI_VOICE_007: Total Spoken Incremented
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_VOICE_007_TotalSpokenIncremented", "[voice][integration]") {
|
TEST_CASE("TI_VOICE_007_TotalSpokenIncremented", "[voice][integration]") {
|
||||||
VoiceTestFixture f;
|
VoiceTestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Complete one speak cycle
|
// Complete one speak cycle
|
||||||
f.io.injectMessage("voice:speaking_started", {});
|
f.io.injectMessage("voice:speaking_started", {});
|
||||||
f.process();
|
f.process();
|
||||||
f.io.injectMessage("voice:speaking_ended", {});
|
f.io.injectMessage("voice:speaking_ended", {});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
// Complete another
|
// Complete another
|
||||||
f.io.injectMessage("voice:speaking_started", {});
|
f.io.injectMessage("voice:speaking_started", {});
|
||||||
f.process();
|
f.process();
|
||||||
f.io.injectMessage("voice:speaking_ended", {});
|
f.io.injectMessage("voice:speaking_ended", {});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
// Verify counter
|
// Verify counter
|
||||||
auto state = f.module.getState();
|
auto state = f.module.getState();
|
||||||
// TODO: Verify totalSpoken == 2
|
// TODO: Verify totalSpoken == 2
|
||||||
SUCCEED(); // Placeholder
|
SUCCEED(); // Placeholder
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_VOICE_008: TTS Disabled Config
|
// TI_VOICE_008: TTS Disabled Config
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_VOICE_008_TTSDisabledConfig", "[voice][integration]") {
|
TEST_CASE("TI_VOICE_008_TTSDisabledConfig", "[voice][integration]") {
|
||||||
VoiceTestFixture f;
|
VoiceTestFixture f;
|
||||||
f.configure({{"ttsEnabled", false}});
|
f.configure({{"ttsEnabled", false}});
|
||||||
|
|
||||||
// Try to trigger speak
|
// Try to trigger speak
|
||||||
f.io.injectMessage("ai:response", {{"text", "Should not speak"}});
|
f.io.injectMessage("ai:response", {{"text", "Should not speak"}});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
// Should NOT publish speak request
|
// Should NOT publish speak request
|
||||||
REQUIRE(f.io.wasPublished("voice:speak") == false);
|
REQUIRE(f.io.wasPublished("voice:speak") == false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_VOICE_009: Tool Command Speak
|
// TI_VOICE_009: Tool Command Speak
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_VOICE_009_ToolCommandSpeak", "[voice][integration]") {
|
TEST_CASE("TI_VOICE_009_ToolCommandSpeak", "[voice][integration]") {
|
||||||
VoiceTestFixture f;
|
VoiceTestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Send speak command via tool
|
// Send speak command via tool
|
||||||
f.io.injectMessage("voice:command", {
|
f.io.injectMessage("voice:command", {
|
||||||
{"action", "speak"},
|
{"action", "speak"},
|
||||||
{"text", "Hello from tool"}
|
{"text", "Hello from tool"}
|
||||||
});
|
});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
// Verify speak published
|
// Verify speak published
|
||||||
REQUIRE(f.io.wasPublished("voice:speak"));
|
REQUIRE(f.io.wasPublished("voice:speak"));
|
||||||
auto msg = f.io.getLastPublished("voice:speak");
|
auto msg = f.io.getLastPublished("voice:speak");
|
||||||
REQUIRE(msg["text"] == "Hello from tool");
|
REQUIRE(msg["text"] == "Hello from tool");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TI_VOICE_010: State Serialization
|
// TI_VOICE_010: State Serialization
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
TEST_CASE("TI_VOICE_010_StateSerialization", "[voice][integration]") {
|
TEST_CASE("TI_VOICE_010_StateSerialization", "[voice][integration]") {
|
||||||
VoiceTestFixture f;
|
VoiceTestFixture f;
|
||||||
f.configure();
|
f.configure();
|
||||||
|
|
||||||
// Build state
|
// Build state
|
||||||
f.io.injectMessage("voice:speaking_started", {});
|
f.io.injectMessage("voice:speaking_started", {});
|
||||||
f.process();
|
f.process();
|
||||||
f.io.injectMessage("voice:speaking_ended", {});
|
f.io.injectMessage("voice:speaking_ended", {});
|
||||||
f.process();
|
f.process();
|
||||||
|
|
||||||
// Get state
|
// Get state
|
||||||
auto state = f.module.getState();
|
auto state = f.module.getState();
|
||||||
REQUIRE(state != nullptr);
|
REQUIRE(state != nullptr);
|
||||||
|
|
||||||
// Restore
|
// Restore
|
||||||
VoiceModule module2;
|
VoiceModule module2;
|
||||||
grove::JsonDataNode configNode2("config", json::object());
|
grove::JsonDataNode configNode2("config", json::object());
|
||||||
module2.setConfiguration(configNode2, &f.io, nullptr);
|
module2.setConfiguration(configNode2, &f.io, nullptr);
|
||||||
module2.setState(*state);
|
module2.setState(*state);
|
||||||
|
|
||||||
auto state2 = module2.getState();
|
auto state2 = module2.getState();
|
||||||
REQUIRE(state2 != nullptr);
|
REQUIRE(state2 != nullptr);
|
||||||
SUCCEED(); // Placeholder
|
SUCCEED(); // Placeholder
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,82 +1,82 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <catch2/catch_test_macros.hpp>
|
#include <catch2/catch_test_macros.hpp>
|
||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
namespace aissia::tests {
|
namespace aissia::tests {
|
||||||
|
|
||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Custom Catch2 Matchers and Macros
|
// Custom Catch2 Matchers and Macros
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Require that a message was published to a topic
|
* @brief Require that a message was published to a topic
|
||||||
*/
|
*/
|
||||||
#define REQUIRE_PUBLISHED(io, topic) \
|
#define REQUIRE_PUBLISHED(io, topic) \
|
||||||
REQUIRE_MESSAGE(io.wasPublished(topic), "Expected message on topic: " << topic)
|
REQUIRE_MESSAGE(io.wasPublished(topic), "Expected message on topic: " << topic)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Require that no message was published to a topic
|
* @brief Require that no message was published to a topic
|
||||||
*/
|
*/
|
||||||
#define REQUIRE_NOT_PUBLISHED(io, topic) \
|
#define REQUIRE_NOT_PUBLISHED(io, topic) \
|
||||||
REQUIRE_MESSAGE(!io.wasPublished(topic), "Did not expect message on topic: " << topic)
|
REQUIRE_MESSAGE(!io.wasPublished(topic), "Did not expect message on topic: " << topic)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Require specific count of messages on a topic
|
* @brief Require specific count of messages on a topic
|
||||||
*/
|
*/
|
||||||
#define REQUIRE_PUBLISH_COUNT(io, topic, count) \
|
#define REQUIRE_PUBLISH_COUNT(io, topic, count) \
|
||||||
REQUIRE(io.countPublished(topic) == count)
|
REQUIRE(io.countPublished(topic) == count)
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// JSON Helpers
|
// JSON Helpers
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Create a minimal valid config for a module
|
* @brief Create a minimal valid config for a module
|
||||||
*/
|
*/
|
||||||
inline json makeConfig(const json& overrides = json::object()) {
|
inline json makeConfig(const json& overrides = json::object()) {
|
||||||
json config = json::object();
|
json config = json::object();
|
||||||
for (auto& [key, value] : overrides.items()) {
|
for (auto& [key, value] : overrides.items()) {
|
||||||
config[key] = value;
|
config[key] = value;
|
||||||
}
|
}
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Check if JSON contains expected fields
|
* @brief Check if JSON contains expected fields
|
||||||
*/
|
*/
|
||||||
inline bool jsonContains(const json& j, const json& expected) {
|
inline bool jsonContains(const json& j, const json& expected) {
|
||||||
for (auto& [key, value] : expected.items()) {
|
for (auto& [key, value] : expected.items()) {
|
||||||
if (!j.contains(key) || j[key] != value) {
|
if (!j.contains(key) || j[key] != value) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Test Tags
|
// Test Tags
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
// Module tags
|
// Module tags
|
||||||
constexpr const char* TAG_SCHEDULER = "[scheduler]";
|
constexpr const char* TAG_SCHEDULER = "[scheduler]";
|
||||||
constexpr const char* TAG_NOTIFICATION = "[notification]";
|
constexpr const char* TAG_NOTIFICATION = "[notification]";
|
||||||
constexpr const char* TAG_MONITORING = "[monitoring]";
|
constexpr const char* TAG_MONITORING = "[monitoring]";
|
||||||
constexpr const char* TAG_AI = "[ai]";
|
constexpr const char* TAG_AI = "[ai]";
|
||||||
constexpr const char* TAG_VOICE = "[voice]";
|
constexpr const char* TAG_VOICE = "[voice]";
|
||||||
constexpr const char* TAG_STORAGE = "[storage]";
|
constexpr const char* TAG_STORAGE = "[storage]";
|
||||||
|
|
||||||
// MCP tags
|
// MCP tags
|
||||||
constexpr const char* TAG_MCP = "[mcp]";
|
constexpr const char* TAG_MCP = "[mcp]";
|
||||||
constexpr const char* TAG_MCP_TYPES = "[mcp][types]";
|
constexpr const char* TAG_MCP_TYPES = "[mcp][types]";
|
||||||
constexpr const char* TAG_MCP_TRANSPORT = "[mcp][transport]";
|
constexpr const char* TAG_MCP_TRANSPORT = "[mcp][transport]";
|
||||||
constexpr const char* TAG_MCP_CLIENT = "[mcp][client]";
|
constexpr const char* TAG_MCP_CLIENT = "[mcp][client]";
|
||||||
|
|
||||||
// Common tags
|
// Common tags
|
||||||
constexpr const char* TAG_INTEGRATION = "[integration]";
|
constexpr const char* TAG_INTEGRATION = "[integration]";
|
||||||
constexpr const char* TAG_UNIT = "[unit]";
|
constexpr const char* TAG_UNIT = "[unit]";
|
||||||
|
|
||||||
} // namespace aissia::tests
|
} // namespace aissia::tests
|
||||||
|
|||||||
@ -1,92 +1,92 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <grove/JsonDataNode.h>
|
#include <grove/JsonDataNode.h>
|
||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
namespace aissia::tests {
|
namespace aissia::tests {
|
||||||
|
|
||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Simulates game time for testing modules
|
* @brief Simulates game time for testing modules
|
||||||
*
|
*
|
||||||
* Modules receive time info via process() input:
|
* Modules receive time info via process() input:
|
||||||
* {
|
* {
|
||||||
* "gameTime": 123.45, // Total elapsed time in seconds
|
* "gameTime": 123.45, // Total elapsed time in seconds
|
||||||
* "deltaTime": 0.1 // Time since last frame
|
* "deltaTime": 0.1 // Time since last frame
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
class TimeSimulator {
|
class TimeSimulator {
|
||||||
public:
|
public:
|
||||||
TimeSimulator() = default;
|
TimeSimulator() = default;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Create input data for module.process()
|
* @brief Create input data for module.process()
|
||||||
* @param deltaTime Time since last frame (default 0.1s = 10Hz)
|
* @param deltaTime Time since last frame (default 0.1s = 10Hz)
|
||||||
*/
|
*/
|
||||||
json createInput(float deltaTime = 0.1f) {
|
json createInput(float deltaTime = 0.1f) {
|
||||||
json input = {
|
json input = {
|
||||||
{"gameTime", m_gameTime},
|
{"gameTime", m_gameTime},
|
||||||
{"deltaTime", deltaTime}
|
{"deltaTime", deltaTime}
|
||||||
};
|
};
|
||||||
m_gameTime += deltaTime;
|
m_gameTime += deltaTime;
|
||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Create input as IDataNode
|
* @brief Create input as IDataNode
|
||||||
*/
|
*/
|
||||||
std::unique_ptr<grove::JsonDataNode> createInputNode(float deltaTime = 0.1f) {
|
std::unique_ptr<grove::JsonDataNode> createInputNode(float deltaTime = 0.1f) {
|
||||||
return std::make_unique<grove::JsonDataNode>("input", createInput(deltaTime));
|
return std::make_unique<grove::JsonDataNode>("input", createInput(deltaTime));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Advance time without creating input
|
* @brief Advance time without creating input
|
||||||
*/
|
*/
|
||||||
void advance(float seconds) {
|
void advance(float seconds) {
|
||||||
m_gameTime += seconds;
|
m_gameTime += seconds;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Advance time by minutes (convenience for hyperfocus tests)
|
* @brief Advance time by minutes (convenience for hyperfocus tests)
|
||||||
*/
|
*/
|
||||||
void advanceMinutes(float minutes) {
|
void advanceMinutes(float minutes) {
|
||||||
m_gameTime += minutes * 60.0f;
|
m_gameTime += minutes * 60.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Set absolute time
|
* @brief Set absolute time
|
||||||
*/
|
*/
|
||||||
void setTime(float time) {
|
void setTime(float time) {
|
||||||
m_gameTime = time;
|
m_gameTime = time;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get current game time
|
* @brief Get current game time
|
||||||
*/
|
*/
|
||||||
float getTime() const {
|
float getTime() const {
|
||||||
return m_gameTime;
|
return m_gameTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Reset to zero
|
* @brief Reset to zero
|
||||||
*/
|
*/
|
||||||
void reset() {
|
void reset() {
|
||||||
m_gameTime = 0.0f;
|
m_gameTime = 0.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Simulate multiple frames
|
* @brief Simulate multiple frames
|
||||||
* @param count Number of frames to simulate
|
* @param count Number of frames to simulate
|
||||||
* @param deltaTime Time per frame
|
* @param deltaTime Time per frame
|
||||||
*/
|
*/
|
||||||
void simulateFrames(int count, float deltaTime = 0.1f) {
|
void simulateFrames(int count, float deltaTime = 0.1f) {
|
||||||
m_gameTime += count * deltaTime;
|
m_gameTime += count * deltaTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
float m_gameTime = 0.0f;
|
float m_gameTime = 0.0f;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace aissia::tests
|
} // namespace aissia::tests
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user