2026-10-24 20:35:20 +04:00
|
|
|
import type { ImageContent, Message, TextContent, Tool } from "@earendil-works/pi-ai";
|
|
|
|
|
import { getCurrentSystemPrompt, getCurrentTools } from "@earendil-works/pi-ai/utils/transcript";
|
|
|
|
|
|
2026-10-25 02:04:22 +04:00
|
|
|
export type ClaudeContentBlock =
|
|
|
|
|
| { type: "text"; text: string }
|
|
|
|
|
| { type: "image"; source: { type: "base64"; media_type: string; data: string } };
|
2026-10-24 20:35:20 +04:00
|
|
|
|
|
|
|
|
export type ClaudeInput = {
|
|
|
|
|
systemPrompt: string;
|
|
|
|
|
tools: Tool[];
|
|
|
|
|
userContent: ClaudeContentBlock[];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type ReplayPart = string | ImageContent;
|
|
|
|
|
|
|
|
|
|
function toImageBlock(image: ImageContent): ClaudeContentBlock {
|
|
|
|
|
return { type: "image", source: { type: "base64", media_type: image.mimeType, data: image.data } };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getLabelledParts(label: string, content: string | (TextContent | ImageContent)[]): ReplayPart[] {
|
|
|
|
|
if (typeof content === "string") return [`${label}\n${content}`];
|
2026-10-25 02:04:22 +04:00
|
|
|
|
2026-10-24 20:35:20 +04:00
|
|
|
const text = content.flatMap((block) => (block.type === "text" ? [block.text] : [])).join("\n");
|
|
|
|
|
const images = content.filter((block) => block.type === "image");
|
2026-10-25 02:04:22 +04:00
|
|
|
|
2026-10-24 20:35:20 +04:00
|
|
|
return [text ? `${label}\n${text}` : label, ...images];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getReplayParts(message: Message): ReplayPart[] {
|
|
|
|
|
switch (message.role) {
|
|
|
|
|
case "system":
|
|
|
|
|
return [];
|
2026-10-25 02:04:22 +04:00
|
|
|
|
2026-10-24 20:35:20 +04:00
|
|
|
case "user":
|
|
|
|
|
return getLabelledParts("[user]", message.content);
|
2026-10-25 02:04:22 +04:00
|
|
|
|
2026-10-24 20:35:20 +04:00
|
|
|
case "assistant":
|
|
|
|
|
return message.content.flatMap((block) => {
|
|
|
|
|
if (block.type === "text") return [`[assistant]\n${block.text}`];
|
|
|
|
|
if (block.type === "toolCall") return [`[tool call ${block.name}] ${JSON.stringify(block.arguments)}`];
|
|
|
|
|
return [];
|
|
|
|
|
});
|
2026-10-25 02:04:22 +04:00
|
|
|
|
|
|
|
|
case "toolResult": {
|
|
|
|
|
const label = message.isError ? `[tool result ${message.toolName} error]` : `[tool result ${message.toolName}]`;
|
|
|
|
|
return getLabelledParts(label, message.content);
|
|
|
|
|
}
|
2026-10-24 20:35:20 +04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toContentBlocks(parts: ReplayPart[]): ClaudeContentBlock[] {
|
|
|
|
|
const blocks: ClaudeContentBlock[] = [];
|
|
|
|
|
let pendingText: string[] = [];
|
2026-10-25 02:04:22 +04:00
|
|
|
|
2026-10-24 20:35:20 +04:00
|
|
|
const flushText = () => {
|
|
|
|
|
if (pendingText.length > 0) blocks.push({ type: "text", text: pendingText.join("\n\n") });
|
|
|
|
|
pendingText = [];
|
|
|
|
|
};
|
2026-10-25 02:04:22 +04:00
|
|
|
|
2026-10-24 20:35:20 +04:00
|
|
|
for (const part of parts) {
|
|
|
|
|
if (typeof part === "string") {
|
|
|
|
|
pendingText.push(part);
|
|
|
|
|
} else {
|
|
|
|
|
flushText();
|
|
|
|
|
blocks.push(toImageBlock(part));
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-25 02:04:22 +04:00
|
|
|
|
2026-10-24 20:35:20 +04:00
|
|
|
flushText();
|
2026-10-25 02:04:22 +04:00
|
|
|
|
2026-10-24 20:35:20 +04:00
|
|
|
return blocks;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toUnlabelledBlocks(content: string | (TextContent | ImageContent)[]): ClaudeContentBlock[] {
|
|
|
|
|
if (typeof content === "string") return [{ type: "text", text: content }];
|
2026-10-25 02:04:22 +04:00
|
|
|
|
2026-10-24 20:35:20 +04:00
|
|
|
return content.map((block) => (block.type === "text" ? { type: "text", text: block.text } : toImageBlock(block)));
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-25 02:04:22 +04:00
|
|
|
function toUserContent(conversation: Message[]): ClaudeContentBlock[] {
|
|
|
|
|
const [first] = conversation;
|
|
|
|
|
if (conversation.length === 1 && first?.role === "user") return toUnlabelledBlocks(first.content);
|
|
|
|
|
|
|
|
|
|
return toContentBlocks(conversation.flatMap(getReplayParts));
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 20:35:20 +04:00
|
|
|
export function toClaudeInput(messages: Message[]): ClaudeInput {
|
|
|
|
|
const conversation = messages.filter((message) => message.role !== "system");
|
2026-10-25 02:04:22 +04:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
systemPrompt: getCurrentSystemPrompt(messages),
|
|
|
|
|
tools: getCurrentTools(messages),
|
|
|
|
|
userContent: toUserContent(conversation),
|
|
|
|
|
};
|
2026-10-24 20:35:20 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function toStreamJsonLine(content: ClaudeContentBlock[]): string {
|
|
|
|
|
return JSON.stringify({ type: "user", message: { role: "user", content } });
|
|
|
|
|
}
|