feat(ssa): Complete QPER architecture - Query, Planner, Execute, Reflection layers
Implement the full QPER intelligent analysis pipeline: - Phase E+: Block-based standardization for all 7 R tools, DynamicReport renderer, Word export enhancement - Phase Q: LLM intent parsing with dynamic Zod validation against real column names, ClarificationCard component, DataProfile is_id_like tagging - Phase P: ConfigLoader with Zod schema validation and hot-reload API, DecisionTableService (4-dimension matching), FlowTemplateService with EPV protection, PlannedTrace audit output - Phase R: ReflectionService with statistical slot injection, sensitivity analysis conflict rules, ConclusionReport with section reveal animation, conclusion caching API, graceful R error classification End-to-end test: 40/40 passed across two complete analysis scenarios. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -13,6 +13,10 @@ import { logger } from '../../../common/logging/index.js';
|
||||
import { workflowPlannerService } from '../services/WorkflowPlannerService.js';
|
||||
import { workflowExecutorService } from '../services/WorkflowExecutorService.js';
|
||||
import { dataProfileService } from '../services/DataProfileService.js';
|
||||
import { queryService } from '../services/QueryService.js';
|
||||
import { reflectionService } from '../services/ReflectionService.js';
|
||||
import { prisma } from '../../../config/database.js';
|
||||
import { cache } from '../../../common/cache/index.js';
|
||||
|
||||
// 请求类型定义
|
||||
interface PlanWorkflowBody {
|
||||
@@ -74,6 +78,109 @@ export default async function workflowRoutes(app: FastifyInstance) {
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* POST /workflow/intent
|
||||
* Phase Q: LLM 意图理解 — 解析用户自然语言为结构化 ParsedQuery
|
||||
*/
|
||||
app.post<{ Body: PlanWorkflowBody }>(
|
||||
'/intent',
|
||||
async (request, reply) => {
|
||||
const { sessionId, userQuery } = request.body;
|
||||
|
||||
if (!sessionId || !userQuery) {
|
||||
return reply.status(400).send({
|
||||
success: false,
|
||||
error: 'sessionId and userQuery are required'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info('[SSA:API] Parsing intent', { sessionId, userQuery });
|
||||
|
||||
const parsed = await queryService.parseIntent(userQuery, sessionId);
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
intent: parsed,
|
||||
needsClarification: parsed.needsClarification,
|
||||
clarificationCards: parsed.clarificationCards || [],
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
logger.error('[SSA:API] Intent parsing failed', {
|
||||
sessionId,
|
||||
error: error.message
|
||||
});
|
||||
|
||||
return reply.status(500).send({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* POST /workflow/clarify
|
||||
* Phase Q: 用户回答追问卡片后,补全 ParsedQuery 并重新规划
|
||||
*/
|
||||
app.post<{ Body: { sessionId: string; userQuery: string; selections: Record<string, string> } }>(
|
||||
'/clarify',
|
||||
async (request, reply) => {
|
||||
const { sessionId, userQuery, selections } = request.body;
|
||||
|
||||
if (!sessionId) {
|
||||
return reply.status(400).send({
|
||||
success: false,
|
||||
error: 'sessionId is required'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info('[SSA:API] Processing clarification', { sessionId, selections });
|
||||
|
||||
// 将用户选择拼接到原始 query 中,重新走 intent 解析
|
||||
const selectionText = Object.entries(selections)
|
||||
.map(([key, value]) => `${key}: ${value}`)
|
||||
.join('; ');
|
||||
const enrichedQuery = userQuery
|
||||
? `${userQuery}(补充说明:${selectionText})`
|
||||
: selectionText;
|
||||
|
||||
const parsed = await queryService.parseIntent(enrichedQuery, sessionId);
|
||||
|
||||
// 如果这次置信度足够,直接生成工作流计划
|
||||
if (!parsed.needsClarification) {
|
||||
const plan = await workflowPlannerService.planWorkflow(sessionId, enrichedQuery);
|
||||
return reply.send({
|
||||
success: true,
|
||||
intent: parsed,
|
||||
plan,
|
||||
needsClarification: false,
|
||||
});
|
||||
}
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
intent: parsed,
|
||||
needsClarification: true,
|
||||
clarificationCards: parsed.clarificationCards || [],
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
logger.error('[SSA:API] Clarification failed', {
|
||||
sessionId,
|
||||
error: error.message
|
||||
});
|
||||
|
||||
return reply.status(500).send({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* POST /workflow/:workflowId/execute
|
||||
* 执行工作流
|
||||
@@ -329,6 +436,73 @@ export default async function workflowRoutes(app: FastifyInstance) {
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* GET /workflow/sessions/:sessionId/conclusion
|
||||
* 获取会话的分析结论(优先返回缓存,无缓存则从 workflow 结果重新生成)
|
||||
*/
|
||||
app.get<{ Params: { sessionId: string } }>(
|
||||
'/sessions/:sessionId/conclusion',
|
||||
async (request, reply) => {
|
||||
const { sessionId } = request.params;
|
||||
|
||||
try {
|
||||
// 查找该 session 最新的 completed workflow
|
||||
const workflow = await prisma.ssaWorkflow.findFirst({
|
||||
where: { sessionId: sessionId, status: { in: ['completed', 'partial'] } },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
if (!workflow) {
|
||||
return reply.status(404).send({
|
||||
success: false,
|
||||
error: 'No completed workflow found for this session',
|
||||
});
|
||||
}
|
||||
|
||||
// 检查缓存
|
||||
const cacheKey = `ssa:conclusion:${workflow.id}`;
|
||||
const cached = await cache.get(cacheKey);
|
||||
if (cached) {
|
||||
return reply.send({ success: true, conclusion: cached, source: 'cache' });
|
||||
}
|
||||
|
||||
// 无缓存:获取 workflow steps 结果并重新生成
|
||||
const steps = await prisma.ssaWorkflowStep.findMany({
|
||||
where: { workflowId: workflow.id },
|
||||
orderBy: { stepOrder: 'asc' },
|
||||
});
|
||||
|
||||
const results = steps.map((s: any) => ({
|
||||
stepOrder: s.stepOrder,
|
||||
toolCode: s.toolCode,
|
||||
toolName: s.toolName,
|
||||
status: s.status,
|
||||
result: s.outputResult,
|
||||
reportBlocks: s.reportBlocks,
|
||||
executionMs: s.executionMs || 0,
|
||||
}));
|
||||
|
||||
const workflowPlan = workflow.workflowPlan as any;
|
||||
const conclusion = await reflectionService.reflect(
|
||||
{
|
||||
workflowId: workflow.id,
|
||||
goal: workflowPlan?.goal || '统计分析',
|
||||
title: workflowPlan?.title,
|
||||
methodology: workflowPlan?.methodology,
|
||||
plannedTrace: workflowPlan?.planned_trace,
|
||||
},
|
||||
results,
|
||||
);
|
||||
|
||||
return reply.send({ success: true, conclusion, source: conclusion.source });
|
||||
|
||||
} catch (error: any) {
|
||||
logger.error('[SSA:API] Get conclusion failed', { sessionId, error: error.message });
|
||||
return reply.status(500).send({ success: false, error: error.message });
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user