mirror of
https://github.com/doolijb/serene-pub.git
synced 2026-07-09 15:58:23 +00:00
prompt builder rebuild
This commit is contained in:
parent
20fea91d0d
commit
f8a9715853
23 changed files with 2976 additions and 1574 deletions
|
|
@ -182,11 +182,11 @@ Scenario: """ {{{scenario}}} """
|
|||
```
|
||||
{{/if}}
|
||||
|
||||
{{#if wiBefore}} {{{wiBefore}}} {{/if}} {{/systemBlock}}
|
||||
{{#if exampleDialogue}} {{{exampleDialogue}}} {{/if}} {{/systemBlock}}
|
||||
|
||||
{{#each chatMessages}} {{#if (eq role "assistant")}} {{#assistantBlock}} {{{name}}}: {{{message}}} {{/assistantBlock}} {{/if}} {{#if (eq role "user")}} {{#userBlock}} {{{name}}}: {{{message}}} {{/userBlock}} {{/if}} {{/each}}
|
||||
|
||||
{{#if wiAfter}} {{#systemBlock}} {{{wiAfter}}} {{/systemBlock}} {{/if}}
|
||||
{{#if postHistoryInstructions}} {{#systemBlock}} {{{postHistoryInstructions}}} {{/systemBlock}} {{/if}}
|
||||
````
|
||||
|
||||
|
||||
|
|
|
|||
4
src/app.d.ts
vendored
4
src/app.d.ts
vendored
|
|
@ -917,8 +917,8 @@ declare global {
|
|||
nickname?: string
|
||||
description: boolean
|
||||
personality: boolean
|
||||
wiBefore: boolean
|
||||
wiAfter: boolean
|
||||
exampleDialogue: boolean
|
||||
postHistoryInstructions: boolean
|
||||
postHistoryInstructions: boolean
|
||||
}>
|
||||
personas: Array<{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,94 @@
|
|||
<script lang="ts">
|
||||
import * as Icons from "@lucide/svelte"
|
||||
import { Modal } from "@skeletonlabs/skeleton-svelte"
|
||||
|
||||
interface Props {
|
||||
open: boolean
|
||||
modelName: string
|
||||
onClose: () => void
|
||||
onContinue: () => void
|
||||
}
|
||||
|
||||
let {
|
||||
open = $bindable(),
|
||||
modelName,
|
||||
onClose,
|
||||
onContinue
|
||||
}: Props = $props()
|
||||
|
||||
// Generate the Ollama library URL for the model
|
||||
let ollamaUrl = $derived(() => {
|
||||
if (!modelName) return ""
|
||||
|
||||
// Extract the base model name (remove any version/tag info)
|
||||
const baseModelName = modelName.split(":")[0]
|
||||
return `https://ollama.com/library/${baseModelName}/tags`
|
||||
})
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
{open}
|
||||
onOpenChange={(e) => (open = e.open)}
|
||||
contentBase="card bg-surface-100-900 p-6 space-y-4 shadow-xl w-[40em] max-w-dvw-lg border border-surface-300-700"
|
||||
backdropClasses="backdrop-blur-sm"
|
||||
>
|
||||
{#snippet content()}
|
||||
<header class="flex items-center justify-between">
|
||||
<h2 class="h2">Select Model Version</h2>
|
||||
<button onclick={onClose} class="btn-icon btn-icon-sm">
|
||||
<Icons.X size={16} />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<article class="space-y-4">
|
||||
<div class="bg-primary-100 dark:bg-primary-900 border-primary-300 dark:border-primary-700 rounded-lg border p-4">
|
||||
<div class="flex items-start gap-3">
|
||||
<Icons.Info size={20} class="text-primary-600 mt-0.5 flex-shrink-0" />
|
||||
<div class="flex-1">
|
||||
<h3 class="font-semibold text-primary-800 dark:text-primary-200 mb-2">
|
||||
Choose Your Model Version
|
||||
</h3>
|
||||
<p class="text-primary-700-300 text-sm mb-3">
|
||||
To download <strong>{modelName}</strong>, you'll need to select a specific version from the Ollama library.
|
||||
</p>
|
||||
|
||||
<div class="space-y-2 text-sm">
|
||||
<p class="text-primary-700-300">
|
||||
<strong>Step 1:</strong> Click the button below to open the model's page on Ollama.com
|
||||
</p>
|
||||
<p class="text-primary-700-300">
|
||||
<strong>Step 2:</strong> Browse the available versions and copy the full name of the one you want
|
||||
</p>
|
||||
<p class="text-primary-700-300">
|
||||
<strong>Step 3:</strong> Come back here and paste the name in the next screen
|
||||
</p>
|
||||
<p class="text-primary-700-300"><em>(Q4_K_M is generally recommended for a balance between performance and resource usage)</em></p>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<a
|
||||
href={ollamaUrl()}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn preset-filled-primary-500 inline-flex items-center gap-2"
|
||||
>
|
||||
<Icons.ExternalLink size={16} />
|
||||
View {modelName} on Ollama.com
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<footer class="flex justify-end gap-3">
|
||||
<button class="btn preset-tonal" onclick={onClose}>
|
||||
Cancel
|
||||
</button>
|
||||
<button class="btn preset-filled-primary-500" onclick={onContinue}>
|
||||
<Icons.ArrowRight size={16} />
|
||||
Continue to Download
|
||||
</button>
|
||||
</footer>
|
||||
{/snippet}
|
||||
</Modal>
|
||||
|
|
@ -102,8 +102,8 @@ Story history:
|
|||
\`\`\`
|
||||
{{/if}}
|
||||
|
||||
{{#if wiBefore}}
|
||||
{{{wiBefore}}}
|
||||
{{#if exampleDialogue}}
|
||||
{{{exampleDialogue}}}
|
||||
{{/if}}
|
||||
{{/systemBlock}}
|
||||
|
||||
|
|
@ -120,9 +120,9 @@ Story history:
|
|||
{{/if}}
|
||||
{{/each}}
|
||||
|
||||
{{#if wiAfter}}
|
||||
{{#if postHistoryInstructions}}
|
||||
{{#systemBlock}}
|
||||
{{{wiAfter}}}
|
||||
{{{postHistoryInstructions}}}
|
||||
{{/systemBlock}}
|
||||
{{/if}}`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,17 +33,6 @@ async function runMigrations() {
|
|||
} as MigrationConfig)
|
||||
console.log("Migrations applied.")
|
||||
await sync()
|
||||
|
||||
////
|
||||
// DEPRECATE: Temporary migration from SQLite to PostgreSQL - remove in 0.4.0
|
||||
////
|
||||
const { dbPath: sqliteDbPath } = await import("../db_old/drizzle.config")
|
||||
const sqliteDbExists = fs.existsSync(sqliteDbPath)
|
||||
if (sqliteDbExists) {
|
||||
console.log("SQLite database found, migrating to PostgreSQL...")
|
||||
const { migrateToPg } = await import("../db_old/migrateToPg")
|
||||
await migrateToPg()
|
||||
}
|
||||
}
|
||||
|
||||
// Check if database has been initialized by looking for a specific table
|
||||
|
|
|
|||
|
|
@ -363,8 +363,7 @@ export const characters = pgTable("characters", {
|
|||
}), // Optional FK to lorebooks.id
|
||||
extensions: json("extensions").notNull().default({}).$type<Record<string, any>>() ,
|
||||
isFavorite: boolean("is_favorite").notNull().default(false) // 1 if favorite, 0 otherwise
|
||||
}, table => ({
|
||||
}))
|
||||
})
|
||||
|
||||
export const charactersRelations = relations(characters, ({ many, one }) => ({
|
||||
user: one(users, {
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
import { defineConfig } from "drizzle-kit"
|
||||
import envPaths from "env-paths"
|
||||
import { mkdirSync } from "fs"
|
||||
import { existsSync } from "fs"
|
||||
|
||||
const isCI = process.env.CI === "true";
|
||||
export const dataDir = isCI ? "~/SerenePubData" : (envPaths("SerenePub", { suffix: "" }).data + "/data")
|
||||
export const dbPath = `${dataDir}/main.db`
|
||||
export const migrationsDir = "./drizzle"
|
||||
export const schemaDir = "./src/lib/server/db/schema.ts"
|
||||
|
||||
if (!existsSync(dataDir)) {
|
||||
mkdirSync(dataDir, { recursive: true });
|
||||
}
|
||||
|
||||
console.log(`Checking sqlite database path (deprecated): ${dbPath}`)
|
||||
|
||||
// Create the data directory if it doesn't exist
|
||||
|
||||
|
||||
export default defineConfig({
|
||||
schema: schemaDir,
|
||||
dialect: "sqlite",
|
||||
dbCredentials: { url: process.env.DATABASE_URL || dbPath },
|
||||
verbose: true,
|
||||
strict: true,
|
||||
out: migrationsDir
|
||||
})
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
import { drizzle } from "drizzle-orm/better-sqlite3"
|
||||
import Database from "better-sqlite3"
|
||||
import * as schema from "./schema"
|
||||
import { migrate } from "drizzle-orm/better-sqlite3/migrator"
|
||||
import * as dbConfig from "./drizzle.config"
|
||||
import type { MigrationConfig } from "drizzle-orm/migrator"
|
||||
import { copyFileSync } from "fs"
|
||||
import { basename, dirname, extname } from "path"
|
||||
import fs from "fs"
|
||||
import { dev } from "$app/environment"
|
||||
|
||||
|
||||
const client = new Database(process.env.DATABASE_URL || dbConfig.dbPath)
|
||||
|
||||
export const db = drizzle(client, { schema })
|
||||
|
||||
// Compare two version strings in '0.0.0' format
|
||||
export function compareVersions(a: string, b: string): -1 | 0 | 1 {
|
||||
const pa = a.split('.').map(Number)
|
||||
const pb = b.split('.').map(Number)
|
||||
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
||||
const na = pa[i] || 0
|
||||
const nb = pb[i] || 0
|
||||
if (na < nb) return -1
|
||||
if (na > nb) return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
async function runMigrations(oldVersion?: string) {
|
||||
// Backup database before migration in production
|
||||
const dbFile = process.env.DATABASE_URL || dbConfig.dbPath
|
||||
const dir = dirname(dbFile)
|
||||
const base = basename(dbFile, extname(dbFile))
|
||||
const ext = extname(dbFile)
|
||||
const timestamp = new Date()
|
||||
.toISOString()
|
||||
.replace(/[-:T.]/g, "")
|
||||
.slice(0, 14)
|
||||
const backupFile = `${dir}/${base}_backup_v${oldVersion}_${timestamp}${ext}`
|
||||
try {
|
||||
copyFileSync(dbFile, backupFile)
|
||||
console.log(`Database backup created: ${backupFile}`)
|
||||
} catch (err) {
|
||||
console.error("Failed to create database backup:", err)
|
||||
throw new Error("Database backup failed, migration aborted.")
|
||||
}
|
||||
migrate(db, {
|
||||
migrationsFolder: dbConfig.migrationsDir
|
||||
} as MigrationConfig)
|
||||
console.log("Migrations applied.")
|
||||
}
|
||||
|
||||
// Run migrations if in production environment
|
||||
if (!dev) {
|
||||
|
||||
// If it doesn't exist, create a meta.json file in the data directory
|
||||
const metaPath = dbConfig.dataDir + "/meta.json"
|
||||
// Check if the file exists
|
||||
if (!fs.existsSync(metaPath)) {
|
||||
// Create the file with default content
|
||||
fs.writeFileSync(metaPath, JSON.stringify({ version: "0.0.0" }, null, 2))
|
||||
}
|
||||
|
||||
// Check meta.json for version
|
||||
const meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"))
|
||||
// @ts-ignore
|
||||
const appVersion = __APP_VERSION__
|
||||
if (!appVersion) {
|
||||
throw new Error("App version is not defined. Please set __APP_VERSION__.")
|
||||
}
|
||||
const versionCompare = compareVersions(meta.version, appVersion)
|
||||
|
||||
switch (versionCompare) {
|
||||
case 0:
|
||||
console.log("No migration needed, versions match.")
|
||||
break
|
||||
case -1:
|
||||
console.log("Running migrations to update database schema...")
|
||||
runMigrations(meta.version)
|
||||
meta.version = appVersion
|
||||
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2))
|
||||
console.log(`Updated meta.json to version ${appVersion}.`)
|
||||
break
|
||||
case 1:
|
||||
console.warn(`Warning: Database version (${meta.version}) is newer than app version (${appVersion}).`)
|
||||
// This could happen if the app version is rolled back or if the database was manually updated
|
||||
// Handle this case as needed, e.g., notify the user or log an error
|
||||
throw new Error(
|
||||
`Database version (${meta.version}) is newer than app version (${appVersion}). Please check your database integrity.`
|
||||
)
|
||||
default:
|
||||
console.error("Unexpected version comparison result:", versionCompare)
|
||||
throw new Error("Unexpected version comparison result")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,211 +0,0 @@
|
|||
import { db as sqlite } from "./index"
|
||||
import { db as pg } from "../db/index"
|
||||
import * as schema from "../db/schema"
|
||||
import { eq, sql } from "drizzle-orm"
|
||||
|
||||
// This function migrates the SQLite database to PostgreSQL
|
||||
// This will only be available in 0.3.0, remove in 0.4.0
|
||||
export async function migrateToPg() {
|
||||
await pg
|
||||
.transaction(async (tx) => {
|
||||
// Get non default sampling configs from SQLite
|
||||
const samplingConfigs = await sqlite.query.samplingConfigs.findMany({
|
||||
where: (s, { eq }) => eq(s.isImmutable, false)
|
||||
})
|
||||
samplingConfigs.forEach(async (config) => {
|
||||
return await tx.insert(schema.samplingConfigs).values({
|
||||
id: config.id,
|
||||
name: config.name,
|
||||
isImmutable: config.isImmutable,
|
||||
temperatureEnabled: config.temperatureEnabled,
|
||||
contextTokensEnabled: config.contextTokensEnabled,
|
||||
responseTokensEnabled: config.responseTokensEnabled
|
||||
})
|
||||
})
|
||||
// Get non default context configs from SQLite
|
||||
const contextConfigs = await sqlite.query.contextConfigs.findMany({
|
||||
where: (c, { eq }) => eq(c.isImmutable, false)
|
||||
})
|
||||
contextConfigs.forEach(async (config) => {
|
||||
return await tx.insert(schema.contextConfigs).values({
|
||||
id: config.id,
|
||||
name: config.name,
|
||||
isImmutable: false,
|
||||
template: config.template
|
||||
})
|
||||
})
|
||||
// Get non default prompt configs from SQLite
|
||||
const promptConfigs = await sqlite.query.promptConfigs.findMany({
|
||||
where: (p, { eq }) => eq(p.isImmutable, false)
|
||||
})
|
||||
promptConfigs.forEach(async (config) => {
|
||||
return await tx.insert(schema.promptConfigs).values({
|
||||
id: config.id,
|
||||
name: config.name,
|
||||
isImmutable: false,
|
||||
systemPrompt: config.systemPrompt
|
||||
})
|
||||
})
|
||||
// Get connections from SQLite
|
||||
const connections = await sqlite.query.connections.findMany()
|
||||
connections.forEach(async (connection) => {
|
||||
return await tx.insert(schema.connections).values({
|
||||
id: connection.id,
|
||||
name: connection.name,
|
||||
baseUrl: connection.baseUrl,
|
||||
type: connection.type,
|
||||
tokenCounter: connection.tokenCounter,
|
||||
model: connection.model,
|
||||
promptFormat: connection.promptFormat,
|
||||
extraJson: connection.extraJson || {},
|
||||
})
|
||||
})
|
||||
// Get characters from SQLite
|
||||
const characters = await sqlite.query.characters.findMany()
|
||||
characters.forEach(async (character) => {
|
||||
// Convert any String[] fields to string[] (primitive) or text as needed
|
||||
const safeCharacter = {
|
||||
...character,
|
||||
// alternateGreetings should be string[] for JSON field
|
||||
alternateGreetings: character.alternateGreetings
|
||||
? Array.isArray(character.alternateGreetings)
|
||||
? character.alternateGreetings.map((s: any) => String(s))
|
||||
: [String(character.alternateGreetings)]
|
||||
: [],
|
||||
// exampleDialogues should be string for text field
|
||||
exampleDialogues: character.exampleDialogues
|
||||
? (typeof character.exampleDialogues === 'string'
|
||||
? character.exampleDialogues
|
||||
: Array.isArray(character.exampleDialogues)
|
||||
? (character.exampleDialogues as any[]).join('\n')
|
||||
: String(character.exampleDialogues))
|
||||
: null,
|
||||
}
|
||||
return await tx.insert(schema.characters).values({
|
||||
name: safeCharacter.name,
|
||||
description: safeCharacter.description ?? "",
|
||||
userId: safeCharacter.userId,
|
||||
nickname: safeCharacter.nickname ?? null,
|
||||
characterVersion: safeCharacter.characterVersion ?? null,
|
||||
personality: safeCharacter.personality ?? null,
|
||||
scenario: safeCharacter.scenario ?? null,
|
||||
firstMessage: safeCharacter.firstMessage ?? null,
|
||||
alternateGreetings: safeCharacter.alternateGreetings as string[] ?? null,
|
||||
exampleDialogues: safeCharacter.exampleDialogues ?? null,
|
||||
avatar: safeCharacter.avatar ?? null,
|
||||
creatorNotes: safeCharacter.creatorNotes ?? null,
|
||||
creatorNotesMultilingual: safeCharacter.creatorNotesMultilingual ?? null,
|
||||
groupOnlyGreetings: safeCharacter.groupOnlyGreetings ?? null,
|
||||
postHistoryInstructions: safeCharacter.postHistoryInstructions ?? null,
|
||||
isFavorite: !!safeCharacter.isFavorite,
|
||||
})
|
||||
})
|
||||
// Get personas from SQLite
|
||||
const personas = await sqlite.query.personas.findMany()
|
||||
personas.forEach(async (persona) => {
|
||||
return await tx.insert(schema.personas).values({
|
||||
name: persona.name || "",
|
||||
description: persona.description || "",
|
||||
userId: persona.userId,
|
||||
avatar: persona.avatar || null,
|
||||
isDefault: !!persona.isDefault,
|
||||
position: persona.position || null
|
||||
})
|
||||
})
|
||||
// Get chats from SQLite
|
||||
const chats = await sqlite.query.chats.findMany()
|
||||
chats.forEach(async (chat) => {
|
||||
return await tx.insert(schema.chats).values({
|
||||
id: chat.id,
|
||||
name: chat.name || "", // Add the required name property
|
||||
userId: chat.userId,
|
||||
scenario: chat.scenario,
|
||||
isGroup: !!chat.isGroup,
|
||||
groupReplyStrategy: chat.group_reply_strategy,
|
||||
})
|
||||
})
|
||||
// Get chatCharacters from SQLite
|
||||
const chatCharacters = await sqlite.query.chatCharacters.findMany()
|
||||
chatCharacters.forEach(async (chatCharacter) => {
|
||||
// Map null isActive to undefined for compatibility with schema
|
||||
const mappedChatCharacter = {
|
||||
...chatCharacter,
|
||||
isActive: chatCharacter.isActive === null ? undefined : chatCharacter.isActive,
|
||||
}
|
||||
return await tx.insert(schema.chatCharacters).values(mappedChatCharacter)
|
||||
})
|
||||
// Get chatPersonas from SQLite
|
||||
const chatPersonas = await sqlite.query.chatPersonas.findMany()
|
||||
chatPersonas.forEach(async (chatPersona) => {
|
||||
return await tx.insert(schema.chatPersonas).values(chatPersona)
|
||||
})
|
||||
// Get chatMessages from SQLite
|
||||
const chatMessages = await sqlite.query.chatMessages.findMany()
|
||||
chatMessages.forEach(async (chatMessage) => {
|
||||
return await tx.insert(schema.chatMessages).values({
|
||||
id: chatMessage.id,
|
||||
userId: chatMessage.userId,
|
||||
content: chatMessage.content, // Add the required content property
|
||||
chatId: chatMessage.chatId,
|
||||
characterId: chatMessage.characterId ?? null,
|
||||
personaId: chatMessage.personaId ?? null,
|
||||
adapterId: chatMessage.adapterId ?? null,
|
||||
role: chatMessage.role ?? "user",
|
||||
isEdited: !!chatMessage.isEdited,
|
||||
isGenerating: !!chatMessage.isGenerating,
|
||||
isHidden: !!chatMessage.isHidden,
|
||||
})
|
||||
})
|
||||
// Get users from SQLite
|
||||
const users = await sqlite.query.users.findMany()
|
||||
users.forEach(async (user) => {
|
||||
return await tx.update(schema.users)
|
||||
.set({
|
||||
activeConnectionId: user.activeConnectionId,
|
||||
activeSamplingConfigId: user.activeSamplingConfigId,
|
||||
activeContextConfigId: user.activeContextConfigId,
|
||||
activePromptConfigId: user.activePromptConfigId,
|
||||
})
|
||||
.where(eq(schema.users.id, user.id))
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Migration to PostgreSQL failed:", error)
|
||||
throw error
|
||||
})
|
||||
.then(() => {
|
||||
console.log("Migration to PostgreSQL completed successfully.")
|
||||
})
|
||||
|
||||
// Sync up the serial sequence for each of the tables in PostgreSQL
|
||||
const tables = [
|
||||
"chat_messages",
|
||||
"chats",
|
||||
"characters",
|
||||
"connections",
|
||||
"context_configs",
|
||||
"history_entries",
|
||||
"lorebooks",
|
||||
"lorebook_bindings",
|
||||
"world_lore_entries",
|
||||
"character_lore_entries",
|
||||
"personas",
|
||||
"prompt_configs",
|
||||
"sampling_configs",
|
||||
"users"
|
||||
]
|
||||
|
||||
const queries: Promise<any>[] = []
|
||||
tables.forEach((table) => {
|
||||
queries.push(
|
||||
pg.execute(
|
||||
`SELECT setval(
|
||||
pg_get_serial_sequence('${table}', 'id'),
|
||||
(SELECT COALESCE(MAX(id), 1) FROM ${table})
|
||||
);`
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
await Promise.all(queries)
|
||||
}
|
||||
|
|
@ -1,411 +0,0 @@
|
|||
import { updated } from "$app/state"
|
||||
import { relations, sql } from "drizzle-orm"
|
||||
import {
|
||||
sqliteTable,
|
||||
integer,
|
||||
text,
|
||||
numeric,
|
||||
real,
|
||||
blob,
|
||||
SQLiteBoolean
|
||||
} from "drizzle-orm/sqlite-core"
|
||||
import { TokenCounterManager } from "../utils/TokenCounterManager"
|
||||
import { group } from "console"
|
||||
import { GroupReplyStrategies } from "../../shared/constants/GroupReplyStrategies"
|
||||
|
||||
export const users = sqliteTable("users", {
|
||||
id: integer("id").primaryKey(),
|
||||
username: text("username").notNull(),
|
||||
activeConnectionId: integer("active_connection_id").references(() => connections.id, {
|
||||
onDelete: "set null"
|
||||
}),
|
||||
activeSamplingConfigId: integer("active_sampling_id").references(() => samplingConfigs.id, {
|
||||
onDelete: "set null"
|
||||
}),
|
||||
activeContextConfigId: integer("active_context_config_id").references(() => contextConfigs.id, {
|
||||
onDelete: "set null"
|
||||
}),
|
||||
activePromptConfigId: integer("active_prompt_config_id").references(() => promptConfigs.id, {
|
||||
onDelete: "set null"
|
||||
})
|
||||
})
|
||||
|
||||
export const userRelations = relations(users, ({ many, one }) => ({
|
||||
lorebooks: many(lorebooks),
|
||||
characters: many(characters),
|
||||
activeSamplingConfig: one(samplingConfigs, {
|
||||
fields: [users.activeSamplingConfigId],
|
||||
references: [samplingConfigs.id]
|
||||
}),
|
||||
activeConnection: one(connections, {
|
||||
fields: [users.activeConnectionId],
|
||||
references: [connections.id]
|
||||
}),
|
||||
activeContextConfig: one(contextConfigs, {
|
||||
fields: [users.activeContextConfigId],
|
||||
references: [contextConfigs.id]
|
||||
}),
|
||||
activePromptConfig: one(promptConfigs, {
|
||||
fields: [users.activePromptConfigId],
|
||||
references: [promptConfigs.id]
|
||||
}),
|
||||
personas: many(personas)
|
||||
}))
|
||||
|
||||
export const samplingConfigs = sqliteTable("sampling_configs", {
|
||||
id: integer("id").primaryKey(),
|
||||
name: text("name").notNull(), // Name for this sampling config (for selection)
|
||||
isImmutable: integer("is_immutable", { mode: "boolean" }).default(0), // Is this the built-in config? Then we don't want to allow mutation/deletion
|
||||
|
||||
// Tuned defaults for roleplay:
|
||||
// More creative and less repetitive
|
||||
temperature: real("temperature").default(0.7), // Higher = more creative
|
||||
temperatureEnabled: integer("temperature_enabled", { mode: "boolean" }).default(true),
|
||||
|
||||
topP: real("top_p").default(0.92), // Lower than 1, encourages diversity but not too random
|
||||
topPEnabled: integer("top_p_enabled", { mode: "boolean" }).default(true),
|
||||
|
||||
topK: integer("top_k").default(80), // Allows more token options for creative replies
|
||||
topKEnabled: integer("top_k_enabled", { mode: "boolean" }).default(true),
|
||||
|
||||
repetitionPenalty: real("repetition_penalty").default(1.15), // Slightly encourages less repetition but not too harsh
|
||||
repetitionPenaltyEnabled: integer("repetition_penalty_enabled", { mode: "boolean" }).default(
|
||||
true
|
||||
),
|
||||
|
||||
frequencyPenalty: real("frequency_penalty").default(0.2), // Mild penalty for repetitive phrases
|
||||
frequencyPenaltyEnabled: integer("frequency_penalty_enabled", { mode: "boolean" }).default(
|
||||
true
|
||||
),
|
||||
|
||||
presencePenalty: real("presence_penalty").default(0.6), // Encourage new topics and freshness
|
||||
presencePenaltyEnabled: integer("presence_penalty_enabled", { mode: "boolean" }).default(true),
|
||||
|
||||
responseTokens: integer("response_tokens").default(512), // Allow longer, richer replies
|
||||
responseTokensEnabled: integer("response_tokens_enabled", { mode: "boolean" }).default(true),
|
||||
responseTokensUnlocked: integer("response_tokens_unlocked", { mode: "boolean" }).default(false), // Dynamic length allowed
|
||||
|
||||
contextTokens: integer("context_tokens").default(4096), // Keep more conversation in memory/context
|
||||
contextTokensEnabled: integer("context_tokens_enabled", { mode: "boolean" }).default(true),
|
||||
contextTokensUnlocked: integer("context_tokens_unlocked", { mode: "boolean" }).default(false), // Allow for context window expansion
|
||||
|
||||
seed: integer("seed").default(-1), // -1 for random, can be used for deterministic sampling
|
||||
seedEnabled: integer("seed_enabled", { mode: "boolean" }).default(false)
|
||||
})
|
||||
|
||||
export const samplingRelations = relations(samplingConfigs, () => ({}))
|
||||
|
||||
export const connections = sqliteTable("connections", {
|
||||
id: integer("id").primaryKey(),
|
||||
name: text("name").notNull(), // Connection name (e.g., ollama, llama, chatgpt)
|
||||
type: text("type").notNull(), // Connection type/category (e.g., ollama, chatgpt, etc)
|
||||
baseUrl: text("base_url"), // Base URL or endpoint for API
|
||||
model: text("model"), // Model name or identifier
|
||||
// Ollama-specific options
|
||||
extraJson: text("extra_json", { mode: "json" }).$type<Record<string, any>>(), // Additional JSON options for the connections, api keys, etc.
|
||||
tokenCounter: text("token_counter").notNull().default("estimate"),
|
||||
promptFormat: text("prompt_format").default("vicuna")
|
||||
})
|
||||
|
||||
export const connectionsRelations = relations(connections, () => ({}))
|
||||
|
||||
export const contextConfigs = sqliteTable("context_configs", {
|
||||
id: integer("id").primaryKey(),
|
||||
isImmutable: integer("is_immutable", { mode: "boolean" }).default(true),
|
||||
name: text("name").notNull(),
|
||||
template: text("template"), // Sillytavern storyString
|
||||
alwaysForceName: integer("always_force_name", { mode: "boolean" }).default(true) // Always force name2
|
||||
})
|
||||
|
||||
export const contextConfigsRelations = relations(contextConfigs, () => ({}))
|
||||
|
||||
export const promptConfigs = sqliteTable("prompt_configs", {
|
||||
id: integer("id").primaryKey(),
|
||||
isImmutable: integer("is_immutable", { mode: "boolean" }).default(true),
|
||||
name: text("name").notNull(),
|
||||
systemPrompt: text("system_prompt").notNull() // Maps to sillytavern sysPrompt.content
|
||||
})
|
||||
|
||||
export const promptConfigsRelations = relations(promptConfigs, () => ({}))
|
||||
|
||||
export const lorebooks = sqliteTable("lorebooks", {
|
||||
id: integer("id").primaryKey(),
|
||||
userId: integer("user_id")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }), // FK to users.id
|
||||
name: text("name").notNull(), // Lorebook name
|
||||
description: text("description"), // Lorebook description
|
||||
tags: text("tags"), // JSON array of tags
|
||||
entries: text("entries"), // JSON array of lorebook entries (for compatibility with SillyTavern)
|
||||
metadata: text("metadata"), // JSON object for any extra SillyTavern/world/lorebook fields
|
||||
createdAt: text("created_at"), // ISO date string
|
||||
updatedAt: text("updated_at") // ISO date string
|
||||
})
|
||||
|
||||
export const lorebooksRelations = relations(lorebooks, ({ many, one }) => ({
|
||||
entries: many(lorebookEntries),
|
||||
user: one(users, {
|
||||
fields: [lorebooks.userId],
|
||||
references: [users.id]
|
||||
})
|
||||
}))
|
||||
|
||||
export const lorebookEntries = sqliteTable("lorebook_entries", {
|
||||
id: integer("id").primaryKey(),
|
||||
lorebookId: integer("lorebook_id")
|
||||
.notNull()
|
||||
.references(() => lorebooks.id, { onDelete: "cascade" }), // FK to lorebooks.id
|
||||
key: text("key"), // JSON array of keys
|
||||
keySecondary: text("key_secondary"), // JSON array of secondary keys
|
||||
comment: text("comment"),
|
||||
content: text("content"),
|
||||
constant: integer("constant", { mode: "boolean" }).default(false), // Is this entry a constant value?
|
||||
vectorized: integer("vectorized"),
|
||||
selective: integer("selective"),
|
||||
selectiveLogic: integer("selective_logic"),
|
||||
addMemo: integer("add_memo"),
|
||||
order: integer("order"),
|
||||
position: integer("position"),
|
||||
disable: integer("disable", { mode: "boolean" }).default(false), // Is this entry disabled?
|
||||
excludeRecursion: integer("exclude_recursion"),
|
||||
preventRecursion: integer("prevent_recursion"),
|
||||
delayUntilRecursion: integer("delay_until_recursion"),
|
||||
probability: integer("probability"),
|
||||
useProbability: integer("use_probability"),
|
||||
depth: integer("depth"),
|
||||
group: text("group"),
|
||||
groupOverride: integer("group_override"),
|
||||
groupWeight: integer("group_weight"),
|
||||
scanDepth: integer("scan_depth"),
|
||||
caseSensitive: integer("case_sensitive"),
|
||||
matchWholeWords: integer("match_whole_words"),
|
||||
useGroupScoring: integer("use_group_scoring"),
|
||||
automationId: text("automation_id"),
|
||||
role: text("role"),
|
||||
sticky: integer("sticky"),
|
||||
cooldown: integer("cooldown"),
|
||||
delay: integer("delay"),
|
||||
displayIndex: integer("display_index")
|
||||
})
|
||||
|
||||
export const lorebookEntriesRelations = relations(lorebookEntries, ({ one }) => ({
|
||||
lorebook: one(lorebooks, {
|
||||
fields: [lorebookEntries.lorebookId],
|
||||
references: [lorebooks.id]
|
||||
})
|
||||
}))
|
||||
|
||||
export const tags = sqliteTable("tags", {
|
||||
id: integer("id").primaryKey(),
|
||||
name: text("name").notNull(), // Tag name (unique)
|
||||
description: text("description")
|
||||
})
|
||||
|
||||
export const tagsRelations = relations(tags, ({ many }) => ({
|
||||
characterTags: many(characterTags)
|
||||
}))
|
||||
|
||||
export const characterTags = sqliteTable("character_tags", {
|
||||
characterId: integer("character_id")
|
||||
.notNull()
|
||||
.references(() => characters.id, { onDelete: "cascade" }), // FK to characters.id
|
||||
tagId: integer("tag_id")
|
||||
.notNull()
|
||||
.references(() => tags.id, { onDelete: "cascade" }) // FK to tags.id
|
||||
})
|
||||
|
||||
export const characterTagsRelations = relations(characterTags, ({ one }) => ({
|
||||
character: one(characters, {
|
||||
fields: [characterTags.characterId],
|
||||
references: [characters.id]
|
||||
}),
|
||||
tag: one(tags, {
|
||||
fields: [characterTags.tagId],
|
||||
references: [tags.id]
|
||||
})
|
||||
}))
|
||||
|
||||
export const characters = sqliteTable("characters", {
|
||||
id: integer("id").primaryKey(),
|
||||
userId: integer("user_id")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }), // FK to users.id
|
||||
name: text("name").notNull(),
|
||||
nickname: text("nickname"), // Optional nickname
|
||||
characterVersion: text("character_version").default("1.0"), // Version of the character schema
|
||||
description: text("description").notNull(),
|
||||
personality: text("personality"), // Persona field
|
||||
scenario: text("scenario"),
|
||||
firstMessage: text("first_message"),
|
||||
alternateGreetings: text("alternate_greetings", { mode: "json" })
|
||||
.default("[]")
|
||||
.$type<string[]>(), // JSON array of alternate greetings
|
||||
exampleDialogues: text("example_dialogues"), // JSON/text
|
||||
metadata: text("metadata"), // JSON/text for extra fields
|
||||
avatar: text("avatar"), // Path or URL to avatar image
|
||||
creatorNotes: text("creator_notes"), // Notes from the character creator
|
||||
creatorNotesMultilingual: text("creator_notes_multilingual", { mode: "json" })
|
||||
.default("{}")
|
||||
.$type<Record<string, string>>(), // Multilingual creator notes as JSON object
|
||||
groupOnlyGreetings: text("group_only_greetings", { mode: "json" })
|
||||
.default("[]")
|
||||
.$type<String[]>(), // JSON array of greetings for group chats
|
||||
postHistoryInstructions: text("post_history_instructions"), // Instructions for post-history processing
|
||||
source: text("source", { mode: "json" }).default("[]").$type<string[]>(), // JSON array of sources (e.g., URLs, books)
|
||||
assets: text("assets", { mode: "json" }).default("[]").$type<
|
||||
Array<{
|
||||
type: string
|
||||
uri: string
|
||||
name: string
|
||||
ext: string
|
||||
}>
|
||||
>(), // JSON array of asset paths or URLs
|
||||
createdAt: text("created_at").default(sql`(CURRENT_TIMESTAMP)`),
|
||||
updatedAt: text("updated_at").$onUpdate(() => sql`(CURRENT_TIMESTAMP)`),
|
||||
lorebookId: integer("lorebook_id").references(() => lorebooks.id, { onDelete: "set null" }), // Optional FK to lorebooks.id
|
||||
extensions: text("extensions", { mode: "json" }).default("[]").$type<Record<string, any>>(),
|
||||
isFavorite: integer("is_favorite", { mode: "boolean" }).default(false) // 1 if favorite, 0 otherwise
|
||||
})
|
||||
|
||||
export const charactersRelations = relations(characters, ({ many, one }) => ({
|
||||
characterTags: many(characterTags),
|
||||
user: one(users, {
|
||||
fields: [characters.userId],
|
||||
references: [users.id]
|
||||
}),
|
||||
lorebook: one(lorebooks, {
|
||||
fields: [characters.lorebookId],
|
||||
references: [lorebooks.id]
|
||||
})
|
||||
}))
|
||||
|
||||
export const personas = sqliteTable("personas", {
|
||||
id: integer("id").primaryKey(),
|
||||
userId: integer("user_id")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }), // FK to users.id
|
||||
isDefault: integer("is_default", { mode: "boolean" }).default(false), // Is this the default persona for the user?
|
||||
avatar: text("avatar"), // e.g. 'user-default.png', '1747379438925-Ryvn.png'
|
||||
name: text("name").notNull(), // e.g. 'Warren', 'Master Desir'
|
||||
description: text("description").notNull(), // Persona description (long text)
|
||||
position: integer("position").default(0),
|
||||
connections: text("connections"), // JSON array of connection IDs or objects
|
||||
createdAt: text("created_at"),
|
||||
updatedAt: text("updated_at")
|
||||
})
|
||||
|
||||
export const personasRelations = relations(personas, ({ one, many }) => ({
|
||||
user: one(users, {
|
||||
fields: [personas.userId],
|
||||
references: [users.id]
|
||||
})
|
||||
}))
|
||||
|
||||
// Chats (group or 1:1)
|
||||
export const chats = sqliteTable("chats", {
|
||||
id: integer("id").primaryKey(),
|
||||
name: text("name"), // Optional chat/group name
|
||||
isGroup: integer("is_group", {mode: "boolean"}).default(false), // 1 for group chat, 0 for 1:1
|
||||
userId: integer("user_id")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
createdAt: text("created_at"),
|
||||
updatedAt: text("updated_at"),
|
||||
scenario: text("scenario"),
|
||||
metadata: text("metadata"), // JSON for extra settings
|
||||
group_reply_strategy: text("group_reply_strategy").default(GroupReplyStrategies.ORDERED)
|
||||
})
|
||||
|
||||
export const chatsRelations = relations(chats, ({ one, many }) => ({
|
||||
user: one(users, {
|
||||
fields: [chats.userId],
|
||||
references: [users.id]
|
||||
}),
|
||||
chatMessages: many(chatMessages),
|
||||
chatPersonas: many(chatPersonas),
|
||||
chatCharacters: many(chatCharacters)
|
||||
}))
|
||||
|
||||
// Chat messages
|
||||
export const chatMessages = sqliteTable("chat_messages", {
|
||||
id: integer("id").primaryKey(),
|
||||
chatId: integer("chat_id")
|
||||
.notNull()
|
||||
.references(() => chats.id, { onDelete: "cascade" }),
|
||||
userId: integer("user_id")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }), // nullable for system/character messages
|
||||
characterId: integer("character_id").references(() => characters.id, { onDelete: "set null" }), // nullable
|
||||
personaId: integer("persona_id").references(() => personas.id, { onDelete: "set null" }), // nullable
|
||||
role: text("role"), // 'user', 'character', 'system', etc
|
||||
content: text("content").notNull(),
|
||||
createdAt: text("created_at"),
|
||||
updatedAt: text("updated_at"),
|
||||
isEdited: integer("is_edited").default(0), // 1 if edited, 0 otherwise
|
||||
metadata: text("metadata"), // JSON for extra info
|
||||
isGenerating: integer("is_generating", { mode: "boolean" }).default(false), // 1 if processing, 0 otherwise
|
||||
adapterId: text("adapter_id"), // UUID for in-flight adapter instance, nullable
|
||||
isHidden: integer("is_hidden", { mode: "boolean" }).default(false) // Whether this message is processed or not
|
||||
})
|
||||
|
||||
export const chatMessagesRelations = relations(chatMessages, ({ one }) => ({
|
||||
chat: one(chats, {
|
||||
fields: [chatMessages.chatId],
|
||||
references: [chats.id]
|
||||
}),
|
||||
user: one(users, {
|
||||
fields: [chatMessages.userId],
|
||||
references: [users.id]
|
||||
}),
|
||||
character: one(characters, {
|
||||
fields: [chatMessages.characterId],
|
||||
references: [characters.id]
|
||||
}),
|
||||
persona: one(personas, {
|
||||
fields: [chatMessages.personaId],
|
||||
references: [personas.id]
|
||||
})
|
||||
}))
|
||||
|
||||
// Many-to-many: chats <-> personas
|
||||
export const chatPersonas = sqliteTable("chat_personas", {
|
||||
chatId: integer("chat_id")
|
||||
.notNull()
|
||||
.references(() => chats.id, { onDelete: "cascade" }),
|
||||
personaId: integer("persona_id")
|
||||
.references(() => personas.id, { onDelete: "set null" }),
|
||||
position: integer("position").default(0), // Position in the chat
|
||||
})
|
||||
|
||||
export const chatPersonasRelations = relations(chatPersonas, ({ one }) => ({
|
||||
chat: one(chats, {
|
||||
fields: [chatPersonas.chatId],
|
||||
references: [chats.id]
|
||||
}),
|
||||
persona: one(personas, {
|
||||
fields: [chatPersonas.personaId],
|
||||
references: [personas.id]
|
||||
})
|
||||
}))
|
||||
|
||||
// Many-to-many: chats <-> characters
|
||||
export const chatCharacters = sqliteTable("chat_characters", {
|
||||
chatId: integer("chat_id")
|
||||
.notNull()
|
||||
.references(() => chats.id, { onDelete: "cascade" }),
|
||||
characterId: integer("character_id")
|
||||
.references(() => characters.id, { onDelete: "set null" }),
|
||||
position: integer("position").default(0), // Position in the chat
|
||||
isActive: integer("is_active", { mode: "boolean" }).default(true) // 1 if active in chat, 0 if not
|
||||
})
|
||||
|
||||
export const chatCharactersRelations = relations(chatCharacters, ({ one }) => ({
|
||||
chat: one(chats, {
|
||||
fields: [chatCharacters.chatId],
|
||||
references: [chats.id]
|
||||
}),
|
||||
character: one(characters, {
|
||||
fields: [chatCharacters.characterId],
|
||||
references: [characters.id]
|
||||
})
|
||||
}))
|
||||
|
|
@ -7,6 +7,7 @@ import type { BaseConnectionAdapter } from "../connectionAdapters/BaseConnection
|
|||
import { getConnectionAdapter } from "../utils/getConnectionAdapter"
|
||||
import { TokenCounters } from "$lib/server/utils/TokenCounterManager"
|
||||
import { GroupReplyStrategies } from "$lib/shared/constants/GroupReplyStrategies"
|
||||
import { InterpolationEngine } from "../utils/promptBuilder"
|
||||
|
||||
// --- Global map for active adapters ---
|
||||
export const activeAdapters = new Map<string, BaseConnectionAdapter>()
|
||||
|
|
@ -90,10 +91,15 @@ export async function createChat(
|
|||
with: { character: true },
|
||||
orderBy: (cc, { asc }) => asc(cc.position ?? 0)
|
||||
})
|
||||
const chatPersona = await db.query.chatPersonas.findFirst({
|
||||
where: (cp, { eq, and, isNotNull }) => and(eq(cp.chatId, newChat.id), isNotNull(cp.personaId)),
|
||||
with: { persona: true },
|
||||
})
|
||||
for (const cc of chatCharacters) {
|
||||
if (!cc.character) continue
|
||||
const greetings = buildCharacterFirstChatMessage({
|
||||
character: cc.character,
|
||||
persona: chatPersona?.persona,
|
||||
isGroup: !!newChat.isGroup
|
||||
})
|
||||
if (greetings.length > 0) {
|
||||
|
|
@ -816,8 +822,6 @@ export async function updateChat(
|
|||
(id) => !existingChat?.chatPersonas.some((p) => p.personaId === id)
|
||||
)
|
||||
|
||||
console.log("chatId", message.chat.id)
|
||||
|
||||
// Delete characters that are no longer in the chat
|
||||
if (deletedCharacterIds.length > 0) {
|
||||
await db
|
||||
|
|
@ -924,11 +928,17 @@ export async function updateChat(
|
|||
with: { character: true },
|
||||
orderBy: (cc, { asc }) => asc(cc.position ?? 0)
|
||||
})
|
||||
const chatPersona = await db.query.chatPersonas.findFirst({
|
||||
where: (cp, { eq, and, isNotNull }) => and(eq(cp.chatId, message.chat.id), isNotNull(cp.personaId)),
|
||||
with: { persona: true },
|
||||
orderBy: (cp, { asc }) => asc(cp.position ?? 0)
|
||||
})
|
||||
for (const cc of newChatCharacters) {
|
||||
if (!cc.character) continue
|
||||
if (!newCharacterIds.includes(cc.character.id)) continue
|
||||
const greetings = buildCharacterFirstChatMessage({
|
||||
character: cc.character,
|
||||
persona: chatPersona?.persona,
|
||||
isGroup: message.characterIds.length > 1
|
||||
})
|
||||
if (greetings.length > 0) {
|
||||
|
|
@ -1261,23 +1271,30 @@ export async function chatMessageSwipeLeft(
|
|||
// Builds the chatMessage history for the first chat message of a character, with history swipes for the user to choose from
|
||||
function buildCharacterFirstChatMessage({
|
||||
character,
|
||||
persona,
|
||||
isGroup
|
||||
}: {
|
||||
character: SelectCharacter
|
||||
persona: SelectPersona | undefined | null
|
||||
isGroup: boolean
|
||||
}): string[] {
|
||||
console.log("Building first chat message for character:", character.name)
|
||||
const history: string[] = []
|
||||
const engine = new InterpolationEngine()
|
||||
const context = engine.createInterpolationContext({
|
||||
currentCharacterName: character.nickname || character.name,
|
||||
currentPersonaName: persona?.name || "User",
|
||||
})
|
||||
if (!isGroup || !character.groupOnlyGreetings?.length) {
|
||||
if (character.firstMessage) {
|
||||
history.push(character.firstMessage.trim())
|
||||
history.push(engine.interpolateString(character.firstMessage.trim(), context)!)
|
||||
}
|
||||
if (character.alternateGreetings) {
|
||||
history.push(...character.alternateGreetings.map((g) => g.trim()))
|
||||
history.push(...character.alternateGreetings.map((g) => engine.interpolateString(g.trim(), context)!))
|
||||
}
|
||||
} else if (character.groupOnlyGreetings?.length) {
|
||||
// If this is a group chat, use only group greetings
|
||||
history.push(...character.groupOnlyGreetings.map((g) => g.trim()))
|
||||
history.push(...character.groupOnlyGreetings.map((g) => engine.interpolateString(g.trim(), context)!))
|
||||
} else {
|
||||
// Fallback firstMessage if no greetings are available
|
||||
history.push(
|
||||
|
|
|
|||
442
src/lib/server/utils/promptBuilder/ContentInclusionStrategy.ts
Normal file
442
src/lib/server/utils/promptBuilder/ContentInclusionStrategy.ts
Normal file
|
|
@ -0,0 +1,442 @@
|
|||
import type { InterpolationContext } from './InterpolationEngine'
|
||||
import type { ProcessedChatMessage } from './ContentProcessors'
|
||||
|
||||
/**
|
||||
* Configuration for content inclusion strategies
|
||||
*/
|
||||
export interface ContentInclusionConfig {
|
||||
/** Priority levels for different content types */
|
||||
priorities: {
|
||||
chatMessages: number[]
|
||||
worldLore: number[]
|
||||
characterLore: number[]
|
||||
history: number[]
|
||||
}
|
||||
/**
|
||||
* Weights for content inclusion iteration frequency (0-1+)
|
||||
* - 1.0 = check every iteration
|
||||
* - 0.5 = check every 2nd iteration
|
||||
* - 2.0 = check twice per iteration
|
||||
* - 0.0 = never check
|
||||
*
|
||||
* At the end, if there's still room, all content types are checked regardless of weight.
|
||||
*/
|
||||
weights: {
|
||||
chatMessages: number
|
||||
worldLore: number
|
||||
characterLore: number
|
||||
history: number
|
||||
}
|
||||
/** Iteration rates for different threshold states */
|
||||
iterationRates: {
|
||||
belowThreshold: number
|
||||
aboveThreshold: number
|
||||
overLimit: number
|
||||
}
|
||||
/** Whether to enable content matching for lore */
|
||||
enableMatching: {
|
||||
worldLore: boolean
|
||||
characterLore: boolean
|
||||
history: boolean
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default content inclusion configuration
|
||||
*/
|
||||
export const defaultContentInclusionConfig: ContentInclusionConfig = {
|
||||
priorities: {
|
||||
chatMessages: [4, 3, 2, 1, 0],
|
||||
worldLore: [4, 3, 2, 1],
|
||||
characterLore: [4, 3, 2, 1],
|
||||
history: [4, 2, 0]
|
||||
},
|
||||
weights: {
|
||||
chatMessages: 1.0,
|
||||
worldLore: 0.8,
|
||||
characterLore: 0.9,
|
||||
history: 0.6
|
||||
},
|
||||
iterationRates: {
|
||||
belowThreshold: 10,
|
||||
aboveThreshold: 2,
|
||||
overLimit: 1
|
||||
},
|
||||
enableMatching: {
|
||||
worldLore: true,
|
||||
characterLore: true,
|
||||
history: true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of content processing for a single iteration
|
||||
*/
|
||||
export interface ContentProcessingResult {
|
||||
chatMessages: ProcessedChatMessage[]
|
||||
includedWorldLore: SelectWorldLoreEntry[]
|
||||
includedCharacterLore: SelectCharacterLoreEntry[]
|
||||
includedHistory: SelectHistoryEntry[]
|
||||
shouldContinue: boolean
|
||||
iteratorsExhausted: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* State for tracking content inclusion iterations
|
||||
*/
|
||||
export interface ContentInclusionState {
|
||||
priority: number
|
||||
iterationCount: number
|
||||
completed: boolean
|
||||
isBelowThreshold: boolean
|
||||
isAboveThreshold: boolean
|
||||
isOverLimit: boolean
|
||||
totalTokens: number
|
||||
|
||||
// Iterator states
|
||||
messagesIterator: IterableIterator<SelectChatMessage> | null | undefined
|
||||
worldLoreIterator: IterableIterator<SelectWorldLoreEntry> | null | undefined
|
||||
characterLoreIterator: IterableIterator<SelectCharacterLoreEntry> | null | undefined
|
||||
historyIterator: IterableIterator<SelectHistoryEntry> | null | undefined
|
||||
|
||||
// Weight-based iteration tracking
|
||||
weightCounters: {
|
||||
chatMessages: number
|
||||
worldLore: number
|
||||
characterLore: number
|
||||
history: number
|
||||
}
|
||||
|
||||
// Tracking arrays
|
||||
consideredWorldLore: SelectWorldLoreEntry[]
|
||||
consideredCharacterLore: SelectCharacterLoreEntry[]
|
||||
consideredHistory: SelectHistoryEntry[]
|
||||
|
||||
// Failed match tracking
|
||||
messageFailedWorldLoreMatches: Record<number, number[]>
|
||||
messageFailedCharacterLoreMatches: Record<number, number[]>
|
||||
messageFailedHistoryMatches: Record<number, number[]>
|
||||
|
||||
// Included content
|
||||
chatMessages: ProcessedChatMessage[]
|
||||
includedWorldLore: SelectWorldLoreEntry[]
|
||||
includedCharacterLore: SelectCharacterLoreEntry[]
|
||||
includedHistory: SelectHistoryEntry[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for content inclusion strategies
|
||||
*/
|
||||
export interface ContentInclusionStrategy {
|
||||
/**
|
||||
* Initialize the inclusion state
|
||||
*/
|
||||
initializeState(config: ContentInclusionConfig): ContentInclusionState
|
||||
|
||||
/**
|
||||
* Process a single iteration of content inclusion
|
||||
*/
|
||||
processIteration(
|
||||
state: ContentInclusionState,
|
||||
config: ContentInclusionConfig,
|
||||
context: {
|
||||
interpolationContext: InterpolationContext
|
||||
charName: string
|
||||
personaName: string
|
||||
}
|
||||
): Promise<ContentProcessingResult>
|
||||
|
||||
/**
|
||||
* Check if iterators should be reset for the current priority level
|
||||
*/
|
||||
shouldResetIterators(state: ContentInclusionState, config: ContentInclusionConfig): boolean
|
||||
|
||||
/**
|
||||
* Determine the next priority level
|
||||
*/
|
||||
getNextPriority(state: ContentInclusionState, config: ContentInclusionConfig): number
|
||||
|
||||
/**
|
||||
* Check if content should be processed at current threshold state
|
||||
*/
|
||||
shouldProcessContent(
|
||||
contentType: 'worldLore' | 'characterLore' | 'history',
|
||||
state: ContentInclusionState,
|
||||
config: ContentInclusionConfig
|
||||
): boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of content inclusion strategy
|
||||
*/
|
||||
export class DefaultContentInclusionStrategy implements ContentInclusionStrategy {
|
||||
initializeState(config: ContentInclusionConfig): ContentInclusionState {
|
||||
// Normalize weights so the highest weight is 1.0 while maintaining relative proportions
|
||||
const normalizedWeights = this.normalizeWeights(config.weights)
|
||||
|
||||
return {
|
||||
priority: 4,
|
||||
iterationCount: 0,
|
||||
completed: false,
|
||||
isBelowThreshold: true,
|
||||
isAboveThreshold: false,
|
||||
isOverLimit: false,
|
||||
totalTokens: 0,
|
||||
|
||||
messagesIterator: undefined,
|
||||
worldLoreIterator: undefined,
|
||||
characterLoreIterator: undefined,
|
||||
historyIterator: undefined,
|
||||
|
||||
// Initialize weight counters with normalized weights
|
||||
weightCounters: {
|
||||
chatMessages: 0,
|
||||
worldLore: 0,
|
||||
characterLore: 0,
|
||||
history: 0
|
||||
},
|
||||
|
||||
consideredWorldLore: [],
|
||||
consideredCharacterLore: [],
|
||||
consideredHistory: [],
|
||||
|
||||
messageFailedWorldLoreMatches: {},
|
||||
messageFailedCharacterLoreMatches: {},
|
||||
messageFailedHistoryMatches: {},
|
||||
|
||||
chatMessages: [{
|
||||
id: -2, // Placeholder for current character's empty message
|
||||
role: "assistant",
|
||||
name: "", // Will be set by caller
|
||||
message: ""
|
||||
}],
|
||||
includedWorldLore: [],
|
||||
includedCharacterLore: [],
|
||||
includedHistory: []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize weights so the highest weight is 1.0 while maintaining relative proportions
|
||||
*/
|
||||
protected normalizeWeights(weights: ContentInclusionConfig['weights']): ContentInclusionConfig['weights'] {
|
||||
const weightValues = [
|
||||
weights.chatMessages,
|
||||
weights.worldLore,
|
||||
weights.characterLore,
|
||||
weights.history
|
||||
]
|
||||
|
||||
// Find the maximum weight
|
||||
const maxWeight = Math.max(...weightValues)
|
||||
|
||||
// If max weight is 0, return original weights to avoid division by zero
|
||||
if (maxWeight === 0) {
|
||||
return weights
|
||||
}
|
||||
|
||||
// Calculate normalization factor
|
||||
const normalizationFactor = 1.0 / maxWeight
|
||||
|
||||
// Return normalized weights
|
||||
return {
|
||||
chatMessages: weights.chatMessages * normalizationFactor,
|
||||
worldLore: weights.worldLore * normalizationFactor,
|
||||
characterLore: weights.characterLore * normalizationFactor,
|
||||
history: weights.history * normalizationFactor
|
||||
}
|
||||
}
|
||||
|
||||
async processIteration(
|
||||
state: ContentInclusionState,
|
||||
config: ContentInclusionConfig,
|
||||
context: {
|
||||
interpolationContext: InterpolationContext
|
||||
charName: string
|
||||
personaName: string
|
||||
}
|
||||
): Promise<ContentProcessingResult> {
|
||||
// This will be implemented by the caller who has access to iterators and matching logic
|
||||
throw new Error('processIteration should be implemented by PromptBuilder')
|
||||
}
|
||||
|
||||
shouldResetIterators(state: ContentInclusionState, config: ContentInclusionConfig): boolean {
|
||||
const { priority } = state
|
||||
const { priorities } = config
|
||||
|
||||
// Check if current priority level supports any content types
|
||||
const hasMessagesAtPriority = priorities.chatMessages.includes(priority)
|
||||
const hasWorldLoreAtPriority = priorities.worldLore.includes(priority)
|
||||
const hasCharacterLoreAtPriority = priorities.characterLore.includes(priority)
|
||||
const hasHistoryAtPriority = priorities.history.includes(priority)
|
||||
|
||||
// Reset if we have content types for this priority but iterators are null
|
||||
return (hasMessagesAtPriority || hasWorldLoreAtPriority || hasCharacterLoreAtPriority || hasHistoryAtPriority) &&
|
||||
(state.messagesIterator === null && state.worldLoreIterator === null &&
|
||||
state.characterLoreIterator === null && state.historyIterator === null)
|
||||
}
|
||||
|
||||
getNextPriority(state: ContentInclusionState, config: ContentInclusionConfig): number {
|
||||
const currentPriority = state.priority
|
||||
|
||||
// Find the next lower priority that has content types configured
|
||||
for (let p = currentPriority - 1; p >= 0; p--) {
|
||||
const hasContent = config.priorities.chatMessages.includes(p) ||
|
||||
config.priorities.worldLore.includes(p) ||
|
||||
config.priorities.characterLore.includes(p) ||
|
||||
config.priorities.history.includes(p)
|
||||
if (hasContent) {
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
return -1 // No more priorities
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if content should be processed at current threshold state
|
||||
* Weights control iteration frequency: 1.0 = every iteration, 0.5 = every 2nd iteration, etc.
|
||||
*/
|
||||
shouldProcessContent(
|
||||
contentType: 'worldLore' | 'characterLore' | 'history',
|
||||
state: ContentInclusionState,
|
||||
config: ContentInclusionConfig
|
||||
): boolean {
|
||||
// Check if content type is enabled for matching
|
||||
if (!config.enableMatching[contentType]) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Get normalized weight for this content type
|
||||
const normalizedWeights = this.normalizeWeights(config.weights)
|
||||
const weight = normalizedWeights[contentType]
|
||||
|
||||
// If we're filling remaining space at the end, ignore weights and process everything
|
||||
if (!state.isBelowThreshold && this.isInFinalFillMode(state)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Only process lore content when below threshold (unless in final fill mode)
|
||||
if (!state.isBelowThreshold) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Increment the counter for this content type
|
||||
state.weightCounters[contentType] += weight
|
||||
|
||||
// Check if counter has reached 1.0 (time to process)
|
||||
if (state.weightCounters[contentType] >= 1.0) {
|
||||
state.weightCounters[contentType] -= 1.0 // Reset counter, keeping remainder
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we're in final fill mode (iterators exhausted but still have token space)
|
||||
*/
|
||||
protected isInFinalFillMode(state: ContentInclusionState): boolean {
|
||||
// This would be implemented to detect when we should ignore weights
|
||||
// and try to fill remaining space
|
||||
return false // Placeholder - should be implemented based on iterator states
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Weighted content inclusion strategy that uses adaptive iteration frequencies
|
||||
*/
|
||||
export class WeightedContentInclusionStrategy extends DefaultContentInclusionStrategy {
|
||||
shouldProcessContent(
|
||||
contentType: 'worldLore' | 'characterLore' | 'history',
|
||||
state: ContentInclusionState,
|
||||
config: ContentInclusionConfig
|
||||
): boolean {
|
||||
// Check basic conditions first
|
||||
if (!config.enableMatching[contentType]) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Get normalized weight for this content type
|
||||
const normalizedWeights = this.normalizeWeights(config.weights)
|
||||
const baseWeight = normalizedWeights[contentType]
|
||||
|
||||
// If we're filling remaining space at the end, ignore weights and process everything
|
||||
if (!state.isBelowThreshold && this.isInFinalFillMode(state)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Only process lore content when below threshold (unless in final fill mode)
|
||||
if (!state.isBelowThreshold) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Adaptive weight based on token usage - process more aggressively when below threshold
|
||||
const tokenUsageRatio = state.totalTokens > 0 ? state.totalTokens / (state.totalTokens + 1000) : 0
|
||||
const adaptiveWeight = state.isBelowThreshold
|
||||
? baseWeight
|
||||
: baseWeight * (1 - tokenUsageRatio * 0.5) // Reduce frequency as tokens increase
|
||||
|
||||
// Increment the counter for this content type with adaptive weight
|
||||
state.weightCounters[contentType] += adaptiveWeight
|
||||
|
||||
// Check if counter has reached 1.0 (time to process)
|
||||
if (state.weightCounters[contentType] >= 1.0) {
|
||||
state.weightCounters[contentType] -= 1.0 // Reset counter, keeping remainder
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Priority-first content inclusion strategy
|
||||
*/
|
||||
export class PriorityFirstContentInclusionStrategy extends DefaultContentInclusionStrategy {
|
||||
getNextPriority(state: ContentInclusionState, config: ContentInclusionConfig): number {
|
||||
// Always process highest priority content first, regardless of weights
|
||||
return super.getNextPriority(state, config)
|
||||
}
|
||||
|
||||
shouldProcessContent(
|
||||
contentType: 'worldLore' | 'characterLore' | 'history',
|
||||
state: ContentInclusionState,
|
||||
config: ContentInclusionConfig
|
||||
): boolean {
|
||||
// Check basic conditions first
|
||||
if (!config.enableMatching[contentType]) {
|
||||
return false
|
||||
}
|
||||
|
||||
// If we're filling remaining space at the end, ignore weights and process everything
|
||||
if (!state.isBelowThreshold && this.isInFinalFillMode(state)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Only process lore content when below threshold (unless in final fill mode)
|
||||
if (!state.isBelowThreshold) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Always include content if we're at high priority levels (ignore weights)
|
||||
if (state.priority >= 3) {
|
||||
return true
|
||||
}
|
||||
|
||||
// For lower priorities, use weight-based iteration frequency
|
||||
const normalizedWeights = this.normalizeWeights(config.weights)
|
||||
const weight = normalizedWeights[contentType]
|
||||
|
||||
// Increment the counter for this content type
|
||||
state.weightCounters[contentType] += weight
|
||||
|
||||
// Check if counter has reached 1.0 (time to process)
|
||||
if (state.weightCounters[contentType] >= 1.0) {
|
||||
state.weightCounters[contentType] -= 1.0 // Reset counter, keeping remainder
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
866
src/lib/server/utils/promptBuilder/ContentInfillEngine.ts
Normal file
866
src/lib/server/utils/promptBuilder/ContentInfillEngine.ts
Normal file
|
|
@ -0,0 +1,866 @@
|
|||
import type { InterpolationContext } from './InterpolationEngine'
|
||||
import type {
|
||||
ContentInclusionConfig,
|
||||
ContentInclusionState,
|
||||
ContentInclusionStrategy
|
||||
} from './ContentInclusionStrategy'
|
||||
import { defaultContentInclusionConfig } from './ContentInclusionStrategy'
|
||||
import type { ProcessedChatMessage } from './ContentProcessors'
|
||||
import {
|
||||
ChatMessageProcessor,
|
||||
WorldLoreProcessor,
|
||||
CharacterLoreProcessor,
|
||||
HistoryEntryProcessor,
|
||||
LoreMatchingEngine
|
||||
} from './ContentProcessors'
|
||||
import type { LoreMatchingStrategy, MatchingStrategyConfig } from './LoreMatchingStrategies'
|
||||
import { MatchingStrategyFactory, KeywordMatchingStrategy } from './LoreMatchingStrategies'
|
||||
import { parseSplitChatPrompt } from './utils'
|
||||
import { attachCharacterLoreToCharacters } from './LorebookBindingUtils'
|
||||
|
||||
/**
|
||||
* Modular content infill engine that handles the complex logic
|
||||
* of including chat messages, world lore, character lore, and history
|
||||
* in a configurable, priority-based manner.
|
||||
*/
|
||||
export class ContentInfillEngine {
|
||||
private chatMessageProcessor: ChatMessageProcessor
|
||||
private worldLoreProcessor: WorldLoreProcessor
|
||||
private characterLoreProcessor: CharacterLoreProcessor
|
||||
private historyEntryProcessor: HistoryEntryProcessor
|
||||
private loreMatchingEngine: LoreMatchingEngine
|
||||
private currentInterpolationContext?: InterpolationContext
|
||||
|
||||
constructor(
|
||||
private chat: any, // BasePromptChat type
|
||||
private interpolationEngine: any, // InterpolationEngine type
|
||||
private populateLorebookEntryBindings: any,
|
||||
private isHistoryEntry: any,
|
||||
private chatMessageIterator: any,
|
||||
private worldLoreEntryIterator: any,
|
||||
private characterLoreEntryIterator: any,
|
||||
private historyEntryIterator: any,
|
||||
private matchingStrategy?: LoreMatchingStrategy
|
||||
) {
|
||||
this.chatMessageProcessor = new ChatMessageProcessor(chat, interpolationEngine)
|
||||
this.worldLoreProcessor = new WorldLoreProcessor(chat, populateLorebookEntryBindings)
|
||||
this.characterLoreProcessor = new CharacterLoreProcessor(chat, populateLorebookEntryBindings)
|
||||
this.historyEntryProcessor = new HistoryEntryProcessor(chat, populateLorebookEntryBindings, isHistoryEntry)
|
||||
|
||||
// Use provided strategy or default to keyword matching
|
||||
const strategy = matchingStrategy || new KeywordMatchingStrategy()
|
||||
this.loreMatchingEngine = new LoreMatchingEngine(strategy)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the matching strategy at runtime
|
||||
*/
|
||||
async setMatchingStrategy(strategy: LoreMatchingStrategy): Promise<void> {
|
||||
if (strategy.initialize) {
|
||||
await strategy.initialize()
|
||||
}
|
||||
this.loreMatchingEngine.setStrategy(strategy)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new ContentInfillEngine with a matching strategy from config
|
||||
*/
|
||||
static async createWithStrategy(
|
||||
chat: any,
|
||||
interpolationEngine: any,
|
||||
populateLorebookEntryBindings: any,
|
||||
isHistoryEntry: any,
|
||||
chatMessageIterator: any,
|
||||
worldLoreEntryIterator: any,
|
||||
characterLoreEntryIterator: any,
|
||||
historyEntryIterator: any,
|
||||
strategyConfig: MatchingStrategyConfig
|
||||
): Promise<ContentInfillEngine> {
|
||||
const strategy = await MatchingStrategyFactory.createStrategy(strategyConfig)
|
||||
return new ContentInfillEngine(
|
||||
chat,
|
||||
interpolationEngine,
|
||||
populateLorebookEntryBindings,
|
||||
isHistoryEntry,
|
||||
chatMessageIterator,
|
||||
worldLoreEntryIterator,
|
||||
characterLoreEntryIterator,
|
||||
historyEntryIterator,
|
||||
strategy
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current matching strategy name for debugging
|
||||
*/
|
||||
getMatchingStrategyName(): string {
|
||||
return this.loreMatchingEngine.getStrategyName()
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplified infillContent method that uses the modular architecture
|
||||
*/
|
||||
async infillContent({
|
||||
charName,
|
||||
personaName,
|
||||
templateContext,
|
||||
useChatFormat = false,
|
||||
tokenLimit,
|
||||
contextThresholdPercent,
|
||||
tokenCounter,
|
||||
handlebars,
|
||||
contextConfig,
|
||||
strategy,
|
||||
config = defaultContentInclusionConfig
|
||||
}: {
|
||||
charName: string
|
||||
personaName: string
|
||||
templateContext: any
|
||||
useChatFormat?: boolean
|
||||
tokenLimit: number
|
||||
contextThresholdPercent: number
|
||||
tokenCounter: any
|
||||
handlebars: any
|
||||
contextConfig: any
|
||||
strategy?: ContentInclusionStrategy
|
||||
config?: ContentInclusionConfig
|
||||
}) {
|
||||
// Initialize state and processors
|
||||
const state = strategy ? strategy.initializeState(config) : this.initializeDefaultState(config)
|
||||
|
||||
// Set the character name for the placeholder message
|
||||
state.chatMessages[0].name = charName
|
||||
|
||||
// Create interpolation context
|
||||
const interpolationContext = this.interpolationEngine.createInterpolationContext({
|
||||
currentCharacterName: charName,
|
||||
currentPersonaName: personaName
|
||||
})
|
||||
|
||||
// Store for use in template context building
|
||||
this.currentInterpolationContext = interpolationContext
|
||||
|
||||
// Debug timing
|
||||
const debugTimings: any[] = []
|
||||
|
||||
// Main processing loop
|
||||
while (!state.completed) {
|
||||
const iterStart = Date.now()
|
||||
state.iterationCount++
|
||||
|
||||
// Process content for current priority level
|
||||
await this.processContentAtPriority(state, config, {
|
||||
interpolationContext,
|
||||
charName,
|
||||
personaName
|
||||
})
|
||||
|
||||
// Check if we should render and count tokens
|
||||
const shouldRender = this.shouldRenderAtIteration(state, config)
|
||||
if (shouldRender) {
|
||||
const renderResult = await this.renderAndCountTokens(
|
||||
state,
|
||||
templateContext,
|
||||
{
|
||||
useChatFormat,
|
||||
tokenCounter,
|
||||
handlebars,
|
||||
contextConfig,
|
||||
tokenLimit,
|
||||
contextThresholdPercent,
|
||||
charName
|
||||
}
|
||||
)
|
||||
|
||||
state.totalTokens = renderResult.totalTokens
|
||||
state.isBelowThreshold = renderResult.isBelowThreshold
|
||||
state.isAboveThreshold = renderResult.isAboveThreshold
|
||||
state.isOverLimit = renderResult.isOverLimit
|
||||
}
|
||||
|
||||
// Handle over-limit condition
|
||||
if (state.isOverLimit) {
|
||||
this.handleOverLimit(state)
|
||||
}
|
||||
|
||||
// Check completion conditions
|
||||
if (this.shouldComplete(state, config, tokenLimit)) {
|
||||
state.completed = true
|
||||
}
|
||||
|
||||
// Update priority if needed
|
||||
if (this.shouldUpdatePriority(state, config)) {
|
||||
const oldPriority = state.priority
|
||||
state.priority = strategy ? strategy.getNextPriority(state, config) : this.getNextPriority(state, config)
|
||||
if (state.priority < 0) {
|
||||
state.completed = true
|
||||
}
|
||||
}
|
||||
|
||||
const iterEnd = Date.now()
|
||||
debugTimings.push({
|
||||
priority: state.priority,
|
||||
chatMessagesCount: state.chatMessages.length,
|
||||
totalTokens: state.totalTokens,
|
||||
iterationMs: iterEnd - iterStart
|
||||
})
|
||||
|
||||
// Safety check to prevent infinite loops
|
||||
if (state.iterationCount > 1000) {
|
||||
console.error(`ContentInfillEngine: Too many iterations (${state.iterationCount}), breaking`)
|
||||
state.completed = true
|
||||
}
|
||||
}
|
||||
|
||||
// Final render
|
||||
const finalResult = await this.renderFinalResult(
|
||||
state,
|
||||
templateContext,
|
||||
{
|
||||
useChatFormat,
|
||||
tokenCounter,
|
||||
handlebars,
|
||||
contextConfig,
|
||||
charName
|
||||
}
|
||||
)
|
||||
|
||||
return finalResult
|
||||
}
|
||||
|
||||
/**
|
||||
* Process content for the current priority level
|
||||
*/
|
||||
private async processContentAtPriority(
|
||||
state: ContentInclusionState,
|
||||
config: ContentInclusionConfig,
|
||||
context: {
|
||||
interpolationContext: InterpolationContext
|
||||
charName: string
|
||||
personaName: string
|
||||
}
|
||||
) {
|
||||
// Initialize iterators if needed
|
||||
this.initializeIteratorsForPriority(state, config)
|
||||
|
||||
// Process chat messages
|
||||
if (!state.isOverLimit) {
|
||||
this.processChatMessages(state, context)
|
||||
}
|
||||
|
||||
// Process lore content if below threshold
|
||||
if (state.isBelowThreshold && !state.isOverLimit) {
|
||||
this.processWorldLore(state, context)
|
||||
this.processCharacterLore(state, context)
|
||||
this.processHistory(state, context)
|
||||
|
||||
// Perform lore matching
|
||||
await this.performLoreMatching(state, context)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize iterators for the current priority level
|
||||
*/
|
||||
private initializeIteratorsForPriority(state: ContentInclusionState, config: ContentInclusionConfig) {
|
||||
const { priority } = state
|
||||
|
||||
// Check if we need to initialize iterators
|
||||
if (state.messagesIterator === undefined) {
|
||||
// First time setup
|
||||
if (config.priorities.chatMessages.includes(priority)) {
|
||||
state.messagesIterator = this.chatMessageIterator({ priority })
|
||||
}
|
||||
if (config.priorities.worldLore.includes(priority)) {
|
||||
state.worldLoreIterator = this.worldLoreEntryIterator({ priority })
|
||||
}
|
||||
if (config.priorities.characterLore.includes(priority)) {
|
||||
state.characterLoreIterator = this.characterLoreEntryIterator({ chat: this.chat, priority })
|
||||
}
|
||||
if (config.priorities.history.includes(priority)) {
|
||||
state.historyIterator = this.historyEntryIterator({ chat: this.chat, priority })
|
||||
}
|
||||
} else if (this.allIteratorsNull(state)) {
|
||||
// Reset iterators for new priority level
|
||||
if (config.priorities.chatMessages.includes(priority)) {
|
||||
state.messagesIterator = this.chatMessageIterator({ priority })
|
||||
}
|
||||
if (config.priorities.worldLore.includes(priority)) {
|
||||
state.worldLoreIterator = this.worldLoreEntryIterator({ priority })
|
||||
}
|
||||
if (config.priorities.characterLore.includes(priority)) {
|
||||
state.characterLoreIterator = this.characterLoreEntryIterator({ chat: this.chat, priority })
|
||||
}
|
||||
if (config.priorities.history.includes(priority)) {
|
||||
state.historyIterator = this.historyEntryIterator({ chat: this.chat, priority })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process chat messages from iterator
|
||||
*/
|
||||
private processChatMessages(
|
||||
state: ContentInclusionState,
|
||||
context: {
|
||||
interpolationContext: InterpolationContext
|
||||
charName: string
|
||||
personaName: string
|
||||
}
|
||||
) {
|
||||
if (!state.messagesIterator) {
|
||||
return
|
||||
}
|
||||
|
||||
// Process multiple messages when we're not at token limits
|
||||
const batchSize = state.isBelowThreshold ? 5 : state.isOverLimit ? 1 : 2
|
||||
|
||||
for (let i = 0; i < batchSize; i++) {
|
||||
const nextMessageVal = state.messagesIterator.next()
|
||||
|
||||
if (nextMessageVal.done) {
|
||||
state.messagesIterator = null
|
||||
break
|
||||
} else if (nextMessageVal.value) {
|
||||
const processedMessage = this.chatMessageProcessor.processItem(nextMessageVal.value, {
|
||||
...context,
|
||||
priority: state.priority
|
||||
})
|
||||
|
||||
if (processedMessage) {
|
||||
state.chatMessages.push(processedMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process world lore entries from iterator
|
||||
*/
|
||||
private processWorldLore(
|
||||
state: ContentInclusionState,
|
||||
context: {
|
||||
interpolationContext: InterpolationContext
|
||||
charName: string
|
||||
personaName: string
|
||||
}
|
||||
) {
|
||||
if (!state.worldLoreIterator) return
|
||||
|
||||
const nextVal = state.worldLoreIterator.next()
|
||||
|
||||
if (nextVal.done) {
|
||||
state.worldLoreIterator = null
|
||||
} else if (nextVal.value) {
|
||||
if (state.priority === 4) {
|
||||
// High priority - include immediately
|
||||
const processed = this.worldLoreProcessor.processItem(nextVal.value, {
|
||||
...context,
|
||||
priority: state.priority
|
||||
})
|
||||
if (processed) {
|
||||
state.includedWorldLore.push(processed)
|
||||
}
|
||||
} else {
|
||||
// Lower priority - add to considered list for matching
|
||||
state.consideredWorldLore.push(nextVal.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process character lore entries from iterator
|
||||
*/
|
||||
private processCharacterLore(
|
||||
state: ContentInclusionState,
|
||||
context: {
|
||||
interpolationContext: InterpolationContext
|
||||
charName: string
|
||||
personaName: string
|
||||
}
|
||||
) {
|
||||
if (!state.characterLoreIterator) return
|
||||
|
||||
const nextVal = state.characterLoreIterator.next()
|
||||
|
||||
if (nextVal.done) {
|
||||
state.characterLoreIterator = null
|
||||
} else if (nextVal.value) {
|
||||
if (state.priority === 4) {
|
||||
// High priority - include immediately
|
||||
const processed = this.characterLoreProcessor.processItem(nextVal.value, {
|
||||
...context,
|
||||
priority: state.priority
|
||||
})
|
||||
if (processed) {
|
||||
state.includedCharacterLore.push(processed)
|
||||
}
|
||||
} else {
|
||||
// Lower priority - add to considered list for matching
|
||||
state.consideredCharacterLore.push(nextVal.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process history entries from iterator
|
||||
*/
|
||||
private processHistory(
|
||||
state: ContentInclusionState,
|
||||
context: {
|
||||
interpolationContext: InterpolationContext
|
||||
charName: string
|
||||
personaName: string
|
||||
}
|
||||
) {
|
||||
if (!state.historyIterator) return
|
||||
|
||||
const nextVal = state.historyIterator.next()
|
||||
|
||||
if (nextVal.done) {
|
||||
state.historyIterator = null
|
||||
} else if (nextVal.value) {
|
||||
if (state.priority === 4) {
|
||||
// High priority - include immediately
|
||||
const processed = this.historyEntryProcessor.processItem(nextVal.value, {
|
||||
...context,
|
||||
priority: state.priority
|
||||
})
|
||||
if (processed) {
|
||||
state.includedHistory.push(processed)
|
||||
}
|
||||
} else {
|
||||
// Lower priority - add to considered list for matching
|
||||
state.consideredHistory.push(nextVal.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform lore matching against chat messages
|
||||
*/
|
||||
private async performLoreMatching(
|
||||
state: ContentInclusionState,
|
||||
context: {
|
||||
interpolationContext: InterpolationContext
|
||||
charName: string
|
||||
personaName: string
|
||||
}
|
||||
) {
|
||||
// Match world lore
|
||||
const worldLoreResult = await this.loreMatchingEngine.processMatching(
|
||||
state.chatMessages,
|
||||
state.consideredWorldLore,
|
||||
state.messageFailedWorldLoreMatches,
|
||||
this.worldLoreProcessor,
|
||||
{ ...context, priority: state.priority }
|
||||
)
|
||||
state.includedWorldLore.push(...worldLoreResult.matched)
|
||||
state.consideredWorldLore = worldLoreResult.remaining
|
||||
|
||||
// Match character lore
|
||||
const characterLoreResult = await this.loreMatchingEngine.processMatching(
|
||||
state.chatMessages,
|
||||
state.consideredCharacterLore,
|
||||
state.messageFailedCharacterLoreMatches,
|
||||
this.characterLoreProcessor,
|
||||
{ ...context, priority: state.priority }
|
||||
)
|
||||
state.includedCharacterLore.push(...characterLoreResult.matched)
|
||||
state.consideredCharacterLore = characterLoreResult.remaining
|
||||
|
||||
// Match history
|
||||
const historyResult = await this.loreMatchingEngine.processMatching(
|
||||
state.chatMessages,
|
||||
state.consideredHistory,
|
||||
state.messageFailedHistoryMatches,
|
||||
this.historyEntryProcessor,
|
||||
{ ...context, priority: state.priority }
|
||||
)
|
||||
state.includedHistory.push(...historyResult.matched)
|
||||
state.consideredHistory = historyResult.remaining
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
private initializeDefaultState(config: ContentInclusionConfig): ContentInclusionState {
|
||||
return {
|
||||
priority: 4,
|
||||
iterationCount: 0,
|
||||
completed: false,
|
||||
isBelowThreshold: true,
|
||||
isAboveThreshold: false,
|
||||
isOverLimit: false,
|
||||
totalTokens: 0,
|
||||
weightCounters: {
|
||||
chatMessages: 0,
|
||||
worldLore: 0,
|
||||
characterLore: 0,
|
||||
history: 0
|
||||
},
|
||||
|
||||
messagesIterator: undefined,
|
||||
worldLoreIterator: undefined,
|
||||
characterLoreIterator: undefined,
|
||||
historyIterator: undefined,
|
||||
|
||||
consideredWorldLore: [],
|
||||
consideredCharacterLore: [],
|
||||
consideredHistory: [],
|
||||
|
||||
messageFailedWorldLoreMatches: {},
|
||||
messageFailedCharacterLoreMatches: {},
|
||||
messageFailedHistoryMatches: {},
|
||||
|
||||
chatMessages: [{
|
||||
id: -2,
|
||||
role: "assistant",
|
||||
name: "",
|
||||
message: ""
|
||||
}],
|
||||
includedWorldLore: [],
|
||||
includedCharacterLore: [],
|
||||
includedHistory: []
|
||||
}
|
||||
}
|
||||
|
||||
private allIteratorsNull(state: ContentInclusionState): boolean {
|
||||
return state.messagesIterator === null &&
|
||||
state.worldLoreIterator === null &&
|
||||
state.characterLoreIterator === null &&
|
||||
state.historyIterator === null
|
||||
}
|
||||
|
||||
private shouldRenderAtIteration(state: ContentInclusionState, config: ContentInclusionConfig): boolean {
|
||||
const iterationRate = state.isBelowThreshold
|
||||
? config.iterationRates.belowThreshold
|
||||
: state.isAboveThreshold
|
||||
? config.iterationRates.aboveThreshold
|
||||
: state.isOverLimit
|
||||
? config.iterationRates.overLimit
|
||||
: 1
|
||||
|
||||
return (state.priority !== 4 && state.iterationCount % iterationRate === 0) || state.isOverLimit
|
||||
}
|
||||
|
||||
private async renderAndCountTokens(
|
||||
state: ContentInclusionState,
|
||||
templateContext: any,
|
||||
options: {
|
||||
useChatFormat: boolean
|
||||
tokenCounter: any
|
||||
handlebars: any
|
||||
contextConfig: any
|
||||
tokenLimit: number
|
||||
contextThresholdPercent: number
|
||||
charName: string
|
||||
}
|
||||
) {
|
||||
// Build template context with current state
|
||||
const updatedTemplateContext = this.buildTemplateContext(state, templateContext, options.charName, this.currentInterpolationContext)
|
||||
|
||||
// Render prompt
|
||||
const renderedPrompt = options.handlebars.compile(options.contextConfig.template)({
|
||||
...updatedTemplateContext,
|
||||
chatMessages: [...state.chatMessages].reverse()
|
||||
})
|
||||
|
||||
let finalPrompt = renderedPrompt
|
||||
if (options.useChatFormat) {
|
||||
// Parse chat format if needed
|
||||
finalPrompt = JSON.stringify(this.parseSplitChatPrompt(renderedPrompt))
|
||||
}
|
||||
|
||||
// Count tokens
|
||||
const totalTokens = typeof options.tokenCounter.countTokens === "function"
|
||||
? await options.tokenCounter.countTokens(finalPrompt)
|
||||
: 0
|
||||
|
||||
// Update threshold states
|
||||
const isBelowThreshold = totalTokens < options.tokenLimit * options.contextThresholdPercent
|
||||
const isAboveThreshold = totalTokens >= options.tokenLimit * options.contextThresholdPercent
|
||||
const isOverLimit = totalTokens > options.tokenLimit
|
||||
|
||||
return {
|
||||
totalTokens,
|
||||
isBelowThreshold,
|
||||
isAboveThreshold,
|
||||
isOverLimit,
|
||||
renderedPrompt: finalPrompt
|
||||
}
|
||||
}
|
||||
|
||||
private buildTemplateContext(
|
||||
state: ContentInclusionState,
|
||||
baseContext: any,
|
||||
charName: string,
|
||||
interpolationContext?: InterpolationContext
|
||||
) {
|
||||
// Build a complete template context similar to the original method
|
||||
const context = { ...baseContext }
|
||||
|
||||
// Add processed chat messages to context
|
||||
context.chatMessages = state.chatMessages
|
||||
context.characterLore = state.includedCharacterLore
|
||||
|
||||
// If we have an interpolation context, build characters and personas with lore
|
||||
if (interpolationContext) {
|
||||
// Get interpolated characters from the chat's characters
|
||||
const assistantCharacters = this.getInterpolatedCharacters(interpolationContext)
|
||||
const assistantCharactersWithLore = attachCharacterLoreToCharacters(
|
||||
assistantCharacters,
|
||||
state.includedCharacterLore,
|
||||
this.chat
|
||||
)
|
||||
context.characters = JSON.stringify(assistantCharactersWithLore, null, 2)
|
||||
|
||||
// Get interpolated personas from the chat's personas
|
||||
const userCharacters = this.getInterpolatedPersonas(interpolationContext)
|
||||
const userCharactersWithLore = attachCharacterLoreToCharacters(
|
||||
userCharacters,
|
||||
state.includedCharacterLore,
|
||||
this.chat
|
||||
)
|
||||
context.personas = JSON.stringify(userCharactersWithLore, null, 2)
|
||||
}
|
||||
|
||||
// Build world lore object
|
||||
const worldLoreObj: Record<string, string> = {}
|
||||
for (const entry of state.includedWorldLore) {
|
||||
if (entry && entry.name && entry.content) {
|
||||
worldLoreObj[entry.name] = entry.content
|
||||
}
|
||||
}
|
||||
context.worldLore = Object.keys(worldLoreObj).length
|
||||
? JSON.stringify(worldLoreObj, null, 2)
|
||||
: undefined
|
||||
|
||||
// Build history object and track most recent date
|
||||
const historyObj: Record<string, string> = {}
|
||||
state.includedHistory.forEach((entry) => {
|
||||
if (entry.content.trim()) {
|
||||
// Only populate bindings if the entry has the required lorebook fields
|
||||
let populatedEntry = entry
|
||||
if ((entry as any).name && (entry as any).keys) {
|
||||
const lorePopulated = this.populateLorebookEntryBindings(
|
||||
entry as any,
|
||||
this.chat
|
||||
)
|
||||
if (
|
||||
lorePopulated &&
|
||||
typeof lorePopulated.content === "string"
|
||||
) {
|
||||
populatedEntry = {
|
||||
...entry,
|
||||
content: lorePopulated.content
|
||||
}
|
||||
}
|
||||
}
|
||||
// Format date as year-month-day, year-month, or year only
|
||||
const y = entry.year
|
||||
const m = entry.month
|
||||
const d = entry.day
|
||||
let dateKey = String(y)
|
||||
if (m !== undefined && m !== null)
|
||||
dateKey += `-${String(m).padStart(2, "0")}`
|
||||
if (d !== undefined && d !== null)
|
||||
dateKey += `-${String(d).padStart(2, "0")}`
|
||||
historyObj[dateKey] = populatedEntry.content
|
||||
}
|
||||
})
|
||||
|
||||
// Set current date from most recent history entry
|
||||
let mostRecentDate: {
|
||||
year: number
|
||||
month: number | undefined
|
||||
day: number | undefined
|
||||
} | null = null
|
||||
if (state.includedHistory.length) {
|
||||
mostRecentDate = {
|
||||
year: state.includedHistory[0].year,
|
||||
month: !!state.includedHistory[0].month
|
||||
? state.includedHistory[0].month
|
||||
: undefined,
|
||||
day: !!state.includedHistory[0].day
|
||||
? state.includedHistory[0].day
|
||||
: undefined
|
||||
}
|
||||
}
|
||||
// Format currentDate as year-month-day, year-month, or year only
|
||||
if (mostRecentDate) {
|
||||
const y = mostRecentDate.year
|
||||
const m = mostRecentDate.month
|
||||
const d = mostRecentDate.day
|
||||
let dateKey = String(y)
|
||||
if (m !== undefined && m !== null)
|
||||
dateKey += `-${String(m).padStart(2, "0")}`
|
||||
if (d !== undefined && d !== null)
|
||||
dateKey += `-${String(d).padStart(2, "0")}`
|
||||
context.currentDate = dateKey
|
||||
} else {
|
||||
context.currentDate = undefined
|
||||
}
|
||||
|
||||
context.history = Object.keys(historyObj).length
|
||||
? JSON.stringify(historyObj)
|
||||
: undefined
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
/**
|
||||
* Get interpolated characters similar to the original method
|
||||
*/
|
||||
private getInterpolatedCharacters(interpolationContext: InterpolationContext) {
|
||||
const chatCharacters = this.chat.chatCharacters as
|
||||
| (SelectChatCharacter & { character: SelectCharacter })[]
|
||||
| undefined
|
||||
const assistantCharacters = (chatCharacters || []).map((cc) =>
|
||||
this.compileCharacterData(cc.character)
|
||||
)
|
||||
return assistantCharacters.map((c: any) =>
|
||||
this.interpolationEngine.interpolateObject(c, interpolationContext, ['name', 'nickname', 'description', 'personality'])
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get interpolated personas similar to the original method
|
||||
*/
|
||||
private getInterpolatedPersonas(interpolationContext: InterpolationContext) {
|
||||
const userCharacters = (this.chat.chatPersonas || []).map((cp: any) =>
|
||||
this.compilePersonaData(cp.persona)
|
||||
)
|
||||
return userCharacters.map((p: any) =>
|
||||
this.interpolationEngine.interpolateObject(p, interpolationContext, ['name', 'description'])
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile character data similar to the original method
|
||||
*/
|
||||
private compileCharacterData(character: SelectCharacter) {
|
||||
const char = {
|
||||
name: character.name,
|
||||
nickname: character.nickname || undefined,
|
||||
description: character.description,
|
||||
personality: character.personality || undefined
|
||||
}
|
||||
|
||||
// delete any undefined/null properties
|
||||
Object.keys(char).forEach((key) => {
|
||||
if (char[key as keyof typeof char] == null) {
|
||||
delete char[key as keyof typeof char]
|
||||
}
|
||||
})
|
||||
|
||||
return char
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile persona data similar to the original method
|
||||
*/
|
||||
private compilePersonaData(persona: SelectPersona) {
|
||||
const personaData = {
|
||||
name: persona.name,
|
||||
description: persona.description
|
||||
}
|
||||
// delete any undefined/null properties
|
||||
Object.keys(personaData).forEach((key) => {
|
||||
if (personaData[key as keyof typeof personaData] == null) {
|
||||
delete personaData[key as keyof typeof personaData]
|
||||
}
|
||||
})
|
||||
return personaData
|
||||
}
|
||||
|
||||
private handleOverLimit(state: ContentInclusionState) {
|
||||
if (state.chatMessages.length > 1) {
|
||||
state.chatMessages.pop()
|
||||
} else {
|
||||
state.completed = true
|
||||
}
|
||||
}
|
||||
|
||||
private shouldComplete(state: ContentInclusionState, config: ContentInclusionConfig, tokenLimit: number): boolean {
|
||||
// Complete if:
|
||||
// 1. We're over the token limit and have successfully included some content
|
||||
// 2. All iterators are exhausted and we've reached the lowest priority
|
||||
// 3. We have a token count and are within the limit (successful completion)
|
||||
|
||||
if (state.isOverLimit && state.totalTokens <= tokenLimit) {
|
||||
return true // Successfully reduced to within limits
|
||||
}
|
||||
|
||||
if (this.allIteratorsNull(state) && state.priority <= 0) {
|
||||
return true // Exhausted all content
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private shouldUpdatePriority(state: ContentInclusionState, config: ContentInclusionConfig): boolean {
|
||||
return this.allIteratorsNull(state)
|
||||
}
|
||||
|
||||
private getNextPriority(state: ContentInclusionState, config: ContentInclusionConfig): number {
|
||||
const currentPriority = state.priority
|
||||
|
||||
for (let p = currentPriority - 1; p >= 0; p--) {
|
||||
const hasContent = config.priorities.chatMessages.includes(p) ||
|
||||
config.priorities.worldLore.includes(p) ||
|
||||
config.priorities.characterLore.includes(p) ||
|
||||
config.priorities.history.includes(p)
|
||||
if (hasContent) {
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
private parseSplitChatPrompt(prompt: string) {
|
||||
return parseSplitChatPrompt(prompt)
|
||||
}
|
||||
|
||||
private async renderFinalResult(state: ContentInclusionState, templateContext: any, options: any) {
|
||||
// Final render and return the complete result
|
||||
const finalContext = this.buildTemplateContext(state, templateContext, options.charName, this.currentInterpolationContext)
|
||||
|
||||
const renderedPrompt = options.handlebars.compile(options.contextConfig.template)({
|
||||
...finalContext,
|
||||
chatMessages: [...state.chatMessages].reverse()
|
||||
})
|
||||
|
||||
let finalPrompt = renderedPrompt
|
||||
let renderedMessages: any[] | undefined
|
||||
|
||||
if (options.useChatFormat) {
|
||||
renderedMessages = this.parseSplitChatPrompt(renderedPrompt)
|
||||
finalPrompt = JSON.stringify(renderedMessages)
|
||||
}
|
||||
|
||||
const totalTokens = typeof options.tokenCounter.countTokens === "function"
|
||||
? await options.tokenCounter.countTokens(finalPrompt)
|
||||
: 0
|
||||
|
||||
const includedChatMessages = state.chatMessages.length - 1
|
||||
const includedChatMessageIds = state.chatMessages
|
||||
.filter((m) => m.id !== -2)
|
||||
.map((m) => m.id)
|
||||
|
||||
// Calculate excluded IDs
|
||||
const excludedChatMessageIds = (this.chat.chatMessages || [])
|
||||
.map((m: any) => m.id)
|
||||
.filter((id: number) => !includedChatMessageIds.includes(id))
|
||||
|
||||
return {
|
||||
renderedPrompt: !options.useChatFormat ? finalPrompt : undefined,
|
||||
renderedMessages,
|
||||
totalTokens,
|
||||
chatMessages: {
|
||||
included: includedChatMessages,
|
||||
includedIds: includedChatMessageIds,
|
||||
excludedIds: excludedChatMessageIds
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
33
src/lib/server/utils/promptBuilder/ContentMatchers.ts
Normal file
33
src/lib/server/utils/promptBuilder/ContentMatchers.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
export interface ContentMatcher<T> {
|
||||
matches(content: string, entry: T): boolean
|
||||
}
|
||||
|
||||
export class LorebookMatcher implements ContentMatcher<SelectWorldLoreEntry | SelectCharacterLoreEntry> {
|
||||
matches(content: string, entry: SelectWorldLoreEntry | SelectCharacterLoreEntry): boolean {
|
||||
const msgContent = entry.caseSensitive ? content : content.toLowerCase()
|
||||
return entry.keys.split(',').some(key => {
|
||||
const keyToCheck = entry.caseSensitive ? key.trim() : key.toLowerCase().trim()
|
||||
if (entry.useRegex) {
|
||||
const regex = new RegExp(keyToCheck, 'g')
|
||||
return regex.test(msgContent)
|
||||
} else {
|
||||
return msgContent.includes(keyToCheck)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class HistoryMatcher implements ContentMatcher<SelectHistoryEntry> {
|
||||
matches(content: string, entry: SelectHistoryEntry): boolean {
|
||||
const msgContent = entry.caseSensitive ? content : content.toLowerCase()
|
||||
return entry.keys.split(',').some(key => {
|
||||
const keyToCheck = entry.caseSensitive ? key : key.toLowerCase()
|
||||
if (entry.useRegex) {
|
||||
const regex = new RegExp(keyToCheck, 'g')
|
||||
return regex.test(msgContent)
|
||||
} else {
|
||||
return msgContent.includes(keyToCheck)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
296
src/lib/server/utils/promptBuilder/ContentProcessors.ts
Normal file
296
src/lib/server/utils/promptBuilder/ContentProcessors.ts
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
import type { InterpolationContext } from './InterpolationEngine'
|
||||
import type { LoreMatchingStrategy } from './LoreMatchingStrategies'
|
||||
|
||||
// Define processed chat message format
|
||||
export interface ProcessedChatMessage {
|
||||
id: number
|
||||
role: "assistant" | "user"
|
||||
name: string
|
||||
message: string | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Base interface for content processors that handle specific content types
|
||||
*/
|
||||
export interface ContentProcessor<TInput, TOutput = TInput> {
|
||||
/**
|
||||
* Process an individual content item
|
||||
*/
|
||||
processItem(
|
||||
item: TInput,
|
||||
context: {
|
||||
interpolationContext: InterpolationContext
|
||||
charName: string
|
||||
personaName: string
|
||||
priority: number
|
||||
}
|
||||
): TOutput | null
|
||||
|
||||
/**
|
||||
* Check if an item should be included based on priority and other criteria
|
||||
*/
|
||||
shouldInclude(item: TInput, priority: number): boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes chat messages with interpolation and role/name assignment
|
||||
*/
|
||||
export class ChatMessageProcessor implements ContentProcessor<SelectChatMessage, ProcessedChatMessage> {
|
||||
constructor(
|
||||
private chat: any, // BasePromptChat type
|
||||
private interpolationEngine: any // InterpolationEngine type
|
||||
) {}
|
||||
|
||||
processItem(
|
||||
message: SelectChatMessage,
|
||||
context: {
|
||||
interpolationContext: InterpolationContext
|
||||
charName: string
|
||||
personaName: string
|
||||
priority: number
|
||||
}
|
||||
): ProcessedChatMessage | null {
|
||||
const { interpolationContext, charName, personaName } = context
|
||||
|
||||
// Create message-specific interpolation context
|
||||
let msgInterpolationContext = { ...interpolationContext }
|
||||
let assistantName = charName
|
||||
let userName = personaName
|
||||
|
||||
// Handle character-specific context
|
||||
if (message.characterId && this.chat.chatCharacters) {
|
||||
const foundChar = this.chat.chatCharacters.find(
|
||||
(cc: any) => cc.character.id === message.characterId
|
||||
)?.character
|
||||
let foundName: string | undefined
|
||||
if (foundChar) {
|
||||
foundName = foundChar.nickname || foundChar.name
|
||||
}
|
||||
if (message.role === "assistant") {
|
||||
assistantName = foundName || charName
|
||||
}
|
||||
msgInterpolationContext = {
|
||||
...msgInterpolationContext,
|
||||
char: foundName || charName,
|
||||
character: foundName || charName
|
||||
}
|
||||
}
|
||||
|
||||
// Handle persona-specific context
|
||||
if (message.personaId && this.chat.chatPersonas) {
|
||||
const foundPersona = this.chat.chatPersonas.find(
|
||||
(cp: any) => cp.persona.id === message.personaId
|
||||
)?.persona
|
||||
if (foundPersona) {
|
||||
userName = foundPersona.name
|
||||
msgInterpolationContext = {
|
||||
...msgInterpolationContext,
|
||||
user: userName,
|
||||
persona: userName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: message.id,
|
||||
role: message.role === "user" || message.role === "assistant" ? message.role : "assistant",
|
||||
name: message.role === "assistant" ? assistantName : userName,
|
||||
message: this.interpolationEngine.interpolateString(message.content, msgInterpolationContext)
|
||||
}
|
||||
}
|
||||
|
||||
shouldInclude(message: SelectChatMessage, priority: number): boolean {
|
||||
// Messages can be included at any priority
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes world lore entries with lorebook binding population
|
||||
*/
|
||||
export class WorldLoreProcessor implements ContentProcessor<SelectWorldLoreEntry> {
|
||||
constructor(
|
||||
private chat: any, // BasePromptChat type
|
||||
private populateLorebookEntryBindings: (entry: SelectWorldLoreEntry, chat: any) => SelectWorldLoreEntry
|
||||
) {}
|
||||
|
||||
processItem(
|
||||
entry: SelectWorldLoreEntry,
|
||||
context: {
|
||||
interpolationContext: InterpolationContext
|
||||
charName: string
|
||||
personaName: string
|
||||
priority: number
|
||||
}
|
||||
): SelectWorldLoreEntry | null {
|
||||
return this.populateLorebookEntryBindings(entry, this.chat)
|
||||
}
|
||||
|
||||
shouldInclude(entry: SelectWorldLoreEntry, priority: number): boolean {
|
||||
// High priority entries are always included
|
||||
if (priority === 4) return true
|
||||
|
||||
// Lower priority entries need to pass additional checks
|
||||
return true // Can be extended with more sophisticated logic
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes character lore entries with lorebook binding population
|
||||
*/
|
||||
export class CharacterLoreProcessor implements ContentProcessor<SelectCharacterLoreEntry> {
|
||||
constructor(
|
||||
private chat: any, // BasePromptChat type
|
||||
private populateLorebookEntryBindings: (entry: SelectCharacterLoreEntry, chat: any) => SelectCharacterLoreEntry
|
||||
) {}
|
||||
|
||||
processItem(
|
||||
entry: SelectCharacterLoreEntry,
|
||||
context: {
|
||||
interpolationContext: InterpolationContext
|
||||
charName: string
|
||||
personaName: string
|
||||
priority: number
|
||||
}
|
||||
): SelectCharacterLoreEntry | null {
|
||||
return this.populateLorebookEntryBindings(entry, this.chat)
|
||||
}
|
||||
|
||||
shouldInclude(entry: SelectCharacterLoreEntry, priority: number): boolean {
|
||||
// High priority entries are always included
|
||||
if (priority === 4) return true
|
||||
|
||||
// Lower priority entries need to pass additional checks
|
||||
return true // Can be extended with more sophisticated logic
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes history entries with lorebook binding population and date validation
|
||||
*/
|
||||
export class HistoryEntryProcessor implements ContentProcessor<SelectHistoryEntry> {
|
||||
constructor(
|
||||
private chat: any, // BasePromptChat type
|
||||
private populateLorebookEntryBindings: (entry: SelectHistoryEntry, chat: any) => SelectHistoryEntry,
|
||||
private isHistoryEntry: (entry: any) => entry is SelectHistoryEntry
|
||||
) {}
|
||||
|
||||
processItem(
|
||||
entry: SelectHistoryEntry,
|
||||
context: {
|
||||
interpolationContext: InterpolationContext
|
||||
charName: string
|
||||
personaName: string
|
||||
priority: number
|
||||
}
|
||||
): SelectHistoryEntry | null {
|
||||
if (!this.isHistoryEntry(entry)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const populated = this.populateLorebookEntryBindings(entry, this.chat)
|
||||
|
||||
// Validate that it's still a history entry after population
|
||||
if ("date" in populated) {
|
||||
return populated as SelectHistoryEntry
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
shouldInclude(entry: SelectHistoryEntry, priority: number): boolean {
|
||||
// High priority entries are always included
|
||||
if (priority === 4) return true
|
||||
|
||||
// Check if entry has valid date information
|
||||
return entry.year !== undefined && entry.year !== null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles matching logic for lore entries against chat messages
|
||||
* Now uses pluggable matching strategies
|
||||
*/
|
||||
export class LoreMatchingEngine {
|
||||
constructor(private strategy: LoreMatchingStrategy) {}
|
||||
|
||||
/**
|
||||
* Update the matching strategy at runtime
|
||||
*/
|
||||
setStrategy(strategy: LoreMatchingStrategy): void {
|
||||
this.strategy = strategy
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current strategy name for debugging
|
||||
*/
|
||||
getStrategyName(): string {
|
||||
return this.strategy.getName()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a lore entry matches a chat message using the current strategy
|
||||
*/
|
||||
async matchesMessage(
|
||||
entry: SelectWorldLoreEntry | SelectCharacterLoreEntry | SelectHistoryEntry,
|
||||
message: { id: number; message: string | undefined },
|
||||
context?: {
|
||||
interpolationContext: InterpolationContext
|
||||
chatMessages: Array<{ id: number; message: string | undefined }>
|
||||
failedMatches: Record<number, number[]>
|
||||
}
|
||||
): Promise<boolean> {
|
||||
return await this.strategy.matchesMessage(entry, message, context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Process all matching for a set of messages against considered entries
|
||||
*/
|
||||
async processMatching<TInput, TOutput = TInput>(
|
||||
chatMessages: Array<{ id: number; message: string | undefined }>,
|
||||
consideredEntries: TInput[],
|
||||
failedMatches: Record<number, number[]>,
|
||||
processor: ContentProcessor<TInput, TOutput>,
|
||||
context: {
|
||||
interpolationContext: InterpolationContext
|
||||
charName: string
|
||||
personaName: string
|
||||
priority: number
|
||||
}
|
||||
): Promise<{
|
||||
matched: TOutput[]
|
||||
remaining: TInput[]
|
||||
}> {
|
||||
const matched: TOutput[] = []
|
||||
const remaining: TInput[] = []
|
||||
|
||||
// Create enhanced context for strategy
|
||||
const strategyContext = {
|
||||
interpolationContext: context.interpolationContext,
|
||||
chatMessages,
|
||||
failedMatches
|
||||
}
|
||||
|
||||
for (const entry of consideredEntries) {
|
||||
let wasMatched = false
|
||||
|
||||
for (const message of chatMessages) {
|
||||
const isMatch = await this.matchesMessage(entry as any, message, strategyContext)
|
||||
if (isMatch) {
|
||||
const processedEntry = processor.processItem(entry, context)
|
||||
if (processedEntry) {
|
||||
matched.push(processedEntry)
|
||||
wasMatched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!wasMatched) {
|
||||
remaining.push(entry)
|
||||
}
|
||||
}
|
||||
|
||||
return { matched, remaining }
|
||||
}
|
||||
}
|
||||
233
src/lib/server/utils/promptBuilder/InterpolationEngine.ts
Normal file
233
src/lib/server/utils/promptBuilder/InterpolationEngine.ts
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
import Handlebars from "handlebars"
|
||||
|
||||
/**
|
||||
* Context for template interpolation containing character and persona information
|
||||
*/
|
||||
export interface InterpolationContext {
|
||||
/** Current character name (primary assistant) */
|
||||
char: string
|
||||
/** Alias for char - current character name */
|
||||
character: string
|
||||
/** Current user/persona name */
|
||||
user: string
|
||||
/** Alias for user - current persona name */
|
||||
persona: string
|
||||
/** Any additional context variables */
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/**
|
||||
* Character data structure for interpolation
|
||||
*/
|
||||
export interface CharacterData {
|
||||
name: string
|
||||
nickname?: string
|
||||
description: string
|
||||
personality?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/**
|
||||
* Persona data structure for interpolation
|
||||
*/
|
||||
export interface PersonaData {
|
||||
name: string
|
||||
description: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/**
|
||||
* A standalone interpolation engine for handling character/persona/user/char variable substitution
|
||||
* in templates throughout the application.
|
||||
*/
|
||||
export class InterpolationEngine {
|
||||
private handlebars: typeof Handlebars
|
||||
|
||||
constructor(handlebarsInstance?: typeof Handlebars) {
|
||||
this.handlebars = handlebarsInstance || Handlebars.create()
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an interpolation context from character and persona information
|
||||
*/
|
||||
createInterpolationContext({
|
||||
currentCharacterName,
|
||||
currentPersonaName,
|
||||
additionalContext = {}
|
||||
}: {
|
||||
currentCharacterName: string
|
||||
currentPersonaName?: string
|
||||
additionalContext?: Record<string, any>
|
||||
}): InterpolationContext {
|
||||
const personaName = currentPersonaName || "user"
|
||||
|
||||
return {
|
||||
char: currentCharacterName,
|
||||
character: currentCharacterName,
|
||||
user: personaName,
|
||||
persona: personaName,
|
||||
...additionalContext
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpolates a single string template with the given context
|
||||
*/
|
||||
interpolateString(
|
||||
template: string | undefined,
|
||||
context: InterpolationContext
|
||||
): string | undefined {
|
||||
if (!template) return template
|
||||
|
||||
try {
|
||||
return this.handlebars.compile(template)(context)
|
||||
} catch (error) {
|
||||
console.warn("Template interpolation failed:", error)
|
||||
return template
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpolates multiple strings at once
|
||||
*/
|
||||
interpolateStrings(
|
||||
templates: Record<string, string | undefined>,
|
||||
context: InterpolationContext
|
||||
): Record<string, string | undefined> {
|
||||
const result: Record<string, string | undefined> = {}
|
||||
|
||||
for (const [key, template] of Object.entries(templates)) {
|
||||
result[key] = this.interpolateString(template, context)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpolates character data, applying context to name, nickname, description, and personality
|
||||
*/
|
||||
interpolateCharacterData(
|
||||
character: CharacterData,
|
||||
context: InterpolationContext
|
||||
): CharacterData {
|
||||
return {
|
||||
...character,
|
||||
name: this.interpolateString(character.name, context) || character.name,
|
||||
nickname: this.interpolateString(character.nickname, context),
|
||||
description: this.interpolateString(character.description, context) || character.description,
|
||||
personality: this.interpolateString(character.personality, context)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpolates multiple characters at once
|
||||
*/
|
||||
interpolateCharacters(
|
||||
characters: CharacterData[],
|
||||
context: InterpolationContext
|
||||
): CharacterData[] {
|
||||
return characters.map(char => this.interpolateCharacterData(char, context))
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpolates persona data, applying context to name and description
|
||||
*/
|
||||
interpolatePersonaData(
|
||||
persona: PersonaData,
|
||||
context: InterpolationContext
|
||||
): PersonaData {
|
||||
return {
|
||||
...persona,
|
||||
name: this.interpolateString(persona.name, context) || persona.name,
|
||||
description: this.interpolateString(persona.description, context) || persona.description
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpolates multiple personas at once
|
||||
*/
|
||||
interpolatePersonas(
|
||||
personas: PersonaData[],
|
||||
context: InterpolationContext
|
||||
): PersonaData[] {
|
||||
return personas.map(persona => this.interpolatePersonaData(persona, context))
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpolates an object by applying context to all string values recursively
|
||||
*/
|
||||
interpolateObject<T extends Record<string, any>>(
|
||||
obj: T,
|
||||
context: InterpolationContext,
|
||||
stringFields?: (keyof T)[]
|
||||
): T {
|
||||
const result: Record<string, any> = { ...obj }
|
||||
|
||||
// If specific string fields are provided, only interpolate those
|
||||
if (stringFields) {
|
||||
for (const field of stringFields) {
|
||||
if (typeof result[field as string] === 'string') {
|
||||
const interpolated = this.interpolateString(result[field as string] as string, context)
|
||||
result[field as string] = interpolated || result[field as string]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Otherwise, interpolate all string values
|
||||
for (const [key, value] of Object.entries(result)) {
|
||||
if (typeof value === 'string') {
|
||||
const interpolated = this.interpolateString(value, context)
|
||||
result[key] = interpolated || value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result as T
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a message-specific interpolation context that can override the main context
|
||||
* for individual messages (useful when different characters/personas are speaking)
|
||||
*/
|
||||
createMessageContext(
|
||||
baseContext: InterpolationContext,
|
||||
overrides: Partial<InterpolationContext>
|
||||
): InterpolationContext {
|
||||
return {
|
||||
...baseContext,
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new InterpolationEngine instance
|
||||
*/
|
||||
export function createInterpolationEngine(handlebarsInstance?: typeof Handlebars): InterpolationEngine {
|
||||
return new InterpolationEngine(handlebarsInstance)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function for simple string interpolation
|
||||
*/
|
||||
export function interpolateTemplate(
|
||||
template: string | undefined,
|
||||
context: InterpolationContext,
|
||||
handlebarsInstance?: typeof Handlebars
|
||||
): string | undefined {
|
||||
const engine = new InterpolationEngine(handlebarsInstance)
|
||||
return engine.interpolateString(template, context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to create basic interpolation context
|
||||
*/
|
||||
export function createBasicContext(
|
||||
characterName: string,
|
||||
personaName?: string
|
||||
): InterpolationContext {
|
||||
const engine = new InterpolationEngine()
|
||||
return engine.createInterpolationContext({
|
||||
currentCharacterName: characterName,
|
||||
currentPersonaName: personaName
|
||||
})
|
||||
}
|
||||
190
src/lib/server/utils/promptBuilder/LoreMatchingStrategies.ts
Normal file
190
src/lib/server/utils/promptBuilder/LoreMatchingStrategies.ts
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
import type { InterpolationContext } from './InterpolationEngine'
|
||||
import type { ProcessedChatMessage } from './ContentProcessors'
|
||||
|
||||
/**
|
||||
* Base interface for lore matching strategies
|
||||
*/
|
||||
export interface LoreMatchingStrategy {
|
||||
/**
|
||||
* Check if a lore entry matches a chat message
|
||||
*/
|
||||
matchesMessage(
|
||||
entry: SelectWorldLoreEntry | SelectCharacterLoreEntry | SelectHistoryEntry,
|
||||
message: { id: number; message: string | undefined },
|
||||
context?: {
|
||||
interpolationContext: InterpolationContext
|
||||
chatMessages: Array<{ id: number; message: string | undefined }>
|
||||
failedMatches: Record<number, number[]>
|
||||
}
|
||||
): Promise<boolean> | boolean
|
||||
|
||||
/**
|
||||
* Initialize the strategy (e.g., load models, prepare indices)
|
||||
*/
|
||||
initialize?(): Promise<void>
|
||||
|
||||
/**
|
||||
* Clean up resources
|
||||
*/
|
||||
cleanup?(): Promise<void>
|
||||
|
||||
/**
|
||||
* Get strategy name for debugging/logging
|
||||
*/
|
||||
getName(): string
|
||||
}
|
||||
|
||||
/**
|
||||
* Traditional keyword-based matching strategy
|
||||
*/
|
||||
export class KeywordMatchingStrategy implements LoreMatchingStrategy {
|
||||
getName(): string {
|
||||
return 'keyword'
|
||||
}
|
||||
|
||||
matchesMessage(
|
||||
entry: SelectWorldLoreEntry | SelectCharacterLoreEntry | SelectHistoryEntry,
|
||||
message: { id: number; message: string | undefined },
|
||||
context?: {
|
||||
interpolationContext: InterpolationContext
|
||||
chatMessages: Array<{ id: number; message: string | undefined }>
|
||||
failedMatches: Record<number, number[]>
|
||||
}
|
||||
): boolean {
|
||||
// Skip if this combination has already failed
|
||||
if (context?.failedMatches[message.id] && context.failedMatches[message.id].includes(entry.id)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!message.message) {
|
||||
return false
|
||||
}
|
||||
|
||||
let msgContent = entry.caseSensitive
|
||||
? message.message
|
||||
: message.message.toLowerCase()
|
||||
|
||||
const matchFound = entry.keys.split(",").some((key) => {
|
||||
const keyToCheck = entry.caseSensitive
|
||||
? key.trim()
|
||||
: key.toLowerCase().trim()
|
||||
|
||||
if (entry.useRegex) {
|
||||
try {
|
||||
const regex = new RegExp(keyToCheck, "g")
|
||||
return regex.test(msgContent)
|
||||
} catch (e) {
|
||||
// Invalid regex, fall back to string matching
|
||||
return msgContent.includes(keyToCheck)
|
||||
}
|
||||
} else {
|
||||
return msgContent.includes(keyToCheck)
|
||||
}
|
||||
})
|
||||
|
||||
// Track failed matches for future optimization
|
||||
if (!matchFound && context?.failedMatches) {
|
||||
if (!context.failedMatches[message.id]) {
|
||||
context.failedMatches[message.id] = []
|
||||
}
|
||||
context.failedMatches[message.id].push(entry.id)
|
||||
}
|
||||
|
||||
return matchFound
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vector similarity-based matching strategy (future implementation)
|
||||
*/
|
||||
export class VectorMatchingStrategy implements LoreMatchingStrategy {
|
||||
private embeddings: Map<number, number[]> = new Map()
|
||||
private isInitialized = false
|
||||
|
||||
getName(): string {
|
||||
return 'vector'
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
// TODO: Initialize embedding model
|
||||
// TODO: Pre-compute embeddings for lore entries
|
||||
this.isInitialized = true
|
||||
console.log('VectorMatchingStrategy initialized')
|
||||
}
|
||||
|
||||
async cleanup(): Promise<void> {
|
||||
this.embeddings.clear()
|
||||
this.isInitialized = false
|
||||
}
|
||||
|
||||
async matchesMessage(
|
||||
entry: SelectWorldLoreEntry | SelectCharacterLoreEntry | SelectHistoryEntry,
|
||||
message: { id: number; message: string | undefined },
|
||||
context?: {
|
||||
interpolationContext: InterpolationContext
|
||||
chatMessages: Array<{ id: number; message: string | undefined }>
|
||||
failedMatches: Record<number, number[]>
|
||||
}
|
||||
): Promise<boolean> {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error('VectorMatchingStrategy must be initialized before use')
|
||||
}
|
||||
|
||||
if (!message.message) {
|
||||
return false
|
||||
}
|
||||
|
||||
// TODO: Implement vector similarity matching
|
||||
// 1. Generate embedding for message content
|
||||
// 2. Calculate similarity with lore entry embedding
|
||||
// 3. Return true if similarity exceeds threshold
|
||||
|
||||
// Placeholder implementation - fallback to keyword matching for now
|
||||
const keywordStrategy = new KeywordMatchingStrategy()
|
||||
return keywordStrategy.matchesMessage(entry, message, context)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for matching strategies
|
||||
*/
|
||||
export interface MatchingStrategyConfig {
|
||||
strategy: 'keyword' | 'vector'
|
||||
|
||||
// Vector strategy options
|
||||
vectorOptions?: {
|
||||
model?: string
|
||||
threshold?: number
|
||||
maxResults?: number
|
||||
}
|
||||
|
||||
// Performance options
|
||||
performance?: {
|
||||
cacheEmbeddings?: boolean
|
||||
batchSize?: number
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory for creating matching strategies
|
||||
*/
|
||||
export class MatchingStrategyFactory {
|
||||
static async createStrategy(config: MatchingStrategyConfig): Promise<LoreMatchingStrategy> {
|
||||
switch (config.strategy) {
|
||||
case 'keyword':
|
||||
return new KeywordMatchingStrategy()
|
||||
|
||||
case 'vector':
|
||||
const vectorStrategy = new VectorMatchingStrategy()
|
||||
await vectorStrategy.initialize()
|
||||
return vectorStrategy
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown matching strategy: ${config.strategy}`)
|
||||
}
|
||||
}
|
||||
|
||||
static getAvailableStrategies(): string[] {
|
||||
return ['keyword', 'vector']
|
||||
}
|
||||
}
|
||||
307
src/lib/server/utils/promptBuilder/MATCHING_STRATEGIES.md
Normal file
307
src/lib/server/utils/promptBuilder/MATCHING_STRATEGIES.md
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
# Lore Matching Strategies
|
||||
|
||||
The PromptBuilder system now includes a flexible, pluggable matching strategy architecture that allows you to switch between different approaches for matching lore entries (world lore, character lore, and history) against chat messages.
|
||||
|
||||
## Overview
|
||||
|
||||
Previously, the system used only keyword-based matching. Now you can choose from:
|
||||
|
||||
1. **Keyword Matching** - Traditional string/regex-based matching (default)
|
||||
2. **Vector Matching** - Semantic similarity using embeddings (placeholder implementation)
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Basic Usage with Default (Keyword) Matching
|
||||
|
||||
```typescript
|
||||
// Default behavior - uses keyword matching
|
||||
const result = await promptBuilder.infillContentModular({
|
||||
templateContext,
|
||||
charName,
|
||||
personaName,
|
||||
useChatFormat: false
|
||||
})
|
||||
```
|
||||
|
||||
### Using Vector Matching
|
||||
|
||||
```typescript
|
||||
// Use vector-based semantic matching
|
||||
const result = await promptBuilder.infillContentModular({
|
||||
templateContext,
|
||||
charName,
|
||||
personaName,
|
||||
useChatFormat: false,
|
||||
matchingStrategyConfig: {
|
||||
strategy: 'vector',
|
||||
vectorOptions: {
|
||||
threshold: 0.7,
|
||||
maxResults: 10
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Strategy Interface
|
||||
|
||||
All matching strategies implement the `LoreMatchingStrategy` interface:
|
||||
|
||||
```typescript
|
||||
interface LoreMatchingStrategy {
|
||||
matchesMessage(
|
||||
entry: SelectWorldLoreEntry | SelectCharacterLoreEntry | SelectHistoryEntry,
|
||||
message: { id: number; message: string | undefined },
|
||||
context?: {
|
||||
interpolationContext: InterpolationContext
|
||||
chatMessages: Array<{ id: number; message: string | undefined }>
|
||||
failedMatches: Record<number, number[]>
|
||||
}
|
||||
): Promise<boolean> | boolean
|
||||
|
||||
initialize?(): Promise<void>
|
||||
cleanup?(): Promise<void>
|
||||
getName(): string
|
||||
}
|
||||
```
|
||||
|
||||
### Strategy Implementations
|
||||
|
||||
#### KeywordMatchingStrategy
|
||||
- **Purpose**: Fast, traditional string/regex-based matching
|
||||
- **Performance**: Very fast, no setup required
|
||||
- **Use case**: When you need quick matching based on exact keywords
|
||||
- **Configuration**: None required
|
||||
|
||||
#### VectorMatchingStrategy (Placeholder)
|
||||
- **Purpose**: Semantic similarity using embeddings
|
||||
- **Performance**: Slower, requires model initialization
|
||||
- **Use case**: When you want semantic understanding beyond keywords
|
||||
- **Configuration**: Threshold, model selection (future)
|
||||
- **Status**: Currently falls back to keyword matching - awaiting vector implementation
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Keyword Strategy
|
||||
```typescript
|
||||
{
|
||||
strategy: 'keyword'
|
||||
// No additional options
|
||||
}
|
||||
```
|
||||
|
||||
### Vector Strategy
|
||||
```typescript
|
||||
{
|
||||
strategy: 'vector',
|
||||
vectorOptions: {
|
||||
model?: string, // Future: embedding model name
|
||||
threshold?: number, // Similarity threshold (0-1)
|
||||
maxResults?: number // Max results to consider
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Performance Options (Future)
|
||||
```typescript
|
||||
{
|
||||
performance: {
|
||||
cacheEmbeddings?: boolean, // Cache computed embeddings
|
||||
batchSize?: number // Batch size for vector operations
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Factory Pattern
|
||||
|
||||
Use the `MatchingStrategyFactory` for easy strategy creation:
|
||||
|
||||
```typescript
|
||||
import { MatchingStrategyFactory } from './LoreMatchingStrategies'
|
||||
|
||||
// Create a strategy
|
||||
const strategy = await MatchingStrategyFactory.createStrategy({
|
||||
strategy: 'vector',
|
||||
vectorOptions: { threshold: 0.7 }
|
||||
})
|
||||
|
||||
// Get available strategies
|
||||
const available = MatchingStrategyFactory.getAvailableStrategies()
|
||||
console.log(available) // ['keyword', 'vector']
|
||||
```
|
||||
|
||||
## Runtime Strategy Switching
|
||||
|
||||
```typescript
|
||||
// Create with one strategy
|
||||
const engine = await ContentInfillEngine.createWithStrategy(
|
||||
/* parameters */,
|
||||
{ strategy: 'keyword' }
|
||||
)
|
||||
|
||||
// Switch to a different strategy at runtime
|
||||
const vectorStrategy = await MatchingStrategyFactory.createStrategy({
|
||||
strategy: 'vector',
|
||||
vectorOptions: { threshold: 0.8 }
|
||||
})
|
||||
await engine.setMatchingStrategy(vectorStrategy)
|
||||
|
||||
// Check current strategy
|
||||
console.log(engine.getMatchingStrategyName()) // "vector"
|
||||
```
|
||||
|
||||
## Integration Points
|
||||
|
||||
### ContentInfillEngine
|
||||
The `ContentInfillEngine` is the main integration point for matching strategies:
|
||||
|
||||
```typescript
|
||||
// Constructor accepts optional strategy
|
||||
const engine = new ContentInfillEngine(
|
||||
/* ... parameters ... */,
|
||||
optionalMatchingStrategy
|
||||
)
|
||||
|
||||
// Factory method for strategy config
|
||||
const engine = await ContentInfillEngine.createWithStrategy(
|
||||
/* ... parameters ... */,
|
||||
strategyConfig
|
||||
)
|
||||
|
||||
// Runtime strategy updates
|
||||
await engine.setMatchingStrategy(newStrategy)
|
||||
```
|
||||
|
||||
### PromptBuilder
|
||||
The main `PromptBuilder` class supports strategies via `infillContentModular`:
|
||||
|
||||
```typescript
|
||||
await promptBuilder.infillContentModular({
|
||||
/* ... other parameters ... */,
|
||||
matchingStrategy: explicitStrategy, // Direct strategy instance
|
||||
matchingStrategyConfig: strategyConfig // Strategy configuration
|
||||
})
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Strategy Selection Guidelines
|
||||
|
||||
- **Use Keyword** when:
|
||||
- Performance is critical
|
||||
- Simple keyword matching is sufficient
|
||||
- No semantic understanding needed
|
||||
|
||||
- **Use Vector** when:
|
||||
- Semantic similarity is important
|
||||
- Content has complex relationships
|
||||
- Willing to trade performance for accuracy
|
||||
|
||||
### Optimization Tips
|
||||
|
||||
1. **Caching**: Future vector implementations will support embedding caching
|
||||
2. **Batch Processing**: Vector operations can be batched for better performance
|
||||
3. **Thresholds**: Tune similarity thresholds to balance precision vs recall
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Vector Implementation Roadmap
|
||||
|
||||
1. **Embedding Integration**
|
||||
- OpenAI embeddings API
|
||||
- Local embedding models (Sentence Transformers)
|
||||
- Custom embedding endpoints
|
||||
|
||||
2. **Performance Optimizations**
|
||||
- Embedding caching system
|
||||
- Batch processing for multiple entries
|
||||
- Incremental index updates
|
||||
|
||||
3. **Advanced Features**
|
||||
- Multiple embedding models support
|
||||
- Similarity ranking and sorting
|
||||
- Custom similarity functions
|
||||
|
||||
### Configuration Enhancements
|
||||
|
||||
1. **Per-Content-Type Strategies**
|
||||
```typescript
|
||||
{
|
||||
worldLore: { strategy: 'vector' },
|
||||
characterLore: { strategy: 'keyword' },
|
||||
history: { strategy: 'hybrid' }
|
||||
}
|
||||
```
|
||||
|
||||
2. **Dynamic Strategy Selection**
|
||||
- Based on content size
|
||||
- Based on user preferences
|
||||
- Based on performance metrics
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### From Previous Keyword-Only System
|
||||
|
||||
The new system is **fully backward compatible**. Existing code will continue to work without changes, using keyword matching by default.
|
||||
|
||||
To adopt the new system:
|
||||
|
||||
1. **No changes needed** - continues using keyword matching
|
||||
2. **Opt-in to new strategies** - add `matchingStrategyConfig` parameter
|
||||
3. **Runtime switching** - use factory methods for dynamic behavior
|
||||
|
||||
### Example Migration
|
||||
|
||||
```typescript
|
||||
// Old approach (still works)
|
||||
const result = await promptBuilder.infillContentModular({
|
||||
templateContext,
|
||||
charName,
|
||||
personaName,
|
||||
useChatFormat: false
|
||||
})
|
||||
|
||||
// New approach - same result, explicit strategy
|
||||
const result = await promptBuilder.infillContentModular({
|
||||
templateContext,
|
||||
charName,
|
||||
personaName,
|
||||
useChatFormat: false,
|
||||
matchingStrategyConfig: { strategy: 'keyword' }
|
||||
})
|
||||
|
||||
// New approach - enhanced with vector matching
|
||||
const result = await promptBuilder.infillContentModular({
|
||||
templateContext,
|
||||
charName,
|
||||
personaName,
|
||||
useChatFormat: false,
|
||||
matchingStrategyConfig: {
|
||||
strategy: 'vector',
|
||||
vectorOptions: { threshold: 0.7 }
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
See `/examples/MatchingStrategyUsage.ts` for comprehensive usage examples including:
|
||||
|
||||
- Creating engines with different strategies
|
||||
- Runtime strategy switching
|
||||
- Factory pattern usage
|
||||
- Configuration examples for different use cases
|
||||
- Integration with existing PromptBuilder workflow
|
||||
|
||||
## Implementation Status
|
||||
|
||||
- ✅ **Architecture**: Complete strategy interface and factory
|
||||
- ✅ **Keyword Strategy**: Full implementation (existing logic)
|
||||
- 🔄 **Vector Strategy**: Placeholder implementation (falls back to keyword)
|
||||
- ✅ **Hybrid Strategy**: Complete implementation
|
||||
- ✅ **Integration**: ContentInfillEngine and PromptBuilder support
|
||||
- ✅ **Examples**: Comprehensive usage examples
|
||||
- ✅ **Documentation**: This guide
|
||||
|
||||
**Next Steps**: Implement actual vector embeddings in `VectorMatchingStrategy`
|
||||
56
src/lib/server/utils/promptBuilder/QUICK_START.md
Normal file
56
src/lib/server/utils/promptBuilder/QUICK_START.md
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
# Quick Reference: Switching Matching Strategies
|
||||
|
||||
## TL;DR
|
||||
|
||||
You can now easily switch between keyword matching and vector matching (when implemented) for lore entries. Here's how:
|
||||
|
||||
## Default Behavior (No Changes Needed)
|
||||
```typescript
|
||||
// This continues to work as before - uses keyword matching
|
||||
const result = await promptBuilder.infillContentModular({
|
||||
templateContext,
|
||||
charName,
|
||||
personaName,
|
||||
useChatFormat: false
|
||||
})
|
||||
```
|
||||
|
||||
## Switch to Vector Matching
|
||||
```typescript
|
||||
// Use semantic vector matching instead of keywords
|
||||
const result = await promptBuilder.infillContentModular({
|
||||
templateContext,
|
||||
charName,
|
||||
personaName,
|
||||
useChatFormat: false,
|
||||
matchingStrategyConfig: {
|
||||
strategy: 'vector',
|
||||
vectorOptions: { threshold: 0.7 }
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Available Strategies
|
||||
|
||||
1. **`'keyword'`** - Fast, traditional string matching (default)
|
||||
2. **`'vector'`** - Semantic similarity via embeddings (placeholder - falls back to keyword for now)
|
||||
|
||||
## Key Benefits
|
||||
|
||||
✅ **Backward Compatible** - Existing code works unchanged
|
||||
✅ **Easy Switching** - One parameter change to switch strategies
|
||||
✅ **Future Ready** - Vector implementation can be added without code changes
|
||||
✅ **Configurable** - Tune thresholds and weights per use case
|
||||
✅ **Runtime Flexible** - Can change strategies while running
|
||||
|
||||
## What's Ready Now
|
||||
|
||||
- ✅ Full architecture and interfaces
|
||||
- ✅ Keyword strategy (existing logic)
|
||||
- ✅ Strategy factory and configuration
|
||||
- ✅ Integration with PromptBuilder
|
||||
- 🔄 Vector strategy (placeholder - needs embedding implementation)
|
||||
|
||||
## Next Step: Vector Implementation
|
||||
|
||||
When vector matching is implemented, it will provide semantic understanding beyond simple keyword matching, allowing for more intelligent lore selection based on meaning rather than exact words.
|
||||
157
src/lib/server/utils/promptBuilder/README.md
Normal file
157
src/lib/server/utils/promptBuilder/README.md
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
# Prompt Builder Modularization
|
||||
|
||||
This directory contains the modularized prompt builder system, breaking down the monolithic `PromptBuilder` class into focused, reusable components.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
The new modular architecture follows these principles:
|
||||
- **Single Responsibility**: Each class has one clear purpose
|
||||
- **Dependency Injection**: Components can be easily tested and swapped
|
||||
- **Strategy Pattern**: Different implementations for different use cases
|
||||
- **Clear Interfaces**: Well-defined contracts between components
|
||||
|
||||
## Core Modules
|
||||
|
||||
### 1. **TemplateEngine.ts**
|
||||
Handles Handlebars template compilation and helper registration.
|
||||
- Manages Handlebars instance
|
||||
- Registers role-specific helpers (systemBlock, assistantBlock, userBlock)
|
||||
- Provides string interpolation utilities
|
||||
|
||||
### 2. **TokenManager.ts**
|
||||
Manages token counting and limits.
|
||||
- Token counting operations
|
||||
- Threshold and limit checking
|
||||
- Encapsulates token-related logic
|
||||
|
||||
### 3. **ContentMatchers.ts**
|
||||
Strategy pattern for matching content against lore entries.
|
||||
- `LorebookMatcher`: Handles world/character lore matching
|
||||
- `HistoryMatcher`: Handles history entry matching
|
||||
- Supports regex and case-sensitive matching
|
||||
|
||||
### 4. **ContentProviders.ts**
|
||||
Priority-based content provision system.
|
||||
- `MessageProvider`: Provides chat messages by priority
|
||||
- `WorldLoreProvider`: Provides world lore entries
|
||||
- `CharacterLoreProvider`: Provides character lore entries
|
||||
- `HistoryProvider`: Provides history entries
|
||||
|
||||
### 5. **ContextBuilders.ts**
|
||||
Builder pattern for constructing different context types.
|
||||
- `CharacterContextBuilder`: Builds character context data
|
||||
- `PersonaContextBuilder`: Builds persona context data
|
||||
- `ScenarioContextBuilder`: Builds scenario context with interpolation
|
||||
|
||||
### 6. **ContentAssembler.ts**
|
||||
Orchestrates content assembly with token limits.
|
||||
- Priority-based content inclusion
|
||||
- Token-aware assembly process
|
||||
- Lore matching and inclusion logic
|
||||
|
||||
### 7. **Renderers.ts**
|
||||
Strategy pattern for different output formats.
|
||||
- `TextPromptRenderer`: Renders standard text prompts
|
||||
- `ChatCompletionRenderer`: Renders OpenAI chat completion format
|
||||
- `RendererFactory`: Creates appropriate renderer based on options
|
||||
|
||||
### 8. **PromptConfiguration.ts**
|
||||
Encapsulates all configuration data.
|
||||
- Centralizes configuration access
|
||||
- Provides typed getters for configuration values
|
||||
- Simplifies configuration management
|
||||
|
||||
### 9. **PromptCompiler.ts**
|
||||
Main orchestrator that uses all modules.
|
||||
- Coordinates the compilation process
|
||||
- Manages dependencies between modules
|
||||
- Provides clean public API
|
||||
|
||||
### 10. **types.ts**
|
||||
Shared type definitions.
|
||||
- Common interfaces and types
|
||||
- Ensures type consistency across modules
|
||||
|
||||
## Usage
|
||||
|
||||
### New Modular API
|
||||
```typescript
|
||||
const config = new PromptConfiguration(connection, sampling, contextConfig, promptConfig, tokenLimit, threshold)
|
||||
const compiler = new PromptCompiler(config, chat, currentCharacterId, tokenCounter)
|
||||
const result = await compiler.compile({ useChatFormat: true })
|
||||
```
|
||||
|
||||
### Legacy API (still supported)
|
||||
```typescript
|
||||
const builder = new PromptBuilder({ /* existing constructor args */ })
|
||||
const result = await builder.compilePrompt({ useChatFormat: true })
|
||||
```
|
||||
|
||||
### Modular API on existing instance
|
||||
```typescript
|
||||
const builder = new PromptBuilder({ /* existing constructor args */ })
|
||||
const result = await builder.compilePromptModular({ useChatFormat: true })
|
||||
```
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
The migration follows a gradual approach:
|
||||
|
||||
1. ✅ **Phase 1**: Extract core modules (Template, Token, Config)
|
||||
2. ✅ **Phase 2**: Create content strategies (Providers, Matchers, Builders)
|
||||
3. ✅ **Phase 3**: Build orchestrator (Assembler, Renderers, Compiler)
|
||||
4. 🔄 **Phase 4**: Integrate with existing class (current)
|
||||
5. ⏳ **Phase 5**: Move complex assembly logic to modules
|
||||
6. ⏳ **Phase 6**: Deprecate legacy methods
|
||||
7. ⏳ **Phase 7**: Full migration to modular system
|
||||
|
||||
## Benefits
|
||||
|
||||
### Testability
|
||||
Each component can be unit tested in isolation:
|
||||
```typescript
|
||||
const tokenManager = new TokenManager(mockTokenCounter, 1000, 0.8)
|
||||
expect(tokenManager.isOverLimit(1200)).toBe(true)
|
||||
```
|
||||
|
||||
### Extensibility
|
||||
New content types or renderers can be added easily:
|
||||
```typescript
|
||||
class CustomRenderer implements PromptRenderer {
|
||||
render(context: TemplateContext, template: string) {
|
||||
// Custom rendering logic
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Performance
|
||||
Components can be optimized independently and cached:
|
||||
```typescript
|
||||
const templateEngine = new TemplateEngine()
|
||||
templateEngine.registerHelpers({ useChatFormat: true }) // Cache helpers
|
||||
```
|
||||
|
||||
### Maintainability
|
||||
Bugs are isolated to specific modules, making debugging easier.
|
||||
|
||||
## Error Handling
|
||||
|
||||
Each module handles its own errors and provides meaningful error messages:
|
||||
- Template compilation errors include template context
|
||||
- Token counting errors specify the content that failed
|
||||
- Content matching errors identify the problematic entry
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- Template engines are created once and reused
|
||||
- Handlebars helpers are registered once per format type
|
||||
- Content providers use iterators for memory efficiency
|
||||
- Token counting is batched where possible
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. **Caching Layer**: Add caching for compiled templates and token counts
|
||||
2. **Plugin System**: Allow custom content providers and matchers
|
||||
3. **Async Streaming**: Support streaming content assembly for large contexts
|
||||
4. **Metrics Collection**: Built-in performance and usage metrics
|
||||
5. **Configuration Validation**: Runtime validation of configuration objects
|
||||
File diff suppressed because it is too large
Load diff
78
src/lib/server/utils/promptBuilder/types.ts
Normal file
78
src/lib/server/utils/promptBuilder/types.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
export type TemplateContextCharacter = {
|
||||
name: string
|
||||
nickname?: string
|
||||
description: string
|
||||
personality?: string
|
||||
loreEntries?: SelectCharacterLoreEntry[]
|
||||
category?: string
|
||||
lorebookBindingId?: number | null
|
||||
year?: number
|
||||
month?: number
|
||||
day?: number
|
||||
}
|
||||
|
||||
export type TemplateContextPersona = {
|
||||
name: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export type TemplateContext = {
|
||||
instructions: string
|
||||
characters: TemplateContextCharacter[] | string // can be JSON stringified
|
||||
personas: TemplateContextPersona[] | string // can be JSON stringified
|
||||
scenario: string
|
||||
exampleDialogue?: string
|
||||
postHistoryInstructions?: string
|
||||
chatMessages: any[]
|
||||
char: string
|
||||
character: string
|
||||
user: string
|
||||
persona: string
|
||||
worldLore?: string
|
||||
characterLore?: SelectCharacterLoreEntry[]
|
||||
history?: string
|
||||
currentDate?: string
|
||||
__promptBuilderInstance?: any
|
||||
}
|
||||
|
||||
export type CompiledPrompt = {
|
||||
prompt: string | undefined
|
||||
messages: any[] | undefined
|
||||
meta: {
|
||||
promptFormat: string
|
||||
templateName: string | null
|
||||
timestamp: string
|
||||
truncationReason: string | null
|
||||
currentTurnCharacterId: number
|
||||
tokenCounts: {
|
||||
total: number
|
||||
limit: number
|
||||
}
|
||||
chatMessages: {
|
||||
included: number
|
||||
total: number
|
||||
includedIds: number[]
|
||||
excludedIds: number[]
|
||||
}
|
||||
sources: any
|
||||
}
|
||||
}
|
||||
|
||||
export type InterpolationContext = {
|
||||
char: string
|
||||
character: string
|
||||
user: string
|
||||
persona: string
|
||||
}
|
||||
|
||||
export type AssembledContent = {
|
||||
templateContext: TemplateContext
|
||||
includedWorldLoreEntries: SelectWorldLoreEntry[]
|
||||
includedCharacterLoreEntries: SelectCharacterLoreEntry[]
|
||||
includedHistoryEntries: SelectHistoryEntry[]
|
||||
chatMessages: any[]
|
||||
}
|
||||
|
||||
export type CompileOptions = {
|
||||
useChatFormat?: boolean
|
||||
}
|
||||
42
src/lib/server/utils/promptBuilder/utils.ts
Normal file
42
src/lib/server/utils/promptBuilder/utils.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import type { ChatCompletionMessageParam } from "openai/resources/index.mjs"
|
||||
|
||||
/**
|
||||
* Parse a split chat prompt into OpenAI chat format
|
||||
*/
|
||||
export function parseSplitChatPrompt(prompt: string): ChatCompletionMessageParam[] {
|
||||
const blocks = prompt.split(/(?=<@role:(user|assistant|system)>\s*)/g)
|
||||
// TODO: populate the name param, but local models may ignore it anyway
|
||||
const messages = blocks
|
||||
.map((block) => {
|
||||
const match = block.match(
|
||||
/^<@role:(user|assistant|system)>\s*([\s\S]*)$/
|
||||
)
|
||||
return match ? { role: match[1], content: match[2].trim() } : null
|
||||
})
|
||||
.filter(Boolean)
|
||||
|
||||
return messages as ChatCompletionMessageParam[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard for history entries
|
||||
*/
|
||||
export function isHistoryEntry(entry: any): entry is SelectHistoryEntry {
|
||||
return entry && typeof entry === "object" && "date" in entry
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper type guard for extended lorebook
|
||||
*/
|
||||
export function hasLorebookEntries(lorebook: any): lorebook is SelectLorebook & {
|
||||
worldLoreEntries: SelectWorldLoreEntry[]
|
||||
characterLoreEntries: SelectCharacterLoreEntry[]
|
||||
historyEntries: SelectHistoryEntry[]
|
||||
} {
|
||||
return (
|
||||
lorebook &&
|
||||
Array.isArray(lorebook.worldLoreEntries) &&
|
||||
Array.isArray(lorebook.characterLoreEntries) &&
|
||||
Array.isArray(lorebook.historyEntries)
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue