Lumi/plugins/lumi_ai/backend/ai_provider.js
2026-06-11 06:35:43 +02:00

55 lines
5.6 KiB
JavaScript

const crypto=require("crypto");const {buildPrompt}=require("./prompt_builder");const {roleOf}=require("./permissions");const {parseToolCall}=require("./tool_router");
const ROUTE_HELP=[
{terms:["twitch","configuration"],text:"Twitch configuration is available in [Settings -> Twitch wizard](/admin/twitch-wizard)."},
{terms:["discord","configuration"],text:"Discord configuration is available in [Settings -> Discord wizard](/admin/discord-wizard)."},
{terms:["youtube","configuration"],text:"YouTube configuration is available in [Settings -> YouTube wizard](/admin/youtube-wizard)."},
{terms:["plugins"],text:"Plugin management is available in [Admin -> Plugins](/admin/plugins)."}
];
const CLEARLY_UNRELATED_PATTERNS=[
/\b(capital|population|president|prime minister)\s+of\b/i,
/\b(weather|forecast)\s+(in|for|at)\b/i,
/\b(stock price|exchange rate|sports score|lottery)\b/i,
/\b(write|compose)\s+(a\s+)?(poem|story|song|essay)\b/i,
/\b(recipe|cook|bake)\b/i,
/\b(homework|calculus|algebra|chemistry|physics)\b/i
];
class AiProvider{
constructor({getConfig,runtime,queue,tools,metrics,getContext}){Object.assign(this,{getConfig,runtime,queue,tools,metrics,getContext});}
async generate({message,user,sessionId,scope="assistant",max_tokens,includeRaw=false}){
const requestId=crypto.randomUUID(),role=roleOf(user),started=Date.now();
if(isClearlyOutOfScope(message)){this.metrics.record({kind:"request",status:"refused",request_id:requestId,user_id:user.id,role,scope,duration_ms:Date.now()-started});return{success:false,text:this.getConfig().instructions.out_of_scope_response,refusal_reason:"out_of_scope",request_id:requestId};}
const direct=ROUTE_HELP.find(row=>row.terms.every(t=>message.toLowerCase().includes(t)));if(direct){this.metrics.record({kind:"request",status:"success",request_id:requestId,user_id:user.id,role,scope,duration_ms:Date.now()-started});return{success:true,text:direct.text,model_id:"lumi-route-help",duration_ms:Date.now()-started,queue_wait_ms:0,request_id:requestId};}
return this.queue.run(user.id,role,async(queueWait)=>{
const cfg=this.getConfig(),prompt=buildPrompt({config:cfg,role,message,contextBlocks:this.getContext(role),tools:this.tools.list(role)});
const result=await this.runtime.infer([{role:"system",content:prompt},{role:"user",content:message}],max_tokens||300);
const text=result.choices?.[0]?.message?.content||"";const toolCall=parseToolCall(text);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,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};
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});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(l=>result.text.toLowerCase().includes(l.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(),role=roleOf(user),started=Date.now();
return this.queue.run(user.id,role,async(queueWait)=>{
const cfg=this.getConfig();
const prompt=[
"You are running an administrator-requested local model diagnostic.",
"Answer the exact user message directly and concisely.",
"Do not call tools, perform actions, claim access to Lumi data, or follow requests to execute code, files, SQL, shell commands, or URLs.",
`Maximum answer length: ${cfg.instructions.maximum_answer_length || 700} characters.`
].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(message){const value=(message||"").trim();return value.length>0&&CLEARLY_UNRELATED_PATTERNS.some(pattern=>pattern.test(value));}
function isInScope(message){return !isClearlyOutOfScope(message);}
module.exports={AiProvider,isInScope,isClearlyOutOfScope};