690519:1631 224 to 226 AI #01
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
// File: src/modules/ai/intent-classifier/controllers/intent-admin.controller.spec.ts
|
||||
// Change Log
|
||||
// - 2026-05-19: สร้าง Integration test สำหรับ Admin API (T016, US1).
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import {
|
||||
IntentAdminController,
|
||||
IntentPatternAdminController,
|
||||
} from './intent-admin.controller';
|
||||
import { IntentDefinitionService } from '../services/intent-definition.service';
|
||||
import { IntentPatternService } from '../services/intent-pattern.service';
|
||||
import { IntentCategory } from '../interfaces/intent-category.enum';
|
||||
import { JwtAuthGuard } from '../../../../common/guards/jwt-auth.guard';
|
||||
import { RbacGuard } from '../../../../common/guards/rbac.guard';
|
||||
|
||||
/** Guard stub ที่ allow ทุก request */
|
||||
const mockGuard = { canActivate: () => true };
|
||||
|
||||
describe('IntentAdminController', () => {
|
||||
let controller: IntentAdminController;
|
||||
let definitionService: jest.Mocked<IntentDefinitionService>;
|
||||
let patternService: jest.Mocked<IntentPatternService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [IntentAdminController],
|
||||
providers: [
|
||||
{
|
||||
provide: IntentDefinitionService,
|
||||
useValue: {
|
||||
findAll: jest.fn().mockResolvedValue([]),
|
||||
findByCode: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: IntentPatternService,
|
||||
useValue: {
|
||||
findByIntentCode: jest.fn().mockResolvedValue([]),
|
||||
create: jest.fn(),
|
||||
},
|
||||
},
|
||||
Reflector,
|
||||
],
|
||||
})
|
||||
.overrideGuard(JwtAuthGuard)
|
||||
|
||||
.useValue(mockGuard)
|
||||
|
||||
.overrideGuard(RbacGuard)
|
||||
.useValue(mockGuard)
|
||||
.compile();
|
||||
|
||||
controller = module.get<IntentAdminController>(IntentAdminController);
|
||||
definitionService = module.get(IntentDefinitionService);
|
||||
patternService = module.get(IntentPatternService);
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('ควรเรียก service.findAll พร้อม filter', async () => {
|
||||
await controller.findAll('read', 'true');
|
||||
|
||||
expect(definitionService.findAll).toHaveBeenCalledWith({
|
||||
category: 'read',
|
||||
isActive: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('ควรเรียก service.findAll โดยไม่มี filter', async () => {
|
||||
await controller.findAll();
|
||||
|
||||
expect(definitionService.findAll).toHaveBeenCalledWith({
|
||||
category: undefined,
|
||||
isActive: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOne', () => {
|
||||
it('ควรเรียก service.findByCode', async () => {
|
||||
definitionService.findByCode.mockResolvedValue({
|
||||
intentCode: 'GET_RFA',
|
||||
} as never);
|
||||
|
||||
const result = await controller.findOne('GET_RFA');
|
||||
|
||||
expect(definitionService.findByCode).toHaveBeenCalledWith('GET_RFA');
|
||||
expect(result).toEqual({ intentCode: 'GET_RFA' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('ควรเรียก service.create ด้วย dto', async () => {
|
||||
const dto = {
|
||||
intentCode: 'TEST',
|
||||
descriptionTh: 'ทดสอบ',
|
||||
descriptionEn: 'Test',
|
||||
category: IntentCategory.UTILITY,
|
||||
};
|
||||
definitionService.create.mockResolvedValue({
|
||||
...dto,
|
||||
publicId: 'uuid-1',
|
||||
} as never);
|
||||
|
||||
await controller.create(dto);
|
||||
|
||||
expect(definitionService.create).toHaveBeenCalledWith(dto);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('ควรเรียก service.update ด้วย intentCode + dto', async () => {
|
||||
definitionService.update.mockResolvedValue({
|
||||
intentCode: 'GET_RFA',
|
||||
descriptionTh: 'อัปเดต',
|
||||
} as never);
|
||||
|
||||
await controller.update('GET_RFA', { descriptionTh: 'อัปเดต' });
|
||||
|
||||
expect(definitionService.update).toHaveBeenCalledWith('GET_RFA', {
|
||||
descriptionTh: 'อัปเดต',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('findPatterns', () => {
|
||||
it('ควรเรียก patternService.findByIntentCode', async () => {
|
||||
await controller.findPatterns('GET_RFA');
|
||||
|
||||
expect(patternService.findByIntentCode).toHaveBeenCalledWith('GET_RFA');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createPattern', () => {
|
||||
it('ควร merge intentCode กับ dto', async () => {
|
||||
const dto = { patternType: 'keyword' as const, patternValue: 'rfa' };
|
||||
patternService.create.mockResolvedValue({ publicId: 'p-1' } as never);
|
||||
|
||||
await controller.createPattern('GET_RFA', dto);
|
||||
|
||||
expect(patternService.create).toHaveBeenCalledWith({
|
||||
intentCode: 'GET_RFA',
|
||||
...dto,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('IntentPatternAdminController', () => {
|
||||
let controller: IntentPatternAdminController;
|
||||
let patternService: jest.Mocked<IntentPatternService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [IntentPatternAdminController],
|
||||
providers: [
|
||||
{
|
||||
provide: IntentPatternService,
|
||||
useValue: {
|
||||
findByPublicId: jest.fn(),
|
||||
update: jest.fn(),
|
||||
remove: jest.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
},
|
||||
Reflector,
|
||||
],
|
||||
})
|
||||
|
||||
.overrideGuard(JwtAuthGuard)
|
||||
.useValue(mockGuard)
|
||||
.overrideGuard(RbacGuard)
|
||||
.useValue(mockGuard)
|
||||
.compile();
|
||||
|
||||
controller = module.get<IntentPatternAdminController>(
|
||||
IntentPatternAdminController
|
||||
);
|
||||
patternService = module.get(IntentPatternService);
|
||||
});
|
||||
|
||||
describe('findOne', () => {
|
||||
it('ควรเรียก service.findByPublicId', async () => {
|
||||
patternService.findByPublicId.mockResolvedValue({
|
||||
publicId: 'p-1',
|
||||
} as never);
|
||||
|
||||
const result = await controller.findOne('p-1');
|
||||
|
||||
expect(patternService.findByPublicId).toHaveBeenCalledWith('p-1');
|
||||
expect(result).toEqual({ publicId: 'p-1' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('ควรเรียก service.update', async () => {
|
||||
patternService.update.mockResolvedValue({ publicId: 'p-1' } as never);
|
||||
|
||||
await controller.update('p-1', { patternValue: 'new' });
|
||||
|
||||
expect(patternService.update).toHaveBeenCalledWith('p-1', {
|
||||
patternValue: 'new',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
it('ควรเรียก service.remove', async () => {
|
||||
await controller.remove('p-1');
|
||||
|
||||
expect(patternService.remove).toHaveBeenCalledWith('p-1');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,143 @@
|
||||
// File: src/modules/ai/intent-classifier/controllers/intent-admin.controller.ts
|
||||
// Change Log
|
||||
// - 2026-05-19: สร้าง Admin Controller สำหรับจัดการ Intent Definitions/Patterns (ADR-024).
|
||||
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Patch,
|
||||
Delete,
|
||||
Param,
|
||||
Body,
|
||||
Query,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
UseGuards,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { JwtAuthGuard } from '../../../../common/guards/jwt-auth.guard';
|
||||
import { RbacGuard } from '../../../../common/guards/rbac.guard';
|
||||
import { Audit } from '../../../../common/decorators/audit.decorator';
|
||||
import { IntentDefinitionService } from '../services/intent-definition.service';
|
||||
import { IntentPatternService } from '../services/intent-pattern.service';
|
||||
import { CreateIntentDefinitionDto } from '../dto/create-intent-definition.dto';
|
||||
import { UpdateIntentDefinitionDto } from '../dto/update-intent-definition.dto';
|
||||
import { CreateIntentPatternDto } from '../dto/create-intent-pattern.dto';
|
||||
import { UpdateIntentPatternDto } from '../dto/update-intent-pattern.dto';
|
||||
import { IntentCategory } from '../interfaces/intent-category.enum';
|
||||
|
||||
/**
|
||||
* Admin Controller สำหรับจัดการ Intent Definitions และ Patterns
|
||||
* Route prefix: /admin/ai/intent-definitions
|
||||
* Protected by JwtAuthGuard + RbacGuard (system admin only)
|
||||
*/
|
||||
@Controller('admin/ai/intent-definitions')
|
||||
@UseGuards(JwtAuthGuard, RbacGuard)
|
||||
export class IntentAdminController {
|
||||
private readonly logger = new Logger(IntentAdminController.name);
|
||||
|
||||
constructor(
|
||||
private readonly definitionService: IntentDefinitionService,
|
||||
private readonly patternService: IntentPatternService
|
||||
) {}
|
||||
|
||||
// ===== Intent Definitions =====
|
||||
|
||||
/** GET /admin/ai/intent-definitions — ดึงรายการ Intent Definitions */
|
||||
@Get()
|
||||
async findAll(
|
||||
@Query('category') category?: IntentCategory,
|
||||
@Query('isActive') isActive?: string
|
||||
) {
|
||||
const filter = {
|
||||
category,
|
||||
isActive: isActive === undefined ? undefined : isActive === 'true',
|
||||
};
|
||||
const data = await this.definitionService.findAll(filter);
|
||||
return { data };
|
||||
}
|
||||
|
||||
/** GET /admin/ai/intent-definitions/:intentCode — ดึงตาม intentCode */
|
||||
@Get(':intentCode')
|
||||
async findOne(@Param('intentCode') intentCode: string) {
|
||||
return this.definitionService.findByCode(intentCode);
|
||||
}
|
||||
|
||||
/** POST /admin/ai/intent-definitions — สร้าง Intent Definition ใหม่ */
|
||||
@Post()
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@Audit('intent-definition.create', 'IntentDefinition')
|
||||
async create(@Body() dto: CreateIntentDefinitionDto) {
|
||||
return this.definitionService.create(dto);
|
||||
}
|
||||
|
||||
/** PATCH /admin/ai/intent-definitions/:intentCode — อัปเดต */
|
||||
@Patch(':intentCode')
|
||||
@Audit('intent-definition.update', 'IntentDefinition')
|
||||
async update(
|
||||
@Param('intentCode') intentCode: string,
|
||||
@Body() dto: UpdateIntentDefinitionDto
|
||||
) {
|
||||
return this.definitionService.update(intentCode, dto);
|
||||
}
|
||||
|
||||
// ===== Intent Patterns =====
|
||||
|
||||
/** GET /admin/ai/intent-definitions/:intentCode/patterns — ดึง Patterns */
|
||||
@Get(':intentCode/patterns')
|
||||
async findPatterns(@Param('intentCode') intentCode: string) {
|
||||
const data = await this.patternService.findByIntentCode(intentCode);
|
||||
return { data };
|
||||
}
|
||||
|
||||
/** POST /admin/ai/intent-definitions/:intentCode/patterns — สร้าง Pattern */
|
||||
@Post(':intentCode/patterns')
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@Audit('intent-pattern.create', 'IntentPattern')
|
||||
async createPattern(
|
||||
@Param('intentCode') intentCode: string,
|
||||
@Body() dto: CreateIntentPatternDto
|
||||
) {
|
||||
return this.patternService.create({
|
||||
intentCode,
|
||||
...dto,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin Controller สำหรับจัดการ Pattern โดย publicId
|
||||
* Route prefix: /admin/ai/intent-patterns
|
||||
*/
|
||||
@Controller('admin/ai/intent-patterns')
|
||||
@UseGuards(JwtAuthGuard, RbacGuard)
|
||||
export class IntentPatternAdminController {
|
||||
private readonly logger = new Logger(IntentPatternAdminController.name);
|
||||
|
||||
constructor(private readonly patternService: IntentPatternService) {}
|
||||
|
||||
/** GET /admin/ai/intent-patterns/:publicId — ดึง Pattern ตาม publicId */
|
||||
@Get(':publicId')
|
||||
async findOne(@Param('publicId') publicId: string) {
|
||||
return this.patternService.findByPublicId(publicId);
|
||||
}
|
||||
|
||||
/** PATCH /admin/ai/intent-patterns/:publicId — อัปเดต Pattern */
|
||||
@Patch(':publicId')
|
||||
@Audit('intent-pattern.update', 'IntentPattern')
|
||||
async update(
|
||||
@Param('publicId') publicId: string,
|
||||
@Body() dto: UpdateIntentPatternDto
|
||||
) {
|
||||
return this.patternService.update(publicId, dto);
|
||||
}
|
||||
|
||||
/** DELETE /admin/ai/intent-patterns/:publicId — Soft delete Pattern */
|
||||
@Delete(':publicId')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@Audit('intent-pattern.delete', 'IntentPattern')
|
||||
async remove(@Param('publicId') publicId: string) {
|
||||
await this.patternService.remove(publicId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// File: src/modules/ai/intent-classifier/controllers/intent-analytics.controller.ts
|
||||
// Change Log
|
||||
// - 2026-05-19: สร้าง Analytics Controller สำหรับ Intent Classification (T035, US3).
|
||||
|
||||
import { Controller, Get, Query, UseGuards, Logger } from '@nestjs/common';
|
||||
import { JwtAuthGuard } from '../../../../common/guards/jwt-auth.guard';
|
||||
import { RbacGuard } from '../../../../common/guards/rbac.guard';
|
||||
import { IntentAnalyticsService } from '../services/intent-analytics.service';
|
||||
|
||||
/**
|
||||
* Analytics Controller สำหรับ Intent Classification
|
||||
* Route prefix: /admin/ai/intent-analytics
|
||||
* Protected by JwtAuthGuard + RbacGuard (system admin only)
|
||||
*/
|
||||
@Controller('admin/ai/intent-analytics')
|
||||
@UseGuards(JwtAuthGuard, RbacGuard)
|
||||
export class IntentAnalyticsController {
|
||||
private readonly logger = new Logger(IntentAnalyticsController.name);
|
||||
|
||||
constructor(private readonly analyticsService: IntentAnalyticsService) {}
|
||||
|
||||
/**
|
||||
* GET /admin/ai/intent-analytics
|
||||
* ดึงสถิติ Classification ทั้งหมด
|
||||
* @param from ISO date string (optional, default: 30 วันก่อน)
|
||||
* @param to ISO date string (optional, default: ปัจจุบัน)
|
||||
*/
|
||||
@Get()
|
||||
async getAnalytics(@Query('from') from?: string, @Query('to') to?: string) {
|
||||
const fromDate = from ? new Date(from) : undefined;
|
||||
const toDate = to ? new Date(to) : undefined;
|
||||
|
||||
const data = await this.analyticsService.getAnalytics(fromDate, toDate);
|
||||
return { data };
|
||||
}
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
// File: src/modules/ai/intent-classifier/controllers/intent-classify.controller.spec.ts
|
||||
// Change Log
|
||||
// - 2026-05-19: สร้าง Integration test สำหรับ Classification API (T026, US2).
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { IntentClassifyController } from './intent-classify.controller';
|
||||
import { IntentClassifierService } from '../services/intent-classifier.service';
|
||||
import { ClassificationResult } from '../interfaces/classification-result.interface';
|
||||
import { JwtAuthGuard } from '../../../../common/guards/jwt-auth.guard';
|
||||
|
||||
/** Guard stub ที่ allow ทุก request */
|
||||
const mockGuard = { canActivate: () => true };
|
||||
|
||||
describe('IntentClassifyController', () => {
|
||||
let controller: IntentClassifyController;
|
||||
let classifierService: jest.Mocked<IntentClassifierService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [IntentClassifyController],
|
||||
providers: [
|
||||
{
|
||||
provide: IntentClassifierService,
|
||||
useValue: {
|
||||
classify: jest.fn(),
|
||||
},
|
||||
},
|
||||
Reflector,
|
||||
],
|
||||
})
|
||||
.overrideGuard(JwtAuthGuard)
|
||||
.useValue(mockGuard)
|
||||
.compile();
|
||||
|
||||
controller = module.get<IntentClassifyController>(IntentClassifyController);
|
||||
classifierService = module.get(IntentClassifierService);
|
||||
});
|
||||
|
||||
describe('classify', () => {
|
||||
it('ควรเรียก service.classify ด้วย trimmed query', async () => {
|
||||
const mockResult: ClassificationResult = {
|
||||
intentCode: 'SUMMARIZE_DOCUMENT',
|
||||
confidence: 1.0,
|
||||
method: 'pattern',
|
||||
latencyMs: 3,
|
||||
};
|
||||
classifierService.classify.mockResolvedValue(mockResult);
|
||||
|
||||
const result = await controller.classify({
|
||||
query: ' สรุปเอกสาร ',
|
||||
projectPublicId: undefined,
|
||||
userPublicId: undefined,
|
||||
currentDocumentId: undefined,
|
||||
});
|
||||
|
||||
expect(classifierService.classify).toHaveBeenCalledWith({
|
||||
query: 'สรุปเอกสาร',
|
||||
projectPublicId: undefined,
|
||||
userPublicId: undefined,
|
||||
currentDocumentId: undefined,
|
||||
});
|
||||
expect(result.intentCode).toBe('SUMMARIZE_DOCUMENT');
|
||||
expect(result.method).toBe('pattern');
|
||||
});
|
||||
|
||||
it('ควรส่ง context parameters ไปด้วย', async () => {
|
||||
const mockResult: ClassificationResult = {
|
||||
intentCode: 'GET_RFA',
|
||||
confidence: 0.9,
|
||||
method: 'llm_fallback',
|
||||
latencyMs: 500,
|
||||
};
|
||||
classifierService.classify.mockResolvedValue(mockResult);
|
||||
|
||||
await controller.classify({
|
||||
query: 'show rfa',
|
||||
projectPublicId: 'proj-uuid-123',
|
||||
userPublicId: 'user-uuid-456',
|
||||
currentDocumentId: 'doc-uuid-789',
|
||||
});
|
||||
|
||||
expect(classifierService.classify).toHaveBeenCalledWith({
|
||||
query: 'show rfa',
|
||||
projectPublicId: 'proj-uuid-123',
|
||||
userPublicId: 'user-uuid-456',
|
||||
currentDocumentId: 'doc-uuid-789',
|
||||
});
|
||||
});
|
||||
|
||||
it('ควร return ClassificationResult', async () => {
|
||||
const mockResult: ClassificationResult = {
|
||||
intentCode: 'FALLBACK',
|
||||
confidence: 0,
|
||||
method: 'semaphore_overflow',
|
||||
latencyMs: 1,
|
||||
};
|
||||
classifierService.classify.mockResolvedValue(mockResult);
|
||||
|
||||
const result = await controller.classify({
|
||||
query: 'test',
|
||||
});
|
||||
|
||||
expect(result).toEqual(mockResult);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
// File: src/modules/ai/intent-classifier/controllers/intent-classify.controller.ts
|
||||
// Change Log
|
||||
// - 2026-05-19: สร้าง Classification Controller (POST /ai/intent/classify) (ADR-024).
|
||||
|
||||
import { Controller, Post, Body, UseGuards, Logger } from '@nestjs/common';
|
||||
import { Throttle } from '@nestjs/throttler';
|
||||
import { JwtAuthGuard } from '../../../../common/guards/jwt-auth.guard';
|
||||
import { IntentClassifierService } from '../services/intent-classifier.service';
|
||||
import { ClassifyQueryDto } from '../dto/classify-query.dto';
|
||||
import { ClassificationResult } from '../interfaces/classification-result.interface';
|
||||
|
||||
/**
|
||||
* Controller สำหรับ Intent Classification API
|
||||
* Route: POST /ai/intent/classify
|
||||
* Protected by JWT (ทุก authenticated user ใช้ได้)
|
||||
*/
|
||||
@Controller('ai/intent')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class IntentClassifyController {
|
||||
private readonly logger = new Logger(IntentClassifyController.name);
|
||||
|
||||
constructor(private readonly classifierService: IntentClassifierService) {}
|
||||
|
||||
/** POST /ai/intent/classify — Classify user query → intent */
|
||||
@Throttle({ default: { limit: 30, ttl: 60000 } })
|
||||
@Post('classify')
|
||||
async classify(@Body() dto: ClassifyQueryDto): Promise<ClassificationResult> {
|
||||
this.logger.debug(`Classifying: "${dto.query}"`);
|
||||
return this.classifierService.classify({
|
||||
query: dto.query.trim(),
|
||||
projectPublicId: dto.projectPublicId,
|
||||
userPublicId: dto.userPublicId,
|
||||
currentDocumentId: dto.currentDocumentId,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user