Files
AIclinicalresearch/backend/src/modules/iit-manager/engines/HardRuleEngine.js

355 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* HardRuleEngine - 硬规则质控引擎
*
* 功能:
* - 基于 JSON Logic 执行质控规则
* - 支持纳入标准、排除标准、变量范围检查
* - 返回结构化的质控结果
*
* 设计原则:
* - 零容忍:规则判断是确定性的,不依赖 AI 猜测
* - 可追溯:每条规则执行结果都有详细记录
* - 高性能:纯逻辑计算,无 LLM 调用
*/
import jsonLogic from 'json-logic-js';
import { PrismaClient } from '@prisma/client';
import { logger } from '../../../common/logging/index.js';
const prisma = new PrismaClient();
// ============================================================
// HardRuleEngine 实现
// ============================================================
export class HardRuleEngine {
constructor(projectId) {
this.rules = [];
this.fieldMappings = new Map();
this.projectId = projectId;
}
/**
* 初始化引擎(加载规则和字段映射)
*
* @param formName 可选,按表单名过滤规则(用于单表实时质控)
*/
async initialize(formName) {
// 1. 加载质控规则
const skill = await prisma.iitSkill.findFirst({
where: {
projectId: this.projectId,
skillType: 'qc_process',
isActive: true
}
});
if (!skill) {
throw new Error(`No active QC rules found for project: ${this.projectId}`);
}
const config = skill.config;
let allRules = config.rules || [];
// ⭐ 如果指定了 formName则只加载该表单相关的规则
// 规则通过 formName 或 field 字段来判断所属表单
if (formName) {
allRules = allRules.filter((rule) => {
// 优先使用规则中的 formName 字段
if (rule.formName) {
return rule.formName === formName;
}
// 如果规则没有 formName则默认包含兼容旧规则
// TODO: 后续可以通过 field_metadata 表来判断字段所属表单
return true;
});
}
this.rules = allRules;
logger.info('[HardRuleEngine] Rules loaded', {
projectId: this.projectId,
ruleCount: this.rules.length
});
// 2. 加载字段映射
const mappings = await prisma.iitFieldMapping.findMany({
where: { projectId: this.projectId }
});
for (const m of mappings) {
this.fieldMappings.set(m.aliasName, m.actualName);
}
logger.info('[HardRuleEngine] Field mappings loaded', {
mappingCount: this.fieldMappings.size
});
}
/**
* 执行质控检查
*
* @param recordId 记录ID
* @param data 记录数据REDCap 格式)
* @returns 质控结果
*/
execute(recordId, data) {
const startTime = Date.now();
const results = [];
const errors = [];
const warnings = [];
// 1. 数据预处理:应用字段映射
const normalizedData = this.normalizeData(data);
// 2. 执行每条规则
for (const rule of this.rules) {
const result = this.executeRule(rule, normalizedData);
results.push(result);
if (!result.passed) {
if (result.severity === 'error') {
errors.push(result);
}
else if (result.severity === 'warning') {
warnings.push(result);
}
}
}
// 3. 计算整体状态
let overallStatus = 'PASS';
if (errors.length > 0) {
overallStatus = 'FAIL';
}
else if (warnings.length > 0) {
overallStatus = 'WARNING';
}
const duration = Date.now() - startTime;
logger.info('[HardRuleEngine] QC completed', {
recordId,
overallStatus,
totalRules: this.rules.length,
errors: errors.length,
warnings: warnings.length,
duration: `${duration}ms`
});
return {
recordId,
projectId: this.projectId,
timestamp: new Date().toISOString(),
overallStatus,
summary: {
totalRules: this.rules.length,
passed: results.filter(r => r.passed).length,
failed: errors.length,
warnings: warnings.length
},
results,
errors,
warnings
};
}
/**
* 批量质控检查
*
* @param records 记录数组
* @returns 质控结果数组
*/
executeBatch(records) {
return records.map(r => this.execute(r.recordId, r.data));
}
/**
* 执行单条规则
*
* V2.1 优化:生成自包含的 LLM 友好消息
*/
executeRule(rule, data) {
try {
// 获取字段值
const fieldValue = this.getFieldValue(rule.field, data);
// 执行 JSON Logic
const passed = jsonLogic.apply(rule.logic, data);
// V2.1: 解析期望值(从 JSON Logic 中提取)
const expectedValue = this.extractExpectedValue(rule.logic);
const expectedCondition = this.describeLogic(rule.logic);
// V2.1: 构建自包含的 LLM 友好消息
const llmMessage = passed
? '通过'
: this.buildLlmMessage(rule, fieldValue, expectedValue);
// V2.1: 构建结构化证据
const evidence = {
value: fieldValue,
threshold: expectedValue,
unit: rule.metadata?.unit,
};
return {
ruleId: rule.id,
ruleName: rule.name,
field: rule.field,
passed,
message: passed ? '通过' : rule.message,
llmMessage,
severity: rule.severity,
category: rule.category,
actualValue: fieldValue,
expectedValue,
expectedCondition,
evidence,
};
}
catch (error) {
logger.error('[HardRuleEngine] Rule execution error', {
ruleId: rule.id,
error: error.message
});
return {
ruleId: rule.id,
ruleName: rule.name,
field: rule.field,
passed: false,
message: `规则执行出错: ${error.message}`,
llmMessage: `规则执行出错: ${error.message}`,
severity: 'error',
category: rule.category
};
}
}
/**
* V2.1: 从 JSON Logic 中提取期望值
*/
extractExpectedValue(logic) {
const operator = Object.keys(logic)[0];
const args = logic[operator];
switch (operator) {
case '>=':
case '<=':
case '>':
case '<':
case '==':
case '!=':
return String(args[1]);
case 'and':
// 对于 and 逻辑,尝试提取范围
const values = args.map((a) => this.extractExpectedValue(a)).filter(Boolean);
if (values.length === 2) {
return `${values[0]}-${values[1]}`;
}
return values.join(', ');
case '!!':
return '非空/必填';
default:
return '';
}
}
/**
* V2.1: 构建 LLM 友好的自包含消息
*
* 格式:当前 **{actualValue}** (标准: {expectedValue})
*/
buildLlmMessage(rule, actualValue, expectedValue) {
const fieldName = Array.isArray(rule.field) ? rule.field.join(', ') : rule.field;
const displayValue = actualValue !== undefined && actualValue !== null && actualValue !== ''
? `**${actualValue}**`
: '**空**';
// 根据规则类别生成不同的消息格式
switch (rule.category) {
case 'inclusion':
return `**${rule.name}**: 当前值 ${displayValue} (入排标准: ${expectedValue || rule.message})`;
case 'exclusion':
return `**${rule.name}**: 当前值 ${displayValue} 触发排除条件`;
case 'lab_values':
return `**${rule.name}**: 当前值 ${displayValue} (正常范围: ${expectedValue})`;
case 'logic_check':
return `**${rule.name}**: \`${fieldName}\` = ${displayValue} (要求: ${expectedValue || '非空'})`;
default:
return `**${rule.name}**: 当前值 ${displayValue} (标准: ${expectedValue || rule.message})`;
}
}
/**
* 获取字段值(支持映射)
*/
getFieldValue(field, data) {
if (Array.isArray(field)) {
return field.map(f => data[f]);
}
// 先尝试直接获取
if (data[field] !== undefined) {
return data[field];
}
// 再尝试通过映射获取
const mappedField = this.fieldMappings.get(field);
if (mappedField && data[mappedField] !== undefined) {
return data[mappedField];
}
return undefined;
}
/**
* 数据标准化(应用字段映射,转换类型)
*/
normalizeData(data) {
const normalized = { ...data };
// 1. 应用字段映射反向映射actualName -> aliasName
for (const [alias, actual] of this.fieldMappings.entries()) {
if (data[actual] !== undefined && normalized[alias] === undefined) {
normalized[alias] = data[actual];
}
}
// 2. 类型转换(字符串数字转数字)
for (const [key, value] of Object.entries(normalized)) {
if (typeof value === 'string' && value !== '' && !isNaN(Number(value))) {
normalized[key] = Number(value);
}
}
return normalized;
}
/**
* 描述 JSON Logic 表达式(用于报告)
*/
describeLogic(logic) {
const operator = Object.keys(logic)[0];
const args = logic[operator];
switch (operator) {
case '>=':
return `>= ${args[1]}`;
case '<=':
return `<= ${args[1]}`;
case '>':
return `> ${args[1]}`;
case '<':
return `< ${args[1]}`;
case '==':
return `= ${args[1]}`;
case '!=':
return `${args[1]}`;
case 'and':
return args.map((a) => this.describeLogic(a)).join(' 且 ');
case 'or':
return args.map((a) => this.describeLogic(a)).join(' 或 ');
case '!!':
return '非空';
default:
return JSON.stringify(logic);
}
}
/**
* 获取规则列表
*/
getRules() {
return this.rules;
}
/**
* 获取规则统计
*/
getRuleStats() {
const byCategory = {};
const bySeverity = {};
for (const rule of this.rules) {
byCategory[rule.category] = (byCategory[rule.category] || 0) + 1;
bySeverity[rule.severity] = (bySeverity[rule.severity] || 0) + 1;
}
return {
total: this.rules.length,
byCategory,
bySeverity
};
}
}
// ============================================================
// 工厂函数
// ============================================================
/**
* 创建并初始化 HardRuleEngine
*
* @param projectId 项目ID
* @param formName 可选,按表单名过滤规则(用于单表实时质控)
* 如果不传,则加载所有规则(用于全案批量质控)
*/
export async function createHardRuleEngine(projectId, formName) {
const engine = new HardRuleEngine(projectId);
await engine.initialize(formName);
return engine;
}