128 lines
4.6 KiB
TypeScript
128 lines
4.6 KiB
TypeScript
// File: src/modules/ai/ai-validation.service.ts
|
|
// Service ตรวจสอบผลลัพธ์จาก AI ก่อน Write ลง Database (ADR-018 Rule 4)
|
|
|
|
import { Injectable, Logger } from '@nestjs/common';
|
|
import { AiCallbackDto } from './dto/ai-callback.dto';
|
|
import { AiAuditStatus } from './entities/ai-audit-log.entity';
|
|
|
|
// ผลการตรวจสอบ AI Output
|
|
export interface AiValidationResult {
|
|
isValid: boolean;
|
|
action: ConfidenceAction;
|
|
confidence: number;
|
|
reasons: string[]; // เหตุผลที่ validation ผ่านหรือไม่ผ่าน
|
|
}
|
|
|
|
// Action ที่ต้องทำตามระดับ Confidence (ADR-020 Confidence Scoring Strategy)
|
|
export type ConfidenceAction =
|
|
| 'auto_approve' // 0.95-1.00: นำเข้าอัตโนมัติ (Migration เท่านั้น)
|
|
| 'low_priority_review' // 0.85-0.94: รอตรวจสอบ ลำดับต่ำ
|
|
| 'high_priority_review' // 0.60-0.84: รอตรวจสอบ ลำดับสูง
|
|
| 'reject'; // <0.60: ปฏิเสธ ต้องกรอกเอง
|
|
|
|
// Discipline values ที่ถูกต้องตาม Prompt Strategy ใน ADR-020
|
|
const VALID_DISCIPLINES = [
|
|
'Civil',
|
|
'Mechanical',
|
|
'Electrical',
|
|
'Architectural',
|
|
] as const;
|
|
type ValidDiscipline = (typeof VALID_DISCIPLINES)[number];
|
|
|
|
function isValidDiscipline(value: unknown): value is ValidDiscipline {
|
|
return (
|
|
typeof value === 'string' &&
|
|
(VALID_DISCIPLINES as readonly string[]).includes(value)
|
|
);
|
|
}
|
|
|
|
@Injectable()
|
|
export class AiValidationService {
|
|
private readonly logger = new Logger(AiValidationService.name);
|
|
|
|
// Confidence Thresholds ตาม ADR-020
|
|
private readonly THRESHOLD_AUTO_APPROVE = 0.95;
|
|
private readonly THRESHOLD_LOW_PRIORITY = 0.85;
|
|
private readonly THRESHOLD_HIGH_PRIORITY = 0.6;
|
|
|
|
// กำหนด Action ตาม Confidence Score
|
|
getConfidenceAction(confidence: number): ConfidenceAction {
|
|
if (confidence >= this.THRESHOLD_AUTO_APPROVE) return 'auto_approve';
|
|
if (confidence >= this.THRESHOLD_LOW_PRIORITY) return 'low_priority_review';
|
|
if (confidence >= this.THRESHOLD_HIGH_PRIORITY)
|
|
return 'high_priority_review';
|
|
return 'reject';
|
|
}
|
|
|
|
// ตรวจสอบ AI Output ก่อน Write ลง Database (ADR-018 Rule 4)
|
|
validateAiOutput(payload: AiCallbackDto): AiValidationResult {
|
|
const reasons: string[] = [];
|
|
|
|
// 1. ตรวจสอบ Status จาก AI
|
|
if (payload.status !== AiAuditStatus.SUCCESS) {
|
|
reasons.push(`AI processing failed with status: ${payload.status}`);
|
|
this.logger.warn(
|
|
`AI validation failed — status=${payload.status}, model=${payload.aiModel}`
|
|
);
|
|
return { isValid: false, action: 'reject', confidence: 0, reasons };
|
|
}
|
|
|
|
// 2. ตรวจสอบ Confidence Score
|
|
const confidence = payload.confidenceScore ?? 0;
|
|
const action = this.getConfidenceAction(confidence);
|
|
|
|
if (action === 'reject') {
|
|
reasons.push(
|
|
`Confidence score ${confidence} is below minimum threshold ${this.THRESHOLD_HIGH_PRIORITY}`
|
|
);
|
|
}
|
|
|
|
// 3. ตรวจสอบ Extracted Metadata (Enum Enforcement)
|
|
if (payload.extractedMetadata) {
|
|
const { discipline } = payload.extractedMetadata;
|
|
if (discipline !== undefined && !isValidDiscipline(discipline)) {
|
|
reasons.push(
|
|
`Invalid discipline value: "${String(discipline)}". Must be one of: ${VALID_DISCIPLINES.join(', ')}`
|
|
);
|
|
}
|
|
|
|
// ตรวจสอบรูปแบบ Date
|
|
if (payload.extractedMetadata.date) {
|
|
const dateStr = String(payload.extractedMetadata.date);
|
|
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
|
|
if (!dateRegex.test(dateStr)) {
|
|
reasons.push(
|
|
`Invalid date format: "${dateStr}". Expected YYYY-MM-DD`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
const isValid = action !== 'reject' && reasons.length === 0;
|
|
|
|
this.logger.log(
|
|
`AI validation — model=${payload.aiModel}, confidence=${confidence}, action=${action}, valid=${isValid}`
|
|
);
|
|
|
|
return { isValid, action, confidence, reasons };
|
|
}
|
|
|
|
// สร้าง Summary สำหรับ Log (ADR-018 Rule 5 Audit Logging)
|
|
buildAuditSummary(
|
|
payload: AiCallbackDto,
|
|
validationResult: AiValidationResult
|
|
): string {
|
|
return [
|
|
`model=${payload.aiModel}`,
|
|
`confidence=${validationResult.confidence}`,
|
|
`action=${validationResult.action}`,
|
|
`valid=${validationResult.isValid}`,
|
|
validationResult.reasons.length > 0
|
|
? `reasons=[${validationResult.reasons.join('; ')}]`
|
|
: null,
|
|
]
|
|
.filter(Boolean)
|
|
.join(', ');
|
|
}
|
|
}
|