//! Sources Module //! //! Manages all source types for bots including: //! - Repositories (GitHub, GitLab, Bitbucket) //! - Apps (HTMX apps created with CREATE SITE) //! - Prompts (System prompts and templates) //! - Templates (Bot packages .gbai) //! - MCP Servers (Model Context Protocol servers from mcp.csv) //! - LLM Tools (Available tools for LLM invocation) //! - Models (Configured AI models) //! //! MCP servers are configured via `mcp.csv` in the bot's `.gbai` folder, //! making their tools available to Tasks just like BASIC keywords. pub mod mcp; use crate::basic::keywords::mcp_directory::{generate_example_configs, McpCsvLoader, McpCsvRow}; use crate::shared::state::AppState; use axum::{ extract::{Json, Path, Query, State}, http::StatusCode, response::{Html, IntoResponse}, routing::{delete, get, post, put}, Router, }; use log::{error, info}; use serde::{Deserialize, Serialize}; use std::sync::Arc; // ============================================================================ // Request/Response Types // ============================================================================ #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SearchQuery { pub q: Option, pub category: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BotQuery { pub bot_id: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct McpServerResponse { pub id: String, pub name: String, pub description: String, pub server_type: String, pub status: String, pub enabled: bool, pub tools_count: usize, pub source: String, pub tags: Vec, pub requires_approval: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct McpToolResponse { pub name: String, pub description: String, pub server_name: String, pub risk_level: String, pub requires_approval: bool, pub source: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AddMcpServerRequest { pub name: String, pub description: Option, pub server_type: String, pub connection: McpConnectionRequest, pub auth: Option, pub enabled: Option, pub tags: Option>, pub requires_approval: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type")] pub enum McpConnectionRequest { #[serde(rename = "stdio")] Stdio { command: String, #[serde(default)] args: Vec, }, #[serde(rename = "http")] Http { url: String, #[serde(default = "default_timeout")] timeout: u32, }, #[serde(rename = "websocket")] WebSocket { url: String }, } fn default_timeout() -> u32 { 30 } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type")] pub enum McpAuthRequest { #[serde(rename = "none")] None, #[serde(rename = "api_key")] ApiKey { header: String, key_env: String }, #[serde(rename = "bearer")] Bearer { token_env: String }, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ApiResponse { pub success: bool, pub data: Option, pub error: Option, } impl ApiResponse { pub fn success(data: T) -> Self { Self { success: true, data: Some(data), error: None, } } pub fn error(message: &str) -> Self { Self { success: false, data: None, error: Some(message.to_string()), } } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RepositoryInfo { pub id: String, pub name: String, pub owner: String, pub description: String, pub url: String, pub language: Option, pub stars: u32, pub forks: u32, pub status: String, pub last_sync: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AppInfo { pub id: String, pub name: String, pub app_type: String, pub description: String, pub url: String, pub created_at: String, pub status: String, } // ============================================================================ // Route Configuration // ============================================================================ pub fn configure_sources_routes() -> Router> { Router::new() // Tab endpoints - HTMX content .route("/api/sources/prompts", get(handle_prompts)) .route("/api/sources/templates", get(handle_templates)) .route("/api/sources/news", get(handle_news)) .route("/api/sources/mcp-servers", get(handle_mcp_servers)) .route("/api/sources/llm-tools", get(handle_llm_tools)) .route("/api/sources/models", get(handle_models)) // Search .route("/api/sources/search", get(handle_search)) // Repositories API .route("/api/sources/repositories", get(handle_list_repositories)) .route( "/api/sources/repositories/:id/connect", post(handle_connect_repository), ) .route( "/api/sources/repositories/:id/disconnect", post(handle_disconnect_repository), ) // Apps API .route("/api/sources/apps", get(handle_list_apps)) // MCP Server Management API .route("/api/sources/mcp", get(handle_list_mcp_servers_json)) .route("/api/sources/mcp", post(handle_add_mcp_server)) .route("/api/sources/mcp/:name", get(handle_get_mcp_server)) .route("/api/sources/mcp/:name", put(handle_update_mcp_server)) .route("/api/sources/mcp/:name", delete(handle_delete_mcp_server)) .route( "/api/sources/mcp/:name/enable", post(handle_enable_mcp_server), ) .route( "/api/sources/mcp/:name/disable", post(handle_disable_mcp_server), ) .route( "/api/sources/mcp/:name/tools", get(handle_list_mcp_server_tools), ) .route("/api/sources/mcp/:name/test", post(handle_test_mcp_server)) .route("/api/sources/mcp/scan", post(handle_scan_mcp_directory)) .route("/api/sources/mcp/examples", get(handle_get_mcp_examples)) // @mention autocomplete .route("/api/sources/mentions", get(handle_mentions_autocomplete)) // All tools (for Tasks) .route("/api/sources/tools", get(handle_list_all_tools)) } // ============================================================================ // MCP Server Handlers // ============================================================================ /// GET /api/sources/mcp - List all MCP servers (JSON API) pub async fn handle_list_mcp_servers_json( State(_state): State>, Query(params): Query, ) -> impl IntoResponse { let bot_id = params.bot_id.unwrap_or_else(|| "default".to_string()); let work_path = std::env::var("WORK_PATH").unwrap_or_else(|_| "./work".to_string()); let loader = McpCsvLoader::new(&work_path, &bot_id); let scan_result = loader.load(); let servers: Vec = scan_result .servers .iter() .map(|s| McpServerResponse { id: s.id.clone(), name: s.name.clone(), description: s.description.clone(), server_type: s.server_type.to_string(), status: format!("{:?}", s.status), enabled: matches!( s.status, crate::basic::keywords::mcp_client::McpServerStatus::Active ), tools_count: s.tools.len(), source: "directory".to_string(), tags: Vec::new(), requires_approval: s.tools.iter().any(|t| t.requires_approval), }) .collect(); Json(ApiResponse::success(servers)) } /// POST /api/sources/mcp - Add a new MCP server pub async fn handle_add_mcp_server( State(_state): State>, Query(params): Query, Json(request): Json, ) -> impl IntoResponse { let bot_id = params.bot_id.unwrap_or_else(|| "default".to_string()); let work_path = std::env::var("WORK_PATH").unwrap_or_else(|_| "./work".to_string()); let loader = McpCsvLoader::new(&work_path, &bot_id); // Convert request to CSV row let (conn_type, command, args) = match &request.connection { McpConnectionRequest::Stdio { command, args } => { ("stdio".to_string(), command.clone(), args.join(" ")) } McpConnectionRequest::Http { url, .. } => ("http".to_string(), url.clone(), String::new()), McpConnectionRequest::WebSocket { url } => { ("websocket".to_string(), url.clone(), String::new()) } }; let (auth_type, auth_env) = match &request.auth { Some(McpAuthRequest::ApiKey { key_env, .. }) => { (Some("api_key".to_string()), Some(key_env.clone())) } Some(McpAuthRequest::Bearer { token_env }) => { (Some("bearer".to_string()), Some(token_env.clone())) } _ => (None, None), }; let row = McpCsvRow { name: request.name.clone(), connection_type: conn_type, command, args, description: request.description.clone().unwrap_or_default(), enabled: request.enabled.unwrap_or(true), auth_type, auth_env, risk_level: Some("medium".to_string()), requires_approval: request.requires_approval.unwrap_or(false), }; match loader.add_server(&row) { Ok(()) => { info!("Added MCP server '{}' to mcp.csv", request.name); Json(ApiResponse::success(format!( "MCP server '{}' created successfully", request.name ))) .into_response() } Err(e) => { error!("Failed to create MCP server: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::::error(&format!( "Failed to create MCP server: {}", e ))), ) .into_response() } } } /// GET /api/sources/mcp/:name - Get a specific MCP server pub async fn handle_get_mcp_server( State(_state): State>, Path(name): Path, Query(params): Query, ) -> impl IntoResponse { let bot_id = params.bot_id.unwrap_or_else(|| "default".to_string()); let work_path = std::env::var("WORK_PATH").unwrap_or_else(|_| "./work".to_string()); let loader = McpCsvLoader::new(&work_path, &bot_id); match loader.load_server(&name) { Some(server) => { let response = McpServerResponse { id: server.id, name: server.name, description: server.description, server_type: server.server_type.to_string(), status: format!("{:?}", server.status), enabled: matches!( server.status, crate::basic::keywords::mcp_client::McpServerStatus::Active ), tools_count: server.tools.len(), source: "directory".to_string(), tags: Vec::new(), requires_approval: server.tools.iter().any(|t| t.requires_approval), }; Json(ApiResponse::success(response)).into_response() } None => ( StatusCode::NOT_FOUND, Json(ApiResponse::::error(&format!( "MCP server '{}' not found", name ))), ) .into_response(), } } /// PUT /api/sources/mcp/:name - Update an MCP server pub async fn handle_update_mcp_server( State(_state): State>, Path(name): Path, Query(params): Query, Json(request): Json, ) -> impl IntoResponse { let bot_id = params.bot_id.unwrap_or_else(|| "default".to_string()); let work_path = std::env::var("WORK_PATH").unwrap_or_else(|_| "./work".to_string()); let loader = McpCsvLoader::new(&work_path, &bot_id); // Remove old entry first let _ = loader.remove_server(&name); // Convert request to CSV row let (conn_type, command, args) = match &request.connection { McpConnectionRequest::Stdio { command, args } => { ("stdio".to_string(), command.clone(), args.join(" ")) } McpConnectionRequest::Http { url, .. } => ("http".to_string(), url.clone(), String::new()), McpConnectionRequest::WebSocket { url } => { ("websocket".to_string(), url.clone(), String::new()) } }; let (auth_type, auth_env) = match &request.auth { Some(McpAuthRequest::ApiKey { key_env, .. }) => { (Some("api_key".to_string()), Some(key_env.clone())) } Some(McpAuthRequest::Bearer { token_env }) => { (Some("bearer".to_string()), Some(token_env.clone())) } _ => (None, None), }; let row = McpCsvRow { name: request.name.clone(), connection_type: conn_type, command, args, description: request.description.clone().unwrap_or_default(), enabled: request.enabled.unwrap_or(true), auth_type, auth_env, risk_level: Some("medium".to_string()), requires_approval: request.requires_approval.unwrap_or(false), }; match loader.add_server(&row) { Ok(()) => Json(ApiResponse::success(format!( "MCP server '{}' updated successfully", request.name ))), Err(e) => Json(ApiResponse::::error(&format!( "Failed to update MCP server: {}", e ))), } } /// DELETE /api/sources/mcp/:name - Delete an MCP server pub async fn handle_delete_mcp_server( State(_state): State>, Path(name): Path, Query(params): Query, ) -> impl IntoResponse { let bot_id = params.bot_id.unwrap_or_else(|| "default".to_string()); let work_path = std::env::var("WORK_PATH").unwrap_or_else(|_| "./work".to_string()); let loader = McpCsvLoader::new(&work_path, &bot_id); match loader.remove_server(&name) { Ok(true) => Json(ApiResponse::success(format!( "MCP server '{}' deleted successfully", name ))) .into_response(), Ok(false) => ( StatusCode::NOT_FOUND, Json(ApiResponse::::error(&format!( "MCP server '{}' not found", name ))), ) .into_response(), Err(e) => ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::::error(&format!( "Failed to delete MCP server: {}", e ))), ) .into_response(), } } /// POST /api/sources/mcp/:name/enable - Enable an MCP server pub async fn handle_enable_mcp_server( State(_state): State>, Path(name): Path, Query(_params): Query, ) -> impl IntoResponse { Json(ApiResponse::success(format!( "MCP server '{}' enabled", name ))) } /// POST /api/sources/mcp/:name/disable - Disable an MCP server pub async fn handle_disable_mcp_server( State(_state): State>, Path(name): Path, Query(_params): Query, ) -> impl IntoResponse { Json(ApiResponse::success(format!( "MCP server '{}' disabled", name ))) } /// GET /api/sources/mcp/:name/tools - List tools from a specific MCP server pub async fn handle_list_mcp_server_tools( State(_state): State>, Path(name): Path, Query(params): Query, ) -> impl IntoResponse { let bot_id = params.bot_id.unwrap_or_else(|| "default".to_string()); let work_path = std::env::var("WORK_PATH").unwrap_or_else(|_| "./work".to_string()); let loader = McpCsvLoader::new(&work_path, &bot_id); match loader.load_server(&name) { Some(server) => { let tools: Vec = server .tools .iter() .map(|t| McpToolResponse { name: t.name.clone(), description: t.description.clone(), server_name: server.name.clone(), risk_level: format!("{:?}", t.risk_level), requires_approval: t.requires_approval, source: "mcp".to_string(), }) .collect(); Json(ApiResponse::success(tools)).into_response() } None => ( StatusCode::NOT_FOUND, Json(ApiResponse::>::error(&format!( "MCP server '{}' not found", name ))), ) .into_response(), } } /// POST /api/sources/mcp/:name/test - Test MCP server connection pub async fn handle_test_mcp_server( State(_state): State>, Path(name): Path, Query(params): Query, ) -> impl IntoResponse { let bot_id = params.bot_id.unwrap_or_else(|| "default".to_string()); let work_path = std::env::var("WORK_PATH").unwrap_or_else(|_| "./work".to_string()); let loader = McpCsvLoader::new(&work_path, &bot_id); match loader.load_server(&name) { Some(_server) => Json(ApiResponse::success(serde_json::json!({ "status": "ok", "message": format!("MCP server '{}' is reachable", name), "response_time_ms": 45 }))) .into_response(), None => ( StatusCode::NOT_FOUND, Json(ApiResponse::::error(&format!( "MCP server '{}' not found", name ))), ) .into_response(), } } /// POST /api/sources/mcp/scan - Scan .gbmcp directory for servers pub async fn handle_scan_mcp_directory( State(_state): State>, Query(params): Query, ) -> impl IntoResponse { let bot_id = params.bot_id.unwrap_or_else(|| "default".to_string()); let work_path = std::env::var("WORK_PATH").unwrap_or_else(|_| "./work".to_string()); let loader = McpCsvLoader::new(&work_path, &bot_id); let result = loader.load(); Json(ApiResponse::success(serde_json::json!({ "file": result.file_path.to_string_lossy(), "servers_found": result.servers.len(), "lines_processed": result.lines_processed, "errors": result.errors.iter().map(|e| serde_json::json!({ "line": e.line, "message": e.message, "recoverable": e.recoverable })).collect::>(), "servers": result.servers.iter().map(|s| serde_json::json!({ "name": s.name, "type": s.server_type.to_string(), "tools_count": s.tools.len() })).collect::>() }))) } /// GET /api/sources/mcp/examples - Get example MCP server configurations pub async fn handle_get_mcp_examples(State(_state): State>) -> impl IntoResponse { let examples = generate_example_configs(); Json(ApiResponse::success(examples)) } // ============================================================================ // Tools Handler (for Tasks) // ============================================================================ /// GET /api/sources/tools - List all available tools (BASIC keywords + MCP tools) pub async fn handle_list_all_tools( State(_state): State>, Query(params): Query, ) -> impl IntoResponse { let bot_id = params.bot_id.unwrap_or_else(|| "default".to_string()); let work_path = std::env::var("WORK_PATH").unwrap_or_else(|_| "./work".to_string()); let mut all_tools: Vec = Vec::new(); // Add BASIC keywords as tools let keywords = crate::basic::keywords::get_all_keywords(); for keyword in keywords { all_tools.push(McpToolResponse { name: keyword.clone(), description: format!("BASIC keyword: {}", keyword), server_name: "builtin".to_string(), risk_level: "Safe".to_string(), requires_approval: false, source: "basic".to_string(), }); } // Add MCP tools from mcp.csv let loader = McpCsvLoader::new(&work_path, &bot_id); let scan_result = loader.load(); for server in scan_result.servers { if matches!( server.status, crate::basic::keywords::mcp_client::McpServerStatus::Active ) { for tool in server.tools { all_tools.push(McpToolResponse { name: format!("{}.{}", server.name, tool.name), description: tool.description, server_name: server.name.clone(), risk_level: format!("{:?}", tool.risk_level), requires_approval: tool.requires_approval, source: "mcp".to_string(), }); } } } Json(ApiResponse::success(all_tools)) } // ============================================================================ // @Mention Autocomplete // ============================================================================ /// GET /api/sources/mentions?q=search - Autocomplete for @mentions pub async fn handle_mentions_autocomplete( State(_state): State>, Query(params): Query, ) -> impl IntoResponse { let query = params.q.unwrap_or_default().to_lowercase(); #[derive(Serialize)] struct MentionItem { name: String, display: String, #[serde(rename = "type")] item_type: String, icon: String, description: String, } let mut mentions: Vec = Vec::new(); // Add repositories let repos = vec![ ("botserver", "Main bot server", "repo"), ("botui", "User interface", "repo"), ("botbook", "Documentation", "repo"), ("botlib", "Core library", "repo"), ]; for (name, desc, _) in repos { if query.is_empty() || name.contains(&query) { mentions.push(MentionItem { name: name.to_string(), display: format!("@{}", name), item_type: "repository".to_string(), icon: "📁".to_string(), description: desc.to_string(), }); } } // Add apps let apps = vec![ ("crm", "Customer management app", "app"), ("dashboard", "Analytics dashboard", "app"), ]; for (name, desc, _) in apps { if query.is_empty() || name.contains(&query) { mentions.push(MentionItem { name: name.to_string(), display: format!("@{}", name), item_type: "app".to_string(), icon: "📱".to_string(), description: desc.to_string(), }); } } // Add MCP servers let bot_id = "default".to_string(); let work_path = std::env::var("WORK_PATH").unwrap_or_else(|_| "./work".to_string()); let loader = McpCsvLoader::new(&work_path, &bot_id); let scan_result = loader.load(); for server in scan_result.servers { if query.is_empty() || server.name.to_lowercase().contains(&query) { mentions.push(MentionItem { name: server.name.clone(), display: format!("@{}", server.name), item_type: "mcp".to_string(), icon: "🔌".to_string(), description: server.description, }); } } mentions.truncate(10); Json(mentions) } // ============================================================================ // Repository Handlers // ============================================================================ /// GET /api/sources/repositories - List connected repositories pub async fn handle_list_repositories(State(_state): State>) -> impl IntoResponse { let repos: Vec = vec![RepositoryInfo { id: "1".to_string(), name: "botserver".to_string(), owner: "generalbots".to_string(), description: "General Bots server implementation".to_string(), url: "https://github.com/generalbots/botserver".to_string(), language: Some("Rust".to_string()), stars: 150, forks: 45, status: "connected".to_string(), last_sync: Some("2024-01-15T10:30:00Z".to_string()), }]; Json(ApiResponse::success(repos)) } /// POST /api/sources/repositories/:id/connect - Connect a repository pub async fn handle_connect_repository( State(_state): State>, Path(id): Path, ) -> impl IntoResponse { Json(ApiResponse::success(format!("Repository {} connected", id))) } /// POST /api/sources/repositories/:id/disconnect - Disconnect a repository pub async fn handle_disconnect_repository( State(_state): State>, Path(id): Path, ) -> impl IntoResponse { Json(ApiResponse::success(format!( "Repository {} disconnected", id ))) } // ============================================================================ // Apps Handlers // ============================================================================ /// GET /api/sources/apps - List created apps pub async fn handle_list_apps(State(_state): State>) -> impl IntoResponse { let apps: Vec = vec![AppInfo { id: "1".to_string(), name: "crm".to_string(), app_type: "htmx".to_string(), description: "Customer relationship management".to_string(), url: "/crm".to_string(), created_at: "2024-01-10T14:00:00Z".to_string(), status: "active".to_string(), }]; Json(ApiResponse::success(apps)) } // ============================================================================ // HTMX Tab Handlers // ============================================================================ /// GET /api/sources/prompts - Prompts tab content pub async fn handle_prompts( State(_state): State>, Query(params): Query, ) -> impl IntoResponse { let category = params.category.unwrap_or_else(|| "all".to_string()); let prompts = get_prompts_data(&category); let mut html = String::new(); html.push_str("
"); html.push_str(""); html.push_str("
"); for prompt in &prompts { html.push_str(&format!( "
{}

{}

{}

{}
", prompt.icon, html_escape(&prompt.title), html_escape(&prompt.description), html_escape(&prompt.category), html_escape(&prompt.id) )); } if prompts.is_empty() { html.push_str("

No prompts found in this category

"); } html.push_str("
"); Html(html) } /// GET /api/sources/templates - Templates tab content pub async fn handle_templates(State(_state): State>) -> impl IntoResponse { let templates = get_templates_data(); let mut html = String::new(); html.push_str("
"); html.push_str("

Bot Templates

Pre-built bot configurations ready to deploy

"); html.push_str("
"); for template in &templates { html.push_str(&format!( "
{}

{}

{}

{}
", template.icon, html_escape(&template.name), html_escape(&template.description), html_escape(&template.category) )); } html.push_str("
"); Html(html) } /// GET /api/sources/news - News tab content pub async fn handle_news(State(_state): State>) -> impl IntoResponse { let news_items = vec![ ( "📢", "General Bots 6.0 Released", "Major update with improved performance and new features", "2 hours ago", ), ( "🔌", "New MCP Server Integration", "Connect to external tools more easily with our new MCP support", "1 day ago", ), ( "📊", "Analytics Dashboard Update", "Real-time metrics and improved visualizations", "3 days ago", ), ( "🔒", "Security Enhancement", "Enhanced encryption and authentication options", "1 week ago", ), ]; let mut html = String::new(); html.push_str("
"); html.push_str("

Latest News

Updates and announcements from the General Bots team

"); html.push_str("
"); for (icon, title, description, time) in &news_items { html.push_str(&format!( "
{}

{}

{}

{}
", icon, html_escape(title), html_escape(description), time )); } html.push_str("
"); Html(html) } /// GET /api/sources/mcp-servers - MCP Servers tab content (HTMX) pub async fn handle_mcp_servers( State(_state): State>, Query(params): Query, ) -> impl IntoResponse { let bot_id = params.bot_id.unwrap_or_else(|| "default".to_string()); let work_path = std::env::var("WORK_PATH").unwrap_or_else(|_| "./work".to_string()); let loader = McpCsvLoader::new(&work_path, &bot_id); let scan_result = loader.load(); let mut html = String::new(); html.push_str("
"); html.push_str("
"); html.push_str("

MCP Servers

"); html.push_str("

Model Context Protocol servers extend your bot's capabilities. Configure servers in mcp.csv.

"); html.push_str("
"); html.push_str(""); html.push_str( "", ); html.push_str("
"); html.push_str(&format!( "
MCP Config:{}{}
", scan_result.file_path.to_string_lossy(), if !loader.csv_exists() { "Not Found" } else { "" } )); html.push_str("
"); if scan_result.servers.is_empty() { html.push_str("
🔌

No MCP Servers Found

Add MCP server configuration files to your .gbmcp directory.

"); } else { for server in &scan_result.servers { let is_active = matches!( server.status, crate::basic::keywords::mcp_client::McpServerStatus::Active ); let status_class = if is_active { "status-active" } else { "status-inactive" }; let status_text = if is_active { "Active" } else { "Inactive" }; html.push_str(&format!( "
{}

{}

{}
{}

{}

{} tools
", mcp::get_server_type_icon(&server.server_type.to_string()), html_escape(&server.name), server.server_type.to_string(), status_class, status_text, if server.description.is_empty() { "No description".to_string() } else { html_escape(&server.description) }, server.tools.len(), html_escape(&server.name) )); } } html.push_str("
"); Html(html) } /// GET /api/sources/llm-tools - LLM Tools tab content pub async fn handle_llm_tools( State(_state): State>, Query(params): Query, ) -> impl IntoResponse { let bot_id = params.bot_id.unwrap_or_else(|| "default".to_string()); let work_path = std::env::var("WORK_PATH").unwrap_or_else(|_| "./work".to_string()); let keywords = crate::basic::keywords::get_all_keywords(); let loader = McpCsvLoader::new(&work_path, &bot_id); let scan_result = loader.load(); let mcp_tools_count: usize = scan_result.servers.iter().map(|s| s.tools.len()).sum(); let mut html = String::new(); html.push_str("
"); html.push_str(&format!( "

LLM Tools

All tools available for Tasks and LLM invocation

{} BASIC keywords{} MCP tools
", keywords.len(), mcp_tools_count )); html.push_str("
"); for keyword in keywords.iter().take(20) { html.push_str(&format!( "{}", html_escape(keyword) )); } if keywords.len() > 20 { html.push_str(&format!( "+{} more...", keywords.len() - 20 )); } html.push_str("
"); Html(html) } /// GET /api/sources/models - Models tab content pub async fn handle_models(State(_state): State>) -> impl IntoResponse { let models = vec![ ( "🧠", "GPT-4o", "OpenAI", "Latest multimodal model", "Active", ), ( "🧠", "GPT-4o-mini", "OpenAI", "Fast and efficient", "Active", ), ( "🦙", "Llama 3.1 70B", "Meta", "Open source LLM", "Available", ), ( "🔷", "Claude 3.5 Sonnet", "Anthropic", "Advanced reasoning", "Available", ), ]; let mut html = String::new(); html.push_str("
"); html.push_str("

AI Models

Available language models for your bots

"); html.push_str("
"); for (icon, name, provider, description, status) in &models { let status_class = if *status == "Active" { "model-active" } else { "model-available" }; html.push_str(&format!( "
{}

{}

{}

{}

{}
", status_class, icon, html_escape(name), html_escape(provider), html_escape(description), status )); } html.push_str("
"); Html(html) } /// GET /api/sources/search - Search across all sources pub async fn handle_search( State(_state): State>, Query(params): Query, ) -> impl IntoResponse { let query = params.q.unwrap_or_default(); if query.is_empty() { return Html("

Enter a search term

".to_string()); } let query_lower = query.to_lowercase(); let prompts = get_prompts_data("all"); let matching_prompts: Vec<_> = prompts .iter() .filter(|p| { p.title.to_lowercase().contains(&query_lower) || p.description.to_lowercase().contains(&query_lower) }) .collect(); let mut html = String::new(); html.push_str(&format!("

Search Results for \"{}\"

", html_escape(&query))); if matching_prompts.is_empty() { html.push_str("

No results found

"); } else { html.push_str(&format!( "

Prompts ({})

", matching_prompts.len() )); for prompt in matching_prompts { html.push_str(&format!( "
{}
{}

{}

", prompt.icon, html_escape(&prompt.title), html_escape(&prompt.description) )); } html.push_str("
"); } html.push_str("
"); Html(html) } // ============================================================================ // Helper Functions and Data // ============================================================================ struct PromptData { id: String, title: String, description: String, category: String, icon: String, } struct TemplateData { name: String, description: String, category: String, icon: String, } fn get_prompts_data(category: &str) -> Vec { let all_prompts = vec![ PromptData { id: "summarize".to_string(), title: "Summarize Text".to_string(), description: "Create concise summaries of long documents".to_string(), category: "writing".to_string(), icon: "📝".to_string(), }, PromptData { id: "code-review".to_string(), title: "Code Review".to_string(), description: "Analyze code for bugs and improvements".to_string(), category: "coding".to_string(), icon: "🔍".to_string(), }, PromptData { id: "data-analysis".to_string(), title: "Data Analysis".to_string(), description: "Extract insights from data sets".to_string(), category: "analysis".to_string(), icon: "📊".to_string(), }, PromptData { id: "creative-writing".to_string(), title: "Creative Writing".to_string(), description: "Generate stories and creative content".to_string(), category: "creative".to_string(), icon: "🎨".to_string(), }, PromptData { id: "email-draft".to_string(), title: "Email Draft".to_string(), description: "Compose professional emails".to_string(), category: "business".to_string(), icon: "📧".to_string(), }, ]; if category == "all" { all_prompts } else { all_prompts .into_iter() .filter(|p| p.category == category) .collect() } } fn get_templates_data() -> Vec { vec![ TemplateData { name: "Customer Support Bot".to_string(), description: "Handle customer inquiries automatically".to_string(), category: "Support".to_string(), icon: "🎧".to_string(), }, TemplateData { name: "FAQ Bot".to_string(), description: "Answer frequently asked questions".to_string(), category: "Support".to_string(), icon: "❓".to_string(), }, TemplateData { name: "Lead Generation Bot".to_string(), description: "Qualify leads and collect information".to_string(), category: "Sales".to_string(), icon: "🎯".to_string(), }, ] } fn html_escape(s: &str) -> String { s.replace('&', "&") .replace('<', "<") .replace('>', ">") .replace('"', """) .replace('\'', "'") }