aissia/src/main.cpp
StillHammer 059709cd0d 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>
2025-11-26 16:50:30 +08:00

444 lines
16 KiB
C++

#include <grove/ModuleLoader.h>
#include <grove/JsonDataNode.h>
#include <grove/IOFactory.h>
#include "services/LLMService.hpp"
#include "services/StorageService.hpp"
#include "services/PlatformService.hpp"
#include "services/VoiceService.hpp"
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <chrono>
#include <thread>
#include <csignal>
#include <filesystem>
#include <fstream>
#include <map>
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
static volatile bool g_running = true;
void signalHandler(int signal) {
spdlog::info("Signal {} recu, arret en cours...", signal);
g_running = false;
}
// Simple file watcher for hot-reload
class FileWatcher {
public:
void watch(const std::string& path) {
if (fs::exists(path)) {
m_lastModified[path] = fs::last_write_time(path);
}
}
bool hasChanged(const std::string& path) {
if (!fs::exists(path)) return false;
auto currentTime = fs::last_write_time(path);
auto it = m_lastModified.find(path);
if (it == m_lastModified.end()) {
m_lastModified[path] = currentTime;
return false;
}
if (currentTime != it->second) {
it->second = currentTime;
return true;
}
return false;
}
private:
std::unordered_map<std::string, fs::file_time_type> m_lastModified;
};
// Load JSON config file
std::unique_ptr<grove::JsonDataNode> loadConfig(const std::string& path) {
if (fs::exists(path)) {
std::ifstream file(path);
nlohmann::json j;
file >> j;
auto config = std::make_unique<grove::JsonDataNode>("config", j);
spdlog::info("Config chargee: {}", path);
return config;
} else {
spdlog::warn("Config non trouvee: {}, utilisation des defauts", path);
return std::make_unique<grove::JsonDataNode>("config");
}
}
// Module entry in our simple manager
struct ModuleEntry {
std::string name;
std::string configFile;
std::string path;
std::unique_ptr<grove::ModuleLoader> loader;
std::unique_ptr<grove::IIO> io;
grove::IModule* module = nullptr;
};
// Message router between modules and services
class MessageRouter {
public:
void addModuleIO(const std::string& name, grove::IIO* io) {
m_moduleIOs[name] = io;
}
void addServiceIO(const std::string& name, grove::IIO* io) {
m_serviceIOs[name] = io;
}
void routeMessages() {
// Collect all messages from modules and services
std::vector<grove::Message> messages;
for (auto& [name, io] : m_moduleIOs) {
if (!io) continue;
while (io->hasMessages() > 0) {
messages.push_back(io->pullMessage());
}
}
for (auto& [name, io] : m_serviceIOs) {
if (!io) continue;
while (io->hasMessages() > 0) {
messages.push_back(io->pullMessage());
}
}
// Route messages to appropriate destinations
for (auto& msg : messages) {
// Determine destination based on topic prefix
std::string prefix = msg.topic.substr(0, msg.topic.find(':'));
// Route to services
if (prefix == "llm" || prefix == "storage" || prefix == "platform" || prefix == "voice") {
for (auto& [name, io] : m_serviceIOs) {
if (io && msg.data) {
io->publish(msg.topic, cloneDataNode(msg.data.get()));
}
}
}
// Route to modules (broadcast)
for (auto& [name, io] : m_moduleIOs) {
if (io && msg.data) {
io->publish(msg.topic, cloneDataNode(msg.data.get()));
}
}
}
}
private:
std::map<std::string, grove::IIO*> m_moduleIOs;
std::map<std::string, grove::IIO*> m_serviceIOs;
};
int main(int argc, char* argv[]) {
// Setup logging
auto console = spdlog::stdout_color_mt("Aissia");
spdlog::set_default_logger(console);
spdlog::set_level(spdlog::level::debug);
spdlog::set_pattern("[%H:%M:%S.%e] [%n] [%^%l%$] %v");
spdlog::info("========================================");
spdlog::info(" AISSIA - Assistant Personnel IA");
spdlog::info(" Powered by GroveEngine");
spdlog::info(" Architecture: Services + Hot-Reload Modules");
spdlog::info("========================================");
// Signal handling
std::signal(SIGINT, signalHandler);
std::signal(SIGTERM, signalHandler);
// Paths
const std::string modulesDir = "./modules/";
const std::string configDir = "./config/";
// =========================================================================
// Infrastructure Services (non hot-reloadable)
// =========================================================================
spdlog::info("Initialisation des services infrastructure...");
// Create IIO for services
auto llmIO = grove::IOFactory::create("intra", "LLMService");
auto storageIO = grove::IOFactory::create("intra", "StorageService");
auto platformIO = grove::IOFactory::create("intra", "PlatformService");
auto voiceIO = grove::IOFactory::create("intra", "VoiceService");
// LLM Service
aissia::LLMService llmService;
llmService.initialize(llmIO.get());
llmService.loadConfig(configDir + "ai.json");
// Register default tools
llmService.registerTool(
"get_current_time",
"Obtient l'heure actuelle",
{{"type", "object"}, {"properties", nlohmann::json::object()}},
[](const nlohmann::json& input) -> nlohmann::json {
std::time_t now = std::time(nullptr);
std::tm* tm = std::localtime(&now);
char buffer[64];
std::strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", tm);
return {{"time", buffer}};
}
);
// Storage Service
aissia::StorageService storageService;
storageService.initialize(storageIO.get());
{
auto storageConfig = loadConfig(configDir + "storage.json");
std::string dbPath = storageConfig->getString("database_path", "./data/aissia.db");
std::string journalMode = storageConfig->getString("journal_mode", "WAL");
int busyTimeout = storageConfig->getInt("busy_timeout_ms", 5000);
storageService.openDatabase(dbPath, journalMode, busyTimeout);
}
// Platform Service
aissia::PlatformService platformService;
platformService.initialize(platformIO.get());
{
auto monitorConfig = loadConfig(configDir + "monitoring.json");
int pollInterval = monitorConfig->getInt("poll_interval_ms", 1000);
int idleThreshold = monitorConfig->getInt("idle_threshold_seconds", 300);
platformService.configure(pollInterval, idleThreshold);
}
// Voice Service
aissia::VoiceService voiceService;
voiceService.initialize(voiceIO.get());
{
auto voiceConfig = loadConfig(configDir + "voice.json");
auto* ttsNode = voiceConfig->getChildReadOnly("tts");
if (ttsNode) {
bool enabled = ttsNode->getBool("enabled", true);
int rate = ttsNode->getInt("rate", 0);
int volume = ttsNode->getInt("volume", 80);
voiceService.configureTTS(enabled, rate, volume);
}
auto* sttNode = voiceConfig->getChildReadOnly("stt");
if (sttNode) {
bool enabled = sttNode->getBool("enabled", true);
std::string language = sttNode->getString("language", "fr");
std::string apiKeyEnv = sttNode->getString("api_key_env", "OPENAI_API_KEY");
const char* apiKey = std::getenv(apiKeyEnv.c_str());
voiceService.configureSTT(enabled, language, apiKey ? apiKey : "");
}
}
spdlog::info("Services initialises: LLM={}, Storage={}, Platform={}, Voice={}",
llmService.isHealthy() ? "OK" : "FAIL",
storageService.isHealthy() ? "OK" : "FAIL",
platformService.isHealthy() ? "OK" : "FAIL",
voiceService.isHealthy() ? "OK" : "FAIL");
// =========================================================================
// Hot-Reloadable Modules
// =========================================================================
std::map<std::string, ModuleEntry> modules;
FileWatcher watcher;
MessageRouter router;
// Register service IOs with router
router.addServiceIO("LLMService", llmIO.get());
router.addServiceIO("StorageService", storageIO.get());
router.addServiceIO("PlatformService", platformIO.get());
router.addServiceIO("VoiceService", voiceIO.get());
// Liste des modules a charger (sans infrastructure)
std::vector<std::pair<std::string, std::string>> moduleList = {
{"SchedulerModule", "scheduler.json"},
{"NotificationModule", "notification.json"},
{"MonitoringModule", "monitoring.json"},
{"AIModule", "ai.json"},
{"VoiceModule", "voice.json"},
{"StorageModule", "storage.json"},
};
// Charger les modules
for (const auto& [moduleName, configFile] : moduleList) {
std::string modulePath = modulesDir + "lib" + moduleName + ".so";
if (!fs::exists(modulePath)) {
spdlog::warn("{} non trouve: {}", moduleName, modulePath);
continue;
}
ModuleEntry entry;
entry.name = moduleName;
entry.configFile = configFile;
entry.path = modulePath;
entry.loader = std::make_unique<grove::ModuleLoader>();
entry.io = grove::IOFactory::create("intra", moduleName);
auto modulePtr = entry.loader->load(modulePath, moduleName);
if (!modulePtr) {
spdlog::error("Echec du chargement: {}", moduleName);
continue;
}
// Configure
auto config = loadConfig(configDir + configFile);
modulePtr->setConfiguration(*config, entry.io.get(), nullptr);
entry.module = modulePtr.release();
watcher.watch(modulePath);
// Register with router
router.addModuleIO(moduleName, entry.io.get());
spdlog::info("{} charge et configure", moduleName);
modules[moduleName] = std::move(entry);
}
if (modules.empty()) {
spdlog::error("Aucun module charge! Build les modules: cmake --build build --target modules");
return 1;
}
// =========================================================================
// Main Loop
// =========================================================================
spdlog::info("Demarrage de la boucle principale (Ctrl+C pour quitter)");
using Clock = std::chrono::high_resolution_clock;
auto startTime = Clock::now();
auto lastFrame = Clock::now();
const auto targetFrameTime = std::chrono::milliseconds(100); // 10 Hz
grove::JsonDataNode frameInput("frame");
uint64_t frameCount = 0;
while (g_running) {
auto frameStart = Clock::now();
// Calculate times
auto deltaTime = std::chrono::duration<float>(frameStart - lastFrame).count();
auto gameTime = std::chrono::duration<float>(frameStart - startTime).count();
lastFrame = frameStart;
// Prepare frame input
frameInput.setDouble("deltaTime", deltaTime);
frameInput.setInt("frameCount", frameCount);
frameInput.setDouble("gameTime", gameTime);
// =====================================================================
// Hot-Reload Check (every 10 frames = ~1 second)
// =====================================================================
if (frameCount % 10 == 0) {
for (auto& [name, entry] : modules) {
if (watcher.hasChanged(entry.path)) {
spdlog::info("Modification detectee: {}, hot-reload...", name);
// Get state before reload
std::unique_ptr<grove::IDataNode> state;
if (entry.module) {
state = entry.module->getState();
}
// Reload
auto reloaded = entry.loader->load(entry.path, name, true);
if (reloaded) {
auto config = loadConfig(configDir + entry.configFile);
reloaded->setConfiguration(*config, entry.io.get(), nullptr);
if (state) {
reloaded->setState(*state);
}
entry.module = reloaded.release();
spdlog::info("{} recharge avec succes!", name);
}
}
}
}
// =====================================================================
// Process Services (non hot-reloadable infrastructure)
// =====================================================================
llmService.process();
storageService.process();
platformService.process();
voiceService.process();
// =====================================================================
// Process Modules (hot-reloadable)
// =====================================================================
for (auto& [name, entry] : modules) {
if (entry.module) {
entry.module->process(frameInput);
}
}
// =====================================================================
// Route messages between modules and services
// =====================================================================
router.routeMessages();
// =====================================================================
// Frame timing
// =====================================================================
frameCount++;
auto frameEnd = Clock::now();
auto frameDuration = frameEnd - frameStart;
if (frameDuration < targetFrameTime) {
std::this_thread::sleep_for(targetFrameTime - frameDuration);
}
// Status log every 30 seconds
if (frameCount % 300 == 0) {
int minutes = static_cast<int>(gameTime / 60.0f);
int seconds = static_cast<int>(gameTime) % 60;
spdlog::debug("Session: {}m{}s, {} modules actifs, 4 services",
minutes, seconds, modules.size());
}
}
// =========================================================================
// Shutdown
// =========================================================================
spdlog::info("Arret en cours...");
// Shutdown modules first
for (auto& [name, entry] : modules) {
if (entry.module) {
entry.module->shutdown();
spdlog::info("{} arrete", name);
}
}
// Shutdown services
voiceService.shutdown();
platformService.shutdown();
storageService.shutdown();
llmService.shutdown();
spdlog::info("A bientot!");
return 0;
}