feat(ssa): Complete Phase I-IV intelligent dialogue and tool system development

Phase I - Session Blackboard + READ Layer:
- SessionBlackboardService with Postgres-Only cache
- DataProfileService for data overview generation
- PicoInferenceService for LLM-driven PICO extraction
- Frontend DataContextCard and VariableDictionaryPanel
- E2E tests: 31/31 passed

Phase II - Conversation Layer LLM + Intent Router:
- ConversationService with SSE streaming
- IntentRouterService (rule-first + LLM fallback, 6 intents)
- SystemPromptService with 6-segment dynamic assembly
- TokenTruncationService for context management
- ChatHandlerService as unified chat entry
- Frontend SSAChatPane and useSSAChat hook
- E2E tests: 38/38 passed

Phase III - Method Consultation + AskUser Standardization:
- ToolRegistryService with Repository Pattern
- MethodConsultService with DecisionTable + LLM enhancement
- AskUserService with global interrupt handling
- Frontend AskUserCard component
- E2E tests: 13/13 passed

Phase IV - Dialogue-Driven Analysis + QPER Integration:
- ToolOrchestratorService (plan/execute/report)
- analysis_plan SSE event for WorkflowPlan transmission
- Dual-channel confirmation (ask_user card + workspace button)
- PICO as optional hint for LLM parsing
- E2E tests: 25/25 passed

R Statistics Service:
- 5 new R tools: anova_one, baseline_table, fisher, linear_reg, wilcoxon
- Enhanced guardrails and block helpers
- Comprehensive test suite (run_all_tools_test.js)

Documentation:
- Updated system status document (v5.9)
- Updated SSA module status and development plan (v1.8)

Total E2E: 107/107 passed (Phase I: 31, Phase II: 38, Phase III: 13, Phase IV: 25)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-02-22 18:53:39 +08:00
parent bf10dec4c8
commit 3446909ff7
68 changed files with 11583 additions and 412 deletions

View File

@@ -0,0 +1,153 @@
/**
* Phase II — System Prompt 动态组装服务
*
* 六段式组装H2 Lost-in-the-Middle 修正):
* [1] base_system — 固定角色定义
* [2] data_context — DataOverview 摘要
* [3] pico_inference — PICO 分类
* [4] variable_dictionary — 变量字典摘要
* [5] tool_outputs — 工具调用结果(冗长数据放中间)
* [6] intent_instruction — 意图指令(核心指令放最后,永不裁剪)
*
* Token 预算 <= 4000C2超出按 [5] > [4] > [3] > [2] 优先级裁剪。
* [6] intent_instruction 永不裁剪。
*/
import { logger } from '../../../common/logging/index.js';
import { getPromptService } from '../../../common/prompt/index.js';
import { prisma } from '../../../config/database.js';
import { tokenTruncationService, type TruncationOptions } from './TokenTruncationService.js';
import { sessionBlackboardService } from './SessionBlackboardService.js';
import type { SessionBlackboard } from '../types/session-blackboard.types.js';
export type IntentType = 'chat' | 'explore' | 'consult' | 'analyze' | 'discuss' | 'feedback';
const INTENT_PROMPT_CODES: Record<IntentType, string> = {
chat: 'SSA_INTENT_CHAT',
explore: 'SSA_INTENT_EXPLORE',
consult: 'SSA_INTENT_CONSULT',
analyze: 'SSA_INTENT_ANALYZE',
discuss: 'SSA_INTENT_DISCUSS',
feedback: 'SSA_INTENT_FEEDBACK',
};
const MAX_SYSTEM_TOKENS = 4000;
export class SystemPromptService {
/**
* 组装完整 System Prompt六段式H2 修正顺序)
*/
async assemble(
sessionId: string,
intent: IntentType,
toolOutputs?: string,
): Promise<string> {
const promptService = getPromptService(prisma);
// [1] Base system role
let baseSystem = '';
try {
const rendered = await promptService.get('SSA_BASE_SYSTEM', {});
baseSystem = rendered.content;
} catch {
baseSystem = this.fallbackBaseSystem();
}
// [2-4] DataContext from SessionBlackboard (truncated)
let dataContextBlock = '';
const blackboard = await sessionBlackboardService.get(sessionId);
if (blackboard) {
const truncated = tokenTruncationService.truncate(blackboard, {
maxTokens: this.calculateDataBudget(baseSystem, toolOutputs),
strategy: 'balanced',
});
dataContextBlock = tokenTruncationService.toPromptString(truncated);
}
// [5] Tool outputs (placed in middle — H2 fix)
const toolBlock = toolOutputs
? `\n\n## 工具执行结果\n${toolOutputs}`
: '';
// [6] Intent instruction (placed LAST — H2 fix, never truncated)
let intentInstruction = '';
const intentCode = INTENT_PROMPT_CODES[intent];
try {
const rendered = await promptService.get(intentCode, {});
intentInstruction = rendered.content;
} catch {
intentInstruction = this.fallbackIntentInstruction(intent);
}
// Assemble: [1] Base → [2-4] DataContext → [5] ToolOutputs → [6] IntentInstruction
const parts: string[] = [baseSystem];
if (dataContextBlock) {
parts.push(dataContextBlock);
}
if (toolBlock) {
parts.push(toolBlock);
}
// Intent instruction is ALWAYS last (H2 — Lost in the Middle fix)
parts.push(`\n\n## 当前任务指令\n${intentInstruction}`);
const assembled = parts.join('\n\n');
const estimatedTokens = Math.ceil(assembled.length / 2);
logger.debug('[SSA:SystemPrompt] Assembled', {
sessionId,
intent,
estimatedTokens,
hasData: !!blackboard,
hasToolOutput: !!toolOutputs,
});
if (estimatedTokens > MAX_SYSTEM_TOKENS) {
logger.warn('[SSA:SystemPrompt] Exceeded token budget', {
estimatedTokens,
maxTokens: MAX_SYSTEM_TOKENS,
});
}
return assembled;
}
private calculateDataBudget(baseSystem: string, toolOutputs?: string): number {
const baseTokens = Math.ceil(baseSystem.length / 2);
const toolTokens = toolOutputs ? Math.ceil(toolOutputs.length / 2) : 0;
const intentReserve = 500; // intent instruction reserve
return Math.max(500, MAX_SYSTEM_TOKENS - baseTokens - toolTokens - intentReserve);
}
private fallbackBaseSystem(): string {
return `你是 SSA-Pro 智能统计分析助手,专注于临床研究统计分析。
你具备以下能力:
- 理解临床研究数据的结构和特征
- 推荐合适的统计分析方法
- 解读统计分析结果
- 用通俗易懂的语言向医学研究者解释统计概念
沟通原则:
- 使用中文回复
- 语言专业但不晦涩
- 分点作答,条理清晰
- 对不确定的内容如实说明`;
}
private fallbackIntentInstruction(intent: IntentType): string {
const map: Record<IntentType, string> = {
chat: '请基于统计知识和用户数据直接回答用户的问题。不要主动建议执行分析,除非用户明确要求。简洁作答,分点清晰。',
explore: '用户想了解数据的特征。请基于上方的数据摘要信息,帮用户解读数据特征(缺失、分布、异常值等)。可以推断 PICO 结构。不要执行分析。',
consult: '用户在咨询统计方法。请根据数据特征和研究目的推荐合适的统计方法,给出选择理由和前提条件。不要直接执行分析。提供替代方案。',
analyze: '以下是工具执行结果。请向用户简要说明分析进展和关键发现。使用通俗语言,避免过度技术化。',
discuss: '用户想讨论分析结果。请帮助用户深入解读结果,解释统计量的含义,讨论临床意义和局限性。',
feedback: '用户对之前的分析结果不满意或有改进建议。请分析问题原因,提出改进方案(如更换统计方法、调整参数等)。',
};
return map[intent];
}
}
export const systemPromptService = new SystemPromptService();