feat(aia): Protocol Agent MVP complete with one-click generation and Word export
- Add one-click research protocol generation with streaming output - Implement Word document export via Pandoc integration - Add dynamic dual-panel layout with resizable split pane - Implement collapsible content for StatePanel stages - Add conversation history management with title auto-update - Fix scroll behavior, markdown rendering, and UI layout issues - Simplify conversation creation logic for reliability
This commit is contained in:
@@ -8,11 +8,27 @@
|
||||
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { ProtocolOrchestrator } from '../services/ProtocolOrchestrator.js';
|
||||
import { ProtocolExportService } from '../services/ProtocolExportService.js';
|
||||
import { LLMServiceInterface } from '../../services/BaseAgentOrchestrator.js';
|
||||
import { ProtocolStageCode } from '../../types/index.js';
|
||||
import { createStreamingService } from '../../../../common/streaming/index.js';
|
||||
import type { OpenAIMessage } from '../../../../common/streaming/index.js';
|
||||
import { logger } from '../../../../common/logging/index.js';
|
||||
import {
|
||||
FULL_PROTOCOL_GENERATION_PROMPT,
|
||||
TITLE_GENERATION_PROMPT,
|
||||
fillPromptTemplate,
|
||||
buildPromptVariables,
|
||||
} from '../prompts/protocolGenerationPrompts.js';
|
||||
|
||||
// 阶段顺序
|
||||
const STAGE_ORDER: ProtocolStageCode[] = [
|
||||
'scientific_question',
|
||||
'pico',
|
||||
'study_design',
|
||||
'sample_size',
|
||||
'endpoints',
|
||||
];
|
||||
|
||||
// 请求类型定义
|
||||
interface SendMessageBody {
|
||||
@@ -41,11 +57,13 @@ interface GetContextParams {
|
||||
|
||||
export class ProtocolAgentController {
|
||||
private orchestrator: ProtocolOrchestrator;
|
||||
private exportService: ProtocolExportService;
|
||||
private prisma: PrismaClient;
|
||||
|
||||
constructor(prisma: PrismaClient, llmService: LLMServiceInterface) {
|
||||
this.prisma = prisma;
|
||||
this.orchestrator = new ProtocolOrchestrator({ prisma, llmService });
|
||||
this.exportService = new ProtocolExportService(prisma);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -213,30 +231,42 @@ export class ProtocolAgentController {
|
||||
// 获取当前阶段需要输出的字段格式
|
||||
const stageOutputFormat = this.getStageOutputFormat(context.currentStage);
|
||||
|
||||
// 获取阶段任务描述
|
||||
const stageInstructions = this.getStageInstructions(context.currentStage);
|
||||
|
||||
return `你是一位资深的临床研究方法学专家,正在帮助医生设计临床研究方案。
|
||||
|
||||
## ⚠️ 重要:当前阶段
|
||||
|
||||
**🎯 你现在正在处理「${currentStageName}」阶段(第 ${STAGE_ORDER.indexOf(context.currentStage as ProtocolStageCode) + 1}/5 阶段)**
|
||||
|
||||
${stageInstructions}
|
||||
|
||||
## 当前状态
|
||||
- **当前阶段**: ${currentStageName}
|
||||
- **已完成阶段**: ${completedStages.map((s: string) => stageNames[s]).join(', ') || '无'}
|
||||
- **进度**: ${progress}%
|
||||
|
||||
## 已收集的数据${completedDataSummary || '\n暂无已确认的数据'}
|
||||
## 已收集的数据(仅供参考,不要重复讨论)${completedDataSummary || '\n暂无已确认的数据'}
|
||||
|
||||
## 你的任务
|
||||
1. **只围绕「${currentStageName}」阶段与用户对话**,不要跨阶段讨论
|
||||
2. 引导用户提供当前阶段所需的完整信息
|
||||
3. 当信息收集完整时,先用文字总结,然后**必须**在回复末尾输出结构化数据
|
||||
## ❌ 禁止事项
|
||||
1. **不要讨论已完成的阶段**(如${completedStages.map((s: string) => stageNames[s]).join('、') || '无'})
|
||||
2. **不要提前讨论未来的阶段**
|
||||
3. **不要重复已经确认的数据**
|
||||
|
||||
## 当前阶段「${currentStageName}」的输出格式
|
||||
当信息完整时,**必须**在回复末尾添加以下格式的数据提取标签:
|
||||
## ✅ 你的任务
|
||||
1. **只围绕「${currentStageName}」阶段与用户对话**
|
||||
2. 引导用户提供当前阶段所需的信息
|
||||
3. 当信息收集完整时,先用文字总结,然后在回复末尾输出结构化数据
|
||||
|
||||
## 当前阶段「${currentStageName}」的数据格式
|
||||
当信息完整时,**必须**在回复末尾添加:
|
||||
|
||||
${stageOutputFormat}
|
||||
|
||||
## 重要提示
|
||||
- 只有当用户提供了足够的信息后才输出 <extracted_data> 标签
|
||||
- 输出的 JSON 必须是有效格式
|
||||
- 每次对话只关注当前阶段「${currentStageName}」
|
||||
- 回复使用 Markdown 格式,简洁专业`;
|
||||
## 输出要求
|
||||
- 只有当用户提供了足够的「${currentStageName}」信息后才输出 <extracted_data> 标签
|
||||
- 回复使用 Markdown 格式,简洁专业
|
||||
- **再次强调:你只处理「${currentStageName}」**`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -296,6 +326,50 @@ ${stageOutputFormat}
|
||||
</extracted_data>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取阶段任务说明(帮助模型理解当前阶段应该做什么)
|
||||
*/
|
||||
private getStageInstructions(stageCode: string): string {
|
||||
const instructions: Record<string, string> = {
|
||||
scientific_question: `**当前任务:科学问题梳理**
|
||||
你需要帮助用户明确研究的核心科学问题。
|
||||
- 询问用户想研究什么问题
|
||||
- 了解研究背景和临床意义
|
||||
- 引导用户将问题凝练成一句话`,
|
||||
|
||||
pico: `**当前任务:PICO要素确定**
|
||||
你需要帮助用户确定PICO四要素:
|
||||
- P (Population): 研究人群是谁?
|
||||
- I (Intervention): 干预/暴露因素是什么?
|
||||
- C (Comparison): 对照组是什么?
|
||||
- O (Outcome): 主要结局指标是什么?`,
|
||||
|
||||
study_design: `**当前任务:研究设计选择**
|
||||
你需要帮助用户选择合适的研究设计类型:
|
||||
- 根据研究问题推荐研究类型(RCT/队列/病例对照等)
|
||||
- 讨论设计特征(随机/盲法/多中心等)
|
||||
- 确定分组方法`,
|
||||
|
||||
sample_size: `**当前任务:样本量计算**
|
||||
你需要帮助用户估算所需样本量:
|
||||
- 讨论主要结局指标的预期效应量
|
||||
- 确定统计学参数(α, β/Power)
|
||||
- 考虑脱落率
|
||||
- 计算最终样本量`,
|
||||
|
||||
endpoints: `**当前任务:观察指标设计**
|
||||
你需要帮助用户定义研究的观察指标体系:
|
||||
- 主要结局指标(Primary Outcome)
|
||||
- 次要结局指标(Secondary Outcomes)
|
||||
- 安全性指标
|
||||
- 潜在混杂因素
|
||||
|
||||
⚠️ 注意:样本量计算已在上一阶段完成,本阶段只讨论观察指标的定义和测量方法。`,
|
||||
};
|
||||
|
||||
return instructions[stageCode] || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步阶段数据
|
||||
* POST /api/aia/protocol-agent/sync
|
||||
@@ -375,28 +449,91 @@ ${stageOutputFormat}
|
||||
}
|
||||
|
||||
/**
|
||||
* 一键生成研究方案
|
||||
* POST /api/aia/protocol-agent/generate
|
||||
* 获取对话的历史消息
|
||||
* GET /api/aia/protocol-agent/messages/:conversationId
|
||||
*/
|
||||
async generateProtocol(
|
||||
request: FastifyRequest<{ Body: GenerateProtocolBody }>,
|
||||
async getMessages(
|
||||
request: FastifyRequest<{ Params: GetContextParams }>,
|
||||
reply: FastifyReply
|
||||
): Promise<void> {
|
||||
try {
|
||||
const { conversationId, options } = request.body;
|
||||
const userId = (request as any).user?.userId;
|
||||
|
||||
if (!userId) {
|
||||
reply.code(401).send({ error: 'Unauthorized' });
|
||||
return;
|
||||
}
|
||||
const { conversationId } = request.params;
|
||||
|
||||
if (!conversationId) {
|
||||
reply.code(400).send({ error: 'Missing conversationId' });
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取上下文
|
||||
// 验证对话是否存在
|
||||
const conversation = await this.prisma.conversation.findUnique({
|
||||
where: { id: conversationId },
|
||||
});
|
||||
|
||||
if (!conversation) {
|
||||
reply.code(404).send({ error: 'Conversation not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取历史消息
|
||||
const messages = await this.prisma.message.findMany({
|
||||
where: { conversationId },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
select: {
|
||||
id: true,
|
||||
role: true,
|
||||
content: true,
|
||||
thinkingContent: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
reply.send({
|
||||
success: true,
|
||||
data: {
|
||||
messages: messages.map(m => ({
|
||||
id: m.id,
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
thinkingContent: m.thinkingContent,
|
||||
createdAt: m.createdAt,
|
||||
})),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[ProtocolAgentController] getMessages error:', error);
|
||||
reply.code(500).send({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Internal server error',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 一键生成研究方案(流式输出)
|
||||
* POST /api/aia/protocol-agent/generate
|
||||
*
|
||||
* 必填要素:科学问题、PICO、研究设计、观察指标(4/5)
|
||||
* 可选要素:样本量
|
||||
*/
|
||||
async generateProtocol(
|
||||
request: FastifyRequest<{ Body: GenerateProtocolBody }>,
|
||||
reply: FastifyReply
|
||||
): Promise<void> {
|
||||
const { conversationId, options } = request.body;
|
||||
const userId = (request as any).user?.userId;
|
||||
|
||||
if (!userId) {
|
||||
reply.code(401).send({ error: 'Unauthorized' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!conversationId) {
|
||||
reply.code(400).send({ error: 'Missing conversationId' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. 获取上下文
|
||||
const contextService = this.orchestrator.getContextService();
|
||||
const context = await contextService.getContext(conversationId);
|
||||
|
||||
@@ -405,27 +542,98 @@ ${stageOutputFormat}
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否所有阶段都已完成
|
||||
if (!contextService.isAllStagesCompleted(context)) {
|
||||
// 2. 校验必填要素(4/5:科学问题、PICO、研究设计、观察指标)
|
||||
const requiredStages = ['scientific_question', 'pico', 'study_design', 'endpoints'];
|
||||
const completedStages = context.completedStages || [];
|
||||
const missingStages = requiredStages.filter(s => !completedStages.includes(s));
|
||||
|
||||
if (missingStages.length > 0) {
|
||||
const stageNames: Record<string, string> = {
|
||||
scientific_question: '科学问题',
|
||||
pico: 'PICO要素',
|
||||
study_design: '研究设计',
|
||||
endpoints: '观察指标',
|
||||
};
|
||||
const missingNames = missingStages.map(s => stageNames[s]).join('、');
|
||||
reply.code(400).send({
|
||||
error: '请先完成所有5个阶段(科学问题、PICO、研究设计、样本量、观察指标)'
|
||||
error: `请先完成必填要素:${missingNames}`,
|
||||
missingStages,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: 实现方案生成逻辑
|
||||
// 这里先返回占位响应,实际应该调用LLM生成完整方案
|
||||
reply.send({
|
||||
success: true,
|
||||
data: {
|
||||
generationId: 'placeholder',
|
||||
status: 'generating',
|
||||
message: '研究方案生成中...',
|
||||
estimatedTime: 30,
|
||||
logger.info('[ProtocolAgentController] 开始生成研究方案', {
|
||||
conversationId,
|
||||
completedStages,
|
||||
style: options?.style || 'academic',
|
||||
});
|
||||
|
||||
// 3. 构建 Prompt 变量
|
||||
const promptVariables = buildPromptVariables({
|
||||
scientificQuestion: context.scientificQuestion as any,
|
||||
pico: context.pico as any,
|
||||
studyDesign: context.studyDesign as any,
|
||||
sampleSize: context.sampleSize as any,
|
||||
endpoints: context.endpoints as any,
|
||||
});
|
||||
|
||||
// 4. 先生成标题
|
||||
const titlePrompt = fillPromptTemplate(TITLE_GENERATION_PROMPT, promptVariables);
|
||||
|
||||
// 5. 生成完整方案(流式)
|
||||
const fullPromptWithTitle = fillPromptTemplate(FULL_PROTOCOL_GENERATION_PROMPT, {
|
||||
...promptVariables,
|
||||
title: '(标题将根据内容自动生成)', // 占位,实际会在内容中生成
|
||||
});
|
||||
|
||||
// 6. 构建消息
|
||||
const messages: OpenAIMessage[] = [
|
||||
{ role: 'system', content: fullPromptWithTitle },
|
||||
{ role: 'user', content: '请根据以上关键要素,生成完整的临床研究方案。使用 Markdown 格式输出,按照12个章节结构撰写。' },
|
||||
];
|
||||
|
||||
// 7. 使用 StreamingService 流式输出
|
||||
const streamingService = createStreamingService(reply, {
|
||||
model: 'deepseek-v3',
|
||||
temperature: 0.7,
|
||||
maxTokens: 8192, // 长文档需要更多 tokens
|
||||
enableDeepThinking: false,
|
||||
userId,
|
||||
conversationId,
|
||||
});
|
||||
|
||||
await streamingService.streamGenerate(messages, {
|
||||
onComplete: async (fullContent, thinkingContent) => {
|
||||
// 保存生成的方案到数据库
|
||||
await this.prisma.message.create({
|
||||
data: {
|
||||
conversationId,
|
||||
role: 'assistant',
|
||||
content: fullContent,
|
||||
thinkingContent: thinkingContent || null,
|
||||
model: 'deepseek-v3',
|
||||
metadata: {
|
||||
type: 'protocol_generation',
|
||||
style: options?.style || 'academic',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
logger.info('[ProtocolAgentController] 研究方案生成完成', {
|
||||
conversationId,
|
||||
contentLength: fullContent.length,
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
logger.error('[ProtocolAgentController] 研究方案生成失败', {
|
||||
error,
|
||||
conversationId
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[ProtocolAgentController] generateProtocol error:', error);
|
||||
logger.error('[ProtocolAgentController] generateProtocol error:', error);
|
||||
reply.code(500).send({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Internal server error',
|
||||
@@ -465,29 +673,100 @@ ${stageOutputFormat}
|
||||
|
||||
/**
|
||||
* 导出Word文档
|
||||
* POST /api/aia/protocol-agent/generation/:generationId/export
|
||||
* POST /api/aia/protocol-agent/export/docx
|
||||
*
|
||||
* 支持两种模式:
|
||||
* 1. 提供 content: 直接将前端生成的内容转换为 Word
|
||||
* 2. 不提供 content: 从上下文数据生成模板(旧模式)
|
||||
*/
|
||||
async exportWord(
|
||||
request: FastifyRequest<{
|
||||
Params: { generationId: string };
|
||||
Body: { format: 'docx' | 'pdf' };
|
||||
Body: { conversationId?: string; content?: string };
|
||||
}>,
|
||||
reply: FastifyReply
|
||||
): Promise<void> {
|
||||
try {
|
||||
const { generationId } = request.params;
|
||||
const { format } = request.body;
|
||||
const { conversationId, content } = request.body;
|
||||
const userId = (request as any).user?.userId;
|
||||
|
||||
// TODO: 实现导出逻辑
|
||||
if (!userId) {
|
||||
reply.code(401).send({ error: 'Unauthorized' });
|
||||
return;
|
||||
}
|
||||
|
||||
let markdown: string;
|
||||
|
||||
// 模式1:使用前端提供的内容(推荐)
|
||||
if (content && content.trim()) {
|
||||
logger.info('[ProtocolAgentController] 导出 Word: 使用前端生成的内容', {
|
||||
contentLength: content.length,
|
||||
});
|
||||
markdown = content;
|
||||
}
|
||||
// 模式2:从上下文生成模板(兼容旧逻辑)
|
||||
else if (conversationId) {
|
||||
logger.info('[ProtocolAgentController] 导出 Word: 从上下文生成模板', {
|
||||
conversationId,
|
||||
});
|
||||
|
||||
const contextService = this.orchestrator.getContextService();
|
||||
const context = await contextService.getContext(conversationId);
|
||||
|
||||
if (!context) {
|
||||
reply.code(404).send({ error: 'Context not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
markdown = this.exportService.generateProtocolMarkdown({
|
||||
scientificQuestion: context.scientificQuestion as any,
|
||||
pico: context.pico as any,
|
||||
studyDesign: context.studyDesign as any,
|
||||
sampleSize: context.sampleSize as any,
|
||||
endpoints: context.endpoints as any,
|
||||
});
|
||||
} else {
|
||||
reply.code(400).send({ error: 'Missing content or conversationId' });
|
||||
return;
|
||||
}
|
||||
|
||||
// 调用导出服务转换为 Word
|
||||
const buffer = await this.exportService.convertToDocx(markdown);
|
||||
|
||||
// 生成文件名
|
||||
const timestamp = new Date().toISOString().slice(0, 10);
|
||||
const filename = `研究方案_${timestamp}.docx`;
|
||||
|
||||
// 返回文件
|
||||
reply
|
||||
.header('Content-Type', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document')
|
||||
.header('Content-Disposition', `attachment; filename="${encodeURIComponent(filename)}"`)
|
||||
.send(buffer);
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[ProtocolAgentController] exportWord error:', error);
|
||||
reply.code(500).send({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Internal server error',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 Pandoc 服务状态
|
||||
* GET /api/aia/protocol-agent/export/status
|
||||
*/
|
||||
async checkExportStatus(
|
||||
request: FastifyRequest,
|
||||
reply: FastifyReply
|
||||
): Promise<void> {
|
||||
try {
|
||||
const status = await this.exportService.checkPandocStatus();
|
||||
reply.send({
|
||||
success: true,
|
||||
data: {
|
||||
downloadUrl: `/api/aia/protocol-agent/download/${generationId}.${format}`,
|
||||
expiresAt: new Date(Date.now() + 3600 * 1000).toISOString(),
|
||||
},
|
||||
data: status,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[ProtocolAgentController] exportWord error:', error);
|
||||
logger.error('[ProtocolAgentController] checkExportStatus error:', error);
|
||||
reply.code(500).send({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Internal server error',
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
/**
|
||||
* Protocol Generation Prompts
|
||||
* 研究方案生成的 Prompt 模板
|
||||
*
|
||||
* @module agent/protocol/prompts
|
||||
*/
|
||||
|
||||
/**
|
||||
* 方案标题生成 Prompt
|
||||
*/
|
||||
export const TITLE_GENERATION_PROMPT = `你是一位资深的临床研究方案撰写专家。请根据以下关键要素,生成一个专业、准确、符合学术规范的研究方案标题。
|
||||
|
||||
## 关键要素
|
||||
|
||||
**科学问题**: {{scientificQuestion}}
|
||||
|
||||
**PICO 框架**:
|
||||
- P (研究人群): {{population}}
|
||||
- I (干预措施): {{intervention}}
|
||||
- C (对照措施): {{comparison}}
|
||||
- O (结局指标): {{outcome}}
|
||||
|
||||
**研究设计**: {{studyType}}
|
||||
|
||||
## 标题要求
|
||||
|
||||
1. 标题应包含:研究人群、干预措施、对照措施、主要结局、研究类型
|
||||
2. 格式参考:「[干预措施]对[研究人群][主要结局]的[研究类型]」
|
||||
3. 标题长度:20-50字
|
||||
4. 使用专业医学术语
|
||||
5. 不要包含标点符号(除必要的顿号)
|
||||
|
||||
## 输出格式
|
||||
|
||||
只输出标题文本,不要包含任何解释或前缀。`;
|
||||
|
||||
/**
|
||||
* 完整方案生成 Prompt(流式输出)
|
||||
*/
|
||||
export const FULL_PROTOCOL_GENERATION_PROMPT = `你是一位资深的临床研究方案撰写专家,拥有丰富的 RCT、队列研究、病例对照研究方案撰写经验。请根据以下关键要素,生成一份完整的临床研究方案。
|
||||
|
||||
## 关键要素
|
||||
|
||||
**研究标题**: {{title}}
|
||||
|
||||
**科学问题**: {{scientificQuestion}}
|
||||
|
||||
**PICO 框架**:
|
||||
- P (研究人群): {{population}}
|
||||
- I (干预措施): {{intervention}}
|
||||
- C (对照措施): {{comparison}}
|
||||
- O (结局指标): {{outcome}}
|
||||
|
||||
**研究设计**:
|
||||
- 研究类型: {{studyType}}
|
||||
- 设计特征: {{designFeatures}}
|
||||
|
||||
**样本量**: {{sampleSize}}
|
||||
- 计算依据: α={{alpha}}, Power={{power}}, 效应量={{effectSize}}
|
||||
|
||||
**观察指标**:
|
||||
- 主要结局: {{primaryOutcomes}}
|
||||
- 次要结局: {{secondaryOutcomes}}
|
||||
- 安全性指标: {{safetyOutcomes}}
|
||||
|
||||
## 输出要求
|
||||
|
||||
请按以下 12 个章节结构输出,使用 Markdown 格式:
|
||||
|
||||
# {{title}}
|
||||
|
||||
## 1. 研究背景 (Background)
|
||||
- 疾病/问题的流行病学数据
|
||||
- 当前治疗现状与挑战
|
||||
- 研究的科学依据和立题依据
|
||||
- 为什么需要本研究(知识缺口)
|
||||
|
||||
## 2. 研究目的 (Objectives)
|
||||
- **主要目的**: 基于科学问题,明确主要研究假设
|
||||
- **次要目的**: 列出 2-3 个次要目的
|
||||
|
||||
## 3. 研究设计 (Study Design)
|
||||
- 研究类型(RCT/队列/病例对照等)
|
||||
- 设计特征(随机/盲法/多中心等)
|
||||
- 分组方法和随机化方案
|
||||
|
||||
## 4. 研究对象 (Subjects)
|
||||
### 4.1 纳入标准
|
||||
- 基于 P(研究人群)制定 5-8 条纳入标准
|
||||
### 4.2 排除标准
|
||||
- 制定 5-8 条排除标准(安全性考虑、混杂控制)
|
||||
|
||||
## 5. 样本量估算 (Sample Size)
|
||||
- 主要结局指标
|
||||
- 预期效应量
|
||||
- 统计学参数(α, β, Power)
|
||||
- 计算公式和结果
|
||||
- 考虑脱落率后的最终样本量
|
||||
|
||||
## 6. 研究实施步骤 (Implementation)
|
||||
- 研究流程图
|
||||
- 各阶段时间节点
|
||||
- 干预措施详细说明
|
||||
- 对照措施详细说明
|
||||
- 访视计划
|
||||
|
||||
## 7. 观察指标 (Endpoints)
|
||||
### 7.1 主要结局指标
|
||||
- 指标定义、测量方法、测量时点
|
||||
### 7.2 次要结局指标
|
||||
- 各指标的定义和测量方法
|
||||
### 7.3 安全性指标
|
||||
- 不良事件定义和报告流程
|
||||
|
||||
## 8. 数据管理与质量控制 (Data Management)
|
||||
- 数据收集方法
|
||||
- 数据录入和核查
|
||||
- 质量控制措施
|
||||
- 数据安全和保密
|
||||
|
||||
## 9. 统计分析计划 (Statistical Analysis)
|
||||
- 分析人群定义(ITT/PP)
|
||||
- 描述性统计方法
|
||||
- 主要结局分析方法
|
||||
- 次要结局分析方法
|
||||
- 缺失数据处理
|
||||
- 亚组分析计划
|
||||
|
||||
## 10. 伦理与知情同意 (Ethics)
|
||||
- 伦理审批要求
|
||||
- 知情同意流程
|
||||
- 受试者权益保护
|
||||
- 数据隐私保护
|
||||
|
||||
## 11. 研究时间表 (Timeline)
|
||||
- 研究各阶段时间安排
|
||||
- 里程碑节点
|
||||
|
||||
## 12. 参考文献 (References)
|
||||
- 列出 3-5 篇支持本研究的核心文献
|
||||
|
||||
## 撰写规范
|
||||
|
||||
1. **语言**: 使用专业、严谨的学术语言
|
||||
2. **格式**: 严格按照上述 Markdown 格式输出
|
||||
3. **内容**: 每个章节都要有实质性内容,不要使用占位符
|
||||
4. **逻辑**: 各章节内容要前后呼应,逻辑一致
|
||||
5. **篇幅**: 每个章节 200-500 字,总计约 4000-6000 字
|
||||
|
||||
## 重要提示
|
||||
|
||||
- **直接输出方案内容**,不要包含任何开场白、问候语或解释性文字
|
||||
- **禁止**输出类似"好的,作为..."、"我将为您..."等引导语
|
||||
- **第一行必须是**:# {{title}}(方案标题)
|
||||
- 立即开始输出方案正文内容`;
|
||||
|
||||
/**
|
||||
* 单章节生成 Prompt(用于"讨论与优化"后重新生成)
|
||||
*/
|
||||
export const SECTION_REGENERATION_PROMPT = `你是一位资深的临床研究方案撰写专家。请根据用户的反馈,重新撰写研究方案的「{{sectionTitle}}」章节。
|
||||
|
||||
## 研究基本信息
|
||||
|
||||
**研究标题**: {{title}}
|
||||
**科学问题**: {{scientificQuestion}}
|
||||
**PICO**: P={{population}}, I={{intervention}}, C={{comparison}}, O={{outcome}}
|
||||
**研究设计**: {{studyType}}
|
||||
|
||||
## 当前章节内容
|
||||
|
||||
{{currentContent}}
|
||||
|
||||
## 用户反馈
|
||||
|
||||
{{userFeedback}}
|
||||
|
||||
## 输出要求
|
||||
|
||||
1. 根据用户反馈修改和优化当前章节
|
||||
2. 保持与其他章节的一致性和连贯性
|
||||
3. 使用 Markdown 格式
|
||||
4. 只输出该章节内容,以「## {{sectionNumber}}. {{sectionTitle}}」开头
|
||||
|
||||
请直接输出优化后的章节内容:`;
|
||||
|
||||
/**
|
||||
* 章节配置
|
||||
*/
|
||||
export const PROTOCOL_SECTIONS = [
|
||||
{ id: 'background', number: 1, title: '研究背景 (Background)' },
|
||||
{ id: 'objectives', number: 2, title: '研究目的 (Objectives)' },
|
||||
{ id: 'design', number: 3, title: '研究设计 (Study Design)' },
|
||||
{ id: 'subjects', number: 4, title: '研究对象 (Subjects)' },
|
||||
{ id: 'sample_size', number: 5, title: '样本量估算 (Sample Size)' },
|
||||
{ id: 'implementation', number: 6, title: '研究实施步骤 (Implementation)' },
|
||||
{ id: 'endpoints', number: 7, title: '观察指标 (Endpoints)' },
|
||||
{ id: 'data_management', number: 8, title: '数据管理与质量控制 (Data Management)' },
|
||||
{ id: 'statistics', number: 9, title: '统计分析计划 (Statistical Analysis)' },
|
||||
{ id: 'ethics', number: 10, title: '伦理与知情同意 (Ethics)' },
|
||||
{ id: 'timeline', number: 11, title: '研究时间表 (Timeline)' },
|
||||
{ id: 'references', number: 12, title: '参考文献 (References)' },
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* 填充 Prompt 模板
|
||||
*/
|
||||
export function fillPromptTemplate(
|
||||
template: string,
|
||||
variables: Record<string, string | number | undefined>
|
||||
): string {
|
||||
let result = template;
|
||||
for (const [key, value] of Object.entries(variables)) {
|
||||
const placeholder = `{{${key}}}`;
|
||||
result = result.replace(new RegExp(placeholder, 'g'), String(value ?? '未提供'));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Context 数据构建 Prompt 变量
|
||||
*/
|
||||
export function buildPromptVariables(context: {
|
||||
scientificQuestion?: { content?: string };
|
||||
pico?: { population?: string; intervention?: string; comparison?: string; outcome?: string };
|
||||
studyDesign?: { studyType?: string; design?: string[] };
|
||||
sampleSize?: { sampleSize?: number; calculation?: { alpha?: number; power?: number; effectSize?: string } };
|
||||
endpoints?: { outcomes?: { primary?: string[]; secondary?: string[]; safety?: string[] } };
|
||||
}): Record<string, string> {
|
||||
return {
|
||||
scientificQuestion: context.scientificQuestion?.content || '',
|
||||
population: context.pico?.population || '',
|
||||
intervention: context.pico?.intervention || '',
|
||||
comparison: context.pico?.comparison || '',
|
||||
outcome: context.pico?.outcome || '',
|
||||
studyType: context.studyDesign?.studyType || '',
|
||||
designFeatures: context.studyDesign?.design?.join('、') || '',
|
||||
sampleSize: context.sampleSize?.sampleSize?.toString() || '待计算',
|
||||
alpha: context.sampleSize?.calculation?.alpha?.toString() || '0.05',
|
||||
power: context.sampleSize?.calculation?.power?.toString() || '0.8',
|
||||
effectSize: context.sampleSize?.calculation?.effectSize || '',
|
||||
primaryOutcomes: context.endpoints?.outcomes?.primary?.join('、') || '',
|
||||
secondaryOutcomes: context.endpoints?.outcomes?.secondary?.join('、') || '',
|
||||
safetyOutcomes: context.endpoints?.outcomes?.safety?.join('、') || '',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -81,6 +81,22 @@ export async function protocolAgentRoutes(
|
||||
},
|
||||
}, (request, reply) => controller.getContext(request, reply));
|
||||
|
||||
// 获取历史消息
|
||||
fastify.get<{
|
||||
Params: { conversationId: string };
|
||||
}>('/messages/:conversationId', {
|
||||
preHandler: [authenticate],
|
||||
schema: {
|
||||
params: {
|
||||
type: 'object',
|
||||
required: ['conversationId'],
|
||||
properties: {
|
||||
conversationId: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}, (request, reply) => controller.getMessages(request, reply));
|
||||
|
||||
// 一键生成研究方案
|
||||
fastify.post<{
|
||||
Body: {
|
||||
@@ -127,27 +143,27 @@ export async function protocolAgentRoutes(
|
||||
}, (request, reply) => controller.getGeneration(request, reply));
|
||||
|
||||
// 导出Word文档
|
||||
// 支持两种方式:
|
||||
// 1. 提供 content: 直接转换前端生成的内容(推荐)
|
||||
// 2. 提供 conversationId: 从上下文数据生成模板
|
||||
fastify.post<{
|
||||
Params: { generationId: string };
|
||||
Body: { format: 'docx' | 'pdf' };
|
||||
}>('/generation/:generationId/export', {
|
||||
Body: { conversationId?: string; content?: string };
|
||||
}>('/export/docx', {
|
||||
preHandler: [authenticate],
|
||||
schema: {
|
||||
params: {
|
||||
type: 'object',
|
||||
required: ['generationId'],
|
||||
properties: {
|
||||
generationId: { type: 'string' },
|
||||
},
|
||||
},
|
||||
body: {
|
||||
type: 'object',
|
||||
required: ['format'],
|
||||
properties: {
|
||||
format: { type: 'string', enum: ['docx', 'pdf'] },
|
||||
conversationId: { type: 'string' },
|
||||
content: { type: 'string', description: '前端生成的 Markdown 内容' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}, (request, reply) => controller.exportWord(request, reply));
|
||||
|
||||
// 检查导出服务状态
|
||||
fastify.get('/export/status', {
|
||||
preHandler: [authenticate],
|
||||
}, (request, reply) => controller.checkExportStatus(request, reply));
|
||||
}
|
||||
|
||||
|
||||
@@ -182,3 +182,4 @@ export function createLLMServiceAdapter(): LLMServiceInterface {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -284,3 +284,4 @@ export class PromptBuilder {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -189,6 +189,36 @@ export class ProtocolContextService {
|
||||
return requiredStages.every(stage => context.completedStages.includes(stage));
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否可以生成研究方案(4/5 必填项)
|
||||
* 必填:科学问题、PICO、研究设计、观察指标
|
||||
* 可选:样本量
|
||||
*/
|
||||
canGenerateProtocol(context: ProtocolContextData): boolean {
|
||||
const requiredStages: ProtocolStageCode[] = [
|
||||
'scientific_question',
|
||||
'pico',
|
||||
'study_design',
|
||||
'endpoints',
|
||||
];
|
||||
|
||||
return requiredStages.every(stage => context.completedStages.includes(stage));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缺失的必填阶段
|
||||
*/
|
||||
getMissingRequiredStages(context: ProtocolContextData): ProtocolStageCode[] {
|
||||
const requiredStages: ProtocolStageCode[] = [
|
||||
'scientific_question',
|
||||
'pico',
|
||||
'study_design',
|
||||
'endpoints',
|
||||
];
|
||||
|
||||
return requiredStages.filter(stage => !context.completedStages.includes(stage));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取进度百分比
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
/**
|
||||
* Protocol Export Service
|
||||
* 处理研究方案的生成和导出
|
||||
*
|
||||
* @module agent/protocol/services/ProtocolExportService
|
||||
*/
|
||||
|
||||
import axios from 'axios';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { logger } from '../../../../common/logging/index.js';
|
||||
|
||||
// Python 微服务地址
|
||||
const EXTRACTION_SERVICE_URL = process.env.EXTRACTION_SERVICE_URL || 'http://localhost:8000';
|
||||
|
||||
// 方案章节配置
|
||||
const PROTOCOL_SECTIONS = [
|
||||
{ key: 'title', name: '研究题目' },
|
||||
{ key: 'background', name: '研究背景与立题依据' },
|
||||
{ key: 'objectives', name: '研究目的' },
|
||||
{ key: 'design', name: '研究设计' },
|
||||
{ key: 'subjects', name: '研究对象(纳入/排除标准)' },
|
||||
{ key: 'sample_size', name: '样本量估算' },
|
||||
{ key: 'implementation', name: '研究实施步骤与技术路线' },
|
||||
{ key: 'endpoints', name: '观察指标' },
|
||||
{ key: 'data_management', name: '数据管理与质量控制' },
|
||||
{ key: 'safety', name: '安全性评价' },
|
||||
{ key: 'statistics', name: '统计分析计划' },
|
||||
{ key: 'ethics', name: '伦理与知情同意' },
|
||||
{ key: 'timeline', name: '研究时间表' },
|
||||
{ key: 'references', name: '参考文献' },
|
||||
];
|
||||
|
||||
interface ContextData {
|
||||
scientificQuestion?: {
|
||||
content?: string;
|
||||
summary?: string;
|
||||
original?: string;
|
||||
};
|
||||
pico?: {
|
||||
population?: string;
|
||||
intervention?: string;
|
||||
comparison?: string;
|
||||
outcome?: string;
|
||||
};
|
||||
studyDesign?: {
|
||||
studyType?: string;
|
||||
design?: string[];
|
||||
};
|
||||
sampleSize?: {
|
||||
sampleSize?: number;
|
||||
calculation?: {
|
||||
alpha?: number;
|
||||
power?: number;
|
||||
effectSize?: string;
|
||||
};
|
||||
};
|
||||
endpoints?: {
|
||||
outcomes?: {
|
||||
primary?: string[];
|
||||
secondary?: string[];
|
||||
safety?: string[];
|
||||
};
|
||||
confounders?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export class ProtocolExportService {
|
||||
private prisma: PrismaClient;
|
||||
|
||||
constructor(prisma: PrismaClient) {
|
||||
this.prisma = prisma;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 Pandoc 服务可用性
|
||||
*/
|
||||
async checkPandocStatus(): Promise<{
|
||||
available: boolean;
|
||||
version: string | null;
|
||||
message: string;
|
||||
}> {
|
||||
try {
|
||||
const response = await axios.get(`${EXTRACTION_SERVICE_URL}/api/pandoc/status`, {
|
||||
timeout: 5000,
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
logger.error('[ProtocolExportService] Pandoc 状态检查失败:', error);
|
||||
return {
|
||||
available: false,
|
||||
version: null,
|
||||
message: `无法连接到文档服务: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据上下文数据生成 Markdown 格式的研究方案
|
||||
*/
|
||||
generateProtocolMarkdown(context: ContextData, title?: string): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
// 标题
|
||||
const protocolTitle = title || this.generateTitle(context);
|
||||
parts.push(`# 临床研究方案\n\n`);
|
||||
|
||||
// 1. 研究题目
|
||||
parts.push(`## 1. 研究题目\n\n${protocolTitle}\n\n`);
|
||||
|
||||
// 2. 研究背景(占位,可由 LLM 生成)
|
||||
parts.push(`## 2. 研究背景与立题依据\n\n`);
|
||||
parts.push(`(待 LLM 根据科学问题生成)\n\n`);
|
||||
|
||||
// 3. 研究目的
|
||||
parts.push(`## 3. 研究目的\n\n`);
|
||||
if (context.scientificQuestion?.content) {
|
||||
parts.push(`**主要目的**:${context.scientificQuestion.content}\n\n`);
|
||||
}
|
||||
|
||||
// 4. 研究设计
|
||||
parts.push(`## 4. 研究设计\n\n`);
|
||||
if (context.studyDesign) {
|
||||
if (context.studyDesign.studyType) {
|
||||
parts.push(`**研究类型**:${context.studyDesign.studyType}\n\n`);
|
||||
}
|
||||
if (context.studyDesign.design && context.studyDesign.design.length > 0) {
|
||||
parts.push(`**设计特征**:\n`);
|
||||
context.studyDesign.design.forEach(d => {
|
||||
parts.push(`- ${d}\n`);
|
||||
});
|
||||
parts.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 研究对象
|
||||
parts.push(`## 5. 研究对象(纳入/排除标准)\n\n`);
|
||||
if (context.pico?.population) {
|
||||
parts.push(`**目标人群**:${context.pico.population}\n\n`);
|
||||
}
|
||||
parts.push(`### 纳入标准\n\n(待补充)\n\n`);
|
||||
parts.push(`### 排除标准\n\n(待补充)\n\n`);
|
||||
|
||||
// 6. 样本量估算
|
||||
parts.push(`## 6. 样本量估算\n\n`);
|
||||
if (context.sampleSize) {
|
||||
if (context.sampleSize.sampleSize) {
|
||||
parts.push(`**计划样本量**:${context.sampleSize.sampleSize} 例\n\n`);
|
||||
}
|
||||
if (context.sampleSize.calculation) {
|
||||
const calc = context.sampleSize.calculation;
|
||||
parts.push(`**计算依据**:\n`);
|
||||
if (calc.alpha) parts.push(`- α = ${calc.alpha}\n`);
|
||||
if (calc.power) parts.push(`- 1-β = ${calc.power}\n`);
|
||||
if (calc.effectSize) parts.push(`- 效应量 = ${calc.effectSize}\n`);
|
||||
parts.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
// 7. 研究实施步骤
|
||||
parts.push(`## 7. 研究实施步骤与技术路线\n\n`);
|
||||
if (context.pico) {
|
||||
if (context.pico.intervention) {
|
||||
parts.push(`**干预措施**:${context.pico.intervention}\n\n`);
|
||||
}
|
||||
if (context.pico.comparison) {
|
||||
parts.push(`**对照措施**:${context.pico.comparison}\n\n`);
|
||||
}
|
||||
}
|
||||
parts.push(`(技术路线图待补充)\n\n`);
|
||||
|
||||
// 8. 观察指标
|
||||
parts.push(`## 8. 观察指标\n\n`);
|
||||
if (context.endpoints?.outcomes) {
|
||||
const outcomes = context.endpoints.outcomes;
|
||||
if (outcomes.primary && outcomes.primary.length > 0) {
|
||||
parts.push(`### 主要结局指标\n\n`);
|
||||
outcomes.primary.forEach(o => parts.push(`- ${o}\n`));
|
||||
parts.push('\n');
|
||||
}
|
||||
if (outcomes.secondary && outcomes.secondary.length > 0) {
|
||||
parts.push(`### 次要结局指标\n\n`);
|
||||
outcomes.secondary.forEach(o => parts.push(`- ${o}\n`));
|
||||
parts.push('\n');
|
||||
}
|
||||
if (outcomes.safety && outcomes.safety.length > 0) {
|
||||
parts.push(`### 安全性指标\n\n`);
|
||||
outcomes.safety.forEach(o => parts.push(`- ${o}\n`));
|
||||
parts.push('\n');
|
||||
}
|
||||
}
|
||||
if (context.endpoints?.confounders && context.endpoints.confounders.length > 0) {
|
||||
parts.push(`### 潜在混杂因素\n\n`);
|
||||
context.endpoints.confounders.forEach(c => parts.push(`- ${c}\n`));
|
||||
parts.push('\n');
|
||||
}
|
||||
|
||||
// 9-14. 其他章节(占位)
|
||||
parts.push(`## 9. 数据管理与质量控制\n\n(待补充)\n\n`);
|
||||
parts.push(`## 10. 安全性评价\n\n(待补充)\n\n`);
|
||||
parts.push(`## 11. 统计分析计划\n\n(待补充)\n\n`);
|
||||
parts.push(`## 12. 伦理与知情同意\n\n本研究将遵循赫尔辛基宣言的伦理原则,并提交机构伦理委员会审批。\n\n`);
|
||||
parts.push(`## 13. 研究时间表\n\n(待补充)\n\n`);
|
||||
parts.push(`## 14. 参考文献\n\n(待补充)\n\n`);
|
||||
|
||||
return parts.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据上下文生成研究题目
|
||||
*/
|
||||
private generateTitle(context: ContextData): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
// PICO 要素
|
||||
if (context.pico) {
|
||||
if (context.pico.population) parts.push(context.pico.population);
|
||||
if (context.pico.intervention) parts.push(`使用${context.pico.intervention}`);
|
||||
if (context.pico.comparison) parts.push(`与${context.pico.comparison}对比`);
|
||||
if (context.pico.outcome) parts.push(`对${context.pico.outcome}影响`);
|
||||
}
|
||||
|
||||
if (parts.length > 0) {
|
||||
// 研究类型后缀
|
||||
if (context.studyDesign?.studyType) {
|
||||
parts.push(`的${context.studyDesign.studyType}`);
|
||||
} else {
|
||||
parts.push('的临床研究');
|
||||
}
|
||||
return parts.join('');
|
||||
}
|
||||
|
||||
// 使用科学问题作为标题
|
||||
if (context.scientificQuestion?.content) {
|
||||
return context.scientificQuestion.content;
|
||||
}
|
||||
|
||||
return '临床研究方案';
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Markdown 转换为 Word 文档
|
||||
*/
|
||||
async convertToDocx(markdown: string): Promise<Buffer> {
|
||||
try {
|
||||
logger.info('[ProtocolExportService] 开始转换 Markdown → Word');
|
||||
|
||||
const response = await axios.post(
|
||||
`${EXTRACTION_SERVICE_URL}/api/convert/docx`,
|
||||
{
|
||||
content: markdown,
|
||||
use_template: true,
|
||||
title: '临床研究方案',
|
||||
},
|
||||
{
|
||||
responseType: 'arraybuffer',
|
||||
timeout: 30000, // 30秒超时
|
||||
}
|
||||
);
|
||||
|
||||
logger.info(`[ProtocolExportService] Word 转换成功, 大小: ${response.data.length} bytes`);
|
||||
return Buffer.from(response.data);
|
||||
} catch (error) {
|
||||
logger.error('[ProtocolExportService] Word 转换失败:', error);
|
||||
throw new Error(`Word 转换失败: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出研究方案为 Word
|
||||
*/
|
||||
async exportProtocol(
|
||||
conversationId: string,
|
||||
context: ContextData,
|
||||
title?: string
|
||||
): Promise<{
|
||||
buffer: Buffer;
|
||||
filename: string;
|
||||
contentType: string;
|
||||
}> {
|
||||
// 1. 生成 Markdown
|
||||
const markdown = this.generateProtocolMarkdown(context, title);
|
||||
|
||||
// 2. 转换为 Word
|
||||
const buffer = await this.convertToDocx(markdown);
|
||||
|
||||
// 3. 生成文件名
|
||||
const timestamp = new Date().toISOString().slice(0, 10);
|
||||
const filename = `研究方案_${timestamp}.docx`;
|
||||
|
||||
return {
|
||||
buffer,
|
||||
filename,
|
||||
contentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,51 +212,90 @@ export class ProtocolOrchestrator extends BaseAgentOrchestrator {
|
||||
): Promise<Record<string, unknown>> {
|
||||
// 构建凝练 Prompt
|
||||
const condensePrompts: Record<ProtocolStageCode, string> = {
|
||||
scientific_question: `请将以下科学问题内容凝练成一句话(不超过50字),保留核心要点:
|
||||
scientific_question: `请提取并整理以下科学问题的完整内容:
|
||||
|
||||
原始内容:
|
||||
${JSON.stringify(data, null, 2)}
|
||||
|
||||
要求:
|
||||
- 输出格式:{ "content": "一句话科学问题" }
|
||||
- 保留完整的科学问题描述(100-200字)
|
||||
- 包含研究背景、研究对象、研究目的
|
||||
- 输出格式:{ "content": "完整科学问题描述" }
|
||||
- 只输出 JSON,不要其他内容`,
|
||||
|
||||
pico: `请将以下 PICO 要素凝练成简短描述:
|
||||
pico: `请提取并整理以下 PICO 要素的详细内容:
|
||||
|
||||
原始内容:
|
||||
${JSON.stringify(data, null, 2)}
|
||||
|
||||
要求:
|
||||
- 每个要素不超过20字
|
||||
- Population(研究人群):50-100字,包含纳入排除标准要点
|
||||
- Intervention(干预措施):50-100字,包含具体干预方法
|
||||
- Comparison(对照):50-100字,包含对照组设置
|
||||
- Outcome(结局指标):50-100字,包含主要和次要结局
|
||||
- 输出格式:{ "population": "...", "intervention": "...", "comparison": "...", "outcome": "..." }
|
||||
- 只输出 JSON,不要其他内容`,
|
||||
|
||||
study_design: `请将以下研究设计凝练成关键标签:
|
||||
study_design: `请提取并整理以下研究设计的详细内容:
|
||||
|
||||
原始内容:
|
||||
${JSON.stringify(data, null, 2)}
|
||||
|
||||
要求:
|
||||
- 输出格式:{ "studyType": "研究类型", "design": ["特征1", "特征2"] }
|
||||
- 包含研究类型、设计特征、分组方法、盲法等
|
||||
- 输出格式:{ "studyType": "研究类型(如:前瞻性队列研究)", "design": ["设计特征1", "设计特征2", "..."], "details": "其他设计细节说明" }
|
||||
- 只输出 JSON,不要其他内容`,
|
||||
|
||||
sample_size: `请提取样本量关键数据:
|
||||
sample_size: `请提取并整理以下样本量计算的完整内容:
|
||||
|
||||
原始内容:
|
||||
${JSON.stringify(data, null, 2)}
|
||||
|
||||
要求:
|
||||
- 输出格式:{ "sampleSize": 数字, "calculation": { "alpha": 数字, "power": 数字 } }
|
||||
- 保留计算过程和参数依据
|
||||
- 输出格式:
|
||||
{
|
||||
"sampleSize": 总样本量数字,
|
||||
"calculation": {
|
||||
"alpha": α值,
|
||||
"power": 检验效能,
|
||||
"effectSize": "效应值及来源说明"
|
||||
},
|
||||
"rationale": "参数设定依据(如文献来源、预实验数据等)",
|
||||
"groups": [
|
||||
{ "name": "组名", "size": 人数 }
|
||||
],
|
||||
"dropoutRate": "预计失访率及调整说明"
|
||||
}
|
||||
- 只输出 JSON,不要其他内容`,
|
||||
|
||||
endpoints: `请将以下观察指标凝练成简短列表:
|
||||
endpoints: `请提取并整理以下观察指标的详细内容:
|
||||
|
||||
原始内容:
|
||||
${JSON.stringify(data, null, 2)}
|
||||
|
||||
要求:
|
||||
- 每个指标不超过10字
|
||||
- 输出格式:{ "baseline": {...}, "exposure": {...}, "outcomes": {...}, "confounders": [...] }
|
||||
- 保留完整的指标定义、测量方法和时间点
|
||||
- 输出格式:
|
||||
{
|
||||
"baseline": {
|
||||
"demographics": ["人口学特征1", "人口学特征2", "..."],
|
||||
"clinicalHistory": ["临床病史1", "临床病史2", "..."]
|
||||
},
|
||||
"exposure": {
|
||||
"name": "暴露因素名称",
|
||||
"definition": "暴露的具体定义",
|
||||
"measurement": "测量方法",
|
||||
"timing": "测量时间点"
|
||||
},
|
||||
"outcomes": {
|
||||
"primary": ["主要结局指标1(含定义)", "主要结局指标2(含定义)"],
|
||||
"secondary": ["次要结局指标1(含定义)", "次要结局指标2(含定义)"],
|
||||
"safety": ["安全性指标1", "安全性指标2"]
|
||||
},
|
||||
"confounders": ["混杂因素1", "混杂因素2", "..."],
|
||||
"followUp": "随访方案和时间节点"
|
||||
}
|
||||
- 只输出 JSON,不要其他内容`,
|
||||
};
|
||||
|
||||
@@ -428,7 +467,7 @@ ${JSON.stringify(data, null, 2)}
|
||||
stageName: STAGE_NAMES[context.currentStage] || context.currentStage,
|
||||
progress: this.contextService.getProgress(context),
|
||||
stages: this.contextService.getStagesStatus(context),
|
||||
canGenerate: this.contextService.isAllStagesCompleted(context),
|
||||
canGenerate: this.contextService.canGenerateProtocol(context),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user