496 lines
16 KiB
Rust
496 lines
16 KiB
Rust
|
|
//! Core AI service implementation with RAG integration
|
||
|
|
|
||
|
|
use std::net::SocketAddr;
|
||
|
|
use std::sync::Arc;
|
||
|
|
|
||
|
|
use anyhow::Result;
|
||
|
|
use serde::{Deserialize, Serialize};
|
||
|
|
use tokio::sync::RwLock;
|
||
|
|
use tracing::{debug, info};
|
||
|
|
|
||
|
|
/// MCP tool invocation request
|
||
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
|
|
pub struct McpToolRequest {
|
||
|
|
/// Tool name (e.g., "execute_provisioning_plan")
|
||
|
|
pub tool_name: String,
|
||
|
|
/// Tool arguments as JSON
|
||
|
|
pub args: serde_json::Value,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// MCP tool invocation response
|
||
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
|
|
pub struct McpToolResponse {
|
||
|
|
/// Tool execution result
|
||
|
|
pub result: serde_json::Value,
|
||
|
|
/// Execution time in milliseconds
|
||
|
|
pub duration_ms: u64,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// RAG-powered question request with optional hybrid tool execution
|
||
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
|
|
pub struct AskRequest {
|
||
|
|
/// User question
|
||
|
|
pub question: String,
|
||
|
|
/// Optional context
|
||
|
|
pub context: Option<String>,
|
||
|
|
/// Enable automatic tool execution (hybrid mode)
|
||
|
|
/// When true, the RAG answer may trigger tool calls automatically
|
||
|
|
/// Default: false (explicit tool calls only)
|
||
|
|
#[serde(default)]
|
||
|
|
pub enable_tool_execution: Option<bool>,
|
||
|
|
/// Maximum number of tools to execute automatically
|
||
|
|
/// Default: 3
|
||
|
|
#[serde(default)]
|
||
|
|
pub max_tool_calls: Option<u32>,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// RAG-powered question response with optional tool execution results
|
||
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
|
|
pub struct AskResponse {
|
||
|
|
/// Answer from AI
|
||
|
|
pub answer: String,
|
||
|
|
/// Source documents used
|
||
|
|
pub sources: Vec<String>,
|
||
|
|
/// Confidence level (0-100)
|
||
|
|
pub confidence: u8,
|
||
|
|
/// Reasoning explanation
|
||
|
|
pub reasoning: String,
|
||
|
|
/// Tool executions performed (if hybrid mode enabled)
|
||
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||
|
|
pub tool_executions: Option<Vec<crate::mcp::ToolExecution>>,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Extension DAG node
|
||
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
|
|
pub struct DagNode {
|
||
|
|
/// Extension/component name
|
||
|
|
pub name: String,
|
||
|
|
/// Dependencies on other nodes
|
||
|
|
pub dependencies: Vec<String>,
|
||
|
|
/// Component version
|
||
|
|
pub version: String,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Extension DAG response
|
||
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
|
|
pub struct DagResponse {
|
||
|
|
/// DAG nodes (extensions)
|
||
|
|
pub nodes: Vec<DagNode>,
|
||
|
|
/// DAG edges (dependencies)
|
||
|
|
pub edges: Vec<(String, String)>,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Best practice entry
|
||
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
|
|
pub struct BestPractice {
|
||
|
|
/// Practice title
|
||
|
|
pub title: String,
|
||
|
|
/// Practice description
|
||
|
|
pub description: String,
|
||
|
|
/// Category (e.g., "deployment", "security")
|
||
|
|
pub category: String,
|
||
|
|
/// Relevance score (0-100)
|
||
|
|
pub relevance: u8,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Knowledge base document (from RAG ingestion)
|
||
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
|
|
pub struct KnowledgeDocument {
|
||
|
|
/// Document ID
|
||
|
|
pub id: String,
|
||
|
|
/// Document type
|
||
|
|
#[serde(rename = "type")]
|
||
|
|
pub doc_type: String,
|
||
|
|
/// Document title
|
||
|
|
pub title: Option<String>,
|
||
|
|
/// Document name (for extensions)
|
||
|
|
pub name: Option<String>,
|
||
|
|
/// Full content
|
||
|
|
pub content: String,
|
||
|
|
/// Document category
|
||
|
|
pub category: String,
|
||
|
|
/// Tags
|
||
|
|
pub tags: Vec<String>,
|
||
|
|
/// Relevance/importance
|
||
|
|
pub relevance: Option<u8>,
|
||
|
|
/// Dependencies (for extensions)
|
||
|
|
pub dependencies: Option<Vec<String>>,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Knowledge base with documents and relationships
|
||
|
|
#[derive(Debug, Clone)]
|
||
|
|
pub struct KnowledgeBase {
|
||
|
|
/// All documents indexed by ID
|
||
|
|
pub documents: std::collections::HashMap<String, KnowledgeDocument>,
|
||
|
|
/// Document relationships
|
||
|
|
pub relationships: Vec<Relationship>,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Document relationship
|
||
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
|
|
pub struct Relationship {
|
||
|
|
/// Source document ID
|
||
|
|
pub source_id: String,
|
||
|
|
/// Target document ID
|
||
|
|
pub target_id: String,
|
||
|
|
/// Relationship type
|
||
|
|
pub relationship_type: String,
|
||
|
|
/// Strength (0-1)
|
||
|
|
pub strength: f32,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Core AI service
|
||
|
|
pub struct AiService {
|
||
|
|
/// Server address
|
||
|
|
addr: SocketAddr,
|
||
|
|
/// Knowledge base
|
||
|
|
knowledge_base: Arc<RwLock<KnowledgeBase>>,
|
||
|
|
/// MCP tool registry
|
||
|
|
tool_registry: crate::mcp::ToolRegistry,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl AiService {
|
||
|
|
/// Create new AI service
|
||
|
|
pub fn new(addr: SocketAddr) -> Self {
|
||
|
|
Self {
|
||
|
|
addr,
|
||
|
|
knowledge_base: Arc::new(RwLock::new(KnowledgeBase {
|
||
|
|
documents: std::collections::HashMap::new(),
|
||
|
|
relationships: Vec::new(),
|
||
|
|
})),
|
||
|
|
tool_registry: crate::mcp::ToolRegistry::new(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Load knowledge base from JSON files
|
||
|
|
pub async fn load_knowledge_base(&self, knowledge_base_dir: &str) -> Result<()> {
|
||
|
|
info!("Loading knowledge base from: {}", knowledge_base_dir);
|
||
|
|
|
||
|
|
// Load best practice documents
|
||
|
|
let bp_path = format!("{}/best-practices-docs.json", knowledge_base_dir);
|
||
|
|
let bp_content = std::fs::read_to_string(&bp_path)
|
||
|
|
.map_err(|e| anyhow::anyhow!("Failed to read best practices: {}", e))?;
|
||
|
|
let bp_docs: Vec<KnowledgeDocument> = serde_json::from_str(&bp_content)?;
|
||
|
|
|
||
|
|
// Load extension documents
|
||
|
|
let ext_path = format!("{}/extension-docs.json", knowledge_base_dir);
|
||
|
|
let ext_content = std::fs::read_to_string(&ext_path)
|
||
|
|
.map_err(|e| anyhow::anyhow!("Failed to read extensions: {}", e))?;
|
||
|
|
let ext_docs: Vec<KnowledgeDocument> = serde_json::from_str(&ext_content)?;
|
||
|
|
|
||
|
|
// Load relationships
|
||
|
|
let rel_path = format!("{}/relationships.json", knowledge_base_dir);
|
||
|
|
let rel_content = std::fs::read_to_string(&rel_path)
|
||
|
|
.map_err(|e| anyhow::anyhow!("Failed to read relationships: {}", e))?;
|
||
|
|
let relationships: Vec<Relationship> = serde_json::from_str(&rel_content)?;
|
||
|
|
|
||
|
|
// Build document index
|
||
|
|
let mut documents = std::collections::HashMap::new();
|
||
|
|
for doc in bp_docs.into_iter().chain(ext_docs.into_iter()) {
|
||
|
|
documents.insert(doc.id.clone(), doc);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Update knowledge base
|
||
|
|
let mut kb = self.knowledge_base.write().await;
|
||
|
|
kb.documents = documents;
|
||
|
|
kb.relationships = relationships;
|
||
|
|
|
||
|
|
info!(
|
||
|
|
"Knowledge base loaded: {} documents, {} relationships",
|
||
|
|
kb.documents.len(),
|
||
|
|
kb.relationships.len()
|
||
|
|
);
|
||
|
|
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Get service address
|
||
|
|
pub fn addr(&self) -> SocketAddr {
|
||
|
|
self.addr
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Search knowledge base by keyword and category
|
||
|
|
async fn search_knowledge(
|
||
|
|
&self,
|
||
|
|
query: &str,
|
||
|
|
category: Option<&str>,
|
||
|
|
) -> Vec<KnowledgeDocument> {
|
||
|
|
let kb = self.knowledge_base.read().await;
|
||
|
|
let query_lower = query.to_lowercase();
|
||
|
|
|
||
|
|
kb.documents
|
||
|
|
.values()
|
||
|
|
.filter(|doc| {
|
||
|
|
let matches_query = doc.content.to_lowercase().contains(&query_lower)
|
||
|
|
|| doc
|
||
|
|
.tags
|
||
|
|
.iter()
|
||
|
|
.any(|t| t.to_lowercase().contains(&query_lower));
|
||
|
|
|
||
|
|
let matches_category = category.map(|c| doc.category == c).unwrap_or(true);
|
||
|
|
|
||
|
|
matches_query && matches_category
|
||
|
|
})
|
||
|
|
.cloned()
|
||
|
|
.collect()
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Call an MCP tool via registry
|
||
|
|
pub async fn call_mcp_tool(&self, req: McpToolRequest) -> Result<McpToolResponse> {
|
||
|
|
let start = std::time::Instant::now();
|
||
|
|
|
||
|
|
debug!("Calling MCP tool: {}", req.tool_name);
|
||
|
|
|
||
|
|
let result = self.execute_mcp_tool(&req.tool_name, &req.args).await?;
|
||
|
|
let duration_ms = start.elapsed().as_millis() as u64;
|
||
|
|
|
||
|
|
Ok(McpToolResponse {
|
||
|
|
result,
|
||
|
|
duration_ms,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Execute MCP tool with arguments via registry
|
||
|
|
async fn execute_mcp_tool(
|
||
|
|
&self,
|
||
|
|
tool_name: &str,
|
||
|
|
args: &serde_json::Value,
|
||
|
|
) -> Result<serde_json::Value> {
|
||
|
|
// Check if tool exists in registry
|
||
|
|
if !self.tool_registry.has_tool(tool_name) {
|
||
|
|
return Err(anyhow::anyhow!("Unknown tool: {}", tool_name));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Execute tool through registry
|
||
|
|
match self.tool_registry.execute(tool_name, args).await {
|
||
|
|
Ok(result) => Ok(result),
|
||
|
|
Err(e) => Err(anyhow::anyhow!("Tool execution failed: {}", e)),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Execute suggested tools and collect results
|
||
|
|
async fn execute_tools(
|
||
|
|
&self,
|
||
|
|
suggestions: &[crate::tool_integration::ToolSuggestion],
|
||
|
|
max_tools: usize,
|
||
|
|
) -> Option<(
|
||
|
|
Vec<crate::mcp::ToolExecution>,
|
||
|
|
Vec<(String, serde_json::Value)>,
|
||
|
|
)> {
|
||
|
|
if suggestions.is_empty() {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
|
||
|
|
debug!(
|
||
|
|
"Executing {} suggested tools in hybrid mode",
|
||
|
|
suggestions.len().min(max_tools)
|
||
|
|
);
|
||
|
|
|
||
|
|
let mut executions = Vec::new();
|
||
|
|
let mut results = Vec::new();
|
||
|
|
|
||
|
|
for suggestion in suggestions.iter().take(max_tools) {
|
||
|
|
match self
|
||
|
|
.tool_registry
|
||
|
|
.execute(&suggestion.tool_name, &suggestion.args)
|
||
|
|
.await
|
||
|
|
{
|
||
|
|
Ok(result) => {
|
||
|
|
debug!("Tool {} executed successfully", suggestion.tool_name);
|
||
|
|
results.push((suggestion.tool_name.clone(), result.clone()));
|
||
|
|
executions.push(crate::mcp::ToolExecution {
|
||
|
|
tool_name: suggestion.tool_name.clone(),
|
||
|
|
result: result.clone(),
|
||
|
|
duration_ms: 0,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
Err(e) => {
|
||
|
|
debug!("Tool {} execution failed: {}", suggestion.tool_name, e);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if executions.is_empty() {
|
||
|
|
None
|
||
|
|
} else {
|
||
|
|
Some((executions, results))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Ask AI a question using RAG with optional hybrid tool execution
|
||
|
|
pub async fn ask(&self, req: AskRequest) -> Result<AskResponse> {
|
||
|
|
debug!("Processing RAG question: {}", req.question);
|
||
|
|
|
||
|
|
// Search knowledge base for relevant documents
|
||
|
|
let search_results = self.search_knowledge(&req.question, None).await;
|
||
|
|
|
||
|
|
if search_results.is_empty() {
|
||
|
|
return Ok(AskResponse {
|
||
|
|
answer: "I couldn't find any relevant information in the knowledge base for this \
|
||
|
|
question."
|
||
|
|
.to_string(),
|
||
|
|
sources: vec![],
|
||
|
|
confidence: 20,
|
||
|
|
reasoning: "No matching documents found".to_string(),
|
||
|
|
tool_executions: None,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Sort by relevance (best practices have explicit relevance scores)
|
||
|
|
let mut results = search_results;
|
||
|
|
results.sort_by(|a, b| {
|
||
|
|
let a_rel = a.relevance.unwrap_or(50);
|
||
|
|
let b_rel = b.relevance.unwrap_or(50);
|
||
|
|
b_rel.cmp(&a_rel)
|
||
|
|
});
|
||
|
|
|
||
|
|
// Get top 3 most relevant documents
|
||
|
|
let top_results: Vec<_> = results.iter().take(3).collect();
|
||
|
|
|
||
|
|
// Build answer from top results
|
||
|
|
let mut answer_parts =
|
||
|
|
vec!["Based on the knowledge base, here's what I found:".to_string()];
|
||
|
|
|
||
|
|
for doc in &top_results {
|
||
|
|
if let Some(title) = &doc.title {
|
||
|
|
answer_parts.push(format!(
|
||
|
|
"\n- **{}**: {}",
|
||
|
|
title,
|
||
|
|
&doc.content[..std::cmp::min(150, doc.content.len())]
|
||
|
|
));
|
||
|
|
} else if let Some(name) = &doc.name {
|
||
|
|
answer_parts.push(format!(
|
||
|
|
"\n- **{}**: {}",
|
||
|
|
name,
|
||
|
|
&doc.content[..std::cmp::min(150, doc.content.len())]
|
||
|
|
));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
let mut answer = answer_parts.join("\n");
|
||
|
|
let sources: Vec<String> = top_results
|
||
|
|
.iter()
|
||
|
|
.filter_map(|d| d.title.clone().or_else(|| d.name.clone()))
|
||
|
|
.collect();
|
||
|
|
|
||
|
|
let confidence = (top_results.iter().filter_map(|d| d.relevance).sum::<u8>() as f32
|
||
|
|
/ top_results.len() as f32) as u8;
|
||
|
|
|
||
|
|
// Handle hybrid execution mode (auto-trigger tools if enabled)
|
||
|
|
let mut tool_executions = None;
|
||
|
|
if req.enable_tool_execution.unwrap_or(false) {
|
||
|
|
let max_tools = req.max_tool_calls.unwrap_or(3) as usize;
|
||
|
|
let tool_suggestions =
|
||
|
|
crate::tool_integration::analyze_for_tools(&answer, &req.question);
|
||
|
|
|
||
|
|
if let Some((executions, results)) =
|
||
|
|
self.execute_tools(&tool_suggestions, max_tools).await
|
||
|
|
{
|
||
|
|
if !results.is_empty() {
|
||
|
|
answer = crate::tool_integration::enrich_answer_with_results(answer, &results);
|
||
|
|
tool_executions = Some(executions);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Ok(AskResponse {
|
||
|
|
answer,
|
||
|
|
sources,
|
||
|
|
confidence,
|
||
|
|
reasoning: format!(
|
||
|
|
"Retrieved {} relevant documents using keyword search across {} total documents",
|
||
|
|
top_results.len(),
|
||
|
|
results.len()
|
||
|
|
),
|
||
|
|
tool_executions,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Get extension dependency DAG
|
||
|
|
pub async fn get_extension_dag(&self) -> Result<DagResponse> {
|
||
|
|
debug!("Building extension DAG");
|
||
|
|
|
||
|
|
let kb = self.knowledge_base.read().await;
|
||
|
|
|
||
|
|
// Build nodes from extension documents
|
||
|
|
let nodes: Vec<DagNode> = kb
|
||
|
|
.documents
|
||
|
|
.values()
|
||
|
|
.filter(|doc| doc.doc_type == "extension_metadata")
|
||
|
|
.map(|doc| DagNode {
|
||
|
|
name: doc.name.clone().unwrap_or_else(|| doc.id.clone()),
|
||
|
|
dependencies: doc.dependencies.clone().unwrap_or_default(),
|
||
|
|
version: "1.0.0".to_string(),
|
||
|
|
})
|
||
|
|
.collect();
|
||
|
|
|
||
|
|
// Build edges from dependency relationships
|
||
|
|
let edges: Vec<(String, String)> = kb
|
||
|
|
.relationships
|
||
|
|
.iter()
|
||
|
|
.filter(|rel| rel.relationship_type == "depends_on")
|
||
|
|
.map(|rel| {
|
||
|
|
let source = rel.source_id.strip_prefix("ext_").unwrap_or(&rel.source_id);
|
||
|
|
let target = rel.target_id.strip_prefix("ext_").unwrap_or(&rel.target_id);
|
||
|
|
(source.to_string(), target.to_string())
|
||
|
|
})
|
||
|
|
.collect();
|
||
|
|
|
||
|
|
Ok(DagResponse { nodes, edges })
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Get best practices for a category
|
||
|
|
pub async fn get_best_practices(&self, category: &str) -> Result<Vec<BestPractice>> {
|
||
|
|
debug!("Retrieving best practices for category: {}", category);
|
||
|
|
|
||
|
|
let kb = self.knowledge_base.read().await;
|
||
|
|
|
||
|
|
// Filter documents by category and type
|
||
|
|
let mut practices: Vec<BestPractice> = kb
|
||
|
|
.documents
|
||
|
|
.values()
|
||
|
|
.filter(|doc| doc.category == category && doc.doc_type == "best_practice")
|
||
|
|
.map(|doc| BestPractice {
|
||
|
|
title: doc.title.clone().unwrap_or_else(|| doc.id.clone()),
|
||
|
|
description: doc.content.clone(),
|
||
|
|
category: doc.category.clone(),
|
||
|
|
relevance: doc.relevance.unwrap_or(70),
|
||
|
|
})
|
||
|
|
.collect();
|
||
|
|
|
||
|
|
// Sort by relevance descending
|
||
|
|
practices.sort_by(|a, b| b.relevance.cmp(&a.relevance));
|
||
|
|
|
||
|
|
Ok(practices)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Health check endpoint
|
||
|
|
pub async fn health_check(&self) -> Result<()> {
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Get all available tools
|
||
|
|
pub fn list_all_tools(&self) -> Vec<crate::mcp::ToolDefinition> {
|
||
|
|
self.tool_registry.list_tools()
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Get tools by category
|
||
|
|
pub fn tools_by_category(
|
||
|
|
&self,
|
||
|
|
category: crate::mcp::ToolCategory,
|
||
|
|
) -> Vec<crate::mcp::ToolDefinition> {
|
||
|
|
self.tool_registry.tools_by_category(category)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Check if a tool exists
|
||
|
|
pub fn has_tool(&self, name: &str) -> bool {
|
||
|
|
self.tool_registry.has_tool(name)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Default for AiService {
|
||
|
|
fn default() -> Self {
|
||
|
|
Self::new("127.0.0.1:8083".parse().unwrap())
|
||
|
|
}
|
||
|
|
}
|