Macro Agent Harness SDK 类型详解:ClassifyResponses 与 `POST /universal_input/classify` 响应契约
  • 后端
  • 前端
  • 协同办公
  • 即时通讯
  • 企业应用
  • CRM
  • AI Agent
  • 人工智能

【免费下载链接】macro

Macro is a unified workspace for teams: email, chat, docs, tasks, agents, calls, and CRM — @-linked together with shared AI memory.

项目地址:https://gitcode.com/GitHub_Trending/macro3/macro
点击查看免费下载

ClassifyResponses是 Macro Agent Harness SDK 中 Universal Input(命令面板 Command K / 移动端搜索)意图分类接口的唯一成功响应类型集合。本文以该类型别名为切入点,完整拆解POST /universal_input/classify的请求体、响应结构、IntentScore的"独立概率"语义、revision回声机制,并结合仓库中的 Rust 实现与 TypeScript SDK,说明如何在客户端正确消费它,以及错误状态码与降级路径。

类型定义:一个只含 200 的响应映射

原始 TypeDoc 条目非常精简,其全部内容如下:

type ClassifyResponses = { 200: ClassifyInputResponse; }

配合 ClassifyResponses.md 与同名生成的 types.gen.ts,可以看到两个关键事实:

  • ClassifyResponses是一个按 HTTP 状态码索引的映射类型,当前只声明了200这一个成功分支,因此ClassifyResponse的定义就是它的值并集:

    export type ClassifyResponses = { 200: ClassifyInputResponse; }; export type ClassifyResponse = ClassifyResponses[keyof ClassifyResponses];
  • 非 2xx 分支不进入ClassifyResponses,而是由并列的ClassifyErrors承担(见下文"错误契约"一节)。这种"成功/失败分型"的设计让调用方能在类型层面区分result.data与错误枚举,而不必用可选链猜测字段是否存在。

(说明:本条目不涉及界面元素,故不引入配图。)

ClassifyInputResponse:响应体真正承载的信息

ClassifyResponses[200]指向 ClassifyInputResponse.md,它是一个只有两个字段的对象:

type ClassifyInputResponse = { revision: number; scores: IntentScore[]; };

字段含义与源码完全一致(domain.rs):

字段类型说明源码约束
revisionnumber从分类请求原样回声的客户端修订号Rust 侧为u32,服务端不解释其业务含义
scoresIntentScore[]按固定意图-问题顺序排列的独立分数长度必须等于意图问题数量,否则整次调用判为不可用

需要注意两点实现细节:

  1. revision是u32而非任意字符串。TypeDoc 显示number,但 Rust 结构体定义为pub revision: u32,因此客户端应使用单调递增的整数修订号,避免超出 32 位无符号范围。
  2. scores的顺序是契约的一部分,不是随意排列。服务端用INTENT_QUESTIONS.iter().zip(scores)组装结果,顺序由常量数组决定,见 domain.rs。

IntentScore:互不求和、独立判定的七路概率

IntentScore的定义是:

type IntentScore = { intent: InputIntent; score: number; };

文档中一句关键注释被很多人忽略:"Independent yes/no probabilities; these do not sum to one."(独立的是/否概率,不会加和为 1。)

这一点在源码中有明确依据。分类并非一次多分类推理,而是把 7 个固定的"是/否问题"同时送给 Jev 的YesNoClassifier(domain.rs):

let questions = INTENT_QUESTIONS .iter() .map(|(_, text)| YesNoQuestion::try_from(*text).expect("fixed intent question is valid")) .collect::<Vec<_>>(); let scores = classifier .classify( UsageContext::new(AiFeature::DynamicCompletionsApi, user), &serde_json::json!({ "draft": input.text }), &questions, ) .await .map_err(|_| InputError::Unavailable)?; if scores.len() != INTENT_QUESTIONS.len() { return Err(InputError::Unavailable); }

因此工程上必须遵守三条规则:

  • 不要做归一化:不要把 7 个score除以总和当作置信度,那会破坏每个意图独立的概率语义。
  • 不要用"最高分必须超过 0.5"做硬门槛:文档描述的行为是"最强的候选会直接开启草稿,即使分数接近"(见 README.md)。接近的分数应作为"备选可切换"呈现,而不是丢弃。
  • 分数只用于排序与展示,最终落点仍需人工可覆盖:Cmd/Ctrl+Shift+Right向前循环、Left向后循环,手动选择会使旧预测失效。

七种InputIntent(snake_case 序列化,见 domain.rs)在数组中的固定顺序为:ai、search、email、note、task、calendar、message。这意味着scores[0]恒为 AI、scores[5]恒为 Calendar —— 但更稳妥的做法是按intent字段索引,而不是依赖下标。

score的取值范围为[0, 1],在 Rust 侧从Probability::get()取出f32(domain.rs),跨 JSON 边界后成为 TS 的number。

请求侧:ClassifyInputRequest与"只读推断"

响应契约只有与请求契约成对阅读才完整。ClassifyData明确了 URL 与方法:

type ClassifyData = { body: ClassifyInputRequest; path?: never; query?: never; url: "/universal_input/classify"; };

请求体仅两个字段(ClassifyInputRequest.md):

type ClassifyInputRequest = { revision: number; // 不透明客户端修订号,用于丢弃过期预测 text: string; // 当前草稿文本,仅分类、不执行 };

服务端对text有硬性校验(domain.rs):

fn validate_text(text: &str) -> Result<(), InputError> { if text.trim().is_empty() || text.chars().count() > 16_000 { return Err(InputError::InvalidText); } Ok(()) }

即:去空白后不能为空,且字符数(非字节数)不超过 16000。中文、emoji 等按 Unicode 字符计数,这一点与String::len()的字节语义不同,客户端做长度预校验时需使用Array.from(text).length或等价的码点计数方式。

请求体只含text与revision,不含intent、time_zone、reference_time、property_definitions—— 那些属于POST /universal_input/extract的ExtractInputRequest。这从结构上保证了分类阶段不会碰到时区解析或任务属性校验的错误分支。

从 SDK 调用:classify方法

生成的客户端方法位于 sdk.gen.ts:

public classify<ThrowOnError extends boolean = false>( options: Options<ClassifyData, ThrowOnError> ): RequestResult<ClassifyResponses, ClassifyErrors, ThrowOnError> { return (options.client ?? this.client).post<ClassifyResponses, ClassifyErrors, ThrowOnError>({ url: '/universal_input/classify', ...options, headers: { 'Content-Type': 'application/json', ...options.headers } }); }

一次典型的调用与消费可以写成:

import { AgentHarnessClient } from "./generated/agent-harness"; const api = new AgentHarnessClient({ headers: { authorization: `Bearer ${token}` } }); const { data, error } = await api.classify({ body: { text: "Call John at 3pm tomorrow", revision: 42 }, throwOnError: false, }); if (error) { // 自动检测不可用:回退到手动选择类型 } else if (data) { // 按 intent 建表,避免依赖数组下标 const byIntent = new Map(data.scores.map((s) => [s.intent, s.score])); const ranked = [...data.scores].sort((a, b) => b.score - a.score); if (data.revision === currentRevision) { // 只有修订号与当前草稿一致时才应用,丢弃过期预测 applyTopCandidate(ranked, byIntent); } }

三个工程要点:

  • 必须校验revision回声,这是该字段存在的唯一目的,文档注释写得很直白:"Revision echoed from the classification request."
  • 必须处理error分支,不要把ClassifyErrors当成不会发生。
  • 不要在前端本地做前缀解析覆盖分类结果:README 明确"No local prefix parser overrides it",分类器是唯一的落点决策者。

错误契约:合法状态码集合

ClassifyResponses只覆盖 200,其余状态由utoipa注解声明在路由上(axum_router.rs):

状态码触发条件源码依据
200分类成功body = ClassifyInputResponse
400草稿为空或超过 16000 字符InputError::InvalidText→BAD_REQUEST
401缺少或无效凭证MacroAuthorizationExtractor先于业务逻辑执行
402AI 配额/准入策略拒绝AiAdmissionError直接透传响应
503分类器未配置或调用失败InputError::Unavailable→SERVICE_UNAVAILABLE

错误映射逻辑集中在InputApiError::into_response(axum_router.rs):

let status = match &self.0 { InputError::InvalidText | InputError::InvalidTimeZone | InputError::InvalidProperties => StatusCode::BAD_REQUEST, InputError::Unavailable | InputError::Extraction => StatusCode::SERVICE_UNAVAILABLE, InputError::Admission(error) => return (*error).into_response(), };

对应的人类可读消息也在InputError的#[error(...)]中固定:InvalidText→ "Enter between 1 and 16000 characters",Unavailable→ "Automatic detection is unavailable; choose a type to continue"。这些文案本身就是产品的降级指引,客户端可以直接呈现。

认证先于推断这一点有测试兜底:axum_router/test.rs 对classify与extract两个端点分别验证了"无 token"与"无效 token"都返回401,并强调both_mounted_routes_require_authentication_before_inference。

准入、计费与遥测:200背后的前置条件

返回200之前,服务端还做了两件事:

  1. AI 准入检查。classify在拿到分类器后、真正调用模型前执行admission.admit(&user, AiFeature::DynamicCompletionsApi)(domain.rs)。失败即映射为InputError::Admission,进而按AiAdmissionError自身语义响应(典型为 402)。
  2. 用量归因。分类调用携带UsageContext::new(AiFeature::DynamicCompletionsApi, user),使 token 消耗计入该用户与功能维度。

此外,classify方法被#[tracing::instrument]包裹,字段包含revision、draft_chars(字符数)、input.prompt_version与input.recipe_version(domain.rs)。这解释了为什么请求结构体如此之小却能支撑可观测性:修订号与字符数在服务端日志里是可关联的,丢掉revision会让追踪链路断裂。

降级与节流:让这份契约真正可用

ClassifyResponses描述的是"理想路径",但 Universal Input 的设计前提是推断随时可能不可用。README 给出了明确的降级条件(README.md):

  • 未配置 Jev(TYPESAFE_API_KEY)时,classifier为None,classify立即返回InputError::Unavailable(见self.classifier.as_ref().ok_or(InputError::Unavailable)?),前端进入手动类型选择模式。
  • 仅运行前端、后端缺少这两个路由时,同样需要手动填写字段。
  • 客户端以200ms间隔节流输入,并对分类与抽取分别合并,每个队列同时只有一个在途请求;两队列合并到最新文本,因此缓慢的字段抽取不会阻塞类型预测。

这意味着:revision应当随每次文本变化递增,而"最旧的在途响应"要在落地前用修订号拦掉;节流与合并逻辑放在客户端,服务端不负责去重。

自测与验证:不要用单测证明模型准度

仓库提供了两组可直接复用的验证手段:

  • 路由级单测:Classifier桩件对所有问题返回0.5,用于验证传输层契约(认证、序列化、状态码),不用于评估真实模型精度。
  • 固定时钟行为基线:eval/README.md 记录了真实意图/字段错配并对比历史报告,全程不提交任何动作。

README 还给出了一组"只输入、不提交"的草稿样例,用于人工核对scores的排序是否符合预期(README.md):

草稿预期落点
Explain how database indexes workAI
Find the email from Alice about the launchSearch
Email john@example.com that I'll be lateEmail,且有可见正文
Remind me to email Julia tomorrow about reschedulingTask(尽管提到了 email)
# Research notes+ 观察内容Markdown note
Finish the launch report by FridayTask,带截止日期
Call John at 3pm tomorrowEvent,本地 15:00,时长一小时,无来宾
John启动最强候选,备选保留

同时应覆盖:部分输入的草稿、意图变化、重名、显式来宾邀请、时区边界与 provider 失败。手动编辑过的字段会锁定类型并在后续推断中保留,使用 Auto 可恢复自动检测。

小结

ClassifyResponses看似只是一个两行的类型映射,但它锚定了整条链路的关键约定:200 只返回revision与固定顺序的七个独立概率;错误路径由 400/401/402/503 分担;revision是客户端丢弃过期预测的唯一凭据;概率不可归一化、不可互相比较求和。把这几条落实到类型与代码里,就能在不修改ClassifyInputResponse契约的前提下,安全地为 Command K 与移动端搜索接入意图分类——并在分类器缺席时平滑退回手动选择。

  • 后端
  • 前端
  • 协同办公
  • 即时通讯
  • 企业应用
  • CRM
  • AI Agent
  • 人工智能

【免费下载链接】macro

Macro is a unified workspace for teams: email, chat, docs, tasks, agents, calls, and CRM — @-linked together with shared AI memory.

项目地址:https://gitcode.com/GitHub_Trending/macro3/macro
点击查看免费下载

相关推荐

上一篇:FastUI后端数据库性能分析:查询优化与索引设计
下一篇:microG 华为版安装指南:让 HarmonyOS 上的"Google Play 服务不可用"彻底消失

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考