feat: Implement MCP client and internal tools for agentic LLM
Add complete tool calling infrastructure for Claude Code-like functionality: Internal Tools (via GroveEngine IIO): - Scheduler: get_current_task, list_tasks, start_task, complete_task, start_break - Monitoring: get_focus_stats, get_current_app - Storage: save_note, query_notes, get_session_history - Voice: speak MCP Client (for external servers): - StdioTransport for fork/exec JSON-RPC communication - MCPClient for multi-server orchestration - Support for filesystem, brave-search, fetch servers Architecture: - IOBridge for sync request/response over async IIO pub/sub - Tool handlers added to all modules (SchedulerModule, MonitoringModule, StorageModule, VoiceModule) - LLMService unifies internal tools + MCP tools in ToolRegistry 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
9c1e168d4f
commit
059709cd0d
@ -73,6 +73,20 @@ if(OPENSSL_FOUND)
|
|||||||
target_compile_definitions(AissiaLLM PRIVATE CPPHTTPLIB_OPENSSL_SUPPORT)
|
target_compile_definitions(AissiaLLM PRIVATE CPPHTTPLIB_OPENSSL_SUPPORT)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# Tools Library (Internal tools + MCP client)
|
||||||
|
add_library(AissiaTools STATIC
|
||||||
|
src/shared/tools/InternalTools.cpp
|
||||||
|
src/shared/mcp/StdioTransport.cpp
|
||||||
|
src/shared/mcp/MCPClient.cpp
|
||||||
|
)
|
||||||
|
target_include_directories(AissiaTools PUBLIC
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||||
|
)
|
||||||
|
target_link_libraries(AissiaTools PUBLIC
|
||||||
|
GroveEngine::impl
|
||||||
|
spdlog::spdlog
|
||||||
|
)
|
||||||
|
|
||||||
# Platform Library (window tracking)
|
# Platform Library (window tracking)
|
||||||
add_library(AissiaPlatform STATIC
|
add_library(AissiaPlatform STATIC
|
||||||
src/shared/platform/WindowTrackerFactory.cpp
|
src/shared/platform/WindowTrackerFactory.cpp
|
||||||
@ -95,6 +109,7 @@ add_library(AissiaAudio STATIC
|
|||||||
target_include_directories(AissiaAudio PUBLIC
|
target_include_directories(AissiaAudio PUBLIC
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src
|
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||||
${httplib_SOURCE_DIR}
|
${httplib_SOURCE_DIR}
|
||||||
|
${nlohmann_json_SOURCE_DIR}/include
|
||||||
)
|
)
|
||||||
target_link_libraries(AissiaAudio PUBLIC
|
target_link_libraries(AissiaAudio PUBLIC
|
||||||
spdlog::spdlog
|
spdlog::spdlog
|
||||||
@ -123,6 +138,7 @@ target_link_libraries(AissiaServices PUBLIC
|
|||||||
GroveEngine::impl
|
GroveEngine::impl
|
||||||
spdlog::spdlog
|
spdlog::spdlog
|
||||||
AissiaLLM
|
AissiaLLM
|
||||||
|
AissiaTools
|
||||||
AissiaPlatform
|
AissiaPlatform
|
||||||
AissiaAudio
|
AissiaAudio
|
||||||
SQLite::SQLite3
|
SQLite::SQLite3
|
||||||
|
|||||||
31
config/mcp.json
Normal file
31
config/mcp.json
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"servers": {
|
||||||
|
"filesystem": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/home"],
|
||||||
|
"enabled": false,
|
||||||
|
"_comment": "Enable to allow file read/write operations"
|
||||||
|
},
|
||||||
|
"brave-search": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@modelcontextprotocol/server-brave-search"],
|
||||||
|
"env": {
|
||||||
|
"BRAVE_API_KEY": "${BRAVE_API_KEY}"
|
||||||
|
},
|
||||||
|
"enabled": false,
|
||||||
|
"_comment": "Enable for web search (requires BRAVE_API_KEY)"
|
||||||
|
},
|
||||||
|
"fetch": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@modelcontextprotocol/server-fetch"],
|
||||||
|
"enabled": false,
|
||||||
|
"_comment": "Enable for HTTP fetch operations"
|
||||||
|
},
|
||||||
|
"memory": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@modelcontextprotocol/server-memory"],
|
||||||
|
"enabled": false,
|
||||||
|
"_comment": "Enable for persistent memory/knowledge graph"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
338
docs/PLAN_MCP_INTEGRATION.md
Normal file
338
docs/PLAN_MCP_INTEGRATION.md
Normal file
@ -0,0 +1,338 @@
|
|||||||
|
# Plan d'Intégration MCP + Agent Vocal
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Transformer AISSIA en "Claude Code vocal" : un assistant qui peut utiliser des tools (internes + MCP) pour accomplir des tâches, piloté par la voix.
|
||||||
|
|
||||||
|
## Architecture Cible
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ AISSIA - Agent Vocal │
|
||||||
|
├─────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ┌──────────┐ ┌──────────┐ ┌─────────────────────────────────────┐ │
|
||||||
|
│ │ Micro │───►│ STT │───►│ LLMService │ │
|
||||||
|
│ └──────────┘ │ (Whisper)│ │ (Agentic Loop) │ │
|
||||||
|
│ └──────────┘ │ │ │
|
||||||
|
│ │ ┌─────────────────────────────┐ │ │
|
||||||
|
│ │ │ ToolRegistry │ │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ │ ┌───────────────────────┐ │ │ │
|
||||||
|
│ │ │ │ Internal Tools │ │ │ │
|
||||||
|
│ │ │ │ (via GroveEngine) │ │ │ │
|
||||||
|
│ │ │ │ │ │ │ │
|
||||||
|
│ │ │ │ • get_schedule │ │ │ │
|
||||||
|
│ │ │ │ • start_break │ │ │ │
|
||||||
|
│ │ │ │ • get_focus_stats │ │ │ │
|
||||||
|
│ │ │ │ • save_note │ │ │ │
|
||||||
|
│ │ │ │ • query_history │ │ │ │
|
||||||
|
│ │ │ └───────────────────────┘ │ │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ │ ┌───────────────────────┐ │ │ │
|
||||||
|
│ │ │ │ MCP Tools │ │ │ │
|
||||||
|
│ │ │ │ (via MCPClient) │ │ │ │
|
||||||
|
│ │ │ │ │ │ │ │
|
||||||
|
│ │ │ │ • read_file │ │ │ │
|
||||||
|
│ │ │ │ • write_file │ │ │ │
|
||||||
|
│ │ │ │ • list_directory │ │ │ │
|
||||||
|
│ │ │ │ • web_search │ │ │ │
|
||||||
|
│ │ │ │ • fetch_url │ │ │ │
|
||||||
|
│ │ │ └───────────────────────┘ │ │ │
|
||||||
|
│ │ └─────────────────────────────┘ │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ └──────────────────┬──────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌──────────┐ ┌──────────┐ ┌─────────────────────────────────────┐ │
|
||||||
|
│ │ Speaker │◄───│ TTS │◄───│ Response Text │ │
|
||||||
|
│ └──────────┘ │ (espeak) │ └─────────────────────────────────────┘ │
|
||||||
|
│ └──────────┘ │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
|
||||||
|
COMMUNICATION INTERNE (GroveEngine IIO)
|
||||||
|
|
||||||
|
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||||
|
│ Scheduler │ │ Monitoring │ │ Storage │ │ Voice │
|
||||||
|
│ Module │ │ Module │ │ Module │ │ Module │
|
||||||
|
├──────────────┤ ├──────────────┤ ├──────────────┤ ├──────────────┤
|
||||||
|
│ │ │ │ │ │ │ │
|
||||||
|
│ • tasks │ │ • app usage │ │ • sessions │ │ • TTS queue │
|
||||||
|
│ • breaks │ │ • focus time │ │ • notes │ │ • STT events │
|
||||||
|
│ • hyperfocus │ │ • idle │ │ • history │ │ │
|
||||||
|
│ │ │ │ │ │ │ │
|
||||||
|
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘
|
||||||
|
│ │ │ │
|
||||||
|
└────────────────────┴────────────────────┴────────────────────┘
|
||||||
|
│
|
||||||
|
IIO Pub/Sub
|
||||||
|
│
|
||||||
|
┌───────────────┴───────────────┐
|
||||||
|
│ LLMService │
|
||||||
|
│ (écoute les tool:* topics) │
|
||||||
|
└───────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phases d'Implémentation
|
||||||
|
|
||||||
|
### Phase 1 : Tools Internes via IIO
|
||||||
|
|
||||||
|
**Objectif** : Les modules GroveEngine exposent leurs fonctionnalités comme tools LLM.
|
||||||
|
|
||||||
|
**Fichiers à créer/modifier** :
|
||||||
|
- `src/shared/tools/InternalTools.hpp` - Définitions des tools internes
|
||||||
|
- `src/shared/tools/InternalTools.cpp` - Implémentation
|
||||||
|
- `src/services/LLMService.cpp` - Enregistrement des tools
|
||||||
|
|
||||||
|
**Tools à implémenter** :
|
||||||
|
|
||||||
|
| Tool | Module Source | Description |
|
||||||
|
|------|---------------|-------------|
|
||||||
|
| `get_current_task` | SchedulerModule | Retourne la tâche en cours |
|
||||||
|
| `list_tasks` | SchedulerModule | Liste toutes les tâches planifiées |
|
||||||
|
| `start_task` | SchedulerModule | Démarre une tâche par ID |
|
||||||
|
| `complete_task` | SchedulerModule | Marque une tâche comme terminée |
|
||||||
|
| `start_break` | SchedulerModule | Déclenche une pause |
|
||||||
|
| `get_focus_stats` | MonitoringModule | Stats de focus (temps, apps) |
|
||||||
|
| `get_current_app` | MonitoringModule | App actuellement active |
|
||||||
|
| `save_note` | StorageModule | Sauvegarde une note |
|
||||||
|
| `query_notes` | StorageModule | Recherche dans les notes |
|
||||||
|
| `get_session_history` | StorageModule | Historique des sessions |
|
||||||
|
| `speak` | VoiceModule | Fait parler l'assistant |
|
||||||
|
|
||||||
|
**Pattern d'implémentation** :
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Le tool envoie une requête via IIO et attend la réponse
|
||||||
|
json InternalTools::executeGetCurrentTask(const json& input) {
|
||||||
|
// 1. Créer la requête
|
||||||
|
auto request = std::make_unique<grove::JsonDataNode>("request");
|
||||||
|
request->setString("action", "get_current_task");
|
||||||
|
|
||||||
|
// 2. Publier et attendre réponse (sync via promise/future)
|
||||||
|
auto response = m_ioBridge->requestAndWait("scheduler:query", std::move(request), 1000);
|
||||||
|
|
||||||
|
// 3. Convertir en JSON pour le LLM
|
||||||
|
return {
|
||||||
|
{"task_id", response->getString("task_id", "")},
|
||||||
|
{"task_name", response->getString("task_name", "")},
|
||||||
|
{"duration_minutes", response->getInt("duration_minutes", 0)}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 2 : Client MCP
|
||||||
|
|
||||||
|
**Objectif** : Se connecter à des serveurs MCP externes (filesystem, brave-search, etc.)
|
||||||
|
|
||||||
|
**Fichiers à créer** :
|
||||||
|
- `src/shared/mcp/MCPClient.hpp` - Interface client MCP
|
||||||
|
- `src/shared/mcp/MCPClient.cpp` - Implémentation
|
||||||
|
- `src/shared/mcp/MCPTransport.hpp` - Transport stdio/SSE
|
||||||
|
- `src/shared/mcp/MCPTypes.hpp` - Types MCP (Tool, Resource, etc.)
|
||||||
|
|
||||||
|
**Protocole MCP simplifié** :
|
||||||
|
|
||||||
|
```
|
||||||
|
Client Server (ex: filesystem)
|
||||||
|
│ │
|
||||||
|
│──── initialize ─────────────────►│
|
||||||
|
│◄─── initialized (capabilities) ──│
|
||||||
|
│ │
|
||||||
|
│──── tools/list ─────────────────►│
|
||||||
|
│◄─── tools (read_file, etc.) ─────│
|
||||||
|
│ │
|
||||||
|
│──── tools/call ─────────────────►│
|
||||||
|
│ {name: "read_file", │
|
||||||
|
│ arguments: {path: "..."}} │
|
||||||
|
│◄─── result ──────────────────────│
|
||||||
|
│ {content: "..."} │
|
||||||
|
│ │
|
||||||
|
```
|
||||||
|
|
||||||
|
**Transport stdio** :
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class StdioTransport : public IMCPTransport {
|
||||||
|
public:
|
||||||
|
StdioTransport(const std::string& command, const std::vector<std::string>& args) {
|
||||||
|
// Fork + exec le serveur MCP
|
||||||
|
// Rediriger stdin/stdout pour JSON-RPC
|
||||||
|
}
|
||||||
|
|
||||||
|
json sendRequest(const std::string& method, const json& params) {
|
||||||
|
json request = {
|
||||||
|
{"jsonrpc", "2.0"},
|
||||||
|
{"id", m_nextId++},
|
||||||
|
{"method", method},
|
||||||
|
{"params", params}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Écrire sur stdin du process
|
||||||
|
write(request.dump() + "\n");
|
||||||
|
|
||||||
|
// Lire la réponse sur stdout
|
||||||
|
return json::parse(readLine());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Configuration MCP** :
|
||||||
|
|
||||||
|
```json
|
||||||
|
// config/mcp.json
|
||||||
|
{
|
||||||
|
"servers": {
|
||||||
|
"filesystem": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@anthropic-ai/mcp-server-filesystem", "/home/user/documents"],
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"brave-search": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@anthropic-ai/mcp-server-brave-search"],
|
||||||
|
"env": {
|
||||||
|
"BRAVE_API_KEY": "${BRAVE_API_KEY}"
|
||||||
|
},
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"fetch": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@anthropic-ai/mcp-server-fetch"],
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 3 : Unification ToolRegistry
|
||||||
|
|
||||||
|
**Objectif** : Le ToolRegistry agrège tools internes + MCP de façon transparente.
|
||||||
|
|
||||||
|
**Modification de LLMService** :
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void LLMService::initializeTools() {
|
||||||
|
// 1. Tools internes (via IIO)
|
||||||
|
m_internalTools = std::make_unique<InternalTools>(m_io);
|
||||||
|
for (const auto& tool : m_internalTools->getTools()) {
|
||||||
|
m_toolRegistry.registerTool(tool);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Tools MCP (via MCPClient)
|
||||||
|
m_mcpClient = std::make_unique<MCPClient>();
|
||||||
|
m_mcpClient->loadConfig("config/mcp.json");
|
||||||
|
m_mcpClient->connectAll();
|
||||||
|
|
||||||
|
for (const auto& tool : m_mcpClient->listAllTools()) {
|
||||||
|
m_toolRegistry.registerTool(
|
||||||
|
tool.name,
|
||||||
|
tool.description,
|
||||||
|
tool.inputSchema,
|
||||||
|
[this, name = tool.name](const json& input) {
|
||||||
|
return m_mcpClient->callTool(name, input);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_logger->info("Tools loaded: {} internal, {} MCP",
|
||||||
|
m_internalTools->size(), m_mcpClient->toolCount());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 4 : Tests
|
||||||
|
|
||||||
|
**Tests unitaires** :
|
||||||
|
- `tests/test_internal_tools.cpp` - Mock IIO, vérifier tool execution
|
||||||
|
- `tests/test_mcp_client.cpp` - Mock server, vérifier protocole
|
||||||
|
- `tests/test_tool_registry.cpp` - Vérifier aggregation
|
||||||
|
|
||||||
|
**Test d'intégration** :
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// tests/integration/test_agentic_loop.cpp
|
||||||
|
TEST_CASE("Agentic loop with tools") {
|
||||||
|
LLMService service;
|
||||||
|
service.initialize(io);
|
||||||
|
service.loadConfig("config/llm.json");
|
||||||
|
|
||||||
|
// Simuler une query qui nécessite des tools
|
||||||
|
auto result = service.query("Quelle tâche je fais en ce moment ?");
|
||||||
|
|
||||||
|
// Vérifier que le tool get_current_task a été appelé
|
||||||
|
REQUIRE(result.contains("task"));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers à Créer
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── shared/
|
||||||
|
│ ├── tools/
|
||||||
|
│ │ ├── InternalTools.hpp # Tools internes (IIO)
|
||||||
|
│ │ ├── InternalTools.cpp
|
||||||
|
│ │ └── IOBridge.hpp # Request/response sync via IIO
|
||||||
|
│ └── mcp/
|
||||||
|
│ ├── MCPClient.hpp # Client MCP principal
|
||||||
|
│ ├── MCPClient.cpp
|
||||||
|
│ ├── MCPTransport.hpp # Interface transport
|
||||||
|
│ ├── StdioTransport.hpp # Transport stdio (fork/exec)
|
||||||
|
│ ├── StdioTransport.cpp
|
||||||
|
│ └── MCPTypes.hpp # Tool, Resource, etc.
|
||||||
|
├── services/
|
||||||
|
│ └── LLMService.cpp # Modifier pour init tools
|
||||||
|
└── modules/
|
||||||
|
├── SchedulerModule.cpp # Ajouter handlers pour tools
|
||||||
|
├── MonitoringModule.cpp
|
||||||
|
└── StorageModule.cpp
|
||||||
|
|
||||||
|
config/
|
||||||
|
└── mcp.json # Configuration serveurs MCP
|
||||||
|
|
||||||
|
tests/
|
||||||
|
├── test_internal_tools.cpp
|
||||||
|
├── test_mcp_client.cpp
|
||||||
|
└── integration/
|
||||||
|
└── test_agentic_loop.cpp
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ordre d'Implémentation
|
||||||
|
|
||||||
|
1. **IOBridge** - Request/response synchrone via IIO
|
||||||
|
2. **InternalTools** - Définition et implémentation des tools
|
||||||
|
3. **Modules handlers** - Ajouter la gestion des requêtes tools dans chaque module
|
||||||
|
4. **Test internal tools** - Vérifier que ça marche
|
||||||
|
5. **MCPTypes** - Types de base MCP
|
||||||
|
6. **StdioTransport** - Fork/exec + communication JSON-RPC
|
||||||
|
7. **MCPClient** - Orchestration des serveurs
|
||||||
|
8. **Test MCP** - Vérifier avec un vrai serveur (filesystem)
|
||||||
|
9. **Unification** - Merger dans LLMService
|
||||||
|
10. **Test intégration** - End-to-end
|
||||||
|
|
||||||
|
## Estimation Temps
|
||||||
|
|
||||||
|
| Phase | Temps estimé |
|
||||||
|
|-------|--------------|
|
||||||
|
| Phase 1 : Tools Internes | 2-3h |
|
||||||
|
| Phase 2 : Client MCP | 2-3h |
|
||||||
|
| Phase 3 : Unification | 1h |
|
||||||
|
| Phase 4 : Tests | 1-2h |
|
||||||
|
| **Total** | **6-9h** |
|
||||||
|
|
||||||
|
## Dépendances
|
||||||
|
|
||||||
|
- `nlohmann/json` - Déjà présent
|
||||||
|
- `spdlog` - Déjà présent
|
||||||
|
- Node.js/npx - Pour les serveurs MCP (optionnel, on peut aussi implémenter nos propres serveurs en C++)
|
||||||
|
|
||||||
|
## Risques et Mitigations
|
||||||
|
|
||||||
|
| Risque | Mitigation |
|
||||||
|
|--------|------------|
|
||||||
|
| Fork/exec complexe sur Windows | Utiliser `_popen` ou process lib cross-platform |
|
||||||
|
| Timeout sur tools lents | Configurable per-tool, default 30s |
|
||||||
|
| Serveur MCP crash | Reconnect automatique, fallback graceful |
|
||||||
|
| Trop de tools = confusion LLM | Grouper par namespace, limiter à ~20 tools |
|
||||||
19
src/main.cpp
19
src/main.cpp
@ -19,6 +19,21 @@
|
|||||||
|
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
// Helper to clone IDataNode (workaround for missing clone() method)
|
||||||
|
std::unique_ptr<grove::IDataNode> cloneDataNode(grove::IDataNode* node) {
|
||||||
|
if (!node) return nullptr;
|
||||||
|
// Cast to JsonDataNode to access the JSON data
|
||||||
|
auto* jsonNode = dynamic_cast<grove::JsonDataNode*>(node);
|
||||||
|
if (jsonNode) {
|
||||||
|
return std::make_unique<grove::JsonDataNode>(
|
||||||
|
jsonNode->getName(),
|
||||||
|
jsonNode->getJsonData()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Fallback: create empty node
|
||||||
|
return std::make_unique<grove::JsonDataNode>("data");
|
||||||
|
}
|
||||||
|
|
||||||
// Global flag for clean shutdown
|
// Global flag for clean shutdown
|
||||||
static volatile bool g_running = true;
|
static volatile bool g_running = true;
|
||||||
|
|
||||||
@ -121,7 +136,7 @@ public:
|
|||||||
if (prefix == "llm" || prefix == "storage" || prefix == "platform" || prefix == "voice") {
|
if (prefix == "llm" || prefix == "storage" || prefix == "platform" || prefix == "voice") {
|
||||||
for (auto& [name, io] : m_serviceIOs) {
|
for (auto& [name, io] : m_serviceIOs) {
|
||||||
if (io && msg.data) {
|
if (io && msg.data) {
|
||||||
io->publish(msg.topic, msg.data->clone());
|
io->publish(msg.topic, cloneDataNode(msg.data.get()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -129,7 +144,7 @@ public:
|
|||||||
// Route to modules (broadcast)
|
// Route to modules (broadcast)
|
||||||
for (auto& [name, io] : m_moduleIOs) {
|
for (auto& [name, io] : m_moduleIOs) {
|
||||||
if (io && msg.data) {
|
if (io && msg.data) {
|
||||||
io->publish(msg.topic, msg.data->clone());
|
io->publish(msg.topic, cloneDataNode(msg.data.get()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
#include "AIModule.h"
|
#include "AIModule.h"
|
||||||
|
#include <grove/IIO.h>
|
||||||
#include <grove/JsonDataNode.h>
|
#include <grove/JsonDataNode.h>
|
||||||
|
|
||||||
namespace aissia {
|
namespace aissia {
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
#include "MonitoringModule.h"
|
#include "MonitoringModule.h"
|
||||||
|
#include <grove/IIO.h>
|
||||||
#include <grove/JsonDataNode.h>
|
#include <grove/JsonDataNode.h>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
@ -22,7 +23,7 @@ void MonitoringModule::setConfiguration(const grove::IDataNode& configNode,
|
|||||||
|
|
||||||
// Load productive apps list
|
// Load productive apps list
|
||||||
m_productiveApps.clear();
|
m_productiveApps.clear();
|
||||||
auto* prodNode = configNode.getChildReadOnly("productive_apps");
|
auto* prodNode = const_cast<grove::IDataNode&>(configNode).getChildReadOnly("productive_apps");
|
||||||
if (prodNode) {
|
if (prodNode) {
|
||||||
for (const auto& name : prodNode->getChildNames()) {
|
for (const auto& name : prodNode->getChildNames()) {
|
||||||
m_productiveApps.insert(prodNode->getString(name, ""));
|
m_productiveApps.insert(prodNode->getString(name, ""));
|
||||||
@ -36,7 +37,7 @@ void MonitoringModule::setConfiguration(const grove::IDataNode& configNode,
|
|||||||
|
|
||||||
// Load distracting apps list
|
// Load distracting apps list
|
||||||
m_distractingApps.clear();
|
m_distractingApps.clear();
|
||||||
auto* distNode = configNode.getChildReadOnly("distracting_apps");
|
auto* distNode = const_cast<grove::IDataNode&>(configNode).getChildReadOnly("distracting_apps");
|
||||||
if (distNode) {
|
if (distNode) {
|
||||||
for (const auto& name : distNode->getChildNames()) {
|
for (const auto& name : distNode->getChildNames()) {
|
||||||
m_distractingApps.insert(distNode->getString(name, ""));
|
m_distractingApps.insert(distNode->getString(name, ""));
|
||||||
@ -54,6 +55,8 @@ void MonitoringModule::setConfiguration(const grove::IDataNode& configNode,
|
|||||||
m_io->subscribe("platform:window_changed", subConfig);
|
m_io->subscribe("platform:window_changed", subConfig);
|
||||||
m_io->subscribe("platform:idle_detected", subConfig);
|
m_io->subscribe("platform:idle_detected", subConfig);
|
||||||
m_io->subscribe("platform:activity_resumed", subConfig);
|
m_io->subscribe("platform:activity_resumed", subConfig);
|
||||||
|
// Tool request handlers
|
||||||
|
m_io->subscribe("monitoring:query", subConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
m_logger->info("MonitoringModule configure (v2 - sans infrastructure)");
|
m_logger->info("MonitoringModule configure (v2 - sans infrastructure)");
|
||||||
@ -86,8 +89,48 @@ void MonitoringModule::processMessages() {
|
|||||||
else if (msg.topic == "platform:activity_resumed" && msg.data) {
|
else if (msg.topic == "platform:activity_resumed" && msg.data) {
|
||||||
handleActivityResumed(*msg.data);
|
handleActivityResumed(*msg.data);
|
||||||
}
|
}
|
||||||
|
// Tool query handlers
|
||||||
|
else if (msg.topic == "monitoring:query" && msg.data) {
|
||||||
|
handleToolQuery(*msg.data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MonitoringModule::handleToolQuery(const grove::IDataNode& request) {
|
||||||
|
std::string correlationId = request.getString("correlation_id", "");
|
||||||
|
std::string action = request.getString("action", "");
|
||||||
|
|
||||||
|
auto response = std::make_unique<grove::JsonDataNode>("response");
|
||||||
|
response->setString("correlation_id", correlationId);
|
||||||
|
|
||||||
|
if (action == "get_focus_stats") {
|
||||||
|
std::string period = request.getString("period", "today");
|
||||||
|
// For now, we only track "today" stats
|
||||||
|
int productiveMinutes = m_totalProductiveSeconds / 60;
|
||||||
|
int distractingMinutes = m_totalDistractingSeconds / 60;
|
||||||
|
int totalMinutes = productiveMinutes + distractingMinutes;
|
||||||
|
int focusScore = totalMinutes > 0 ? (productiveMinutes * 100 / totalMinutes) : 0;
|
||||||
|
|
||||||
|
response->setInt("productive_minutes", productiveMinutes);
|
||||||
|
response->setInt("distracting_minutes", distractingMinutes);
|
||||||
|
response->setInt("total_minutes", totalMinutes);
|
||||||
|
response->setInt("focus_score", focusScore);
|
||||||
|
response->setString("period", period);
|
||||||
|
response->setBool("is_idle", m_isIdle);
|
||||||
|
response->setString("current_app", m_currentApp);
|
||||||
|
}
|
||||||
|
else if (action == "get_current_app") {
|
||||||
|
response->setString("app_name", m_currentApp);
|
||||||
|
response->setBool("is_productive", isProductiveApp(m_currentApp));
|
||||||
|
response->setBool("is_distracting", isDistractingApp(m_currentApp));
|
||||||
|
response->setBool("is_idle", m_isIdle);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
response->setString("error", "unknown_action");
|
||||||
|
}
|
||||||
|
|
||||||
|
m_io->publish("monitoring:response", std::move(response));
|
||||||
|
}
|
||||||
|
|
||||||
void MonitoringModule::handleWindowChanged(const grove::IDataNode& data) {
|
void MonitoringModule::handleWindowChanged(const grove::IDataNode& data) {
|
||||||
std::string oldApp = data.getString("oldApp", "");
|
std::string oldApp = data.getString("oldApp", "");
|
||||||
|
|||||||
@ -72,6 +72,9 @@ private:
|
|||||||
void handleActivityResumed(const grove::IDataNode& data);
|
void handleActivityResumed(const grove::IDataNode& data);
|
||||||
bool isProductiveApp(const std::string& appName) const;
|
bool isProductiveApp(const std::string& appName) const;
|
||||||
bool isDistractingApp(const std::string& appName) const;
|
bool isDistractingApp(const std::string& appName) const;
|
||||||
|
|
||||||
|
// Tool handlers
|
||||||
|
void handleToolQuery(const grove::IDataNode& request);
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace aissia
|
} // namespace aissia
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
#include "SchedulerModule.h"
|
#include "SchedulerModule.h"
|
||||||
|
#include <grove/IIO.h>
|
||||||
#include <grove/JsonDataNode.h>
|
#include <grove/JsonDataNode.h>
|
||||||
|
|
||||||
namespace aissia {
|
namespace aissia {
|
||||||
@ -29,6 +30,9 @@ void SchedulerModule::setConfiguration(const grove::IDataNode& configNode,
|
|||||||
m_io->subscribe("user:task_switch", subConfig);
|
m_io->subscribe("user:task_switch", subConfig);
|
||||||
m_io->subscribe("monitoring:idle_detected", subConfig);
|
m_io->subscribe("monitoring:idle_detected", subConfig);
|
||||||
m_io->subscribe("monitoring:activity_resumed", subConfig);
|
m_io->subscribe("monitoring:activity_resumed", subConfig);
|
||||||
|
// Tool request handlers
|
||||||
|
m_io->subscribe("scheduler:query", subConfig);
|
||||||
|
m_io->subscribe("scheduler:command", subConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
m_logger->info("SchedulerModule configure: hyperfocus={}min, break_interval={}min",
|
m_logger->info("SchedulerModule configure: hyperfocus={}min, break_interval={}min",
|
||||||
@ -84,7 +88,107 @@ void SchedulerModule::processMessages() {
|
|||||||
// User returned - resume tracking
|
// User returned - resume tracking
|
||||||
m_logger->debug("User active, resuming session tracking");
|
m_logger->debug("User active, resuming session tracking");
|
||||||
}
|
}
|
||||||
|
// Tool query handlers
|
||||||
|
else if (msg.topic == "scheduler:query" && msg.data) {
|
||||||
|
handleToolQuery(*msg.data);
|
||||||
}
|
}
|
||||||
|
else if (msg.topic == "scheduler:command" && msg.data) {
|
||||||
|
handleToolCommand(*msg.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SchedulerModule::handleToolQuery(const grove::IDataNode& request) {
|
||||||
|
std::string correlationId = request.getString("correlation_id", "");
|
||||||
|
std::string action = request.getString("action", "");
|
||||||
|
|
||||||
|
auto response = std::make_unique<grove::JsonDataNode>("response");
|
||||||
|
response->setString("correlation_id", correlationId);
|
||||||
|
|
||||||
|
if (action == "get_current_task") {
|
||||||
|
Task* task = findTask(m_currentTaskId);
|
||||||
|
response->setString("task_id", m_currentTaskId);
|
||||||
|
response->setString("task_name", task ? task->name : (m_currentTaskId.empty() ? "Aucune tache active" : m_currentTaskId));
|
||||||
|
float sessionMinutes = m_currentTaskId.empty() ? 0 : (m_lastActivityTime - m_sessionStartTime) / 60.0f;
|
||||||
|
response->setInt("duration_minutes", static_cast<int>(sessionMinutes));
|
||||||
|
response->setString("started_at", ""); // TODO: format timestamp
|
||||||
|
}
|
||||||
|
else if (action == "list_tasks") {
|
||||||
|
bool includeCompleted = request.getBool("include_completed", false);
|
||||||
|
// Build task list as JSON-like structure
|
||||||
|
int count = 0;
|
||||||
|
for (const auto& task : m_tasks) {
|
||||||
|
if (!includeCompleted && task.completed) continue;
|
||||||
|
auto taskNode = std::make_unique<grove::JsonDataNode>("task_" + std::to_string(count));
|
||||||
|
taskNode->setString("id", task.id);
|
||||||
|
taskNode->setString("name", task.name);
|
||||||
|
taskNode->setInt("estimated_minutes", task.estimatedMinutes);
|
||||||
|
taskNode->setInt("actual_minutes", task.actualMinutes);
|
||||||
|
taskNode->setBool("completed", task.completed);
|
||||||
|
response->setChild("task_" + std::to_string(count), std::move(taskNode));
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
response->setInt("task_count", count);
|
||||||
|
response->setString("current_task", m_currentTaskId);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
response->setString("error", "unknown_action");
|
||||||
|
}
|
||||||
|
|
||||||
|
m_io->publish("scheduler:response", std::move(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
void SchedulerModule::handleToolCommand(const grove::IDataNode& request) {
|
||||||
|
std::string correlationId = request.getString("correlation_id", "");
|
||||||
|
std::string action = request.getString("action", "");
|
||||||
|
|
||||||
|
auto response = std::make_unique<grove::JsonDataNode>("response");
|
||||||
|
response->setString("correlation_id", correlationId);
|
||||||
|
|
||||||
|
if (action == "start_task") {
|
||||||
|
std::string taskId = request.getString("task_id", "");
|
||||||
|
if (taskId.empty()) {
|
||||||
|
response->setString("error", "missing_task_id");
|
||||||
|
} else {
|
||||||
|
startTask(taskId);
|
||||||
|
response->setBool("success", true);
|
||||||
|
response->setString("task_id", taskId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (action == "complete_task") {
|
||||||
|
std::string taskId = request.getString("task_id", "");
|
||||||
|
if (taskId.empty()) {
|
||||||
|
// Complete current task
|
||||||
|
completeCurrentTask();
|
||||||
|
} else {
|
||||||
|
// Complete specific task
|
||||||
|
Task* task = findTask(taskId);
|
||||||
|
if (task) {
|
||||||
|
task->completed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response->setBool("success", true);
|
||||||
|
}
|
||||||
|
else if (action == "start_break") {
|
||||||
|
int duration = request.getInt("duration_minutes", m_breakDurationMinutes);
|
||||||
|
std::string reason = request.getString("reason", "break");
|
||||||
|
m_lastBreakTime = m_lastActivityTime;
|
||||||
|
m_logger->info("Pause demarree: {} minutes ({})", duration, reason);
|
||||||
|
|
||||||
|
// Publish break started event
|
||||||
|
auto event = std::make_unique<grove::JsonDataNode>("break");
|
||||||
|
event->setInt("duration", duration);
|
||||||
|
event->setString("reason", reason);
|
||||||
|
m_io->publish("scheduler:break_started", std::move(event));
|
||||||
|
|
||||||
|
response->setBool("success", true);
|
||||||
|
response->setInt("duration", duration);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
response->setString("error", "unknown_action");
|
||||||
|
}
|
||||||
|
|
||||||
|
m_io->publish("scheduler:response", std::move(response));
|
||||||
}
|
}
|
||||||
|
|
||||||
void SchedulerModule::checkHyperfocus(float currentTime) {
|
void SchedulerModule::checkHyperfocus(float currentTime) {
|
||||||
|
|||||||
@ -81,11 +81,16 @@ private:
|
|||||||
std::shared_ptr<spdlog::logger> m_logger;
|
std::shared_ptr<spdlog::logger> m_logger;
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
|
void processMessages();
|
||||||
void checkHyperfocus(float currentTime);
|
void checkHyperfocus(float currentTime);
|
||||||
void checkBreakReminder(float currentTime);
|
void checkBreakReminder(float currentTime);
|
||||||
void startTask(const std::string& taskId);
|
void startTask(const std::string& taskId);
|
||||||
void completeCurrentTask();
|
void completeCurrentTask();
|
||||||
Task* findTask(const std::string& taskId);
|
Task* findTask(const std::string& taskId);
|
||||||
|
|
||||||
|
// Tool handlers
|
||||||
|
void handleToolQuery(const grove::IDataNode& request);
|
||||||
|
void handleToolCommand(const grove::IDataNode& request);
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace aissia
|
} // namespace aissia
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
#include "StorageModule.h"
|
#include "StorageModule.h"
|
||||||
|
#include <grove/IIO.h>
|
||||||
#include <grove/JsonDataNode.h>
|
#include <grove/JsonDataNode.h>
|
||||||
|
|
||||||
namespace aissia {
|
namespace aissia {
|
||||||
@ -24,6 +25,9 @@ void StorageModule::setConfiguration(const grove::IDataNode& configNode,
|
|||||||
m_io->subscribe("monitoring:app_changed", subConfig);
|
m_io->subscribe("monitoring:app_changed", subConfig);
|
||||||
m_io->subscribe("storage:session_saved", subConfig);
|
m_io->subscribe("storage:session_saved", subConfig);
|
||||||
m_io->subscribe("storage:error", subConfig);
|
m_io->subscribe("storage:error", subConfig);
|
||||||
|
// Tool request handlers
|
||||||
|
m_io->subscribe("storage:query", subConfig);
|
||||||
|
m_io->subscribe("storage:command", subConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
m_logger->info("StorageModule configure (v2 - sans infrastructure)");
|
m_logger->info("StorageModule configure (v2 - sans infrastructure)");
|
||||||
@ -55,7 +59,105 @@ void StorageModule::processMessages() {
|
|||||||
else if (msg.topic == "storage:error" && msg.data) {
|
else if (msg.topic == "storage:error" && msg.data) {
|
||||||
handleStorageError(*msg.data);
|
handleStorageError(*msg.data);
|
||||||
}
|
}
|
||||||
|
// Tool handlers
|
||||||
|
else if (msg.topic == "storage:query" && msg.data) {
|
||||||
|
handleToolQuery(*msg.data);
|
||||||
}
|
}
|
||||||
|
else if (msg.topic == "storage:command" && msg.data) {
|
||||||
|
handleToolCommand(*msg.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void StorageModule::handleToolQuery(const grove::IDataNode& request) {
|
||||||
|
std::string correlationId = request.getString("correlation_id", "");
|
||||||
|
std::string action = request.getString("action", "");
|
||||||
|
|
||||||
|
auto response = std::make_unique<grove::JsonDataNode>("response");
|
||||||
|
response->setString("correlation_id", correlationId);
|
||||||
|
|
||||||
|
if (action == "query_notes") {
|
||||||
|
std::string query = request.getString("query", "");
|
||||||
|
std::string tag = request.getString("tag", "");
|
||||||
|
int limit = request.getInt("limit", 10);
|
||||||
|
|
||||||
|
// Search in-memory notes
|
||||||
|
int count = 0;
|
||||||
|
for (const auto& note : m_notes) {
|
||||||
|
if (count >= limit) break;
|
||||||
|
|
||||||
|
bool matches = query.empty() ||
|
||||||
|
note.content.find(query) != std::string::npos;
|
||||||
|
|
||||||
|
if (!tag.empty()) {
|
||||||
|
bool hasTag = false;
|
||||||
|
for (const auto& t : note.tags) {
|
||||||
|
if (t == tag) { hasTag = true; break; }
|
||||||
|
}
|
||||||
|
matches = matches && hasTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matches) {
|
||||||
|
auto noteNode = std::make_unique<grove::JsonDataNode>("note_" + std::to_string(count));
|
||||||
|
noteNode->setString("id", note.id);
|
||||||
|
noteNode->setString("content", note.content);
|
||||||
|
noteNode->setString("timestamp", note.timestamp);
|
||||||
|
response->setChild("note_" + std::to_string(count), std::move(noteNode));
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response->setInt("count", count);
|
||||||
|
}
|
||||||
|
else if (action == "get_session_history") {
|
||||||
|
int days = request.getInt("days", 7);
|
||||||
|
response->setInt("last_session_id", m_lastSessionId);
|
||||||
|
response->setInt("total_saved", m_totalSaved);
|
||||||
|
response->setString("note", "Full history requires StorageService query");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
response->setString("error", "unknown_action");
|
||||||
|
}
|
||||||
|
|
||||||
|
m_io->publish("storage:response", std::move(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
void StorageModule::handleToolCommand(const grove::IDataNode& request) {
|
||||||
|
std::string correlationId = request.getString("correlation_id", "");
|
||||||
|
std::string action = request.getString("action", "");
|
||||||
|
|
||||||
|
auto response = std::make_unique<grove::JsonDataNode>("response");
|
||||||
|
response->setString("correlation_id", correlationId);
|
||||||
|
|
||||||
|
if (action == "save_note") {
|
||||||
|
std::string content = request.getString("content", "");
|
||||||
|
if (content.empty()) {
|
||||||
|
response->setString("error", "missing_content");
|
||||||
|
} else {
|
||||||
|
// Create note in memory
|
||||||
|
Note note;
|
||||||
|
note.id = "note_" + std::to_string(m_notes.size());
|
||||||
|
note.content = content;
|
||||||
|
note.timestamp = "now"; // TODO: proper timestamp
|
||||||
|
|
||||||
|
m_notes.push_back(note);
|
||||||
|
m_logger->info("Note saved: {}", note.id);
|
||||||
|
|
||||||
|
// Also publish to StorageService for persistence
|
||||||
|
if (m_io) {
|
||||||
|
auto saveReq = std::make_unique<grove::JsonDataNode>("save_note");
|
||||||
|
saveReq->setString("content", content);
|
||||||
|
m_io->publish("storage:save_note", std::move(saveReq));
|
||||||
|
}
|
||||||
|
|
||||||
|
response->setBool("success", true);
|
||||||
|
response->setString("note_id", note.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
response->setString("error", "unknown_action");
|
||||||
|
}
|
||||||
|
|
||||||
|
m_io->publish("storage:response", std::move(response));
|
||||||
}
|
}
|
||||||
|
|
||||||
void StorageModule::handleTaskCompleted(const grove::IDataNode& data) {
|
void StorageModule::handleTaskCompleted(const grove::IDataNode& data) {
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
#include <spdlog/sinks/stdout_color_sinks.h>
|
#include <spdlog/sinks/stdout_color_sinks.h>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace aissia {
|
namespace aissia {
|
||||||
|
|
||||||
@ -47,10 +48,19 @@ public:
|
|||||||
int getVersion() const override { return 2; }
|
int getVersion() const override { return 2; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
// Note structure for in-memory storage
|
||||||
|
struct Note {
|
||||||
|
std::string id;
|
||||||
|
std::string content;
|
||||||
|
std::string timestamp;
|
||||||
|
std::vector<std::string> tags;
|
||||||
|
};
|
||||||
|
|
||||||
// State
|
// State
|
||||||
int m_lastSessionId = 0;
|
int m_lastSessionId = 0;
|
||||||
int m_pendingSaves = 0;
|
int m_pendingSaves = 0;
|
||||||
int m_totalSaved = 0;
|
int m_totalSaved = 0;
|
||||||
|
std::vector<Note> m_notes;
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
grove::IIO* m_io = nullptr;
|
grove::IIO* m_io = nullptr;
|
||||||
@ -63,6 +73,10 @@ private:
|
|||||||
void handleAppChanged(const grove::IDataNode& data);
|
void handleAppChanged(const grove::IDataNode& data);
|
||||||
void handleSessionSaved(const grove::IDataNode& data);
|
void handleSessionSaved(const grove::IDataNode& data);
|
||||||
void handleStorageError(const grove::IDataNode& data);
|
void handleStorageError(const grove::IDataNode& data);
|
||||||
|
|
||||||
|
// Tool handlers
|
||||||
|
void handleToolQuery(const grove::IDataNode& request);
|
||||||
|
void handleToolCommand(const grove::IDataNode& request);
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace aissia
|
} // namespace aissia
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
#include "VoiceModule.h"
|
#include "VoiceModule.h"
|
||||||
|
#include <grove/IIO.h>
|
||||||
#include <grove/JsonDataNode.h>
|
#include <grove/JsonDataNode.h>
|
||||||
|
|
||||||
namespace aissia {
|
namespace aissia {
|
||||||
@ -18,13 +19,13 @@ void VoiceModule::setConfiguration(const grove::IDataNode& configNode,
|
|||||||
m_config = std::make_unique<grove::JsonDataNode>("config");
|
m_config = std::make_unique<grove::JsonDataNode>("config");
|
||||||
|
|
||||||
// TTS config
|
// TTS config
|
||||||
auto* ttsNode = configNode.getChildReadOnly("tts");
|
auto* ttsNode = const_cast<grove::IDataNode&>(configNode).getChildReadOnly("tts");
|
||||||
if (ttsNode) {
|
if (ttsNode) {
|
||||||
m_ttsEnabled = ttsNode->getBool("enabled", true);
|
m_ttsEnabled = ttsNode->getBool("enabled", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// STT config
|
// STT config
|
||||||
auto* sttNode = configNode.getChildReadOnly("stt");
|
auto* sttNode = const_cast<grove::IDataNode&>(configNode).getChildReadOnly("stt");
|
||||||
if (sttNode) {
|
if (sttNode) {
|
||||||
m_sttEnabled = sttNode->getBool("enabled", true);
|
m_sttEnabled = sttNode->getBool("enabled", true);
|
||||||
m_language = sttNode->getString("language", "fr");
|
m_language = sttNode->getString("language", "fr");
|
||||||
@ -39,6 +40,8 @@ void VoiceModule::setConfiguration(const grove::IDataNode& configNode,
|
|||||||
m_io->subscribe("voice:speaking_started", subConfig);
|
m_io->subscribe("voice:speaking_started", subConfig);
|
||||||
m_io->subscribe("voice:speaking_ended", subConfig);
|
m_io->subscribe("voice:speaking_ended", subConfig);
|
||||||
m_io->subscribe("voice:transcription", subConfig);
|
m_io->subscribe("voice:transcription", subConfig);
|
||||||
|
// Tool request handlers
|
||||||
|
m_io->subscribe("voice:command", subConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
m_logger->info("VoiceModule configure (v2 - sans infrastructure): TTS={}, STT={}",
|
m_logger->info("VoiceModule configure (v2 - sans infrastructure): TTS={}, STT={}",
|
||||||
@ -77,6 +80,25 @@ void VoiceModule::processMessages() {
|
|||||||
else if (msg.topic == "voice:transcription" && msg.data) {
|
else if (msg.topic == "voice:transcription" && msg.data) {
|
||||||
handleTranscription(*msg.data);
|
handleTranscription(*msg.data);
|
||||||
}
|
}
|
||||||
|
// Tool command handler
|
||||||
|
else if (msg.topic == "voice:command" && msg.data) {
|
||||||
|
handleToolCommand(*msg.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VoiceModule::handleToolCommand(const grove::IDataNode& request) {
|
||||||
|
std::string action = request.getString("action", "");
|
||||||
|
|
||||||
|
if (action == "speak") {
|
||||||
|
std::string text = request.getString("text", "");
|
||||||
|
std::string priority = request.getString("priority", "normal");
|
||||||
|
|
||||||
|
if (!text.empty()) {
|
||||||
|
bool isPriority = (priority == "high");
|
||||||
|
requestSpeak(text, isPriority);
|
||||||
|
m_logger->info("Tool speak: {} (priority={})", text.substr(0, 50), priority);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -72,6 +72,9 @@ private:
|
|||||||
void handleSpeakingStarted(const grove::IDataNode& data);
|
void handleSpeakingStarted(const grove::IDataNode& data);
|
||||||
void handleSpeakingEnded(const grove::IDataNode& data);
|
void handleSpeakingEnded(const grove::IDataNode& data);
|
||||||
void handleTranscription(const grove::IDataNode& data);
|
void handleTranscription(const grove::IDataNode& data);
|
||||||
|
|
||||||
|
// Tool handlers
|
||||||
|
void handleToolCommand(const grove::IDataNode& request);
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace aissia
|
} // namespace aissia
|
||||||
|
|||||||
@ -53,9 +53,16 @@ bool LLMService::loadConfig(const std::string& configPath) {
|
|||||||
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: "
|
||||||
|
"gerer le planning, verifier le focus, sauvegarder des notes, "
|
||||||
|
"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
|
||||||
|
initializeTools();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
@ -64,6 +71,58 @@ bool LLMService::loadConfig(const std::string& configPath) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void LLMService::initializeTools() {
|
||||||
|
m_logger->info("Initializing tools...");
|
||||||
|
|
||||||
|
// 1. Internal tools (via GroveEngine IIO)
|
||||||
|
if (m_io) {
|
||||||
|
m_internalTools = std::make_unique<InternalTools>(m_io);
|
||||||
|
for (const auto& tool : m_internalTools->getTools()) {
|
||||||
|
m_toolRegistry.registerTool(tool);
|
||||||
|
}
|
||||||
|
m_logger->info("Registered {} internal tools", m_internalTools->size());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. MCP tools (via external servers)
|
||||||
|
m_mcpClient = std::make_unique<mcp::MCPClient>();
|
||||||
|
if (loadMCPConfig("config/mcp.json")) {
|
||||||
|
int connected = m_mcpClient->connectAll();
|
||||||
|
if (connected > 0) {
|
||||||
|
for (const auto& tool : m_mcpClient->listAllTools()) {
|
||||||
|
// Convert MCP tool to our ToolDefinition format
|
||||||
|
m_toolRegistry.registerTool(
|
||||||
|
tool.name,
|
||||||
|
tool.description,
|
||||||
|
tool.inputSchema,
|
||||||
|
[this, toolName = tool.name](const nlohmann::json& input) -> nlohmann::json {
|
||||||
|
auto result = m_mcpClient->callTool(toolName, input);
|
||||||
|
// Convert MCP result to simple JSON
|
||||||
|
if (result.isError) {
|
||||||
|
return {{"error", true}, {"content", result.content}};
|
||||||
|
}
|
||||||
|
// Extract text content
|
||||||
|
std::string text;
|
||||||
|
for (const auto& content : result.content) {
|
||||||
|
if (content.contains("text")) {
|
||||||
|
text += content["text"].get<std::string>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {{"content", text}};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
m_logger->info("Registered {} MCP tools from {} servers",
|
||||||
|
m_mcpClient->toolCount(), connected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_logger->info("Total tools available: {}", m_toolRegistry.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LLMService::loadMCPConfig(const std::string& 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) {
|
||||||
|
|||||||
@ -3,6 +3,8 @@
|
|||||||
#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/mcp/MCPClient.hpp"
|
||||||
|
|
||||||
#include <grove/IIO.h>
|
#include <grove/IIO.h>
|
||||||
#include <grove/JsonDataNode.h>
|
#include <grove/JsonDataNode.h>
|
||||||
@ -51,6 +53,12 @@ public:
|
|||||||
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)
|
||||||
|
void initializeTools();
|
||||||
|
|
||||||
|
/// Load MCP server configurations
|
||||||
|
bool loadMCPConfig(const std::string& configPath);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct Request {
|
struct Request {
|
||||||
std::string query;
|
std::string query;
|
||||||
@ -76,6 +84,8 @@ private:
|
|||||||
// 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<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
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#include "ITTSEngine.hpp"
|
#include "ITTSEngine.hpp"
|
||||||
#include <spdlog/spdlog.h>
|
#include <spdlog/spdlog.h>
|
||||||
|
#include <spdlog/sinks/stdout_color_sinks.h>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
#include "ITTSEngine.hpp"
|
#include "ITTSEngine.hpp"
|
||||||
#include <spdlog/spdlog.h>
|
#include <spdlog/spdlog.h>
|
||||||
|
#include <spdlog/sinks/stdout_color_sinks.h>
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#include "SAPITTSEngine.hpp"
|
#include "SAPITTSEngine.hpp"
|
||||||
|
|||||||
@ -12,7 +12,9 @@
|
|||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <map>
|
||||||
#include <spdlog/spdlog.h>
|
#include <spdlog/spdlog.h>
|
||||||
|
#include <spdlog/sinks/stdout_color_sinks.h>
|
||||||
|
|
||||||
namespace aissia {
|
namespace aissia {
|
||||||
|
|
||||||
|
|||||||
302
src/shared/mcp/MCPClient.cpp
Normal file
302
src/shared/mcp/MCPClient.cpp
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
#include "MCPClient.hpp"
|
||||||
|
#include <spdlog/sinks/stdout_color_sinks.h>
|
||||||
|
|
||||||
|
#include <fstream>
|
||||||
|
|
||||||
|
namespace aissia::mcp {
|
||||||
|
|
||||||
|
MCPClient::MCPClient() {
|
||||||
|
m_logger = spdlog::get("MCPClient");
|
||||||
|
if (!m_logger) {
|
||||||
|
m_logger = spdlog::stdout_color_mt("MCPClient");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MCPClient::~MCPClient() {
|
||||||
|
disconnectAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MCPClient::loadConfig(const std::string& configPath) {
|
||||||
|
try {
|
||||||
|
std::ifstream file(configPath);
|
||||||
|
if (!file.is_open()) {
|
||||||
|
m_logger->warn("MCP config not found: {}", configPath);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
json config;
|
||||||
|
file >> config;
|
||||||
|
|
||||||
|
if (!config.contains("servers")) {
|
||||||
|
m_logger->warn("No 'servers' key in MCP config");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_configs.clear();
|
||||||
|
for (auto& [name, serverConfig] : config["servers"].items()) {
|
||||||
|
auto cfg = MCPServerConfig::fromJson(name, serverConfig);
|
||||||
|
if (cfg.enabled && !cfg.command.empty()) {
|
||||||
|
m_configs[name] = cfg;
|
||||||
|
m_logger->info("Loaded MCP server config: {}", name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_logger->info("Loaded {} MCP server configs", m_configs.size());
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
m_logger->error("Failed to load MCP config: {}", e.what());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int MCPClient::connectAll() {
|
||||||
|
int connected = 0;
|
||||||
|
|
||||||
|
for (const auto& [name, config] : m_configs) {
|
||||||
|
if (connect(name)) {
|
||||||
|
connected++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_logger->info("Connected to {}/{} MCP servers", connected, m_configs.size());
|
||||||
|
return connected;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MCPClient::connect(const std::string& serverName) {
|
||||||
|
auto configIt = m_configs.find(serverName);
|
||||||
|
if (configIt == m_configs.end()) {
|
||||||
|
m_logger->error("Unknown MCP server: {}", serverName);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_servers.count(serverName) > 0 && m_servers[serverName].initialized) {
|
||||||
|
m_logger->debug("Server {} already connected", serverName);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_logger->info("Connecting to MCP server: {}", serverName);
|
||||||
|
|
||||||
|
ConnectedServer server;
|
||||||
|
server.config = configIt->second;
|
||||||
|
server.transport = std::make_unique<StdioTransport>(server.config);
|
||||||
|
|
||||||
|
if (!server.transport->start()) {
|
||||||
|
m_logger->error("Failed to start MCP server: {}", serverName);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!initializeServer(server)) {
|
||||||
|
m_logger->error("Failed to initialize MCP server: {}", serverName);
|
||||||
|
server.transport->stop();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!discoverTools(server)) {
|
||||||
|
m_logger->warn("Failed to discover tools for: {}", serverName);
|
||||||
|
// Continue anyway, server might not have tools
|
||||||
|
}
|
||||||
|
|
||||||
|
m_servers[serverName] = std::move(server);
|
||||||
|
m_logger->info("Connected to MCP server: {} ({} tools)",
|
||||||
|
serverName, m_servers[serverName].tools.size());
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MCPClient::disconnect(const std::string& serverName) {
|
||||||
|
auto it = m_servers.find(serverName);
|
||||||
|
if (it == m_servers.end()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (it->second.transport) {
|
||||||
|
it->second.transport->stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
m_servers.erase(it);
|
||||||
|
m_logger->info("Disconnected from MCP server: {}", serverName);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MCPClient::disconnectAll() {
|
||||||
|
for (auto& [name, server] : m_servers) {
|
||||||
|
if (server.transport) {
|
||||||
|
server.transport->stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_servers.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MCPClient::initializeServer(ConnectedServer& server) {
|
||||||
|
// Send initialize request
|
||||||
|
JsonRpcRequest initReq;
|
||||||
|
initReq.method = "initialize";
|
||||||
|
initReq.params = {
|
||||||
|
{"protocolVersion", "2024-11-05"},
|
||||||
|
{"capabilities", {
|
||||||
|
{"roots", {{"listChanged", true}}}
|
||||||
|
}},
|
||||||
|
{"clientInfo", {
|
||||||
|
{"name", "AISSIA"},
|
||||||
|
{"version", "1.0.0"}
|
||||||
|
}}
|
||||||
|
};
|
||||||
|
|
||||||
|
auto response = server.transport->sendRequest(initReq, 10000);
|
||||||
|
|
||||||
|
if (response.isError()) {
|
||||||
|
m_logger->error("Initialize failed: {}",
|
||||||
|
response.error->dump());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.result.has_value()) {
|
||||||
|
server.info = MCPServerInfo::fromJson(*response.result);
|
||||||
|
m_logger->debug("Server info: {} v{}",
|
||||||
|
server.info.name, server.info.version);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send initialized notification
|
||||||
|
server.transport->sendNotification("notifications/initialized", json::object());
|
||||||
|
|
||||||
|
server.initialized = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MCPClient::discoverTools(ConnectedServer& server) {
|
||||||
|
if (!server.info.capabilities.hasTools) {
|
||||||
|
m_logger->debug("Server {} does not support tools", server.config.name);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonRpcRequest listReq;
|
||||||
|
listReq.method = "tools/list";
|
||||||
|
listReq.params = json::object();
|
||||||
|
|
||||||
|
auto response = server.transport->sendRequest(listReq, 10000);
|
||||||
|
|
||||||
|
if (response.isError()) {
|
||||||
|
m_logger->error("tools/list failed: {}",
|
||||||
|
response.error->dump());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
server.tools.clear();
|
||||||
|
|
||||||
|
if (response.result.has_value() && response.result->contains("tools")) {
|
||||||
|
for (const auto& toolJson : (*response.result)["tools"]) {
|
||||||
|
MCPTool tool = MCPTool::fromJson(toolJson);
|
||||||
|
server.tools.push_back(tool);
|
||||||
|
m_logger->debug("Discovered tool: {}:{}", server.config.name, tool.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<MCPTool> MCPClient::listAllTools() {
|
||||||
|
std::vector<MCPTool> allTools;
|
||||||
|
|
||||||
|
for (const auto& [serverName, server] : m_servers) {
|
||||||
|
for (const auto& tool : server.tools) {
|
||||||
|
MCPTool prefixedTool = tool;
|
||||||
|
prefixedTool.name = serverName + ":" + tool.name;
|
||||||
|
allTools.push_back(prefixedTool);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allTools;
|
||||||
|
}
|
||||||
|
|
||||||
|
MCPToolResult MCPClient::callTool(const std::string& toolName, const json& arguments) {
|
||||||
|
auto [serverName, actualToolName] = parseToolName(toolName);
|
||||||
|
|
||||||
|
auto it = m_servers.find(serverName);
|
||||||
|
if (it == m_servers.end()) {
|
||||||
|
MCPToolResult error;
|
||||||
|
error.isError = true;
|
||||||
|
error.content = {{{"type", "text"}, {"text", "Server not connected: " + serverName}}};
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonRpcRequest callReq;
|
||||||
|
callReq.method = "tools/call";
|
||||||
|
callReq.params = {
|
||||||
|
{"name", actualToolName},
|
||||||
|
{"arguments", arguments}
|
||||||
|
};
|
||||||
|
|
||||||
|
m_logger->debug("Calling tool {}:{} with args: {}",
|
||||||
|
serverName, actualToolName, arguments.dump().substr(0, 100));
|
||||||
|
|
||||||
|
auto response = it->second.transport->sendRequest(callReq, 30000);
|
||||||
|
|
||||||
|
MCPToolResult result;
|
||||||
|
|
||||||
|
if (response.isError()) {
|
||||||
|
result.isError = true;
|
||||||
|
std::string errorMsg = response.error.has_value() ?
|
||||||
|
response.error->dump() : "Unknown error";
|
||||||
|
result.content = {{{"type", "text"}, {"text", errorMsg}}};
|
||||||
|
m_logger->error("Tool call failed: {}", errorMsg);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.result.has_value()) {
|
||||||
|
auto& res = *response.result;
|
||||||
|
if (res.contains("content")) {
|
||||||
|
for (const auto& contentItem : res["content"]) {
|
||||||
|
result.content.push_back(contentItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.isError = res.value("isError", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_logger->debug("Tool {} returned {} content items",
|
||||||
|
toolName, result.content.size());
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t MCPClient::toolCount() const {
|
||||||
|
size_t count = 0;
|
||||||
|
for (const auto& [_, server] : m_servers) {
|
||||||
|
count += server.tools.size();
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MCPClient::isConnected(const std::string& serverName) const {
|
||||||
|
auto it = m_servers.find(serverName);
|
||||||
|
return it != m_servers.end() && it->second.initialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> MCPClient::getConnectedServers() const {
|
||||||
|
std::vector<std::string> names;
|
||||||
|
for (const auto& [name, _] : m_servers) {
|
||||||
|
names.push_back(name);
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pair<std::string, std::string> MCPClient::parseToolName(const std::string& toolName) {
|
||||||
|
size_t colonPos = toolName.find(':');
|
||||||
|
if (colonPos == std::string::npos) {
|
||||||
|
// No prefix, try to find tool in any server
|
||||||
|
for (const auto& [serverName, server] : m_servers) {
|
||||||
|
for (const auto& tool : server.tools) {
|
||||||
|
if (tool.name == toolName) {
|
||||||
|
return {serverName, toolName};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {"", toolName};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
toolName.substr(0, colonPos),
|
||||||
|
toolName.substr(colonPos + 1)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace aissia::mcp
|
||||||
110
src/shared/mcp/MCPClient.hpp
Normal file
110
src/shared/mcp/MCPClient.hpp
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "MCPTypes.hpp"
|
||||||
|
#include "MCPTransport.hpp"
|
||||||
|
#include "StdioTransport.hpp"
|
||||||
|
|
||||||
|
#include <spdlog/spdlog.h>
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <map>
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace aissia::mcp {
|
||||||
|
|
||||||
|
using json = nlohmann::json;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Client for managing multiple MCP servers
|
||||||
|
*
|
||||||
|
* Handles:
|
||||||
|
* - Loading server configurations
|
||||||
|
* - Starting/stopping servers
|
||||||
|
* - Initializing MCP protocol
|
||||||
|
* - Discovering and calling tools
|
||||||
|
*/
|
||||||
|
class MCPClient {
|
||||||
|
public:
|
||||||
|
MCPClient();
|
||||||
|
~MCPClient();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Load server configurations from JSON file
|
||||||
|
* @param configPath Path to mcp.json config file
|
||||||
|
* @return true if config loaded successfully
|
||||||
|
*/
|
||||||
|
bool loadConfig(const std::string& configPath);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Connect to all enabled servers
|
||||||
|
* @return Number of servers successfully connected
|
||||||
|
*/
|
||||||
|
int connectAll();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Connect to a specific server by name
|
||||||
|
* @return true if connected successfully
|
||||||
|
*/
|
||||||
|
bool connect(const std::string& serverName);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Disconnect from a specific server
|
||||||
|
*/
|
||||||
|
void disconnect(const std::string& serverName);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Disconnect from all servers
|
||||||
|
*/
|
||||||
|
void disconnectAll();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief List all available tools across all connected servers
|
||||||
|
* @return Vector of tools with server prefix (e.g., "filesystem:read_file")
|
||||||
|
*/
|
||||||
|
std::vector<MCPTool> listAllTools();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Call a tool by name
|
||||||
|
* @param toolName Full tool name (e.g., "filesystem:read_file")
|
||||||
|
* @param arguments Tool arguments
|
||||||
|
* @return Tool result
|
||||||
|
*/
|
||||||
|
MCPToolResult callTool(const std::string& toolName, const json& arguments);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get total number of available tools
|
||||||
|
*/
|
||||||
|
size_t toolCount() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check if a server is connected
|
||||||
|
*/
|
||||||
|
bool isConnected(const std::string& serverName) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get list of connected server names
|
||||||
|
*/
|
||||||
|
std::vector<std::string> getConnectedServers() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct ConnectedServer {
|
||||||
|
MCPServerConfig config;
|
||||||
|
std::unique_ptr<IMCPTransport> transport;
|
||||||
|
MCPServerInfo info;
|
||||||
|
std::vector<MCPTool> tools;
|
||||||
|
bool initialized = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::shared_ptr<spdlog::logger> m_logger;
|
||||||
|
std::map<std::string, MCPServerConfig> m_configs;
|
||||||
|
std::map<std::string, ConnectedServer> m_servers;
|
||||||
|
|
||||||
|
// MCP protocol methods
|
||||||
|
bool initializeServer(ConnectedServer& server);
|
||||||
|
bool discoverTools(ConnectedServer& server);
|
||||||
|
std::pair<std::string, std::string> parseToolName(const std::string& toolName);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace aissia::mcp
|
||||||
51
src/shared/mcp/MCPTransport.hpp
Normal file
51
src/shared/mcp/MCPTransport.hpp
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "MCPTypes.hpp"
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
#include <string>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace aissia::mcp {
|
||||||
|
|
||||||
|
using json = nlohmann::json;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Abstract transport interface for MCP communication
|
||||||
|
*/
|
||||||
|
class IMCPTransport {
|
||||||
|
public:
|
||||||
|
virtual ~IMCPTransport() = default;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Start the transport (connect/spawn process)
|
||||||
|
* @return true if successful
|
||||||
|
*/
|
||||||
|
virtual bool start() = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Stop the transport
|
||||||
|
*/
|
||||||
|
virtual void stop() = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check if transport is running
|
||||||
|
*/
|
||||||
|
virtual bool isRunning() const = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Send a JSON-RPC request and wait for response
|
||||||
|
* @param request The request to send
|
||||||
|
* @param timeoutMs Timeout in milliseconds
|
||||||
|
* @return The response
|
||||||
|
*/
|
||||||
|
virtual JsonRpcResponse sendRequest(const JsonRpcRequest& request, int timeoutMs = 30000) = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Send a notification (no response expected)
|
||||||
|
* @param method Method name
|
||||||
|
* @param params Parameters
|
||||||
|
*/
|
||||||
|
virtual void sendNotification(const std::string& method, const json& params) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace aissia::mcp
|
||||||
186
src/shared/mcp/MCPTypes.hpp
Normal file
186
src/shared/mcp/MCPTypes.hpp
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
namespace aissia::mcp {
|
||||||
|
|
||||||
|
using json = nlohmann::json;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief MCP Tool definition
|
||||||
|
*/
|
||||||
|
struct MCPTool {
|
||||||
|
std::string name;
|
||||||
|
std::string description;
|
||||||
|
json inputSchema;
|
||||||
|
|
||||||
|
json toJson() const {
|
||||||
|
return {
|
||||||
|
{"name", name},
|
||||||
|
{"description", description},
|
||||||
|
{"inputSchema", inputSchema}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static MCPTool fromJson(const json& j) {
|
||||||
|
MCPTool tool;
|
||||||
|
tool.name = j.value("name", "");
|
||||||
|
tool.description = j.value("description", "");
|
||||||
|
tool.inputSchema = j.value("inputSchema", json::object());
|
||||||
|
return tool;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief MCP Resource definition
|
||||||
|
*/
|
||||||
|
struct MCPResource {
|
||||||
|
std::string uri;
|
||||||
|
std::string name;
|
||||||
|
std::string description;
|
||||||
|
std::string mimeType;
|
||||||
|
|
||||||
|
static MCPResource fromJson(const json& j) {
|
||||||
|
MCPResource res;
|
||||||
|
res.uri = j.value("uri", "");
|
||||||
|
res.name = j.value("name", "");
|
||||||
|
res.description = j.value("description", "");
|
||||||
|
res.mimeType = j.value("mimeType", "");
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Result of a tool call
|
||||||
|
*/
|
||||||
|
struct MCPToolResult {
|
||||||
|
std::vector<json> content; // Array of content blocks
|
||||||
|
bool isError = false;
|
||||||
|
|
||||||
|
json toJson() const {
|
||||||
|
return {
|
||||||
|
{"content", content},
|
||||||
|
{"isError", isError}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief MCP Server capabilities
|
||||||
|
*/
|
||||||
|
struct MCPCapabilities {
|
||||||
|
bool hasTools = false;
|
||||||
|
bool hasResources = false;
|
||||||
|
bool hasPrompts = false;
|
||||||
|
|
||||||
|
static MCPCapabilities fromJson(const json& j) {
|
||||||
|
MCPCapabilities caps;
|
||||||
|
if (j.contains("tools")) caps.hasTools = true;
|
||||||
|
if (j.contains("resources")) caps.hasResources = true;
|
||||||
|
if (j.contains("prompts")) caps.hasPrompts = true;
|
||||||
|
return caps;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief MCP Server info after initialization
|
||||||
|
*/
|
||||||
|
struct MCPServerInfo {
|
||||||
|
std::string name;
|
||||||
|
std::string version;
|
||||||
|
MCPCapabilities capabilities;
|
||||||
|
|
||||||
|
static MCPServerInfo fromJson(const json& j) {
|
||||||
|
MCPServerInfo info;
|
||||||
|
info.name = j.value("name", "unknown");
|
||||||
|
info.version = j.value("version", "0.0.0");
|
||||||
|
if (j.contains("capabilities")) {
|
||||||
|
info.capabilities = MCPCapabilities::fromJson(j["capabilities"]);
|
||||||
|
}
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief JSON-RPC request
|
||||||
|
*/
|
||||||
|
struct JsonRpcRequest {
|
||||||
|
std::string jsonrpc = "2.0";
|
||||||
|
int id;
|
||||||
|
std::string method;
|
||||||
|
json params;
|
||||||
|
|
||||||
|
json toJson() const {
|
||||||
|
json j = {
|
||||||
|
{"jsonrpc", jsonrpc},
|
||||||
|
{"id", id},
|
||||||
|
{"method", method}
|
||||||
|
};
|
||||||
|
if (!params.is_null()) {
|
||||||
|
j["params"] = params;
|
||||||
|
}
|
||||||
|
return j;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief JSON-RPC response
|
||||||
|
*/
|
||||||
|
struct JsonRpcResponse {
|
||||||
|
std::string jsonrpc;
|
||||||
|
int id;
|
||||||
|
std::optional<json> result;
|
||||||
|
std::optional<json> error;
|
||||||
|
|
||||||
|
bool isError() const { return error.has_value(); }
|
||||||
|
|
||||||
|
static JsonRpcResponse fromJson(const json& j) {
|
||||||
|
JsonRpcResponse resp;
|
||||||
|
resp.jsonrpc = j.value("jsonrpc", "2.0");
|
||||||
|
resp.id = j.value("id", 0);
|
||||||
|
if (j.contains("result")) {
|
||||||
|
resp.result = j["result"];
|
||||||
|
}
|
||||||
|
if (j.contains("error")) {
|
||||||
|
resp.error = j["error"];
|
||||||
|
}
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief MCP Server configuration
|
||||||
|
*/
|
||||||
|
struct MCPServerConfig {
|
||||||
|
std::string name;
|
||||||
|
std::string command;
|
||||||
|
std::vector<std::string> args;
|
||||||
|
std::map<std::string, std::string> env;
|
||||||
|
bool enabled = true;
|
||||||
|
|
||||||
|
static MCPServerConfig fromJson(const std::string& name, const json& j) {
|
||||||
|
MCPServerConfig config;
|
||||||
|
config.name = name;
|
||||||
|
config.command = j.value("command", "");
|
||||||
|
config.enabled = j.value("enabled", true);
|
||||||
|
|
||||||
|
if (j.contains("args") && j["args"].is_array()) {
|
||||||
|
for (const auto& arg : j["args"]) {
|
||||||
|
config.args.push_back(arg.get<std::string>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (j.contains("env") && j["env"].is_object()) {
|
||||||
|
for (auto& [key, value] : j["env"].items()) {
|
||||||
|
config.env[key] = value.get<std::string>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace aissia::mcp
|
||||||
396
src/shared/mcp/StdioTransport.cpp
Normal file
396
src/shared/mcp/StdioTransport.cpp
Normal file
@ -0,0 +1,396 @@
|
|||||||
|
#include "StdioTransport.hpp"
|
||||||
|
#include <spdlog/sinks/stdout_color_sinks.h>
|
||||||
|
|
||||||
|
#include <sstream>
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
namespace aissia::mcp {
|
||||||
|
|
||||||
|
StdioTransport::StdioTransport(const MCPServerConfig& config) : m_config(config) {
|
||||||
|
m_logger = spdlog::get("MCP:" + config.name);
|
||||||
|
if (!m_logger) {
|
||||||
|
m_logger = spdlog::stdout_color_mt("MCP:" + config.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StdioTransport::~StdioTransport() {
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool StdioTransport::start() {
|
||||||
|
if (m_running) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_logger->info("Starting MCP server: {} {}", m_config.command,
|
||||||
|
[&]() {
|
||||||
|
std::string args;
|
||||||
|
for (const auto& a : m_config.args) args += a + " ";
|
||||||
|
return args;
|
||||||
|
}());
|
||||||
|
|
||||||
|
if (!spawnProcess()) {
|
||||||
|
m_logger->error("Failed to spawn MCP server process");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_running = true;
|
||||||
|
|
||||||
|
// Start reader thread
|
||||||
|
m_readerThread = std::thread(&StdioTransport::readerLoop, this);
|
||||||
|
|
||||||
|
m_logger->info("MCP server started successfully");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void StdioTransport::stop() {
|
||||||
|
if (!m_running) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_running = false;
|
||||||
|
|
||||||
|
// Close write pipe to signal EOF to child
|
||||||
|
#ifdef _WIN32
|
||||||
|
if (m_stdinWrite) {
|
||||||
|
CloseHandle(m_stdinWrite);
|
||||||
|
m_stdinWrite = nullptr;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
if (m_stdinFd >= 0) {
|
||||||
|
close(m_stdinFd);
|
||||||
|
m_stdinFd = -1;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Wait for reader thread
|
||||||
|
if (m_readerThread.joinable()) {
|
||||||
|
m_readerThread.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Terminate process
|
||||||
|
#ifdef _WIN32
|
||||||
|
if (m_processHandle) {
|
||||||
|
TerminateProcess(m_processHandle, 0);
|
||||||
|
CloseHandle(m_processHandle);
|
||||||
|
m_processHandle = nullptr;
|
||||||
|
}
|
||||||
|
if (m_stdoutRead) {
|
||||||
|
CloseHandle(m_stdoutRead);
|
||||||
|
m_stdoutRead = nullptr;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
if (m_pid > 0) {
|
||||||
|
kill(m_pid, SIGTERM);
|
||||||
|
waitpid(m_pid, nullptr, 0);
|
||||||
|
m_pid = -1;
|
||||||
|
}
|
||||||
|
if (m_stdoutFd >= 0) {
|
||||||
|
close(m_stdoutFd);
|
||||||
|
m_stdoutFd = -1;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
m_logger->info("MCP server stopped");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool StdioTransport::isRunning() const {
|
||||||
|
return m_running;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool StdioTransport::spawnProcess() {
|
||||||
|
#ifdef _WIN32
|
||||||
|
// Windows implementation
|
||||||
|
SECURITY_ATTRIBUTES sa;
|
||||||
|
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
|
||||||
|
sa.bInheritHandle = TRUE;
|
||||||
|
sa.lpSecurityDescriptor = nullptr;
|
||||||
|
|
||||||
|
HANDLE stdinRead, stdoutWrite;
|
||||||
|
|
||||||
|
// Create pipes
|
||||||
|
if (!CreatePipe(&stdinRead, &m_stdinWrite, &sa, 0)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!CreatePipe(&m_stdoutRead, &stdoutWrite, &sa, 0)) {
|
||||||
|
CloseHandle(stdinRead);
|
||||||
|
CloseHandle(m_stdinWrite);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't inherit our end of the pipes
|
||||||
|
SetHandleInformation(m_stdinWrite, HANDLE_FLAG_INHERIT, 0);
|
||||||
|
SetHandleInformation(m_stdoutRead, HANDLE_FLAG_INHERIT, 0);
|
||||||
|
|
||||||
|
// Build command line
|
||||||
|
std::string cmdLine = m_config.command;
|
||||||
|
for (const auto& arg : m_config.args) {
|
||||||
|
cmdLine += " " + arg;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup environment
|
||||||
|
std::string envBlock;
|
||||||
|
for (const auto& [key, value] : m_config.env) {
|
||||||
|
std::string resolvedValue = value;
|
||||||
|
// Resolve ${VAR} references
|
||||||
|
if (value.find("${") != std::string::npos) {
|
||||||
|
size_t start = value.find("${");
|
||||||
|
size_t end = value.find("}");
|
||||||
|
if (end > start) {
|
||||||
|
std::string envVar = value.substr(start + 2, end - start - 2);
|
||||||
|
const char* envVal = std::getenv(envVar.c_str());
|
||||||
|
if (envVal) {
|
||||||
|
resolvedValue = envVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
envBlock += key + "=" + resolvedValue + '\0';
|
||||||
|
}
|
||||||
|
envBlock += '\0';
|
||||||
|
|
||||||
|
STARTUPINFOA si = {};
|
||||||
|
si.cb = sizeof(si);
|
||||||
|
si.hStdInput = stdinRead;
|
||||||
|
si.hStdOutput = stdoutWrite;
|
||||||
|
si.hStdError = GetStdHandle(STD_ERROR_HANDLE);
|
||||||
|
si.dwFlags |= STARTF_USESTDHANDLES;
|
||||||
|
|
||||||
|
PROCESS_INFORMATION pi = {};
|
||||||
|
|
||||||
|
BOOL success = CreateProcessA(
|
||||||
|
nullptr,
|
||||||
|
const_cast<char*>(cmdLine.c_str()),
|
||||||
|
nullptr, nullptr,
|
||||||
|
TRUE,
|
||||||
|
0,
|
||||||
|
envBlock.empty() ? nullptr : const_cast<char*>(envBlock.c_str()),
|
||||||
|
nullptr,
|
||||||
|
&si, &pi
|
||||||
|
);
|
||||||
|
|
||||||
|
CloseHandle(stdinRead);
|
||||||
|
CloseHandle(stdoutWrite);
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
CloseHandle(m_stdinWrite);
|
||||||
|
CloseHandle(m_stdoutRead);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_processHandle = pi.hProcess;
|
||||||
|
CloseHandle(pi.hThread);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
#else
|
||||||
|
// Unix implementation
|
||||||
|
int stdinPipe[2];
|
||||||
|
int stdoutPipe[2];
|
||||||
|
|
||||||
|
if (pipe(stdinPipe) < 0 || pipe(stdoutPipe) < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_pid = fork();
|
||||||
|
|
||||||
|
if (m_pid < 0) {
|
||||||
|
close(stdinPipe[0]);
|
||||||
|
close(stdinPipe[1]);
|
||||||
|
close(stdoutPipe[0]);
|
||||||
|
close(stdoutPipe[1]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_pid == 0) {
|
||||||
|
// Child process
|
||||||
|
close(stdinPipe[1]); // Close write end of stdin
|
||||||
|
close(stdoutPipe[0]); // Close read end of stdout
|
||||||
|
|
||||||
|
dup2(stdinPipe[0], STDIN_FILENO);
|
||||||
|
dup2(stdoutPipe[1], STDOUT_FILENO);
|
||||||
|
|
||||||
|
close(stdinPipe[0]);
|
||||||
|
close(stdoutPipe[1]);
|
||||||
|
|
||||||
|
// Set environment variables
|
||||||
|
for (const auto& [key, value] : m_config.env) {
|
||||||
|
std::string resolvedValue = value;
|
||||||
|
if (value.find("${") != std::string::npos) {
|
||||||
|
size_t start = value.find("${");
|
||||||
|
size_t end = value.find("}");
|
||||||
|
if (end > start) {
|
||||||
|
std::string envVar = value.substr(start + 2, end - start - 2);
|
||||||
|
const char* envVal = std::getenv(envVar.c_str());
|
||||||
|
if (envVal) {
|
||||||
|
resolvedValue = envVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setenv(key.c_str(), resolvedValue.c_str(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build argv
|
||||||
|
std::vector<char*> argv;
|
||||||
|
argv.push_back(const_cast<char*>(m_config.command.c_str()));
|
||||||
|
for (const auto& arg : m_config.args) {
|
||||||
|
argv.push_back(const_cast<char*>(arg.c_str()));
|
||||||
|
}
|
||||||
|
argv.push_back(nullptr);
|
||||||
|
|
||||||
|
execvp(m_config.command.c_str(), argv.data());
|
||||||
|
_exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parent process
|
||||||
|
close(stdinPipe[0]); // Close read end of stdin
|
||||||
|
close(stdoutPipe[1]); // Close write end of stdout
|
||||||
|
|
||||||
|
m_stdinFd = stdinPipe[1];
|
||||||
|
m_stdoutFd = stdoutPipe[0];
|
||||||
|
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void StdioTransport::readerLoop() {
|
||||||
|
m_logger->debug("Reader thread started");
|
||||||
|
|
||||||
|
while (m_running) {
|
||||||
|
std::string line = readLine();
|
||||||
|
if (line.empty()) {
|
||||||
|
if (m_running) {
|
||||||
|
m_logger->warn("EOF from MCP server");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
json message = json::parse(line);
|
||||||
|
handleMessage(message);
|
||||||
|
} catch (const json::exception& e) {
|
||||||
|
m_logger->error("Failed to parse MCP message: {}", e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_logger->debug("Reader thread stopped");
|
||||||
|
}
|
||||||
|
|
||||||
|
void StdioTransport::handleMessage(const json& message) {
|
||||||
|
// Check if this is a response
|
||||||
|
if (message.contains("id") && (message.contains("result") || message.contains("error"))) {
|
||||||
|
int id = message["id"].get<int>();
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
auto it = m_pendingRequests.find(id);
|
||||||
|
if (it != m_pendingRequests.end()) {
|
||||||
|
auto pending = it->second;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> reqLock(pending->mutex);
|
||||||
|
pending->response = JsonRpcResponse::fromJson(message);
|
||||||
|
pending->hasResponse = true;
|
||||||
|
}
|
||||||
|
pending->cv.notify_one();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: Handle notifications from server
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonRpcResponse StdioTransport::sendRequest(const JsonRpcRequest& request, int timeoutMs) {
|
||||||
|
if (!m_running) {
|
||||||
|
JsonRpcResponse error;
|
||||||
|
error.error = json{{"code", -1}, {"message", "Transport not running"}};
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a mutable copy with assigned ID
|
||||||
|
JsonRpcRequest req = request;
|
||||||
|
req.id = m_nextRequestId++;
|
||||||
|
|
||||||
|
// Create pending request
|
||||||
|
auto pending = std::make_shared<PendingRequest>();
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
m_pendingRequests[req.id] = pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send request
|
||||||
|
std::string line = req.toJson().dump();
|
||||||
|
m_logger->debug("Sending: {}", line.substr(0, 200));
|
||||||
|
writeLine(line);
|
||||||
|
|
||||||
|
// Wait for response
|
||||||
|
std::unique_lock<std::mutex> lock(pending->mutex);
|
||||||
|
bool received = pending->cv.wait_for(lock, std::chrono::milliseconds(timeoutMs), [&] {
|
||||||
|
return pending->hasResponse;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> globalLock(m_mutex);
|
||||||
|
m_pendingRequests.erase(req.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!received) {
|
||||||
|
m_logger->warn("Request {} timed out", req.method);
|
||||||
|
JsonRpcResponse error;
|
||||||
|
error.id = req.id;
|
||||||
|
error.error = json{{"code", -2}, {"message", "Request timed out"}};
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pending->response;
|
||||||
|
}
|
||||||
|
|
||||||
|
void StdioTransport::sendNotification(const std::string& method, const json& params) {
|
||||||
|
if (!m_running) return;
|
||||||
|
|
||||||
|
json notification = {
|
||||||
|
{"jsonrpc", "2.0"},
|
||||||
|
{"method", method}
|
||||||
|
};
|
||||||
|
if (!params.is_null()) {
|
||||||
|
notification["params"] = params;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeLine(notification.dump());
|
||||||
|
}
|
||||||
|
|
||||||
|
void StdioTransport::writeLine(const std::string& line) {
|
||||||
|
std::string data = line + "\n";
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
DWORD written;
|
||||||
|
WriteFile(m_stdinWrite, data.c_str(), static_cast<DWORD>(data.size()), &written, nullptr);
|
||||||
|
#else
|
||||||
|
ssize_t result = write(m_stdinFd, data.c_str(), data.size());
|
||||||
|
(void)result; // Ignore return value for now
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string StdioTransport::readLine() {
|
||||||
|
std::string line;
|
||||||
|
char c;
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
DWORD bytesRead;
|
||||||
|
while (m_running && ReadFile(m_stdoutRead, &c, 1, &bytesRead, nullptr) && bytesRead > 0) {
|
||||||
|
if (c == '\n') break;
|
||||||
|
if (c != '\r') line += c;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
while (m_running) {
|
||||||
|
ssize_t n = read(m_stdoutFd, &c, 1);
|
||||||
|
if (n <= 0) break;
|
||||||
|
if (c == '\n') break;
|
||||||
|
if (c != '\r') line += c;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (!line.empty()) {
|
||||||
|
m_logger->debug("Received: {}", line.substr(0, 200));
|
||||||
|
}
|
||||||
|
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace aissia::mcp
|
||||||
80
src/shared/mcp/StdioTransport.hpp
Normal file
80
src/shared/mcp/StdioTransport.hpp
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "MCPTransport.hpp"
|
||||||
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
|
#include <thread>
|
||||||
|
#include <mutex>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <queue>
|
||||||
|
#include <atomic>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <windows.h>
|
||||||
|
#else
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace aissia::mcp {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Stdio transport for MCP servers
|
||||||
|
*
|
||||||
|
* Spawns a child process and communicates via stdin/stdout using JSON-RPC.
|
||||||
|
*/
|
||||||
|
class StdioTransport : public IMCPTransport {
|
||||||
|
public:
|
||||||
|
StdioTransport(const MCPServerConfig& config);
|
||||||
|
~StdioTransport() override;
|
||||||
|
|
||||||
|
bool start() override;
|
||||||
|
void stop() override;
|
||||||
|
bool isRunning() const override;
|
||||||
|
|
||||||
|
JsonRpcResponse sendRequest(const JsonRpcRequest& request, int timeoutMs = 30000) override;
|
||||||
|
void sendNotification(const std::string& method, const json& params) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
MCPServerConfig m_config;
|
||||||
|
std::shared_ptr<spdlog::logger> m_logger;
|
||||||
|
|
||||||
|
std::atomic<bool> m_running{false};
|
||||||
|
std::atomic<int> m_nextRequestId{1};
|
||||||
|
|
||||||
|
// Process handles
|
||||||
|
#ifdef _WIN32
|
||||||
|
HANDLE m_processHandle = nullptr;
|
||||||
|
HANDLE m_stdinWrite = nullptr;
|
||||||
|
HANDLE m_stdoutRead = nullptr;
|
||||||
|
#else
|
||||||
|
pid_t m_pid = -1;
|
||||||
|
int m_stdinFd = -1;
|
||||||
|
int m_stdoutFd = -1;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Reader thread
|
||||||
|
std::thread m_readerThread;
|
||||||
|
std::mutex m_mutex;
|
||||||
|
std::condition_variable m_responseCV;
|
||||||
|
|
||||||
|
// Pending requests
|
||||||
|
struct PendingRequest {
|
||||||
|
std::mutex mutex;
|
||||||
|
std::condition_variable cv;
|
||||||
|
JsonRpcResponse response;
|
||||||
|
bool hasResponse = false;
|
||||||
|
};
|
||||||
|
std::unordered_map<int, std::shared_ptr<PendingRequest>> m_pendingRequests;
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
bool spawnProcess();
|
||||||
|
void readerLoop();
|
||||||
|
void writeLine(const std::string& line);
|
||||||
|
std::string readLine();
|
||||||
|
void handleMessage(const json& message);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace aissia::mcp
|
||||||
@ -1,5 +1,6 @@
|
|||||||
#include "IWindowTracker.hpp"
|
#include "IWindowTracker.hpp"
|
||||||
#include <spdlog/spdlog.h>
|
#include <spdlog/spdlog.h>
|
||||||
|
#include <spdlog/sinks/stdout_color_sinks.h>
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#include "Win32WindowTracker.hpp"
|
#include "Win32WindowTracker.hpp"
|
||||||
|
|||||||
187
src/shared/tools/IOBridge.hpp
Normal file
187
src/shared/tools/IOBridge.hpp
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <grove/IIO.h>
|
||||||
|
#include <grove/JsonDataNode.h>
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
#include <spdlog/spdlog.h>
|
||||||
|
#include <spdlog/sinks/stdout_color_sinks.h>
|
||||||
|
|
||||||
|
#include <mutex>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <queue>
|
||||||
|
#include <chrono>
|
||||||
|
#include <atomic>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
namespace aissia {
|
||||||
|
|
||||||
|
using json = nlohmann::json;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Synchronous request/response bridge over IIO pub/sub
|
||||||
|
*
|
||||||
|
* Allows tools to make blocking requests to modules and wait for responses.
|
||||||
|
* Each request gets a unique correlation ID to match responses.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* auto response = bridge.request("scheduler:query", {"action": "get_current_task"}, 1000);
|
||||||
|
*/
|
||||||
|
class IOBridge {
|
||||||
|
public:
|
||||||
|
explicit IOBridge(grove::IIO* io) : m_io(io) {
|
||||||
|
m_logger = spdlog::get("IOBridge");
|
||||||
|
if (!m_logger) {
|
||||||
|
m_logger = spdlog::stdout_color_mt("IOBridge");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Send a request and wait for response
|
||||||
|
*
|
||||||
|
* @param topic Topic to publish request to
|
||||||
|
* @param request Request data (will add correlation_id)
|
||||||
|
* @param timeoutMs Timeout in milliseconds
|
||||||
|
* @return Response data or error JSON
|
||||||
|
*/
|
||||||
|
json request(const std::string& topic, const json& request, int timeoutMs = 5000) {
|
||||||
|
std::string correlationId = generateCorrelationId();
|
||||||
|
|
||||||
|
// Create response promise
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
m_pendingRequests[correlationId] = std::make_shared<PendingRequest>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build request with correlation ID
|
||||||
|
auto requestNode = std::make_unique<grove::JsonDataNode>("request");
|
||||||
|
requestNode->setString("correlation_id", correlationId);
|
||||||
|
|
||||||
|
// Copy all fields from input request
|
||||||
|
for (auto& [key, value] : request.items()) {
|
||||||
|
if (value.is_string()) {
|
||||||
|
requestNode->setString(key, value.get<std::string>());
|
||||||
|
} else if (value.is_number_integer()) {
|
||||||
|
requestNode->setInt(key, value.get<int>());
|
||||||
|
} else if (value.is_number_float()) {
|
||||||
|
requestNode->setDouble(key, value.get<double>());
|
||||||
|
} else if (value.is_boolean()) {
|
||||||
|
requestNode->setBool(key, value.get<bool>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publish request
|
||||||
|
m_io->publish(topic, std::move(requestNode));
|
||||||
|
m_logger->debug("Request sent to {} with correlation_id={}", topic, correlationId);
|
||||||
|
|
||||||
|
// Wait for response
|
||||||
|
auto pending = m_pendingRequests[correlationId];
|
||||||
|
std::unique_lock<std::mutex> lock(pending->mutex);
|
||||||
|
|
||||||
|
bool received = pending->cv.wait_for(lock, std::chrono::milliseconds(timeoutMs), [&] {
|
||||||
|
return pending->hasResponse;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> globalLock(m_mutex);
|
||||||
|
m_pendingRequests.erase(correlationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!received) {
|
||||||
|
m_logger->warn("Request to {} timed out after {}ms", topic, timeoutMs);
|
||||||
|
return {{"error", "timeout"}, {"message", "Request timed out"}};
|
||||||
|
}
|
||||||
|
|
||||||
|
return pending->response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Process incoming response messages
|
||||||
|
*
|
||||||
|
* Call this from the service's process loop to handle responses
|
||||||
|
* that come back from modules.
|
||||||
|
*
|
||||||
|
* @param topic The topic the message was received on
|
||||||
|
* @param data The message data
|
||||||
|
*/
|
||||||
|
void handleResponse(const std::string& topic, const grove::IDataNode& data) {
|
||||||
|
std::string correlationId = data.getString("correlation_id", "");
|
||||||
|
if (correlationId.empty()) {
|
||||||
|
return; // Not a response to our request
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<PendingRequest> pending;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
auto it = m_pendingRequests.find(correlationId);
|
||||||
|
if (it == m_pendingRequests.end()) {
|
||||||
|
m_logger->debug("Received response for unknown correlation_id={}", correlationId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pending = it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert IDataNode to JSON
|
||||||
|
json response;
|
||||||
|
|
||||||
|
// Extract common fields - this is a simplified conversion
|
||||||
|
// For complex nested data, we'd need to traverse the tree
|
||||||
|
auto* jsonNode = dynamic_cast<const grove::JsonDataNode*>(&data);
|
||||||
|
if (jsonNode) {
|
||||||
|
response = jsonNode->getJsonData();
|
||||||
|
} else {
|
||||||
|
// Fallback: extract known field types
|
||||||
|
response["correlation_id"] = correlationId;
|
||||||
|
// Add more fields as needed based on what modules return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signal the waiting thread
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(pending->mutex);
|
||||||
|
pending->response = response;
|
||||||
|
pending->hasResponse = true;
|
||||||
|
}
|
||||||
|
pending->cv.notify_one();
|
||||||
|
|
||||||
|
m_logger->debug("Response received for correlation_id={}", correlationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Subscribe to response topics
|
||||||
|
*
|
||||||
|
* Call this during initialization to listen for responses
|
||||||
|
*/
|
||||||
|
void subscribeToResponses() {
|
||||||
|
if (!m_io) return;
|
||||||
|
|
||||||
|
grove::SubscriptionConfig config;
|
||||||
|
// Subscribe to all response topics
|
||||||
|
m_io->subscribe("scheduler:response", config);
|
||||||
|
m_io->subscribe("monitoring:response", config);
|
||||||
|
m_io->subscribe("storage:response", config);
|
||||||
|
m_io->subscribe("voice:response", config);
|
||||||
|
m_io->subscribe("tool:response", config);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct PendingRequest {
|
||||||
|
std::mutex mutex;
|
||||||
|
std::condition_variable cv;
|
||||||
|
json response;
|
||||||
|
bool hasResponse = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
grove::IIO* m_io;
|
||||||
|
std::shared_ptr<spdlog::logger> m_logger;
|
||||||
|
|
||||||
|
std::mutex m_mutex;
|
||||||
|
std::unordered_map<std::string, std::shared_ptr<PendingRequest>> m_pendingRequests;
|
||||||
|
std::atomic<uint64_t> m_requestCounter{0};
|
||||||
|
|
||||||
|
std::string generateCorrelationId() {
|
||||||
|
auto now = std::chrono::steady_clock::now().time_since_epoch().count();
|
||||||
|
return "req_" + std::to_string(now) + "_" + std::to_string(m_requestCounter++);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace aissia
|
||||||
412
src/shared/tools/InternalTools.cpp
Normal file
412
src/shared/tools/InternalTools.cpp
Normal file
@ -0,0 +1,412 @@
|
|||||||
|
#include "InternalTools.hpp"
|
||||||
|
#include <spdlog/sinks/stdout_color_sinks.h>
|
||||||
|
|
||||||
|
namespace aissia {
|
||||||
|
|
||||||
|
InternalTools::InternalTools(grove::IIO* io) : m_io(io) {
|
||||||
|
m_logger = spdlog::get("InternalTools");
|
||||||
|
if (!m_logger) {
|
||||||
|
m_logger = spdlog::stdout_color_mt("InternalTools");
|
||||||
|
}
|
||||||
|
|
||||||
|
m_bridge = std::make_unique<IOBridge>(io);
|
||||||
|
m_bridge->subscribeToResponses();
|
||||||
|
|
||||||
|
registerTools();
|
||||||
|
|
||||||
|
m_logger->info("InternalTools initialized with {} tools", m_tools.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
void InternalTools::registerTools() {
|
||||||
|
// ========================================================================
|
||||||
|
// SCHEDULER TOOLS
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
m_tools.push_back({
|
||||||
|
"get_current_task",
|
||||||
|
"Get the task currently being worked on. Returns task name, duration, and status.",
|
||||||
|
{
|
||||||
|
{"type", "object"},
|
||||||
|
{"properties", json::object()},
|
||||||
|
{"required", json::array()}
|
||||||
|
},
|
||||||
|
[this](const json& input) { return getCurrentTask(input); }
|
||||||
|
});
|
||||||
|
|
||||||
|
m_tools.push_back({
|
||||||
|
"list_tasks",
|
||||||
|
"List all planned tasks for today. Returns task names, estimated durations, and completion status.",
|
||||||
|
{
|
||||||
|
{"type", "object"},
|
||||||
|
{"properties", {
|
||||||
|
{"include_completed", {
|
||||||
|
{"type", "boolean"},
|
||||||
|
{"description", "Whether to include completed tasks"},
|
||||||
|
{"default", false}
|
||||||
|
}}
|
||||||
|
}},
|
||||||
|
{"required", json::array()}
|
||||||
|
},
|
||||||
|
[this](const json& input) { return listTasks(input); }
|
||||||
|
});
|
||||||
|
|
||||||
|
m_tools.push_back({
|
||||||
|
"start_task",
|
||||||
|
"Start working on a specific task by its ID or name.",
|
||||||
|
{
|
||||||
|
{"type", "object"},
|
||||||
|
{"properties", {
|
||||||
|
{"task_id", {
|
||||||
|
{"type", "string"},
|
||||||
|
{"description", "Task ID or name to start"}
|
||||||
|
}}
|
||||||
|
}},
|
||||||
|
{"required", {"task_id"}}
|
||||||
|
},
|
||||||
|
[this](const json& input) { return startTask(input); }
|
||||||
|
});
|
||||||
|
|
||||||
|
m_tools.push_back({
|
||||||
|
"complete_task",
|
||||||
|
"Mark the current task or a specific task as completed.",
|
||||||
|
{
|
||||||
|
{"type", "object"},
|
||||||
|
{"properties", {
|
||||||
|
{"task_id", {
|
||||||
|
{"type", "string"},
|
||||||
|
{"description", "Task ID to complete. If not provided, completes current task."}
|
||||||
|
}}
|
||||||
|
}},
|
||||||
|
{"required", json::array()}
|
||||||
|
},
|
||||||
|
[this](const json& input) { return completeTask(input); }
|
||||||
|
});
|
||||||
|
|
||||||
|
m_tools.push_back({
|
||||||
|
"start_break",
|
||||||
|
"Start a break period. The user will be reminded when break is over.",
|
||||||
|
{
|
||||||
|
{"type", "object"},
|
||||||
|
{"properties", {
|
||||||
|
{"duration_minutes", {
|
||||||
|
{"type", "integer"},
|
||||||
|
{"description", "Break duration in minutes"},
|
||||||
|
{"default", 10}
|
||||||
|
}},
|
||||||
|
{"reason", {
|
||||||
|
{"type", "string"},
|
||||||
|
{"description", "Reason for break (coffee, stretch, etc.)"}
|
||||||
|
}}
|
||||||
|
}},
|
||||||
|
{"required", json::array()}
|
||||||
|
},
|
||||||
|
[this](const json& input) { return startBreak(input); }
|
||||||
|
});
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// MONITORING TOOLS
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
m_tools.push_back({
|
||||||
|
"get_focus_stats",
|
||||||
|
"Get focus statistics: time spent on productive vs distracting apps, current session duration.",
|
||||||
|
{
|
||||||
|
{"type", "object"},
|
||||||
|
{"properties", {
|
||||||
|
{"period", {
|
||||||
|
{"type", "string"},
|
||||||
|
{"enum", {"today", "week", "month"}},
|
||||||
|
{"description", "Time period for stats"},
|
||||||
|
{"default", "today"}
|
||||||
|
}}
|
||||||
|
}},
|
||||||
|
{"required", json::array()}
|
||||||
|
},
|
||||||
|
[this](const json& input) { return getFocusStats(input); }
|
||||||
|
});
|
||||||
|
|
||||||
|
m_tools.push_back({
|
||||||
|
"get_current_app",
|
||||||
|
"Get the currently active application name and window title.",
|
||||||
|
{
|
||||||
|
{"type", "object"},
|
||||||
|
{"properties", json::object()},
|
||||||
|
{"required", json::array()}
|
||||||
|
},
|
||||||
|
[this](const json& input) { return getCurrentApp(input); }
|
||||||
|
});
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// STORAGE TOOLS
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
m_tools.push_back({
|
||||||
|
"save_note",
|
||||||
|
"Save a note or reminder for the user. Notes are searchable and timestamped.",
|
||||||
|
{
|
||||||
|
{"type", "object"},
|
||||||
|
{"properties", {
|
||||||
|
{"content", {
|
||||||
|
{"type", "string"},
|
||||||
|
{"description", "Note content to save"}
|
||||||
|
}},
|
||||||
|
{"tags", {
|
||||||
|
{"type", "array"},
|
||||||
|
{"items", {{"type", "string"}}},
|
||||||
|
{"description", "Optional tags for categorization"}
|
||||||
|
}}
|
||||||
|
}},
|
||||||
|
{"required", {"content"}}
|
||||||
|
},
|
||||||
|
[this](const json& input) { return saveNote(input); }
|
||||||
|
});
|
||||||
|
|
||||||
|
m_tools.push_back({
|
||||||
|
"query_notes",
|
||||||
|
"Search through saved notes by keyword or tag.",
|
||||||
|
{
|
||||||
|
{"type", "object"},
|
||||||
|
{"properties", {
|
||||||
|
{"query", {
|
||||||
|
{"type", "string"},
|
||||||
|
{"description", "Search query"}
|
||||||
|
}},
|
||||||
|
{"tag", {
|
||||||
|
{"type", "string"},
|
||||||
|
{"description", "Filter by tag"}
|
||||||
|
}},
|
||||||
|
{"limit", {
|
||||||
|
{"type", "integer"},
|
||||||
|
{"description", "Maximum number of notes to return"},
|
||||||
|
{"default", 10}
|
||||||
|
}}
|
||||||
|
}},
|
||||||
|
{"required", json::array()}
|
||||||
|
},
|
||||||
|
[this](const json& input) { return queryNotes(input); }
|
||||||
|
});
|
||||||
|
|
||||||
|
m_tools.push_back({
|
||||||
|
"get_session_history",
|
||||||
|
"Get history of work sessions with durations and focus scores.",
|
||||||
|
{
|
||||||
|
{"type", "object"},
|
||||||
|
{"properties", {
|
||||||
|
{"days", {
|
||||||
|
{"type", "integer"},
|
||||||
|
{"description", "Number of days to look back"},
|
||||||
|
{"default", 7}
|
||||||
|
}}
|
||||||
|
}},
|
||||||
|
{"required", json::array()}
|
||||||
|
},
|
||||||
|
[this](const json& input) { return getSessionHistory(input); }
|
||||||
|
});
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// VOICE TOOLS
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
m_tools.push_back({
|
||||||
|
"speak",
|
||||||
|
"Make the assistant speak a message out loud using text-to-speech.",
|
||||||
|
{
|
||||||
|
{"type", "object"},
|
||||||
|
{"properties", {
|
||||||
|
{"message", {
|
||||||
|
{"type", "string"},
|
||||||
|
{"description", "Message to speak"}
|
||||||
|
}},
|
||||||
|
{"priority", {
|
||||||
|
{"type", "string"},
|
||||||
|
{"enum", {"low", "normal", "high"}},
|
||||||
|
{"description", "Speech priority"},
|
||||||
|
{"default", "normal"}
|
||||||
|
}}
|
||||||
|
}},
|
||||||
|
{"required", {"message"}}
|
||||||
|
},
|
||||||
|
[this](const json& input) { return speak(input); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ToolDefinition> InternalTools::getTools() const {
|
||||||
|
return m_tools;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InternalTools::processResponses() {
|
||||||
|
if (!m_io) return;
|
||||||
|
|
||||||
|
while (m_io->hasMessages() > 0) {
|
||||||
|
auto msg = m_io->pullMessage();
|
||||||
|
|
||||||
|
// Check if this is a response message
|
||||||
|
if (msg.topic.find(":response") != std::string::npos && msg.data) {
|
||||||
|
m_bridge->handleResponse(msg.topic, *msg.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// TOOL IMPLEMENTATIONS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
json InternalTools::getCurrentTask(const json& input) {
|
||||||
|
auto response = m_bridge->request("scheduler:query", {
|
||||||
|
{"action", "get_current_task"}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.contains("error")) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
{"task_id", response.value("task_id", "")},
|
||||||
|
{"task_name", response.value("task_name", "No active task")},
|
||||||
|
{"duration_minutes", response.value("duration_minutes", 0)},
|
||||||
|
{"started_at", response.value("started_at", "")}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
json InternalTools::listTasks(const json& input) {
|
||||||
|
bool includeCompleted = input.value("include_completed", false);
|
||||||
|
|
||||||
|
auto response = m_bridge->request("scheduler:query", {
|
||||||
|
{"action", "list_tasks"},
|
||||||
|
{"include_completed", includeCompleted}
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
json InternalTools::startTask(const json& input) {
|
||||||
|
std::string taskId = input.value("task_id", "");
|
||||||
|
if (taskId.empty()) {
|
||||||
|
return {{"error", "missing_parameter"}, {"message", "task_id is required"}};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto response = m_bridge->request("scheduler:command", {
|
||||||
|
{"action", "start_task"},
|
||||||
|
{"task_id", taskId}
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
json InternalTools::completeTask(const json& input) {
|
||||||
|
std::string taskId = input.value("task_id", "");
|
||||||
|
|
||||||
|
auto response = m_bridge->request("scheduler:command", {
|
||||||
|
{"action", "complete_task"},
|
||||||
|
{"task_id", taskId} // Empty = complete current task
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
json InternalTools::startBreak(const json& input) {
|
||||||
|
int duration = input.value("duration_minutes", 10);
|
||||||
|
std::string reason = input.value("reason", "break");
|
||||||
|
|
||||||
|
auto response = m_bridge->request("scheduler:command", {
|
||||||
|
{"action", "start_break"},
|
||||||
|
{"duration_minutes", duration},
|
||||||
|
{"reason", reason}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
{"success", true},
|
||||||
|
{"message", "Break started for " + std::to_string(duration) + " minutes"},
|
||||||
|
{"reason", reason}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
json InternalTools::getFocusStats(const json& input) {
|
||||||
|
std::string period = input.value("period", "today");
|
||||||
|
|
||||||
|
auto response = m_bridge->request("monitoring:query", {
|
||||||
|
{"action", "get_focus_stats"},
|
||||||
|
{"period", period}
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
json InternalTools::getCurrentApp(const json& input) {
|
||||||
|
auto response = m_bridge->request("monitoring:query", {
|
||||||
|
{"action", "get_current_app"}
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
json InternalTools::saveNote(const json& input) {
|
||||||
|
std::string content = input.value("content", "");
|
||||||
|
if (content.empty()) {
|
||||||
|
return {{"error", "missing_parameter"}, {"message", "content is required"}};
|
||||||
|
}
|
||||||
|
|
||||||
|
json tags = input.value("tags", json::array());
|
||||||
|
|
||||||
|
auto response = m_bridge->request("storage:command", {
|
||||||
|
{"action", "save_note"},
|
||||||
|
{"content", content},
|
||||||
|
{"tags", tags}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
{"success", true},
|
||||||
|
{"message", "Note saved"},
|
||||||
|
{"note_id", response.value("note_id", "")}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
json InternalTools::queryNotes(const json& input) {
|
||||||
|
std::string query = input.value("query", "");
|
||||||
|
std::string tag = input.value("tag", "");
|
||||||
|
int limit = input.value("limit", 10);
|
||||||
|
|
||||||
|
auto response = m_bridge->request("storage:query", {
|
||||||
|
{"action", "query_notes"},
|
||||||
|
{"query", query},
|
||||||
|
{"tag", tag},
|
||||||
|
{"limit", limit}
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
json InternalTools::getSessionHistory(const json& input) {
|
||||||
|
int days = input.value("days", 7);
|
||||||
|
|
||||||
|
auto response = m_bridge->request("storage:query", {
|
||||||
|
{"action", "get_session_history"},
|
||||||
|
{"days", days}
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
json InternalTools::speak(const json& input) {
|
||||||
|
std::string message = input.value("message", "");
|
||||||
|
if (message.empty()) {
|
||||||
|
return {{"error", "missing_parameter"}, {"message", "message is required"}};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string priority = input.value("priority", "normal");
|
||||||
|
|
||||||
|
// For speak, we don't wait for response - it's fire and forget
|
||||||
|
auto requestNode = std::make_unique<grove::JsonDataNode>("request");
|
||||||
|
requestNode->setString("action", "speak");
|
||||||
|
requestNode->setString("text", message);
|
||||||
|
requestNode->setString("priority", priority);
|
||||||
|
|
||||||
|
m_io->publish("voice:command", std::move(requestNode));
|
||||||
|
|
||||||
|
return {
|
||||||
|
{"success", true},
|
||||||
|
{"message", "Speech queued"}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace aissia
|
||||||
70
src/shared/tools/InternalTools.hpp
Normal file
70
src/shared/tools/InternalTools.hpp
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "IOBridge.hpp"
|
||||||
|
#include "../llm/ToolRegistry.hpp"
|
||||||
|
|
||||||
|
#include <grove/IIO.h>
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace aissia {
|
||||||
|
|
||||||
|
using json = nlohmann::json;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Internal tools that communicate with GroveEngine modules via IIO
|
||||||
|
*
|
||||||
|
* These tools allow the LLM to interact with:
|
||||||
|
* - SchedulerModule: tasks, breaks, hyperfocus
|
||||||
|
* - MonitoringModule: app usage, focus stats
|
||||||
|
* - StorageModule: notes, history
|
||||||
|
* - VoiceModule: TTS control
|
||||||
|
*/
|
||||||
|
class InternalTools {
|
||||||
|
public:
|
||||||
|
explicit InternalTools(grove::IIO* io);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get all tool definitions for registration
|
||||||
|
*/
|
||||||
|
std::vector<ToolDefinition> getTools() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Process incoming responses from modules
|
||||||
|
*
|
||||||
|
* Call this in the service's process loop
|
||||||
|
*/
|
||||||
|
void processResponses();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get number of registered tools
|
||||||
|
*/
|
||||||
|
size_t size() const { return m_tools.size(); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
grove::IIO* m_io;
|
||||||
|
std::unique_ptr<IOBridge> m_bridge;
|
||||||
|
std::shared_ptr<spdlog::logger> m_logger;
|
||||||
|
std::vector<ToolDefinition> m_tools;
|
||||||
|
|
||||||
|
// Tool implementations
|
||||||
|
json getCurrentTask(const json& input);
|
||||||
|
json listTasks(const json& input);
|
||||||
|
json startTask(const json& input);
|
||||||
|
json completeTask(const json& input);
|
||||||
|
json startBreak(const json& input);
|
||||||
|
json getFocusStats(const json& input);
|
||||||
|
json getCurrentApp(const json& input);
|
||||||
|
json saveNote(const json& input);
|
||||||
|
json queryNotes(const json& input);
|
||||||
|
json getSessionHistory(const json& input);
|
||||||
|
json speak(const json& input);
|
||||||
|
|
||||||
|
// Helper to create tool definitions
|
||||||
|
void registerTools();
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace aissia
|
||||||
Loading…
Reference in New Issue
Block a user