Lumi/plugins/lumi_ai/index.js
2026-06-25 14:10:04 +02:00

2072 lines
88 KiB
JavaScript

const fs = require("fs");
const path = require("path");
const express = require("express");
const { ensureDataDirs, resolveData } = require("./backend/paths");
const { getConfig, saveConfig, getRuntimeState } = require("./backend/config_manager");
const { detectHardware, estimateAllocation, performanceTuningHints } = require("./backend/hardware");
const metrics = require("./backend/metrics");
const { roleOf } = require("./backend/permissions");
const { canUseAssistant } = require("./backend/assistant_permissions");
const { RequestQueue } = require("./backend/queue_manager");
const { ToolRegistry } = require("./backend/tool_router");
const { DownloadManager } = require("./backend/downloader");
const { RuntimeManager, GateRuntimeManager, combinedResourceEstimate } = require("./backend/runtime_manager");
const { AiProvider } = require("./backend/ai_provider");
const { GateProvider } = require("./backend/gate_provider");
const { SafeAnswerCache } = require("./backend/cache");
const { AssistantRequestJobs } = require("./backend/request_jobs");
const { getLatestDiagnostic, createDiagnosticsBundle } = require("./backend/diagnostics");
const { evaluateAssistantAvailability } = require("./backend/assistant_availability");
const { buildVisibilityDiagnostics } = require("./backend/assistant_visibility");
const repoIndexer = require("./backend/repo_indexer");
const { HARD_RULES, normalizeScope } = require("./backend/scope_manager");
const { AiAccessControl } = require("./backend/access_control");
const { AiRateLimiter, mergeLimits } = require("./backend/rate_limits");
const { buildOriginContext, formatPlatformReply, formatPlatformReplyDetails } = require("./backend/commands");
const { AssistantPanelDiagnostics } = require("./backend/assistant_panel_diagnostics");
const { formatAssistantResponse } = require("./backend/response_formatter");
const { FeedbackStore, FEEDBACK_KINDS, FEEDBACK_TAGS, improvementAccess } = require("./backend/feedback");
const { CorrectionStore, PROMOTION_TARGETS } = require("./backend/corrections");
const { EvalStore } = require("./backend/evals");
const { TrainingExporter } = require("./backend/training_export");
const { ToolRepoClient } = require("./backend/tool_repo_client");
const { ToolInstaller } = require("./backend/tool_installer");
const { ToolLoader } = require("./backend/tool_loader");
const { ToolManager } = require("./backend/tool_manager");
const { ToolSettings } = require("./backend/tool_settings");
const { buildAllowedToolsSection } = require("./backend/prompt_builder");
const storage = require("./backend/storage");
const { formatBytes, bytesFromMb, sanityCheckSize } = require("./backend/size_utils");
const { SOURCE_DEFAULTS } = require("./backend/controller");
const PLUGIN_ID = "lumi_ai";
const TOKEN_PRESETS = Object.freeze([
{ label: "Tiny (256)", value: 256, description: "Small helper replies and minimal context." },
{ label: "Very small (512)", value: 512, description: "Short replies and low memory usage." },
{ label: "Small (1024)", value: 1024, description: "Compact answers and lightweight request gates." },
{ label: "Short (2048)", value: 2048, description: "Short conversations and normal commands." },
{ label: "Medium (4096)", value: 4096, description: "Balanced default for normal assistant use." },
{ label: "Large (8192)", value: 8192, description: "Longer conversations and documents." },
{ label: "Extended (16384)", value: 16384, description: "Long context when the selected model supports it." },
{ label: "Extra extended (32768)", value: 32768, description: "Highest supported local preset for large-context models." }
]);
const CONTEXT_OPTIONS = TOKEN_PRESETS;
const GATE_CONTEXT_OPTIONS = TOKEN_PRESETS.filter((option) => option.value >= 512 && option.value <= 4096);
const modelManifest = require("./models_manifest.json");
const runtimeManifest = require("./runtime_manifest.json");
module.exports = {
id: PLUGIN_ID,
init({ web, settings, commandRouter, db, plugin }) {
ensureDataDirs();
if (!repoIndexer.loadIndex()) {
try { repoIndexer.refreshIndex(); }
catch (error) { console.warn("Lumi AI repository index initialization failed", error.message); }
}
let config = getConfig();
metrics.configureRetention(config.work_history_retention);
const feedbackStore = new FeedbackStore();
const correctionStore = new CorrectionStore({
getConfig: () => config,
verifyLink: isVerifiedImprovementLink
});
const getModel = (id) => modelManifest.models.find((model) => model.id === id);
const downloads = new DownloadManager((entry) => metrics.record(entry));
const accessControl = new AiAccessControl((entry) => metrics.record(entry));
const rateLimiter = new AiRateLimiter(() => config, (entry) => metrics.record(entry));
const panelTemplatePath = path.join(__dirname, "views", "assistant-panel.ejs");
const panelDiagnostics = new AssistantPanelDiagnostics(panelTemplatePath);
panelDiagnostics.templateCheck(
["endpoint", "user"],
{ endpoint: `/plugins/${PLUGIN_ID}`, user: { id: "diagnostic-user" } }
);
const queue = new RequestQueue(() => config);
const requestJobs = new AssistantRequestJobs();
const tools = new ToolRegistry((entry) => metrics.record(entry));
const toolRepoClient = new ToolRepoClient({ settings });
const toolInstaller = new ToolInstaller({ repoClient: toolRepoClient });
const toolLoader = new ToolLoader({
registry: tools,
installer: toolInstaller,
settings,
lumiAiVersion: require("./plugin.json").version,
lumiVersion: require("../../package.json").version
});
const toolSettings = new ToolSettings({ installer: toolInstaller });
const toolManager = new ToolManager({
repoClient: toolRepoClient,
installer: toolInstaller,
loader: toolLoader,
settings: toolSettings
});
const contextProviders = new Map();
const frontendVisibility = new Map();
const getSafeContext = (input = {}) => {
const context = typeof input === "string" ? { role: input } : input;
const diagnostics = [];
const blocks = [];
getSafeContext.lastDiagnostics = diagnostics;
getSafeContext.lastErrors = [];
for (const fn of contextProviders.values()) {
try {
const result = fn(context);
if (result && typeof result === "object" && !Array.isArray(result)) {
if (result.diagnostics) diagnostics.push(safeContextDiagnostic(result.diagnostics));
blocks.push(...normalizeContext(result.blocks ?? result.context ?? result.items ?? []));
} else {
blocks.push(...normalizeContext(result));
}
} catch (error) {
getSafeContext.lastErrors.push(String(error.message || error).slice(0, 300));
}
}
return blocks;
};
getSafeContext.lastDiagnostics = [];
getSafeContext.lastErrors = [];
const runtime = new RuntimeManager({
getConfig: () => config,
getModel,
runtimeManifest,
onCrash: (message) => metrics.record({ kind: "runtime", status: "failed", runtime_crash: true, message }),
onDiagnostic: (entry) => metrics.record(entry)
});
const gateRuntime = new GateRuntimeManager({
getConfig: () => config,
getModel,
onDiagnostic: (entry) => metrics.record(entry)
});
const gate = new GateProvider({
getConfig: () => config,
runtime: gateRuntime,
lookupRepo: (message) => repoIndexer.lookupSupport(message),
lookupCorrection: (context) => correctionStore.findPredefined({
message: context.message,
role: context.role,
origin: context.origin || context.platform,
platform: context.platform
}),
cache: new SafeAnswerCache(() => config),
metrics
});
let mainStartPromise = null;
let gateStartPromise = null;
const ensureMainRuntime = async (options = {}) => {
const health = await runtime.health();
if (health.healthy) return health;
if (!mainStartPromise) {
mainStartPromise = runtime.start(options).finally(() => { mainStartPromise = null; });
}
return mainStartPromise;
};
const ensureGateRuntime = async () => {
if (gateRuntime.status().state === "running") return gateRuntime.health();
if (!gateStartPromise) {
gateStartPromise = gateRuntime.start().finally(() => { gateStartPromise = null; });
}
return gateStartPromise;
};
const provider = new AiProvider({
getConfig: () => config,
runtime,
gate,
queue,
tools,
metrics,
getContext: getSafeContext,
lookupRepo: (message) => repoIndexer.lookupSupport(message),
getRepoContext: (message, role, allowModeratorCodeHelp) =>
repoIndexer.supportContext(message, repoIndexer.loadIndex(), 8, role, allowModeratorCodeHelp),
getCorrections: (context) => correctionStore.context(context),
ensureRuntime: ensureMainRuntime
});
const evalStore = new EvalStore({ provider });
const trainingExporter = new TrainingExporter({
feedback: feedbackStore,
corrections: correctionStore
});
const startRuntimes = async (options = {}) => {
if (config.enabled) {
ensureGateRuntime().catch((error) => {
metrics.record({ kind: "gate_runtime", status: "failed", reason_code: "gate_start_failed", message: error.message });
web.emitEvent?.("ai:model_status", {
status: "gate_start_failed",
message: `Lumi AI gate runtime failed to start: ${error.message}`
}, { role: "admin" });
});
}
const main = await ensureMainRuntime(options);
return { ...main, gate: await gateRuntime.health() };
};
const stopRuntimes = async (options = {}) => {
await gateRuntime.stop();
return runtime.stop(options);
};
const restartRuntimes = async () => {
await gateRuntime.stop();
const main = await runtime.restart();
if (config.enabled) {
try { await ensureGateRuntime(); }
catch (error) {
metrics.record({ kind: "gate_runtime", status: "failed", reason_code: "gate_restart_failed", message: error.message });
web.emitEvent?.("ai:model_status", {
status: "gate_restart_failed",
message: `Lumi AI gate runtime restart failed: ${error.message}`
}, { role: "admin" });
}
}
return { ...main, gate: await gateRuntime.health() };
};
let gateRecoveryPending = false;
let runtimeInstallInProgress = false;
const gateMonitor = setInterval(async () => {
if (
gateRecoveryPending ||
runtimeInstallInProgress ||
!config.enabled ||
gateRuntime.status().state === "running"
) return;
gateRecoveryPending = true;
try { await ensureGateRuntime(); }
catch (error) {
web.emitEvent?.("ai:model_status", {
status: "gate_recovery_failed",
message: `Lumi AI gate recovery failed: ${error.message}`
}, { role: "admin" });
}
finally { gateRecoveryPending = false; }
}, 30000);
gateMonitor.unref?.();
const api = {
health: () => runtime.health(),
capabilities: () => ({
provider: "local_llama_cpp",
enabled: config.enabled,
model_id: config.selected_model_id,
roles: config.assistant_visibility,
tools: tools.list("admin").map((tool) => tool.tool_id)
}),
metrics_summary: () => metrics.report(),
generate: (input) => provider.generate(input),
classify: (input) => provider.classify(input),
summarize: (input) => provider.summarize(input),
route_tool: async ({ message, allowed_tools = [], ...input }) => {
const result = await provider.generate({ message, ...input, scope: "route_tool" });
if (result.tool_call && !allowed_tools.includes(result.tool_call.tool)) {
return { ...result, success: false, tool_call: null, refusal_reason: "tool_not_allowed" };
}
return result;
},
registerTool: (definition) => tools.register(definition),
unregisterToolOwner: (owner) => tools.unregisterOwner(owner),
registerContext: (id, factory) => {
if (!id || typeof factory !== "function") throw new Error("Invalid AI context provider.");
contextProviders.set(id, factory);
},
unregisterContext: (id) => contextProviders.delete(id)
};
global.lumiFrameworks = global.lumiFrameworks || {};
global.lumiFrameworks.ai = api;
global.lumiFrameworks.lumi_ai = api;
if (typeof global.lumiFrameworks.okf?.context === "function") {
api.registerContext("okf", global.lumiFrameworks.okf.context);
}
const router = web.createRouter();
router.use("/assets", express.static(path.join(__dirname, "public")));
router.get("/", async (req, res) => {
if (!req.session.user?.isAdmin) return denied(res);
const hardware = detectHardware(modelManifest.models, runtimeManifest);
const runtimeTarget = getRuntimeTarget(hardware);
const selectedModel = getModel(config.selected_model_id);
const runtimeStatus = await runtime.health();
const gateStatus = await gateRuntime.health();
const gpuAllocation = estimateAllocation({
model: selectedModel,
contextSize: config.context_size,
gpu: hardware.gpu,
backend: runtimeStatus.runtime_backend,
intentPercent: config.gpu_allocation_intent_percent,
managedUsageMb: runtimeStatus.state === "running" ? runtimeStatus.estimated_gpu_memory_mb : 0
});
const assistantAvailability = evaluateAssistantAvailability({
user: req.session.user,
config,
model: selectedModel,
runtimeHealth: runtimeStatus,
providerAvailable: Boolean(provider)
});
const visibilityDiagnostics = buildVisibilityDiagnostics({
user: req.session.user,
config,
model: selectedModel,
runtimeHealth: runtimeStatus,
providerAvailable: Boolean(provider),
frontend: frontendVisibility.get(req.session.user.id)
});
const usage = storage.storageUsage(modelManifest.models, config.selected_model_id);
const resourceEstimate = combinedResourceEstimate({
main: runtimeStatus,
gate: gateStatus,
hardware
});
const recentGeneration = metrics.history(100).find((entry) =>
entry.kind === "request" && Number(entry.generation_tps) > 0
);
const tuningHints = performanceTuningHints({
model: selectedModel,
config,
gpu: hardware.gpu,
allocation: gpuAllocation,
generationTps: Number(recentGeneration?.generation_tps) || 0
});
const workHistoryPage = metrics.workHistoryPage(req.query, req.query.work_page, 20);
const metricsPage = metrics.historyPage(req.query.metrics_page, 25);
const slowRequestsPage = metrics.slowRequestsPage(req.query.slow_page, 15);
const accessPage = paginateRows(accessControl.list(), req.query.access_page, 25);
const logPage = storage.listLogsPage(req.query.logs_page, 25);
const runtimeFolderSize = usage.categories.runtime;
const selectedModelPath = selectedModel ? storage.modelPath(selectedModel) : null;
const modelFileSize = selectedModelPath && fs.existsSync(selectedModelPath)
? fs.statSync(selectedModelPath).size
: 0;
const runtimeDownloadSize = runtimeTarget
? Number(runtimeTarget.size || 0) + (runtimeTarget.dependencies || []).reduce((sum, dependency) => sum + Number(dependency.size || 0), 0)
: 0;
const sizeDiagnostics = [
sanityCheckSize("Runtime folder", runtimeFolderSize, 5 * 1024 ** 3),
sanityCheckSize("Runtime archive", runtimeDownloadSize, 2 * 1024 ** 3),
sanityCheckSize("Selected model", selectedModel?.size || 0, 100 * 1024 ** 3),
sanityCheckSize("Installed model", modelFileSize, 100 * 1024 ** 3),
sanityCheckSize("Estimated GPU memory", bytesFromMb(gpuAllocation.estimated_gpu_memory_mb), 100 * 1024 ** 3)
].filter((check) => !check.valid);
for (const diagnostic of sizeDiagnostics) console.warn(`Lumi AI size diagnostic: ${diagnostic.message}`);
const models = modelManifest.models.map((model) => ({
...model,
downloaded: fs.existsSync(resolveData("models", model.filename)),
installed_size: fs.existsSync(resolveData("models", model.filename))
? fs.statSync(resolveData("models", model.filename)).size
: 0,
compatible: model.ram_gb * 1024 <= hardware.total_ram_mb && model.size / 1048576 <= hardware.free_disk_mb
}));
const installedModels = models.filter((model) => model.downloaded);
res.render(path.join(__dirname, "views", "settings.ejs"), {
title: "Lumi AI",
config,
models,
installedModels,
selectedModelInstalled: installedModels.some((model) => model.id === config.selected_model_id),
contextOptions: CONTEXT_OPTIONS,
tokenPresets: TOKEN_PRESETS,
gateContextOptions: GATE_CONTEXT_OPTIONS,
runtimeTarget,
runtimeDownloadSize,
runtimeManifest,
runtimeStatus,
gateStatus,
resourceEstimate,
tuningHints,
jobDiagnostics: requestJobs.diagnostics(),
gpuAllocation,
assistantAvailability,
visibilityDiagnostics,
panelDiagnostics: panelDiagnostics.snapshot(),
activeAiRestrictions: accessPage.entries,
accessPage,
recentRateLimitDenials: rateLimiter.recentDenials(),
hardRules: HARD_RULES,
assistantReason: describeAssistantReason(assistantAvailability.reason_code),
repoIndexStatus: repoIndexer.indexStatus(),
runtimeState: getRuntimeState(),
latestDiagnostic: getLatestDiagnostic(),
runtimeFolderSize,
modelFileSize,
storageUsage: usage,
sizeDiagnostics,
hardware,
metrics: metrics.report(),
workHistoryPage,
workFilters: req.query,
history: metricsPage.entries,
metricsPage,
slowRequestsPage,
logFiles: logPage.entries,
logPage,
formatBytes,
formatDate,
formatDuration
});
});
router.post("/settings", async (req, res) => {
if (!req.session.user?.isAdmin) return denied(res);
const model = getModel(req.body.selected_model_id);
if (!model) return flash(req, res, "error", "Unknown model.");
if (!fs.existsSync(resolveData("models", model.filename))) {
return flash(req, res, "error", "Selected model must be installed before it can be saved.");
}
const contextValues = CONTEXT_OPTIONS.map((option) => option.value);
const requestedContext = Number(req.body.context_size);
if (!contextValues.includes(requestedContext)) {
return flash(req, res, "error", "Choose a supported AI context size.");
}
const contextSize = requestedContext;
const gateContextValues = GATE_CONTEXT_OPTIONS.map((option) => option.value);
const requestedGateContext = Number(req.body.gate_context_size);
if (!gateContextValues.includes(requestedGateContext)) {
return flash(req, res, "error", "Choose a supported gate context size.");
}
const previousConfig = config;
let nextConfig;
try {
nextConfig = {
...config,
enabled: req.body.enabled === "on",
selected_model_id: model.id,
context_size: contextSize,
internal_generation_char_budget: boundedInt(req.body.internal_generation_char_budget, 2000, 64000, 16000),
threads: boundedInt(req.body.threads, 0, 256, 0),
gpu_allocation_intent_percent: boundedInt(req.body.gpu_allocation_intent_percent, 0, 100, 0),
concurrency: boundedInt(req.body.concurrency, 1, 8, 1),
max_queue_length: boundedInt(req.body.max_queue_length, 1, 100, 8),
request_timeout_ms: boundedInt(req.body.hard_generation_timeout_ms, 30000, 3600000, 600000),
ui_soft_timeout_ms: boundedInt(req.body.ui_soft_timeout_ms, 5000, 300000, 45000),
hard_generation_timeout_ms: boundedInt(req.body.hard_generation_timeout_ms, 30000, 3600000, 600000),
source_profiles: sourceProfilesFromBody(req.body, config.source_profiles),
batch_size: boundedInt(req.body.batch_size, 32, 4096, 512),
ubatch_size: boundedInt(req.body.ubatch_size, 16, 4096, 128),
per_user_requests_per_minute: boundedInt(req.body.per_user_requests_per_minute, 1, 120, 6),
admin_bypass_rate_limit: req.body.admin_bypass_rate_limit === "on",
assistant_enabled: req.body.assistant_enabled === "on",
assistant_debug_logging: req.body.assistant_debug_logging === "on",
assistant_visibility: {
admins: req.body.visibility_admins === "on",
mods: req.body.visibility_mods === "on",
users: req.body.visibility_users === "on"
},
work_history_retention: {
mode: req.body.work_history_retention_mode === "age" ? "age" : "count",
count: boundedInt(req.body.work_history_retention_count, 50, 10000, 500),
age_value: boundedInt(req.body.work_history_retention_age_value, 1, 1000, 30),
age_unit: ["hours", "days", "weeks", "months", "years"].includes(req.body.work_history_retention_age_unit)
? req.body.work_history_retention_age_unit
: "days"
},
commands: {
enabled: req.body.command_enabled === "on",
triggers: parseCommandTriggers(req.body.command_triggers),
platforms: {
discord: req.body.command_platform_discord === "on",
twitch: req.body.command_platform_twitch === "on",
youtube: req.body.command_platform_youtube === "on",
kick: req.body.command_platform_kick === "on",
other: req.body.command_platform_other === "on"
},
roles: {
admins: req.body.command_role_admins === "on",
mods: req.body.command_role_mods === "on",
users: req.body.command_role_users === "on"
},
unavailable_message: cleanText(req.body.command_unavailable_message, 500),
denied_message: cleanText(req.body.command_denied_message, 500)
},
rate_limits: mergeLimits({
roles: {
admin: limitFromBody(req.body, "limit_role_admin", 30, 60),
mod: limitFromBody(req.body, "limit_role_mod", 12, 60),
user: limitFromBody(req.body, "limit_role_user", 4, 60)
},
platforms: {
webui: limitFromBody(req.body, "limit_platform_webui", 60, 60),
discord: limitFromBody(req.body, "limit_platform_discord", 30, 60),
twitch: limitFromBody(req.body, "limit_platform_twitch", 20, 60),
youtube: limitFromBody(req.body, "limit_platform_youtube", 20, 60),
kick: limitFromBody(req.body, "limit_platform_kick", 20, 60),
other: limitFromBody(req.body, "limit_platform_other", 20, 60)
},
per_user: limitFromBody(req.body, "limit_user", 2, 30),
per_channel: limitFromBody(req.body, "limit_channel", 12, 60),
queue_when_limited: req.body.queue_when_limited === "on"
}),
gate: {
...config.gate,
model_id: getModel(req.body.gate_model_id)?.id || config.gate.model_id,
context_size: requestedGateContext,
threads: boundedInt(req.body.gate_threads, 1, 16, 2),
timeout_ms: boundedInt(req.body.gate_timeout_ms, 1000, 5000, 3000),
high_confidence_threshold: boundedNumber(req.body.gate_high_confidence_threshold, 0.5, 0.99, 0.88),
main_llm_threshold: boundedNumber(req.body.gate_main_llm_threshold, 0.1, 0.95, 0.72),
predefined_enabled: req.body.gate_predefined_enabled === "on",
cache_ttl_seconds: boundedInt(req.body.gate_cache_ttl_seconds, 30, 604800, 3600),
repeat_force_window_seconds: boundedInt(req.body.gate_repeat_force_window_seconds, 0, 3600, 90),
similarity_threshold: boundedNumber(req.body.gate_similarity_threshold, 0.5, 1, 0.86),
force_prefix: cleanText(req.body.gate_force_prefix, 40)
},
support_scope: normalizeScope({
allowed_topics: cleanText(req.body.allowed_topics, 3000),
allowed_support_domains: cleanText(req.body.allowed_support_domains, 3000),
answer_style: cleanText(req.body.answer_style, 2000),
linking_behavior: cleanText(req.body.linking_behavior, 2000),
repo_lookup_enabled: req.body.repo_lookup_enabled === "on",
allow_deterministic_help_shortcuts: req.body.allow_deterministic_help_shortcuts === "on",
allow_moderator_code_help: req.body.allow_moderator_code_help === "on",
clarification_behavior: cleanText(req.body.clarification_behavior, 2000),
max_answer_length: boundedInt(req.body.maximum_answer_length, 100, 4000, 4000),
role_overrides: {
admin: cleanText(req.body.scope_admin, 3000),
mod: cleanText(req.body.scope_mod, 3000),
user: cleanText(req.body.scope_user, 3000)
}
}),
instructions: {
out_of_scope_response: cleanText(req.body.out_of_scope_response, 1000),
roleplay_intensity: boundedInt(req.body.roleplay_intensity, 0, 10, 0),
community_tone: cleanText(req.body.community_tone, 2000),
admin_custom: cleanText(req.body.admin_custom, 6000)
},
logging: {
log_prompts: req.body.log_prompts === "on",
log_responses: req.body.log_responses === "on",
log_tool_calls: req.body.log_tool_calls === "on",
log_metrics: req.body.log_metrics === "on",
log_internal_audit: req.body.log_internal_audit === "on"
},
improvement: {
...config.improvement,
allow_moderators_to_review_responses: req.body.allow_moderators_to_review_responses === "on",
trusted_moderator_reviewers: parseIdList(req.body.trusted_moderator_reviewers),
corrections_enabled: req.body.corrections_enabled === "on"
}
};
} catch (error) {
return flash(req, res, "error", error.message);
}
config = saveConfig(nextConfig);
metrics.configureRetention(config.work_history_retention);
registerAssistantCommands({
commandRouter,
provider,
runtime,
getConfig: () => config,
accessControl,
rateLimiter,
metrics,
getBaseUrl: () => configuredBaseUrl(settings)
});
writeCommandsManifest(plugin?.dir || __dirname, config);
const runtimeSettingsChanged = [
"selected_model_id",
"context_size",
"threads",
"batch_size",
"ubatch_size",
"gpu_allocation_intent_percent"
].some((key) => previousConfig[key] !== config[key]);
const gateSettingsChanged = JSON.stringify(previousConfig.gate) !== JSON.stringify(config.gate);
const enabledChanged = previousConfig.enabled !== config.enabled;
if ((runtimeSettingsChanged || gateSettingsChanged || enabledChanged) && runtime.status().state === "running") {
try {
await restartRuntimes();
} catch (error) {
return flash(req, res, "error", `Settings saved, but runtime restart failed: ${error.message}`);
}
} else if (!config.enabled && gateRuntime.status().state === "running") {
await gateRuntime.stop();
}
return flash(req, res, "success", "Lumi AI settings saved.");
});
router.post("/download/runtime", (req, res) => {
if (!req.session.user?.isAdmin) return denied(res);
const wantsJson = req.accepts(["json", "html"]) === "json";
const hardware = detectHardware(modelManifest.models, runtimeManifest);
const target = getRuntimeTarget(hardware);
if (!target) {
if (wantsJson) return res.status(400).json({ error: "No managed llama.cpp runtime is available for this platform." });
return flash(req, res, "error", "No managed llama.cpp runtime is available for this platform.");
}
try {
const job = downloads.start({
id: "runtime",
...target,
kind: "runtime",
archive: true,
runtimeMetadata: {
backend: target.backend || "cpu",
backend_variant: target.backend_variant || null,
version: runtimeManifest.version,
target: target.filename,
dependencies: Array.isArray(target.dependencies)
? target.dependencies.map((dependency) => ({
filename: dependency.filename,
sha256: dependency.sha256,
size: dependency.size || 0
}))
: []
},
beforeInstall: async () => {
runtimeInstallInProgress = true;
await stopRuntimes({ manual: true, reason: "runtime_reinstall" });
},
afterInstall: async () => {
runtimeInstallInProgress = false;
}
});
if (wantsJson) return res.json({ success: true, job });
return flash(req, res, "success", `${String(target.backend || "CPU").toUpperCase()} runtime download started. Lumi AI runtimes will stop when the new runtime is ready to install; start them again after the download completes.`);
} catch (error) {
if (wantsJson) return res.status(400).json({ error: error.message });
return flash(req, res, "error", error.message);
}
});
router.post("/download/model/:id", (req, res) => {
if (!req.session.user?.isAdmin) return denied(res);
const wantsJson = req.accepts(["json", "html"]) === "json";
const model = getModel(req.params.id);
if (!model) {
if (wantsJson) return res.status(404).json({ error: "Unknown model." });
return flash(req, res, "error", "Unknown model.");
}
if (
(model.id === config.selected_model_id && runtime.status().state === "running") ||
(model.id === config.gate.model_id && gateRuntime.status().state === "running")
) {
if (wantsJson) return res.status(400).json({ error: "Stop the AI runtimes before replacing an active model." });
return flash(req, res, "error", "Stop the AI runtimes before replacing an active model.");
}
const hardware = detectHardware(modelManifest.models);
const incompatible = model.ram_gb * 1024 > hardware.total_ram_mb || model.size / 1048576 > hardware.free_disk_mb;
if (incompatible && req.body.override_compatibility !== "on") {
if (wantsJson) return res.status(400).json({ error: "This model exceeds detected RAM or free disk. Check override to download anyway." });
return flash(req, res, "error", "This model exceeds detected RAM or free disk. Check override to download anyway.");
}
try {
const job = downloads.start({
id: `model:${model.id}`,
url: `https://huggingface.co/${model.repo}/resolve/${model.revision}/${model.filename}`,
filename: model.filename,
sha256: model.sha256,
size: model.size,
kind: "model"
});
if (wantsJson) return res.json({ success: true, job });
return flash(req, res, "success", `${model.label} download started.`);
} catch (error) {
if (wantsJson) return res.status(400).json({ error: error.message });
return flash(req, res, "error", error.message);
}
});
router.get("/api/status", async (req, res) => {
const permission = canUseAssistant({
user: req.session.user,
config,
origin: "webui",
platform: "webui",
requestedSurface: "webui_chat"
});
if (!permission.allowed) return res.status(403).json({ error: "Access denied.", reason: permission.reason });
res.json({
runtime: await runtime.health(),
gate: await gateRuntime.health(),
queue_length: queue.length,
enabled: config.enabled,
model_id: config.selected_model_id
});
});
router.get("/api/downloads", (req, res) => {
if (!req.session.user?.isAdmin) return res.status(403).json({ error: "Access denied." });
res.json(Object.fromEntries(downloads.jobs));
});
router.get("/api/gpu-capacity", (req, res) => {
if (!req.session.user?.isAdmin) return res.status(403).json({ error: "Access denied." });
const model = getModel(req.query.model_id || config.selected_model_id);
if (!model) return res.status(400).json({ error: "Unknown model." });
const contextSize = boundedInt(req.query.context_size, 512, 131072, config.context_size);
const hardware = detectHardware(modelManifest.models, runtimeManifest);
const installedBackend = runtime.runtimeMetadata().backend;
res.json(estimateAllocation({
model,
contextSize,
gpu: hardware.gpu,
backend: installedBackend,
intentPercent: req.query.intent_percent ?? config.gpu_allocation_intent_percent,
managedUsageMb: runtime.activeAcceleration?.estimated_gpu_memory_mb || 0
}));
});
router.get("/api/assistant-diagnostics", async (req, res) => {
if (!req.session.user?.isAdmin) return res.status(403).json({ error: "Access denied." });
const availability = evaluateAssistantAvailability({
user: req.session.user,
config,
model: getModel(config.selected_model_id),
runtimeHealth: await runtime.health(),
providerAvailable: Boolean(provider)
});
res.json({ ...availability, reason: describeAssistantReason(availability.reason_code) });
});
const visibilityDebugHandler = async (req, res) => {
if (!req.session.user?.isAdmin) return res.status(403).json({ error: "Access denied." });
const runtimeHealth = await runtime.health();
const diagnostics = buildVisibilityDiagnostics({
user: req.session.user,
config,
model: getModel(config.selected_model_id),
runtimeHealth,
providerAvailable: Boolean(provider),
frontend: frontendVisibility.get(req.session.user.id)
});
res.set("Cache-Control", "no-store");
return res.json({
...diagnostics,
reason: describeAssistantReason(diagnostics.reason_code),
frontend_reported: frontendVisibility.has(req.session.user.id),
...panelDiagnostics.snapshot()
});
};
const visibilityReportHandler = (req, res) => {
if (!req.session.user?.isAdmin) return res.status(403).json({ error: "Access denied." });
frontendVisibility.set(req.session.user.id, {
assistant_slot_found: Boolean(req.body.assistant_slot_found),
frontend_loader_loaded: Boolean(req.body.frontend_loader_loaded),
panel_html_returned: Boolean(req.body.panel_html_returned),
mount_successful: Boolean(req.body.mount_successful),
reported_at: new Date().toISOString()
});
panelDiagnostics.frontend(req.body);
return res.json({ success: true });
};
router.get("/api/assistant/visibility-debug", visibilityDebugHandler);
router.post("/api/assistant/visibility-debug", visibilityReportHandler);
if (typeof web.addRoute === "function") {
web.addRoute("get", "/api/lumi-ai/assistant/visibility-debug", visibilityDebugHandler);
web.addRoute("post", "/api/lumi-ai/assistant/visibility-debug", visibilityReportHandler);
}
router.post("/repo-index/refresh", (req, res) => {
if (!req.session.user?.isAdmin) return denied(res);
try {
const index = req.body.source === "public"
? repoIndexer.refreshPublicIndex()
: repoIndexer.refreshIndex();
return flash(req, res, "success", `Repository support index refreshed with ${index.routes.length} routes.`);
} catch (error) {
return flash(req, res, "error", `Repository index refresh failed: ${error.message}`);
}
});
router.post("/access-control", (req, res) => {
if (!req.session.user?.isAdmin) return denied(res);
try {
accessControl.set(cleanText(req.body.user_id, 200), {
action: req.body.action,
timeoutUntil: req.body.timeout_until,
reason: cleanText(req.body.reason, 500),
silent: req.body.silent === "on",
actorId: req.session.user.id
});
return flash(req, res, "success", "AI user access updated.");
} catch (error) {
return flash(req, res, "error", error.message);
}
});
router.post("/models/:id/delete", (req, res) => {
if (!req.session.user?.isAdmin) return denied(res);
try {
const result = storage.deleteModel(getModel(req.params.id), {
selectedModelId: config.selected_model_id,
gateModelId: config.gate.model_id,
runtimeRunning: runtime.status().state === "running",
gateRuntimeRunning: gateRuntime.status().state === "running",
confirmed: req.body.confirm === "yes"
});
return flash(req, res, "success", result.deleted ? `Model deleted. Recovered ${formatBytes(result.bytes_recovered)}.` : "Model was not installed.");
} catch (error) {
return flash(req, res, "error", error.message);
}
});
router.post("/models/:id/verify", async (req, res) => {
if (!req.session.user?.isAdmin) return denied(res);
const result = await runtime.verifyModel(req.params.id);
return flash(req, res, result.success ? "success" : "error", result.success ? "Model verification passed." : result.message);
});
router.post("/storage/cleanup", (req, res) => {
if (!req.session.user?.isAdmin) return denied(res);
try {
const categories = Array.isArray(req.body.categories) ? req.body.categories : req.body.categories ? [req.body.categories] : [];
const activeDownload = [...downloads.jobs.values()].some((job) => !["complete", "error"].includes(job.state));
if (activeDownload && categories.some((category) => ["tmp", "runtime_archives"].includes(category))) {
throw new Error("Wait for active downloads before cleaning temporary files or runtime archives.");
}
const result = storage.cleanupStorage(categories, {
models: modelManifest.models,
selectedModelId: config.selected_model_id,
gateModelId: config.gate.model_id,
runtimeRunning: runtime.status().state === "running" || gateRuntime.status().state === "running",
activeLogPath: runtime.activeLogPath
});
return flash(req, res, "success", `Storage cleanup recovered ${formatBytes(result.recovered_bytes)}.`);
} catch (error) {
return flash(req, res, "error", error.message);
}
});
router.get("/logs/:name", (req, res) => {
if (!req.session.user?.isAdmin) return denied(res);
try {
const log = storage.readLogTail(req.params.name);
return res.render(path.join(__dirname, "views", "log-viewer.ejs"), {
title: `Lumi AI log - ${log.name}`,
log,
formatBytes,
formatDate
});
} catch (error) {
return res.status(404).render("error", { title: "Log unavailable", message: error.message });
}
});
router.get("/logs/:name/download", (req, res) => {
if (!req.session.user?.isAdmin) return res.status(403).json({ error: "Access denied." });
try { return res.download(storage.resolveLog(req.params.name)); }
catch (error) { return res.status(404).json({ error: error.message }); }
});
router.post("/logs/:name/delete", (req, res) => {
if (!req.session.user?.isAdmin) return denied(res);
try {
const result = storage.deleteLog(req.params.name, runtime.activeLogPath);
return flash(req, res, "success", `Log deleted. Recovered ${formatBytes(result.bytes_recovered)}.`);
} catch (error) {
return flash(req, res, "error", error.message);
}
});
router.post("/runtime/:action", async (req, res) => {
if (!req.session.user?.isAdmin) return res.status(403).json({ error: "Access denied." });
try {
const action = req.params.action;
if (!["start", "stop", "restart", "self-test", "verify-runtime", "verify-model", "verify-gate-model"].includes(action)) throw new Error("Unknown runtime action.");
const result = action === "self-test" ? await runtime.selfTest()
: action === "verify-runtime" ? runtime.verifyRuntimeInstallation()
: action === "verify-model" ? await runtime.verifyModel()
: action === "verify-gate-model" ? await gateRuntime.verifyModel()
: action === "stop"
? await stopRuntimes({ manual: true, reason: "admin_stop" })
: action === "restart" ? await restartRuntimes() : await startRuntimes();
if (result?.success === false) return res.status(400).json({ error: result.message, diagnostic: result });
res.json(result);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
router.get("/diagnostics/download", (req, res) => {
if (!req.session.user?.isAdmin) return res.status(403).json({ error: "Access denied." });
const file = createDiagnosticsBundle({
config,
runtimeState: getRuntimeState(),
manifest: {
runtime: runtimeManifest,
model: getModel(config.selected_model_id),
gate_model: getModel(config.gate.model_id)
},
metrics: metrics.report()
});
return res.download(file);
});
router.post("/assistant/message", async (req, res) => {
const permission = canUseAssistant({
user: req.session.user,
config,
origin: "webui",
platform: "webui",
requestedSurface: "webui_chat"
});
if (!permission.allowed) {
return res.status(403).json({
error: "Lumi AI is unavailable for this account.",
reason: permission.reason,
permission: permission.debug_details
});
}
const message = cleanText(req.body.message, 6000);
if (!message) return res.status(400).json({ error: "Message is required." });
const originContext = webOriginContext(req);
const access = authorizeAiRequest({ userId: req.session.user.id, context: originContext, accessControl, rateLimiter });
if (!access.allowed) return res.status(429).json({ error: access.message, reason: access.reason, retry_after_seconds: access.retry_after_seconds });
const requestUser = { ...req.session.user };
const requestRole = roleOf(requestUser);
const requestConfig = config;
const requestSessionId = req.sessionID;
const history = normalizeConversationHistory(req.body.history);
const requestStarted = Date.now();
const job = requestJobs.create({
userId: requestUser.id,
metadata: {
context_size: requestConfig.context_size,
batch_size: requestConfig.batch_size,
ubatch_size: requestConfig.ubatch_size,
threads: requestConfig.threads
},
execute: async (updateStage, signal) => {
try {
const result = await provider.generate({
message,
user: requestUser,
sessionId: requestSessionId,
originContext,
allowDeterministicShortcut: requestConfig.support_scope.allow_deterministic_help_shortcuts,
history,
signal,
onStage: updateStage
});
updateStage("formatting", { route: result.route_used, ...(result.diagnostics || {}) });
const delivered = finalizeAssistantResult(result, {
role: requestRole,
config: requestConfig,
baseUrl: originContext.base_url,
maxLength: originContext.max_message_length,
requestMessage: message
});
metrics.record({
kind: "delivery",
status: "success",
scope: "webui_chat",
route_used: result.route_used || "llm",
route_class: result.route_class,
max_output_tokens_used: result.max_output_tokens_used,
role: requestRole,
user_id: requestUser.id,
internal_generated_length: result.internal_generated_length || String(result.text || "").length,
final_reply_length: delivered.original_final_length,
original_final_length: delivered.original_final_length,
delivered_length: delivered.delivered_length,
final_tokens: estimateTokenCountFromLength(delivered.original_final_length),
delivered_tokens: estimateTokenCountFromLength(delivered.delivered_length),
truncated: delivered.truncated,
delivery_action: delivered.delivery_action,
...controllerDeliveryFields(result),
...(result.stage_timings || {})
});
return {
...delivered,
diagnostics: result.diagnostics || null,
feedback_context: {
user_message: message,
assistant_answer: delivered.text,
route_used: result.route_used || "main_llm",
role: requestRole,
origin: originContext.origin,
platform: originContext.platform,
model: result.model_id || requestConfig.selected_model_id,
timestamp: new Date().toISOString()
}
};
} catch (error) {
metrics.record({
kind: "request",
status: "failed",
user_id: requestUser.id,
role: requestRole,
message: error.message,
timeout: error.name === "TimeoutError" || error.code === "HARD_GENERATION_TIMEOUT",
cancelled: error.code === "REQUEST_CANCELLED",
total_ms: Date.now() - requestStarted,
duration_ms: Date.now() - requestStarted
});
if (error.code === "QUEUE_FULL" && !error.retry_after_seconds) error.retry_after_seconds = 5;
throw error;
}
}
});
return res.status(202).json({
job_id: job.id,
state: job.state,
stage: job.stage,
status_url: `/plugins/${PLUGIN_ID}/assistant/jobs/${job.id}`,
cancel_url: `/plugins/${PLUGIN_ID}/assistant/jobs/${job.id}/cancel`,
soft_timeout_url: `/plugins/${PLUGIN_ID}/assistant/jobs/${job.id}/soft-timeout`,
ui_soft_timeout_ms: requestConfig.ui_soft_timeout_ms
});
});
router.post("/assistant/feedback", (req, res) => {
const permission = canUseAssistant({
user: req.session.user,
config,
origin: "webui",
platform: "webui",
requestedSurface: "webui_chat"
});
if (!permission.allowed) return res.status(403).json({ error: "Access denied." });
try {
const entry = feedbackStore.capture({
user_message: req.body.user_message,
assistant_answer: req.body.assistant_answer,
route_used: req.body.route_used,
role: roleOf(req.session.user),
origin: "webui",
platform: "webui",
model: req.body.model,
timestamp: req.body.timestamp,
feedback_tag: req.body.feedback_tag,
feedback_kind: req.body.feedback_kind,
optional_correction: req.body.optional_correction
}, req.session.user);
return res.status(201).json({ success: true, id: entry.id });
} catch (error) {
return res.status(400).json({ error: error.message });
}
});
router.get("/assistant/jobs/:id", (req, res) => {
const permission = canUseAssistant({
user: req.session.user,
config,
origin: "webui",
platform: "webui",
requestedSurface: "webui_chat"
});
if (!permission.allowed) return res.status(403).json({ error: "Access denied." });
const job = requestJobs.get(req.params.id, req.session.user.id);
if (!job) return res.status(404).json({ error: "Assistant request was not found or expired." });
return res.json(job);
});
router.post("/assistant/jobs/:id/cancel", (req, res) => {
const permission = canUseAssistant({
user: req.session.user,
config,
origin: "webui",
platform: "webui",
requestedSurface: "webui_chat"
});
if (!permission.allowed) return res.status(403).json({ error: "Access denied." });
const job = requestJobs.cancel(req.params.id, req.session.user.id, {
admin: Boolean(req.session.user?.isAdmin)
});
if (!job) return res.status(404).json({ error: "Assistant request was not found or expired." });
metrics.record({
kind: "request_job",
status: "cancelled",
job_id: job.id,
user_id: req.session.user.id,
stage: job.stage,
elapsed_ms: job.elapsed_ms
});
return res.json(job);
});
router.post("/assistant/jobs/:id/soft-timeout", (req, res) => {
const permission = canUseAssistant({
user: req.session.user,
config,
origin: "webui",
platform: "webui",
requestedSurface: "webui_chat"
});
if (!permission.allowed) return res.status(403).json({ error: "Access denied." });
const job = requestJobs.markSoftTimeout(req.params.id, req.session.user.id);
if (!job) return res.status(404).json({ error: "Assistant request was not found or expired." });
metrics.record({
kind: "request_job",
status: "soft_timeout",
job_id: job.id,
user_id: req.session.user.id,
stage: job.stage,
elapsed_ms: job.elapsed_ms,
still_running: job.still_running
});
return res.json(job);
});
router.post("/assistant/test", async (req, res) => {
if (!req.session.user?.isAdmin) return res.status(403).json({ error: "Access denied." });
const message = cleanText(req.body.message, 6000);
if (!message) return res.status(400).json({ error: "Message is required." });
const simulatedRole = ["admin", "mod", "user"].includes(req.body.role) ? req.body.role : "admin";
const simulatedUser = {
id: req.session.user.id,
username: req.session.user.username,
isAdmin: simulatedRole === "admin",
isMod: simulatedRole === "mod"
};
const allowTools = req.body.allow_tools === true;
const testOrigin = ["webui", "discord", "twitch", "youtube", "kick", "other"].includes(req.body.origin)
? req.body.origin
: "webui";
try {
const result = await provider.test({
message,
user: simulatedUser,
includeRaw: Boolean(req.body.show_raw_output),
allowTools,
originContext: diagnosticOriginContext(simulatedUser, testOrigin)
});
if (!req.body.show_raw_prompt) delete result.raw_prompt;
res.json(result);
} catch (error) {
res.status(503).json({ error: error.message });
}
});
router.post("/assistant/confirm", async (req, res) => {
const permission = canUseAssistant({
user: req.session.user,
config,
origin: "webui",
platform: "webui",
requestedSurface: "webui_chat"
});
if (!permission.allowed) return res.status(403).json({ error: "Access denied.", reason: permission.reason });
try { res.json({ success: true, result: await tools.confirm({ id: req.body.id, user: req.session.user, sessionId: req.sessionID }) }); }
catch (error) { res.status(400).json({ error: error.message }); }
});
router.post("/assistant/cancel", (req, res) => {
const permission = canUseAssistant({
user: req.session.user,
config,
origin: "webui",
platform: "webui",
requestedSurface: "webui_chat"
});
if (!permission.allowed) return res.status(403).json({ error: "Access denied.", reason: permission.reason });
const cancelled = tools.cancel(req.body.id, req.session.user.id);
metrics.record({ kind: "tool", status: cancelled ? "cancelled" : "failed", user_id: req.session.user.id });
res.json({ success: cancelled });
});
router.get("/api/tools", async (req, res) => {
if (!req.session.user?.isAdmin) return res.status(403).json({ error: "Access denied." });
try {
return res.json(await toolManager.list({ force: req.query.refresh === "1" }));
} catch (error) {
return res.status(503).json({ error: error.message });
}
});
router.get("/api/tools-diagnostics", async (req, res) => {
if (!req.session.user?.isAdmin) return res.status(403).json({ error: "Access denied." });
const role = ["admin", "mod", "user"].includes(req.query.role) ? req.query.role : "admin";
const origin = ["webui", "discord", "twitch", "youtube", "kick", "other"].includes(req.query.origin)
? req.query.origin
: "webui";
const simulatedUser = {
id: req.session.user.id,
username: req.session.user.username,
isAdmin: role === "admin",
isMod: role === "mod"
};
try {
const diagnostics = await toolManager.diagnostics({
role,
user: simulatedUser,
context: diagnosticOriginContext(simulatedUser, origin)
});
return res.json({
...diagnostics,
prompt_preview: buildAllowedToolsSection(diagnostics.prompt_tools)
});
} catch (error) {
return res.status(503).json({ error: error.message });
}
});
router.get("/api/tools/:id/readme", async (req, res) => {
if (!req.session.user?.isAdmin) return res.status(403).json({ error: "Access denied." });
try {
return res.json(await toolManager.readme(req.params.id));
} catch (error) {
return res.status(404).json({ error: error.message });
}
});
router.get("/api/tools/:id/settings", (req, res) => {
if (!req.session.user?.isAdmin) return res.status(403).json({ error: "Access denied." });
try {
return res.json(toolManager.settingsFor(req.params.id));
} catch (error) {
return res.status(404).json({ error: error.message });
}
});
router.post("/api/tools/:id/settings", express.json({ limit: "64kb" }), async (req, res) => {
if (!req.session.user?.isAdmin) return res.status(403).json({ error: "Access denied." });
try {
return res.json(await toolManager.saveSettings(req.params.id, req.body?.values || {}));
} catch (error) {
return res.status(400).json({ error: error.message });
}
});
router.post("/api/tools/:id/settings/reset", async (req, res) => {
if (!req.session.user?.isAdmin) return res.status(403).json({ error: "Access denied." });
try {
return res.json(await toolManager.resetSettings(req.params.id));
} catch (error) {
return res.status(400).json({ error: error.message });
}
});
router.get("/tools/:id/assets/*", (req, res) => {
if (!req.session.user?.isAdmin) {
const permission = canUseAssistant({
user: req.session.user,
config,
origin: "webui",
platform: "webui",
requestedSurface: "webui_chat"
});
if (!permission.allowed) return res.status(403).end();
}
const file = toolManager.resolveAsset(req.params.id, req.params[0], {
allowInstalled: req.session.user?.isAdmin === true
});
return file ? res.sendFile(file) : res.status(404).end();
});
router.post("/tools/:id/enable", async (req, res) => {
if (!req.session.user?.isAdmin) return toolDenied(req, res);
try {
const result = await toolManager.enable(req.params.id);
return toolActionResponse(req, res, "AI tool enabled.", result);
} catch (error) {
return toolActionError(req, res, error);
}
});
router.post("/tools/:id/disable", async (req, res) => {
if (!req.session.user?.isAdmin) return toolDenied(req, res);
try {
const result = await toolManager.disable(req.params.id);
return toolActionResponse(req, res, "AI tool disabled.", result);
} catch (error) {
return toolActionError(req, res, error);
}
});
router.post("/tools/:id/update", async (req, res) => {
if (!req.session.user?.isAdmin) return toolDenied(req, res);
try {
const result = await toolManager.update(req.params.id);
return toolActionResponse(req, res, "AI tool updated.", result);
} catch (error) {
return toolActionError(req, res, error);
}
});
router.post("/tools/:id/delete", async (req, res) => {
if (!req.session.user?.isAdmin) return toolDenied(req, res);
try {
const result = await toolManager.delete(req.params.id);
return toolActionResponse(req, res, "AI tool deleted.", result);
} catch (error) {
return toolActionError(req, res, error);
}
});
router.get("/improvement_center", (req, res) => {
const access = improvementAccess(req.session.user, config);
if (!access.allowed) return deniedImprovement(res);
return res.render(path.join(__dirname, "views", "improvement-center.ejs"), {
title: "Lumi AI Improvement Center",
config,
access,
feedbackTags: FEEDBACK_TAGS,
feedbackKinds: FEEDBACK_KINDS,
promotionTargets: PROMOTION_TARGETS,
reviews: feedbackStore.list({
page: req.query.review_page,
pageSize: 15,
status: cleanText(req.query.status, 30)
}),
corrections: correctionStore.list({ page: req.query.correction_page, pageSize: 15 }),
evalCases: evalStore.list({ page: req.query.eval_page, pageSize: 15 }),
evalResults: evalStore.results(25),
formatDate
});
});
router.post("/improvement_center/settings", (req, res) => {
const access = improvementAccess(req.session.user, config);
if (!access.can_approve) return deniedImprovement(res);
config = saveConfig({
...config,
improvement: {
...config.improvement,
allow_moderators_to_review_responses: req.body.allow_moderators_to_review_responses === "on",
trusted_moderator_reviewers: parseIdList(req.body.trusted_moderator_reviewers),
corrections_enabled: req.body.corrections_enabled === "on"
}
});
ensureSidebarNavItem(settings);
return improvementFlash(req, res, "success", "Improvement Center settings saved.");
});
router.post("/improvement_center/reviews/:id", (req, res) => {
const access = improvementAccess(req.session.user, config);
if (!access.allowed) return deniedImprovement(res);
try {
const action = cleanText(req.body.action, 30);
if (action === "flag" && access.can_flag) {
feedbackStore.setStatus(req.params.id, "flagged", req.session.user, req.body.review_notes);
} else if (action === "verify" && access.can_verify) {
feedbackStore.verify(req.params.id, req.session.user, req.body.review_notes);
} else if (action === "approve" && access.can_approve) {
feedbackStore.setStatus(req.params.id, "approved", req.session.user, req.body.review_notes);
} else if (action === "reject" && access.can_approve) {
feedbackStore.setStatus(req.params.id, "rejected", req.session.user, req.body.review_notes);
} else if (action === "edit" && access.can_edit) {
feedbackStore.edit(req.params.id, req.body, req.session.user);
} else if (action === "export" && access.can_export) {
feedbackStore.markExportApproved(req.params.id, req.session.user);
} else if (action === "delete" && access.can_delete) {
feedbackStore.delete(req.params.id);
} else {
return deniedImprovement(res);
}
return improvementFlash(req, res, "success", `Review ${action} completed.`);
} catch (error) {
return improvementFlash(req, res, "error", error.message);
}
});
router.post("/improvement_center/reviews/:id/implement", (req, res) => {
const access = improvementAccess(req.session.user, config);
if (!access.can_implement) return deniedImprovement(res);
try {
const review = feedbackStore.get(req.params.id);
if (!review || review.status !== "approved") throw new Error("Approve the review before implementing it.");
const target = PROMOTION_TARGETS.includes(req.body.target) ? req.body.target : "correction";
if (target === "eval_case") {
evalStore.add({
prompt: review.user_message,
role: req.body.min_role || review.role,
origin: req.body.permission_origin || review.origin,
expected_behavior: req.body.corrected_answer || review.optional_correction,
forbidden_behavior: req.body.forbidden_behavior,
expected_link: req.body.expected_link,
notes: req.body.notes
}, req.session.user);
} else if (target === "training_export") {
feedbackStore.markExportApproved(review.id, req.session.user);
} else {
correctionStore.createFromFeedback(review, {
...req.body,
target,
explicitly_safe: req.body.explicitly_safe === "on",
enabled: req.body.enabled === "on"
}, req.session.user);
}
return improvementFlash(req, res, "success", "Approved feedback was promoted. Save Corrections before it becomes active.");
} catch (error) {
return improvementFlash(req, res, "error", error.message);
}
});
router.post("/improvement_center/corrections/save", (req, res) => {
const access = improvementAccess(req.session.user, config);
if (!access.can_implement) return deniedImprovement(res);
const result = correctionStore.saveCorrections(req.session.user);
return improvementFlash(req, res, "success", `Corrections saved. ${result.active} of ${result.total} are active.`);
});
router.post("/improvement_center/corrections/:id", (req, res) => {
const access = improvementAccess(req.session.user, config);
if (!access.allowed) return deniedImprovement(res);
try {
const action = cleanText(req.body.action, 30);
if (action === "verify" && access.can_verify) {
correctionStore.verify(req.params.id, req.session.user);
} else if (action === "edit" && access.can_edit) {
if (req.body.expected_link && !isVerifiedImprovementLink(req.body.expected_link)) {
throw new Error("Internal correction links must match a verified Lumi route.");
}
correctionStore.update(req.params.id, {
...req.body,
explicitly_safe: req.body.explicitly_safe === "on",
enabled: req.body.enabled === "on"
});
} else if (action === "toggle" && access.can_edit) {
correctionStore.setEnabled(req.params.id, req.body.enabled === "on");
} else if (action === "delete" && access.can_delete) {
correctionStore.delete(req.params.id);
} else {
return deniedImprovement(res);
}
return improvementFlash(req, res, "success", `Correction ${action} completed. Save Corrections to activate changes.`);
} catch (error) {
return improvementFlash(req, res, "error", error.message);
}
});
router.post("/improvement_center/evals", (req, res) => {
const access = improvementAccess(req.session.user, config);
if (!access.can_run_evals) return deniedImprovement(res);
try {
if (req.body.expected_link && !isVerifiedImprovementLink(req.body.expected_link)) {
throw new Error("Expected links must match a verified Lumi route.");
}
evalStore.add(req.body, req.session.user);
return improvementFlash(req, res, "success", "Eval case added.");
} catch (error) {
return improvementFlash(req, res, "error", error.message);
}
});
router.post("/improvement_center/evals/:id/delete", (req, res) => {
const access = improvementAccess(req.session.user, config);
if (!access.can_run_evals) return deniedImprovement(res);
evalStore.delete(req.params.id);
return improvementFlash(req, res, "success", "Eval case deleted.");
});
router.post("/improvement_center/evals/run", async (req, res) => {
const access = improvementAccess(req.session.user, config);
if (!access.can_run_evals) return deniedImprovement(res);
try {
const results = await evalStore.runAll({ provider, actor: req.session.user });
return improvementFlash(req, res, "success", `Eval run completed with ${results.length} result(s).`);
} catch (error) {
return improvementFlash(req, res, "error", error.message);
}
});
router.post("/improvement_center/exports/:format", (req, res) => {
const access = improvementAccess(req.session.user, config);
if (!access.can_export) return deniedImprovement(res);
try {
const output = trainingExporter.export(req.params.format);
return res.download(output.file, output.filename);
} catch (error) {
return improvementFlash(req, res, "error", error.message);
}
});
web.mount(`/plugins/${PLUGIN_ID}`, router, {
label: "Lumi AI",
role: "admin",
section: "plugins"
});
web.addNavItem({
label: "AI Improvement Center",
path: `/plugins/${PLUGIN_ID}/improvement_center`,
role: "mod",
section: "moderation",
canAccess: (user) => improvementAccess(user, config).allowed
});
let removeAssistantPanel = () => {};
if (typeof web.addAssistantPanel === "function") {
removeAssistantPanel = web.addAssistantPanel({
id: PLUGIN_ID,
version: require("./plugin.json").version,
canAccess: (user) => canUseAssistant({
user,
config,
origin: "webui",
platform: "webui",
requestedSurface: "webui_panel"
}),
getAvailability: async (user) => evaluateAssistantAvailability({
user,
config,
model: getModel(config.selected_model_id),
runtimeHealth: await runtime.health(),
providerAvailable: Boolean(provider),
origin: "webui",
platform: "webui",
requestedSurface: "webui_panel"
}),
view: path.join(__dirname, "views", "assistant-panel.ejs"),
requiredLocals: ["endpoint", "user"],
onRenderDiagnostic: (report) => {
panelDiagnostics.update(report);
if (report.panel_html_error) panelDiagnostics.log("render_failure", report);
},
stylesheet: `/plugins/${PLUGIN_ID}/assets/assistant.css`,
script: `/plugins/${PLUGIN_ID}/assets/assistant.js`,
getDebug: () => ({
enabled: Boolean(config.assistant_debug_logging),
...panelDiagnostics.snapshot()
}),
locals: { endpoint: `/plugins/${PLUGIN_ID}` }
});
} else {
console.warn("Lumi AI assistant panel hook is unavailable; settings remain accessible.");
}
ensureSidebarNavItem(settings);
registerAssistantCommands({
commandRouter,
provider,
runtime,
getConfig: () => config,
accessControl,
rateLimiter,
metrics,
getBaseUrl: () => configuredBaseUrl(settings)
});
writeCommandsManifest(plugin?.dir || __dirname, config);
setImmediate(() => toolManager.loadEnabled().catch((error) =>
console.error("Lumi AI tool loader failed", error)
));
if (config.enabled) {
setImmediate(() => ensureGateRuntime().catch((error) =>
console.error("Lumi AI gate runtime start failed", error)
));
}
const state = getRuntimeState();
if (shouldAutoResume(config, state)) {
setImmediate(() => startRuntimes({ resume: true }).catch((error) => console.error("Lumi AI runtime resume failed", error)));
}
return async () => {
clearInterval(gateMonitor);
removeAssistantPanel();
commandRouter?.clearCommands?.(PLUGIN_ID);
await toolManager.stopAll();
await stopRuntimes({ manual: false, reason: "bot_shutdown" });
if (global.lumiFrameworks?.ai === api) delete global.lumiFrameworks.ai;
if (global.lumiFrameworks?.lumi_ai === api) delete global.lumiFrameworks.lumi_ai;
};
}
};
function getRuntimeTarget(hardware = detectHardware(modelManifest.models, runtimeManifest)) {
const selection = hardware.runtime_selection;
return selection?.target ? { ...selection.target, backend: selection.backend } : null;
}
function normalizeContext(value) {
if (Array.isArray(value)) return value.filter((item) => typeof item === "string");
return typeof value === "string" ? [value] : [];
}
function safeContextDiagnostic(value) {
const row = value && typeof value === "object" ? value : {};
return {
provider: cleanText(row.provider || row.kind, 40),
kind: cleanText(row.kind, 40),
query: cleanText(row.query, 500),
candidate_count: boundedInt(row.candidate_count, 0, 1000, 0),
returned_count: boundedInt(row.returned_count, 0, 1000, 0),
limit: boundedInt(row.limit, 0, 1000, 0),
okf_retrieval: cleanText(row.okf_retrieval, 40),
reason: cleanText(row.reason, 160)
};
}
function boundedInt(value, min, max, fallback) {
const number = Number.parseInt(value, 10);
return Number.isFinite(number) ? Math.min(max, Math.max(min, number)) : fallback;
}
function boundedNumber(value, min, max, fallback) {
const number = Number(value);
return Number.isFinite(number) ? Math.min(max, Math.max(min, number)) : fallback;
}
function cleanText(value, max) {
return String(value || "").trim().slice(0, max);
}
function flash(req, res, type, message) {
req.session.flash = { type, message };
return res.redirect(`/plugins/${PLUGIN_ID}`);
}
function toolActionResponse(req, res, message, result = {}) {
if (req.accepts(["json", "html"]) === "json") return res.json({ success: true, message, result });
req.session.flash = { type: "success", message };
return res.redirect(`/plugins/${PLUGIN_ID}`);
}
function toolActionError(req, res, error) {
if (req.accepts(["json", "html"]) === "json") return res.status(400).json({ error: error.message });
req.session.flash = { type: "error", message: error.message };
return res.redirect(`/plugins/${PLUGIN_ID}`);
}
function toolDenied(req, res) {
if (req.accepts(["json", "html"]) === "json") return res.status(403).json({ error: "Access denied." });
return denied(res);
}
function improvementFlash(req, res, type, message) {
req.session.flash = { type, message };
return res.redirect(`/plugins/${PLUGIN_ID}/improvement_center`);
}
function denied(res) {
return res.status(403).render("error", { title: "Access denied", message: "Administrator access is required." });
}
function deniedImprovement(res) {
return res.status(403).render("error", {
title: "Access denied",
message: "Improvement Center access is not enabled for this account."
});
}
function parseIdList(value) {
return [...new Set(String(value || "")
.split(/[\s,;]+/)
.map((entry) => entry.trim())
.filter(Boolean))]
.slice(0, 250);
}
function isVerifiedImprovementLink(value) {
const cleaned = cleanText(value, 2000).replace(/^(?:GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s+/i, "");
if (!cleaned) return true;
let pathname;
try {
const url = new URL(cleaned, "https://lumi.invalid");
if (!["http:", "https:"].includes(url.protocol)) return false;
pathname = url.pathname;
} catch {
return false;
}
return repoIndexer.verifiedRoutePaths().includes(pathname);
}
function formatDuration(ms) {
if (!ms) return "0 ms";
return ms < 1000 ? `${ms} ms` : `${(ms / 1000).toFixed(1)} s`;
}
function formatDate(value) {
const date = new Date(value);
return Number.isNaN(date.getTime()) ? "-" : date.toLocaleString();
}
function describeAssistantReason(code) {
return ({
anonymous: "User is not logged in.",
feature_disabled: "AI Assistant is disabled.",
role_forbidden: "The current role is not enabled for the assistant.",
model_not_selected: "No model is selected.",
model_missing: "The selected model is not installed.",
runtime_missing: "The managed runtime is not installed.",
runtime_unusable: "The runtime self-test has not passed.",
runtime_stopped: "The runtime is stopped.",
runtime_starting: "The runtime is starting but not ready.",
runtime_error: "The runtime is in an error state.",
runtime_unhealthy: "The runtime health check failed.",
assistant_disabled: "The sidebar assistant is disabled.",
provider_unavailable: "The AI provider is unavailable."
})[code] || (code ? `Assistant unavailable: ${code}.` : "Assistant is available.");
}
function ensureSidebarNavItem(settings) {
if (!settings?.getSetting || !settings?.setSetting) return;
const raw = settings.getSetting("nav_structure", null);
if (!raw) return;
let structure = raw;
if (typeof structure === "string") {
try { structure = JSON.parse(structure); } catch { return; }
}
if (!structure?.enabled || !Array.isArray(structure.sections)) return;
const navId = "plugins_lumi_ai";
const improvementNavId = "plugins_lumi_ai_improvement_center";
for (const section of structure.sections) {
if (Array.isArray(section.items)) {
section.items = section.items.filter((item) => ![navId, improvementNavId].includes(item));
}
}
let plugins = structure.sections.find((section) => section.id === "plugins");
if (!plugins) {
plugins = { id: "plugins", label: "Plugins", icon: "blocks", items: [] };
structure.sections.push(plugins);
}
plugins.items = Array.isArray(plugins.items) ? plugins.items : [];
plugins.items.push(navId);
let moderation = structure.sections.find((section) => section.id === "moderation");
if (!moderation) {
moderation = { id: "moderation", label: "Mod", icon: "shield", items: [] };
structure.sections.push(moderation);
}
moderation.items = Array.isArray(moderation.items) ? moderation.items : [];
moderation.items.push(improvementNavId);
settings.setSetting("nav_structure", structure);
}
function shouldAutoResume(config, state) {
return Boolean(config.enabled && state.desired_state === "running" && !state.last_manual_stop && !state.last_crashed);
}
function registerAssistantCommands({
commandRouter,
provider,
runtime,
getConfig,
accessControl,
rateLimiter,
metrics: commandMetrics = metrics,
getBaseUrl = () => ""
}) {
if (!commandRouter) return;
const config = getConfig();
if (!config.commands.enabled) {
commandRouter.registerCommands(PLUGIN_ID, []);
return;
}
const platforms = Object.entries(config.commands.platforms)
.filter(([, enabled]) => enabled)
.map(([platform]) => platform);
commandRouter.registerCommands(PLUGIN_ID, [{
id: "lumi_ai:assistant",
triggers: config.commands.triggers,
platforms,
handler: async (ctx) => {
const current = getConfig();
const originContext = buildOriginContext(ctx, ctx.trigger);
originContext.base_url = getBaseUrl();
const started = Date.now();
const user = {
id: originContext.user_id,
username: originContext.username,
isAdmin: originContext.is_admin,
isMod: originContext.is_mod
};
const permission = canUseAssistant({
user,
config: current,
origin: originContext.origin,
platform: originContext.platform,
requestedSurface: "command",
roleHint: originContext.role,
roleSource: "command_origin"
});
if (!permission.allowed) {
commandMetrics.record({
kind: "request",
status: "refused",
scope: "platform_command",
route_used: "denial",
role: permission.normalized_role,
user_id: originContext.user_id,
platform: originContext.platform,
reason: permission.reason,
duration_ms: Date.now() - started
});
if (permission.reason === "command_disabled" || permission.reason === "platform_forbidden") return false;
await ctx.reply(formatPlatformReply(current.commands.denied_message, [], originContext));
return true;
}
if (!ctx.argsText.trim()) {
await ctx.reply(`Usage: !${ctx.trigger} <question>`);
return true;
}
const access = authorizeAiRequest({
userId: originContext.user_id,
context: originContext,
accessControl,
rateLimiter
});
if (!access.allowed) {
commandMetrics.record({
kind: "request",
status: "refused",
scope: "platform_command",
route_used: "denial",
role: permission.normalized_role,
user_id: originContext.user_id,
platform: originContext.platform,
reason: access.reason,
duration_ms: Date.now() - started
});
if (!access.silent) {
const message = access.reason === "rate_limited"
? `Lumi Assistant is rate limited. Try again in ${access.retry_after_seconds}s.`
: current.commands.denied_message;
await ctx.reply(formatPlatformReply(message, [], originContext));
}
return true;
}
const health = await runtime.health();
if (!current.enabled || health.state !== "running" || !health.healthy) {
commandMetrics.record({
kind: "request",
status: "failed",
scope: "platform_command",
route_used: "unavailable_fallback",
role: permission.normalized_role,
user_id: originContext.user_id,
platform: originContext.platform,
duration_ms: Date.now() - started
});
await ctx.reply(formatPlatformReply(current.commands.unavailable_message, [], originContext));
return true;
}
try {
const result = await provider.generate({
message: ctx.argsText.trim(),
user,
sessionId: `command:${originContext.platform}:${originContext.message_id || Date.now()}`,
scope: "platform_command",
originContext,
allowDeterministicShortcut: current.support_scope?.allow_deterministic_help_shortcuts === true
});
const delivered = finalizeAssistantResult(result, {
role: permission.normalized_role,
config: current,
baseUrl: originContext.base_url,
maxLength: originContext.max_message_length,
requestMessage: ctx.argsText.trim()
});
const reply = formatPlatformReplyDetails(delivered.text, delivered.links, originContext);
await ctx.reply(reply.text);
commandMetrics.record({
kind: "command_audit",
status: "success",
scope: "platform_command",
route_used: result.route_used || "llm",
role: permission.normalized_role,
user_id: originContext.user_id,
platform: originContext.platform,
duration_ms: Date.now() - started,
original_final_length: delivered.original_final_length,
final_reply_length: reply.original_final_length,
delivered_length: reply.delivered_length,
final_tokens: estimateTokenCountFromLength(delivered.original_final_length),
delivered_tokens: estimateTokenCountFromLength(reply.delivered_length),
truncated: delivered.truncated || reply.delivered_length < delivered.original_final_length,
delivery_action: reply.delivery_action !== "none" ? reply.delivery_action : delivered.delivery_action,
...controllerDeliveryFields(result),
internal_generated_length: result.internal_generated_length || String(result.text || "").length
});
} catch (error) {
commandMetrics.record({
kind: "request",
status: "failed",
scope: "platform_command",
route_used: "unavailable_fallback",
role: permission.normalized_role,
user_id: originContext.user_id,
platform: originContext.platform,
message: error.message,
duration_ms: Date.now() - started
});
await ctx.reply(formatPlatformReply(current.commands.unavailable_message, [], originContext));
}
return true;
}
}]);
}
function authorizeAiRequest({ userId, context, accessControl, rateLimiter }) {
const access = accessControl.check(userId, context);
if (!access.allowed) return access;
const rate = rateLimiter.check(context);
if (!rate.allowed) {
return {
...rate,
message: `Lumi Assistant is rate limited. Try again in ${rate.retry_after_seconds}s.`
};
}
return { allowed: true };
}
function webOriginContext(req) {
const user = req.session.user;
const role = roleOf(user);
return {
origin: "webui",
platform: "webui",
channel_id: null,
channel_name: null,
server_id: null,
user_id: user.id,
username: user.username,
display_name: user.username,
role,
is_admin: role === "admin",
is_mod: role === "mod",
message_id: null,
reply_mode: "panel",
format_capabilities: { markdown: false, html: true },
max_message_length: null,
base_url: `${req.protocol}://${req.get("host")}`,
source_plugin: "lumi_ai",
source_command: "webui_assistant",
permission_context: { identified_user: true, webui_actions_allowed: true }
};
}
function diagnosticOriginContext(user, origin = "webui") {
const role = roleOf(user);
const limits = { webui: null, discord: 1900, twitch: 450, youtube: 1800, kick: 450, other: 1000 };
return {
origin,
platform: origin,
channel_id: "diagnostic-channel",
channel_name: "Diagnostic channel",
server_id: "diagnostic-server",
user_id: user.id,
username: user.username,
display_name: user.username,
role,
is_admin: role === "admin",
is_mod: role === "mod",
message_id: "diagnostic-message",
reply_mode: origin === "webui" ? "panel" : "same_channel",
format_capabilities: {
markdown: origin === "discord",
html: origin === "webui"
},
max_message_length: origin === "webui" ? null : (limits[origin] || limits.other),
source_plugin: "lumi_ai",
source_command: "admin_diagnostic",
permission_context: {
identified_user: true,
webui_actions_allowed: origin === "webui"
}
};
}
function finalizeAssistantResult(result, { role, config, baseUrl = "", maxLength = null, requestMessage = "" }) {
const formatted = formatAssistantResponse({
text: normalizeCustomCommandReply(result.text, requestMessage),
links: result.links,
baseUrl,
verifiedRoutes: repoIndexer.verifiedRoutePaths(),
role,
allowModeratorCodeHelp: config.support_scope?.allow_moderator_code_help === true,
maxLength
});
const output = { ...result, ...formatted };
if (role !== "admin") {
delete output.source;
output.raw_response = null;
}
return output;
}
function controllerDeliveryFields(result = {}) {
const decision = result.controller_decision || {};
const profile = decision.source_profile || {};
return {
controller_route: decision.route,
controller_intent: decision.intent,
controller_complexity: decision.complexity,
controller_reason_code: decision.reason_code,
controller_confidence: decision.confidence,
okf_retrieval_depth: decision.okf_retrieval,
source_profile: profile.source,
target_final_chars: profile.target_chars,
hard_final_chars: profile.hard_chars,
controller_fallback_used: decision.fallback_used
};
}
function estimateTokenCountFromLength(value) {
const number = Number(value);
return Number.isFinite(number) && number > 0 ? Math.max(1, Math.ceil(number / 4)) : 0;
}
function normalizeCustomCommandReply(text, requestMessage) {
const request = String(requestMessage || "");
const output = String(text || "");
if (!/\b(custom|advanced)\s+(?:javascript|js|python|command)|\bcustom command\b/i.test(request)) return output;
if (/```(?:javascript|python)\s*\n[\s\S]*?```/i.test(output)) return output;
const language = /\bpython\b/i.test(request) || /^\s*def\s+run\s*\(/m.test(output) ? "python" : "javascript";
const codePattern = language === "python"
? /(^|\n)(def\s+run\s*\([\s\S]+)$/m
: /(^|\n)((?:async\s+)?function\s+run\s*\([\s\S]+)$/m;
const match = output.match(codePattern);
if (!match) return output;
const start = match.index + match[1].length;
const explanation = output.slice(0, start).trim();
const code = output.slice(start).trim();
return `${explanation ? `${explanation}\n\n` : ""}\`\`\`${language}\n${code}\n\`\`\``;
}
function configuredBaseUrl(settings) {
for (const key of ["discord_redirect_uri", "twitch_redirect_uri", "youtube_redirect_uri"]) {
const value = settings?.getSetting?.(key, "");
if (!value) continue;
try { return new URL(value).origin; } catch {}
}
return "";
}
function paginateRows(rows, pageValue, pageSize = 25) {
const size = Math.max(1, Number.parseInt(pageSize, 10) || 25);
const pages = Math.max(1, Math.ceil(rows.length / size));
const page = Math.min(pages, Math.max(1, Number.parseInt(pageValue, 10) || 1));
const start = (page - 1) * size;
return { entries: rows.slice(start, start + size), page, pages, page_size: size, total: rows.length };
}
function normalizeConversationHistory(history) {
if (!Array.isArray(history)) return [];
return history.slice(-12).map((entry) => ({
role: ["user", "assistant"].includes(entry?.role) ? entry.role : null,
content: cleanText(entry?.content, 4000)
})).filter((entry) => entry.role && entry.content);
}
function searchKnownUsers(database, query) {
const value = String(query || "").trim().replace(/[%_]/g, "");
if (value.length < 2) return [];
const pattern = `%${value}%`;
const rows = database.prepare(
"SELECT DISTINCT p.id, p.internal_username, i.provider, i.provider_user_id, i.display_name " +
"FROM user_profiles p LEFT JOIN user_identities i ON i.user_id = p.id " +
"WHERE p.id LIKE ? OR p.internal_username LIKE ? OR i.provider_user_id LIKE ? OR i.display_name LIKE ? " +
"ORDER BY p.internal_username LIMIT 30"
).all(pattern, pattern, pattern, pattern);
return [...rows.reduce((map, row) => {
const user = map.get(row.id) || { id: row.id, username: row.internal_username, identities: [] };
if (row.provider) {
user.identities.push({
provider: row.provider,
provider_user_id: row.provider_user_id,
display_name: row.display_name
});
}
map.set(row.id, user);
return map;
}, new Map()).values()];
}
function parseCommandTriggers(value) {
const triggers = String(value || "assistant,lumi").split(/[,\s]+/)
.map((entry) => entry.trim().replace(/^!+/, "").toLowerCase())
.filter((entry) => /^[a-z0-9_-]+$/.test(entry));
return [...new Set(triggers.length ? triggers : ["assistant", "lumi"])];
}
function limitFromBody(body, prefix, fallbackRequests, fallbackWindow) {
return {
requests: boundedInt(body[`${prefix}_requests`], 0, 10000, fallbackRequests),
window_seconds: boundedInt(body[`${prefix}_window`], 1, 86400, fallbackWindow)
};
}
function sourceProfilesFromBody(body, current = {}) {
return Object.fromEntries(Object.entries(SOURCE_DEFAULTS).map(([source, fallback]) => {
const existing = current?.[source] || {};
const hardRaw = body[`source_profile_${source}_hard_chars`];
const targetFallback = existing.target_chars ?? fallback.target_chars;
const hardFallback = existing.hard_chars ?? fallback.hard_chars ?? 1000;
return [source, {
source,
target_chars: boundedInt(body[`source_profile_${source}_target_chars`], 100, 12000, targetFallback),
hard_chars: String(hardRaw ?? "").trim() === ""
? null
: boundedInt(hardRaw, 100, 12000, hardFallback),
allow_sections: body[`source_profile_${source}_allow_sections`] === "on",
allow_long_answer: body[`source_profile_${source}_allow_long_answer`] === "on",
allow_split: body[`source_profile_${source}_allow_split`] === "on"
}];
}));
}
function writeCommandsManifest(pluginDir, config) {
const triggers = config.commands.triggers || ["assistant", "lumi"];
const primary = triggers[0] || "assistant";
const manifest = {
pluginId: PLUGIN_ID,
pluginName: "Lumi AI",
platformKeys: {
discord: "command_platform_discord",
twitch: "command_platform_twitch",
youtube: "command_platform_youtube",
kick: "command_platform_kick"
},
commands: [{
id: "assistant",
trigger: primary,
usage: `${primary} <question>`,
name: "Lumi Assistant",
description: "Ask Lumi Assistant a scoped Lumi, bot, community, or WebUI support question.",
level: "public",
platforms: Object.entries(config.commands.platforms).filter(([, enabled]) => enabled).map(([key]) => key),
aliases: triggers.slice(1)
}]
};
fs.writeFileSync(path.join(pluginDir, "cmds.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
}
module.exports.shouldAutoResume = shouldAutoResume;
module.exports.registerAssistantCommands = registerAssistantCommands;
module.exports.authorizeAiRequest = authorizeAiRequest;
module.exports.searchKnownUsers = searchKnownUsers;
module.exports.finalizeAssistantResult = finalizeAssistantResult;