340 lines
12 KiB
JavaScript
340 lines
12 KiB
JavaScript
/**
|
|
* 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; |