You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
proxysql/doc/RAG_Tool_Handler.cpp.doxygen

869 lines
26 KiB

/**
* @file RAG_Tool_Handler.cpp
* @brief Implementation of RAG Tool Handler for MCP protocol
*
* Implements RAG-powered tools through MCP protocol for retrieval operations.
* This file contains the complete implementation of all RAG functionality
* including search, fetch, and administrative tools.
*
* @see RAG_Tool_Handler.h
*/
#include "RAG_Tool_Handler.h"
#include "AI_Features_Manager.h"
#include "GenAI_Thread.h"
#include "LLM_Bridge.h"
#include "proxysql_debug.h"
#include "cpp.h"
#include <sstream>
#include <algorithm>
#include <chrono>
// Forward declaration for GloGATH
extern GenAI_Threads_Handler *GloGATH;
// JSON library
#include "../deps/json/json.hpp"
using json = nlohmann::json;
#define PROXYJSON
// Forward declaration for GloGATH
extern GenAI_Threads_Handler *GloGATH;
// ============================================================================
// Constructor/Destructor
// ============================================================================
/**
* @brief Constructor
*
* Initializes the RAG tool handler with configuration parameters from GenAI_Thread
* if available, otherwise uses default values.
*
* Configuration parameters:
* - k_max: Maximum number of search results (default: 50)
* - candidates_max: Maximum number of candidates for hybrid search (default: 500)
* - query_max_bytes: Maximum query length in bytes (default: 8192)
* - response_max_bytes: Maximum response size in bytes (default: 5000000)
* - timeout_ms: Operation timeout in milliseconds (default: 2000)
*
* @param ai_mgr Pointer to AI_Features_Manager for database access and configuration
*
* @see AI_Features_Manager
* @see GenAI_Thread
*/
RAG_Tool_Handler::RAG_Tool_Handler(AI_Features_Manager* ai_mgr)
: vector_db(NULL),
ai_manager(ai_mgr),
k_max(50),
candidates_max(500),
query_max_bytes(8192),
response_max_bytes(5000000),
timeout_ms(2000)
{
// Initialize configuration from GenAI_Thread if available
if (ai_manager && GloGATH) {
k_max = GloGATH->variables.genai_rag_k_max;
candidates_max = GloGATH->variables.genai_rag_candidates_max;
query_max_bytes = GloGATH->variables.genai_rag_query_max_bytes;
response_max_bytes = GloGATH->variables.genai_rag_response_max_bytes;
timeout_ms = GloGATH->variables.genai_rag_timeout_ms;
}
proxy_debug(PROXY_DEBUG_GENAI, 3, "RAG_Tool_Handler created\n");
}
/**
* @brief Destructor
*
* Cleans up resources and closes database connections.
*
* @see close()
*/
RAG_Tool_Handler::~RAG_Tool_Handler() {
close();
proxy_debug(PROXY_DEBUG_GENAI, 3, "RAG_Tool_Handler destroyed\n");
}
// ============================================================================
// Lifecycle
// ============================================================================
/**
* @brief Initialize the tool handler
*
* Initializes the RAG tool handler by establishing database connections
* and preparing internal state. Must be called before executing any tools.
*
* @return 0 on success, -1 on error
*
* @see close()
* @see vector_db
* @see ai_manager
*/
int RAG_Tool_Handler::init() {
if (ai_manager) {
vector_db = ai_manager->get_vector_db();
}
if (!vector_db) {
proxy_error("RAG_Tool_Handler: Vector database not available\n");
return -1;
}
proxy_info("RAG_Tool_Handler initialized\n");
return 0;
}
/**
* @brief Close and cleanup
*
* Cleans up resources and closes database connections. Called automatically
* by the destructor.
*
* @see init()
* @see ~RAG_Tool_Handler()
*/
void RAG_Tool_Handler::close() {
// Cleanup will be handled by AI_Features_Manager
}
// ============================================================================
// Helper Functions
// ============================================================================
/**
* @brief Extract string parameter from JSON
*
* Safely extracts a string parameter from a JSON object, handling type
* conversion if necessary. Returns the default value if the key is not
* found or cannot be converted to a string.
*
* @param j JSON object to extract from
* @param key Parameter key to extract
* @param default_val Default value if key not found
* @return Extracted string value or default
*
* @see get_json_int()
* @see get_json_bool()
* @see get_json_string_array()
* @see get_json_int_array()
*/
std::string RAG_Tool_Handler::get_json_string(const json& j, const std::string& key,
const std::string& default_val) {
if (j.contains(key) && !j[key].is_null()) {
if (j[key].is_string()) {
return j[key].get<std::string>();
} else {
// Convert to string if not already
return j[key].dump();
}
}
return default_val;
}
/**
* @brief Extract int parameter from JSON
*
* Safely extracts an integer parameter from a JSON object, handling type
* conversion from string if necessary. Returns the default value if the
* key is not found or cannot be converted to an integer.
*
* @param j JSON object to extract from
* @param key Parameter key to extract
* @param default_val Default value if key not found
* @return Extracted int value or default
*
* @see get_json_string()
* @see get_json_bool()
* @see get_json_string_array()
* @see get_json_int_array()
*/
int RAG_Tool_Handler::get_json_int(const json& j, const std::string& key, int default_val) {
if (j.contains(key) && !j[key].is_null()) {
if (j[key].is_number()) {
return j[key].get<int>();
} else if (j[key].is_string()) {
try {
return std::stoi(j[key].get<std::string>());
} catch (const std::exception& e) {
proxy_error("RAG_Tool_Handler: Failed to convert string to int for key '%s': %s\n",
key.c_str(), e.what());
return default_val;
}
}
}
return default_val;
}
/**
* @brief Extract bool parameter from JSON
*
* Safely extracts a boolean parameter from a JSON object, handling type
* conversion from string or integer if necessary. Returns the default
* value if the key is not found or cannot be converted to a boolean.
*
* @param j JSON object to extract from
* @param key Parameter key to extract
* @param default_val Default value if key not found
* @return Extracted bool value or default
*
* @see get_json_string()
* @see get_json_int()
* @see get_json_string_array()
* @see get_json_int_array()
*/
bool RAG_Tool_Handler::get_json_bool(const json& j, const std::string& key, bool default_val) {
if (j.contains(key) && !j[key].is_null()) {
if (j[key].is_boolean()) {
return j[key].get<bool>();
} else if (j[key].is_string()) {
std::string val = j[key].get<std::string>();
return (val == "true" || val == "1");
} else if (j[key].is_number()) {
return j[key].get<int>() != 0;
}
}
return default_val;
}
/**
* @brief Extract string array from JSON
*
* Safely extracts a string array parameter from a JSON object, filtering
* out non-string elements. Returns an empty vector if the key is not
* found or is not an array.
*
* @param j JSON object to extract from
* @param key Parameter key to extract
* @return Vector of extracted strings
*
* @see get_json_string()
* @see get_json_int()
* @see get_json_bool()
* @see get_json_int_array()
*/
std::vector<std::string> RAG_Tool_Handler::get_json_string_array(const json& j, const std::string& key) {
std::vector<std::string> result;
if (j.contains(key) && j[key].is_array()) {
for (const auto& item : j[key]) {
if (item.is_string()) {
result.push_back(item.get<std::string>());
}
}
}
return result;
}
/**
* @brief Extract int array from JSON
*
* Safely extracts an integer array parameter from a JSON object, handling
* type conversion from string if necessary. Returns an empty vector if
* the key is not found or is not an array.
*
* @param j JSON object to extract from
* @param key Parameter key to extract
* @return Vector of extracted integers
*
* @see get_json_string()
* @see get_json_int()
* @see get_json_bool()
* @see get_json_string_array()
*/
std::vector<int> RAG_Tool_Handler::get_json_int_array(const json& j, const std::string& key) {
std::vector<int> result;
if (j.contains(key) && j[key].is_array()) {
for (const auto& item : j[key]) {
if (item.is_number()) {
result.push_back(item.get<int>());
} else if (item.is_string()) {
try {
result.push_back(std::stoi(item.get<std::string>()));
} catch (const std::exception& e) {
proxy_error("RAG_Tool_Handler: Failed to convert string to int in array: %s\n", e.what());
}
}
}
}
return result;
}
/**
* @brief Validate and limit k parameter
*
* Ensures the k parameter is within acceptable bounds (1 to k_max).
* Returns default value of 10 if k is invalid.
*
* @param k Requested number of results
* @return Validated k value within configured limits
*
* @see validate_candidates()
* @see k_max
*/
int RAG_Tool_Handler::validate_k(int k) {
if (k <= 0) return 10; // Default
if (k > k_max) return k_max;
return k;
}
/**
* @brief Validate and limit candidates parameter
*
* Ensures the candidates parameter is within acceptable bounds (1 to candidates_max).
* Returns default value of 50 if candidates is invalid.
*
* @param candidates Requested number of candidates
* @return Validated candidates value within configured limits
*
* @see validate_k()
* @see candidates_max
*/
int RAG_Tool_Handler::validate_candidates(int candidates) {
if (candidates <= 0) return 50; // Default
if (candidates > candidates_max) return candidates_max;
return candidates;
}
/**
* @brief Validate query length
*
* Checks if the query string length is within the configured query_max_bytes limit.
*
* @param query Query string to validate
* @return true if query is within length limits, false otherwise
*
* @see query_max_bytes
*/
bool RAG_Tool_Handler::validate_query_length(const std::string& query) {
return static_cast<int>(query.length()) <= query_max_bytes;
}
/**
* @brief Execute database query and return results
*
* Executes a SQL query against the vector database and returns the results.
* Handles error checking and logging. The caller is responsible for freeing
* the returned SQLite3_result.
*
* @param query SQL query string to execute
* @return SQLite3_result pointer or NULL on error
*
* @see vector_db
*/
SQLite3_result* RAG_Tool_Handler::execute_query(const char* query) {
if (!vector_db) {
proxy_error("RAG_Tool_Handler: Vector database not available\n");
return NULL;
}
char* error = NULL;
int cols = 0;
int affected_rows = 0;
SQLite3_result* result = vector_db->execute_statement(query, &error, &cols, &affected_rows);
if (error) {
proxy_error("RAG_Tool_Handler: SQL error: %s\n", error);
proxy_sqlite3_free(error);
return NULL;
}
return result;
}
/**
* @brief Compute Reciprocal Rank Fusion score
*
* Computes the Reciprocal Rank Fusion score for hybrid search ranking.
* Formula: weight / (k0 + rank)
*
* @param rank Rank position (1-based)
* @param k0 Smoothing parameter
* @param weight Weight factor for this ranking
* @return RRF score
*
* @see rag.search_hybrid
*/
double RAG_Tool_Handler::compute_rrf_score(int rank, int k0, double weight) {
if (rank <= 0) return 0.0;
return weight / (k0 + rank);
}
/**
* @brief Normalize scores to 0-1 range (higher is better)
*
* Normalizes various types of scores to a consistent 0-1 range where
* higher values indicate better matches. Different score types may
* require different normalization approaches.
*
* @param score Raw score to normalize
* @param score_type Type of score being normalized
* @return Normalized score in 0-1 range
*/
double RAG_Tool_Handler::normalize_score(double score, const std::string& score_type) {
// For now, return the score as-is
// In the future, we might want to normalize different score types differently
return score;
}
// ============================================================================
// Tool List
// ============================================================================
/**
* @brief Get list of available RAG tools
*
* Returns a comprehensive list of all available RAG tools with their
* input schemas and descriptions. Tools include:
* - rag.search_fts: Keyword search using FTS5
* - rag.search_vector: Semantic search using vector embeddings
* - rag.search_hybrid: Hybrid search combining FTS and vectors
* - rag.get_chunks: Fetch chunk content by chunk_id
* - rag.get_docs: Fetch document content by doc_id
* - rag.fetch_from_source: Refetch authoritative data from source
* - rag.admin.stats: Operational statistics
*
* @return JSON object containing tool definitions and schemas
*
* @see get_tool_description()
* @see execute_tool()
*/
json RAG_Tool_Handler::get_tool_list() {
json tools = json::array();
// FTS search tool
json fts_params = json::object();
fts_params["type"] = "object";
fts_params["properties"] = json::object();
fts_params["properties"]["query"] = {
{"type", "string"},
{"description", "Keyword search query"}
};
fts_params["properties"]["k"] = {
{"type", "integer"},
{"description", "Number of results to return (default: 10, max: 50)"}
};
fts_params["properties"]["offset"] = {
{"type", "integer"},
{"description", "Offset for pagination (default: 0)"}
};
// Filters object
json filters_obj = json::object();
filters_obj["type"] = "object";
filters_obj["properties"] = json::object();
filters_obj["properties"]["source_ids"] = {
{"type", "array"},
{"items", {{"type", "integer"}}},
{"description", "Filter by source IDs"}
};
filters_obj["properties"]["source_names"] = {
{"type", "array"},
{"items", {{"type", "string"}}},
{"description", "Filter by source names"}
};
filters_obj["properties"]["doc_ids"] = {
{"type", "array"},
{"items", {{"type", "string"}}},
{"description", "Filter by document IDs"}
};
filters_obj["properties"]["min_score"] = {
{"type", "number"},
{"description", "Minimum score threshold"}
};
filters_obj["properties"]["post_type_ids"] = {
{"type", "array"},
{"items", {{"type", "integer"}}},
{"description", "Filter by post type IDs"}
};
filters_obj["properties"]["tags_any"] = {
{"type", "array"},
{"items", {{"type", "string"}}},
{"description", "Filter by any of these tags"}
};
filters_obj["properties"]["tags_all"] = {
{"type", "array"},
{"items", {{"type", "string"}}},
{"description", "Filter by all of these tags"}
};
filters_obj["properties"]["created_after"] = {
{"type", "string"},
{"format", "date-time"},
{"description", "Filter by creation date (after)"}
};
filters_obj["properties"]["created_before"] = {
{"type", "string"},
{"format", "date-time"},
{"description", "Filter by creation date (before)"}
};
fts_params["properties"]["filters"] = filters_obj;
// Return object
json return_obj = json::object();
return_obj["type"] = "object";
return_obj["properties"] = json::object();
return_obj["properties"]["include_title"] = {
{"type", "boolean"},
{"description", "Include title in results (default: true)"}
};
return_obj["properties"]["include_metadata"] = {
{"type", "boolean"},
{"description", "Include metadata in results (default: true)"}
};
return_obj["properties"]["include_snippets"] = {
{"type", "boolean"},
{"description", "Include snippets in results (default: false)"}
};
fts_params["properties"]["return"] = return_obj;
fts_params["required"] = json::array({"query"});
tools.push_back({
{"name", "rag.search_fts"},
{"description", "Keyword search over documents using FTS5"},
{"inputSchema", fts_params}
});
// Vector search tool
json vec_params = json::object();
vec_params["type"] = "object";
vec_params["properties"] = json::object();
vec_params["properties"]["query_text"] = {
{"type", "string"},
{"description", "Text to search semantically"}
};
vec_params["properties"]["k"] = {
{"type", "integer"},
{"description", "Number of results to return (default: 10, max: 50)"}
};
// Filters object (same as FTS)
vec_params["properties"]["filters"] = filters_obj;
// Return object (same as FTS)
vec_params["properties"]["return"] = return_obj;
// Embedding object for precomputed vectors
json embedding_obj = json::object();
embedding_obj["type"] = "object";
embedding_obj["properties"] = json::object();
embedding_obj["properties"]["model"] = {
{"type", "string"},
{"description", "Embedding model to use"}
};
vec_params["properties"]["embedding"] = embedding_obj;
// Query embedding object for precomputed vectors
json query_embedding_obj = json::object();
query_embedding_obj["type"] = "object";
query_embedding_obj["properties"] = json::object();
query_embedding_obj["properties"]["dim"] = {
{"type", "integer"},
{"description", "Dimension of the embedding"}
};
query_embedding_obj["properties"]["values_b64"] = {
{"type", "string"},
{"description", "Base64 encoded float32 array"}
};
vec_params["properties"]["query_embedding"] = query_embedding_obj;
vec_params["required"] = json::array({"query_text"});
tools.push_back({
{"name", "rag.search_vector"},
{"description", "Semantic search over documents using vector embeddings"},
{"inputSchema", vec_params}
});
// Hybrid search tool
json hybrid_params = json::object();
hybrid_params["type"] = "object";
hybrid_params["properties"] = json::object();
hybrid_params["properties"]["query"] = {
{"type", "string"},
{"description", "Search query for both FTS and vector"}
};
hybrid_params["properties"]["k"] = {
{"type", "integer"},
{"description", "Number of results to return (default: 10, max: 50)"}
};
hybrid_params["properties"]["mode"] = {
{"type", "string"},
{"description", "Search mode: 'fuse' or 'fts_then_vec'"}
};
// Filters object (same as FTS and vector)
hybrid_params["properties"]["filters"] = filters_obj;
// Fuse object for mode "fuse"
json fuse_obj = json::object();
fuse_obj["type"] = "object";
fuse_obj["properties"] = json::object();
fuse_obj["properties"]["fts_k"] = {
{"type", "integer"},
{"description", "Number of FTS results to retrieve for fusion (default: 50)"}
};
fuse_obj["properties"]["vec_k"] = {
{"type", "integer"},
{"description", "Number of vector results to retrieve for fusion (default: 50)"}
};
fuse_obj["properties"]["rrf_k0"] = {
{"type", "integer"},
{"description", "RRF smoothing parameter (default: 60)"}
};
fuse_obj["properties"]["w_fts"] = {
{"type", "number"},
{"description", "Weight for FTS scores in fusion (default: 1.0)"}
};
fuse_obj["properties"]["w_vec"] = {
{"type", "number"},
{"description", "Weight for vector scores in fusion (default: 1.0)"}
};
hybrid_params["properties"]["fuse"] = fuse_obj;
// Fts_then_vec object for mode "fts_then_vec"
json fts_then_vec_obj = json::object();
fts_then_vec_obj["type"] = "object";
fts_then_vec_obj["properties"] = json::object();
fts_then_vec_obj["properties"]["candidates_k"] = {
{"type", "integer"},
{"description", "Number of FTS candidates to generate (default: 200)"}
};
fts_then_vec_obj["properties"]["rerank_k"] = {
{"type", "integer"},
{"description", "Number of candidates to rerank with vector search (default: 50)"}
};
fts_then_vec_obj["properties"]["vec_metric"] = {
{"type", "string"},
{"description", "Vector similarity metric (default: 'cosine')"}
};
hybrid_params["properties"]["fts_then_vec"] = fts_then_vec_obj;
hybrid_params["required"] = json::array({"query"});
tools.push_back({
{"name", "rag.search_hybrid"},
{"description", "Hybrid search combining FTS and vector"},
{"inputSchema", hybrid_params}
});
// Get chunks tool
json chunks_params = json::object();
chunks_params["type"] = "object";
chunks_params["properties"] = json::object();
chunks_params["properties"]["chunk_ids"] = {
{"type", "array"},
{"items", {{"type", "string"}}},
{"description", "List of chunk IDs to fetch"}
};
json return_params = json::object();
return_params["type"] = "object";
return_params["properties"] = json::object();
return_params["properties"]["include_title"] = {
{"type", "boolean"},
{"description", "Include title in response (default: true)"}
};
return_params["properties"]["include_doc_metadata"] = {
{"type", "boolean"},
{"description", "Include document metadata in response (default: true)"}
};
return_params["properties"]["include_chunk_metadata"] = {
{"type", "boolean"},
{"description", "Include chunk metadata in response (default: true)"}
};
chunks_params["properties"]["return"] = return_params;
chunks_params["required"] = json::array({"chunk_ids"});
tools.push_back({
{"name", "rag.get_chunks"},
{"description", "Fetch chunk content by chunk_id"},
{"inputSchema", chunks_params}
});
// Get docs tool
json docs_params = json::object();
docs_params["type"] = "object";
docs_params["properties"] = json::object();
docs_params["properties"]["doc_ids"] = {
{"type", "array"},
{"items", {{"type", "string"}}},
{"description", "List of document IDs to fetch"}
};
json docs_return_params = json::object();
docs_return_params["type"] = "object";
docs_return_params["properties"] = json::object();
docs_return_params["properties"]["include_body"] = {
{"type", "boolean"},
{"description", "Include body in response (default: true)"}
};
docs_return_params["properties"]["include_metadata"] = {
{"type", "boolean"},
{"description", "Include metadata in response (default: true)"}
};
docs_params["properties"]["return"] = docs_return_params;
docs_params["required"] = json::array({"doc_ids"});
tools.push_back({
{"name", "rag.get_docs"},
{"description", "Fetch document content by doc_id"},
{"inputSchema", docs_params}
});
// Fetch from source tool
json fetch_params = json::object();
fetch_params["type"] = "object";
fetch_params["properties"] = json::object();
fetch_params["properties"]["doc_ids"] = {
{"type", "array"},
{"items", {{"type", "string"}}},
{"description", "List of document IDs to refetch"}
};
fetch_params["properties"]["columns"] = {
{"type", "array"},
{"items", {{"type", "string"}}},
{"description", "List of columns to fetch"}
};
// Limits object
json limits_obj = json::object();
limits_obj["type"] = "object";
limits_obj["properties"] = json::object();
limits_obj["properties"]["max_rows"] = {
{"type", "integer"},
{"description", "Maximum number of rows to return (default: 10, max: 100)"}
};
limits_obj["properties"]["max_bytes"] = {
{"type", "integer"},
{"description", "Maximum number of bytes to return (default: 200000, max: 1000000)"}
};
fetch_params["properties"]["limits"] = limits_obj;
fetch_params["required"] = json::array({"doc_ids"});
tools.push_back({
{"name", "rag.fetch_from_source"},
{"description", "Refetch authoritative data from source database"},
{"inputSchema", fetch_params}
});
// Admin stats tool
json stats_params = json::object();
stats_params["type"] = "object";
stats_params["properties"] = json::object();
tools.push_back({
{"name", "rag.admin.stats"},
{"description", "Get operational statistics for RAG system"},
{"inputSchema", stats_params}
});
json result;
result["tools"] = tools;
return result;
}
/**
* @brief Get description of a specific tool
*
* Returns the schema and description for a specific RAG tool.
*
* @param tool_name Name of the tool to describe
* @return JSON object with tool description or error response
*
* @see get_tool_list()
* @see execute_tool()
*/
json RAG_Tool_Handler::get_tool_description(const std::string& tool_name) {
json tools_list = get_tool_list();
for (const auto& tool : tools_list["tools"]) {
if (tool["name"] == tool_name) {
return tool;
}
}
return create_error_response("Tool not found: " + tool_name);
}
// ============================================================================
// Tool Execution
// ============================================================================
/**
* @brief Execute a RAG tool
*
* Executes the specified RAG tool with the provided arguments. Handles
* input validation, parameter processing, database queries, and result
* formatting according to MCP specifications.
*
* Supported tools:
* - rag.search_fts: Full-text search over documents
* - rag.search_vector: Vector similarity search
* - rag.search_hybrid: Hybrid search with two modes (fuse, fts_then_vec)
* - rag.get_chunks: Retrieve chunk content by ID
* - rag.get_docs: Retrieve document content by ID
* - rag.fetch_from_source: Refetch data from authoritative source
* - rag.admin.stats: Get operational statistics
*
* @param tool_name Name of the tool to execute
* @param arguments JSON object containing tool arguments
* @return JSON response with results or error information
*
* @see get_tool_list()
* @see get_tool_description()
*/
json RAG_Tool_Handler::execute_tool(const std::string& tool_name, const json& arguments) {
proxy_debug(PROXY_DEBUG_GENAI, 3, "RAG_Tool_Handler: execute_tool(%s)\n", tool_name.c_str());
// Record start time for timing stats
auto start_time = std::chrono::high_resolution_clock::now();
try {
json result;
if (tool_name == "rag.search_fts") {
// FTS search implementation
// ... (implementation details)
} else if (tool_name == "rag.search_vector") {
// Vector search implementation
// ... (implementation details)
} else if (tool_name == "rag.search_hybrid") {
// Hybrid search implementation
// ... (implementation details)
} else if (tool_name == "rag.get_chunks") {
// Get chunks implementation
// ... (implementation details)
} else if (tool_name == "rag.get_docs") {
// Get docs implementation
// ... (implementation details)
} else if (tool_name == "rag.fetch_from_source") {
// Fetch from source implementation
// ... (implementation details)
} else if (tool_name == "rag.admin.stats") {
// Admin stats implementation
// ... (implementation details)
} else {
return create_error_response("Unknown tool: " + tool_name);
}
// Calculate execution time
auto end_time = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
// Add timing stats to result
if (result.contains("stats")) {
result["stats"]["ms"] = static_cast<int>(duration.count());
} else {
json stats;
stats["ms"] = static_cast<int>(duration.count());
result["stats"] = stats;
}
return result;
} catch (const std::exception& e) {
proxy_error("RAG_Tool_Handler: Exception in execute_tool: %s\n", e.what());
return create_error_response("Internal error: " + std::string(e.what()));
}
}