Files
lcbp3/backend/src/modules/ai/tool/ai-tool-registry.service.ts
T
admin ea5499123e
CI / CD Pipeline / build (push) Failing after 3m57s
CI / CD Pipeline / deploy (push) Has been skipped
690519:1631 224 to 226 AI #01
2026-05-19 16:31:50 +07:00

132 lines
5.1 KiB
TypeScript

// File: src/modules/ai/tool/ai-tool-registry.service.ts
// Change Log
// - 2026-05-19: สร้าง AiToolRegistryService — Static Map จาก ServerIntent ไปยัง Tool Handlers (ADR-025).
// - 2026-05-19: เพิ่ม Audit Logging สำหรับทุก Tool Execution (ADR-023, FR-005).
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { v7 as uuidv7 } from 'uuid';
import { ServerIntent } from './types/server-intent.enum';
import { ToolCallResult } from './types/tool-call-result.type';
import { ToolHandlerContext } from './types/tool-handler-context.type';
import { RfaToolService } from './rfa-tool.service';
import { DrawingToolService } from './drawing-tool.service';
import { TransmittalToolService } from './transmittal-tool.service';
import { AiAuditLog, AiAuditStatus } from '../entities/ai-audit-log.entity';
/** ชนิดของ Tool Handler function */
type ToolHandler = (
context: ToolHandlerContext
) => Promise<ToolCallResult<unknown>>;
@Injectable()
export class AiToolRegistryService {
private readonly logger = new Logger(AiToolRegistryService.name);
/** Static Map จาก ServerIntent ไปยัง Tool Handler */
private readonly handlerMap: Map<ServerIntent, ToolHandler>;
constructor(
private readonly rfaToolService: RfaToolService,
private readonly drawingToolService: DrawingToolService,
private readonly transmittalToolService: TransmittalToolService,
@InjectRepository(AiAuditLog)
private readonly auditLogRepo: Repository<AiAuditLog>
) {
// ลงทะเบียน handlers ใน Static Map ตาม ADR-025
this.handlerMap = new Map<ServerIntent, ToolHandler>([
[ServerIntent.GET_RFA, (ctx) => this.rfaToolService.getRfa(ctx)],
[
ServerIntent.GET_DRAWING,
(ctx) => this.drawingToolService.getDrawing(ctx),
],
[
ServerIntent.GET_TRANSMITTAL,
(ctx) => this.transmittalToolService.getTransmittal(ctx),
],
]);
}
/**
* ส่ง Intent ไปยัง Tool Handler ที่ตรงกัน
* พร้อม Audit Logging ทุก Execution (FR-005)
*/
async dispatch(
intent: string,
context: ToolHandlerContext
): Promise<ToolCallResult<unknown>> {
const startMs = Date.now();
const handler = this.handlerMap.get(intent as ServerIntent);
if (!handler) {
this.logger.warn(`ไม่พบ Handler สำหรับ Intent: ${intent}`);
const result: ToolCallResult<unknown> = {
ok: false,
reason: 'INVALID_PARAMS',
message: `ไม่รองรับ Intent '${intent}'`,
};
await this.writeAuditLog(intent, context, result, Date.now() - startMs);
return result;
}
let result: ToolCallResult<unknown>;
try {
result = await handler(context);
} catch (error: unknown) {
const errMsg = error instanceof Error ? error.message : 'Unknown error';
this.logger.error(
`Tool Handler สำหรับ Intent '${intent}' เกิด exception: ${errMsg}`
);
result = {
ok: false,
reason: 'SERVICE_ERROR',
message: 'เกิดข้อผิดพลาดภายในระบบ กรุณาลองใหม่อีกครั้ง',
};
}
const latencyMs = Date.now() - startMs;
await this.writeAuditLog(intent, context, result, latencyMs);
return result;
}
/**
* คืน handler function สำหรับ Unit Test (ตรวจสอบว่ามี intent นั้นอยู่หรือไม่)
*/
getHandler(intent: ServerIntent): ToolHandler | undefined {
return this.handlerMap.get(intent);
}
/**
* บันทึก Audit Log ทุก Tool Execution (ADR-023 FR-005)
* ทำแบบ fire-and-forget เพื่อไม่บล็อก response
*/
private async writeAuditLog(
intent: string,
context: ToolHandlerContext,
result: ToolCallResult<unknown>,
latencyMs: number
): Promise<void> {
try {
const log = this.auditLogRepo.create({
publicId: uuidv7(),
aiModel: 'tool-layer', // ระบุ layer ใน model field
modelName: intent,
processingTimeMs: latencyMs,
status: result.ok ? AiAuditStatus.SUCCESS : AiAuditStatus.FAILED,
errorMessage: result.ok ? undefined : result.reason,
aiSuggestionJson: {
intent,
projectPublicId: context.projectPublicId,
userPublicId: context.requestUser.publicId,
params: context.params ?? {},
ok: result.ok,
reason: result.ok ? undefined : result.reason,
},
});
await this.auditLogRepo.save(log);
} catch (auditError: unknown) {
// Audit log ล้มเหลวต้องไม่กระทบ response หลัก (ข้อผิดพลาดเป็น non-critical)
this.logger.error(
`เขียน Audit Log ล้มเหลว: ${(auditError as Error).message}`
);
}
}
}