Files
sausage/extensions/messages/compat.ts
T

44 lines
2.4 KiB
TypeScript

/** Small world equivalents of the shared helpers used by messages and lineage. */
import type { ConversationId, EntryRecord, Tx } from '@earendil-works/pi-durable'
export { defineEvent } from '@earendil-works/sausage-events'
import { defineEvent } from '@earendil-works/sausage-events'
export const defineApi = <S>() => (name: string): {name: string; service?: S} => ({name})
export type Device = {device: string; login?: string; address?: string}
export const UserInputEvent = defineEvent('user.input')
// pi-ai 1.5.0 uses 3.5 characters per estimated text token.
export const estimateTextTokens = (text: string) => Math.ceil(text.length / 3.5)
export const onCommit = (harness: any, advance: any) => {
const observer = harness.observeCommits({advance, end: () => {}})
return () => observer.detach()
}
export function formatAgo(at: number, now = Date.now(), {ago = false} = {}): string {
const minutes = Math.floor(Math.max(0, now - at) / 60000)
const unit = (value: number, suffix: string) => `${value}${suffix}${ago ? ' ago' : ''}`
if (minutes < 1) return ago ? 'just now' : 'now'
if (minutes < 60) return unit(minutes, 'm')
if (minutes < 60 * 24) return unit(Math.floor(minutes / 60), 'h')
if (minutes < 60 * 24 * 7) return unit(Math.floor(minutes / 60 / 24), 'd')
const date = new Date(at)
return date.toLocaleDateString(undefined, date.getFullYear() === new Date(now).getFullYear()
? {month: 'short', day: 'numeric'} : {month: 'short', year: '2-digit'})
}
export const imageMimeTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']
export const maxImageBytes = 2 * 1024 * 1024, maxImages = 4
export function imagesOf(entry: EntryRecord | undefined) {
return (entry?.model ?? []).flatMap(message =>
(message.role === 'user' || message.role === 'toolResult') && typeof message.content !== 'string'
? message.content.flatMap(block => block.type === 'image' ? [{type: 'image' as const, data: block.data, mimeType: block.mimeType}] : []) : [])
}
export async function latestImagesEntry(tx: Tx, conversationId: ConversationId) {
let cursor
do {
const page = await tx.scanEntries({conversationId}, 200, cursor)
for (const entry of page.items) {
if (imagesOf(entry).length) return entry
if (entry.input || entry.kind === 'pi.user') throw new Error('images: no images in this turn')
}
cursor = page.next
} while (cursor)
throw new Error('images: no images in this turn')
}