diff --git a/PROMPT_SUCCESSEUR.md b/PROMPT_SUCCESSEUR.md new file mode 100644 index 0000000..2ddaa0b --- /dev/null +++ b/PROMPT_SUCCESSEUR.md @@ -0,0 +1,105 @@ +# Prompt Successeur - AISSIA + +## Contexte + +Tu reprends le développement d'**AISSIA**, un assistant vocal agentique en C++17 basé sur GroveEngine. + +**Architecture** : Services (non hot-reload) + Modules (.so hot-reload) + MCP (client et serveur) + +## État Actuel + +✅ Build OK : `cmake -B build && cmake --build build -j4` +✅ 6 modules hot-reload fonctionnels +✅ 4 services infrastructure (LLM, Storage, Platform, Voice) +✅ 17 tools pour l'agent LLM +✅ Mode MCP Server : `./build/aissia --mcp-server` +✅ Mode interactif : `./build/aissia --interactive` ou `-i` +✅ **Tests MCP : 50/50 passent** (transport + client) +✅ **Tests totaux : 102/110** (8 échecs dans tests modules) + +## Fichiers Clés + +| Fichier | Rôle | +|---------|------| +| `src/main.cpp` | Entry point, charge modules, route messages, mode interactif | +| `src/services/LLMService.*` | Boucle agentique, ToolRegistry, appels Claude | +| `src/shared/mcp/MCPServer.*` | AISSIA comme serveur MCP (stdio JSON-RPC) | +| `src/shared/mcp/MCPClient.*` | Consomme serveurs MCP externes | +| `src/shared/mcp/StdioTransport.*` | Transport stdio pour MCP (spawne process enfant) | +| `src/shared/tools/FileSystemTools.*` | 6 tools fichiers (read/write/edit/glob/grep) | +| `src/shared/tools/InternalTools.*` | 11 tools internes (scheduler, voice, storage) | + +## Communication + +``` +┌─────────────┐ IIO pub/sub ┌─────────────┐ +│ Modules │ ◄──────────────────► │ Services │ +│ (.so hot) │ JsonDataNode │ (static) │ +└─────────────┘ └─────────────┘ + │ + HTTP │ + ▼ + ┌─────────────┐ + │ Claude API │ + └─────────────┘ +``` + +## Commandes + +```bash +# Build +cmake -B build && cmake --build build -j4 + +# Run normal (boucle principale sans interaction) +./build/aissia + +# Run avec interface stdin interactive +./build/aissia --interactive # ou -i +# Tape "quit" ou "q" pour quitter + +# Run comme serveur MCP (pour Claude Code) +./build/aissia --mcp-server + +# Tests +cp tests/fixtures/mock_mcp.json build/tests/fixtures/ +./build/tests/aissia_tests # Tous (102/110) +./build/tests/aissia_tests "[mcp]" # MCP (50/50) +./build/tests/aissia_tests "[transport]" # Transport (20/20) +./build/tests/aissia_tests "[client]" # Client (15/15) +``` + +## Tests qui échouent (8) + +Tous dans les modules, pas critiques : +- `MonitoringModuleTests.cpp` : 4 tests (monitoring:app_changed non publié) +- `AIModuleTests.cpp` : 2 tests (ai:suggestion non publié) +- `VoiceModuleTests.cpp` : 1 test (voice:speak assertion) +- `StorageModuleTests.cpp` : 1 test (durationMinutes != 45) + +## Prochaines Étapes Suggérées + +1. **Tester la boucle agentique** - `export ANTHROPIC_API_KEY=sk-...` puis `./build/aissia -i` + - Essayer "Quelle heure est-il ?" (tool get_current_time) + - Essayer "Liste les fichiers dans src/" (tool glob) + +2. **Fixer les 8 tests modules** - Problèmes de publication IIO dans les mocks + +3. **Exposer InternalTools via MCP Server** - Actuellement seuls FileSystem + get_current_time sont exposés. Les InternalTools (scheduler, voice, storage) nécessitent que les modules tournent (IIO). + +## Notes Techniques + +- **API key** : `ANTHROPIC_API_KEY` dans env ou `.env` +- **WSL** : window tracker et TTS utilisent des stubs +- **GroveEngine** : symlink vers `../GroveEngine` +- **Hot-reload** : modifier un .so dans `build/modules/` → rechargé automatiquement +- **Tests fixtures** : toujours copier `mock_mcp.json` avant tests client + +## Changements Récents (cette session) + +1. **Fix python → python3** dans configs et tests MCP +2. **Fix StdioTransport** : + - Préserve l'ID de requête fourni par l'utilisateur + - Détecte les commandes invalides (waitpid après 100ms) + - Stop non-bloquant (ferme stdout avant join reader) + - Ignore les messages avec id=null (notifications) +3. **Ajout mode interactif** (`--interactive`) pour tester la boucle LLM diff --git a/src/main.cpp b/src/main.cpp index 0a87600..1949b27 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -20,6 +20,9 @@ #include #include #include +#include +#include +#include namespace fs = std::filesystem; @@ -105,6 +108,8 @@ struct ModuleEntry { // Message router between modules and services class MessageRouter { public: + using MessageCallback = std::function; + void addModuleIO(const std::string& name, grove::IIO* io) { m_moduleIOs[name] = io; } @@ -113,6 +118,10 @@ public: m_serviceIOs[name] = io; } + void setMessageCallback(MessageCallback callback) { + m_callback = callback; + } + void routeMessages() { // Collect all messages from modules and services std::vector messages; @@ -133,6 +142,11 @@ public: // Route messages to appropriate destinations for (auto& msg : messages) { + // Call callback if set + if (m_callback && msg.data) { + m_callback(msg.topic, msg.data.get()); + } + // Determine destination based on topic prefix std::string prefix = msg.topic.substr(0, msg.topic.find(':')); @@ -157,6 +171,7 @@ public: private: std::map m_moduleIOs; std::map m_serviceIOs; + MessageCallback m_callback; }; // Run AISSIA as MCP server (stdio mode) @@ -172,6 +187,20 @@ int runMCPServer() { // Create tool registry with FileSystem tools aissia::ToolRegistry registry; + // Register get_current_time tool + registry.registerTool( + "get_current_time", + "Get the current date and time", + {{"type", "object"}, {"properties", nlohmann::json::object()}}, + [](const nlohmann::json& input) -> nlohmann::json { + std::time_t now = std::time(nullptr); + std::tm* tm = std::localtime(&now); + char buffer[64]; + std::strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", tm); + return {{"time", buffer}}; + } + ); + // Register FileSystem tools for (const auto& toolDef : aissia::tools::FileSystemTools::getToolDefinitions()) { std::string toolName = toolDef["name"].get(); @@ -195,6 +224,39 @@ int runMCPServer() { return 0; } +// Flag for interactive mode +static std::atomic g_interactive{false}; +static grove::IIO* g_interactiveIO = nullptr; + +// Stdin reader thread for interactive mode +void stdinReaderThread() { + std::string line; + std::cout << "\n[AISSIA] Mode interactif active. Tapez vos messages (quit pour quitter):\n> " << std::flush; + + while (g_running && std::getline(std::cin, line)) { + if (line == "quit" || line == "exit" || line == "q") { + g_running = false; + break; + } + + if (line.empty()) { + std::cout << "> " << std::flush; + continue; + } + + // Send message to LLM service via IIO (from InteractiveClient, not LLMService) + if (g_interactiveIO) { + auto request = std::make_unique("request"); + request->setString("query", line); + request->setString("conversation_id", "interactive"); + g_interactiveIO->publish("llm:request", std::move(request)); + spdlog::info("Message envoye: {}", line); + } + + std::cout << "> " << std::flush; + } +} + int main(int argc, char* argv[]) { // Check for MCP server mode if (argc > 1 && (std::strcmp(argv[1], "--mcp-server") == 0 || @@ -202,6 +264,14 @@ int main(int argc, char* argv[]) { return runMCPServer(); } + // Check for interactive mode + for (int i = 1; i < argc; ++i) { + if (std::strcmp(argv[i], "--interactive") == 0 || + std::strcmp(argv[i], "-i") == 0) { + g_interactive = true; + } + } + // Setup logging auto console = spdlog::stdout_color_mt("Aissia"); spdlog::set_default_logger(console); @@ -308,6 +378,22 @@ int main(int argc, char* argv[]) { platformService.isHealthy() ? "OK" : "FAIL", voiceService.isHealthy() ? "OK" : "FAIL"); + // Create separate IO for interactive mode (to avoid self-delivery skip) + std::unique_ptr interactiveIO; + std::unique_ptr stdinThread; + if (g_interactive) { + spdlog::info("Mode interactif active (--interactive)"); + interactiveIO = grove::IOFactory::create("intra", "InteractiveClient"); + g_interactiveIO = interactiveIO.get(); + + // Subscribe to LLM responses to display them + grove::SubscriptionConfig subConfig; + interactiveIO->subscribe("llm:response", subConfig); + interactiveIO->subscribe("llm:error", subConfig); + + stdinThread = std::make_unique(stdinReaderThread); + } + // ========================================================================= // Hot-Reloadable Modules // ========================================================================= @@ -321,6 +407,27 @@ int main(int argc, char* argv[]) { router.addServiceIO("PlatformService", platformIO.get()); router.addServiceIO("VoiceService", voiceIO.get()); + // Setup interactive mode callback to display LLM responses + if (g_interactive) { + router.setMessageCallback([](const std::string& topic, grove::IDataNode* data) { + if (topic == "llm:response" && data) { + auto* jsonNode = dynamic_cast(data); + if (jsonNode) { + std::string text = jsonNode->getString("text", ""); + if (!text.empty()) { + std::cout << "\n[AISSIA] " << text << "\n> " << std::flush; + } + } + } else if (topic == "llm:error" && data) { + auto* jsonNode = dynamic_cast(data); + if (jsonNode) { + std::string message = jsonNode->getString("message", "Unknown error"); + std::cout << "\n[ERREUR] " << message << "\n> " << std::flush; + } + } + }); + } + // Liste des modules a charger (sans infrastructure) std::vector> moduleList = { {"SchedulerModule", "scheduler.json"}, @@ -451,6 +558,24 @@ int main(int argc, char* argv[]) { // ===================================================================== router.routeMessages(); + // ===================================================================== + // Process interactive mode responses + // ===================================================================== + if (g_interactive && interactiveIO) { + while (interactiveIO->hasMessages() > 0) { + auto msg = interactiveIO->pullMessage(); + if (msg.topic == "llm:response" && msg.data) { + std::string text = msg.data->getString("text", ""); + if (!text.empty()) { + std::cout << "\n[AISSIA] " << text << "\n> " << std::flush; + } + } else if (msg.topic == "llm:error" && msg.data) { + std::string error = msg.data->getString("message", "Unknown error"); + std::cout << "\n[ERREUR] " << error << "\n> " << std::flush; + } + } + } + // ===================================================================== // Frame timing // ===================================================================== @@ -477,6 +602,13 @@ int main(int argc, char* argv[]) { // ========================================================================= spdlog::info("Arret en cours..."); + // Stop interactive thread + if (stdinThread && stdinThread->joinable()) { + // Thread will exit when g_running becomes false or on EOF + // We can't cleanly interrupt stdin reading, so just detach + stdinThread->detach(); + } + // Shutdown modules first for (auto& [name, entry] : modules) { if (entry.module) { @@ -491,6 +623,8 @@ int main(int argc, char* argv[]) { storageService.shutdown(); llmService.shutdown(); + g_interactiveIO = nullptr; + spdlog::info("A bientot!"); return 0; } diff --git a/src/shared/http/HttpClient.hpp b/src/shared/http/HttpClient.hpp index 0f160f8..aec630b 100644 --- a/src/shared/http/HttpClient.hpp +++ b/src/shared/http/HttpClient.hpp @@ -68,18 +68,17 @@ public: HttpResponse response; try { - std::unique_ptr client; + // Create client with full URL scheme for proper HTTPS support + std::string url = (m_useSSL ? "https://" : "http://") + m_host; + httplib::Client client(url); if (m_useSSL) { - client = std::make_unique(m_host); - client->enable_server_certificate_verification(true); - } else { - client = std::make_unique(m_host); + client.enable_server_certificate_verification(true); } - client->set_connection_timeout(m_timeoutSeconds); - client->set_read_timeout(m_timeoutSeconds); - client->set_write_timeout(m_timeoutSeconds); + client.set_connection_timeout(m_timeoutSeconds); + client.set_read_timeout(m_timeoutSeconds); + client.set_write_timeout(m_timeoutSeconds); httplib::Headers headers; headers.emplace("Content-Type", "application/json"); @@ -90,7 +89,7 @@ public: std::string bodyStr = body.dump(); m_logger->debug("POST {} ({} bytes)", path, bodyStr.size()); - auto result = client->Post(path, headers, bodyStr, "application/json"); + auto result = client.Post(path, headers, bodyStr, "application/json"); if (result) { response.status = result->status; @@ -120,24 +119,22 @@ public: HttpResponse response; try { - std::unique_ptr client; + std::string url = (m_useSSL ? "https://" : "http://") + m_host; + httplib::Client client(url); if (m_useSSL) { - client = std::make_unique(m_host); - client->enable_server_certificate_verification(true); - } else { - client = std::make_unique(m_host); + client.enable_server_certificate_verification(true); } - client->set_connection_timeout(m_timeoutSeconds); - client->set_read_timeout(m_timeoutSeconds); + client.set_connection_timeout(m_timeoutSeconds); + client.set_read_timeout(m_timeoutSeconds); httplib::Headers headers; for (const auto& [key, value] : m_headers) { headers.emplace(key, value); } - auto result = client->Post(path, headers, items); + auto result = client.Post(path, headers, items); if (result) { response.status = result->status; @@ -158,23 +155,22 @@ public: HttpResponse response; try { - std::unique_ptr client; + std::string url = (m_useSSL ? "https://" : "http://") + m_host; + httplib::Client client(url); if (m_useSSL) { - client = std::make_unique(m_host); - } else { - client = std::make_unique(m_host); + client.enable_server_certificate_verification(true); } - client->set_connection_timeout(m_timeoutSeconds); - client->set_read_timeout(m_timeoutSeconds); + client.set_connection_timeout(m_timeoutSeconds); + client.set_read_timeout(m_timeoutSeconds); httplib::Headers headers; for (const auto& [key, value] : m_headers) { headers.emplace(key, value); } - auto result = client->Get(path, headers); + auto result = client.Get(path, headers); if (result) { response.status = result->status; diff --git a/src/shared/llm/ClaudeProvider.cpp b/src/shared/llm/ClaudeProvider.cpp index 2870d29..a3fe2af 100644 --- a/src/shared/llm/ClaudeProvider.cpp +++ b/src/shared/llm/ClaudeProvider.cpp @@ -45,7 +45,8 @@ LLMResponse ClaudeProvider::chat(const std::string& systemPrompt, request["tools"] = convertTools(tools); } - m_logger->debug("Sending request to Claude: {} messages", messages.size()); + m_logger->debug("Sending request to Claude: {} messages, {} tools", messages.size(), tools.size()); + m_logger->trace("Request body: {}", request.dump(2)); auto response = m_client->post("/v1/messages", request); diff --git a/src/shared/mcp/StdioTransport.cpp b/src/shared/mcp/StdioTransport.cpp index 3e4f477..50cbe5d 100644 --- a/src/shared/mcp/StdioTransport.cpp +++ b/src/shared/mcp/StdioTransport.cpp @@ -3,6 +3,10 @@ #include #include +#include +#include +#include +#include namespace aissia::mcp { @@ -50,26 +54,12 @@ void StdioTransport::stop() { m_running = false; - // Close write pipe to signal EOF to child + // Close pipes and terminate process first, so reader thread can exit #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); @@ -80,10 +70,13 @@ void StdioTransport::stop() { m_stdoutRead = nullptr; } #else + if (m_stdinFd >= 0) { + close(m_stdinFd); + m_stdinFd = -1; + } if (m_pid > 0) { kill(m_pid, SIGTERM); - waitpid(m_pid, nullptr, 0); - m_pid = -1; + // Don't wait here - let the reader thread see EOF first } if (m_stdoutFd >= 0) { close(m_stdoutFd); @@ -91,6 +84,19 @@ void StdioTransport::stop() { } #endif + // Now wait for reader thread (should exit quickly since stdout is closed) + if (m_readerThread.joinable()) { + m_readerThread.join(); + } + + // Final cleanup - wait for child process +#ifndef _WIN32 + if (m_pid > 0) { + waitpid(m_pid, nullptr, 0); + m_pid = -1; + } +#endif + m_logger->info("MCP server stopped"); } @@ -248,6 +254,27 @@ bool StdioTransport::spawnProcess() { m_stdinFd = stdinPipe[1]; m_stdoutFd = stdoutPipe[0]; + // Give the child a moment to fail if command doesn't exist + // 100ms should be enough for execvp to fail on invalid command + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Check if child is still alive + int status; + pid_t result = waitpid(m_pid, &status, WNOHANG); + if (result == m_pid) { + // Child has exited - command likely failed (exit code 1 from _exit) + m_logger->error("Child process exited immediately (command not found?)"); + close(m_stdinFd); + close(m_stdoutFd); + m_stdinFd = -1; + m_stdoutFd = -1; + m_pid = -1; + return false; + } else if (result == -1) { + // Error in waitpid + m_logger->error("waitpid error: {}", strerror(errno)); + } + return true; #endif } @@ -276,8 +303,9 @@ void StdioTransport::readerLoop() { } void StdioTransport::handleMessage(const json& message) { - // Check if this is a response - if (message.contains("id") && (message.contains("result") || message.contains("error"))) { + // Check if this is a response (id must be a number, not null) + if (message.contains("id") && message["id"].is_number() && + (message.contains("result") || message.contains("error"))) { int id = message["id"].get(); std::lock_guard lock(m_mutex); @@ -302,9 +330,11 @@ JsonRpcResponse StdioTransport::sendRequest(const JsonRpcRequest& request, int t return error; } - // Create a mutable copy with assigned ID + // Create a mutable copy, use provided ID if > 0, otherwise generate one JsonRpcRequest req = request; - req.id = m_nextRequestId++; + if (req.id <= 0) { + req.id = m_nextRequestId++; + } // Create pending request auto pending = std::make_shared(); diff --git a/tests/fixtures/mock_mcp.json b/tests/fixtures/mock_mcp.json index 61e6407..51aebf7 100644 --- a/tests/fixtures/mock_mcp.json +++ b/tests/fixtures/mock_mcp.json @@ -1,7 +1,7 @@ { "servers": { "mock_server": { - "command": "python", + "command": "python3", "args": ["tests/fixtures/mock_mcp_server.py"], "enabled": true }, @@ -11,7 +11,7 @@ "enabled": false }, "echo_server": { - "command": "python", + "command": "python3", "args": ["tests/fixtures/echo_server.py"], "enabled": true } diff --git a/tests/mcp/MCPClientTests.cpp b/tests/mcp/MCPClientTests.cpp index 81254e3..676235d 100644 --- a/tests/mcp/MCPClientTests.cpp +++ b/tests/mcp/MCPClientTests.cpp @@ -38,7 +38,7 @@ TEST_CASE("TI_CLIENT_001_LoadConfigValid", "[mcp][client]") { json config = { {"servers", { {"test_server", { - {"command", "python"}, + {"command", "python3"}, {"args", json::array({"server.py"})}, {"enabled", true} }} @@ -112,7 +112,7 @@ TEST_CASE("TI_CLIENT_005_ConnectAllSkipsDisabled", "[mcp][client]") { json config = { {"servers", { {"enabled_server", { - {"command", "python"}, + {"command", "python3"}, {"args", json::array({"tests/fixtures/echo_server.py"})}, {"enabled", true} }}, @@ -143,12 +143,12 @@ TEST_CASE("TI_CLIENT_006_ConnectSingleServer", "[mcp][client]") { json config = { {"servers", { {"server1", { - {"command", "python"}, + {"command", "python3"}, {"args", json::array({"tests/fixtures/echo_server.py"})}, {"enabled", true} }}, {"server2", { - {"command", "python"}, + {"command", "python3"}, {"args", json::array({"tests/fixtures/echo_server.py"})}, {"enabled", true} }} @@ -178,7 +178,7 @@ TEST_CASE("TI_CLIENT_007_DisconnectSingleServer", "[mcp][client]") { json config = { {"servers", { {"server1", { - {"command", "python"}, + {"command", "python3"}, {"args", json::array({"tests/fixtures/echo_server.py"})}, {"enabled", true} }} @@ -205,12 +205,12 @@ TEST_CASE("TI_CLIENT_008_DisconnectAllCleansUp", "[mcp][client]") { json config = { {"servers", { {"server1", { - {"command", "python"}, + {"command", "python3"}, {"args", json::array({"tests/fixtures/echo_server.py"})}, {"enabled", true} }}, {"server2", { - {"command", "python"}, + {"command", "python3"}, {"args", json::array({"tests/fixtures/echo_server.py"})}, {"enabled", true} }} @@ -366,7 +366,7 @@ TEST_CASE("TI_CLIENT_015_IsConnectedAccurate", "[mcp][client]") { json config = { {"servers", { {"test_server", { - {"command", "python"}, + {"command", "python3"}, {"args", json::array({"tests/fixtures/echo_server.py"})}, {"enabled", true} }} diff --git a/tests/mcp/StdioTransportTests.cpp b/tests/mcp/StdioTransportTests.cpp index a86934e..e04523d 100644 --- a/tests/mcp/StdioTransportTests.cpp +++ b/tests/mcp/StdioTransportTests.cpp @@ -20,7 +20,7 @@ using json = nlohmann::json; MCPServerConfig makeEchoServerConfig() { MCPServerConfig config; config.name = "echo"; - config.command = "python"; + config.command = "python3"; config.args = {"tests/fixtures/echo_server.py"}; config.enabled = true; return config; @@ -29,7 +29,7 @@ MCPServerConfig makeEchoServerConfig() { MCPServerConfig makeMockMCPServerConfig() { MCPServerConfig config; config.name = "mock_mcp"; - config.command = "python"; + config.command = "python3"; config.args = {"tests/fixtures/mock_mcp_server.py"}; config.enabled = true; return config;