feat(asset-ai): generate contextual presentations and drilldown plans
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
@@ -7,6 +7,9 @@ import { vehicleRepository } from "../vehicles/repository.js";
|
||||
import { visibleVehicles } from "./model.js";
|
||||
import { initialPlan, planConversation } from "./planner.js";
|
||||
import { execute, applyAction } from "./execute.js";
|
||||
import { executeComposition } from "./composition.js";
|
||||
import { generatePresentation } from "./generative.js";
|
||||
import { applyGeneratedAction } from "./generated-action.js";
|
||||
import {
|
||||
getSession,
|
||||
getNode,
|
||||
@@ -16,6 +19,7 @@ import {
|
||||
acquire,
|
||||
conversationContext,
|
||||
} from "./session.js";
|
||||
import { resultScope, followupSuggestions } from "./presentation.js";
|
||||
import { gapStore } from "./gaps.js";
|
||||
import {
|
||||
CATEGORY_LABELS,
|
||||
@@ -23,6 +27,7 @@ import {
|
||||
GROUP_LABELS,
|
||||
type AssistantRequest,
|
||||
type AssistantResult,
|
||||
type AnalysisQuery,
|
||||
type GapReason,
|
||||
type GapStatus,
|
||||
} from "../../../shared/assets/assistant.js";
|
||||
@@ -248,20 +253,26 @@ router.post("/ask", async (c) => {
|
||||
const context = conversationContext(currentSession, baseNode);
|
||||
const previous = context.previous;
|
||||
let plan = previous || initialPlan();
|
||||
let queries: AnalysisQuery[] | undefined;
|
||||
let warning: string | undefined;
|
||||
let reply: string | undefined;
|
||||
let answerKind: "answer" | "clarify" | undefined;
|
||||
let suggestions: string[] | undefined;
|
||||
let unanswered: AssistantResult["unanswered"];
|
||||
let correction: string | undefined;
|
||||
let engine: "deepseek" | "local" = "local";
|
||||
let parent = baseNode;
|
||||
if (input.action) {
|
||||
if (!previous) throw new Error("请先完成一次统计。");
|
||||
plan = applyAction(
|
||||
previous,
|
||||
input.action,
|
||||
await execute(vehicles, previous),
|
||||
);
|
||||
demand = `${CATEGORY_LABELS[previous.category]} ${GROUP_LABELS[previous.groupBy]} ${input.action.type}`;
|
||||
const currentData = await execute(vehicles, previous);
|
||||
if (input.action.type === "explore") {
|
||||
const exploration = applyGeneratedAction(previous, input.action, currentData, baseNode?.presentation);
|
||||
plan = exploration.plan;
|
||||
demand = exploration.label;
|
||||
} else {
|
||||
plan = applyAction(previous, input.action, currentData);
|
||||
demand = `${CATEGORY_LABELS[previous.category]} ${GROUP_LABELS[previous.groupBy]} ${input.action.type}`;
|
||||
}
|
||||
} else {
|
||||
demand = input
|
||||
.message!.trim()
|
||||
@@ -277,11 +288,14 @@ router.post("/ask", async (c) => {
|
||||
signal: controller.signal,
|
||||
});
|
||||
plan = decision.plan;
|
||||
queries = decision.queries;
|
||||
reason = decision.reason;
|
||||
warning = decision.warning;
|
||||
reply = decision.reply;
|
||||
answerKind = decision.answerKind;
|
||||
suggestions = decision.suggestions;
|
||||
unanswered = decision.unanswered;
|
||||
correction = decision.correction;
|
||||
engine = decision.engine;
|
||||
if (decision.baseResultId)
|
||||
parent = getNode(currentSession, decision.baseResultId);
|
||||
@@ -294,8 +308,12 @@ router.post("/ask", async (c) => {
|
||||
? "组织回答与下一步建议"
|
||||
: "查询数据并计算统计结果",
|
||||
});
|
||||
let result: AssistantResult =
|
||||
plan.kind === "clarify"
|
||||
// Reserve enough session slots before executing or publishing any part of an answer.
|
||||
if (currentSession.nodes.size + 1 + (queries?.length || 0) > 100)
|
||||
throw new Error("本次对话分析步骤已满,请新建对话。");
|
||||
let result: AssistantResult = queries?.length
|
||||
? await executeComposition(vehicles, queries, input.subject, controller.signal)
|
||||
: plan.kind === "clarify"
|
||||
? {
|
||||
plan,
|
||||
title:
|
||||
@@ -315,8 +333,21 @@ router.post("/ask", async (c) => {
|
||||
}
|
||||
: await execute(vehicles, plan);
|
||||
if (controller.signal.aborted) return;
|
||||
plan = result.plan;
|
||||
if (result.unanswered?.length) {
|
||||
unanswered = [...(unanswered || []), ...result.unanswered];
|
||||
reason ||= "generation";
|
||||
}
|
||||
result.engine = engine;
|
||||
if (suggestions?.length) result.suggestions = suggestions;
|
||||
if (plan.kind === "clarify" && suggestions?.length) result.suggestions = suggestions;
|
||||
result.scope = resultScope(plan, input.subject);
|
||||
result.suggestions = followupSuggestions(result);
|
||||
if (plan.kind === "clarify" && !result.sections) result.outcome = reason === "unsupported" ? "unsupported" : answerKind === "answer" ? "help" : "clarification";
|
||||
if (unanswered?.length) {
|
||||
result.unanswered = unanswered;
|
||||
if (plan.kind !== "clarify") result.outcome = "partial";
|
||||
}
|
||||
if (correction) result.correction = { message: correction, resultId: parent ? trail(currentSession, parent).at(-1)?.resultId : undefined };
|
||||
if (warning) result.warning = warning;
|
||||
if (plan.kind !== "clarify") {
|
||||
const includes = Object.entries(plan.filters).map(
|
||||
@@ -378,6 +409,28 @@ router.post("/ask", async (c) => {
|
||||
demand = `${demand.slice(0, 400)}(承接:${scope})`.slice(0, 600);
|
||||
}
|
||||
}
|
||||
if (result.plan.kind !== "clarify" || result.sections?.some(section => section.result)) {
|
||||
await emit({ type: "step", label: "根据结果设计展示与下钻路径" });
|
||||
await generatePresentation(result, input.message || `${baseNode?.question || "当前分析"};${demand}`, controller.signal);
|
||||
if (result.presentationStatus === "generated" || result.sections?.some(section => section.result?.presentationStatus === "generated")) {
|
||||
engine = "deepseek";
|
||||
result.engine = engine;
|
||||
}
|
||||
}
|
||||
if (controller.signal.aborted) return;
|
||||
for (const item of result.sections ? result.sections.flatMap(section => section.result ? [section.result] : []) : [result]) {
|
||||
if (!item.presentation && item.presentationWarning)
|
||||
item.warning = [item.warning, item.presentationWarning].filter(Boolean).join(" ");
|
||||
}
|
||||
const missingPresentation = result.sections
|
||||
? result.sections.filter(section => section.result?.presentationStatus === "fallback").map(section => ({ question: `${section.question}(定制展示与下钻)`, reason: section.result!.presentationWarning || "展示方案未能生成,已保留统计结果。" }))
|
||||
: result.presentationStatus === "fallback" ? [{ question: `${input.message || demand}(定制展示与下钻)`, reason: result.presentationWarning || "展示方案未能生成,已保留统计结果。" }] : [];
|
||||
if (missingPresentation.length) {
|
||||
unanswered = [...(unanswered || []), ...missingPresentation];
|
||||
result.unanswered = unanswered;
|
||||
result.outcome = "partial";
|
||||
reason ||= "generation";
|
||||
}
|
||||
const node = addNode(currentSession, {
|
||||
plan,
|
||||
parent: parent?.id,
|
||||
@@ -386,14 +439,18 @@ router.post("/ask", async (c) => {
|
||||
reason,
|
||||
question: input.message || demand,
|
||||
answer: result.summary,
|
||||
container: !!result.sections,
|
||||
presentation: result.presentation,
|
||||
resultContext:
|
||||
plan.kind === "clarify"
|
||||
? undefined
|
||||
: {
|
||||
groups: result.groups.slice(0, 100),
|
||||
inventoryBreakdown: result.inventoryBreakdown,
|
||||
metrics: result.metrics,
|
||||
insights: result.insights,
|
||||
summary: result.summary,
|
||||
analyses: parent?.resultContext?.analyses,
|
||||
},
|
||||
});
|
||||
result = {
|
||||
@@ -402,15 +459,55 @@ router.post("/ask", async (c) => {
|
||||
resultId: node.id,
|
||||
breadcrumbs: trail(currentSession, node),
|
||||
};
|
||||
if (reason) {
|
||||
if (!result.sections && node.resultContext?.analyses) {
|
||||
node.resultContext.analyses = node.resultContext.analyses.map(analysis => analysis.id === parent?.id ? {
|
||||
id: node.id, title: result.title, plan, summary: result.summary, metrics: result.metrics,
|
||||
} : analysis);
|
||||
result.analyses = node.resultContext.analyses;
|
||||
}
|
||||
if (result.sections) {
|
||||
for (const section of result.sections) {
|
||||
if (!section.result) continue;
|
||||
const child = section.result;
|
||||
const childNode = addNode(currentSession, {
|
||||
plan: child.plan, parent: node.id, label: section.title,
|
||||
demand: `${section.question}(原问题:${demand})`.slice(0, 600),
|
||||
question: section.question, answer: child.summary,
|
||||
presentation: child.presentation,
|
||||
resultContext: {
|
||||
groups: child.groups.slice(0, 100), metrics: child.metrics,
|
||||
insights: child.insights, summary: child.summary,
|
||||
inventoryBreakdown: child.inventoryBreakdown,
|
||||
},
|
||||
});
|
||||
child.engine = engine;
|
||||
child.sessionId = currentSession.id;
|
||||
child.resultId = childNode.id;
|
||||
child.breadcrumbs = trail(currentSession, childNode);
|
||||
}
|
||||
result.analyses = result.sections.flatMap(section => section.result ? [{
|
||||
id: section.result.resultId!, title: section.title, plan: section.result.plan,
|
||||
summary: section.result.summary, metrics: section.result.metrics,
|
||||
}] : []);
|
||||
if (node.resultContext) node.resultContext.analyses = result.analyses;
|
||||
for (const section of result.sections) {
|
||||
if (section.result?.resultId) {
|
||||
const childNode = getNode(currentSession, section.result.resultId)!;
|
||||
if (childNode.resultContext) childNode.resultContext.analyses = result.analyses;
|
||||
}
|
||||
}
|
||||
}
|
||||
const gapReason = reason || (unanswered?.length ? "unsupported" : undefined);
|
||||
if (gapReason) {
|
||||
if (unanswered?.length) demand = `${unanswered.map(item => `${item.question}:${item.reason}`).join(";")}(原问题:${demand})`.slice(0, 600);
|
||||
let recorded = false;
|
||||
try {
|
||||
await gapStore.record(user.userId, demand, reason);
|
||||
await gapStore.record(user.userId, demand, gapReason);
|
||||
recorded = true;
|
||||
} catch {
|
||||
/* Report failure explicitly, never pretend it was persisted. */
|
||||
}
|
||||
result.gap = { recorded, reason, demand };
|
||||
result.gap = { recorded, reason: gapReason, demand };
|
||||
}
|
||||
await emit({
|
||||
type: "step",
|
||||
|
||||
Reference in New Issue
Block a user