feat(ssa): Implement dual-channel architecture Phase 1-3 (QPER + LLM Agent pipeline)

Completed:
- Phase 1: DB schema (execution_mode + ssa_agent_executions), ModeToggle component, Session PATCH API
- Phase 2: AgentPlannerService + AgentCoderService (streaming) + CodeRunnerService + R Docker /execute-code endpoint
- Phase 3: AgentCodePanel (3-step confirmation UI), SSE event handling (7 agent events), streaming code display
- Three-step confirmation pipeline: plan -> user confirm -> stream code -> user confirm -> execute R code -> results
- R Docker sandbox /execute-code endpoint with 120s timeout + block_helpers preloaded
- E2E dual-channel test script (8 tests)
- Updated R engine architecture doc (v1.5) and SSA module status doc (v4.0)

Technical details:
- AgentCoderService uses LLM streaming (chatStream) for real-time code generation feedback
- ReviewerAgent temporarily disabled, prioritizing Plan -> Code -> Execute flow
- CodeRunnerService wraps user code with auto data loading (df variable injection)
- Frontend handles agent_planning, agent_plan_ready, code_generating, code_generated, code_executing, code_result events
- ask_user mechanism used for plan and code confirmation steps

Files: 24 files (4 new services, 2 new components, 1 migration, 1 E2E test, 16 modified)
Made-with: Cursor
This commit is contained in:
2026-03-02 22:23:54 +08:00
parent 71d32d11ee
commit aadceb5cde
24 changed files with 2694 additions and 56 deletions

View File

@@ -12,6 +12,7 @@
import { FastifyInstance, FastifyRequest } from 'fastify';
import { logger } from '../../../common/logging/index.js';
import { prisma } from '../../../config/database.js';
import { conversationService } from '../services/ConversationService.js';
import { intentRouterService } from '../services/IntentRouterService.js';
import { chatHandlerService } from '../services/ChatHandlerService.js';
@@ -114,6 +115,41 @@ export default async function chatRoutes(app: FastifyInstance) {
}
// ── H1 结束 ──
// 3. 读取 session 的执行模式
const session = await (prisma.ssaSession as any).findUnique({
where: { id: sessionId },
select: { executionMode: true },
});
const executionMode = (session?.executionMode as string) || 'qper';
// ── Agent 通道分流 ──
if (executionMode === 'agent') {
const placeholderMsgId = await conversationService.createAssistantPlaceholder(
conversationId, 'chat',
);
const metaEvent = JSON.stringify({
type: 'intent_classified',
intent: 'analyze',
confidence: 1,
source: 'agent_mode',
guardTriggered: false,
});
writer.write(`data: ${metaEvent}\n\n`);
const result = await chatHandlerService.handleAgentMode(
sessionId, conversationId, content.trim(), writer, placeholderMsgId,
);
logger.info('[SSA:Chat] Agent mode request completed', {
sessionId, success: result.success,
});
writer.end();
return;
}
// ── QPER 通道(现有逻辑) ──
// 3. 意图分类
const intentResult = await intentRouterService.classify(content.trim(), sessionId);

View File

@@ -136,6 +136,33 @@ export default async function sessionRoutes(app: FastifyInstance) {
return reply.send(session);
});
/**
* PATCH /sessions/:id/execution-mode
* 切换双通道执行模式 (qper / agent)
*/
app.patch('/:id/execution-mode', async (req, reply) => {
const { id } = req.params as { id: string };
const { executionMode } = req.body as { executionMode: string };
if (!executionMode || !['qper', 'agent'].includes(executionMode)) {
return reply.status(400).send({ error: 'executionMode must be "qper" or "agent"' });
}
const session = await prisma.ssaSession.findUnique({ where: { id } });
if (!session) {
return reply.status(404).send({ error: 'Session not found' });
}
await (prisma.ssaSession as any).update({
where: { id },
data: { executionMode },
});
logger.info('[SSA:Session] Execution mode switched', { sessionId: id, executionMode });
return reply.send({ success: true, executionMode });
});
// 获取消息历史
app.get('/:id/messages', async (req, reply) => {
const { id } = req.params as { id: string };