Fix various lua low level access bugs (#575)

### 1 Access key was not added properly
First commit fixes this 

### 2 Lua engine was getting killed when it waits for async function
runLua function gets run in two different paths (i.e. runLua for lua
edit triggers and another one for onOuput handling) However, when one of
invocations waits for another promise from js side (i.e. low level
function) to finish, it gives chance for next trigger mode to run which
would call `if(luaEngine){ luaEngine.global.close() }` that will close
lua engine for the invocation that was waiting for promise which is
problematic.

Second commit fixes this by having a pool of engine for each trigger
mode having single mutex that prevents running same engine at the same
time while allowing different tigger modes to run parallely

### 3 Break statements were missing making every trigger to run for
input event.
This was causing insane lagging and buggy behaviours. Third commit fixes
this.
This commit is contained in:
kwaroran
2024-07-17 16:44:17 +09:00
committed by GitHub
2 changed files with 498 additions and 384 deletions

88
src/ts/mutex.ts Normal file
View File

@@ -0,0 +1,88 @@
/**
* A lock for synchronizing async operations.
* Use this to protect a critical section
* from getting modified by multiple async operations
* at the same time.
*/
export class Mutex {
/**
* When multiple operations attempt to acquire the lock,
* this queue remembers the order of operations.
*/
private _queue: {
resolve: (release: ReleaseFunction) => void
}[] = []
private _isLocked = false
/**
* Wait until the lock is acquired.
* @returns A function that releases the acquired lock.
*/
acquire() {
return new Promise<ReleaseFunction>((resolve) => {
this._queue.push({resolve})
this._dispatch()
});
}
/**
* Enqueue a function to be run serially.
*
* This ensures no other functions will start running
* until `callback` finishes running.
* @param callback Function to be run exclusively.
* @returns The return value of `callback`.
*/
async runExclusive<T>(callback: () => Promise<T>) {
const release = await this.acquire()
try {
return await callback()
} finally {
release()
}
}
/**
* Check the availability of the resource
* and provide access to the next operation in the queue.
*
* _dispatch is called whenever availability changes,
* such as after lock acquire request or lock release.
*/
private _dispatch() {
if (this._isLocked) {
// The resource is still locked.
// Wait until next time.
return
}
const nextEntry = this._queue.shift()
if (!nextEntry) {
// There is nothing in the queue.
// Do nothing until next dispatch.
return
}
// The resource is available.
this._isLocked = true
// and give access to the next operation
// in the queue.
nextEntry.resolve(this._buildRelease())
}
/**
* Build a release function for each operation
* so that it can release the lock after
* the operation is complete.
*/
private _buildRelease(): ReleaseFunction {
return () => {
// Each release function make
// the resource available again
this._isLocked = false
// and call dispatch.
this._dispatch()
}
}
}
type ReleaseFunction = () => void

View File

@@ -11,14 +11,21 @@ import type { OpenAIChat } from ".";
import { requestChatData } from "./request"; import { requestChatData } from "./request";
import { v4 } from "uuid"; import { v4 } from "uuid";
import { getModuleTriggers } from "./modules"; import { getModuleTriggers } from "./modules";
import { Mutex } from "../mutex";
let luaFactory:LuaFactory let luaFactory:LuaFactory
let luaEngine:LuaEngine
let lastCode = ''
let LuaSafeIds = new Set<string>() let LuaSafeIds = new Set<string>()
let LuaEditDisplayIds = new Set<string>() let LuaEditDisplayIds = new Set<string>()
let LuaLowLevelIds = new Set<string>() let LuaLowLevelIds = new Set<string>()
interface LuaEngineState {
code: string;
engine: LuaEngine;
mutex: Mutex;
}
let LuaEngines = new Map<string, LuaEngineState>()
export async function runLua(code:string, arg:{ export async function runLua(code:string, arg:{
char?:character|groupChat|simpleCharacterArgument, char?:character|groupChat|simpleCharacterArgument,
chat?:Chat chat?:Chat
@@ -37,412 +44,431 @@ export async function runLua(code:string, arg:{
let stopSending = false let stopSending = false
let lowLevelAccess = arg.lowLevelAccess ?? false let lowLevelAccess = arg.lowLevelAccess ?? false
if(!luaEngine || lastCode !== code){ if(!luaFactory){
if(luaEngine){ await makeLuaFactory()
luaEngine.global.close() }
let luaEngineState = LuaEngines.get(mode)
let wasEmpty = false
if (!luaEngineState) {
luaEngineState = {
code,
engine: await luaFactory.createEngine({injectObjects: true}),
mutex: new Mutex()
} }
if(!luaFactory){ LuaEngines.set(mode, luaEngineState)
makeLuaFactory() wasEmpty = true
} }
luaEngine = await luaFactory.createEngine({injectObjects: true}) return await luaEngineState.mutex.runExclusive(async () => {
luaEngine.global.set('setChatVar', (id:string,key:string, value:string) => { if (wasEmpty || code !== luaEngineState.code) {
if(!LuaSafeIds.has(id) && !LuaEditDisplayIds.has(id)){ if (!wasEmpty)
return luaEngineState.engine.global.close()
} luaEngineState.engine = await luaFactory.createEngine({injectObjects: true})
setVar(key, value) const luaEngine = luaEngineState.engine
}) luaEngine.global.set('setChatVar', (id:string,key:string, value:string) => {
luaEngine.global.set('getChatVar', (id:string,key:string) => { if(!LuaSafeIds.has(id) && !LuaEditDisplayIds.has(id)){
if(!LuaSafeIds.has(id) && !LuaEditDisplayIds.has(id)){ return
return
}
return getVar(key)
})
luaEngine.global.set('stopChat', (id:string) => {
if(!LuaSafeIds.has(id)){
return
}
stopSending = true
})
luaEngine.global.set('alertError', (id:string, value:string) => {
if(!LuaSafeIds.has(id)){
return
}
alertError(value)
})
luaEngine.global.set('alertNormal', (id:string, value:string) => {
if(!LuaSafeIds.has(id)){
return
}
alertNormal(value)
})
luaEngine.global.set('alertInput', (id:string, value:string) => {
if(!LuaSafeIds.has(id)){
return
}
return alertInput(value)
})
luaEngine.global.set('setChat', (id:string, index:number, value:string) => {
if(!LuaSafeIds.has(id)){
return
}
const message = chat.message?.at(index)
if(message){
message.data = value
}
CurrentChat.set(chat)
})
luaEngine.global.set('setChatRole', (id:string, index:number, value:string) => {
if(!LuaSafeIds.has(id)){
return
}
const message = chat.message?.at(index)
if(message){
message.role = value === 'user' ? 'user' : 'char'
}
CurrentChat.set(chat)
})
luaEngine.global.set('cutChat', (id:string, start:number, end:number) => {
if(!LuaSafeIds.has(id)){
return
}
chat.message = chat.message.slice(start,end)
CurrentChat.set(chat)
})
luaEngine.global.set('removeChat', (id:string, index:number) => {
if(!LuaSafeIds.has(id)){
return
}
chat.message.splice(index, 1)
CurrentChat.set(chat)
})
luaEngine.global.set('addChat', (id:string, role:string, value:string) => {
if(!LuaSafeIds.has(id)){
return
}
let roleData:'user'|'char' = role === 'user' ? 'user' : 'char'
chat.message.push({role: roleData, data: value})
CurrentChat.set(chat)
})
luaEngine.global.set('insertChat', (id:string, index:number, role:string, value:string) => {
if(!LuaSafeIds.has(id)){
return
}
let roleData:'user'|'char' = role === 'user' ? 'user' : 'char'
chat.message.splice(index, 0, {role: roleData, data: value})
CurrentChat.set(chat)
})
luaEngine.global.set('removeChat', (id:string, index:number) => {
if(!LuaSafeIds.has(id)){
return
}
chat.message.splice(index, 1)
CurrentChat.set(chat)
})
luaEngine.global.set('getChatLength', (id:string) => {
if(!LuaSafeIds.has(id)){
return
}
return chat.message.length
})
luaEngine.global.set('getFullChatMain', (id:string) => {
const data = JSON.stringify(chat.message.map((v) => {
return {
role: v.role,
data: v.data
}
}))
return data
})
luaEngine.global.set('setFullChatMain', (id:string, value:string) => {
const realValue = JSON.parse(value)
if(!LuaSafeIds.has(id)){
return
}
chat.message = realValue.map((v) => {
return {
role: v.role,
data: v.data
} }
setVar(key, value)
})
luaEngine.global.set('getChatVar', (id:string,key:string) => {
if(!LuaSafeIds.has(id) && !LuaEditDisplayIds.has(id)){
return
}
return getVar(key)
})
luaEngine.global.set('stopChat', (id:string) => {
if(!LuaSafeIds.has(id)){
return
}
stopSending = true
})
luaEngine.global.set('alertError', (id:string, value:string) => {
if(!LuaSafeIds.has(id)){
return
}
alertError(value)
})
luaEngine.global.set('alertNormal', (id:string, value:string) => {
if(!LuaSafeIds.has(id)){
return
}
alertNormal(value)
})
luaEngine.global.set('alertInput', (id:string, value:string) => {
if(!LuaSafeIds.has(id)){
return
}
return alertInput(value)
})
luaEngine.global.set('setChat', (id:string, index:number, value:string) => {
if(!LuaSafeIds.has(id)){
return
}
const message = chat.message?.at(index)
if(message){
message.data = value
}
CurrentChat.set(chat)
})
luaEngine.global.set('setChatRole', (id:string, index:number, value:string) => {
if(!LuaSafeIds.has(id)){
return
}
const message = chat.message?.at(index)
if(message){
message.role = value === 'user' ? 'user' : 'char'
}
CurrentChat.set(chat)
})
luaEngine.global.set('cutChat', (id:string, start:number, end:number) => {
if(!LuaSafeIds.has(id)){
return
}
chat.message = chat.message.slice(start,end)
CurrentChat.set(chat)
})
luaEngine.global.set('removeChat', (id:string, index:number) => {
if(!LuaSafeIds.has(id)){
return
}
chat.message.splice(index, 1)
CurrentChat.set(chat)
})
luaEngine.global.set('addChat', (id:string, role:string, value:string) => {
if(!LuaSafeIds.has(id)){
return
}
let roleData:'user'|'char' = role === 'user' ? 'user' : 'char'
chat.message.push({role: roleData, data: value})
CurrentChat.set(chat)
})
luaEngine.global.set('insertChat', (id:string, index:number, role:string, value:string) => {
if(!LuaSafeIds.has(id)){
return
}
let roleData:'user'|'char' = role === 'user' ? 'user' : 'char'
chat.message.splice(index, 0, {role: roleData, data: value})
CurrentChat.set(chat)
})
luaEngine.global.set('removeChat', (id:string, index:number) => {
if(!LuaSafeIds.has(id)){
return
}
chat.message.splice(index, 1)
CurrentChat.set(chat)
})
luaEngine.global.set('getChatLength', (id:string) => {
if(!LuaSafeIds.has(id)){
return
}
return chat.message.length
})
luaEngine.global.set('getFullChatMain', (id:string) => {
const data = JSON.stringify(chat.message.map((v) => {
return {
role: v.role,
data: v.data
}
}))
return data
}) })
CurrentChat.set(chat)
})
luaEngine.global.set('logMain', (value:string) => { luaEngine.global.set('setFullChatMain', (id:string, value:string) => {
console.log(JSON.parse(value)) const realValue = JSON.parse(value)
}) if(!LuaSafeIds.has(id)){
return
}
chat.message = realValue.map((v) => {
return {
role: v.role,
data: v.data
}
})
CurrentChat.set(chat)
})
//Low Level Access luaEngine.global.set('logMain', (value:string) => {
luaEngine.global.set('similarity', async (id:string, source:string, value:string[]) => { console.log(JSON.parse(value))
if(!LuaLowLevelIds.has(id)){ })
return
}
const processer = new HypaProcesser('MiniLM')
await processer.addText(value)
return await processer.similaritySearch(source)
})
luaEngine.global.set('generateImage', async (id:string, value:string, negValue:string = '') => { //Low Level Access
if(!LuaLowLevelIds.has(id)){ luaEngine.global.set('similarity', async (id:string, source:string, value:string[]) => {
return if(!LuaLowLevelIds.has(id)){
} return
const gen = await generateAIImage(value, char as character, negValue, 'inlay') }
if(!gen){ const processer = new HypaProcesser('MiniLM')
return 'Error: Image generation failed' await processer.addText(value)
} return await processer.similaritySearch(source)
const imgHTML = new Image() })
imgHTML.src = gen
const inlay = await writeInlayImage(imgHTML)
return `{{inlay::${inlay}}}`
})
luaEngine.global.set('LLMMain', async (id:string, promptStr:string) => { luaEngine.global.set('generateImage', async (id:string, value:string, negValue:string = '') => {
let prompt:{ if(!LuaLowLevelIds.has(id)){
role: string, return
content: string }
}[] = JSON.parse(promptStr) const gen = await generateAIImage(value, char as character, negValue, 'inlay')
if(!LuaLowLevelIds.has(id)){ if(!gen){
return return 'Error: Image generation failed'
} }
let promptbody:OpenAIChat[] = prompt.map((dict) => { const imgHTML = new Image()
let role:'system'|'user'|'assistant' = 'assistant' imgHTML.src = gen
switch(dict['role']){ const inlay = await writeInlayImage(imgHTML)
case 'system': return `{{inlay::${inlay}}}`
case 'sys': })
role = 'system'
break luaEngine.global.set('LLMMain', async (id:string, promptStr:string) => {
case 'user': let prompt:{
role = 'user' role: string,
break content: string
case 'assistant': }[] = JSON.parse(promptStr)
case 'bot': if(!LuaLowLevelIds.has(id)){
case 'char':{ return
role = 'assistant' }
break let promptbody:OpenAIChat[] = prompt.map((dict) => {
let role:'system'|'user'|'assistant' = 'assistant'
switch(dict['role']){
case 'system':
case 'sys':
role = 'system'
break
case 'user':
role = 'user'
break
case 'assistant':
case 'bot':
case 'char':{
role = 'assistant'
break
}
}
return {
content: dict['content'] ?? '',
role: role,
}
})
const result = await requestChatData({
formated: promptbody,
bias: {},
useStreaming: false,
noMultiGen: true,
}, 'model')
if(result.type === 'fail'){
return JSON.stringify({
success: false,
result: 'Error: ' + result.result
})
}
if(result.type === 'streaming' || result.type === 'multiline'){
return JSON.stringify({
success: false,
result: result.result
})
}
return JSON.stringify({
success: true,
result: result.result
})
})
luaEngine.global.set('simpleLLM', async (id:string, prompt:string) => {
if(!LuaLowLevelIds.has(id)){
return
}
const result = await requestChatData({
formated: [{
role: 'user',
content: prompt
}],
bias: {},
useStreaming: false,
noMultiGen: true,
}, 'model')
if(result.type === 'fail'){
return {
success: false,
result: 'Error: ' + result.result
}
}
if(result.type === 'streaming' || result.type === 'multiline'){
return {
success: false,
result: result.result
} }
} }
return { return {
content: dict['content'] ?? '', success: true,
role: role,
}
})
const result = await requestChatData({
formated: promptbody,
bias: {},
useStreaming: false,
noMultiGen: true,
}, 'model')
if(result.type === 'fail'){
return JSON.stringify({
success: false,
result: 'Error: ' + result.result
})
}
if(result.type === 'streaming' || result.type === 'multiline'){
return JSON.stringify({
success: false,
result: result.result
})
}
return JSON.stringify({
success: true,
result: result.result
})
})
luaEngine.global.set('simpleLLM', async (id:string, prompt:string) => {
if(!LuaLowLevelIds.has(id)){
return
}
const result = await requestChatData({
formated: [{
role: 'user',
content: prompt
}],
bias: {},
useStreaming: false,
noMultiGen: true,
}, 'model')
if(result.type === 'fail'){
return {
success: false,
result: 'Error: ' + result.result
}
}
if(result.type === 'streaming' || result.type === 'multiline'){
return {
success: false,
result: result.result result: result.result
} }
} })
luaEngine.global.set('getName', async (id:string) => {
if(!LuaSafeIds.has(id)){
return
}
const db = get(DataBase)
const selectedChar = get(selectedCharID)
const char = db.characters[selectedChar]
return char.name
})
return { luaEngine.global.set('setName', async (id:string, name:string) => {
success: true, if(!LuaSafeIds.has(id)){
result: result.result return
} }
}) const db = get(DataBase)
const selectedChar = get(selectedCharID)
luaEngine.global.set('getName', async (id:string) => { if(typeof name !== 'string'){
if(!LuaSafeIds.has(id)){ throw('Invalid data type')
return }
} db.characters[selectedChar].name = name
const db = get(DataBase) setDatabase(db)
const selectedChar = get(selectedCharID) })
const char = db.characters[selectedChar]
return char.name
})
luaEngine.global.set('setName', async (id:string, name:string) => { luaEngine.global.set('setDescription', async (id:string, desc:string) => {
if(!LuaSafeIds.has(id)){ if(!LuaSafeIds.has(id)){
return return
} }
const db = get(DataBase) const db = get(DataBase)
const selectedChar = get(selectedCharID) const selectedChar = get(selectedCharID)
if(typeof name !== 'string'){ const char =db.characters[selectedChar]
throw('Invalid data type') if(typeof data !== 'string'){
} throw('Invalid data type')
db.characters[selectedChar].name = name }
setDatabase(db) if(char.type === 'group'){
}) throw('Character is a group')
}
char.desc = desc
db.characters[selectedChar] = char
setDatabase(db)
})
luaEngine.global.set('setDescription', async (id:string, desc:string) => { luaEngine.global.set('setCharacterFirstMessage', async (id:string, data:string) => {
if(!LuaSafeIds.has(id)){ if(!LuaSafeIds.has(id)){
return return
} }
const db = get(DataBase) const db = get(DataBase)
const selectedChar = get(selectedCharID) const selectedChar = get(selectedCharID)
const char =db.characters[selectedChar] const char = db.characters[selectedChar]
if(typeof data !== 'string'){ if(typeof data !== 'string'){
throw('Invalid data type') return false
} }
if(char.type === 'group'){ char.firstMessage = data
throw('Character is a group') db.characters[selectedChar] = char
} setDatabase(db)
char.desc = desc return true
db.characters[selectedChar] = char })
setDatabase(db)
})
luaEngine.global.set('setCharacterFirstMessage', async (id:string, data:string) => { luaEngine.global.set('getCharacterFirstMessage', async (id:string) => {
if(!LuaSafeIds.has(id)){ if(!LuaSafeIds.has(id)){
return return
} }
const db = get(DataBase) const db = get(DataBase)
const selectedChar = get(selectedCharID) const selectedChar = get(selectedCharID)
const char = db.characters[selectedChar] const char = db.characters[selectedChar]
if(typeof data !== 'string'){ return char.firstMessage
return false })
}
char.firstMessage = data
db.characters[selectedChar] = char
setDatabase(db)
return true
})
luaEngine.global.set('getCharacterFirstMessage', async (id:string) => { luaEngine.global.set('getBackgroundEmbedding', async (id:string) => {
if(!LuaSafeIds.has(id)){ if(!LuaSafeIds.has(id)){
return return
} }
const db = get(DataBase) const db = get(DataBase)
const selectedChar = get(selectedCharID) const selectedChar = get(selectedCharID)
const char = db.characters[selectedChar] const char = db.characters[selectedChar]
return char.firstMessage return char.backgroundHTML
}) })
luaEngine.global.set('getBackgroundEmbedding', async (id:string) => { luaEngine.global.set('setBackgroundEmbedding', async (id:string, data:string) => {
if(!LuaSafeIds.has(id)){ if(!LuaSafeIds.has(id)){
return return
} }
const db = get(DataBase) const db = get(DataBase)
const selectedChar = get(selectedCharID) const selectedChar = get(selectedCharID)
const char = db.characters[selectedChar] if(typeof data !== 'string'){
return char.backgroundHTML return false
}) }
db.characters[selectedChar].backgroundHTML = data
setDatabase(db)
return true
})
luaEngine.global.set('setBackgroundEmbedding', async (id:string, data:string) => { await luaEngine.doString(luaCodeWarper(code))
if(!LuaSafeIds.has(id)){ luaEngineState.code = code
return
}
const db = get(DataBase)
const selectedChar = get(selectedCharID)
if(typeof data !== 'string'){
return false
}
db.characters[selectedChar].backgroundHTML = data
setDatabase(db)
return true
})
await luaEngine.doString(luaCodeWarper(code))
lastCode = code
}
let accessKey = v4()
if(mode === 'editDisplay'){
LuaEditDisplayIds.add(accessKey)
}
else{
LuaSafeIds.add(accessKey)
if(lowLevelAccess){
LuaLowLevelIds.add(v4())
} }
} let accessKey = v4()
let res:any if(mode === 'editDisplay'){
try { LuaEditDisplayIds.add(accessKey)
switch(mode){ }
case 'input':{ else{
const func = luaEngine.global.get('onInput') LuaSafeIds.add(accessKey)
if(func){ if(lowLevelAccess){
res = await func(accessKey) LuaLowLevelIds.add(accessKey)
} }
} }
case 'output':{ let res:any
const func = luaEngine.global.get('onOutput') const luaEngine = luaEngineState.engine
if(func){ try {
res = await func(accessKey) switch(mode){
} case 'input':{
} const func = luaEngine.global.get('onInput')
case 'start':{ if(func){
const func = luaEngine.global.get('onStart') res = await func(accessKey)
if(func){ }
res = await func(accessKey) break
} }
} case 'output':{
case 'editRequest': const func = luaEngine.global.get('onOutput')
case 'editDisplay': if(func){
case 'editInput': res = await func(accessKey)
case 'editOutput':{ }
const func = luaEngine.global.get('callListenMain') break
if(func){ }
res = await func(mode, accessKey, JSON.stringify(data)) case 'start':{
res = JSON.parse(res) const func = luaEngine.global.get('onStart')
} if(func){
} res = await func(accessKey)
default:{ }
const func = luaEngine.global.get(mode) break
if(func){ }
res = await func(accessKey) case 'editRequest':
} case 'editDisplay':
} case 'editInput':
} case 'editOutput':{
if(res === false){ const func = luaEngine.global.get('callListenMain')
stopSending = true if(func){
res = await func(mode, accessKey, JSON.stringify(data))
res = JSON.parse(res)
}
break
}
default:{
const func = luaEngine.global.get(mode)
if(func){
res = await func(accessKey)
}
break
}
}
if(res === false){
stopSending = true
}
} catch (error) {
console.error(error)
} }
} catch (error) {
console.error(error)
}
LuaSafeIds.delete(accessKey) LuaSafeIds.delete(accessKey)
LuaLowLevelIds.delete(accessKey) LuaLowLevelIds.delete(accessKey)
return { return {
stopSending, chat, res stopSending, chat, res
} }
})
} }
async function makeLuaFactory(){ async function makeLuaFactory(){