ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Genkit Python A2UI Surfaces 中间件:让模型在 generate 中渲染交互式 UI 卡片

Genkit Python A2UI Surfaces 中间件:让模型在 generate 中渲染交互式 UI 卡片 Genkit Python A2UI Surfaces 中间件让模型在 generate 中渲染交互式 UI 卡片【免费下载链接】genkitOpen-source framework for building agentic apps in JavaScript, Go, Dart, and Python, built and used in production by Google项目地址: https://gitcode.com/GitHub_Trending/ge/genkit本篇围绕py/packages/genkit-a2ui包的 README 展开系统讲解 Genkit Python 的实验性 A2UI 中间件如何仅通过在ai.generate或define_agent的use[...]中加一个Surfaces()让模型以a2ui围栏输出 UI 描述并被自动改写为application/a2uijson数据部分以及下一轮对话时这些数据部分如何再转回文本让模型看见此前的界面与用户点击并结合仓库源码深入剖析中间件的改写链路、目录catalog注册机制、内置 basic 目录的组件清单、流式解析器与三种校验模式帮助你在 Python 应用里落地会画界面的 Agent。1. 包定位与快速上手genkit-a2ui是 Genkit Python 的实验性 A2UIAgent-to-UI中间件包位于 py/packages/genkit-a2ui。其核心行为在 README 中用三句话概括把Surfaces()加到ai.generate或define_agent的use[...]中模型若输出a2ui围栏中间件把它改写为application/a2uijson数据部分data part下一轮请求时这些数据部分重新变回文本使模型能看到此前的界面surface和按钮点击事件。最小可用示例继承自 READMEfrom genkit import Genkit from genkit_a2ui import Surfaces, envelopes_from_parts from genkit_google_genai import GoogleAI ai Genkit(plugins[GoogleAI()]) response await ai.generate( modelgoogleai/gemini-2.5-flash, promptShow me the weather in Tokyo, use[Surfaces()], ) envelopes envelopes_from_parts(response.message.content)envelopes_from_parts从响应消息的 content 中提取所有 A2UI 数据部分里的信封envelope列表供前端渲染器或业务代码消费。从 pyproject.toml 看该包当前版本为0.11.0依赖genkit0.9.0与pydantic2.10.5支持 Python 3.10–3.14分类标记为 Development Status 3Alpha因此 README 末尾特别标注了 Status: experimental在生产使用前应关注其 API 稳定性。2. 双向改写出站的围栏→数据部分入站的数据部分→文本理解Surfaces的关键在于它是一个双向翻译器。源码入口是 py/packages/genkit-a2ui/src/genkit_a2ui/_middleware.py其中Surfaces继承自 Genkit 的BaseMiddleware通过重写wrap_model钩子介入模型调用出站response 方向。wrap_model在调用next_fn前后做两件事若开启了流式ctx.on_chunk非空通过ctx.replace_on_chunk安装一个ChunkHandler在每个流式 chunk 到达时经StreamParser增量解析——围栏一旦闭合就立即改写为数据部分下发客户端无需等整条消息完成即可拿到卡片流结束后调用transform_response对完整消息再解析一次保证response.message与流式内容一致。源码注释特别提到SurfaceIdReplay会在最终解析时回放流式阶段已分配的表面 id避免流式与最终结果 id 不一致。入站request 方向。wrap_model开头先执行sanitize_inbound(request...)遍历历史消息凡mimeType为application/a2uijson的 part 一律转换为文本再发给模型。转换规则由summarize_envelopes实现普通界面信封createSurface/updateComponents/updateDataModel/deleteSurface按序聚合重新打包成a2ui围栏文本——即把卡片原样回放给模型看用户交互产生的action信封单独渲染为一行[UI action refresh on surface s1 context{city:Tokyo}]这样的文本让模型知道用户在哪个表面点了哪个按钮、带了什么上下文若一条消息只剩空的 A2UI part会补一个[UI]占位文本保证消息非空。异常收尾不抢救。ABNORMAL_FINISH_REASONS集合包含BLOCKED、ABORTED、INTERRUPTED、FAILED、OTHER、UNKNOWN六种 finish reason命中其一的响应会被原样返回而不做改写——源码注释解释得很直白stop is the result, not a salvaged card停止本身就是结果别去抢救一张残卡。这些行为都有对应测试钉住出站改写见 py/packages/genkit-a2ui/tests/generate_a2ui_test.py例如test_generate_a2ui_rewrites_fence_to_data_part断言围栏消失、数据部分出现入站回放见 py/packages/genkit-a2ui/tests/generate_a2ui_inbound_test.py例如test_generate_a2ui_sends_click_to_model_as_text断言按钮点击以文本而非数据部分到达模型、历史围栏按表面 id 完整回放、不同表面的围栏在点击处正确拆分。3. Surfaces 配置项全解Surfaces接受SurfacesConfig同样定义在 _middleware.py字段、别名与默认值如下字段别名类型默认值说明instructions—system|nonesystem是否把 A2UI 使用说明注入系统提示validationvalidatestrict|warn|offwarn围栏/组件校验强度surface_idsurfaceIdstr \| NoneNone固定表面 id 策略缺省时每次自动分配 UUIDcatalog—str \| NoneNone目录 id缺省解析为内置 basic 目录version—v0.9|v0.9.1v0.9写入信封的 A2UI 协议版本各字段的实际效果可以从源码中逐一对应instructions为system时inject_instructions会把目录渲染出的使用说明追加到请求中第一条roleSYSTEM消息的末尾若请求没有系统消息则新建一条并置顶。设为none则跳过注入。validation传给StreamParser控制解析失败时的处置——strict抛出A2uiParseError、warn记录日志并丢弃该块保留原始模型文本、off静默放行。surface_idsurface_id_factory据此构造 id 生成器——给了策略字符串就恒定返回该字符串测试test_generate_a2ui_new_surface_does_not_reuse_history_id验证了Surfaces(surface_idsfc-new)时新卡片的createSurfaceid 恰为sfc-new否则生成uuid4。源码注释指出指定固定 id 可以防止模型从历史里复制旧表面 id造成串卡。catalog从 registry 中按 id 解析目录详见下节。version常量定义在 _types.pySupportedVersion目前只允许v0.9与v0.9.1默认DEFAULT_VERSION v0.9注释提醒拼错版本会给信封盖上渲染器画不出来的戳。4. 目录Catalog系统组件清单即模型的可用 UI 词汇表README 的第二段示例展示了自定义目录的用法from genkit_a2ui import Surfaces, A2uiCatalog, A2uiCatalogComponent, load_catalog catalog A2uiCatalog( idhttps://my-app.org/catalogs/custom.json, components( A2uiCatalogComponent(nameBanner, descriptionA prominent alert., propstitle: string.), ), ) load_catalog(ai, catalog) response await ai.generate( modelgoogleai/gemini-2.5-flash, promptShow a warning banner, use[Surfaces(catalogcatalog.id)], )目录的数据结构定义在 py/packages/genkit-a2ui/src/genkit_a2ui/_catalog.pyA2uiCatalogComponent是只含name、description、props三个字符串的冻结 dataclass——注意props是自然语言描述如title: string.因为它最终会拼进系统提示给模型阅读而非机器校验 schemaA2uiCatalog含一个id字符串README 示例用 URL 形式标识和组件元组as_value()导出为{id: ..., components: [{name,description,props}, ...]}的普通字典from_value()反序列化并做宽松校验缺id或components列表、组件缺name时返回None。注册与解析逻辑集中在 py/packages/genkit-a2ui/src/genkit_a2ui/_loader.pyload_catalog(ai, catalog)以A2UI_CATALOG_VALUE_TYPE常量值为a2ui-catalog为类型键、以目录id为资源 id 注册到ai.registry若同 id 已注册但内容不同只告警并保留已有目录幂等且先到先得。id为空直接抛ValueErrorload_catalog_file(ai, ./my-catalog.json)从磁盘读 JSON 后走同一注册流程读取失败或 JSON 不合法、结构不是目录时都抛带上下文的ValueErrorregister_basic_catalog(ai)把内置BASIC_CATALOG放进 registry。README 说明这样做的目的是让 Developer UI 能把它和自定义目录一起列出来对应接口为GET /api/values?typea2ui-catalogresolve_catalogSurfaces(catalog...)在wrap_model中的实际解析函数——先查 registry未注册时若 id 恰好等于默认值basic或内置 basic 目录的规范 idhttps://a2ui.org/specification/v0_9/catalogs/basic/catalog.json见 _types.py直接回退到内置BASIC_CATALOG否则抛错提示先用 load_catalog 注册或用默认目录。这也解释了为什么 README 里不注册、直接Surfaces()也能用——默认 idDEFAULT_CATALOG_ID basic天然落在回退分支里。4.1 内置 basic 目录组件一览BASIC_CATALOG在 _catalog.py 中定义了 12 个组件其名称、描述与属性约束会被逐条写进系统提示render_catalog_instructions按- 名称: 描述 Props: 属性格式拼接组件用途关键属性Text文本展示标题用variant而非内嵌 Markdowntext必填variant?: h1\|h2\|h3\|h4\|h5\|caption\|bodyImageURL 图片url必填fit?、variant?icon/avatar/smallFeature/mediumFeature/largeFeature/headerIconMaterial 图标限定 60 个精确名称禁止自造如不存在 cloud、thermostatname必填枚举见源码BASIC_ICON_NAMESRow/Column水平 / 垂直布局容器children: string[]必填组件 id 数组justify?、align?List列表容器children必填direction?、listStyle?Card单子节点卡片child必填单个 id多个需包 Column/RowDivider分隔线axis?: horizontal\|verticalButton可点击按钮点击把事件回传给 Agentchild必填action: {event:{name, context?}}必填TextField/CheckBox/Slider表单输入label/value必填Slider另需max与value可选min、step4.2 系统提示如何生成render_catalog_instructions(catalog)用string.Template组装出注入模型的系统指令其骨架为节选自 _catalog.py 中的INSTRUCTIONS模板声明你可以渲染富交互 UI当结果适合展示而非讲述天气、列表、表单、对比、确认等时输出一个以a2ui为标签的围栏代码块内含 A2UI 信封 JSON 数组规定 UI 是邻接表adjacency list组件是扁平数组靠字符串id引用组树不允许嵌套对象必须有且仅有一个id: root数值可以是字面量也可以是数据模型绑定{path: /somePath}顺序约定先createSurface带catalogId再updateComponents最后可选updateDataModel可以合并在一个数组里按序发出交互组件触发action其name会在用户操作时回传给模型所以要取有意义的名字用户与界面交互后的更新响应必须整面重渲染重新createSurfaceupdateComponents不能假设旧表面仍存在若目录含TextField/CheckBox/Slider额外附加表单章节输入的value必须绑定{path}且提交按钮的action.event.context必须回显同一路径否则点击事件到达时context为空、用户输入丢失若目录含CardColumnText内嵌一个天气卡片完整示例createSurface→updateComponents→updateDataModel设置/temp否则退化为最小示例收尾强调不要解释 JSON直接渲染表面 id 用字面占位符SURFACE_ID系统会替换为真实 id。模板还会追加让界面好看些的风格提示分层布局、标题用 heading variant、主按钮用primary等仅当目录包含对应组件时才出现。5. 流式解析器围栏如何被安全地切开、缝合与校验StreamParserpy/packages/genkit-a2ui/src/genkit_a2ui/_parser.py是中间件的解析核心设计目标是处理围栏可能被切在任意 chunk 边界的现实问题开围栏检测OPEN_FENCE_RE匹配a2ui起始PARTIAL_OPEN_FENCE_RE识别缓冲尾部的疑似半个围栏、、 乃至不完整的 a2u 把这些字符扣下不发防止一个被拆成两段的围栏泄漏成普通文本——测试test_generate_a2ui_stitches_fence_split_across_parts正是验证跨 part 的围栏最终合并为一个数据部分闭围栏等待进入块内后若找不到行首且非最终 flush则ClosedBlock.need_more让解析器继续等下一个 chunk只向前消费已确认安全的行信封归一化finalize_block解析 JSON数组或单对象都接受逐信封经normalize_envelope处理——补version字段、用fill_placeholder_id把空值或字面量SURFACE_ID占位替换为真实表面 id、校验updateComponents.components里每个组件的component类型必须在目录已知集合内根节点检查validate_root要求组件列表中存在id: rootcreateSurface 的三种情形块内含createSurface时force_surface_id会把新铸造的 id 强制盖到所有信封上防止模型复用历史里旧表面的 id块里只带某个模型已知的表面 id 的更新时不额外发明createSurface避免抹掉用户正在点击的卡片块里既无createSurface又是新表面时自动补一个createSurface信封置于最前保证客户端有表面可画。校验失败的处置由ValidateMode决定strict抛A2uiParseError流式场景下ChunkHandler会把该错误暂存避免被框架包装成 INTERNAL 错误待最终响应时再抛出warn记日志后丢弃该块、保留原始文本off直接放行。6. A2UI 数据部分MIME 与信封结构数据部分的读写助手在 py/packages/genkit-a2ui/src/genkit_a2ui/_part.pya2ui_part(envelopes)构造Part(DataPart(data{envelopes: [...], metadata{mimeType: application/a2uijson}}))has_a2ui_mime(part)仅凭metadata.mimeType A2UI_MIME_TYPE常量application/a2uijson判断一个 part 是否是 A2UI part——源码注释强调mime 类型是渲染器与下一次 generate 认定卡片的依据即使 part 里还没有信封is_a2ui_part在此之上再要求data是 dict 且含envelopes键envelopes_from_parts(parts)从若干 part 中安全地抽出所有envelopes里的 dict 项即 README 快速上手里envelopes_from_parts(response.message.content)的实现入参为None或无匹配时返回空列表。由此可以确认中间件前后 A2UI 内容在消息里的两种形态模型侧永远是文本围栏或[UI action ...]行应用/渲染器侧是application/a2uijson数据部分envelopes数组中的信封键集合见 _types.py 的SURFACE_KEYScreateSurface、updateComponents、updateDataModel、deleteSurface另外用户点击会产生action信封含name、surfaceId、context等字段可由summarize_envelopes的解析逻辑印证。7. 组合使用与验证方式与 Agent 组合README 指出Surfaces()同样可用于define_agent的use[...]中间件本质是wrap_model钩子因此在模型级生效generate与 Agent 的模型调用都会经过同一套改写逻辑与插件共存示例中Genkit(plugins[GoogleAI()])表明Surfaces属于 generate 层的use中间件与 provider 插件无冲突模型只需遵循注入的系统提示即可无需任何 provider 特化支持跑测试验证行为包内测试用可编程模型programmableModel分别钉住两层行为——L1出站见 generate_a2ui_test.py围栏改写、纯散文不受影响、仅围栏响应、跨 part 缝合、围栏前后文本保序等L2入站见 generate_a2ui_inbound_test.py点击事件以文本回传、历史表面按原 id 回放、空 A2UI 消息保留[UI]占位、带 A2UI mime 但无 envelopes 的裸 part 被丢弃等目录文件加载见 catalog_loader_test.py。8. 小结genkit-a2ui用一条Surfaces()中间件打通了模型输出文本围栏 → 应用收到结构化 UI 数据 → 下一轮模型又能读懂 UI 与用户操作的完整闭环出站由StreamParser流式安全地切缝围栏并补全校验版本戳、SURFACE_ID占位替换、root 检查、目录内组件白名单入站由sanitize_inbound把数据部分重新讲成人话围栏回放 [UI action ...]行。目录系统把模型可以画哪些组件变成可注册、可枚举、可被 Developer UI 列出的 registry 值load_catalog/load_catalog_file/register_basic_catalog覆盖了代码内定义、磁盘 JSON 与内置 basic 三种来源。由于包仍标注为实验性0.11.0Alpha建议在生产集成前跟踪其 API 变化并用包内自带的可编程模型测试集作为行为回归的参照基线。【免费下载链接】genkitOpen-source framework for building agentic apps in JavaScript, Go, Dart, and Python, built and used in production by Google项目地址: https://gitcode.com/GitHub_Trending/ge/genkit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进