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:
153
backend/src/modules/ssa/services/SystemPromptService.ts
Normal file
153
backend/src/modules/ssa/services/SystemPromptService.ts
Normal 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 预算 <= 4000(C2),超出按 [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();
|
||||
Reference in New Issue
Block a user