fix: Enable HTTPS for Claude API and fix interactive mode IIO routing

- HttpClient: Use full URL with scheme (https://) for proper SSL support
- main.cpp: Create separate InteractiveClient IO to avoid self-delivery skip
- main.cpp: Process llm:response messages in main loop for terminal display
- ClaudeProvider: Add debug logging for request details

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
StillHammer 2025-11-28 07:27:56 +08:00
parent ac230b195d
commit 58d7ca4355
8 changed files with 324 additions and 58 deletions

105
PROMPT_SUCCESSEUR.md Normal file
View File

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

View File

@ -20,6 +20,9 @@
#include <fstream>
#include <map>
#include <cstring>
#include <iostream>
#include <atomic>
#include <functional>
namespace fs = std::filesystem;
@ -105,6 +108,8 @@ struct ModuleEntry {
// Message router between modules and services
class MessageRouter {
public:
using MessageCallback = std::function<void(const std::string& topic, grove::IDataNode* data)>;
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<grove::Message> 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<std::string, grove::IIO*> m_moduleIOs;
std::map<std::string, grove::IIO*> 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<std::string>();
@ -195,6 +224,39 @@ int runMCPServer() {
return 0;
}
// Flag for interactive mode
static std::atomic<bool> 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<grove::JsonDataNode>("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<grove::IIO> interactiveIO;
std::unique_ptr<std::thread> 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<std::thread>(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<grove::JsonDataNode*>(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<grove::JsonDataNode*>(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<std::pair<std::string, std::string>> 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;
}

View File

@ -68,18 +68,17 @@ public:
HttpResponse response;
try {
std::unique_ptr<httplib::Client> 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<httplib::Client>(m_host);
client->enable_server_certificate_verification(true);
} else {
client = std::make_unique<httplib::Client>(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<httplib::Client> client;
std::string url = (m_useSSL ? "https://" : "http://") + m_host;
httplib::Client client(url);
if (m_useSSL) {
client = std::make_unique<httplib::Client>(m_host);
client->enable_server_certificate_verification(true);
} else {
client = std::make_unique<httplib::Client>(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<httplib::Client> client;
std::string url = (m_useSSL ? "https://" : "http://") + m_host;
httplib::Client client(url);
if (m_useSSL) {
client = std::make_unique<httplib::Client>(m_host);
} else {
client = std::make_unique<httplib::Client>(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;

View File

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

View File

@ -3,6 +3,10 @@
#include <sstream>
#include <cstdlib>
#include <chrono>
#include <thread>
#include <cerrno>
#include <cstring>
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<int>();
std::lock_guard<std::mutex> 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<PendingRequest>();

View File

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

View File

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

View File

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