55 lines
5.6 KiB
JavaScript
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};
|