181 lines
7.8 KiB
JavaScript
181 lines
7.8 KiB
JavaScript
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 };
|