/** * DashCaddy AI Intent Router * * Takes natural language input and returns structured, actionable intents * that can be executed against the DashCaddy API. * * POST /api/v1/ai/intent * Body: { message: "I want to stream movies", context: {} } * Returns: { intent, confidence, actions, followup } * * The intent router uses pattern matching (not an LLM call) so it works * instantly and offline. For complex queries, it can delegate to an * external LLM via the LLM_PROXY_URL env var. */ const express = require('express'); const { ok, errorResponse } = require('../src/utils/responses'); // ─── Intent Pattern Library ───────────────────────────────────────────────── const INTENT_PATTERNS = [ // ── Deploy intents ── { intent: 'deploy', patterns: [ /\b(?:deploy|install|set up|setup|host|run|start|spin up|launch)\b.*\b(?:plex|jellyfin|emby|sonarr|radarr|nextcloud|gitea|vaultwarden|adguard|wireguard|home.assistant|grafana|prometheus|qbittorrent|transmission|portainer|redis|postgres|mariadb|mongodb|nginx)\b/i, /\b(?:i want|i need|can you|help me|let'?s)\b.*\b(?:deploy|install|set up|host|run)\b/i, ], action: 'dashcaddy_deploy_app', extractApp: (msg) => { const apps = ['plex', 'jellyfin', 'emby', 'sonarr', 'radarr', 'prowlarr', 'lidarr', 'readarr', 'qbittorrent', 'transmission', 'nextcloud', 'vaultwarden', 'gitea', 'adguard', 'pihole', 'wireguard', 'home assistant', 'homeassistant', 'grafana', 'prometheus', 'portainer', 'redis', 'postgres', 'postgresql', 'mariadb', 'mongodb', 'nginx', 'caddy', 'uptime kuma', 'code-server']; for (const app of apps) { if (msg.toLowerCase().includes(app)) return app.replace(/\s+/g, '-'); } return null; }, }, // ── Streaming/Media intents ── { intent: 'recommend', patterns: [ /\b(?:stream|streaming|movie|movies|tv show|tv shows|film|films|watch|media)\b/i, ], action: 'dashcaddy_wizard_recommend', suggestCategories: ['media-streaming'], response: (msg) => ({ message: 'For media streaming, I recommend:', recommendations: [ { app: 'plex', reason: 'Stream movies and TV shows to any device' }, { app: 'jellyfin', reason: 'Free open-source alternative to Plex, no premium features locked' }, { app: 'emby', reason: 'Media server with live TV and parental controls' }, { app: 'sonarr', reason: 'Automatically download TV shows' }, { app: 'radarr', reason: 'Automatically download movies' }, { app: 'qbittorrent', reason: 'Download client for media files' }, ], question: 'Would you like me to deploy any of these?', disclaimer: 'DashCaddy provides deployment tools only. Users are responsible for complying with all applicable copyright and intellectual property laws. Always stream content you own or have rights to access.', }), }, // ── Password manager ── { intent: 'recommend', patterns: [ /\b(?:password|passwords|password manager|vaultwarden|bitwarden|1password|lastpass|secure password)\b/i, ], action: 'dashcaddy_wizard_recommend', suggestCategories: ['file-sync'], response: (msg) => ({ message: 'For password management, I recommend:', recommendations: [ { app: 'vaultwarden', reason: 'Self-hosted Bitwarden-compatible password manager' }, ], question: 'Would you like me to deploy Vaultwarden?', }), }, // ── Ad blocking ── { intent: 'recommend', patterns: [ /\b(?:ad block|adblock|block ads|ad blocking|pihole|adguard|dns blocking)\b/i, ], action: 'dashcaddy_wizard_recommend', suggestCategories: ['home-network'], response: (msg) => ({ message: 'For network-wide ad blocking, I recommend:', recommendations: [ { app: 'adguard', reason: 'DNS-level ad blocking for your entire network' }, { app: 'pihole', reason: 'Alternative DNS ad blocker with detailed statistics' }, ], question: 'Would you like me to set up ad blocking?', }), }, // ── File storage ── { intent: 'recommend', patterns: [ /\b(?:file storage|cloud storage|google drive|dropbox|file sync|nextcloud|owncloud)\b/i, ], action: 'dashcaddy_wizard_recommend', suggestCategories: ['file-sync'], response: (msg) => ({ message: 'For file storage and sync, I recommend:', recommendations: [ { app: 'nextcloud', reason: 'Self-hosted Google Drive replacement' }, ], question: 'Would you like me to deploy Nextcloud?', }), }, // ── Development ── { intent: 'recommend', patterns: [ /\b(?:git|code|develop|programming|ide|vs code|github|self-hosted git)\b/i, ], action: 'dashcaddy_wizard_recommend', suggestCategories: ['development'], response: (msg) => ({ message: 'For development tools, I recommend:', recommendations: [ { app: 'gitea', reason: 'Self-hosted Git with CI/CD pipelines' }, { app: 'code-server', reason: 'VS Code in your browser' }, ], question: 'Would you like me to deploy any of these?', }), }, // ── Diagnostics ── { intent: 'diagnose', patterns: [ /\b(?:why|what'?s wrong|broken|down|not working|slow|error|failing|crashed|unhealthy|diagnose|troubleshoot|debug)\b/i, ], action: 'dashcaddy_diagnose', extractService: (msg) => { // Try to extract service name from "why is X down" patterns const match = msg.match(/(?:why is |is |)(\w+)\s+(?:down|slow|broken|not working|failing|crashed)/i); if (match) return match[1].toLowerCase(); return null; }, response: (msg) => ({ message: 'Let me check what\'s going on...', action: 'diagnose', }), }, // ── Backup ── { intent: 'backup', patterns: [ /\b(?:backup|back up|save|snapshot|export)\b/i, ], action: 'dashcaddy_create_backup', response: (msg) => ({ message: 'Creating a full system backup now...', action: 'backup', }), }, // ── Health check ── { intent: 'health', patterns: [ /\b(?:health|healthy|status|everything ok|all good|system check|how are things)\b/i, ], action: 'dashcaddy_system_health', response: (msg) => ({ message: 'Checking system health...', action: 'health_check', }), }, // ── List/show ── { intent: 'list', patterns: [ /\b(?:list|show|what.*running|what.*deployed|what.*have|what.*services)\b/i, ], action: 'dashcaddy_list_services', response: (msg) => ({ message: 'Here are your services:', action: 'list_services', }), }, ]; // ─── Intent Router ────────────────────────────────────────────────────────── function routeIntent(message) { const msg = message.toLowerCase().trim(); // Try each intent pattern for (const intent of INTENT_PATTERNS) { for (const pattern of intent.patterns) { if (pattern.test(message)) { const result = { intent: intent.intent, confidence: 0.85, action: intent.action, message: message, response: typeof intent.response === 'function' ? intent.response(message) : null, }; // Extract app name for deploy intents if (intent.extractApp) { const app = intent.extractApp(message); if (app) result.appId = app; } // Extract service name for diagnose intents if (intent.extractService) { const service = intent.extractService(message); if (service) result.serviceId = service; } // Suggest categories for recommend intents if (intent.suggestCategories) { result.categories = intent.suggestCategories; } return result; } } } // No match — return a fallback that suggests using the catalog return { intent: 'unknown', confidence: 0.3, message, response: { message: 'I\'m not sure what you\'d like to do. Here are some things I can help with:', suggestions: [ 'Deploy an app: "Deploy Plex" or "Set up Nextcloud"', 'Get recommendations: "I want to stream movies" or "Block ads on my network"', 'Check status: "Is everything OK?" or "Why is Plex down?"', 'Browse catalog: "What can I self-host?"', 'Create backup: "Back up everything"', ], action: 'suggest', }, }; } // ─── Express Route ────────────────────────────────────────────────────────── module.exports = function({ asyncHandler }) { const wrap = asyncHandler || ((fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next)); const router = express.Router(); /** * POST /api/v1/ai/intent * * Natural language → structured action plan */ router.post('/ai/intent', wrap(async (req, res) => { const { message, context = {} } = req.body || {}; if (!message || typeof message !== 'string') { return errorResponse(res, 400, 'message (string) is required'); } const result = routeIntent(message); // Add context from the request result.context = context; result.timestamp = new Date().toISOString(); // For deploy intents with an appId, include the deploy plan if (result.intent === 'deploy' && result.appId) { result.deployPlan = { templateId: result.appId, endpoint: 'POST /api/v1/discover/adopt', body: { containerId: null, // Will be set after container creation serviceId: result.appId, name: result.appId.charAt(0).toUpperCase() + result.appId.slice(1), port: null, // Will be set from template generateDns: true, generateRoute: true, }, nextSteps: [ `Search catalog: GET /api/v1/catalog/search?q=${result.appId}`, `Get template: GET /api/v1/catalog/${result.appId}`, `Deploy: POST /api/v1/discover/adopt`, ], }; } // For recommend intents, include the wizard endpoint if (result.intent === 'recommend' && result.categories) { result.wizardCall = { endpoint: 'POST /api/v1/wizard/recommend', body: { categories: result.categories, hardwareProfile: 'medium' }, }; } ok(res, result); })); /** * GET /api/v1/ai/capabilities * Returns what the AI can do — useful for agent self-discovery */ router.get('/ai/capabilities', wrap(async (req, res) => { ok(res, { intents: [...new Set(INTENT_PATTERNS.map(p => p.intent))], capabilities: [ { name: 'deploy', description: 'Deploy self-hosted applications from the catalog' }, { name: 'recommend', description: 'Get service recommendations based on goals' }, { name: 'diagnose', description: 'Troubleshoot service issues' }, { name: 'backup', description: 'Create full system backups' }, { name: 'health', description: 'Check system and service health' }, { name: 'list', description: 'List services and containers' }, ], tools: '17 MCP tools available via MCP protocol at src/mcp/mcp-server.js', exampleQueries: [ 'Deploy Plex', 'I want to stream movies', 'Block ads on my network', 'Why is Plex down?', 'Back up everything', 'What services am I running?', ], }); })); return router; }; module.exports.routeIntent = routeIntent;