import { parseChatML, risuChatParser } from "../parser"; import { DataBase, type Chat, type character } from "../storage/database"; import { tokenize } from "../tokenizer"; import { getModuleTriggers } from "./modules"; import { get } from "svelte/store"; import { CurrentCharacter, CurrentChat, ReloadGUIPointer, selectedCharID } from "../stores"; import { processMultiCommand } from "./command"; import { parseKeyValue } from "../util"; import { alertError, alertInput, alertNormal, alertSelect } from "../alert"; import type { OpenAIChat } from "."; import { HypaProcesser } from "./memory/hypamemory"; import { requestChatData } from "./request"; import { generateAIImage } from "./stableDiff"; import { writeInlayImage } from "./files/image"; import { runLua } from "./lua"; export interface triggerscript{ comment: string; type: 'start'|'manual'|'output'|'input' conditions: triggerCondition[] effect:triggerEffect[] lowLevelAccess?: boolean } export type triggerCondition = triggerConditionsVar|triggerConditionsExists|triggerConditionsChatIndex export type triggerEffect = triggerCode|triggerEffectCutChat|triggerEffectModifyChat|triggerEffectImgGen|triggerEffectRegex|triggerEffectRunLLM|triggerEffectCheckSimilarity|triggerEffectSendAIprompt|triggerEffectShowAlert|triggerEffectSetvar|triggerEffectSystemPrompt|triggerEffectImpersonate|triggerEffectCommand|triggerEffectStop|triggerEffectRunTrigger export type triggerConditionsVar = { type:'var'|'value' var:string value:string operator:'='|'!='|'>'|'<'|'>='|'<='|'null'|'true' } export type triggerCode = { type: 'triggercode'|'triggerlua', code: string } export type triggerConditionsChatIndex = { type:'chatindex' value:string operator:'='|'!='|'>'|'<'|'>='|'<='|'null'|'true' } export type triggerConditionsExists ={ type: 'exists' value:string type2: 'strict'|'loose'|'regex', depth: number } export interface triggerEffectSetvar{ type: 'setvar', operator: '='|'+='|'-='|'*='|'/=' var:string value:string } export interface triggerEffectCutChat{ type: 'cutchat', start: string, end: string } export interface triggerEffectModifyChat{ type: 'modifychat', index: string, value: string } export interface triggerEffectSystemPrompt{ type: 'systemprompt', location: 'start'|'historyend'|'promptend', value:string } export interface triggerEffectImpersonate{ type: 'impersonate' role: 'user'|'char', value:string }type triggerMode = 'start'|'manual'|'output'|'input' export interface triggerEffectCommand{ type: 'command', value: string } export interface triggerEffectRegex{ type: 'extractRegex', value: string regex: string flags: string result: string inputVar: string } export interface triggerEffectShowAlert{ type: 'showAlert', alertType: string value: string inputVar: string } export interface triggerEffectRunTrigger{ type: 'runtrigger', value: string } export interface triggerEffectStop{ type: 'stop' } export interface triggerEffectSendAIprompt{ type: 'sendAIprompt' } export interface triggerEffectImgGen{ type: 'runImgGen', value: string, negValue: string, inputVar: string } export interface triggerEffectCheckSimilarity{ type: 'checkSimilarity', source: string, value: string, inputVar: string } export interface triggerEffectRunLLM{ type: 'runLLM', value: string, inputVar: string } export type additonalSysPrompt = { start:string, historyend: string, promptend: string } export async function runTrigger(char:character,mode:triggerMode, arg:{ chat: Chat, recursiveCount?: number additonalSysPrompt?: additonalSysPrompt stopSending?: boolean manualName?: string }){ arg.recursiveCount ??= 0 char = structuredClone(char) let varChanged = false let stopSending = arg.stopSending ?? false const CharacterlowLevelAccess = char.lowLevelAccess ?? false let sendAIprompt = false const currentChat = get(CurrentChat) let additonalSysPrompt:additonalSysPrompt = arg.additonalSysPrompt ?? { start:'', historyend: '', promptend: '' } const triggers = char.triggerscript.map((v) => { v.lowLevelAccess = CharacterlowLevelAccess return v }).concat(getModuleTriggers()) const db = get(DataBase) const defaultVariables = parseKeyValue(char.defaultVariables).concat(parseKeyValue(db.templateDefaultVariables)) let chat = structuredClone(arg.chat ?? char.chats[char.chatPage]) if((!triggers) || (triggers.length === 0)){ return null } function getVar(key:string){ const state = chat.scriptstate?.['$' + key] if(state === undefined || state === null){ const findResult = defaultVariables.find((f) => { return f[0] === key }) if(findResult){ return findResult[1] } return 'null' } return state.toString() } function setVar(key:string, value:string){ const selectedCharId = get(selectedCharID) const currentCharacter = get(CurrentCharacter) const db = get(DataBase) varChanged = true chat.scriptstate ??= {} chat.scriptstate['$' + key] = value currentChat.scriptstate = chat.scriptstate currentCharacter.chats[currentCharacter.chatPage].scriptstate = chat.scriptstate db.characters[selectedCharId].chats[currentCharacter.chatPage].scriptstate = chat.scriptstate } for(const trigger of triggers){ if(trigger.effect[0]?.type === 'triggercode' || trigger.effect[0]?.type === 'triggerlua'){ // } else if(arg.manualName){ if(trigger.comment !== arg.manualName){ continue } } else if(mode !== trigger.type){ continue } let pass = true for(const condition of trigger.conditions){ if(condition.type === 'var' || condition.type === 'chatindex' || condition.type === 'value'){ let varValue = (condition.type === 'var') ? (getVar(condition.var) ?? 'null') : (condition.type === 'chatindex') ? (chat.message.length.toString()) : (condition.type === 'value') ? condition.var : null if(varValue === undefined || varValue === null){ pass = false break } else{ const conditionValue = risuChatParser(condition.value,{chara:char}) varValue = risuChatParser(varValue,{chara:char}) switch(condition.operator){ case 'true': { if(varValue !== 'true' && varValue !== '1'){ pass = false } break } case '=': if(varValue !== conditionValue){ pass = false } break case '!=': if(varValue === conditionValue){ pass = false } break case '>': if(Number(varValue) <= Number(conditionValue)){ pass = false } break case '<': if(Number(varValue) >= Number(conditionValue)){ pass = false } break case '>=': if(Number(varValue) < Number(conditionValue)){ pass = false } break case '<=': if(Number(varValue) > Number(conditionValue)){ pass = false } break case 'null': if(varValue !== 'null'){ pass = false } break } } } else if(condition.type === 'exists'){ const conditionValue = risuChatParser(condition.value,{chara:char}) const val = risuChatParser(conditionValue,{chara:char}) let da = chat.message.slice(0-condition.depth).map((v)=>v.data).join(' ') if(condition.type2 === 'strict'){ pass = da.split(' ').includes(val) } else if(condition.type2 === 'loose'){ pass = da.toLowerCase().includes(val.toLowerCase()) } else if(condition.type2 === 'regex'){ pass = new RegExp(val).test(da) } } if(!pass){ break } } if(!pass){ continue } for(const effect of trigger.effect){ switch(effect.type){ case'setvar': { const effectValue = risuChatParser(effect.value,{chara:char}) const varKey = risuChatParser(effect.var,{chara:char}) let originalVar = Number(getVar(varKey)) if(Number.isNaN(originalVar)){ originalVar = 0 } let resultValue = '' switch(effect.operator){ case '=':{ resultValue = effectValue break } case '+=':{ resultValue = (originalVar + Number(effectValue)).toString() break } case '-=':{ resultValue = (originalVar - Number(effectValue)).toString() break } case '*=':{ resultValue = (originalVar * Number(effectValue)).toString() break } case '/=':{ resultValue = (originalVar / Number(effectValue)).toString() break } } setVar(varKey, resultValue) break } case 'systemprompt':{ const effectValue = risuChatParser(effect.value,{chara:char}) additonalSysPrompt[effect.location] += effectValue + "\n\n" break } case 'impersonate':{ const effectValue = risuChatParser(effect.value,{chara:char}) if(effect.role === 'user'){ chat.message.push({role: 'user', data: effectValue}) } else if(effect.role === 'char'){ chat.message.push({role: 'char', data: effectValue}) } break } case 'command':{ const effectValue = risuChatParser(effect.value,{chara:char}) await processMultiCommand(effectValue) break } case 'stop':{ stopSending = true break } case 'runtrigger':{ if(arg.recursiveCount < 10 || trigger.lowLevelAccess){ arg.recursiveCount++ const r = await runTrigger(char,'manual',{ chat, recursiveCount: arg.recursiveCount, additonalSysPrompt, stopSending, manualName: effect.value }) if(r){ additonalSysPrompt = r.additonalSysPrompt chat = r.chat stopSending = r.stopSending } } break } case 'cutchat':{ const start = Number(risuChatParser(effect.start,{chara:char})) const end = Number(risuChatParser(effect.end,{chara:char})) chat.message = chat.message.slice(start,end) break } case 'modifychat':{ const index = Number(risuChatParser(effect.index,{chara:char})) const value = risuChatParser(effect.value,{chara:char}) if(chat.message[index]){ chat.message[index].data = value } break } // low level access only case 'showAlert':{ if(!trigger.lowLevelAccess){ break } const effectValue = risuChatParser(effect.value,{chara:char}) const inputVar = risuChatParser(effect.inputVar,{chara:char}) switch(effect.alertType){ case 'normal':{ alertNormal(effectValue) break } case 'error':{ alertError(effectValue) break } case 'input':{ const val = await alertInput(effectValue) setVar(inputVar, val) break; } case 'select':{ const val = await alertSelect(effectValue.split('§')) setVar(inputVar, val) } } break } case 'sendAIprompt':{ if(!trigger.lowLevelAccess){ break } sendAIprompt = true break } case 'runLLM':{ if(!trigger.lowLevelAccess){ break } const effectValue = risuChatParser(effect.value,{chara:char}) const varName = effect.inputVar let promptbody:OpenAIChat[] = parseChatML(effectValue) if(!promptbody){ promptbody = [{role:'user', content:effectValue}] } const result = await requestChatData({ formated: promptbody, bias: {}, useStreaming: false, noMultiGen: true, }, 'model') if(result.type === 'fail' || result.type === 'streaming' || result.type === 'multiline'){ setVar(varName, 'Error: ' + result.result) } else{ setVar(varName, result.result) } break } case 'checkSimilarity':{ if(!trigger.lowLevelAccess){ break } const processer = new HypaProcesser('MiniLM') const effectValue = risuChatParser(effect.value,{chara:char}) const source = risuChatParser(effect.source,{chara:char}) await processer.addText(effectValue.split('§')) const val = await processer.similaritySearch(source) setVar(effect.inputVar, val.join('§')) break } case 'extractRegex':{ if(!trigger.lowLevelAccess){ break } const effectValue = risuChatParser(effect.value,{chara:char}) const regex = new RegExp(effect.regex, effect.flags) const regexResult = regex.exec(effectValue) const result = effect.result.replace(/\$[0-9]+/g, (match) => { const index = Number(match.slice(1)) return regexResult[index] }).replace(/\$&/g, regexResult[0]).replace(/\$\$/g, '$') setVar(effect.inputVar, result) break } case 'runImgGen':{ if(!trigger.lowLevelAccess){ break } const effectValue = risuChatParser(effect.value,{chara:char}) const negValue = risuChatParser(effect.negValue,{chara:char}) const gen = await generateAIImage(effectValue, char, negValue, 'inlay') if(!gen){ setVar(effect.inputVar, 'Error: Image generation failed') break } const imgHTML = new Image() imgHTML.src = gen const inlay = await writeInlayImage(imgHTML) const res = `{{inlay::${inlay}}}` setVar(effect.inputVar, res) break } case 'triggerlua':{ const triggerCodeResult = await runLua(effect.code,{ lowLevelAccess: trigger.lowLevelAccess, mode: mode === 'manual' ? arg.manualName : mode, setVar: setVar, getVar: getVar, char: char, chat: chat, }) if(triggerCodeResult.stopSending){ stopSending = true } chat = get(CurrentChat) break } } } } let caculatedTokens = 0 if(additonalSysPrompt.start){ caculatedTokens += await tokenize(additonalSysPrompt.start) } if(additonalSysPrompt.historyend){ caculatedTokens += await tokenize(additonalSysPrompt.historyend) } if(additonalSysPrompt.promptend){ caculatedTokens += await tokenize(additonalSysPrompt.promptend) } if(varChanged){ const currentChat = get(CurrentChat) currentChat.scriptstate = chat.scriptstate ReloadGUIPointer.set(get(ReloadGUIPointer) + 1) } return {additonalSysPrompt, chat, tokens:caculatedTokens, stopSending, sendAIprompt} }