/** * Phase II — 意图处理器(Intent Handlers) * * 按意图类型分发处理逻辑: * - chat: 直接 LLM 对话 * - explore: READ 工具 + LLM 解读 * - analyze: 转入 QPER 流水线 + LLM 摘要 * - consult: LLM 方法推荐(Phase III 增强) * - discuss: LLM 结果解读(Phase V 增强) * - feedback: LLM 改进建议(Phase V 增强) */ import { logger } from '../../../common/logging/index.js'; import { conversationService, type StreamWriter } from './ConversationService.js'; import { sessionBlackboardService } from './SessionBlackboardService.js'; import { tokenTruncationService } from './TokenTruncationService.js'; import { methodConsultService } from './MethodConsultService.js'; import { askUserService, type AskUserResponse } from './AskUserService.js'; import { toolOrchestratorService } from './ToolOrchestratorService.js'; import type { IntentType } from './SystemPromptService.js'; import type { IntentResult } from './IntentRouterService.js'; export interface HandleResult { messageId: string; intent: IntentType; success: boolean; error?: string; } export class ChatHandlerService { /** * 统一处理入口:按意图分发 */ async handle( sessionId: string, conversationId: string, userContent: string, intentResult: IntentResult, writer: StreamWriter, placeholderMessageId: string, ): Promise { const intent = intentResult.intent; try { // 如果上下文守卫被触发且有提示消息,直接作为 LLM 上下文的一部分 let toolOutputs: string | undefined; if (intentResult.guardTriggered && intentResult.guardMessage) { toolOutputs = `[系统提示] ${intentResult.guardMessage}`; } switch (intent) { case 'chat': return await this.handleChat(sessionId, conversationId, writer, placeholderMessageId, intent, toolOutputs); case 'explore': return await this.handleExplore(sessionId, conversationId, writer, placeholderMessageId, toolOutputs); case 'consult': return await this.handleConsult(sessionId, conversationId, writer, placeholderMessageId, toolOutputs); case 'analyze': return await this.handleAnalyze(sessionId, conversationId, userContent, writer, placeholderMessageId, toolOutputs); case 'discuss': return await this.handleDiscuss(sessionId, conversationId, writer, placeholderMessageId, toolOutputs); case 'feedback': return await this.handleChat(sessionId, conversationId, writer, placeholderMessageId, 'feedback', toolOutputs); default: return await this.handleChat(sessionId, conversationId, writer, placeholderMessageId, 'chat', toolOutputs); } } catch (error: any) { logger.error('[SSA:ChatHandler] Handler error', { sessionId, intent, error: error.message, }); await conversationService.markAssistantError(placeholderMessageId, error.message); return { messageId: placeholderMessageId, intent, success: false, error: error.message, }; } } // ──────────────────────────────────────────── // chat / consult / feedback — 直接 LLM 对话 // ──────────────────────────────────────────── private async handleChat( sessionId: string, conversationId: string, writer: StreamWriter, placeholderMessageId: string, intent: IntentType, toolOutputs?: string, ): Promise { const messages = await conversationService.buildContext( sessionId, conversationId, intent, toolOutputs, ); const result = await conversationService.streamToSSE(messages, writer, { temperature: 0.7, maxTokens: 2000, }); await conversationService.finalizeAssistantMessage( placeholderMessageId, result.content, result.thinking, result.tokens, ); return { messageId: placeholderMessageId, intent, success: true, }; } // ──────────────────────────────────────────── // explore — 数据探索(READ 工具 + LLM 解读) // ──────────────────────────────────────────── private async handleExplore( sessionId: string, conversationId: string, writer: StreamWriter, placeholderMessageId: string, guardToolOutput?: string, ): Promise { // 从 SessionBlackboard 提取数据摘要作为 tool output const blackboard = await sessionBlackboardService.get(sessionId); let toolOutputs = guardToolOutput || ''; if (blackboard) { const truncated = tokenTruncationService.truncate(blackboard, { maxTokens: 1500, strategy: 'balanced', }); const exploreData: string[] = []; if (truncated.overview) { exploreData.push(`数据概览:\n${truncated.overview}`); } if (truncated.variables) { exploreData.push(`变量列表:\n${truncated.variables}`); } if (truncated.pico) { exploreData.push(`PICO 推断:\n${truncated.pico}`); } if (truncated.report) { exploreData.push(`数据诊断:\n${truncated.report}`); } if (exploreData.length > 0) { toolOutputs = (toolOutputs ? toolOutputs + '\n\n' : '') + exploreData.join('\n\n'); } } const messages = await conversationService.buildContext( sessionId, conversationId, 'explore', toolOutputs || undefined, ); const result = await conversationService.streamToSSE(messages, writer, { temperature: 0.7, maxTokens: 2000, }); await conversationService.finalizeAssistantMessage( placeholderMessageId, result.content, result.thinking, result.tokens, ); return { messageId: placeholderMessageId, intent: 'explore', success: true, }; } // ──────────────────────────────────────────── // analyze — 转入 QPER(混合模式:LLM 摘要 + WorkspacePane 详情) // ──────────────────────────────────────────── /** * Phase IV: analyze 意图 — 对话驱动分析 * * 流程: plan 生成 → SSE 推 analysis_plan → LLM 方案说明 → ask_user 确认 * 执行由前端通过 /workflow/{id}/stream 触发(D1: 保留独立 workflow SSE) */ private async handleAnalyze( sessionId: string, conversationId: string, userMessage: string, writer: StreamWriter, placeholderMessageId: string, guardToolOutput?: string, ): Promise { // 1. 调用 ToolOrchestratorService 生成计划(D5: PICO hint 自动注入) const planResult = await toolOrchestratorService.plan(sessionId, userMessage); if (!planResult.success || !planResult.plan) { const fallbackHint = [ guardToolOutput, `[系统提示] 分析计划生成失败: ${planResult.error || '未知错误'}。`, '请友好地告知用户需要更明确的分析需求描述,例如需要指明要分析哪些变量、比较什么。', ].filter(Boolean).join('\n'); const messages = await conversationService.buildContext( sessionId, conversationId, 'analyze', fallbackHint, ); const result = await conversationService.streamToSSE(messages, writer, { temperature: 0.5, maxTokens: 800, }); await conversationService.finalizeAssistantMessage( placeholderMessageId, result.content, result.thinking, result.tokens, ); return { messageId: placeholderMessageId, intent: 'analyze', success: true }; } const plan = planResult.plan; // 2. SSE 推送 analysis_plan 事件(D2: 前端自动创建 AnalysisRecord) const planEvent = `data: ${JSON.stringify({ type: 'analysis_plan', plan, })}\n\n`; writer.write(planEvent); logger.info('[SSA:ChatHandler] analysis_plan pushed via SSE', { sessionId, workflowId: plan.workflow_id, totalSteps: plan.total_steps, }); // 3. LLM 流式生成方案说明 const planSummary = toolOrchestratorService.formatPlanForLLM(plan); const toolOutputs = [ guardToolOutput, planSummary, '[系统提示] 你刚刚为用户制定了上述分析方案。请用自然语言向用户解释这个方案:包括为什么选这些方法、分析步骤的逻辑。不要重复列步骤编号和工具代码,要用用户能理解的语言说明。最后提示用户确认方案后即可执行。', ].filter(Boolean).join('\n\n'); const messages = await conversationService.buildContext( sessionId, conversationId, 'analyze', toolOutputs, ); const result = await conversationService.streamToSSE(messages, writer, { temperature: 0.5, maxTokens: 1200, }); await conversationService.finalizeAssistantMessage( placeholderMessageId, result.content, result.thinking, result.tokens, ); // 4. 推送 ask_user 确认卡片(复用 Phase III AskUserService) const confirmQ = { inputType: 'confirm' as const, question: '请确认上述分析方案', context: `方案: ${plan.title},共 ${plan.total_steps} 个步骤`, options: [ { id: 'confirm_plan', label: '确认执行', value: 'confirm_plan' }, { id: 'change_method', label: '修改方案', value: 'change_method' }, ], metadata: { workflowId: plan.workflow_id, planTitle: plan.title, }, }; const event = await askUserService.createQuestion(sessionId, confirmQ); writer.write(askUserService.formatSSE(event)); return { messageId: placeholderMessageId, intent: 'analyze', success: true, }; } // ──────────────────────────────────────────── // discuss — 结果讨论(注入分析结果上下文) // ──────────────────────────────────────────── private async handleDiscuss( sessionId: string, conversationId: string, writer: StreamWriter, placeholderMessageId: string, guardToolOutput?: string, ): Promise { const blackboard = await sessionBlackboardService.get(sessionId); let toolOutputs = guardToolOutput || ''; // 注入 QPER trace 摘要 if (blackboard?.qperTrace && blackboard.qperTrace.length > 0) { const traceItems = blackboard.qperTrace .filter(t => t.status === 'success') .slice(-5) .map(t => `- 步骤${t.stepIndex}: ${t.toolCode} → ${t.summary}`) .join('\n'); if (traceItems) { toolOutputs = (toolOutputs ? toolOutputs + '\n\n' : '') + `最近分析结果:\n${traceItems}`; } } const messages = await conversationService.buildContext( sessionId, conversationId, 'discuss', toolOutputs || undefined, ); const result = await conversationService.streamToSSE(messages, writer, { temperature: 0.7, maxTokens: 2000, }); await conversationService.finalizeAssistantMessage( placeholderMessageId, result.content, result.thinking, result.tokens, ); return { messageId: placeholderMessageId, intent: 'discuss', success: true, }; } // ──────────────────────────────────────────── // consult — 方法推荐(Phase III: method_consult + ask_user) // ──────────────────────────────────────────── private async handleConsult( sessionId: string, conversationId: string, writer: StreamWriter, placeholderMessageId: string, guardToolOutput?: string, ): Promise { let toolOutputs = guardToolOutput || ''; // 1. 调用 MethodConsultService 获取推荐 const recommendation = await methodConsultService.recommend(sessionId); const recText = methodConsultService.formatForLLM(recommendation); toolOutputs = (toolOutputs ? toolOutputs + '\n\n' : '') + recText; logger.info('[SSA:ChatHandler] Method consult result', { sessionId, matched: recommendation.matched, primaryMethod: recommendation.primaryMethod?.code, matchScore: recommendation.matchScore, needsClarification: recommendation.needsClarification, }); // 2. LLM 生成自然语言推荐(P1: 结论先行 + 结构化列表) const messages = await conversationService.buildContext( sessionId, conversationId, 'consult', toolOutputs, ); const result = await conversationService.streamToSSE(messages, writer, { temperature: 0.5, maxTokens: 1500, }); await conversationService.finalizeAssistantMessage( placeholderMessageId, result.content, result.thinking, result.tokens, ); // 3. 如果有明确推荐,推送 ask_user 确认卡片 if (recommendation.matched && recommendation.primaryMethod) { const confirmQ = askUserService.buildMethodConfirmQuestion( recommendation.primaryMethod.name, recommendation.primaryMethod.code, recommendation.fallbackMethod?.name, ); const event = await askUserService.createQuestion(sessionId, confirmQ); writer.write(askUserService.formatSSE(event)); } return { messageId: placeholderMessageId, intent: 'consult', success: true, }; } // ──────────────────────────────────────────── // ask_user 响应处理(Phase III) // ──────────────────────────────────────────── async handleAskUserResponse( sessionId: string, conversationId: string, response: AskUserResponse, writer: StreamWriter, placeholderMessageId: string, ): Promise { // 清除 pending 状态 await askUserService.clearPending(sessionId); if (response.action === 'skip') { // 用户跳过 — 生成友好回复 const messages = await conversationService.buildContext( sessionId, conversationId, 'chat', '[系统提示] 用户跳过了上一个确认问题。请友好地回应,表示随时可以继续。', ); const result = await conversationService.streamToSSE(messages, writer, { temperature: 0.7, maxTokens: 500, }); await conversationService.finalizeAssistantMessage( placeholderMessageId, result.content, result.thinking, result.tokens, ); return { messageId: placeholderMessageId, intent: 'chat', success: true }; } // 用户选择了具体选项 const selectedValue = response.selectedValues?.[0]; if (selectedValue === 'confirm_plan') { // Phase IV: 确认分析方案 → 前端将触发 executeWorkflow const workflowId = response.metadata?.workflowId || ''; const messages = await conversationService.buildContext( sessionId, conversationId, 'analyze', `[系统提示] 用户已确认分析方案(workflow: ${workflowId})。请简要确认:"好的,方案已确认,正在准备执行分析..."。`, ); const result = await conversationService.streamToSSE(messages, writer, { temperature: 0.3, maxTokens: 300, }); await conversationService.finalizeAssistantMessage( placeholderMessageId, result.content, result.thinking, result.tokens, ); // 推送 plan_confirmed 事件,前端据此触发 executeWorkflow const confirmEvent = `data: ${JSON.stringify({ type: 'plan_confirmed', workflowId, })}\n\n`; writer.write(confirmEvent); return { messageId: placeholderMessageId, intent: 'analyze', success: true }; } else if (selectedValue === 'confirm') { // Phase III: 确认使用推荐方法 → 提示可以开始分析 const messages = await conversationService.buildContext( sessionId, conversationId, 'analyze', '[系统提示] 用户已确认使用推荐的统计方法。请简要确认方案,告知用户可以在对话中说"开始分析"或在右侧面板触发执行。', ); const result = await conversationService.streamToSSE(messages, writer, { temperature: 0.5, maxTokens: 800, }); await conversationService.finalizeAssistantMessage( placeholderMessageId, result.content, result.thinking, result.tokens, ); return { messageId: placeholderMessageId, intent: 'analyze', success: true }; } else if (selectedValue === 'use_fallback') { // 使用备选方案 const messages = await conversationService.buildContext( sessionId, conversationId, 'consult', '[系统提示] 用户选择使用备选方案。请确认切换,并简要说明备选方案的适用场景。', ); const result = await conversationService.streamToSSE(messages, writer, { temperature: 0.5, maxTokens: 800, }); await conversationService.finalizeAssistantMessage( placeholderMessageId, result.content, result.thinking, result.tokens, ); return { messageId: placeholderMessageId, intent: 'consult', success: true }; } else if (selectedValue === 'change_method') { // 用户想换方法 → 引导重新描述 const messages = await conversationService.buildContext( sessionId, conversationId, 'consult', '[系统提示] 用户不满意当前推荐,想换方法。请询问用户希望使用什么方法,或引导其更详细地描述分析需求。', ); const result = await conversationService.streamToSSE(messages, writer, { temperature: 0.7, maxTokens: 800, }); await conversationService.finalizeAssistantMessage( placeholderMessageId, result.content, result.thinking, result.tokens, ); return { messageId: placeholderMessageId, intent: 'consult', success: true }; } // 其他情况:fallback return await this.handleChat(sessionId, conversationId, writer, placeholderMessageId, 'chat'); } } export const chatHandlerService = new ChatHandlerService();