Files
sausage/extensions/messages/images.ts
T

50 lines
2.2 KiB
TypeScript

/** Resolve immutable image references within the sender's visible history. */
import { int, lit, union } from '@earendil-works/pi-schema'
import type { ConversationId, EntryId, EntryRecord, Tx } from '@earendil-works/pi-durable'
import { imagesOf, latestImagesEntry } from './compat.ts'
import { imageMimeTypes, maxImageBytes, maxImages } from './compat.ts'
/**
* Which images to forward: `latest`, the newest entry with images in the sender's current turn (its input or a tool
* result such as a screenshot since), or an entry's id.
*/
export type ImagesRef = 'latest' | number
export const SCAN_PAGE = 200
/** Resolve `ref` against conversation `from`'s visible history to the entry holding the images, checking the limits. */
export async function resolveImages(tx: Tx, from: ConversationId, ref: ImagesRef) {
let entry: EntryRecord | undefined
if (ref === 'latest') entry = await latestImagesEntry(tx, from)
else {
if (!Number.isInteger(ref) || ref < 1)
throw new Error('images must be "latest" or the id of an entry with images')
entry = (
await tx.scanEntries(
{
conversationId: from,
minEntryId: ref as EntryId,
maxEntryId: ref as EntryId,
},
1,
)
).items[0]
if (!entry) throw new Error(`images: entry ${ref} is not in this conversation`)
}
const images = imagesOf(entry)
if (!images.length) throw new Error(`images: entry ${entry.id} has no images`)
if (images.length > maxImages)
throw new Error(
`images: entry ${entry.id} has ${images.length} images; at most ${maxImages} go along`,
)
for (const image of images)
if (
!imageMimeTypes.includes(image.mimeType) ||
(image.data.length * 3 / 4 - (image.data.endsWith('==') ? 2 : image.data.endsWith('=') ? 1 : 0)) > maxImageBytes
)
throw new Error(`images: entry ${entry.id} has an unsupported image or one over 2 MiB`)
return { entry: entry.id, count: images.length }
}
export const imagesParameter = union([lit('latest'), int({ minimum: 1 })], {
description:
'Forward images from your conversation: "latest" for those of this turn (your current input, or a tool result such as a screenshot since; it fails when there are none), or an entry id for older ones.',
})