const crypto = require("crypto"); const { buildPrompt } = require("./prompt_builder"); const { roleOf } = require("./permissions"); const { parseToolCall } = require("./tool_router"); const { normalizeScope } = require("./scope_manager"); class AiProvider { constructor({ getConfig, runtime, queue, tools, metrics, getContext, lookupRepo, getRepoContext }) { Object.assign(this, { getConfig, runtime, queue, tools, metrics, getContext, lookupRepo, getRepoContext }); } async generate({ message, user, sessionId, scope = "assistant", max_tokens, includeRaw = false, originContext = null, allowDeterministicShortcut = null, history = [] }) { const requestId = crypto.randomUUID(); const role = roleOf(user); const started = Date.now(); const cfg = this.getConfig(); const supportScope = normalizeScope(cfg.support_scope); const repoAnswer = this.lookupRepo?.(message) || null; const shortcutSurfaceAllowed = scope === "assistant" || scope === "platform_command"; const guardedRepoAnswer = ["clarification", "contact", "unknown"].includes(repoAnswer?.type); const verifiedRouteAnswer = isExactHelpShortcut(message, repoAnswer); if (shortcutSurfaceAllowed && (guardedRepoAnswer || verifiedRouteAnswer)) { this.metrics.record({ kind: "request", status: "success", request_id: requestId, user_id: user.id, role, scope: "repo_lookup", route_used: `repo_${repoAnswer.type}`, duration_ms: Date.now() - started }); return { success: true, text: repoAnswer.text, links: repoAnswer.links || [], source: repoAnswer.source || null, model_id: "lumi-repo-index", route_used: `repo_${repoAnswer.type}`, internal_generated_length: repoAnswer.text.length, duration_ms: Date.now() - started, queue_wait_ms: 0, request_id: requestId }; } return this.queue.run(user.id, role, async (queueWait) => { const repoContext = supportScope.repo_lookup_enabled ? this.getRepoContext?.(message, role, supportScope.allow_moderator_code_help) || [] : []; const platformToolsAllowed = originContext?.permission_context?.webui_actions_allowed !== false; const prompt = buildPrompt({ config: cfg, role, message, contextBlocks: this.getContext(role), repoContext, originContext, tools: platformToolsAllowed ? this.tools.list(role) : [] }); const conversation = normalizeHistory(history); const internalBudget = Math.max(2000, Math.min(64000, Number(cfg.internal_generation_char_budget) || 16000)); const result = await this.runtime.infer( [ { role: "system", content: prompt }, ...conversation, { role: "user", content: message } ], max_tokens || Math.min(8192, Math.ceil(internalBudget / 3)) ); const text = result.choices?.[0]?.message?.content || ""; const toolCall = platformToolsAllowed ? parseToolCall(text) : null; let confirmation = null; let toolResult = null; if (toolCall) { const prepared = this.tools.prepare({ tool: toolCall.tool, args: toolCall.arguments, user, role, sessionId }); if (prepared.execute) toolResult = await this.tools.execute({ checked: prepared.checked, user, requestId }); confirmation = prepared.confirmation; } const out = { success: true, text: confirmation ? `Please confirm: ${confirmation.display_name}.` : toolResult ? `Action completed: ${JSON.stringify(toolResult)}` : text, links: [], raw_response: cfg.logging.log_responses || includeRaw ? result : null, tool_call: toolCall, tool_result: toolResult, confirmation, model_id: cfg.selected_model_id, duration_ms: Date.now() - started, queue_wait_ms: queueWait, finish_reason: result.choices?.[0]?.finish_reason || null, request_id: requestId, route_used: "llm", internal_generated_length: text.length }; this.metrics.record({ kind: "request", status: "success", request_id: requestId, user_id: user.id, role, scope, model: cfg.selected_model_id, duration_ms: out.duration_ms, queue_wait_ms: queueWait, tool_requested: toolCall?.tool || null, tool_executed: false, route_used: "llm", internal_generated_length: text.length }); return out; }); } async classify({ message, labels, user }) { const result = await this.generate({ message: `Classify this Lumi-related request into exactly one label: ${labels.join(", ")}. Request: ${message}`, user, scope: "classify", max_tokens: 40 }); return { ...result, label: labels.find((label) => result.text.toLowerCase().includes(label.toLowerCase())) || null }; } async summarize({ text, max_length = 500, user }) { return this.generate({ message: `Summarize this Lumi-related content in at most ${max_length} characters:\n${text}`, user, scope: "summarize", max_tokens: Math.ceil(max_length / 3) }); } async test({ message, user, max_tokens = 300, includeRaw = false }) { const requestId = crypto.randomUUID(); const role = roleOf(user); const started = Date.now(); return this.queue.run(user.id, role, async (queueWait) => { const cfg = this.getConfig(); const prompt = [ "You are Lumi Assistant, the built-in assistant for Lumi Bot, running an administrator-requested local model diagnostic.", "Answer the exact user message directly and concisely.", "Do not identify yourself as the underlying model.", "Do not call tools, perform actions, claim access to Lumi data, or follow requests to execute code, files, SQL, shell commands, or URLs." ].join("\n"); const result = await this.runtime.infer([{ role: "system", content: prompt }, { role: "user", content: message }], max_tokens); const text = result.choices?.[0]?.message?.content || ""; const output = { success: true, text, raw_response: includeRaw ? result : null, raw_prompt: prompt, tool_call: null, tool_result: null, confirmation: null, model_id: cfg.selected_model_id, duration_ms: Date.now() - started, queue_wait_ms: queueWait, finish_reason: result.choices?.[0]?.finish_reason || null, request_id: requestId }; this.metrics.record({ kind: "request", status: "success", request_id: requestId, user_id: user.id, role, scope: "model_test", model: cfg.selected_model_id, duration_ms: output.duration_ms, queue_wait_ms: queueWait }); return output; }); } } function isClearlyOutOfScope() { return false; } function isInScope() { return true; } function isIdentityQuery(message) { return /\b(who|what)\s+(are|r)\s+you\b|\byour\s+(name|identity)\b/i.test(String(message || "")); } function isExactHelpShortcut(message, repoAnswer) { if (isIdentityQuery(message) || repoAnswer?.type !== "route") return false; if (repoAnswer?.source?.confidence !== "high") return false; return /\b(where|open|find|navigate|page|screen|menu|settings?|configuration|wizard)\b/i.test(String(message || "")); } function normalizeHistory(history, maxMessages = 12, maxCharacters = 12000) { const rows = Array.isArray(history) ? history.slice(-maxMessages) : []; const output = []; let used = 0; for (let index = rows.length - 1; index >= 0; index -= 1) { const role = rows[index]?.role; const content = String(rows[index]?.content || "").trim(); if (!["user", "assistant"].includes(role) || !content) continue; if (used + content.length > maxCharacters) break; output.unshift({ role, content }); used += content.length; } return output; } module.exports = { AiProvider, isInScope, isClearlyOutOfScope, isIdentityQuery, isExactHelpShortcut, normalizeHistory };