feat(iit): Implement event-level QC architecture V3.1 with dynamic rule filtering, report deduplication and AI intent enhancement
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
-- CRA 智能质控引擎支持
|
||||
-- 扩展 IitSkill 表,新增 IitQcReport 表
|
||||
|
||||
-- AlterTable: 扩展 skills 表支持 CRA 五大规则体系
|
||||
ALTER TABLE "iit_schema"."skills" ADD COLUMN "level" TEXT NOT NULL DEFAULT 'normal',
|
||||
ADD COLUMN "priority" INTEGER NOT NULL DEFAULT 100,
|
||||
ADD COLUMN "required_tags" TEXT[] DEFAULT ARRAY[]::TEXT[],
|
||||
ADD COLUMN "rule_type" TEXT NOT NULL DEFAULT 'HARD_RULE';
|
||||
|
||||
-- CreateTable: 质控报告存储表
|
||||
CREATE TABLE "iit_schema"."qc_reports" (
|
||||
"id" TEXT NOT NULL,
|
||||
"project_id" TEXT NOT NULL,
|
||||
"report_type" TEXT NOT NULL DEFAULT 'daily',
|
||||
"summary" JSONB NOT NULL,
|
||||
"issues" JSONB NOT NULL,
|
||||
"llm_report" TEXT NOT NULL,
|
||||
"generated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"expires_at" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "qc_reports_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex: 报告索引
|
||||
CREATE INDEX "idx_qc_report_project_type" ON "iit_schema"."qc_reports"("project_id", "report_type");
|
||||
|
||||
CREATE INDEX "idx_qc_report_generated" ON "iit_schema"."qc_reports"("generated_at");
|
||||
|
||||
-- CreateIndex: Skill 触发类型索引
|
||||
CREATE INDEX "idx_iit_skill_active_trigger" ON "iit_schema"."skills"("is_active", "trigger_type");
|
||||
@@ -959,19 +959,26 @@ model IitAuditLog {
|
||||
|
||||
/// Skill 配置存储表 - 存储质控规则、SOP流程图
|
||||
/// 支持 webhook/cron/event 三种触发方式
|
||||
/// V3.0 扩展:支持 CRA 五大规则体系
|
||||
model IitSkill {
|
||||
id String @id @default(uuid())
|
||||
projectId String @map("project_id")
|
||||
skillType String @map("skill_type") // qc_process | daily_briefing | general_chat | weekly_report | visit_reminder
|
||||
skillType String @map("skill_type") // qc_process | daily_briefing | general_chat | weekly_report | visit_reminder | variable_qc | inclusion_exclusion | protocol_deviation | ae_monitoring | ethics_compliance
|
||||
name String // 技能名称
|
||||
description String? // 技能描述
|
||||
config Json @db.JsonB // 核心配置 JSON(SOP 流程图)
|
||||
config Json @db.JsonB // 核心配置 JSON(SOP 流程图 / 规则定义)
|
||||
isActive Boolean @default(true) @map("is_active")
|
||||
version Int @default(1)
|
||||
|
||||
// V2.9 新增:主动触发能力
|
||||
triggerType String @default("webhook") @map("trigger_type") // 'webhook' | 'cron' | 'event'
|
||||
cronSchedule String? @map("cron_schedule") // Cron 表达式,如 "0 9 * * *"
|
||||
triggerType String @default("webhook") @map("trigger_type") // 'webhook' | 'cron' | 'manual'
|
||||
cronSchedule String? @map("cron_schedule") // Cron 表达式,如 "0 2 * * *" (每日凌晨2点)
|
||||
|
||||
// V3.0 新增:CRA 质控引擎支持
|
||||
ruleType String @default("HARD_RULE") @map("rule_type") // 'HARD_RULE' | 'LLM_CHECK' | 'HYBRID'
|
||||
level String @default("normal") @map("level") // 'blocking' | 'normal' - 阻断性检查优先
|
||||
priority Int @default(100) @map("priority") // 执行优先级,数字越小越优先
|
||||
requiredTags String[] @default([]) @map("required_tags") // 数据依赖标签,如 ['#lab', '#demographics']
|
||||
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
@@ -979,10 +986,41 @@ model IitSkill {
|
||||
@@unique([projectId, skillType], map: "unique_iit_skill_project_type")
|
||||
@@index([projectId], map: "idx_iit_skill_project")
|
||||
@@index([isActive], map: "idx_iit_skill_active")
|
||||
@@index([isActive, triggerType], map: "idx_iit_skill_active_trigger")
|
||||
@@map("skills")
|
||||
@@schema("iit_schema")
|
||||
}
|
||||
|
||||
/// 质控报告存储表 - 存储预生成的 LLM 友好报告
|
||||
/// 报告驱动模式:后台预计算 → LLM 阅读报告 → 回答用户
|
||||
model IitQcReport {
|
||||
id String @id @default(uuid())
|
||||
projectId String @map("project_id")
|
||||
|
||||
// 报告类型
|
||||
reportType String @default("daily") @map("report_type") // 'daily' | 'weekly' | 'on_demand'
|
||||
|
||||
// 统计摘要
|
||||
summary Json @db.JsonB // { totalRecords, criticalIssues, pendingQueries, passRate }
|
||||
|
||||
// 详细问题列表
|
||||
issues Json @db.JsonB // [{ record, rule, severity, description, evidence }]
|
||||
|
||||
// LLM 友好的 XML 报告
|
||||
llmReport String @map("llm_report") @db.Text
|
||||
|
||||
// 报告生成时间
|
||||
generatedAt DateTime @default(now()) @map("generated_at")
|
||||
|
||||
// 报告有效期(过期后需重新生成)
|
||||
expiresAt DateTime? @map("expires_at")
|
||||
|
||||
@@index([projectId, reportType], map: "idx_qc_report_project_type")
|
||||
@@index([generatedAt], map: "idx_qc_report_generated")
|
||||
@@map("qc_reports")
|
||||
@@schema("iit_schema")
|
||||
}
|
||||
|
||||
/// 字段名映射字典表 - 解决 LLM 生成的字段名与 REDCap 实际字段名不一致的问题
|
||||
model IitFieldMapping {
|
||||
id String @id @default(uuid())
|
||||
|
||||
384
backend/prisma/seeds/cra-qc-skills.seed.ts
Normal file
384
backend/prisma/seeds/cra-qc-skills.seed.ts
Normal file
@@ -0,0 +1,384 @@
|
||||
/**
|
||||
* 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());
|
||||
Reference in New Issue
Block a user