CopilotKit 多模态示例文件(demo-files)详解:从示例注入到 AG-UI 附件管线的完整实现

📅 发布时间:2026/9/13 7:50:00
CopilotKit 多模态示例文件(demo-files)详解:从示例注入到 AG-UI 附件管线的完整实现
CopilotKit 多模态示例文件demo-files详解从示例注入到 AG-UI 附件管线的完整实现【免费下载链接】CopilotKitThe Frontend Stack for Agents Generative UI. React, Angular, Mobile, Slack, and more. Makers of the AG-UI Protocol项目地址: https://gitcode.com/GitHub_Trending/co/CopilotKit导读showcase/integrations/crewai-conversational-flows是 CopilotKit 仓库中基于 CrewAI Conversational Flows 构建的多模态对话演示其/demos/multimodal页面支持图片与 PDF 上传并额外提供Try with sample image / PDF两个一键示例按钮。本文以 public/demo-files/README.md 为主体深入剖析这两个示例二进制文件sample.png、sample.pdf的约定、前端注入管线、CrewAI 视觉模型的流式处理以及 E2E 测试如何利用它们做软断言。读完你将掌握示例文件应满足何种规范、它们如何与真实上传共用同一套附件管线、以及遇到 Git LFS 指针或文件缺失时如何定位与修复。demo-files 目录的角色定位该目录位于 public/demo-files/目录内包含 README.md、sample.png、sample.pdf三个文件被/demos/multimodal页面引用。因为文件放在 Next.js 的public/目录下它们在构建后会被直接以静态资源方式托管前端可以通过公开路径/demo-files/sample.png、/demo-files/sample.pdf以 HTTP 方式获取。README 明确了两个核心约定文件格式要求内容要求用途sample.png小于 50 KB 的 PNG建议是 CopilotKit Logo 或其他可辨识的品牌标识让具备视觉能力的 Agent 有内容可描述Try with sample image 按钮注入sample.pdf小于 50 KB 的单页 PDF内容必须提及 CopilotKit以便 E2E 软断言成立例如 CopilotKit 快速上手的单页导出Try with sample PDF 按钮注入两个文件大小被严格限制在 50 KB 以内是为了让演示保持轻量、base64 内联传输时不会撑爆消息体积前端 multimodal-chat.tsx 中MAX_FILE_SIZE_BYTES为 10 MB示例文件远小于该上限。PDF 内容要求包含 CopilotKit 一词是因为 multimodal.spec.ts 中的断言await expect(asstMsg).toContainText(/copilotkit/i)依赖模型读出的文本与关键词匹配。README 同时强调这些文件必须以二进制形式提交不得被当作文本入库这对应仓库根目录 .gitattributes 中的规则——*.png、*.pdf均被声明为filterlfs difflfs mergelfs -text即走 Git LFS 跟踪。示例按钮与真实上传共用同一条附件管线README 的核心设计思想是示例按钮注入的文件与回形针paperclip真实上传走的是完全相同的排队代码路径。其链路如下页面组件 page.tsx 通过公开路径/demo-files/sample.png、/demo-files/sample.pdf拉取文件将拉取到的二进制 blob 包装成File送入与回形针按钮相同的AttachmentsConfig.onUpload回调从而复用同一套附件排队queueing逻辑。这样设计的好处是示例路径与真实上传路径共享同一段代码只要一条路径工作正常另一条也必然正常不会出现演示按钮能跑、真实上传坏了或反之的偏差。不过仓库源码显示该设计经历了两次迭代。在 sample-attachment-buttons.tsx 的头部注释中记录了一个重要的实现变更早期版本通过DataTransfer构造合成change事件驱动聊天组件隐藏的input typefile让示例路径走onUpload/useAttachments管线。问题在于注入附件后自动发送预设 prompt 时附件仍处于status: uploading状态而CopilotChat.onSubmitInput会以Cannot send while attachments are uploading拒绝发送并清空输入外部检测上传完成又需要抓取 spinner 遮罩在慢渲染下极易产生竞态。最终版本完全绕开 DOM直接走 V2 Agent 表面——先用agent.addMessage(...)入队一条文本 附件 content parts的完整用户消息附件以已完成的 base64 content part形式构造再调用copilotkit.runAgent({ agent })派发。这与CopilotChat.onSubmitInput内部行为一致相同的数据形状、相同的运行时但因为没有上传中状态天然不存在竞态。// sample-attachment-buttons.tsx 中的核心发送逻辑节选 agent.addMessage({ id: generateMessageId(), role: user, content: [ { type: text, text: spec.autoPrompt }, { type: partType, // PDF → document图片 → image source: { type: data, value: sample.base64, mimeType: spec.mimeType, }, metadata: { filename: spec.filename, size: sample.size }, }, ], } as Parameterstypeof agent.addMessage[0]); await copilotkit.runAgent({ agent });其中spec由两个示例条目构成每个条目定义了按钮文案、文件名、MIME 类型、测试标识与自动发送的 promptconst SAMPLES: readonly SampleSpec[] [ { buttonLabel: Try with sample image, filename: sample.png, mimeType: image/png, testId: multimodal-sample-image-button, fetchUrl: /demo-files/sample.png, autoPrompt: can you tell me what is in this demo image I just attached, }, { buttonLabel: Try with sample PDF, filename: sample.pdf, mimeType: application/pdf, testId: multimodal-sample-pdf-button, fetchUrl: /demo-files/sample.pdf, autoPrompt: can you tell me what is in this demo pdf I just attached, }, ];autoPrompt被设计为长而具体的句子如can you tell me what is in this demo image I just attached原因有二它既会作为用户消息气泡渲染又会被 aimock演示用的模拟后端按该字符串匹配并返回预设回复这种措辞不会与任意用户 prompt 冲突——随机上传的用户会用不同的问法从而自然落到代理转发而非命中模拟回复。拉取后的三重校验HTTP 状态、LFS 指针、魔数签名在将文件 base64 化之前前端会对拉取结果做三道防御性检查见 sample-attachment-buttons.tsxHTTP 状态检查fetch返回非 2xx 时抛出带状态码的错误并提示Is the file bundled under public/demo-files/...?。Git LFS 指针检测Next.js 在构建期若未执行git lfs pull会以Content-Type: image/png把 LFSpointer 文件以version https://git-lfs...开头的极短纯文本桩原样提供给客户端。如果不做拦截这些残缺指针字节会被 base64 编码后当作看起来合法的 PNG 发给 Agent并在聊天界面渲染成破损的img。因此代码会解码文件头部 64 字节命中LFS_POINTER_PREFIX version https://git-lfs前缀就立刻报错并给出修复提示部署环境需要运行git lfs pull或设置GIT_LFS_ENABLED1确保 Next.js 提供服务前二进制文件已被检出。魔数magic bytes签名校验按 MIME 类型校验文件真实签名——PNG 以0x89 0x50 0x4E 0x47‰PNG开头PDF 以0x25 0x50 0x44 0x46%PDF开头。若不符说明文件损坏或提交了错误的资产。const MAGIC_BYTES: Recordstring, number[] { image/png: [0x89, 0x50, 0x4e, 0x47], // ‰PNG application/pdf: [0x25, 0x50, 0x44, 0x46], // %PDF };值得一提的是实际仓库中这两个示例文件当前仍是 LFS 指针内容可通过od读取验证文件头部即version https://git-lfs...这意味着直接克隆而未执行git lfs pull的环境下前端这第二道校验会立即兜底报错而非静默渲染破损附件——这正是fail loudly with an actionable error设计意图的体现。校验通过后文件通过FileReader.readAsDataURL转为data:mime;base64,payload形式的 Data URL再剥离data:...;base64,前缀只保留 base64 载荷作为 content part 的source.value。onUpload 回调内联 base64 的附件上传实现示例注入与回形针上传最终都会汇入同一个onUpload。该回调由 file-to-data-attachment.ts 实现export function fileToDataAttachment(file: File): PromiseDataUploadResult { return new Promise((resolve, reject) { const reader new FileReader(); reader.onerror () reject(reader.error ?? new Error(FileReader failed for ${file.name})); reader.onload () { const result reader.result; if (typeof result ! string) { reject(new Error(Unexpected FileReader result type for ${file.name})); return; } // result 形如 data:image/png;base64,iVBORw0K...剥离前缀 const commaIdx result.indexOf(,); const base64 commaIdx 0 ? result.slice(commaIdx 1) : result; resolve({ type: data, value: base64, mimeType: file.type || application/octet-stream, metadata: { filename: file.name, size: file.size }, }); }; reader.readAsDataURL(file); }); }关键点它返回的是AttachmentUploadResult的data变体内联 base64而非url变体即不上传外部存储完全自包含——与 Wave 2b 演示规格一致。mimeType取自浏览器提供的file.type空值时回退为application/octet-stream。在 multimodal-chat.tsx 中onUpload被装配进CopilotChat的AttachmentsConfigCopilotChat agentIdmultimodal-demo classNameh-full attachments{{ enabled: true, accept: ACCEPT_MIME, // image/*,application/pdf maxSize: MAX_FILE_SIZE_BYTES, // 10 * 1024 * 1024 10 MB onUpload, onUploadFailed: (err) { console.warn([multimodal-demo] attachment rejected, err); }, }} /accept: image/*,application/pdf限定可选文件类型maxSize: 10 MB限制单文件体积示例文件 50 KB 远低于此但真实上传会被限制onUploadFailed仅做console.warn不破坏默认 UI——校验失败时CopilotChat本身会展示 toast 式提示。后端链路专用运行时路由与 CrewAI 视觉 Flow前端只管注入真正的多模态理解发生在后端。该演示为多模态场景专门划分了一个独立运行时路由route.ts将 base64 图片/PDF 转发与视觉 content block 处理约束在单个 cell 内其他 demo 的运行时保持精简、聊天 LLM 不受影响const AGENT_URL process.env.AGENT_URL || http://localhost:8000; function createAgent() { return new HttpAgent({ url: ${AGENT_URL}/conversational_flows/multimodal }); } const agents: Recordstring, AbstractAgent { multimodal-demo: createAgent(), // 页面 CopilotKit agentmultimodal-demo 解析到这里 default: createAgent(), // 供内部无参 useAgent() 调用的别名 }; export const POST async (req: NextRequest) { try { const copilotHandler createCopilotRuntimeHandler({ runtime: new CopilotRuntime({ agents }), basePath: /api/copilotkit-multimodal, mode: single-route, }); return await copilotHandler(req); } catch (error: unknown) { const e error as { message?: string; stack?: string }; return NextResponse.json( { error: e.message, stack: e.stack }, { status: 500 }, ); } };其中HttpAgent指向后端 CrewAI Flow 的 AG-UI 端点/conversational_flows/multimodal默认localhost:8000可用环境变量AGENT_URL覆盖。页面 page.tsx 通过CopilotKit runtimeUrl/api/copilotkit-multimodal agentmultimodal-demo绑定到这条链路。后端是专门的视觉 Flow multimodal_flow.py。CrewAI bridgeag_ui_crewai会在 Flow 启动前把 AG-UI 附件 parts 规范化为 LiteLLM/OpenAI 可用的 content blocksFlow 再将它们转发给视觉模型。其核心逻辑系统提示词要求模型检查附件图片与文档文本、描述相关内容并在回答中明确指出附件模态若检测到消息中含 PDFdata:application/pdf前缀走aresponsesResponses API路径其中_pdf_responses_input把 bridge 规范化后的 blocks 再转换为input_filePDF 用file_data携带 data URL与input_image图片parts否则走常规acompletionChat Completions路径并附带toolsself.state.copilotkit.actions保留工具调用能力两条路径都以copilotkit_stream流式包装后追加到self.state.messages。模型在示例配置中为openai/gpt-5.4经 LiteLLM 访问仅在视觉模型请求时启用。兼容层legacy converter shim 与附件去重现代 AG-UI 形状的{ type: image | document, source: {...} }parts 在经ag-ui/langgraph0.0.x 发布版转发到 LangChain 时会被静默丢弃——旧版转换器只认 legacy 的{ type: binary, mimeType, data | url }形状。为此 legacy-converter-shim.tsx 在onRunInitialized阶段遍历用户消息为每个现代媒体 part追加一个 legacybinary镜像而非替换因为CopilotChatUserMessage.getMediaParts只渲染image|audio|video|document替换会导致附件在 UI 中消失。该改写是幂等的已带binary镜像的消息再次运行不会重复追加。另一侧从服务器回传的消息快照里同一个附件会出现两次客户端添加的现代 part 转换器重转的 shape且 PDF 会被一律打成type: image落到破损的img渲染上。shim 在onMessagesSnapshotEvent与onRunFinalized两个钩子里按source.value去重并按 mimeType 重新判定类型image/*→imageaudio/*→audiovideo/*→video其余 →document确保 PDF 走DocumentAttachment图标 文件名渲染图片走ImageAttachment每个附件在用户气泡中恰好一个 chip。E2E 测试如何消费这些示例文件multimodal.spec.ts 专门针对示例注入路径做端到端验证真实 OS 文件选择器无法被 Playwright 稳定自动化而示例按钮与回形针路径最终汇聚到同一 V2 Agent 表面因此该套件足以覆盖渲染 往返全链路。测试桩包括页面加载后示例行、两个示例按钮、输入框、回形针均可见点击图片按钮自动发送预设 prompt、用户气泡中恰好 1 个img、无 Failed to load image助手回复命中/copilotkit|logo|image/i点击 PDF 按钮用户气泡中 0 个img、可见PDF标签DocumentAttachment渲染、且不出现[Attached document]文本验证 PDF 扁平化中间件只作用于模型请求、不污染 UI 气泡同一会话中图片后 PDF与PDF 后图片两个方向的组合各自消息保持独立且唯一的 chip无串扰、无加倍。文件头注释明确列出这些断言对应着多模态重写过程中真实踩过的回归 bug因此既是回归测试也是 demo-files 文件内容规范PNG 可识别、PDF 含 CopilotKit 字样的验收标准。测试还要求 aimock 配置与SAMPLES中一致的两个 canned prompt见 showcase/aimock 下的 fixture保证模拟回复可命中。故障排查速查结合 README 与源码示例按钮可能出现的故障及其根因如下现象根因处置点击示例按钮提示Could not fetch sample ... HTTP 404/xxxpublic/demo-files/下文件缺失或路径不符确认 sample.png、sample.pdf 存在且文件名一致报错is a Git LFS pointer, not the real asset克隆环境未执行git lfs pullNext.js 把 LFS 指针文本当资源返回部署/本地运行前执行git lfs pull或设置GIT_LFS_ENABLED1报错does not have a valid image/png (application/pdf) signature文件损坏或提交了错误资产用file命令核对魔数重新提交正确的二进制文件页面能渲染但示例按钮报 fetch 错误回形针/拖拽正常示例文件缺失README 明确说明该降级行为补全 public/demo-files/ 下两个文件即可恢复不影响真实上传README 特别声明即使示例文件缺失演示页仍能正常渲染只是示例按钮会报 fetch 错误回形针与拖拽上传路径不受影响。这保证了演示的真实上传能力永远可用示例按钮仅是锦上添花的便捷入口。小结public/demo-files/虽只是两个静态二进制文件却串起了 CopilotKit 多模态演示的完整链路公开路径拉取 → 魔数/LFS 防御 → base64 化 → 与回形针共用的附件管线 → 专用运行时路由 → CrewAI 视觉 Flow → 兼容层去重还原 → E2E 断言。理解这套约定文件大小上限、PDF 内容关键词、LFS 提交要求与实现细节你既能快速复现多模态附件 Demo也能在其他基于 CopilotKit 的项目中安全地设计预置示例附件 真实上传双路径。【免费下载链接】CopilotKitThe Frontend Stack for Agents Generative UI. React, Angular, Mobile, Slack, and more. Makers of the AG-UI Protocol项目地址: https://gitcode.com/GitHub_Trending/co/CopilotKit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考