feat(asset-ai): simplify count answers and refine mobile conversation layout
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
@@ -257,3 +257,15 @@ test("metric references keep one unit when the model appends it in prose", async
|
||||
await generatePresentation(value,"说明库存",signal());
|
||||
assert.equal(value.presentation?.blocks[0].text,"库存2 台,其中待交车0台。");
|
||||
});
|
||||
|
||||
test("verified single count bypasses generated report and external request", async (t) => {
|
||||
configure(t);
|
||||
t.mock.method(globalThis, "fetch", async () => { throw new Error("count must not call presentation model"); });
|
||||
const r = result({plan:plan({category:"abnormal",groupBy:"none"}),outcome:"empty",rows:[],groups:[],metrics:[{label:"异动车辆",value:0,unit:"台"}]});
|
||||
await generatePresentation(r,"当前有多少异动车辆?",new AbortController().signal);
|
||||
assert.equal(r.conciseCount,true);
|
||||
assert.equal(r.outcome,"complete");
|
||||
assert.equal(r.presentation,undefined);
|
||||
assert.equal(r.presentationWarning,undefined);
|
||||
assert.match(r.summary,/异动车辆为 0 台/);
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
ResultPresentation,
|
||||
} from "../../../shared/assets/assistant.js";
|
||||
import { validatePlan } from "./model.js";
|
||||
import { simplifyCountAnswer } from "./presentation.js";
|
||||
|
||||
type Dataset = "groups" | "rows" | "daily" | "comparison";
|
||||
type Field = { field: string; label: string; type: "number" | "string" | "boolean" };
|
||||
@@ -158,6 +159,8 @@ function fallback(target: AssistantResult, warning: string) {
|
||||
}
|
||||
/** Generate presentation metadata only; query data and authorization are unchanged. */
|
||||
export async function generatePresentation(result: AssistantResult, question: string, signal: AbortSignal): Promise<void> {
|
||||
if (signal.aborted) throw signal.reason || new Error("已取消");
|
||||
if (simplifyCountAnswer(result, question)) return;
|
||||
if (process.env.ASSET_AI_EXTERNAL_ENABLED === "0") return;
|
||||
if (signal.aborted) throw signal.reason || new Error("已取消");
|
||||
const targets = targetsFor(result).filter(target => target.result.plan.responseMode !== "vehicle-status");
|
||||
|
||||
@@ -158,3 +158,11 @@ test("presentation receives authorized aggregates while vehicle rows stay in the
|
||||
);
|
||||
assert.equal(events.filter((e) => e.type === "step").length, 4);
|
||||
});
|
||||
|
||||
test("inventory subsets do not inherit parent inventory metric cards", () => {
|
||||
for (const category of ["abnormal", "pending", "available", "awaiting"] as const) {
|
||||
const r = assetResult(vehicles, {kind:"assets", category, groupBy:"none", view:"chart", filters:{}});
|
||||
assert.equal(r.metrics.length, 1, category);
|
||||
assert.doesNotMatch(r.summary, /其中异动.*待交车/);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -335,7 +335,7 @@ export function assetResult(
|
||||
missingDueDate: "未填写预计完成时间",
|
||||
}[plan.exception || "overdue"];
|
||||
let summary = rows.length
|
||||
? `${scope}共有 ${rows.length.toLocaleString()} 台${CATEGORY_LABELS[plan.category]}${plan.exception ? `,均为${exceptionLabel}的异动车辆` : ""}。${abnormal || pending ? `其中异动 ${abnormal} 台、待交车 ${pending} 台,已计入库存。` : ""}${plan.topN ? `下方展示排名前 ${plan.topN} 的分组,总量与占比仍按完整筛选范围计算。` : ""}`
|
||||
? `${scope}共有 ${rows.length.toLocaleString()} 台${CATEGORY_LABELS[plan.category]}${plan.exception ? `,均为${exceptionLabel}的异动车辆` : ""}。${["all", "inventory"].includes(plan.category) && (abnormal || pending) ? `其中异动 ${abnormal} 台、待交车 ${pending} 台,已计入库存。` : ""}${plan.topN ? `下方展示排名前 ${plan.topN} 的分组,总量与占比仍按完整筛选范围计算。` : ""}`
|
||||
: `${scope}的${CATEGORY_LABELS[plan.category]}为 0 台,暂无符合条件车辆。`;
|
||||
let inventoryBreakdown: AssistantResult["inventoryBreakdown"];
|
||||
if (plan.category === "all") summary = `${scope}共有资产 ${rows.length.toLocaleString()} 台,其中运营 ${operating.toLocaleString()} 台、库存 ${inventory.toLocaleString()} 台,运营率 ${operationRate}%。`;
|
||||
@@ -405,7 +405,7 @@ export function assetResult(
|
||||
unit: "%",
|
||||
},
|
||||
]
|
||||
: [
|
||||
: plan.category === "inventory" ? [
|
||||
{
|
||||
label: "其中可运营",
|
||||
value: selected.filter((v) =>
|
||||
@@ -420,9 +420,9 @@ export function assetResult(
|
||||
).length,
|
||||
unit: "台",
|
||||
},
|
||||
]),
|
||||
] : []),
|
||||
...(plan.analysis === "briefing" ? [{ label: "其中可运营库存", value: selected.filter(v => isInventoryVehicle(v) && ["3", "可运营"].includes(v.operationStatus || "")).length, unit: "台" }, { label: "其中待运营库存", value: selected.filter(v => isInventoryVehicle(v) && ["4", "待运营"].includes(v.operationStatus || "")).length, unit: "台" }] : []),
|
||||
...(!["all", "operating"].includes(plan.category) || plan.analysis === "briefing"
|
||||
...(plan.category === "inventory" || plan.analysis === "briefing"
|
||||
? [
|
||||
{ label: "其中异动", value: abnormal, unit: "台" },
|
||||
{ label: "其中待交车", value: pending, unit: "台" },
|
||||
|
||||
@@ -23,3 +23,30 @@ test("opening inventory from a briefing leaves briefing mode and keeps inherited
|
||||
const next=applyAction(briefing,{type:"category",category:"inventory"},result(briefing));
|
||||
assert.equal(next.category,"inventory");assert.equal(next.analysis,"distribution");assert.deepEqual(next.filters,briefing.filters);
|
||||
});
|
||||
|
||||
test("simple counts answer directly, including verified zero, while preserving scope", async () => {
|
||||
const { simplifyCountAnswer } = await import("./presentation.js");
|
||||
for (const count of [0, 7]) {
|
||||
const r = result({...plan, category:"abnormal", groupBy:"none", view:"chart"});
|
||||
r.outcome = count ? "complete" : "empty";
|
||||
r.metrics = [{label:"异动车辆",value:count,unit:"台"},{label:"其中待交车",value:0,unit:"台"}];
|
||||
r.scope = resultScope(r.plan,"测试公司");
|
||||
assert.equal(simplifyCountAnswer(r,"当前有多少异动车辆?"),true);
|
||||
assert.match(r.summary,new RegExp(`异动车辆为 ${count} 台`));
|
||||
assert.match(r.summary,/测试公司.*4.5T.*嘉兴市/);
|
||||
assert.equal(r.metrics.length,1);
|
||||
assert.equal(r.outcome,"complete");
|
||||
assert.deepEqual(r.suggestions,[]);
|
||||
}
|
||||
});
|
||||
|
||||
test("counts never collapse analysis requests, missing metrics or incomplete results", async () => {
|
||||
const { simplifyCountAnswer } = await import("./presentation.js");
|
||||
const make = () => ({...result({...plan,category:"abnormal",groupBy:"none",view:"chart"}), outcome:"complete" as const,metrics:[{label:"异动车辆",value:7,unit:"台"}]});
|
||||
for (const question of ["各区域有多少异动车辆", "异动和待交车分别多少", "异动有多少,占比多少", "有多少异动车辆,查看明细", "分析异动车辆数量变化"]) {
|
||||
assert.equal(simplifyCountAnswer(make(),question),false,question);
|
||||
}
|
||||
for (const override of [{outcome:"partial" as const},{warning:"源数据未接入"},{metrics:[]},{plan:{...make().plan,view:"list" as const}},{plan:{...make().plan,groupBy:"city" as const}}]) {
|
||||
assert.equal(simplifyCountAnswer({...make(),...override},"当前有多少异动车辆?"),false);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,31 @@
|
||||
import { CATEGORY_LABELS, GROUP_LABELS, type AssistantResult, type QueryPlan } from "../../../shared/assets/assistant.js";
|
||||
|
||||
/** Only simplify verified, single-scope count queries; never infer zero from missing data. */
|
||||
export function simplifyCountAnswer(result: AssistantResult, question: string): boolean {
|
||||
const p = result.plan;
|
||||
const metric = result.metrics.find(m => m.label === CATEGORY_LABELS[p.category] && m.unit === "台");
|
||||
if (result.sections?.length || p.kind !== "assets" || p.responseMode ||
|
||||
p.groupBy !== "none" || p.secondaryGroupBy || p.view !== "chart" ||
|
||||
(p.analysis && p.analysis !== "distribution") || p.comparePrevious || p.topN ||
|
||||
result.warning || result.unanswered?.length || result.gap ||
|
||||
!["complete", "empty"].includes(result.outcome || "") ||
|
||||
!metric || !Number.isFinite(metric.value) || metric.value < 0 ||
|
||||
!/(多少|几台|几辆|数量|总数)/.test(question) ||
|
||||
/(分别|各|分布|占比|比例|运营率|对比|比较|趋势|变化|原因|为什么|分析|明细|清单|列表|表格|图表|排行|最多|最少|以及|和|与|、)/.test(question)) return false;
|
||||
const scope = result.scope || resultScope(p, null);
|
||||
const filters = scope.filters.slice(1);
|
||||
result.conciseCount = true;
|
||||
result.summary = `${scope.company}${filters.length ? `,${filters.join(",")}` : ""},当前${CATEGORY_LABELS[p.category]}为 ${metric.value.toLocaleString()} 台。`;
|
||||
result.metrics = [metric];
|
||||
result.suggestions = [];
|
||||
// A successful zero count is an answer, not a query failure or missing dataset.
|
||||
result.outcome = "complete";
|
||||
delete result.presentation;
|
||||
delete result.presentationStatus;
|
||||
delete result.presentationWarning;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function resultScope(plan: QueryPlan, subject: string | null): NonNullable<AssistantResult["scope"]> {
|
||||
const filters = [
|
||||
plan.kind === "flow" ? "交退换车业务" : CATEGORY_LABELS[plan.category],
|
||||
|
||||
Reference in New Issue
Block a user