feat: add ccv3 decorators

This commit is contained in:
kwaroran
2024-05-25 11:04:27 +09:00
parent 41d2db2f59
commit 7cdf1918e5
2 changed files with 286 additions and 114 deletions

View File

@@ -4,7 +4,7 @@ import { CharEmotion, selectedCharID } from "../stores";
import { ChatTokenizer, tokenize, tokenizeNum } from "../tokenizer";
import { language } from "../../lang";
import { alertError } from "../alert";
import { loadLoreBookPrompt } from "./lorebook";
import { loadLoreBookPrompt, loadLoreBookV3Prompt } from "./lorebook";
import { findCharacterbyId, getAuthorNoteDefaultText, isLastCharPunctuation, trimUntilPunctuation } from "../util";
import { requestChatData } from "./request";
import { stableDiff } from "./stableDiff";
@@ -338,11 +338,36 @@ export async function sendChat(chatProcessIndex = -1,arg:{chatAdditonalTokens?:n
}
}
const lorepmt = await loadLoreBookPrompt()
unformated.lorebook.push({
role: 'system',
content: risuChatParser(lorepmt.act, {chara: currentChar})
const lorepmt = await loadLoreBookV3Prompt()
const normalActives = lorepmt.actives.filter(v => {
return v.pos === ''
})
console.log(normalActives)
for(const lorebook of normalActives){
unformated.lorebook.push({
role: lorebook.role,
content: risuChatParser(lorebook.prompt, {chara: currentChar})
})
}
const descActives = lorepmt.actives.filter(v => {
return v.pos === 'after_desc' || v.pos === 'before_desc' || v.pos === 'personality' || v.pos === 'scenario'
})
for(const lorebook of descActives){
const c = {
role: lorebook.role,
content: risuChatParser(lorebook.prompt, {chara: currentChar})
}
if(lorebook.pos === 'before_desc'){
unformated.description.unshift(c)
}
else{
unformated.description.push(c)
}
}
if(db.personaPrompt){
unformated.personaPrompt.push({
role: 'system',
@@ -365,10 +390,24 @@ export async function sendChat(chatProcessIndex = -1,arg:{chatAdditonalTokens?:n
}
}
if(lorepmt.special_act){
const postEverythingLorebooks = lorepmt.actives.filter(v => {
return v.pos === 'depth' && v.depth === 0 && v.role !== 'assistant'
})
for(const lorebook of postEverythingLorebooks){
unformated.postEverything.push({
role: 'system',
content: risuChatParser(lorepmt.special_act, {chara: currentChar})
role: lorebook.role,
content: risuChatParser(lorebook.prompt, {chara: currentChar})
})
}
//Since assistant needs to be prefill, we need to add assistant lorebooks after user/system lorebooks
const postEverythingAssistantLorebooks = lorepmt.actives.filter(v => {
return v.pos === 'depth' && v.depth === 0 && v.role === 'assistant'
})
for(const lorebook of postEverythingAssistantLorebooks){
unformated.postEverything.push({
role: lorebook.role,
content: risuChatParser(lorebook.prompt, {chara: currentChar})
})
}
@@ -652,6 +691,17 @@ export async function sendChat(chatProcessIndex = -1,arg:{chatAdditonalTokens?:n
index++
}
const depthPrompts = lorepmt.actives.filter(v => {
return (v.pos === 'depth' && v.depth > 0) || v.pos === 'reverse_depth'
})
for(const depthPrompt of depthPrompts){
const chat:OpenAIChat = {
role: depthPrompt.role,
content: risuChatParser(depthPrompt.prompt, {chara: currentChar})
}
currentTokens += await tokenizer.tokenizeChat(chat)
}
if(nowChatroom.supaMemory && (db.supaMemoryType !== 'none' || db.hanuraiEnable)){
chatProcessStage.set(2)
@@ -746,6 +796,14 @@ export async function sendChat(chatProcessIndex = -1,arg:{chatAdditonalTokens?:n
return v.content !== ''
})
for(const depthPrompt of depthPrompts){
const chat:OpenAIChat = {
role: depthPrompt.role,
content: risuChatParser(depthPrompt.prompt, {chara: currentChar})
}
const depth = depthPrompt.pos === 'depth' ? (depthPrompt.depth) : (unformated.chats.length - depthPrompt.depth)
unformated.chats.splice(depth,0,chat)
}
if(triggerResult){
if(triggerResult.additonalSysPrompt.promptend){

View File

@@ -8,6 +8,7 @@ import { language } from "../../lang";
import { downloadFile } from "../storage/globalApi";
import { HypaProcesser } from "./memory/hypamemory";
import { getModuleLorebooks } from "./modules";
import { CCardLib } from "@risuai/ccardlib";
export function addLorebook(type:number) {
let selectedID = get(selectedCharID)
@@ -219,15 +220,18 @@ export async function loadLoreBookV3Prompt(){
const loreDepth = char.loreSettings?.scanDepth ?? db.loreBookDepth
const loreToken = char.loreSettings?.tokenBudget ?? db.loreBookToken
const fullWordMatching = char.loreSettings?.fullWordMatching ?? false
const chatLength = currentChat.length + 1 //includes first message
const recursiveScanning = char.loreSettings?.recursiveScanning ?? false
let recursiveAdditionalPrompt = ''
const searchMatch = (text:Message[],arg:{
const searchMatch = (messages:Message[],arg:{
keys:string[],
searchDepth:number,
regex:boolean
fullWordMatching:boolean
}) => {
const sliced = text.slice(text.length - arg.searchDepth,text.length)
let mText = sliced.join(" ")
const sliced = messages.slice(messages.length - arg.searchDepth,messages.length)
let mText = sliced.join(" ") + recursiveAdditionalPrompt
if(arg.regex){
const regexString = arg.keys[0]
if(!regexString.startsWith('/')){
@@ -254,6 +258,7 @@ export async function loadLoreBookV3Prompt(){
})
}
else{
mText = mText.replace(/ /g,'')
return arg.keys.some((key) => {
return mText.includes(key.toLowerCase())
})
@@ -262,129 +267,238 @@ export async function loadLoreBookV3Prompt(){
}
let matching = true
let sactivated:string[] = []
let decoratedArray:{
let actives:{
depth:number,
pos:string,
prompt:string
role:'system'|'user'|'assistant'
priority:number
tokens:number
}[] = []
let activatied:string[] = []
let activatedIndexes:number[] = []
let disabledUIPrompts:string[] = []
while(matching){
matching = false
for(let i=0;i<fullLore.length;i++){
const decorated = decoratorParser(fullLore[i].content)
const searchDepth = decorated.decorators['scan_depth'] ? parseInt(decorated.decorators['scan_depth'][0]) : loreDepth
let matched = searchMatch(currentChat,{
keys: fullLore[i].key.split(','),
searchDepth: searchDepth,
regex: fullLore[i].key.startsWith('/'),
fullWordMatching: fullWordMatching
if(activatedIndexes.includes(i)){
continue
}
let activated = true
let pos = ''
let depth = 0
let scanDepth = loreDepth
let priority = fullLore[i].insertorder
let forceState:string = 'none'
let role:'system'|'user'|'assistant' = 'system'
let searchQueries:{
keys:string[],
negative:boolean
}[] = []
const content = CCardLib.decorator.parse(fullLore[i].content, (name, arg) => {
switch(name){
case 'end':{
pos = 'depth'
depth = 0
return
}
case 'activate_only_after':{
const int = parseInt(arg[0])
if(Number.isNaN(int)){
return false
}
if(chatLength < int){
activated = false
}
return
}
case 'activate_only_every': {
const int = parseInt(arg[0])
if(Number.isNaN(int)){
return false
}
if(chatLength % int !== 0){
activated = false
}
return
}
case 'keep_activate_after_match':{
//TODO
return false
}
case 'dont_activate_after_match': {
//TODO
return false
}
case 'depth':
case 'reverse_depth':{
const int = parseInt(arg[0])
if(Number.isNaN(int)){
return false
}
depth = int
pos = name === 'depth' ? 'depth' : 'reverse_depth'
return
}
case 'instruct_depth':
case 'reverse_instruct_depth':
case 'instruct_scan_depth':{
//the instruct mode does not exists in risu
return false
}
case 'role':{
if(arg[0] === 'user' || arg[0] === 'assistant' || arg[0] === 'system'){
role = arg[0]
return
}
return false
}
case 'scan_depth':{
scanDepth = parseInt(arg[0])
return
}
case 'is_greeting':{
const int = parseInt(arg[0])
if(Number.isNaN(int)){
return false
}
}
case 'position':{
if(["after_desc", "before_desc", "personality", "scenario"].includes(arg[0])){
pos = arg[0]
return
}
return false
}
case 'ignore_on_max_context':{
priority = -1000
return
}
case 'additional_keys':{
searchQueries.push({
keys: arg,
negative: false
})
return
}
case 'exclude_keys':{
searchQueries.push({
keys: arg,
negative: true
})
return
}
case 'is_user_icon':{
//TODO
return false
}
case 'activate':{
forceState = 'activate'
return
}
case 'dont_activate':{
forceState = 'deactivate'
return
}
case 'disable_ui_prompt':{
if(['post_history_instructions','system_prompt'].includes(arg[0])){
disabledUIPrompts.push(arg[0])
return
}
return false
}
default:{
return false
}
}
})
if(decorated.decorators['dont_activate']){
continue
}
if(!matched){
continue
}
const addtitionalKeys = decorated.decorators['additional_keys'] ?? []
if(addtitionalKeys.length > 0){
const additionalMatched = searchMatch(currentChat,{
keys: decorated.decorators['additional_keys'],
searchDepth: searchDepth,
regex: false,
fullWordMatching: fullWordMatching
})
if(!additionalMatched){
continue
}
}
const excludeKeys = decorated.decorators['exclude_keys'] ?? []
if(excludeKeys.length > 0){
const excludeMatched = searchMatch(currentChat,{
keys: decorated.decorators['exclude_keys'],
searchDepth: searchDepth,
regex: false,
fullWordMatching: fullWordMatching
})
if(excludeMatched){
continue
}
}
matching = true
fullLore.splice(i,1)
i--
const depth = decorated.decorators['depth'] ? parseInt(decorated.decorators['depth'][0]) : null
if(depth === 0){
sactivated.push(decorated.prompt)
continue
if(!activated || forceState !== 'none' || fullLore[i].alwaysActive){
//if the lore is not activated or force activated, skip the search
}
if(depth){
decoratedArray.push({
else if(fullLore[i].useRegex){
const match = searchMatch(currentChat, {
keys: [fullLore[i].key],
searchDepth: scanDepth,
regex: true,
fullWordMatching: fullWordMatching
})
if(!match){
activated = false
}
}
else{
searchQueries.push({
keys: fullLore[i].key.split(','),
negative: false
})
for(const query of searchQueries){
const result = searchMatch(currentChat, {
keys: query.keys,
searchDepth: scanDepth,
regex: false,
fullWordMatching: fullWordMatching
})
if(query.negative){
if(result){
activated = false
break
}
}
else{
if(!result){
activated = false
break
}
}
}
}
if(forceState === 'activate'){
activated = true
}
else if(forceState === 'deactivate'){
activated = false
}
if(activated){
actives.push({
depth: depth,
pos: '',
prompt: decorated.prompt
pos: pos,
prompt: content,
role: role,
priority: priority,
tokens: await tokenize(content)
})
continue
activatedIndexes.push(i)
if(recursiveScanning){
matching = true
recursiveAdditionalPrompt += content + '\n\n'
}
}
if(decorated.decorators['position']){
decoratedArray.push({
depth: -1,
pos: decorated.decorators['position'][0],
prompt: decorated.prompt
})
continue
}
activatied.push(decorated.prompt)
}
}
}
const supportedDecorators = ['depth','dont_activate','position','scan_depth','additional_keys', 'exclude_keys']
export function decoratorParser(prompt:string){
const split = prompt.split('\n')
let decorators:{[name:string]:string[]} = {}
const activesSorted = actives.sort((a,b) => {
return b.priority - a.priority
})
let fallbacking = false
for(let i=0;i<split.length;i++){
const line = split[i].trim()
if(line.startsWith('@@')){
const data = line.startsWith('@@@') ? line.replace('@@@','') : line.replace('@@','')
const name = data.split(' ')[0]
const values = data.replace(name,'').trim().split(',')
if(!supportedDecorators.includes(name)){
fallbacking = true
continue
}
if((!line.startsWith('@@@')) || fallbacking){
decorators[name] = values
}
let usedTokens = 0
const activesFiltered = activesSorted.filter((act) => {
if(usedTokens + act.tokens <= loreToken){
usedTokens += act.tokens
return true
}
else if(line === '@@end' || line === '@@@end'){
decorators['depth'] = ['0']
}
else{
return {
prompt: split.slice(i).join('\n').trim(),
decorators: decorators
}
}
}
return false
})
return {
prompt: '',
decorators: decorators
actives: activesFiltered,
}
}
export async function importLoreBook(mode:'global'|'local'|'sglobal'){