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>
188 lines
6.2 KiB
C++
188 lines
6.2 KiB
C++
#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
|