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>
444 lines
16 KiB
C++
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;
|
|
}
|