feat(ssa): Complete Phase V-A editable analysis plan variables
Features: - Add editable variable selection in workflow plan (SingleVarSelect + MultiVarTags) - Implement 3-layer flexible interception (warning bar + icon + blocking dialog) - Add tool_param_constraints.json for 12 statistical tools parameter validation - Add PATCH /workflow/:id/params API with Zod structural validation - Implement synchronous parameter sync before execution (Promise chaining) - Fix LLM hallucination by strict system prompt constraints - Fix DynamicReport object-based rows compatibility (R baseline_table) - Fix Word export row.map error with same normalization logic - Restore inferGroupingVar for smart default variable selection - Add ReactMarkdown rendering in SSAChatPane - Update SSA module status document to v3.5 Modified files: - backend: workflow.routes, ChatHandlerService, SystemPromptService, FlowTemplateService - frontend: WorkflowTimeline, SSAWorkspacePane, DynamicReport, SSAChatPane, ssaStore, ssa.css - config: tool_param_constraints.json (new) - docs: SSA status doc, team review reports Tested: Cohort study end-to-end execution + report export verified Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -32,25 +32,34 @@ const PROMPTS: PromptDef[] = [
|
||||
name: 'SSA 基础角色定义',
|
||||
description: 'Phase II — 对话层 LLM 的固定角色 System Prompt,始终作为 [1] 段注入',
|
||||
variables: [],
|
||||
content: `你是 SSA-Pro 智能统计分析助手,专注于临床研究统计分析领域。
|
||||
content: `你是 SSA-Pro 智能统计分析助手。你的职责是**规划、解释和沟通**,而非计算。
|
||||
|
||||
## 你的身份
|
||||
## 你的身份与职能边界
|
||||
|
||||
你是一位经验丰富的生物统计顾问,服务于临床研究人员和医学院师生。你不仅能执行统计分析,更重要的是帮助用户理解数据、选择方法、解读结果。
|
||||
你是「分析规划者」和「结果解读者」,不是「计算引擎」。
|
||||
系统后端有独立的 R 统计计算引擎(R-Engine),所有统计计算均由 R 引擎完成并返回真实结果。
|
||||
|
||||
## 核心能力
|
||||
### 你可以做的:
|
||||
- 理解用户的分析需求,识别意图
|
||||
- 推荐合适的统计方法,解释选择理由和前提条件
|
||||
- 制定分析方案(选择工具、参数配置)
|
||||
- 解读 R 引擎返回的**真实**结果,用通俗语言解释给用户
|
||||
- 识别 PICO 结构,解读数据特征
|
||||
|
||||
1. **数据理解** — 解读数据结构、变量类型、缺失模式、异常值和分布特征
|
||||
2. **方法推荐** — 根据研究设计和数据特征推荐合适的统计方法,说明前提条件和替代方案
|
||||
3. **结果解读** — 用通俗易懂的语言解释 p 值、置信区间、效应量等统计概念
|
||||
4. **PICO 识别** — 识别研究的人群、干预、对照和结局变量
|
||||
### 绝对禁止(铁律):
|
||||
- **禁止编造或生成任何数值结果**(P值、均值、标准差、置信区间、检验统计量、OR、RR 等)
|
||||
- **禁止生成模拟/假设的分析结果表格**
|
||||
- **禁止在 R 引擎尚未执行时预测结果**
|
||||
- 如果还没有 R 引擎的执行结果,只能说明方案状态(如"方案已确认,即将启动分析")
|
||||
|
||||
**关键原则:没有 R 引擎的真实输出 → 绝不回答任何具体数值。违反此原则将导致临床研究的严重误导。**
|
||||
|
||||
## 沟通原则
|
||||
|
||||
- 使用中文回复
|
||||
- 语言专业但不晦涩,避免不必要的术语堆砌
|
||||
- 分点作答,条理清晰
|
||||
- 对不确定的内容如实说明,不编造数据或结论
|
||||
- 对不确定的内容如实说明
|
||||
- 回复简洁聚焦,不要过度发散
|
||||
- 当用户的问题涉及其数据时,优先引用数据上下文中的实际信息`,
|
||||
modelConfig: { model: 'deepseek-v3', temperature: 0.7, maxTokens: 2000 },
|
||||
@@ -69,7 +78,8 @@ const PROMPTS: PromptDef[] = [
|
||||
2. 不要主动建议"帮你执行分析",除非用户明确要求
|
||||
3. 如果问题与用户数据相关,引用数据上下文中的具体信息
|
||||
4. 如果问题超出统计分析范围,礼貌说明并引导回统计话题
|
||||
5. 回复简洁,不超过 300 字`,
|
||||
5. 回复简洁,不超过 300 字
|
||||
6. 禁止编造用户数据的具体数值(均值、P值等),只有 R 引擎返回的才是真实数据`,
|
||||
modelConfig: { model: 'deepseek-v3', temperature: 0.7, maxTokens: 1500 },
|
||||
},
|
||||
{
|
||||
@@ -104,7 +114,8 @@ const PROMPTS: PromptDef[] = [
|
||||
2. 必须说明:推荐方法、选择理由、前提条件(如正态性要求)
|
||||
3. 提供至少一个替代方案(如非参数替代)
|
||||
4. 不要直接执行分析,等待用户确认方案后再执行
|
||||
5. 如果信息不足以做出推荐,主动追问缺少的关键信息`,
|
||||
5. 如果信息不足以做出推荐,主动追问缺少的关键信息
|
||||
6. 禁止给出假设的分析结果数值来论证方法优劣`,
|
||||
modelConfig: { model: 'deepseek-v3', temperature: 0.7, maxTokens: 2000 },
|
||||
},
|
||||
{
|
||||
@@ -112,16 +123,21 @@ const PROMPTS: PromptDef[] = [
|
||||
name: 'SSA analyze 意图指令',
|
||||
description: 'Phase II — analyze 意图的指令段,用于播报 QPER 执行进度',
|
||||
variables: [],
|
||||
content: `## 当前任务:分析执行播报
|
||||
content: `## 当前任务:分析规划与执行协调
|
||||
|
||||
系统正在执行统计分析(通过 QPER 引擎),你的任务是向用户简要说明进展。
|
||||
你正在协助用户进行统计分析的规划和协调。
|
||||
|
||||
规则:
|
||||
1. 如果提供了工具执行结果,用通俗语言向用户解释关键发现
|
||||
2. 避免复制粘贴原始 R 输出,提炼核心信息(p 值、效应量、置信区间)
|
||||
3. 使用用户能理解的语言,必要时解释统计术语
|
||||
4. 回复控制在 200 字以内,详细结果可在分析报告中查看
|
||||
5. 如果执行出错,简要说明原因并建议解决方案`,
|
||||
### 核心规则:
|
||||
1. **你的职责是解释分析方案和方法选择理由**,而非执行计算
|
||||
2. **所有数值结果只能引用 R 引擎返回的真实输出**(会以"工具执行结果"的形式提供给你)
|
||||
3. 如果提供了 R 引擎的真实执行结果,用通俗语言向用户解读关键发现
|
||||
4. 如果 R 引擎尚未执行或未返回结果,**只能说明方案状态**(如"方案已确认,正在启动分析")
|
||||
5. 回复控制在 200 字以内
|
||||
|
||||
### 绝对禁止:
|
||||
- 禁止自行生成 P 值、均值、标准差、置信区间、检验统计量
|
||||
- 禁止生成分析结果表格(除非表格数据来自 R 引擎输出)
|
||||
- 禁止在没有 R 引擎输出时编造任何数值`,
|
||||
modelConfig: { model: 'deepseek-v3', temperature: 0.5, maxTokens: 1000 },
|
||||
},
|
||||
{
|
||||
@@ -134,7 +150,7 @@ const PROMPTS: PromptDef[] = [
|
||||
用户想深入讨论已有的分析结果。
|
||||
|
||||
规则:
|
||||
1. 基于上方注入的分析结果,帮助用户深入解读
|
||||
1. **仅基于 R 引擎返回的真实分析结果**进行解读,不要补充或编造 R 引擎未返回的数值
|
||||
2. 解释统计量的含义(如 p 值的正确解读、置信区间的意义)
|
||||
3. 讨论结果的临床意义(不仅是统计显著性)
|
||||
4. 指出分析的局限性和注意事项
|
||||
|
||||
52
backend/src/modules/ssa/config/tool_param_constraints.json
Normal file
52
backend/src/modules/ssa/config/tool_param_constraints.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"ST_DESCRIPTIVE": {
|
||||
"variables": { "paramType": "multi", "requiredType": "any", "hint": "选择需要描述的变量" },
|
||||
"group_var": { "paramType": "single", "requiredType": "categorical", "hint": "分组变量(可选)" }
|
||||
},
|
||||
"ST_T_TEST_IND": {
|
||||
"group_var": { "paramType": "single", "requiredType": "categorical", "maxLevels": 2, "hint": "T检验要求二分类分组变量" },
|
||||
"value_var": { "paramType": "single", "requiredType": "numeric", "hint": "T检验要求连续型因变量" }
|
||||
},
|
||||
"ST_MANN_WHITNEY": {
|
||||
"group_var": { "paramType": "single", "requiredType": "categorical", "maxLevels": 2, "hint": "Mann-Whitney检验要求二分类分组变量" },
|
||||
"value_var": { "paramType": "single", "requiredType": "numeric", "hint": "要求连续型因变量" }
|
||||
},
|
||||
"ST_T_TEST_PAIRED": {
|
||||
"before_var": { "paramType": "single", "requiredType": "numeric", "hint": "前测变量应为连续型" },
|
||||
"after_var": { "paramType": "single", "requiredType": "numeric", "hint": "后测变量应为连续型" }
|
||||
},
|
||||
"ST_WILCOXON": {
|
||||
"before_var": { "paramType": "single", "requiredType": "numeric", "hint": "前测变量应为连续型" },
|
||||
"after_var": { "paramType": "single", "requiredType": "numeric", "hint": "后测变量应为连续型" }
|
||||
},
|
||||
"ST_CHI_SQUARE": {
|
||||
"var1": { "paramType": "single", "requiredType": "categorical", "hint": "卡方检验要求分类变量" },
|
||||
"var2": { "paramType": "single", "requiredType": "categorical", "hint": "卡方检验要求分类变量" }
|
||||
},
|
||||
"ST_FISHER": {
|
||||
"var1": { "paramType": "single", "requiredType": "categorical", "hint": "Fisher检验要求分类变量" },
|
||||
"var2": { "paramType": "single", "requiredType": "categorical", "hint": "Fisher检验要求分类变量" }
|
||||
},
|
||||
"ST_CORRELATION": {
|
||||
"var_x": { "paramType": "single", "requiredType": "numeric", "hint": "相关分析要求连续型变量" },
|
||||
"var_y": { "paramType": "single", "requiredType": "numeric", "hint": "相关分析要求连续型变量" }
|
||||
},
|
||||
"ST_LOGISTIC_BINARY": {
|
||||
"outcome_var": { "paramType": "single", "requiredType": "categorical", "maxLevels": 2, "hint": "二元Logistic回归要求二分类结局变量" },
|
||||
"predictors": { "paramType": "multi", "requiredType": "any", "hint": "预测变量" },
|
||||
"confounders": { "paramType": "multi", "requiredType": "any", "hint": "混杂因素(可选)" }
|
||||
},
|
||||
"ST_LINEAR_REG": {
|
||||
"outcome_var": { "paramType": "single", "requiredType": "numeric", "hint": "线性回归要求连续型结局变量" },
|
||||
"predictors": { "paramType": "multi", "requiredType": "any", "hint": "预测变量" },
|
||||
"confounders": { "paramType": "multi", "requiredType": "any", "hint": "混杂因素(可选)" }
|
||||
},
|
||||
"ST_ANOVA_ONE": {
|
||||
"group_var": { "paramType": "single", "requiredType": "categorical", "minLevels": 3, "hint": "ANOVA要求3组及以上分组变量" },
|
||||
"value_var": { "paramType": "single", "requiredType": "numeric", "hint": "要求连续型因变量" }
|
||||
},
|
||||
"ST_BASELINE_TABLE": {
|
||||
"group_var": { "paramType": "single", "requiredType": "categorical", "minLevels": 2, "maxLevels": 5, "hint": "基线表需要分类分组变量" },
|
||||
"analyze_vars": { "paramType": "multi", "requiredType": "any", "hint": "选择需要分析的变量" }
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
*/
|
||||
|
||||
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { z } from 'zod';
|
||||
import { logger } from '../../../common/logging/index.js';
|
||||
import { workflowPlannerService } from '../services/WorkflowPlannerService.js';
|
||||
import { workflowExecutorService } from '../services/WorkflowExecutorService.js';
|
||||
@@ -372,6 +373,143 @@ export default async function workflowRoutes(app: FastifyInstance) {
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* PATCH /workflow/:workflowId/params
|
||||
* Phase V: 批量更新 workflow step 参数(变量可编辑化)
|
||||
*
|
||||
* Zod 结构校验防火墙:
|
||||
* - 结构非法(字段类型错误、必填字段缺失)→ 400 Bad Request
|
||||
* - 统计学不合理但结构合法 → 放行,交给 R 引擎 tryCatch
|
||||
*/
|
||||
app.patch<{ Params: { workflowId: string }; Body: { steps: Array<{ stepOrder: number; params: Record<string, any> }> } }>(
|
||||
'/:workflowId/params',
|
||||
async (request, reply) => {
|
||||
const { workflowId } = request.params;
|
||||
const { steps } = request.body;
|
||||
|
||||
const PatchStepSchema = z.object({
|
||||
stepOrder: z.number().int().positive(),
|
||||
params: z.record(z.string(), z.unknown()),
|
||||
});
|
||||
const PatchBodySchema = z.object({
|
||||
steps: z.array(PatchStepSchema).min(1),
|
||||
});
|
||||
|
||||
const validation = PatchBodySchema.safeParse({ steps });
|
||||
if (!validation.success) {
|
||||
return reply.status(400).send({
|
||||
success: false,
|
||||
error: 'Invalid request body',
|
||||
details: validation.error.flatten(),
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const workflow = await prisma.ssaWorkflow.findUnique({
|
||||
where: { id: workflowId },
|
||||
select: { id: true, status: true, sessionId: true },
|
||||
});
|
||||
|
||||
if (!workflow) {
|
||||
return reply.status(404).send({ success: false, error: 'Workflow not found' });
|
||||
}
|
||||
|
||||
if (workflow.status !== 'planned' && workflow.status !== 'pending') {
|
||||
return reply.status(409).send({
|
||||
success: false,
|
||||
error: `Cannot modify params for workflow in '${workflow.status}' state`,
|
||||
});
|
||||
}
|
||||
|
||||
// Validate variable names exist in session data schema
|
||||
const session = await prisma.ssaSession.findUnique({
|
||||
where: { id: workflow.sessionId },
|
||||
select: { dataSchema: true },
|
||||
});
|
||||
const schema = session?.dataSchema as any;
|
||||
const validColumnNames = new Set<string>(
|
||||
(schema?.columns || []).map((c: any) => c.name)
|
||||
);
|
||||
|
||||
for (const stepPatch of validation.data.steps) {
|
||||
for (const [key, value] of Object.entries(stepPatch.params)) {
|
||||
if (typeof value === 'string' && value && !key.startsWith('_')) {
|
||||
if (['group_var', 'outcome_var', 'value_var', 'var_x', 'var_y',
|
||||
'before_var', 'after_var', 'var1', 'var2'].includes(key)) {
|
||||
if (validColumnNames.size > 0 && !validColumnNames.has(value)) {
|
||||
return reply.status(400).send({
|
||||
success: false,
|
||||
error: `Variable '${value}' in step ${stepPatch.stepOrder}.${key} does not exist in the dataset`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Array.isArray(value) && ['analyze_vars', 'predictors', 'variables', 'confounders'].includes(key)) {
|
||||
for (const v of value) {
|
||||
if (typeof v === 'string' && validColumnNames.size > 0 && !validColumnNames.has(v)) {
|
||||
return reply.status(400).send({
|
||||
success: false,
|
||||
error: `Variable '${v}' in step ${stepPatch.stepOrder}.${key} does not exist in the dataset`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update each step's inputParams in the database
|
||||
const updatePromises = validation.data.steps.map((stepPatch) =>
|
||||
prisma.ssaWorkflowStep.updateMany({
|
||||
where: {
|
||||
workflowId,
|
||||
stepOrder: stepPatch.stepOrder,
|
||||
},
|
||||
data: {
|
||||
inputParams: stepPatch.params as any,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(updatePromises);
|
||||
|
||||
// Also update the workflowPlan JSON blob's steps params
|
||||
const currentPlan = await prisma.ssaWorkflow.findUnique({
|
||||
where: { id: workflowId },
|
||||
select: { workflowPlan: true },
|
||||
});
|
||||
if (currentPlan?.workflowPlan) {
|
||||
const plan = currentPlan.workflowPlan as any;
|
||||
if (plan.steps) {
|
||||
for (const stepPatch of validation.data.steps) {
|
||||
const planStep = plan.steps.find((s: any) => s.step_number === stepPatch.stepOrder);
|
||||
if (planStep) {
|
||||
planStep.params = stepPatch.params;
|
||||
}
|
||||
}
|
||||
await prisma.ssaWorkflow.update({
|
||||
where: { id: workflowId },
|
||||
data: { workflowPlan: plan },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('[SSA:API] Workflow params updated', {
|
||||
workflowId,
|
||||
stepsUpdated: validation.data.steps.length,
|
||||
});
|
||||
|
||||
return reply.send({ success: true, stepsUpdated: validation.data.steps.length });
|
||||
|
||||
} catch (error: any) {
|
||||
logger.error('[SSA:API] Patch workflow params failed', {
|
||||
workflowId,
|
||||
error: error.message,
|
||||
});
|
||||
return reply.status(500).send({ success: false, error: error.message });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* POST /workflow/profile
|
||||
* 生成数据画像
|
||||
|
||||
@@ -239,7 +239,8 @@ export class ChatHandlerService {
|
||||
const toolOutputs = [
|
||||
guardToolOutput,
|
||||
planSummary,
|
||||
'[系统提示] 你刚刚为用户制定了上述分析方案。请用自然语言向用户解释这个方案:包括为什么选这些方法、分析步骤的逻辑。不要重复列步骤编号和工具代码,要用用户能理解的语言说明。最后提示用户确认方案后即可执行。',
|
||||
'[系统指令] 你刚刚为用户制定了上述分析方案。请用自然语言向用户解释这个方案:包括为什么选这些方法、分析步骤的逻辑。不要重复列步骤编号和工具代码,要用用户能理解的语言说明。最后提示用户确认方案后即可执行。',
|
||||
'【禁止事项】不要预测、模拟或编造任何分析结果、数值或表格。方案只是计划,R 引擎尚未执行,你不知道结果是什么。',
|
||||
].filter(Boolean).join('\n\n');
|
||||
|
||||
const messages = await conversationService.buildContext(
|
||||
@@ -397,7 +398,9 @@ export class ChatHandlerService {
|
||||
writer: StreamWriter,
|
||||
placeholderMessageId: string,
|
||||
): Promise<HandleResult> {
|
||||
// 清除 pending 状态
|
||||
// 先读取 pending 元数据(含 workflowId),再清除
|
||||
const pending = await askUserService.getPending(sessionId);
|
||||
const pendingMeta = pending?.metadata || {};
|
||||
await askUserService.clearPending(sessionId);
|
||||
|
||||
if (response.action === 'skip') {
|
||||
@@ -423,15 +426,21 @@ export class ChatHandlerService {
|
||||
const selectedValue = response.selectedValues?.[0];
|
||||
|
||||
if (selectedValue === 'confirm_plan') {
|
||||
// Phase IV: 确认分析方案 → 前端将触发 executeWorkflow
|
||||
const workflowId = response.metadata?.workflowId || '';
|
||||
// Phase IV: 确认分析方案 → 前端打开工作区,用户手动点击执行
|
||||
const workflowId = pendingMeta.workflowId || response.metadata?.workflowId || '';
|
||||
const messages = await conversationService.buildContext(
|
||||
sessionId, conversationId, 'analyze',
|
||||
`[系统提示] 用户已确认分析方案(workflow: ${workflowId})。请简要确认:"好的,方案已确认,正在准备执行分析..."。`,
|
||||
[
|
||||
`[系统指令——严格遵守] 用户已确认分析方案(workflow: ${workflowId})。`,
|
||||
'你只需回复一句简短的确认消息,例如:"好的,方案已确认。请在右侧工作区点击「开始执行分析」启动 R 引擎。"',
|
||||
'【铁律】禁止在此回复中生成任何分析结果、表格、P值、统计量、数值。',
|
||||
'你不是计算引擎,所有数值结果将由 R 统计引擎独立计算后返回。',
|
||||
'你的回复不得超过 2 句话。',
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
const result = await conversationService.streamToSSE(messages, writer, {
|
||||
temperature: 0.3, maxTokens: 300,
|
||||
temperature: 0.1, maxTokens: 150,
|
||||
});
|
||||
|
||||
await conversationService.finalizeAssistantMessage(
|
||||
@@ -451,12 +460,12 @@ export class ChatHandlerService {
|
||||
// Phase III: 确认使用推荐方法 → 提示可以开始分析
|
||||
const messages = await conversationService.buildContext(
|
||||
sessionId, conversationId, 'analyze',
|
||||
'[系统提示] 用户已确认使用推荐的统计方法。请简要确认方案,告知用户可以在对话中说"开始分析"或在右侧面板触发执行。',
|
||||
'[系统指令] 用户已确认使用推荐的统计方法。请简要确认方案,告知用户可以在对话中说"开始分析"或在右侧面板触发执行。禁止生成任何数值或假设的分析结果。',
|
||||
);
|
||||
|
||||
const result = await conversationService.streamToSSE(messages, writer, {
|
||||
temperature: 0.5,
|
||||
maxTokens: 800,
|
||||
temperature: 0.3,
|
||||
maxTokens: 500,
|
||||
});
|
||||
|
||||
await conversationService.finalizeAssistantMessage(
|
||||
|
||||
@@ -131,7 +131,7 @@ export class FlowTemplateService {
|
||||
params[key] = query.outcome_var;
|
||||
break;
|
||||
case '{{grouping_var}}':
|
||||
params[key] = query.grouping_var;
|
||||
params[key] = query.grouping_var || this.inferGroupingVar(query, profile);
|
||||
break;
|
||||
case '{{all_predictors}}':
|
||||
params[key] = query.predictor_vars;
|
||||
@@ -150,6 +150,51 @@ export class FlowTemplateService {
|
||||
return { params, epvWarning };
|
||||
}
|
||||
|
||||
/**
|
||||
* 当 LLM 未识别 grouping_var 时,从数据画像中自动推断
|
||||
* 优先选择:二分类变量(排除 outcome_var),最典型的分组/暴露变量
|
||||
*/
|
||||
private inferGroupingVar(query: ParsedQuery, profile?: DataProfile | null): string | null {
|
||||
if (!profile?.columns) return null;
|
||||
|
||||
const excludeVars = new Set<string>();
|
||||
if (query.outcome_var) excludeVars.add(query.outcome_var.toLowerCase());
|
||||
|
||||
const binaryCandidates = profile.columns.filter(c =>
|
||||
c.type === 'categorical' &&
|
||||
c.totalLevels === 2 &&
|
||||
!excludeVars.has(c.name.toLowerCase())
|
||||
);
|
||||
|
||||
if (binaryCandidates.length > 0) {
|
||||
const chosen = binaryCandidates[0].name;
|
||||
logger.info('[SSA:FlowTemplate] Auto-inferred grouping_var', {
|
||||
chosen,
|
||||
candidates: binaryCandidates.map(c => c.name),
|
||||
});
|
||||
return chosen;
|
||||
}
|
||||
|
||||
const categoricalCandidates = profile.columns.filter(c =>
|
||||
c.type === 'categorical' &&
|
||||
c.totalLevels !== undefined &&
|
||||
c.totalLevels >= 2 &&
|
||||
c.totalLevels <= 5 &&
|
||||
!excludeVars.has(c.name.toLowerCase())
|
||||
);
|
||||
|
||||
if (categoricalCandidates.length > 0) {
|
||||
const chosen = categoricalCandidates[0].name;
|
||||
logger.info('[SSA:FlowTemplate] Auto-inferred grouping_var (categorical)', {
|
||||
chosen,
|
||||
});
|
||||
return chosen;
|
||||
}
|
||||
|
||||
logger.warn('[SSA:FlowTemplate] No suitable grouping_var found in profile');
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建默认参数(非 paramsMapping 模板步骤使用)
|
||||
*/
|
||||
|
||||
@@ -123,12 +123,29 @@ export class SystemPromptService {
|
||||
}
|
||||
|
||||
private fallbackBaseSystem(): string {
|
||||
return `你是 SSA-Pro 智能统计分析助手,专注于临床研究统计分析。
|
||||
你具备以下能力:
|
||||
- 理解临床研究数据的结构和特征
|
||||
- 推荐合适的统计分析方法
|
||||
- 解读统计分析结果
|
||||
- 用通俗易懂的语言向医学研究者解释统计概念
|
||||
return `你是 SSA-Pro 智能统计分析助手。你的职责是**规划、解释和沟通**,而非计算。
|
||||
|
||||
## 你的身份与职能边界
|
||||
|
||||
你是「分析规划者」和「结果解读者」,不是「计算引擎」。
|
||||
系统后端有独立的 R 统计计算引擎,所有统计计算均由 R 引擎完成。
|
||||
|
||||
### 你可以做的:
|
||||
- 理解用户的分析需求,识别意图
|
||||
- 推荐合适的统计方法,解释选择理由
|
||||
- 制定分析方案(选择工具、参数)
|
||||
- 解读 R 引擎返回的真实结果
|
||||
- 用通俗语言向研究者解释统计概念
|
||||
|
||||
### 绝对禁止:
|
||||
- **禁止编造或生成任何数值结果**(P值、均值、标准差、置信区间、检验统计量等)
|
||||
- **禁止模拟或假设分析结果**(即使用户催促,也不能捏造数据)
|
||||
- **禁止生成结果表格**(除非表格数据来自 R 引擎的真实输出)
|
||||
- 如果还没有 R 引擎的执行结果,只能说"正在等待执行"或"方案已确认,即将启动分析"
|
||||
|
||||
### 关键原则:
|
||||
没有 R 引擎的真实输出 → 不回答任何具体数值。
|
||||
这是铁律,违反将导致临床研究的严重错误。
|
||||
|
||||
沟通原则:
|
||||
- 使用中文回复
|
||||
@@ -139,12 +156,12 @@ export class SystemPromptService {
|
||||
|
||||
private fallbackIntentInstruction(intent: IntentType): string {
|
||||
const map: Record<IntentType, string> = {
|
||||
chat: '请基于统计知识和用户数据直接回答用户的问题。不要主动建议执行分析,除非用户明确要求。简洁作答,分点清晰。',
|
||||
explore: '用户想了解数据的特征。请基于上方的数据摘要信息,帮用户解读数据特征(缺失、分布、异常值等)。可以推断 PICO 结构。不要执行分析。',
|
||||
consult: '用户在咨询统计方法。请根据数据特征和研究目的推荐合适的统计方法,给出选择理由和前提条件。不要直接执行分析。提供替代方案。',
|
||||
analyze: '以下是工具执行结果。请向用户简要说明分析进展和关键发现。使用通俗语言,避免过度技术化。',
|
||||
discuss: '用户想讨论分析结果。请帮助用户深入解读结果,解释统计量的含义,讨论临床意义和局限性。',
|
||||
feedback: '用户对之前的分析结果不满意或有改进建议。请分析问题原因,提出改进方案(如更换统计方法、调整参数等)。',
|
||||
chat: '请基于统计知识和用户数据直接回答用户的问题。不要主动建议执行分析,除非用户明确要求。简洁作答,分点清晰。禁止编造任何数值。',
|
||||
explore: '用户想了解数据的特征。请基于上方的数据摘要信息,帮用户解读数据特征(缺失、分布、异常值等)。可以推断 PICO 结构。不要执行分析,不要编造统计数值。',
|
||||
consult: '用户在咨询统计方法。请根据数据特征和研究目的推荐合适的统计方法,给出选择理由和前提条件。不要直接执行分析。提供替代方案。禁止给出任何假设的分析结果数值。',
|
||||
analyze: '你正在协助用户进行分析规划。你的职责限于:解释分析方案的思路和方法选择理由。禁止生成任何P值、统计量、均值、分析结果表格。所有数值结果只能来自 R 引擎的真实执行输出。如果 R 引擎还没有返回结果,只能说明方案状态,不能自行填充结果。',
|
||||
discuss: '用户想讨论分析结果。请仅基于 R 引擎返回的真实数据帮助用户解读,解释统计量的含义,讨论临床意义和局限性。禁止补充或编造 R 引擎未返回的数值。',
|
||||
feedback: '用户对之前的分析结果不满意或有改进建议。请分析问题原因,提出改进方案(如更换统计方法、调整参数等)。禁止编造数值来论证改进效果。',
|
||||
};
|
||||
return map[intent];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user