Fix web-ui and example for new agent API

- AgentInterface composes UserMessageWithAttachments for attachments
- Updated example to use convertToLlm instead of transport/messageTransformer
- Fixed declaration merging: target pi-agent-core, add path to example tsconfig
- Fixed typo in CustomAgentMessages key (user-with-attachment -> user-with-attachments)
- customMessageTransformer properly converts UserMessageWithAttachments to content blocks
This commit is contained in:
Mario Zechner 2025-12-28 11:01:12 +01:00
parent 6ddc7418da
commit 7a39f9eb11
6 changed files with 70 additions and 24 deletions

View file

@ -1,6 +1,6 @@
import { Alert } from "@mariozechner/mini-lit/dist/Alert.js"; import { Alert } from "@mariozechner/mini-lit/dist/Alert.js";
import type { Message } from "@mariozechner/pi-ai"; import type { ImageContent, Message, TextContent } from "@mariozechner/pi-ai";
import type { AppMessage, MessageRenderer } from "@mariozechner/pi-web-ui"; import type { AgentMessage, Attachment, MessageRenderer, UserMessageWithAttachments } from "@mariozechner/pi-web-ui";
import { registerMessageRenderer } from "@mariozechner/pi-web-ui"; import { registerMessageRenderer } from "@mariozechner/pi-web-ui";
import { html } from "lit"; import { html } from "lit";
@ -16,8 +16,9 @@ export interface SystemNotificationMessage {
timestamp: string; timestamp: string;
} }
// Extend CustomMessages interface via declaration merging // Extend CustomAgentMessages interface via declaration merging
declare module "@mariozechner/pi-web-ui" { // This must target pi-agent-core where CustomAgentMessages is defined
declare module "@mariozechner/pi-agent-core" {
interface CustomAgentMessages { interface CustomAgentMessages {
"system-notification": SystemNotificationMessage; "system-notification": SystemNotificationMessage;
} }
@ -74,8 +75,28 @@ export function createSystemNotification(
// 5. CUSTOM MESSAGE TRANSFORMER // 5. CUSTOM MESSAGE TRANSFORMER
// ============================================================================ // ============================================================================
// Transform custom messages to user messages with <system> tags so LLM can see them // Convert attachments to content blocks
export function customMessageTransformer(messages: AppMessage[]): Message[] { 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
export function customMessageTransformer(messages: AgentMessage[]): Message[] {
return messages return messages
.filter((m) => { .filter((m) => {
// Filter out artifact messages - they're for session reconstruction only // Filter out artifact messages - they're for session reconstruction only
@ -85,7 +106,11 @@ export function customMessageTransformer(messages: AppMessage[]): Message[] {
// Keep LLM-compatible messages + custom messages // Keep LLM-compatible messages + custom messages
return ( return (
m.role === "user" || m.role === "assistant" || m.role === "toolResult" || m.role === "system-notification" m.role === "user" ||
m.role === "user-with-attachments" ||
m.role === "assistant" ||
m.role === "toolResult" ||
m.role === "system-notification"
); );
}) })
.map((m) => { .map((m) => {
@ -95,13 +120,25 @@ export function customMessageTransformer(messages: AppMessage[]): Message[] {
return { return {
role: "user", role: "user",
content: `<system>${notification.message}</system>`, content: `<system>${notification.message}</system>`,
timestamp: Date.now(),
} as Message; } as Message;
} }
// Strip attachments from user messages // Convert user-with-attachments to user message with content blocks
if (m.role === "user-with-attachment") { if (m.role === "user-with-attachments") {
const { attachments: _, ...rest } = m; const msg = m as UserMessageWithAttachments;
return rest as Message; 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; return m as Message;

View file

@ -1,10 +1,9 @@
import "@mariozechner/mini-lit/dist/ThemeToggle.js"; import "@mariozechner/mini-lit/dist/ThemeToggle.js";
import { Agent, type AgentMessage } from "@mariozechner/pi-agent-core";
import { getModel } from "@mariozechner/pi-ai"; import { getModel } from "@mariozechner/pi-ai";
import { import {
Agent,
type AgentState, type AgentState,
ApiKeyPromptDialog, ApiKeyPromptDialog,
type AppMessage,
AppStorage, AppStorage,
ChatPanel, ChatPanel,
CustomProvidersStore, CustomProvidersStore,
@ -13,7 +12,6 @@ import {
// PersistentStorageDialog, // TODO: Fix - currently broken // PersistentStorageDialog, // TODO: Fix - currently broken
ProviderKeysStore, ProviderKeysStore,
ProvidersModelsTab, ProvidersModelsTab,
ProviderTransport,
ProxyTab, ProxyTab,
SessionListDialog, SessionListDialog,
SessionsStore, SessionsStore,
@ -75,7 +73,7 @@ let agent: Agent;
let chatPanel: ChatPanel; let chatPanel: ChatPanel;
let agentUnsubscribe: (() => void) | undefined; let agentUnsubscribe: (() => void) | undefined;
const generateTitle = (messages: AppMessage[]): string => { const generateTitle = (messages: AgentMessage[]): string => {
const firstUserMsg = messages.find((m) => m.role === "user"); const firstUserMsg = messages.find((m) => m.role === "user");
if (!firstUserMsg || firstUserMsg.role !== "user") return ""; if (!firstUserMsg || firstUserMsg.role !== "user") return "";
@ -99,7 +97,7 @@ const generateTitle = (messages: AppMessage[]): string => {
return text.length <= 50 ? text : `${text.substring(0, 47)}...`; return text.length <= 50 ? text : `${text.substring(0, 47)}...`;
}; };
const shouldSaveSession = (messages: AppMessage[]): boolean => { const shouldSaveSession = (messages: AgentMessage[]): boolean => {
const hasUserMsg = messages.some((m: any) => m.role === "user"); const hasUserMsg = messages.some((m: any) => m.role === "user");
const hasAssistantMsg = messages.some((m: any) => m.role === "assistant"); const hasAssistantMsg = messages.some((m: any) => m.role === "assistant");
return hasUserMsg && hasAssistantMsg; return hasUserMsg && hasAssistantMsg;
@ -166,8 +164,6 @@ const createAgent = async (initialState?: Partial<AgentState>) => {
agentUnsubscribe(); agentUnsubscribe();
} }
const transport = new ProviderTransport();
agent = new Agent({ agent = new Agent({
initialState: initialState || { initialState: initialState || {
systemPrompt: `You are a helpful AI assistant with access to various tools. systemPrompt: `You are a helpful AI assistant with access to various tools.
@ -182,9 +178,8 @@ Feel free to use these tools when needed to provide accurate and helpful respons
messages: [], messages: [],
tools: [], tools: [],
}, },
transport, // Custom transformer: convert custom messages to LLM-compatible format
// Custom transformer: convert system notifications to user messages with <system> tags convertToLlm: customMessageTransformer,
messageTransformer: customMessageTransformer,
}); });
agentUnsubscribe = agent.subscribe((event: any) => { agentUnsubscribe = agent.subscribe((event: any) => {

View file

@ -6,6 +6,7 @@
"moduleResolution": "bundler", "moduleResolution": "bundler",
"paths": { "paths": {
"*": ["./*"], "*": ["./*"],
"@mariozechner/pi-agent-core": ["../../agent/dist/index.d.ts"],
"@mariozechner/pi-ai": ["../../ai/dist/index.d.ts"], "@mariozechner/pi-ai": ["../../ai/dist/index.d.ts"],
"@mariozechner/pi-tui": ["../../tui/dist/index.d.ts"], "@mariozechner/pi-tui": ["../../tui/dist/index.d.ts"],
"@mariozechner/pi-web-ui": ["../dist/index.d.ts"] "@mariozechner/pi-web-ui": ["../dist/index.d.ts"]

View file

@ -12,6 +12,7 @@ import type { Agent, AgentEvent } from "@mariozechner/pi-agent-core";
import type { Attachment } from "../utils/attachment-utils.js"; import type { Attachment } from "../utils/attachment-utils.js";
import { formatUsage } from "../utils/format.js"; import { formatUsage } from "../utils/format.js";
import { i18n } from "../utils/i18n.js"; import { i18n } from "../utils/i18n.js";
import type { UserMessageWithAttachments } from "./Messages.js";
import type { StreamingMessageContainer } from "./StreamingMessageContainer.js"; import type { StreamingMessageContainer } from "./StreamingMessageContainer.js";
@customElement("agent-interface") @customElement("agent-interface")
@ -202,7 +203,18 @@ export class AgentInterface extends LitElement {
this._messageEditor.attachments = []; this._messageEditor.attachments = [];
this._autoScroll = true; // Enable auto-scroll when sending a message this._autoScroll = true; // Enable auto-scroll when sending a message
await this.session?.prompt(input, attachments); // Compose message with attachments if any
if (attachments && attachments.length > 0) {
const message: UserMessageWithAttachments = {
role: "user-with-attachments",
content: input,
attachments,
timestamp: Date.now(),
};
await this.session?.prompt(message);
} else {
await this.session?.prompt(input);
}
} }
private renderMessages() { private renderMessages() {

View file

@ -9,6 +9,7 @@ import { Brain, Loader2, Paperclip, Send, Sparkles, Square } from "lucide";
import { type Attachment, loadAttachment } from "../utils/attachment-utils.js"; import { type Attachment, loadAttachment } from "../utils/attachment-utils.js";
import { i18n } from "../utils/i18n.js"; import { i18n } from "../utils/i18n.js";
import "./AttachmentTile.js"; import "./AttachmentTile.js";
import type { ThinkingLevel } from "@mariozechner/pi-agent-core";
@customElement("message-editor") @customElement("message-editor")
export class MessageEditor extends LitElement { export class MessageEditor extends LitElement {
@ -28,7 +29,7 @@ export class MessageEditor extends LitElement {
@property() isStreaming = false; @property() isStreaming = false;
@property() currentModel?: Model<any>; @property() currentModel?: Model<any>;
@property() thinkingLevel: "off" | "minimal" | "low" | "medium" | "high" = "off"; @property() thinkingLevel: ThinkingLevel = "off";
@property() showAttachmentButton = true; @property() showAttachmentButton = true;
@property() showModelSelector = true; @property() showModelSelector = true;
@property() showThinkingSelector = true; @property() showThinkingSelector = true;

View file

@ -34,7 +34,7 @@ export interface ArtifactMessage {
declare module "@mariozechner/pi-agent-core" { declare module "@mariozechner/pi-agent-core" {
interface CustomAgentMessages { interface CustomAgentMessages {
"user-with-attachment": UserMessageWithAttachments; "user-with-attachments": UserMessageWithAttachments;
artifact: ArtifactMessage; artifact: ArtifactMessage;
} }
} }