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
This commit is contained in:
Mario Zechner 2025-12-28 11:06:26 +01:00
parent 7a39f9eb11
commit 13a1991ec2
6 changed files with 127 additions and 76 deletions

View file

@ -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`<user-message .message=${msg}></user-message>`,

View file

@ -285,3 +285,93 @@ export class AbortedMessage extends LitElement {
return html`<span class="text-sm text-destructive italic">${i18n("Request aborted")}</span>`;
}
}
// ============================================================================
// 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);
}

View file

@ -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") {