385 lines
12 KiB
TypeScript
385 lines
12 KiB
TypeScript
/**
|
|
* CRA 智能质控引擎 - 默认规则配置 Seed
|
|
*
|
|
* 五大规则体系:
|
|
* 1. 变量质控 (VARIABLE_QC) - 硬规则
|
|
* 2. 入排标准 (INCLUSION_EXCLUSION) - LLM 检查
|
|
* 3. 方案偏离 (PROTOCOL_DEVIATION) - 混合规则
|
|
* 4. AE 监测 (AE_MONITORING) - LLM 检查
|
|
* 5. 伦理合规 (ETHICS_COMPLIANCE) - 硬规则
|
|
*/
|
|
|
|
import { PrismaClient } from '@prisma/client';
|
|
|
|
const prisma = new PrismaClient();
|
|
|
|
/**
|
|
* 默认 Skills 配置
|
|
*/
|
|
const DEFAULT_SKILLS = [
|
|
// ============================================================
|
|
// 1. 变量质控 - 硬规则 (Level: normal, 实时触发)
|
|
// ============================================================
|
|
{
|
|
skillType: 'variable_qc',
|
|
name: '变量质控',
|
|
description: '针对每个变量的数据校验,包括空值检查、数值范围、格式验证',
|
|
ruleType: 'HARD_RULE',
|
|
level: 'normal',
|
|
priority: 100,
|
|
triggerType: 'webhook',
|
|
requiredTags: ['#demographics', '#lab'],
|
|
config: {
|
|
engine: 'HardRuleEngine',
|
|
rules: [
|
|
{
|
|
id: 'VQ-001',
|
|
name: '年龄范围检查',
|
|
field: 'age',
|
|
formName: 'demographics',
|
|
logic: {
|
|
and: [
|
|
{ '>=': [{ var: 'age' }, 18] },
|
|
{ '<=': [{ var: 'age' }, 100] },
|
|
],
|
|
},
|
|
message: '年龄超出合理范围 (18-100岁)',
|
|
severity: 'error',
|
|
category: 'lab_values',
|
|
},
|
|
{
|
|
id: 'VQ-002',
|
|
name: 'BMI 范围检查',
|
|
field: 'bmi',
|
|
formName: 'demographics',
|
|
logic: {
|
|
and: [
|
|
{ '>': [{ var: 'bmi' }, 10] },
|
|
{ '<': [{ var: 'bmi' }, 60] },
|
|
],
|
|
},
|
|
message: 'BMI 值异常 (正常范围 10-60)',
|
|
severity: 'warning',
|
|
category: 'lab_values',
|
|
},
|
|
{
|
|
id: 'VQ-003',
|
|
name: '体重范围检查',
|
|
field: 'weight',
|
|
formName: 'demographics',
|
|
logic: {
|
|
and: [
|
|
{ '>': [{ var: 'weight' }, 20] },
|
|
{ '<': [{ var: 'weight' }, 300] },
|
|
],
|
|
},
|
|
message: '体重值异常 (正常范围 20-300kg)',
|
|
severity: 'warning',
|
|
category: 'lab_values',
|
|
},
|
|
{
|
|
id: 'VQ-004',
|
|
name: '身高范围检查',
|
|
field: 'height',
|
|
formName: 'demographics',
|
|
logic: {
|
|
and: [
|
|
{ '>': [{ var: 'height' }, 50] },
|
|
{ '<': [{ var: 'height' }, 250] },
|
|
],
|
|
},
|
|
message: '身高值异常 (正常范围 50-250cm)',
|
|
severity: 'warning',
|
|
category: 'lab_values',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
|
|
// ============================================================
|
|
// 2. 入排标准 - LLM 检查 (Level: normal, 定时触发)
|
|
// ============================================================
|
|
{
|
|
skillType: 'inclusion_exclusion',
|
|
name: '入排标准核查',
|
|
description: '判断受试者是否符合研究方案的入组标准和排除标准',
|
|
ruleType: 'LLM_CHECK',
|
|
level: 'normal',
|
|
priority: 200,
|
|
triggerType: 'cron',
|
|
cronSchedule: '0 2 * * *', // 每日凌晨 2 点
|
|
requiredTags: ['#demographics', '#medical_history', '#lab'],
|
|
config: {
|
|
engine: 'SoftRuleEngine',
|
|
model: 'deepseek-v3',
|
|
systemPrompt: '你是一个专业的临床研究监查员,负责核查受试者是否符合入组标准。',
|
|
checks: [
|
|
{
|
|
id: 'IE-001',
|
|
name: '年龄入组标准',
|
|
desc: '检查受试者年龄是否符合研究方案规定的入组年龄范围',
|
|
promptTemplate: `请根据以下受试者数据,判断其年龄是否符合入组标准。
|
|
|
|
受试者年龄: {{age}}
|
|
|
|
研究方案通常要求受试者年龄在 18-75 岁之间。请判断此受试者是否符合年龄入组标准。`,
|
|
requiredTags: ['#demographics'],
|
|
category: 'inclusion',
|
|
severity: 'critical',
|
|
},
|
|
{
|
|
id: 'IE-002',
|
|
name: '确诊时间入组标准',
|
|
desc: '检查受试者确诊时间是否在研究方案规定的时间窗口内',
|
|
promptTemplate: `请根据以下受试者数据,判断其确诊时间是否符合入组标准。
|
|
|
|
确诊日期: {{diagnosis_date}}
|
|
入组日期: {{enrollment_date}}
|
|
|
|
研究方案通常要求确诊时间在入组前一定时间内(如 3 个月、6 个月等)。请判断此受试者是否符合确诊时间入组标准。`,
|
|
requiredTags: ['#demographics', '#medical_history'],
|
|
category: 'inclusion',
|
|
severity: 'critical',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
|
|
// ============================================================
|
|
// 3. 方案偏离 - 混合规则 (Level: normal, 定时触发)
|
|
// ============================================================
|
|
{
|
|
skillType: 'protocol_deviation',
|
|
name: '方案偏离检测',
|
|
description: '检测访视超窗、漏做检查、违反用药规定等方案偏离情况',
|
|
ruleType: 'HYBRID',
|
|
level: 'normal',
|
|
priority: 300,
|
|
triggerType: 'cron',
|
|
cronSchedule: '0 3 * * *', // 每日凌晨 3 点
|
|
requiredTags: ['#visits', '#medications'],
|
|
config: {
|
|
engine: 'HybridEngine',
|
|
hardRules: [
|
|
{
|
|
id: 'PD-001',
|
|
name: '访视间隔检查',
|
|
field: ['visit_1_date', 'visit_2_date'],
|
|
logic: {
|
|
'<=': [
|
|
{ '-': [{ var: 'visit_2_date' }, { var: 'visit_1_date' }] },
|
|
31, // 最大间隔 28+3 天
|
|
],
|
|
},
|
|
message: '访视超窗:访视 2 与访视 1 间隔超过 31 天',
|
|
severity: 'warning',
|
|
category: 'logic_check',
|
|
},
|
|
],
|
|
softChecks: [
|
|
{
|
|
id: 'PD-002',
|
|
name: '禁用药物检查',
|
|
desc: '检查受试者是否使用了研究方案禁止的药物',
|
|
promptTemplate: '请检查受试者的用药记录,判断是否存在使用研究方案禁止药物的情况。',
|
|
requiredTags: ['#medications'],
|
|
category: 'protocol_deviation',
|
|
severity: 'warning',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
|
|
// ============================================================
|
|
// 4. AE 监测 - LLM 检查 (Level: normal, 定时触发)
|
|
// ============================================================
|
|
{
|
|
skillType: 'ae_monitoring',
|
|
name: 'AE 事件监测',
|
|
description: '检测未报告的不良事件,包括实验室异常与 AE 记录的一致性检查',
|
|
ruleType: 'LLM_CHECK',
|
|
level: 'normal',
|
|
priority: 250,
|
|
triggerType: 'cron',
|
|
cronSchedule: '0 4 * * *', // 每日凌晨 4 点
|
|
requiredTags: ['#lab', '#ae'],
|
|
config: {
|
|
engine: 'SoftRuleEngine',
|
|
model: 'deepseek-v3',
|
|
systemPrompt: '你是一个专业的药物安全监查员,负责核查不良事件报告的完整性和准确性。',
|
|
checks: [
|
|
{
|
|
id: 'AE-001',
|
|
name: 'Lab 异常与 AE 一致性',
|
|
desc: '检查实验室检查异常值是否已在 AE 表中报告',
|
|
promptTemplate: `请对比以下实验室检查数据和不良事件记录,判断是否存在未报告的实验室异常。
|
|
|
|
重点关注:
|
|
1. Grade 3 及以上的实验室异常是否已记录为 AE
|
|
2. 持续异常是否已报告
|
|
3. 与基线相比显著变化的指标
|
|
|
|
请给出判断结果,并列出可能遗漏报告的异常。`,
|
|
requiredTags: ['#lab', '#ae'],
|
|
category: 'ae_detection',
|
|
severity: 'critical',
|
|
},
|
|
{
|
|
id: 'AE-002',
|
|
name: 'SAE 报告时效性',
|
|
desc: '检查严重不良事件是否在规定时间内报告',
|
|
promptTemplate: `请检查以下 SAE 记录,判断报告时效是否符合法规要求。
|
|
|
|
通常要求:
|
|
- 致死/危及生命的 SAE: 24 小时内报告
|
|
- 其他 SAE: 15 天内报告
|
|
|
|
请判断各 SAE 的报告时效是否合规。`,
|
|
requiredTags: ['#ae'],
|
|
category: 'ae_detection',
|
|
severity: 'critical',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
|
|
// ============================================================
|
|
// 5. 伦理合规 - 硬规则 (Level: blocking, 实时触发)
|
|
// ============================================================
|
|
{
|
|
skillType: 'ethics_compliance',
|
|
name: '伦理合规检查',
|
|
description: '检查知情同意书签署时间、隐私保护等伦理合规问题',
|
|
ruleType: 'HARD_RULE',
|
|
level: 'blocking', // 阻断性检查,失败则跳过后续 AI 检查
|
|
priority: 10, // 最高优先级
|
|
triggerType: 'webhook',
|
|
requiredTags: ['#consent', '#demographics'],
|
|
config: {
|
|
engine: 'HardRuleEngine',
|
|
rules: [
|
|
{
|
|
id: 'EC-001',
|
|
name: '知情同意书签署时间',
|
|
field: ['icf_date', 'first_visit_date'],
|
|
logic: {
|
|
'<=': [{ var: 'icf_date' }, { var: 'first_visit_date' }],
|
|
},
|
|
message: '伦理违规:知情同意书签署日期晚于首次访视日期',
|
|
severity: 'error',
|
|
category: 'ethics',
|
|
},
|
|
{
|
|
id: 'EC-002',
|
|
name: '知情同意书必填检查',
|
|
field: 'icf_date',
|
|
logic: {
|
|
'!!': [{ var: 'icf_date' }],
|
|
},
|
|
message: '伦理违规:缺少知情同意书签署日期',
|
|
severity: 'error',
|
|
category: 'ethics',
|
|
},
|
|
{
|
|
id: 'EC-003',
|
|
name: '受试者 ID 必填检查',
|
|
field: 'record_id',
|
|
logic: {
|
|
'!!': [{ var: 'record_id' }],
|
|
},
|
|
message: '数据完整性:缺少受试者 ID',
|
|
severity: 'error',
|
|
category: 'ethics',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
];
|
|
|
|
/**
|
|
* 为项目创建默认 Skills
|
|
*
|
|
* @param projectId 项目 ID
|
|
*/
|
|
export async function seedDefaultSkills(projectId: string): Promise<void> {
|
|
console.log(`[Seed] Creating default skills for project: ${projectId}`);
|
|
|
|
for (const skillData of DEFAULT_SKILLS) {
|
|
try {
|
|
// 使用 upsert 避免重复创建
|
|
await prisma.iitSkill.upsert({
|
|
where: {
|
|
projectId_skillType: {
|
|
projectId,
|
|
skillType: skillData.skillType,
|
|
},
|
|
},
|
|
update: {
|
|
name: skillData.name,
|
|
description: skillData.description,
|
|
ruleType: skillData.ruleType,
|
|
level: skillData.level,
|
|
priority: skillData.priority,
|
|
triggerType: skillData.triggerType,
|
|
cronSchedule: skillData.cronSchedule,
|
|
requiredTags: skillData.requiredTags,
|
|
config: skillData.config,
|
|
isActive: true,
|
|
updatedAt: new Date(),
|
|
},
|
|
create: {
|
|
projectId,
|
|
skillType: skillData.skillType,
|
|
name: skillData.name,
|
|
description: skillData.description,
|
|
ruleType: skillData.ruleType,
|
|
level: skillData.level,
|
|
priority: skillData.priority,
|
|
triggerType: skillData.triggerType,
|
|
cronSchedule: skillData.cronSchedule,
|
|
requiredTags: skillData.requiredTags,
|
|
config: skillData.config,
|
|
isActive: true,
|
|
},
|
|
});
|
|
|
|
console.log(`[Seed] Created/Updated skill: ${skillData.name}`);
|
|
} catch (error: any) {
|
|
console.error(`[Seed] Failed to create skill: ${skillData.name}`, error.message);
|
|
}
|
|
}
|
|
|
|
console.log(`[Seed] Completed creating default skills for project: ${projectId}`);
|
|
}
|
|
|
|
/**
|
|
* 主函数 - 可以直接运行
|
|
*/
|
|
async function main() {
|
|
// 获取命令行参数
|
|
const projectId = process.argv[2];
|
|
|
|
if (!projectId) {
|
|
console.error('Usage: npx ts-node prisma/seeds/cra-qc-skills.seed.ts <projectId>');
|
|
console.error('Example: npx ts-node prisma/seeds/cra-qc-skills.seed.ts test0102-pd-study');
|
|
process.exit(1);
|
|
}
|
|
|
|
// 检查项目是否存在
|
|
const project = await prisma.iitProject.findUnique({
|
|
where: { id: projectId },
|
|
});
|
|
|
|
if (!project) {
|
|
console.error(`Project not found: ${projectId}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
await seedDefaultSkills(projectId);
|
|
}
|
|
|
|
// 如果直接运行此文件
|
|
main()
|
|
.catch(console.error)
|
|
.finally(() => prisma.$disconnect());
|