Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions common/grammar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ MainCharacter "main-character" = ("mainchar"i / "mainbot"i) { return "main-char"
Character "character" = ("char"i / "character"i / "bot"i) { return "char" }
User "user" = "user"i { return "user" }
Scenario "scenario" = "scenario"i { return "scenario" }
SceneClock "scene-clock" = ("scene_clock"i / "sceneclock"i) { return "scene_clock" }
Impersonate "impersonating" = ("impersonate"i / "impersonating"i / "impersonality"i) { return "impersonating" }
Persona "personality" = ("personality"i / "persona"i) { return "personality" }
AllPersona "all_personalities" = ("all_personas"i / "all_personalities"i) { return "all_personalities" }
Expand Down Expand Up @@ -173,6 +174,7 @@ Interp "interp"
/ UserEmbed
/ User
/ Scenario
/ SceneClock
/ Persona
/ Impersonate
/ AllPersona
Expand Down
23 changes: 21 additions & 2 deletions common/prompt-order.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ export function promptOrderToTemplate(
) {
const parts: string[] = []
const preamble = getOrderHolder(format, 'preamble')
const normalized = withSceneClock(order)
if (preamble) parts.push(preamble)

for (const item of order) {
for (const item of normalized) {
if (!item.enabled) continue

const text = getOrderHolder(format, item.placeholder)
Expand All @@ -36,7 +37,7 @@ export function promptOrderToTemplate(
}

export function promptOrderToSections(opts: OrderOptions) {
const order = (opts.order || SIMPLE_ORDER).filter(
const order = withSceneClock(opts.order || SIMPLE_ORDER).filter(
(o) =>
o.placeholder !== 'system_prompt' &&
o.placeholder !== 'ujb' &&
Expand Down Expand Up @@ -75,9 +76,23 @@ function getOrderHolder(format: string, holder: string) {
return formatHolders[format]?.[holder] || formatHolders.Universal[holder] || ''
}

function withSceneClock(order: NonNullable<AppSchema.GenSettings['promptOrder']>) {
if (order.some((item) => item.placeholder === 'scene_clock')) return order

const index = order.findIndex((item) => item.placeholder === 'scenario')
const sceneClock = { placeholder: 'scene_clock', enabled: true }

if (index === -1) {
return [sceneClock, ...order]
}

return order.slice(0, index + 1).concat(sceneClock, order.slice(index + 1))
}

export const SIMPLE_ORDER: NonNullable<AppSchema.GenSettings['promptOrder']> = [
'system_prompt',
'scenario',
'scene_clock',
'personality',
'chat_embed',
'memory',
Expand All @@ -89,6 +104,7 @@ export const formatHolders: Record<string, Record<string, string>> = {
Universal: {
system_prompt: neat`<system>{{#if system_prompt}}{{value}}{{#else}}${defaultSystemPrompt}{{/else}}{{/if}}</system>`,
scenario: neat`{{#if scenario}}The scenario of the conversation:\n{{scenario}}\n{{/if}}`,
scene_clock: `{{scene_clock}}`,
memory: neat`{{#if memory}}"{{char}}'s" memories:\n{{memory}}\n{{/if}}`,
personality: neat`{{#if personality}}{{char}}'s personality:\n{{personality}}\n{{/if}}`,
impersonating: neat`{{#if impersonating}}{{user}}'s personality:\n{{impersonating}}\n{{/if}}`,
Expand Down Expand Up @@ -145,6 +161,7 @@ export const formatHolders: Record<string, Record<string, string>> = {
system_prompt: neat`<|im_start|>system
{{#if system_prompt}}{{value}}<|im_end|>{{#else}}${defaultSystemPrompt}<|im_end|>{{/else}}{{/if}}`,
scenario: neat`{{#if scenario}}The scenario of the conversation:\n{{scenario}}\n{{/if}}`,
scene_clock: `{{scene_clock}}`,
memory: neat`{{#if memory}}"{{char}}'s" memories:\n{{memory}}\n{{/if}}`,
personality: neat`{{#if personality}}{{char}}'s personality:\n{{personality}}\n{{/if}}`,
impersonating: neat`{{#if impersonating}}{{user}}'s personality:\n{{impersonating}}\n{{/if}}`,
Expand All @@ -162,6 +179,7 @@ export const formatHolders: Record<string, Record<string, string>> = {
system_prompt: neat`<|begin_of_text|><|start_header_id|>system
{{#if system_prompt}}{{value}}<|eot_id|>{{#else}}${defaultSystemPrompt}<|eot_id|>{{/else}}{{/if}}`,
scenario: neat`{{#if scenario}}The scenario of the conversation:\n{{scenario}}\n{{/if}}`,
scene_clock: `{{scene_clock}}`,
memory: neat`{{#if memory}}"{{char}}'s" memories:\n{{memory}}\n{{/if}}`,
personality: neat`{{#if personality}}"{{char}}'s" personality:\n{{personality}}\n{{/if}}`,
impersonating: neat`{{#if impersonating}}"{{user}}'s" personality:\n{{impersonating}}\n{{/if}}`,
Expand All @@ -178,6 +196,7 @@ export const formatHolders: Record<string, Record<string, string>> = {
'Pyg/Simple': {
history: `Start of the conversation:\n\n{{history}}`,
scenario: `{{#if scenario}}Scenario: {{scenario}}{{/if}}`,
scene_clock: `{{scene_clock}}`,
},
}

Expand Down
66 changes: 64 additions & 2 deletions common/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export type PromptLine = {

export type PromptPlaceholders = {
scenario?: string
sceneClock?: string
greeting?: string
sampleChat?: string[]
persona: string
Expand Down Expand Up @@ -138,6 +139,7 @@ const HOLDER_NAMES = {
memory: 'memory',
post: 'post',
scenario: 'scenario',
sceneClock: 'scene_clock',
history: 'history',
systemPrompt: 'system_prompt',
linebreak: 'br',
Expand All @@ -154,6 +156,7 @@ export const HOLDERS = {
ujb: /{{ujb}}/gi,
sampleChat: /{{example_dialogue}}/gi,
scenario: /{{scenario}}/gi,
sceneClock: /{{scene_clock}}/gi,
memory: /{{memory}}/gi,
persona: /{{personality}}/gi,
allPersonas: /{{all_personalities}}/gi,
Expand Down Expand Up @@ -405,7 +408,7 @@ export function getTemplate(opts: Pick<GenerateRequestV2, 'settings' | 'chat'>)
}

// Deprecated
return ensureValidTemplate(template)
return ensureValidTemplate(template, opts.chat.sceneClock?.enabled ? undefined : ['sceneClock'])
}

type InjectOpts = {
Expand Down Expand Up @@ -466,15 +469,22 @@ function replaceSectionTags(sections: Record<string, string[] | any>, format: Mo
*/
export function ensureValidTemplate(
template: string,
skip?: Array<'history' | 'post' | 'persona' | 'scenario' | 'userEmbed' | 'chatEmbed'>
skip?: Array<
'history' | 'post' | 'persona' | 'scenario' | 'sceneClock' | 'userEmbed' | 'chatEmbed'
>
) {
const skips = new Set(skip || [])

let hasHistory = !!template.match(HOLDERS.history) || !!template.match(/{{\#each msg}}/gi)
let hasPost = !!template.match(HOLDERS.post)
let hasSceneClock = !!template.match(HOLDERS.sceneClock)

let modified = template

if (!skips.has('sceneClock') && !hasSceneClock) {
modified = insertSceneClockHolder(modified)
}

if (!skips.has('post') && !skips.has('history') && !hasHistory && !hasPost) {
modified += `\n{{history}}\n{{post}}`
} else if (!skips.has('history') && !hasHistory && hasPost) {
Expand All @@ -486,6 +496,20 @@ export function ensureValidTemplate(
return modified
}

function insertSceneClockHolder(template: string) {
const sceneClock = `{{${HOLDER_NAMES.sceneClock}}}`

if (template.match(HOLDERS.persona)) {
return template.replace(HOLDERS.persona, `${sceneClock}{{${HOLDER_NAMES.persona}}}`)
}

if (template.match(HOLDERS.history)) {
return template.replace(HOLDERS.history, `${sceneClock}{{${HOLDER_NAMES.history}}}`)
}

return `${template}${sceneClock}`
}

type PromptPartsOptions = Pick<
PromptOpts,
| 'kind'
Expand Down Expand Up @@ -575,6 +599,7 @@ export async function buildPromptPlaceholders(
// replyAs instead of the main character
// (we always use the main character's scenario, not replyAs)
parts.scenario = replace(opts.resolvedScenario, char.name)
parts.sceneClock = getSceneClockPrompt(chat.sceneClock)

const sampleChat =
replyAs._id === char._id && !!chat.overrides
Expand Down Expand Up @@ -643,6 +668,43 @@ export async function buildPromptPlaceholders(
}
}

function getSceneClockPrompt(clock?: AppSchema.SceneClock) {
if (!clock?.enabled) return ''

const date = clock.date?.trim()
const time = clock.time?.trim()
const current = [date, time].filter(Boolean).join(' ')
if (!current && !clock.dayOfWeek?.trim() && !clock.calendarName?.trim() && !clock.notes?.trim())
return ''

const lines = ['[Scene Clock]']
if (current) {
lines.push(`Current in-scene date and time: ${current}.`)
lines.push(
'Use this as the current in-scene time, not a fixed value. Advance or change it whenever the narrative or user indicates that time passes.'
)
}
if (clock.dayOfWeek?.trim()) {
lines.push(`Day of week: ${clock.dayOfWeek.trim()}.`)
}
if (clock.calendarName?.trim()) {
lines.push(`Calendar: ${clock.calendarName.trim()}.`)
}
if (clock.notes?.trim()) {
lines.push(`Notes: ${clock.notes.trim()}`)
}
if (clock.allowAssistantUpdates) {
lines.push(
'When your response will change the in-scene date or time, begin the response with exactly one hidden control tag before any dialogue:',
'<scene_clock_update>{"date":"NEW_DATE","time":"NEW_TIME"}</scene_clock_update>',
'The opening and closing scene_clock_update tags are mandatory. Use valid JSON inside them. You may include date, time, dayOfWeek, calendarName, or notes; omit unchanged fields. Place the tag at the very start, never at the end, and do not mention it in the dialogue.'
)
}
lines.push('[/Scene Clock]')

return `${lines.join('\n')}\n`
}

function getSupplementaryParts(opts: PromptPartsOptions, replyAs: AppSchema.Character) {
const { settings, chat } = opts
const parts = {
Expand Down
78 changes: 78 additions & 0 deletions common/scene-clock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { AppSchema } from './types'

export const SCENE_CLOCK_UPDATE_TAG =
/(?:<\s*)?scene[\s_-]*clock[\s_-]*update\s*>\s*([\s\S]*?)\s*(?:<\s*)?\/\s*scene[\s_-]*clock[\s_-]*update\s*>/gi

const SCENE_CLOCK_UPDATE_MARKDOWN =
/^\s*\*{1,2}\s*scene[\s_-]*clock[\s_-]*update\s*\*{0,2}\s*(\{[^\n]*\})\s*\*{0,2}(?:\s*\n|$)/i

type SceneClockUpdate = Partial<
Pick<AppSchema.SceneClock, 'date' | 'time' | 'dayOfWeek' | 'calendarName' | 'notes'>
>

export function parseSceneClockUpdate(response: string): {
text: string
update?: SceneClockUpdate
} {
let update: SceneClockUpdate | undefined
let text = response.replace(SCENE_CLOCK_UPDATE_TAG, (_, json: string) => {
const parsed = parseUpdateJson(json)
if (parsed) update = parsed
return ''
})

if (!update) {
const markdownUpdate = text.match(SCENE_CLOCK_UPDATE_MARKDOWN)
if (markdownUpdate) {
update = parseUpdateJson(markdownUpdate[1])
text = text.slice(markdownUpdate[0].length)
}
}

if (!update) {
const leadingJson = text.match(/^\s*(\{[^\n]*\})(?:\s*\n|$)/)
if (leadingJson) {
const parsed = parseUpdateJson(leadingJson[1])
if (parsed) {
update = parsed
text = text.slice(leadingJson[0].length)
}
}
}

// Keep accepting the previous end-of-response fallback for existing prompts and retries.
if (!update) {
const trailingJson = text.match(/(?:^|\n)\s*(\{[^\n]*\})\s*$/)
if (trailingJson?.index !== undefined) {
const parsed = parseUpdateJson(trailingJson[1])
if (parsed) {
update = parsed
text = text.slice(0, trailingJson.index)
}
}
}

return {
text: text.replace(/[ \t]+\n/g, '\n').trim(),
update,
}
}

function parseUpdateJson(value: string): SceneClockUpdate | undefined {
try {
const parsed = JSON.parse(value)
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return

const update: SceneClockUpdate = {}
for (const key of ['date', 'time', 'dayOfWeek', 'calendarName', 'notes'] as const) {
const next = parsed[key]
if (next === undefined) continue
if (typeof next !== 'string') return
update[key] = next.trim()
}

return Object.keys(update).length ? update : undefined
} catch {
return
}
}
4 changes: 4 additions & 0 deletions common/template-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ type Holder =
| 'char'
| 'user'
| 'scenario'
| 'scene_clock'
| 'personality'
| 'example_dialogue'
| 'history'
Expand Down Expand Up @@ -1003,6 +1004,9 @@ function getPlaceholder(
case 'scenario':
return opts.parts?.scenario || opts.chat?.scenario || opts.char?.scenario || ''

case 'scene_clock':
return opts.parts?.sceneClock || ''

case 'memory':
return opts.parts?.memory || ''

Expand Down
18 changes: 18 additions & 0 deletions common/types/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,23 @@ export namespace AppSchema {
kind: 'saga-session'
}

export type SceneClockDateFormat = 'DD/MM/YYYY' | 'MM/DD/YYYY' | 'YYYY-MM-DD' | 'Long'
export type SceneClockTimeFormat = '12h' | '24h'

export interface SceneClock {
enabled: boolean
date: string
time: string
dateFormat: SceneClockDateFormat
timeFormat: SceneClockTimeFormat
dayOfWeek?: string
calendarName?: string
notes?: string
allowAssistantUpdates?: boolean
lastUpdatedBy?: 'user' | 'assistant' | 'system'
lastUpdatedAt?: string
}

export interface Chat {
_id: string
kind: 'chat'
Expand Down Expand Up @@ -297,6 +314,7 @@ export namespace AppSchema {

scenarioIds?: string[]
scenarioStates?: string[]
sceneClock?: SceneClock

treeLeafId?: string

Expand Down
14 changes: 14 additions & 0 deletions srv/api/chat/edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,19 @@ export const updateChat = handle(async ({ params, body, user, userId }) => {
useOverrides: 'boolean?',
userEmbedId: 'string?',
scenarioStates: ['string?'],
sceneClock: optional({
enabled: 'boolean',
date: 'string',
time: 'string',
dateFormat: ['DD/MM/YYYY', 'MM/DD/YYYY', 'YYYY-MM-DD', 'Long'],
timeFormat: ['12h', '24h'],
dayOfWeek: 'string?',
calendarName: 'string?',
notes: 'string?',
allowAssistantUpdates: 'boolean?',
lastUpdatedBy: ['user', 'assistant', 'system', null],
lastUpdatedAt: 'string?',
}),
systemPrompt: 'string?',
postHistoryInstructions: 'string?',
imageSource: 'string?',
Expand Down Expand Up @@ -54,6 +67,7 @@ export const updateChat = handle(async ({ params, body, user, userId }) => {
userEmbedId: body.userEmbedId ?? prev.userEmbedId,
scenarioIds: body.scenarioIds ?? prev.scenarioIds,
scenarioStates: body.scenarioStates ?? prev.scenarioStates,
sceneClock: body.sceneClock ?? prev.sceneClock,
imageSource: (body.imageSource as any) ?? prev.imageSource,
imageSettings: body.imageSettings,
invisible: body.invisible ?? prev.invisible,
Expand Down
1 change: 1 addition & 0 deletions tests/__snapshots__/prompt.spec.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ Object {
"SAMPLECHAT OtherBot",
],
"scenario": "MAIN MainChar",
"sceneClock": "",
"systemPrompt": undefined,
"ujb": "!!UJB_PROMPT!!",
"userEmbeds": Array [],
Expand Down
Loading