UNPKG

@llumiverse/core

Version:

Provide an universal API to LLMs. Support for existing LLMs can be added by writing a driver.

142 lines (123 loc) 4.55 kB
import { JSONSchema } from "@llumiverse/common"; import { PromptRole, PromptSegment } from "@llumiverse/common"; import { readStreamAsBase64 } from "../stream.js"; import { getJSONSafetyNotice } from "./commons.js"; export interface NovaMessage { role: 'user' | 'assistant', content: NovaMessagePart[] } export interface NovaSystemMessage { text: string } interface NovaMessagePart { text?: string // only set for text messages image?: { format: "jpeg" | "png" | "gif" | "webp", source: { bytes: string //"base64", } } video?: { format: "mkv" | "mov" | "mp4" | "webm" | "three_gp" | "flv" | "mpeg" | "mpg" | "wmv", source: { //Option 1: sending a s3 location s3Location?: { uri: string, // example: s3://my-bucket/object-key bucketOwner: string // optional. example: "123456789012" } //Option 2: sending a base64 encoded video bytes?: string //"base64", } } } export interface NovaMessagesPrompt { system?: NovaSystemMessage[]; messages: NovaMessage[]; negative?: string; mask?: string; } /** * A formatter used by Bedrock to format prompts for nova related models */ export async function formatNovaPrompt(segments: PromptSegment[], schema?: JSONSchema): Promise<NovaMessagesPrompt> { const system: string[] = []; const safety: string[] = []; const messages: NovaMessage[] = []; let negative: string = ""; let mask: string = ""; for (const segment of segments) { const parts: NovaMessagePart[] = []; if (segment.files) for (const f of segment.files) { //TODO add video support if (!f.mime_type?.startsWith('image')) { continue; } const source = await f.getStream(); const data = await readStreamAsBase64(source); const format = f.mime_type?.split('/')[1] || 'png'; parts.push({ image: { format: format as "jpeg" | "png" | "gif" | "webp", source: { bytes: data } } }) } if (segment.content) { parts.push({ text: segment.content }) } if (segment.role === PromptRole.system) { system.push(segment.content); } else if (segment.role === PromptRole.safety) { safety.push(segment.content); } else if (messages.length > 0 && messages[messages.length - 1].role === segment.role) { //Maybe can remove for nova? //concatenate messages of the same role (Claude requires alternative user and assistant roles) messages[messages.length - 1].content.push(...parts); } else if (segment.role === PromptRole.negative) { negative = negative.concat(segment.content, ', '); } else if (segment.role === PromptRole.mask) { mask = mask.concat(segment.content, ' '); } else if (segment.role !== PromptRole.tool) { messages.push({ role: segment.role, content: parts }); } } if (schema) { safety.push("IMPORTANT: " + getJSONSafetyNotice(schema)); } // messages must contains at least 1 item. If the prompt does not contains a user message (but only system messages) // we need to put the system messages in the messages array let systemMessage = system.join('\n').trim(); if (messages.length === 0) { if (!systemMessage) { throw new Error('Prompt must contain at least one message'); } messages.push({ content: [{ text: systemMessage }], role: 'user' }); systemMessage = safety.join('\n'); } else if (safety.length > 0) { systemMessage = systemMessage + '\n\nIMPORTANT: ' + safety.join('\n'); } /*start Nova's message to make sure it answers properly in JSON if enabled, this requires to add the { to Nova's response*/ if (schema) { messages.push({ role: "assistant", content: [{ text: "{" }] }); } // put system messages first and safety last return { system: systemMessage ? [{ text: systemMessage }] : [{ text: "" }], messages: messages, negative: negative || undefined, mask: mask || undefined, } }