import type { Message } from "@langchain/langgraph-sdk"; import { extractContentFromMessage, extractReasoningContentFromMessage, hasContent, hasToolCalls, stripUploadedFilesTag, } from "../messages/utils"; import type { AgentThread } from "./types"; import { titleOfThread } from "./utils"; function formatMessageContent(message: Message): string { const text = extractContentFromMessage(message); if (!text) return ""; return stripUploadedFilesTag(text); } function formatToolCalls(message: Message): string { if (message.type !== "ai" || !hasToolCalls(message)) return ""; const calls = message.tool_calls ?? []; return calls.map((call) => `- **Tool:** \`${call.name}\``).join("\n"); } export function formatThreadAsMarkdown( thread: AgentThread, messages: Message[], ): string { const title = titleOfThread(thread); const createdAt = thread.created_at ? new Date(thread.created_at).toLocaleString() : "Unknown"; const lines: string[] = [ `# ${title}`, "", `*Exported on ${new Date().toLocaleString()} · Created ${createdAt}*`, "", "---", "", ]; for (const message of messages) { if (message.type === "human") { const content = formatMessageContent(message); if (content) { lines.push(`## 🧑 User`, "", content, "", "---", ""); } } else if (message.type === "ai") { const reasoning = extractReasoningContentFromMessage(message); const content = formatMessageContent(message); const toolCalls = formatToolCalls(message); if (!content && !toolCalls && !reasoning) continue; lines.push(`## 🤖 Assistant`); if (reasoning) { lines.push( "", "
", "Thinking", "", reasoning, "", "
", ); } if (toolCalls) { lines.push("", toolCalls); } if (content && hasContent(message)) { lines.push("", content); } lines.push("", "---", ""); } } return lines.join("\n").trimEnd() + "\n"; } export function formatThreadAsJSON( thread: AgentThread, messages: Message[], ): string { const exportData = { title: titleOfThread(thread), thread_id: thread.thread_id, created_at: thread.created_at, exported_at: new Date().toISOString(), messages: messages.map((msg) => ({ type: msg.type, id: msg.id, content: typeof msg.content === "string" ? msg.content : msg.content, ...(msg.type === "ai" && msg.tool_calls?.length ? { tool_calls: msg.tool_calls } : {}), })), }; return JSON.stringify(exportData, null, 2); } function sanitizeFilename(name: string): string { return name.replace(/[^\p{L}\p{N}_\- ]/gu, "").trim() || "conversation"; } export function downloadAsFile( content: string, filename: string, mimeType: string, ) { const blob = new Blob([content], { type: mimeType }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } export function exportThreadAsMarkdown( thread: AgentThread, messages: Message[], ) { const markdown = formatThreadAsMarkdown(thread, messages); const filename = `${sanitizeFilename(titleOfThread(thread))}.md`; downloadAsFile(markdown, filename, "text/markdown;charset=utf-8"); } export function exportThreadAsJSON(thread: AgentThread, messages: Message[]) { const json = formatThreadAsJSON(thread, messages); const filename = `${sanitizeFilename(titleOfThread(thread))}.json`; downloadAsFile(json, filename, "application/json;charset=utf-8"); }