From 13a1991ec205c9d0745ebd02c5d7868291e44092 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sun, 28 Dec 2025 11:06:26 +0100 Subject: [PATCH] Add defaultConvertToLlm to web-ui, simplify example - web-ui exports: defaultConvertToLlm, convertAttachments, isUserMessageWithAttachments, isArtifactMessage - defaultConvertToLlm handles UserMessageWithAttachments and filters ArtifactMessage - Example's customMessageTransformer now extends defaultConvertToLlm - Removes duplicated attachment conversion logic from example --- .../web-ui/example/src/custom-messages.ts | 91 +++++-------------- packages/web-ui/example/src/main.ts | 6 +- packages/web-ui/src/components/MessageList.ts | 2 +- packages/web-ui/src/components/Messages.ts | 90 ++++++++++++++++++ .../components/StreamingMessageContainer.ts | 2 +- packages/web-ui/src/index.ts | 12 ++- 6 files changed, 127 insertions(+), 76 deletions(-) diff --git a/packages/web-ui/example/src/custom-messages.ts b/packages/web-ui/example/src/custom-messages.ts index 31c5d3fd..ef9544fe 100644 --- a/packages/web-ui/example/src/custom-messages.ts +++ b/packages/web-ui/example/src/custom-messages.ts @@ -1,7 +1,7 @@ import { Alert } from "@mariozechner/mini-lit/dist/Alert.js"; -import type { ImageContent, Message, TextContent } from "@mariozechner/pi-ai"; -import type { AgentMessage, Attachment, MessageRenderer, UserMessageWithAttachments } from "@mariozechner/pi-web-ui"; -import { registerMessageRenderer } from "@mariozechner/pi-web-ui"; +import type { Message } from "@mariozechner/pi-ai"; +import type { AgentMessage, MessageRenderer } from "@mariozechner/pi-web-ui"; +import { defaultConvertToLlm, registerMessageRenderer } from "@mariozechner/pi-web-ui"; import { html } from "lit"; // ============================================================================ @@ -75,72 +75,25 @@ export function createSystemNotification( // 5. CUSTOM MESSAGE TRANSFORMER // ============================================================================ -// Convert attachments to content blocks -function convertAttachments(attachments: Attachment[]): (TextContent | ImageContent)[] { - const content: (TextContent | ImageContent)[] = []; - for (const attachment of attachments) { - if (attachment.type === "image") { - content.push({ - type: "image", - data: attachment.content, - mimeType: attachment.mimeType, - } as ImageContent); - } else if (attachment.type === "document" && attachment.extractedText) { - content.push({ - type: "text", - text: `\n\n[Document: ${attachment.fileName}]\n${attachment.extractedText}`, - } as TextContent); - } - } - return content; -} - -// Transform custom messages to LLM-compatible messages +/** + * Custom message transformer that extends defaultConvertToLlm. + * Handles system-notification messages by converting them to user messages. + */ export function customMessageTransformer(messages: AgentMessage[]): Message[] { - return messages - .filter((m) => { - // Filter out artifact messages - they're for session reconstruction only - if (m.role === "artifact") { - return false; - } + // First, handle our custom system-notification type + const processed = messages.map((m): AgentMessage => { + if (m.role === "system-notification") { + const notification = m as SystemNotificationMessage; + // Convert to user message with tags + return { + role: "user", + content: `${notification.message}`, + timestamp: Date.now(), + }; + } + return m; + }); - // Keep LLM-compatible messages + custom messages - return ( - m.role === "user" || - m.role === "user-with-attachments" || - m.role === "assistant" || - m.role === "toolResult" || - m.role === "system-notification" - ); - }) - .map((m) => { - // Transform system notifications to user messages - if (m.role === "system-notification") { - const notification = m as SystemNotificationMessage; - return { - role: "user", - content: `${notification.message}`, - timestamp: Date.now(), - } as Message; - } - - // Convert user-with-attachments to user message with content blocks - if (m.role === "user-with-attachments") { - const msg = m as UserMessageWithAttachments; - const textContent: (TextContent | ImageContent)[] = - typeof msg.content === "string" ? [{ type: "text", text: msg.content }] : [...msg.content]; - - if (msg.attachments) { - textContent.push(...convertAttachments(msg.attachments)); - } - - return { - role: "user", - content: textContent, - timestamp: msg.timestamp, - } as Message; - } - - return m as Message; - }); + // Then use defaultConvertToLlm for standard handling + return defaultConvertToLlm(processed); } diff --git a/packages/web-ui/example/src/main.ts b/packages/web-ui/example/src/main.ts index 376d6b36..c233517e 100644 --- a/packages/web-ui/example/src/main.ts +++ b/packages/web-ui/example/src/main.ts @@ -74,8 +74,8 @@ let chatPanel: ChatPanel; let agentUnsubscribe: (() => void) | undefined; const generateTitle = (messages: AgentMessage[]): string => { - const firstUserMsg = messages.find((m) => m.role === "user"); - if (!firstUserMsg || firstUserMsg.role !== "user") return ""; + const firstUserMsg = messages.find((m) => m.role === "user" || m.role === "user-with-attachments"); + if (!firstUserMsg || (firstUserMsg.role !== "user" && firstUserMsg.role !== "user-with-attachments")) return ""; let text = ""; const content = firstUserMsg.content; @@ -98,7 +98,7 @@ const generateTitle = (messages: AgentMessage[]): string => { }; const shouldSaveSession = (messages: AgentMessage[]): boolean => { - const hasUserMsg = messages.some((m: any) => m.role === "user"); + const hasUserMsg = messages.some((m: any) => m.role === "user" || m.role === "user-with-attachments"); const hasAssistantMsg = messages.some((m: any) => m.role === "assistant"); return hasUserMsg && hasAssistantMsg; }; diff --git a/packages/web-ui/src/components/MessageList.ts b/packages/web-ui/src/components/MessageList.ts index 2586bf58..97670ba3 100644 --- a/packages/web-ui/src/components/MessageList.ts +++ b/packages/web-ui/src/components/MessageList.ts @@ -50,7 +50,7 @@ export class MessageList extends LitElement { } // Fall back to built-in renderers - if (msg.role === "user") { + if (msg.role === "user" || msg.role === "user-with-attachments") { items.push({ key: `msg:${index}`, template: html``, diff --git a/packages/web-ui/src/components/Messages.ts b/packages/web-ui/src/components/Messages.ts index 35637a30..7d68ff4a 100644 --- a/packages/web-ui/src/components/Messages.ts +++ b/packages/web-ui/src/components/Messages.ts @@ -285,3 +285,93 @@ export class AbortedMessage extends LitElement { return html`${i18n("Request aborted")}`; } } + +// ============================================================================ +// Default Message Transformer +// ============================================================================ + +import type { AgentMessage } from "@mariozechner/pi-agent-core"; +import type { Message } from "@mariozechner/pi-ai"; + +/** + * Convert attachments to content blocks for LLM. + * - Images become ImageContent blocks + * - Documents with extractedText become TextContent blocks with filename header + */ +export function convertAttachments(attachments: Attachment[]): (TextContent | ImageContent)[] { + const content: (TextContent | ImageContent)[] = []; + for (const attachment of attachments) { + if (attachment.type === "image") { + content.push({ + type: "image", + data: attachment.content, + mimeType: attachment.mimeType, + } as ImageContent); + } else if (attachment.type === "document" && attachment.extractedText) { + content.push({ + type: "text", + text: `\n\n[Document: ${attachment.fileName}]\n${attachment.extractedText}`, + } as TextContent); + } + } + return content; +} + +/** + * Check if a message is a UserMessageWithAttachments. + */ +export function isUserMessageWithAttachments(msg: AgentMessage): msg is UserMessageWithAttachments { + return (msg as UserMessageWithAttachments).role === "user-with-attachments"; +} + +/** + * Check if a message is an ArtifactMessage. + */ +export function isArtifactMessage(msg: AgentMessage): msg is ArtifactMessage { + return (msg as ArtifactMessage).role === "artifact"; +} + +/** + * Default convertToLlm for web-ui apps. + * + * Handles: + * - UserMessageWithAttachments: converts to user message with content blocks + * - ArtifactMessage: filtered out (UI-only, for session reconstruction) + * - Standard LLM messages (user, assistant, toolResult): passed through + */ +export function defaultConvertToLlm(messages: AgentMessage[]): Message[] { + return messages + .filter((m) => { + // Filter out artifact messages - they're for session reconstruction only + if (isArtifactMessage(m)) { + return false; + } + return true; + }) + .map((m): Message | null => { + // Convert user-with-attachments to user message with content blocks + if (isUserMessageWithAttachments(m)) { + const textContent: (TextContent | ImageContent)[] = + typeof m.content === "string" ? [{ type: "text", text: m.content }] : [...m.content]; + + if (m.attachments) { + textContent.push(...convertAttachments(m.attachments)); + } + + return { + role: "user", + content: textContent, + timestamp: m.timestamp, + } as Message; + } + + // Pass through standard LLM roles + if (m.role === "user" || m.role === "assistant" || m.role === "toolResult") { + return m as Message; + } + + // Filter out unknown message types + return null; + }) + .filter((m): m is Message => m !== null); +} diff --git a/packages/web-ui/src/components/StreamingMessageContainer.ts b/packages/web-ui/src/components/StreamingMessageContainer.ts index d3703dea..3d269315 100644 --- a/packages/web-ui/src/components/StreamingMessageContainer.ts +++ b/packages/web-ui/src/components/StreamingMessageContainer.ts @@ -74,7 +74,7 @@ export class StreamingMessageContainer extends LitElement { if (msg.role === "toolResult") { // Skip standalone tool result in streaming; the stable list will render paired tool-message return html``; - } else if (msg.role === "user") { + } else if (msg.role === "user" || msg.role === "user-with-attachments") { // Skip standalone tool result in streaming; the stable list will render it immediiately return html``; } else if (msg.role === "assistant") { diff --git a/packages/web-ui/src/index.ts b/packages/web-ui/src/index.ts index e6c9e808..6c9fd4d5 100644 --- a/packages/web-ui/src/index.ts +++ b/packages/web-ui/src/index.ts @@ -12,8 +12,16 @@ export { Input } from "./components/Input.js"; export { MessageEditor } from "./components/MessageEditor.js"; export { MessageList } from "./components/MessageList.js"; // Message components -export type { UserMessageWithAttachments } from "./components/Messages.js"; -export { AssistantMessage, ToolMessage, UserMessage } from "./components/Messages.js"; +export type { ArtifactMessage, UserMessageWithAttachments } from "./components/Messages.js"; +export { + AssistantMessage, + convertAttachments, + defaultConvertToLlm, + isArtifactMessage, + isUserMessageWithAttachments, + ToolMessage, + UserMessage, +} from "./components/Messages.js"; // Message renderer registry export { getMessageRenderer,