Skip to content

Commit

Permalink
Fixes and tweaks (#981)
Browse files Browse the repository at this point in the history
- Add option to hide avatar
- Fix char persona switch
- Add charlib automod prep
- Tweak homepage char card avatar size
- Fix default preset change from char form
- Announcements in notify modal
- Change site gaps
- Json schema type safety and callbacks
- Fix google unlink only if not registered as google
- Allow image through non-chat inference
- Char form random nam
  • Loading branch information
sceuick authored Jul 21, 2024
1 parent ac4c271 commit 5301614
Show file tree
Hide file tree
Showing 68 changed files with 1,998 additions and 663 deletions.
4 changes: 2 additions & 2 deletions .babelrc
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
"presets": ["solid"],
"env": {
"development": {
"presets": ["babel-preset-solid"],
"plugins": ["module:solid-refresh/babel"]
"presets": ["babel-preset-solid"]
// "plugins": ["module:solid-refresh/babel"]
}
}
}
2 changes: 2 additions & 0 deletions common/adapters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ export const OPENAI_MODELS = {
GPT4_Turbo: 'gpt-4-turbo',
GPT4_Turbo_0409: 'gpt-4-turbo-2024-04-09',
GPT4_Omni: 'gpt-4o',
GPT4_Omni_Mini: 'gpt-4o-mini',
} as const

export const MISTRAL_MODELS = {
Expand Down Expand Up @@ -187,6 +188,7 @@ export const OPENAI_CHAT_MODELS: Record<string, boolean> = {
[OPENAI_MODELS.GPT45_Preview]: true,
[OPENAI_MODELS.GPT4_Turbo_0409]: true,
[OPENAI_MODELS.GPT4_Omni]: true,
[OPENAI_MODELS.GPT4_Omni_Mini]: true,
}

/** Note: claude-v1 and claude-instant-v1 not included as they may point
Expand Down
196 changes: 185 additions & 11 deletions common/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@ import { defaultTemplate } from './mode-templates'
import { buildMemoryPrompt } from './memory'
import { defaultPresets, getFallbackPreset, isDefaultPreset } from './presets'
import { parseTemplate } from './template-parser'
import { getMessageAuthor, getBotName, trimSentence } from './util'
import { getMessageAuthor, getBotName, trimSentence, neat } from './util'
import { Memory } from './types'
import { promptOrderToTemplate } from './prompt-order'
import { ModelFormat, replaceTags } from './presets/templates'

export type TickHandler<T = any> = (response: string, state: InferenceState, json?: T) => void

export type InferenceState = 'partial' | 'done' | 'error' | 'warning'

export const SAMPLE_CHAT_MARKER = `System: New conversation started. Previous conversations are examples only.`
export const SAMPLE_CHAT_PREAMBLE = `How {{char}} speaks:`

Expand Down Expand Up @@ -129,6 +133,66 @@ export const HOLDERS = {
userEmbed: /{{user_embed}}/gi,
}

const defaultFieldPrompt = neat`
{{prop}}:
{{value}}
`
export function buildModPrompt(opts: {
prompt: string
fields: string
char: Partial<AppSchema.Character>
}) {
const aliases: { [key in keyof AppSchema.Character]?: string } = {
sampleChat: 'Example Dialogue',
postHistoryInstructions: 'Character Jailbreak',
systemPrompt: 'Character Instructions',
}

const props: Array<keyof AppSchema.Character> = [
'name',
'description',
'appearance',
'scenario',
'greeting',
'sampleChat',
'systemPrompt',
'postHistoryInstructions',
]

const inject = (prop: string, value: string) =>
(opts.fields || defaultFieldPrompt)
.replace(/{{prop}}/gi, prop)
.replace(/{{value}}/gi, value)
.replace(/\n\n+/g, '\n')

const fields = props
.filter((f) => {
const value = opts.char[f]
if (typeof value !== 'string') return false
return !!value.trim()
})
.map((f) => {
const value = opts.char[f]
if (typeof value !== 'string') return ''

const prop = titlize(aliases[f] || f)
return inject(prop, value)
})

for (const [attr, values] of Object.entries(opts.char.persona?.attributes || {})) {
const value = values.join(', ')
if (!value.trim()) continue

fields.push(inject(`Attribute '${titlize(attr)}'`, value))
}

return opts.prompt.replace(/{{fields}}/gi, fields.join('\n\n'))
}

function titlize(str: string) {
return `${str[0].toUpperCase()}${str.slice(1).toLowerCase()}`
}

/**
* This is only ever invoked client-side
* @param opts
Expand Down Expand Up @@ -914,15 +978,20 @@ export function resolveScenario(
return result.trim()
}

export type JsonType =
| { type: 'string'; description?: string; title?: string; maxLength?: number }
| { type: 'integer'; title?: string; description?: string }
| { type: 'enum'; enum: string[]; title?: string; description?: string }
export type JsonType = { title?: string; description?: string; valid?: string } & (
| { type: 'string'; maxLength?: number }
| { type: 'integer' }
| { type: 'enum'; enum: string[] }
| { type: 'bool' }
)

export type JsonProps = Record<string, JsonType>

export type JsonSchema = {
title: string
type: 'object'
properties: Record<string, JsonType>
properties: JsonProps
required: string[]
}

export const schema = {
Expand All @@ -943,26 +1012,131 @@ export const schema = {
description: o.desc,
}),
bool: (o?: { title?: string; desc?: string }) => ({
type: 'enum',
enum: ['true', 'false'],
type: 'bool',
enum: ['true', 'false', 'yes', 'no'],
title: o?.title,
description: o?.desc,
}),
} satisfies Record<string, (...args: any[]) => JsonType>

export function toJsonSchema(body: Record<string, JsonType>): JsonSchema {
export function toJsonSchema(body: JsonProps): JsonSchema {
const schema: JsonSchema = {
title: 'Response',
type: 'object',
properties: {},
required: [],
}

const props: JsonSchema['properties'] = {}

for (const [key, def] of Object.entries(body)) {
props[key] = def
for (let [key, def] of Object.entries(body)) {
key = key.replace(/_/g, ' ')
props[key] = { ...def }

delete props[key].valid
if (def.type === 'bool') {
props[key].type = 'enum'

// @ts-ignore
props[key].enum = ['true', 'false', 'yes', 'no']
}
}

schema.properties = props
schema.required = Object.keys(props)
return schema
}

type ToJsonPrimitive<T extends JsonType> = T['type'] extends 'string'
? string
: T['type'] extends 'integer'
? number
: T['type'] extends 'bool'
? boolean
: string[]

type JsonValid<T extends JsonProps> = { [key in keyof T]: ToJsonPrimitive<T[key]> }

export function fromJsonResponse<T extends JsonProps>(
schema: T,
response: any,
output: any = {}
): JsonValid<T> {
const json: Record<string, any> = tryJsonParseResponse(response)

for (let [key, value] of Object.entries(json)) {
const underscored = key.replace(/ /g, '_')

if (underscored in schema) {
key = underscored
}

const def = schema[key]
if (!def) continue

output[key] = value
if (def.type === 'bool') {
output[key] = value.trim() === 'true' || value.trim() === 'yes'
}
}

return output as JsonValid<T>
}

export function tryJsonParseResponse(res: string) {
if (typeof res === 'object') return res
try {
const json = JSON.parse(res)
return json
} catch (ex) {}

try {
const json = JSON.parse(res + '}')
return json
} catch (ex) {}

try {
if (res.trim().endsWith(',')) {
const json = JSON.parse(res.slice(0, -1))
return json
}
} catch (ex) {}

return {}
}

export function onJsonTickHandler<T extends JsonProps>(
schema: T,
handler: (res: Partial<JsonValid<T>>, state: InferenceState) => void
) {
let curr: Partial<JsonValid<T>> = {}
const parser: TickHandler = (res, state) => {
if (state === 'done') {
const body = fromJsonResponse(schema, tryJsonParseResponse(res))
if (Object.keys(body).length === 0) {
handler(curr, state)
return
}

handler(body, state)
return
}

if (state === 'partial') {
const body = fromJsonResponse(schema, tryJsonParseResponse(res))
const keys = Object.keys(body).length
if (keys === 0) return

const changed = Object.keys(curr).length !== keys
if (!changed) return

Object.assign(curr, body)
handler(curr, state)
return
}

handler(curr, state)
}

return parser
}
24 changes: 23 additions & 1 deletion common/types/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { UISettings } from './ui'
import { FullSprite } from './sprite'
import { ModelFormat } from '../presets/templates'
import * as Saga from './saga'
import type { JsonProps } from '../prompt'

export type AllDoc =
| AppSchema.Announcement
Expand Down Expand Up @@ -87,6 +88,14 @@ export namespace AppSchema {

maxGuidanceTokens: number
maxGuidanceVariables: number

modPresetId: string
modPrompt: string
modFieldPrompt: string
modSchema: JsonProps

charlibPublish: 'off' | 'users' | 'subscribers' | 'moderators' | 'admins'
charlibGuidelines: string
}

export type ImageModel = {
Expand All @@ -103,6 +112,8 @@ export namespace AppSchema {
title: string
content: string

location?: 'notification' | 'home'

/** Date ISO string */
showAt: string
hide: boolean
Expand Down Expand Up @@ -146,7 +157,7 @@ export namespace AppSchema {
preset: GenSettings &
Pick<
SubscriptionModel,
'allowGuestUsage' | 'isDefaultSub' | 'tokenizer' | '_id' | 'service'
'allowGuestUsage' | 'isDefaultSub' | 'tokenizer' | '_id' | 'service' | 'levels'
> & {
kind: 'submodel'
}
Expand Down Expand Up @@ -209,12 +220,16 @@ export namespace AppSchema {

updatedAt?: string

/** Date ISO string of last seen announcement */
announcement?: string

kind: 'user'
username: string
hash: string
apiKey?: string

admin: boolean
role?: 'moderator' | 'admin'

novelApiKey: string
novelModel: string
Expand Down Expand Up @@ -509,6 +524,12 @@ export namespace AppSchema {
userId: string
}

export interface SubscriptionModelLevel {
level: number
maxTokens: number
maxContextLength: number
}

export interface SubscriptionModel extends GenSettings {
_id: string
kind: 'subscription-setting'
Expand All @@ -523,6 +544,7 @@ export namespace AppSchema {
deletedAt?: string
tokenizer?: string
guidanceCapable?: boolean
levels: SubscriptionModelLevel[]
}

export interface GenSettings {
Expand Down
2 changes: 1 addition & 1 deletion common/types/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export const BG_THEME = ['truegray', 'coolgray', 'bluegray'] as const

export const UI_FONT = ['default', 'lato'] as const

export const AVATAR_SIZES = ['xs', 'sm', 'md', 'lg', 'xl', '2xl', '3xl'] as const
export const AVATAR_SIZES = ['hide', 'xs', 'sm', 'md', 'lg', 'xl', '2xl', '3xl', 'max3xl'] as const
export const AVATAR_CORNERS = ['sm', 'md', 'lg', 'circle', 'none'] as const

export const CHAT_WIDTHS = ['full', 'narrow', 'xl', '2xl', '3xl', 'fill'] as const
Expand Down
25 changes: 25 additions & 0 deletions common/valid/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,31 @@ export function assertValid<T extends Validator>(
}
}

/**
* @destructive
* Removes top-level properties from the object that aren't in the validator
*/
export function assertStrict<T extends Validator>(
opts: {
type: T
partial?: boolean
error?: string
},
compare: any
): asserts compare is UnwrapBody<T> {
const { type, partial } = opts
const { errors } = validateBody(type, compare, { notThrow: true, partial })
if (errors.length) {
throw new Error(`${opts.error || 'Request body is invalid'}: ${errors.join(', ')}`)
}

for (const key in compare) {
if (key in type === false) {
delete compare[key]
}
}
}

export function isValidPartial<T extends Validator>(
type: T,
compare: any
Expand Down
Loading

0 comments on commit 5301614

Please sign in to comment.