mirror of
https://github.com/hexastack/hexabot
synced 2025-06-26 18:27:28 +00:00
feat: initial commit
This commit is contained in:
262
api/src/nlp/controllers/nlp-entity.controller.spec.ts
Normal file
262
api/src/nlp/controllers/nlp-entity.controller.spec.ts
Normal file
@@ -0,0 +1,262 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { MethodNotAllowedException, NotFoundException } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { IGNORED_TEST_FIELDS } from '@/utils/test/constants';
|
||||
import { nlpEntityFixtures } from '@/utils/test/fixtures/nlpentity';
|
||||
import {
|
||||
installNlpValueFixtures,
|
||||
nlpValueFixtures,
|
||||
} from '@/utils/test/fixtures/nlpvalue';
|
||||
import { getPageQuery } from '@/utils/test/pagination';
|
||||
import {
|
||||
closeInMongodConnection,
|
||||
rootMongooseTestModule,
|
||||
} from '@/utils/test/test';
|
||||
|
||||
import { NlpEntityController } from './nlp-entity.controller';
|
||||
import { NlpEntityCreateDto } from '../dto/nlp-entity.dto';
|
||||
import { NlpEntityRepository } from '../repositories/nlp-entity.repository';
|
||||
import { NlpSampleEntityRepository } from '../repositories/nlp-sample-entity.repository';
|
||||
import { NlpValueRepository } from '../repositories/nlp-value.repository';
|
||||
import {
|
||||
NlpEntityModel,
|
||||
NlpEntity,
|
||||
NlpEntityFull,
|
||||
} from '../schemas/nlp-entity.schema';
|
||||
import { NlpSampleEntityModel } from '../schemas/nlp-sample-entity.schema';
|
||||
import { NlpValueModel } from '../schemas/nlp-value.schema';
|
||||
import { NlpEntityService } from '../services/nlp-entity.service';
|
||||
import { NlpValueService } from '../services/nlp-value.service';
|
||||
|
||||
describe('NlpEntityController', () => {
|
||||
let nlpEntityController: NlpEntityController;
|
||||
let nlpValueService: NlpValueService;
|
||||
let nlpEntityService: NlpEntityService;
|
||||
let intentEntityId: string;
|
||||
let buitInEntityId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [NlpEntityController],
|
||||
imports: [
|
||||
rootMongooseTestModule(installNlpValueFixtures),
|
||||
MongooseModule.forFeature([
|
||||
NlpEntityModel,
|
||||
NlpSampleEntityModel,
|
||||
NlpValueModel,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
LoggerService,
|
||||
NlpEntityService,
|
||||
NlpEntityRepository,
|
||||
NlpValueService,
|
||||
NlpSampleEntityRepository,
|
||||
NlpValueRepository,
|
||||
EventEmitter2,
|
||||
],
|
||||
}).compile();
|
||||
nlpEntityController = module.get<NlpEntityController>(NlpEntityController);
|
||||
nlpValueService = module.get<NlpValueService>(NlpValueService);
|
||||
nlpEntityService = module.get<NlpEntityService>(NlpEntityService);
|
||||
|
||||
intentEntityId = (
|
||||
await nlpEntityService.findOne({
|
||||
name: 'intent',
|
||||
})
|
||||
).id;
|
||||
buitInEntityId = (
|
||||
await nlpEntityService.findOne({
|
||||
name: 'built_in',
|
||||
})
|
||||
).id;
|
||||
});
|
||||
afterAll(async () => {
|
||||
await closeInMongodConnection();
|
||||
});
|
||||
|
||||
afterEach(jest.clearAllMocks);
|
||||
|
||||
describe('findPage', () => {
|
||||
it('should find nlp entities,and foreach nlp entity, populate the corresponding values', async () => {
|
||||
const pageQuery = getPageQuery<NlpEntity>({ sort: ['name', 'desc'] });
|
||||
const result = await nlpEntityController.findPage(
|
||||
pageQuery,
|
||||
['values'],
|
||||
{},
|
||||
);
|
||||
const entitiesWithValues = nlpEntityFixtures.reduce(
|
||||
(acc, curr, index) => {
|
||||
acc.push({
|
||||
...curr,
|
||||
values: nlpValueFixtures.filter(
|
||||
({ entity }) => parseInt(entity) === index,
|
||||
),
|
||||
});
|
||||
return acc;
|
||||
},
|
||||
[],
|
||||
);
|
||||
expect(result).toEqualPayload(
|
||||
entitiesWithValues.sort((a, b) => {
|
||||
if (a.name > b.name) {
|
||||
return -1;
|
||||
}
|
||||
if (a.name < b.name) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}),
|
||||
[...IGNORED_TEST_FIELDS, 'entity'],
|
||||
);
|
||||
});
|
||||
|
||||
it('should find nlp entities', async () => {
|
||||
const pageQuery = getPageQuery<NlpEntity>({ sort: ['name', 'desc'] });
|
||||
const result = await nlpEntityController.findPage(
|
||||
pageQuery,
|
||||
['invalidCriteria'],
|
||||
{},
|
||||
);
|
||||
expect(result).toEqualPayload(
|
||||
nlpEntityFixtures.sort((a, b) => {
|
||||
if (a.name > b.name) {
|
||||
return -1;
|
||||
}
|
||||
if (a.name < b.name) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('count', () => {
|
||||
it('should count the nlp entities', async () => {
|
||||
const result = await nlpEntityController.filterCount();
|
||||
const count = nlpEntityFixtures.length;
|
||||
expect(result).toEqual({ count });
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create nlp entity', async () => {
|
||||
const sentimentEntity: NlpEntityCreateDto = {
|
||||
name: 'sentiment',
|
||||
lookups: ['trait'],
|
||||
builtin: false,
|
||||
};
|
||||
const result = await nlpEntityController.create(sentimentEntity);
|
||||
expect(result).toEqualPayload(sentimentEntity);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteOne', () => {
|
||||
it('should delete a nlp entity', async () => {
|
||||
const result = await nlpEntityController.deleteOne(intentEntityId);
|
||||
expect(result.deletedCount).toEqual(1);
|
||||
});
|
||||
|
||||
it('should throw exception when nlp entity id not found', async () => {
|
||||
await expect(
|
||||
nlpEntityController.deleteOne(intentEntityId),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
|
||||
it('should throw exception when nlp entity is builtin', async () => {
|
||||
await expect(
|
||||
nlpEntityController.deleteOne(buitInEntityId),
|
||||
).rejects.toThrow(MethodNotAllowedException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOne', () => {
|
||||
it('should find a nlp entity', async () => {
|
||||
const firstNameEntity = await nlpEntityService.findOne({
|
||||
name: 'first_name',
|
||||
});
|
||||
const result = await nlpEntityController.findOne(firstNameEntity.id, []);
|
||||
|
||||
expect(result).toEqualPayload(
|
||||
nlpEntityFixtures.find(({ name }) => name === 'first_name'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should find a nlp entity, and populate its values', async () => {
|
||||
const firstNameEntity = await nlpEntityService.findOne({
|
||||
name: 'first_name',
|
||||
});
|
||||
const firstNameValues = await nlpValueService.findOne({ value: 'jhon' });
|
||||
const firstNameWithValues: NlpEntityFull = {
|
||||
...firstNameEntity,
|
||||
values: [firstNameValues],
|
||||
};
|
||||
const result = await nlpEntityController.findOne(firstNameEntity.id, [
|
||||
'values',
|
||||
]);
|
||||
expect(result).toEqualPayload(firstNameWithValues);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException when Id does not exist', async () => {
|
||||
await expect(
|
||||
nlpEntityController.findOne(intentEntityId, ['values']),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateOne', () => {
|
||||
it('should update a nlp entity', async () => {
|
||||
const firstNameEntity = await nlpEntityService.findOne({
|
||||
name: 'first_name',
|
||||
});
|
||||
const updatedNlpEntity: NlpEntityCreateDto = {
|
||||
name: 'updated',
|
||||
doc: '',
|
||||
lookups: ['trait'],
|
||||
builtin: false,
|
||||
};
|
||||
const result = await nlpEntityController.updateOne(
|
||||
firstNameEntity.id,
|
||||
updatedNlpEntity,
|
||||
);
|
||||
expect(result).toEqualPayload(updatedNlpEntity);
|
||||
});
|
||||
|
||||
it('should throw exception when nlp entity id not found', async () => {
|
||||
const updateNlpEntity: NlpEntityCreateDto = {
|
||||
name: 'updated',
|
||||
doc: '',
|
||||
lookups: ['trait'],
|
||||
builtin: false,
|
||||
};
|
||||
await expect(
|
||||
nlpEntityController.updateOne(intentEntityId, updateNlpEntity),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
|
||||
it('should throw exception when nlp entity is builtin', async () => {
|
||||
const updateNlpEntity: NlpEntityCreateDto = {
|
||||
name: 'updated',
|
||||
doc: '',
|
||||
lookups: ['trait'],
|
||||
builtin: false,
|
||||
};
|
||||
await expect(
|
||||
nlpEntityController.updateOne(buitInEntityId, updateNlpEntity),
|
||||
).rejects.toThrow(MethodNotAllowedException);
|
||||
});
|
||||
});
|
||||
});
|
||||
208
api/src/nlp/controllers/nlp-entity.controller.ts
Normal file
208
api/src/nlp/controllers/nlp-entity.controller.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Patch,
|
||||
Delete,
|
||||
Param,
|
||||
Query,
|
||||
HttpCode,
|
||||
NotFoundException,
|
||||
UseInterceptors,
|
||||
MethodNotAllowedException,
|
||||
InternalServerErrorException,
|
||||
} from '@nestjs/common';
|
||||
import { CsrfCheck } from '@tekuconcept/nestjs-csrf';
|
||||
import { TFilterQuery } from 'mongoose';
|
||||
|
||||
import { CsrfInterceptor } from '@/interceptors/csrf.interceptor';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { BaseController } from '@/utils/generics/base-controller';
|
||||
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
|
||||
import { PageQueryPipe } from '@/utils/pagination/pagination-query.pipe';
|
||||
import { PopulatePipe } from '@/utils/pipes/populate.pipe';
|
||||
import { SearchFilterPipe } from '@/utils/pipes/search-filter.pipe';
|
||||
|
||||
import { NlpEntityCreateDto } from '../dto/nlp-entity.dto';
|
||||
import { NlpEntity, NlpEntityStub } from '../schemas/nlp-entity.schema';
|
||||
import { NlpEntityService } from '../services/nlp-entity.service';
|
||||
|
||||
@UseInterceptors(CsrfInterceptor)
|
||||
@Controller('nlpentity')
|
||||
export class NlpEntityController extends BaseController<
|
||||
NlpEntity,
|
||||
NlpEntityStub
|
||||
> {
|
||||
constructor(
|
||||
private readonly nlpEntityService: NlpEntityService,
|
||||
private readonly logger: LoggerService,
|
||||
) {
|
||||
super(nlpEntityService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new NLP entity.
|
||||
*
|
||||
* This endpoint receives the NLP entity data in the request body, validates it, and creates a new entry in the database.
|
||||
*
|
||||
* @param createNlpEntityDto - Data transfer object containing the details of the NLP entity to create.
|
||||
*
|
||||
* @returns The newly created NLP entity.
|
||||
*/
|
||||
@CsrfCheck(true)
|
||||
@Post()
|
||||
async create(
|
||||
@Body() createNlpEntityDto: NlpEntityCreateDto,
|
||||
): Promise<NlpEntity> {
|
||||
return await this.nlpEntityService.create(createNlpEntityDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the number of NLP entities based on provided filters.
|
||||
*
|
||||
* This endpoint allows users to apply filters to count the number of entities in the system that match specific criteria.
|
||||
*
|
||||
* @param filters - Optional filters to apply when counting entities.
|
||||
*
|
||||
* @returns The count of NLP entities matching the filters.
|
||||
*/
|
||||
@Get('count')
|
||||
async filterCount(
|
||||
@Query(new SearchFilterPipe<NlpEntity>({ allowedFields: ['name', 'doc'] }))
|
||||
filters?: TFilterQuery<NlpEntity>,
|
||||
) {
|
||||
return await this.count(filters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a single NLP entity by ID.
|
||||
*
|
||||
* This endpoint returns an NLP entity if found by its ID, optionally allowing population of specified fields.
|
||||
*
|
||||
* @param id - The ID of the NLP entity to find.
|
||||
* @param populate - Fields to populate, such as 'values'.
|
||||
*
|
||||
* @returns The NLP entity found by the ID.
|
||||
*/
|
||||
@Get(':id')
|
||||
async findOne(
|
||||
@Param('id') id: string,
|
||||
@Query(PopulatePipe) populate: string[],
|
||||
) {
|
||||
const doc = this.canPopulate(populate, ['values'])
|
||||
? await this.nlpEntityService.findOneAndPopulate(id)
|
||||
: await this.nlpEntityService.findOne(id);
|
||||
if (!doc) {
|
||||
this.logger.warn(`Unable to find NLP Entity by id ${id}`);
|
||||
throw new NotFoundException(`NLP Entity with ID ${id} not found`);
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a paginated list of NLP entities with optional filters.
|
||||
*
|
||||
* This endpoint supports pagination and allows users to retrieve a filtered list of NLP entities.
|
||||
*
|
||||
* @param pageQuery - The pagination details such as page number and size.
|
||||
* @param populate - Fields to populate in the retrieved entities.
|
||||
* @param filters - Filters to apply when retrieving entities.
|
||||
*
|
||||
* @returns A paginated list of NLP entities.
|
||||
*/
|
||||
@Get()
|
||||
async findPage(
|
||||
@Query(PageQueryPipe) pageQuery: PageQueryDto<NlpEntity>,
|
||||
@Query(PopulatePipe) populate: string[],
|
||||
@Query(new SearchFilterPipe<NlpEntity>({ allowedFields: ['name', 'doc'] }))
|
||||
filters: TFilterQuery<NlpEntity>,
|
||||
) {
|
||||
return this.canPopulate(populate, ['values'])
|
||||
? await this.nlpEntityService.findPageAndPopulate(filters, pageQuery)
|
||||
: await this.nlpEntityService.findPage(filters, pageQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an NLP entity by ID.
|
||||
*
|
||||
* This endpoint allows updating an existing NLP entity. The entity must not be a built-in entity.
|
||||
*
|
||||
* @param id - The ID of the NLP entity to update.
|
||||
* @param updateNlpEntityDto - The new data for the NLP entity.
|
||||
*
|
||||
* @returns The updated NLP entity.
|
||||
*/
|
||||
@CsrfCheck(true)
|
||||
@Patch(':id')
|
||||
async updateOne(
|
||||
@Param('id') id: string,
|
||||
@Body() updateNlpEntityDto: NlpEntityCreateDto,
|
||||
): Promise<NlpEntity> {
|
||||
const nlpEntity = await this.nlpEntityService.findOne(id);
|
||||
if (!nlpEntity) {
|
||||
this.logger.warn(`Unable to update NLP Entity by id ${id}`);
|
||||
throw new NotFoundException(`NLP Entity with ID ${id} not found`);
|
||||
}
|
||||
if (nlpEntity.builtin) {
|
||||
throw new MethodNotAllowedException(
|
||||
`Cannot update builtin NLP Entity ${nlpEntity.name}`,
|
||||
);
|
||||
}
|
||||
|
||||
const result = await this.nlpEntityService.updateOne(
|
||||
id,
|
||||
updateNlpEntityDto,
|
||||
);
|
||||
if (!result) {
|
||||
this.logger.warn(`Failed to update NLP Entity by id ${id}`);
|
||||
throw new InternalServerErrorException(
|
||||
`Failed to update NLP Entity with ID ${id}`,
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an NLP entity by ID.
|
||||
*
|
||||
* This endpoint deletes an NLP entity from the system, provided it is not a built-in entity.
|
||||
*
|
||||
* @param id - The ID of the NLP entity to delete.
|
||||
*
|
||||
* @returns The result of the deletion operation.
|
||||
*/
|
||||
@CsrfCheck(true)
|
||||
@Delete(':id')
|
||||
@HttpCode(204)
|
||||
async deleteOne(@Param('id') id: string) {
|
||||
const nlpEntity = await this.nlpEntityService.findOne(id);
|
||||
if (!nlpEntity) {
|
||||
this.logger.warn(`Unable to delete NLP Entity by id ${id}`);
|
||||
throw new NotFoundException(`NLP Entity with ID ${id} not found`);
|
||||
}
|
||||
if (nlpEntity.builtin) {
|
||||
throw new MethodNotAllowedException(
|
||||
`Cannot delete builtin NLP Entity ${nlpEntity.name}`,
|
||||
);
|
||||
}
|
||||
|
||||
const result = await this.nlpEntityService.deleteCascadeOne(id);
|
||||
if (result.deletedCount === 0) {
|
||||
this.logger.warn(`Failed to delete NLP Entity by id ${id}`);
|
||||
throw new InternalServerErrorException(
|
||||
`Failed to delete NLP Entity with ID ${id}`,
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
420
api/src/nlp/controllers/nlp-sample.controller.spec.ts
Normal file
420
api/src/nlp/controllers/nlp-sample.controller.spec.ts
Normal file
@@ -0,0 +1,420 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { BadRequestException, NotFoundException } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { AttachmentRepository } from '@/attachment/repositories/attachment.repository';
|
||||
import { AttachmentModel } from '@/attachment/schemas/attachment.schema';
|
||||
import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { SettingRepository } from '@/setting/repositories/setting.repository';
|
||||
import { SettingModel } from '@/setting/schemas/setting.schema';
|
||||
import { SettingSeeder } from '@/setting/seeds/setting.seed';
|
||||
import { SettingService } from '@/setting/services/setting.service';
|
||||
import { installAttachmentFixtures } from '@/utils/test/fixtures/attachment';
|
||||
import { nlpSampleFixtures } from '@/utils/test/fixtures/nlpsample';
|
||||
import { installNlpSampleEntityFixtures } from '@/utils/test/fixtures/nlpsampleentity';
|
||||
import { getPageQuery } from '@/utils/test/pagination';
|
||||
import {
|
||||
closeInMongodConnection,
|
||||
rootMongooseTestModule,
|
||||
} from '@/utils/test/test';
|
||||
|
||||
import { NlpSampleController } from './nlp-sample.controller';
|
||||
import { NlpSampleDto } from '../dto/nlp-sample.dto';
|
||||
import { NlpEntityRepository } from '../repositories/nlp-entity.repository';
|
||||
import { NlpSampleEntityRepository } from '../repositories/nlp-sample-entity.repository';
|
||||
import { NlpSampleRepository } from '../repositories/nlp-sample.repository';
|
||||
import { NlpValueRepository } from '../repositories/nlp-value.repository';
|
||||
import { NlpEntityModel } from '../schemas/nlp-entity.schema';
|
||||
import { NlpSampleEntityModel } from '../schemas/nlp-sample-entity.schema';
|
||||
import { NlpSample, NlpSampleModel } from '../schemas/nlp-sample.schema';
|
||||
import { NlpValueModel } from '../schemas/nlp-value.schema';
|
||||
import { NlpSampleState } from '../schemas/types';
|
||||
import { NlpEntityService } from '../services/nlp-entity.service';
|
||||
import { NlpSampleEntityService } from '../services/nlp-sample-entity.service';
|
||||
import { NlpSampleService } from '../services/nlp-sample.service';
|
||||
import { NlpValueService } from '../services/nlp-value.service';
|
||||
import { NlpService } from '../services/nlp.service';
|
||||
|
||||
describe('NlpSampleController', () => {
|
||||
let nlpSampleController: NlpSampleController;
|
||||
let nlpSampleEntityService: NlpSampleEntityService;
|
||||
let nlpSampleService: NlpSampleService;
|
||||
let nlpEntityService: NlpEntityService;
|
||||
let nlpValueService: NlpValueService;
|
||||
let attachmentService: AttachmentService;
|
||||
let byeJhonSampleId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [NlpSampleController],
|
||||
imports: [
|
||||
rootMongooseTestModule(async () => {
|
||||
await installNlpSampleEntityFixtures();
|
||||
await installAttachmentFixtures();
|
||||
}),
|
||||
MongooseModule.forFeature([
|
||||
NlpSampleModel,
|
||||
NlpSampleEntityModel,
|
||||
AttachmentModel,
|
||||
NlpEntityModel,
|
||||
NlpValueModel,
|
||||
SettingModel,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
LoggerService,
|
||||
NlpSampleRepository,
|
||||
NlpSampleEntityRepository,
|
||||
AttachmentService,
|
||||
NlpEntityService,
|
||||
AttachmentRepository,
|
||||
NlpEntityRepository,
|
||||
NlpValueService,
|
||||
NlpValueRepository,
|
||||
NlpSampleService,
|
||||
NlpSampleEntityService,
|
||||
EventEmitter2,
|
||||
NlpService,
|
||||
SettingRepository,
|
||||
SettingService,
|
||||
SettingSeeder,
|
||||
{
|
||||
provide: ExtendedI18nService,
|
||||
useValue: {
|
||||
t: jest.fn().mockImplementation((t) => t),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: CACHE_MANAGER,
|
||||
useValue: {
|
||||
del: jest.fn(),
|
||||
get: jest.fn(),
|
||||
set: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
nlpSampleController = module.get<NlpSampleController>(NlpSampleController);
|
||||
nlpSampleEntityService = module.get<NlpSampleEntityService>(
|
||||
NlpSampleEntityService,
|
||||
);
|
||||
nlpSampleService = module.get<NlpSampleService>(NlpSampleService);
|
||||
nlpEntityService = module.get<NlpEntityService>(NlpEntityService);
|
||||
nlpValueService = module.get<NlpValueService>(NlpValueService);
|
||||
byeJhonSampleId = (
|
||||
await nlpSampleService.findOne({
|
||||
text: 'Bye Jhon',
|
||||
})
|
||||
).id;
|
||||
attachmentService = module.get<AttachmentService>(AttachmentService);
|
||||
});
|
||||
afterAll(async () => {
|
||||
await closeInMongodConnection();
|
||||
});
|
||||
|
||||
afterEach(jest.clearAllMocks);
|
||||
|
||||
describe('findPage', () => {
|
||||
it('should find nlp samples, and foreach sample populate the corresponding entities', async () => {
|
||||
const pageQuery = getPageQuery<NlpSample>({ sort: ['text', 'desc'] });
|
||||
const result = await nlpSampleController.findPage(
|
||||
pageQuery,
|
||||
['entities'],
|
||||
{},
|
||||
);
|
||||
const nlpSamples = await nlpSampleService.findAll();
|
||||
const nlpSampleEntities = await nlpSampleEntityService.findAll();
|
||||
const nlpSampleFixturesWithEntities = nlpSamples.reduce(
|
||||
(acc, currSample) => {
|
||||
const sampleWithEntities = {
|
||||
...currSample,
|
||||
entities: nlpSampleEntities.filter((currSampleEntity) => {
|
||||
return currSampleEntity.sample === currSample.id;
|
||||
}),
|
||||
};
|
||||
acc.push(sampleWithEntities);
|
||||
return acc;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
expect(result).toEqualPayload(nlpSampleFixturesWithEntities);
|
||||
});
|
||||
|
||||
it('should find nlp samples', async () => {
|
||||
const pageQuery = getPageQuery<NlpSample>({ sort: ['text', 'desc'] });
|
||||
const result = await nlpSampleController.findPage(
|
||||
pageQuery,
|
||||
['invalidCriteria'],
|
||||
{},
|
||||
);
|
||||
expect(result).toEqualPayload(nlpSampleFixtures);
|
||||
});
|
||||
});
|
||||
|
||||
describe('count', () => {
|
||||
it('should count the nlp samples', async () => {
|
||||
const result = await nlpSampleController.count({});
|
||||
const count = nlpSampleFixtures.length;
|
||||
expect(result).toEqual({ count });
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create nlp sample', async () => {
|
||||
const nlSample: NlpSampleDto = {
|
||||
text: 'text1',
|
||||
trained: true,
|
||||
type: NlpSampleState.test,
|
||||
entities: [],
|
||||
};
|
||||
const result = await nlpSampleController.create(nlSample);
|
||||
expect(result).toEqualPayload(nlSample);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteOne', () => {
|
||||
it('should delete a nlp sample', async () => {
|
||||
const result = await nlpSampleController.deleteOne(byeJhonSampleId);
|
||||
expect(result.deletedCount).toEqual(1);
|
||||
});
|
||||
|
||||
it('should throw exception when nlp sample id not found', async () => {
|
||||
await expect(
|
||||
nlpSampleController.deleteOne(byeJhonSampleId),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOne', () => {
|
||||
it('should find a nlp sample', async () => {
|
||||
const yessSample = await nlpSampleService.findOne({
|
||||
text: 'yess',
|
||||
});
|
||||
const result = await nlpSampleController.findOne(yessSample.id, [
|
||||
'invalidCreteria',
|
||||
]);
|
||||
expect(result).toEqualPayload(nlpSampleFixtures[0]);
|
||||
});
|
||||
|
||||
it('should find a nlp sample and populate its entities', async () => {
|
||||
const yessSample = await nlpSampleService.findOne({
|
||||
text: 'yess',
|
||||
});
|
||||
const yessSampleEntity = await nlpSampleEntityService.findOne({
|
||||
sample: yessSample.id,
|
||||
});
|
||||
const result = await nlpSampleController.findOne(yessSample.id, [
|
||||
'entities',
|
||||
]);
|
||||
const samplesWithEntities = {
|
||||
...nlpSampleFixtures[0],
|
||||
entities: [yessSampleEntity],
|
||||
};
|
||||
expect(result).toEqualPayload(samplesWithEntities);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException when Id does not exist', async () => {
|
||||
await expect(
|
||||
nlpSampleController.findOne(byeJhonSampleId, ['entities']),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateOne', () => {
|
||||
it('should update a nlp sample', async () => {
|
||||
const yessSample = await nlpSampleService.findOne({
|
||||
text: 'yess',
|
||||
});
|
||||
const result = await nlpSampleController.updateOne(yessSample.id, {
|
||||
text: 'updated',
|
||||
trained: true,
|
||||
type: NlpSampleState.test,
|
||||
entities: [
|
||||
{
|
||||
entity: 'intent',
|
||||
value: 'update',
|
||||
},
|
||||
],
|
||||
});
|
||||
const updatedSample = {
|
||||
text: 'updated',
|
||||
trained: false,
|
||||
type: NlpSampleState.test,
|
||||
entities: [
|
||||
{
|
||||
entity: expect.stringMatching(/^[a-z0-9]+$/),
|
||||
sample: expect.stringMatching(/^[a-z0-9]+$/),
|
||||
value: expect.stringMatching(/^[a-z0-9]+$/),
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(result.text).toEqual(updatedSample.text);
|
||||
expect(result.type).toEqual(updatedSample.type);
|
||||
expect(result.trained).toEqual(updatedSample.trained);
|
||||
expect(result.entities).toMatchObject(updatedSample.entities);
|
||||
});
|
||||
|
||||
it('should throw exception when nlp sample id not found', async () => {
|
||||
await expect(
|
||||
nlpSampleController.updateOne(byeJhonSampleId, {
|
||||
text: 'updated',
|
||||
trained: true,
|
||||
type: NlpSampleState.test,
|
||||
}),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('import', () => {
|
||||
it('should throw exception when attachment is not found', async () => {
|
||||
const invalidattachmentId = (
|
||||
await attachmentService.findOne({
|
||||
name: 'store2.jpg',
|
||||
})
|
||||
).id;
|
||||
await attachmentService.deleteOne({ name: 'store2.jpg' });
|
||||
await expect(
|
||||
nlpSampleController.import(invalidattachmentId),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
|
||||
it('should throw exception when file location is not present', async () => {
|
||||
const attachmentId = (
|
||||
await attachmentService.findOne({
|
||||
name: 'store1.jpg',
|
||||
})
|
||||
).id;
|
||||
jest.spyOn(fs, 'existsSync').mockReturnValueOnce(false);
|
||||
await expect(nlpSampleController.import(attachmentId)).rejects.toThrow(
|
||||
NotFoundException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return a failure if an error occurs when parsing csv file ', async () => {
|
||||
const mockCsvDataWithErrors: string = `intent,entities,lang,question
|
||||
greeting,person,en`;
|
||||
jest.spyOn(fs, 'existsSync').mockReturnValueOnce(true);
|
||||
jest.spyOn(fs, 'readFileSync').mockReturnValueOnce(mockCsvDataWithErrors);
|
||||
const attachmentId = (
|
||||
await attachmentService.findOne({
|
||||
name: 'store1.jpg',
|
||||
})
|
||||
).id;
|
||||
|
||||
const mockParsedCsvDataWithErrors = {
|
||||
data: [{ intent: 'greeting', entities: 'person', lang: 'en' }],
|
||||
errors: [
|
||||
{
|
||||
type: 'FieldMismatch',
|
||||
code: 'TooFewFields',
|
||||
message: 'Too few fields: expected 4 fields but parsed 3',
|
||||
row: 0,
|
||||
},
|
||||
],
|
||||
meta: {
|
||||
delimiter: ',',
|
||||
linebreak: '\n',
|
||||
aborted: false,
|
||||
truncated: false,
|
||||
cursor: 49,
|
||||
fields: ['intent', 'entities', 'lang', 'question'],
|
||||
},
|
||||
};
|
||||
await expect(nlpSampleController.import(attachmentId)).rejects.toThrow(
|
||||
new BadRequestException({
|
||||
cause: mockParsedCsvDataWithErrors.errors,
|
||||
description: 'Error while parsing CSV',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should import data from a CSV file', async () => {
|
||||
const attachmentId = (
|
||||
await attachmentService.findOne({
|
||||
name: 'store1.jpg',
|
||||
})
|
||||
).id;
|
||||
const mockCsvData: string = [
|
||||
`text,intent,language`,
|
||||
`Was kostet dieser bmw,preis,de`,
|
||||
].join('\n');
|
||||
jest.spyOn(fs, 'existsSync').mockReturnValueOnce(true);
|
||||
jest.spyOn(fs, 'readFileSync').mockReturnValueOnce(mockCsvData);
|
||||
|
||||
const result = await nlpSampleController.import(attachmentId);
|
||||
const intentEntityResult = await nlpEntityService.findOne({
|
||||
name: 'intent',
|
||||
});
|
||||
const languageEntityResult = await nlpEntityService.findOne({
|
||||
name: 'language',
|
||||
});
|
||||
const preisValueResult = await nlpValueService.findOne({
|
||||
value: 'preis',
|
||||
});
|
||||
const deValueResult = await nlpValueService.findOne({
|
||||
value: 'de',
|
||||
});
|
||||
const textSampleResult = await nlpSampleService.findOne({
|
||||
text: 'Was kostet dieser bmw',
|
||||
});
|
||||
const intentEntity = {
|
||||
name: 'intent',
|
||||
lookups: ['trait'],
|
||||
doc: '',
|
||||
builtin: false,
|
||||
};
|
||||
const languageEntity = {
|
||||
name: 'language',
|
||||
lookups: ['trait'],
|
||||
builtin: false,
|
||||
doc: '',
|
||||
};
|
||||
const preisVlueEntity = await nlpEntityService.findOne({
|
||||
name: 'intent',
|
||||
});
|
||||
const preisValue = {
|
||||
value: 'preis',
|
||||
expressions: [],
|
||||
builtin: false,
|
||||
entity: preisVlueEntity.id,
|
||||
};
|
||||
const deValueEntity = await nlpEntityService.findOne({
|
||||
name: 'language',
|
||||
});
|
||||
const deValue = {
|
||||
value: 'de',
|
||||
expressions: [],
|
||||
builtin: false,
|
||||
entity: deValueEntity.id,
|
||||
};
|
||||
const textSample = {
|
||||
text: 'Was kostet dieser bmw',
|
||||
trained: false,
|
||||
type: 'train',
|
||||
};
|
||||
|
||||
expect(languageEntityResult).toEqualPayload(languageEntity);
|
||||
expect(intentEntityResult).toEqualPayload(intentEntity);
|
||||
expect(preisValueResult).toEqualPayload(preisValue);
|
||||
expect(deValueResult).toEqualPayload(deValue);
|
||||
expect(textSampleResult).toEqualPayload(textSample);
|
||||
expect(result).toEqual({ success: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
420
api/src/nlp/controllers/nlp-sample.controller.ts
Normal file
420
api/src/nlp/controllers/nlp-sample.controller.ts
Normal file
@@ -0,0 +1,420 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import { join } from 'path';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
import {
|
||||
BadRequestException,
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
HttpCode,
|
||||
NotFoundException,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
Res,
|
||||
StreamableFile,
|
||||
UseInterceptors,
|
||||
} from '@nestjs/common';
|
||||
import { CsrfCheck } from '@tekuconcept/nestjs-csrf';
|
||||
import { Response } from 'express';
|
||||
import { TFilterQuery } from 'mongoose';
|
||||
import Papa from 'papaparse';
|
||||
|
||||
import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import { config } from '@/config';
|
||||
import { CsrfInterceptor } from '@/interceptors/csrf.interceptor';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { BaseController } from '@/utils/generics/base-controller';
|
||||
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
|
||||
import { PageQueryPipe } from '@/utils/pagination/pagination-query.pipe';
|
||||
import { PopulatePipe } from '@/utils/pipes/populate.pipe';
|
||||
import { SearchFilterPipe } from '@/utils/pipes/search-filter.pipe';
|
||||
|
||||
import { NlpSampleCreateDto, NlpSampleDto } from '../dto/nlp-sample.dto';
|
||||
import {
|
||||
NlpSample,
|
||||
NlpSampleFull,
|
||||
NlpSampleStub,
|
||||
} from '../schemas/nlp-sample.schema';
|
||||
import { NlpSampleEntityValue, NlpSampleState } from '../schemas/types';
|
||||
import { NlpEntityService } from '../services/nlp-entity.service';
|
||||
import { NlpSampleEntityService } from '../services/nlp-sample-entity.service';
|
||||
import { NlpSampleService } from '../services/nlp-sample.service';
|
||||
import { NlpService } from '../services/nlp.service';
|
||||
|
||||
@UseInterceptors(CsrfInterceptor)
|
||||
@Controller('nlpsample')
|
||||
export class NlpSampleController extends BaseController<
|
||||
NlpSample,
|
||||
NlpSampleStub
|
||||
> {
|
||||
constructor(
|
||||
private readonly nlpSampleService: NlpSampleService,
|
||||
private readonly attachmentService: AttachmentService,
|
||||
private readonly nlpSampleEntityService: NlpSampleEntityService,
|
||||
private readonly nlpEntityService: NlpEntityService,
|
||||
private readonly logger: LoggerService,
|
||||
private readonly nlpService: NlpService,
|
||||
) {
|
||||
super(nlpSampleService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports the NLP samples in a formatted JSON file, using the Rasa NLU format.
|
||||
*
|
||||
* @param response - Response object to handle file download.
|
||||
* @param type - Optional filter for NLP sample type.
|
||||
*
|
||||
* @returns A streamable JSON file containing the exported data.
|
||||
*/
|
||||
@Get('export')
|
||||
async export(
|
||||
@Res({ passthrough: true }) response: Response,
|
||||
@Query('type') type?: NlpSampleState,
|
||||
) {
|
||||
const samples = await this.nlpSampleService.findAndPopulate(
|
||||
type ? { type } : {},
|
||||
);
|
||||
const entities = await this.nlpEntityService.findAllAndPopulate();
|
||||
const result = this.nlpSampleService.formatRasaNlu(samples, entities);
|
||||
|
||||
// Sending the JSON data as a file
|
||||
const buffer = Buffer.from(JSON.stringify(result));
|
||||
const readableInstance = new Readable({
|
||||
read() {
|
||||
this.push(buffer);
|
||||
this.push(null); // indicates the end of the stream
|
||||
},
|
||||
});
|
||||
|
||||
return new StreamableFile(readableInstance, {
|
||||
type: 'application/json',
|
||||
disposition: `attachment; filename=nlp_export${
|
||||
type ? `_${type}` : ''
|
||||
}.json`,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new NLP sample with associated entities.
|
||||
*
|
||||
* @param createNlpSampleDto - The DTO containing the NLP sample and its entities.
|
||||
*
|
||||
* @returns The newly created NLP sample with its entities.
|
||||
*/
|
||||
@CsrfCheck(true)
|
||||
@Post()
|
||||
async create(
|
||||
@Body() { entities: nlpEntities, ...createNlpSampleDto }: NlpSampleDto,
|
||||
): Promise<NlpSampleFull> {
|
||||
const nlpSample = await this.nlpSampleService.create(
|
||||
createNlpSampleDto as NlpSampleCreateDto,
|
||||
);
|
||||
|
||||
const entities = await this.nlpSampleEntityService.storeSampleEntities(
|
||||
nlpSample,
|
||||
nlpEntities,
|
||||
);
|
||||
|
||||
return {
|
||||
...nlpSample,
|
||||
entities,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the filtered number of NLP samples.
|
||||
*
|
||||
* @param filters - The filters to apply when counting samples.
|
||||
*
|
||||
* @returns The count of samples that match the filters.
|
||||
*/
|
||||
@Get('count')
|
||||
async filterCount(
|
||||
@Query(new SearchFilterPipe<NlpSample>({ allowedFields: ['text', 'type'] }))
|
||||
filters?: TFilterQuery<NlpSample>,
|
||||
) {
|
||||
return await this.count(filters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyzes the input text using the NLP service and returns the parsed result.
|
||||
*
|
||||
* @param text - The input text to be analyzed.
|
||||
*
|
||||
* @returns The result of the NLP parsing process.
|
||||
*/
|
||||
@Get('message')
|
||||
async message(@Query('text') text: string) {
|
||||
return this.nlpService.getNLP().parse(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the samples and entities for a given sample type.
|
||||
*
|
||||
* @param type - The sample type (e.g., 'train', 'test').
|
||||
* @returns An object containing the samples and entities.
|
||||
* @private
|
||||
*/
|
||||
private async getSamplesAndEntitiesByType(type: NlpSample['type']) {
|
||||
const samples = await this.nlpSampleService.findAndPopulate({
|
||||
type,
|
||||
});
|
||||
|
||||
const entities = await this.nlpEntityService.findAllAndPopulate();
|
||||
|
||||
return { samples, entities };
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates the training process for the NLP service using the 'train' sample type.
|
||||
*
|
||||
* @returns The result of the training process.
|
||||
*/
|
||||
@Get('train')
|
||||
async train() {
|
||||
const { samples, entities } =
|
||||
await this.getSamplesAndEntitiesByType('train');
|
||||
|
||||
return await this.nlpService.getNLP().train(samples, entities);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the NLP service using the 'test' sample type.
|
||||
*
|
||||
* @returns The result of the evaluation process.
|
||||
*/
|
||||
@Get('evaluate')
|
||||
async evaluate() {
|
||||
const { samples, entities } =
|
||||
await this.getSamplesAndEntitiesByType('test');
|
||||
|
||||
return await this.nlpService.getNLP().evaluate(samples, entities);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a single NLP sample by its ID.
|
||||
*
|
||||
* @param id - The ID of the NLP sample to find.
|
||||
* @param populate - Fields to populate in the returned sample.
|
||||
*
|
||||
* @returns The requested NLP sample if found.
|
||||
*/
|
||||
@Get(':id')
|
||||
async findOne(
|
||||
@Param('id') id: string,
|
||||
@Query(PopulatePipe) populate: string[],
|
||||
) {
|
||||
const doc = this.canPopulate(populate, ['entities'])
|
||||
? await this.nlpSampleService.findOneAndPopulate(id)
|
||||
: await this.nlpSampleService.findOne(id);
|
||||
if (!doc) {
|
||||
this.logger.warn(`Unable to find NLP Sample by id ${id}`);
|
||||
throw new NotFoundException(`NLP Sample with ID ${id} not found`);
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a paginated list of NLP samples.
|
||||
*
|
||||
* @param pageQuery - The query for pagination.
|
||||
* @param populate - Fields to populate in the returned samples.
|
||||
* @param filters - Filters to apply when fetching samples.
|
||||
*
|
||||
* @returns A paginated list of NLP samples.
|
||||
*/
|
||||
@Get()
|
||||
async findPage(
|
||||
@Query(PageQueryPipe) pageQuery: PageQueryDto<NlpSample>,
|
||||
@Query(PopulatePipe) populate: string[],
|
||||
@Query(new SearchFilterPipe<NlpSample>({ allowedFields: ['text', 'type'] }))
|
||||
filters: TFilterQuery<NlpSample>,
|
||||
) {
|
||||
return this.canPopulate(populate, ['entities'])
|
||||
? await this.nlpSampleService.findPageAndPopulate(filters, pageQuery)
|
||||
: await this.nlpSampleService.findPage(filters, pageQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing NLP sample by its ID.
|
||||
*
|
||||
* @param id - The ID of the NLP sample to update.
|
||||
* @param updateNlpSampleDto - The DTO containing the updated sample data.
|
||||
*
|
||||
* @returns The updated NLP sample with its entities.
|
||||
*/
|
||||
@CsrfCheck(true)
|
||||
@Patch(':id')
|
||||
async updateOne(
|
||||
@Param('id') id: string,
|
||||
@Body() updateNlpSampleDto: NlpSampleDto,
|
||||
): Promise<NlpSampleFull> {
|
||||
const { text, type, entities } = updateNlpSampleDto;
|
||||
const sample = await this.nlpSampleService.updateOne(id, {
|
||||
text,
|
||||
type,
|
||||
trained: false,
|
||||
});
|
||||
|
||||
if (!sample) {
|
||||
this.logger.warn(`Unable to update NLP Sample by id ${id}`);
|
||||
throw new NotFoundException(`NLP Sample with ID ${id} not found`);
|
||||
}
|
||||
|
||||
await this.nlpSampleEntityService.deleteMany({ sample: id });
|
||||
|
||||
const updatedSampleEntities =
|
||||
await this.nlpSampleEntityService.storeSampleEntities(sample, entities);
|
||||
|
||||
return {
|
||||
...sample,
|
||||
entities: updatedSampleEntities,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an NLP sample by its ID.
|
||||
*
|
||||
* @param id - The ID of the NLP sample to delete.
|
||||
*
|
||||
* @returns The result of the deletion operation.
|
||||
*/
|
||||
@CsrfCheck(true)
|
||||
@Delete(':id')
|
||||
@HttpCode(204)
|
||||
async deleteOne(@Param('id') id: string) {
|
||||
const result = await this.nlpSampleService.deleteCascadeOne(id);
|
||||
if (result.deletedCount === 0) {
|
||||
this.logger.warn(`Unable to delete NLP Sample by id ${id}`);
|
||||
throw new NotFoundException(`NLP Sample with ID ${id} not found`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports NLP samples from a CSV file.
|
||||
*
|
||||
* @param file - The file path or ID of the CSV file to import.
|
||||
*
|
||||
* @returns A success message after the import process is completed.
|
||||
*/
|
||||
@CsrfCheck(true)
|
||||
@Post('import/:file')
|
||||
async import(
|
||||
@Param('file')
|
||||
file: string,
|
||||
) {
|
||||
// Check if file is present
|
||||
const importedFile = await this.attachmentService.findOne(file);
|
||||
if (!importedFile) {
|
||||
throw new NotFoundException('Missing file!');
|
||||
}
|
||||
const filePath = importedFile
|
||||
? join(config.parameters.uploadDir, importedFile.location)
|
||||
: undefined;
|
||||
|
||||
// Check if file location is present
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new NotFoundException('File does not exist');
|
||||
}
|
||||
|
||||
const allEntities = await this.nlpEntityService.findAll();
|
||||
|
||||
// Check if file location is present
|
||||
if (allEntities.length === 0) {
|
||||
throw new NotFoundException(
|
||||
'No entities found, please create them first.',
|
||||
);
|
||||
}
|
||||
|
||||
// Read file content
|
||||
const data = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
// Parse local CSV file
|
||||
const result: {
|
||||
errors: any[];
|
||||
data: Array<Record<string, string>>;
|
||||
} = Papa.parse(data, {
|
||||
header: true,
|
||||
skipEmptyLines: true,
|
||||
});
|
||||
|
||||
if (result.errors && result.errors.length > 0) {
|
||||
this.logger.warn(
|
||||
`Errors parsing the file: ${JSON.stringify(result.errors)}`,
|
||||
);
|
||||
throw new BadRequestException(result.errors, {
|
||||
cause: result.errors,
|
||||
description: 'Error while parsing CSV',
|
||||
});
|
||||
}
|
||||
// Remove data with no intent
|
||||
const filteredData = result.data.filter((d) => d.intent !== 'none');
|
||||
// Reduce function to ensure executing promises one by one
|
||||
for (const d of filteredData) {
|
||||
try {
|
||||
// Check if a sample with the same text already exists
|
||||
const existingSamples = await this.nlpSampleService.find({
|
||||
text: d.text,
|
||||
});
|
||||
|
||||
// Skip if sample already exists
|
||||
|
||||
if (Array.isArray(existingSamples) && existingSamples.length > 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create a new sample dto
|
||||
const sample: NlpSampleCreateDto = {
|
||||
text: d.text,
|
||||
trained: false,
|
||||
};
|
||||
|
||||
// Create a new sample entity dto
|
||||
const entities: NlpSampleEntityValue[] = allEntities
|
||||
.filter(({ name }) => name in d)
|
||||
.map(({ name }) => {
|
||||
return {
|
||||
entity: name,
|
||||
value: d[name],
|
||||
};
|
||||
});
|
||||
|
||||
// Store any new entity/value
|
||||
const storedEntities = await this.nlpEntityService.storeNewEntities(
|
||||
sample.text,
|
||||
entities,
|
||||
['trait'],
|
||||
);
|
||||
// Store sample
|
||||
const createdSample = await this.nlpSampleService.create(sample);
|
||||
// Map and assign the sample ID to each stored entity
|
||||
const sampleEntities = storedEntities.map((se) => ({
|
||||
...se,
|
||||
sample: createdSample?.id,
|
||||
}));
|
||||
|
||||
// Store sample entities
|
||||
await this.nlpSampleEntityService.createMany(sampleEntities);
|
||||
} catch (err) {
|
||||
this.logger.error('Error occurred when extracting data. ', err);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log('Import process completed successfully.');
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
232
api/src/nlp/controllers/nlp-value.controller.spec.ts
Normal file
232
api/src/nlp/controllers/nlp-value.controller.spec.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { NotFoundException } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { nlpEntityFixtures } from '@/utils/test/fixtures/nlpentity';
|
||||
import {
|
||||
installNlpValueFixtures,
|
||||
nlpValueFixtures,
|
||||
} from '@/utils/test/fixtures/nlpvalue';
|
||||
import { getPageQuery } from '@/utils/test/pagination';
|
||||
import {
|
||||
closeInMongodConnection,
|
||||
rootMongooseTestModule,
|
||||
} from '@/utils/test/test';
|
||||
|
||||
import { NlpValueController } from './nlp-value.controller';
|
||||
import { NlpValueCreateDto } from '../dto/nlp-value.dto';
|
||||
import { NlpEntityRepository } from '../repositories/nlp-entity.repository';
|
||||
import { NlpSampleEntityRepository } from '../repositories/nlp-sample-entity.repository';
|
||||
import { NlpValueRepository } from '../repositories/nlp-value.repository';
|
||||
import { NlpEntityModel } from '../schemas/nlp-entity.schema';
|
||||
import { NlpSampleEntityModel } from '../schemas/nlp-sample-entity.schema';
|
||||
import { NlpValueModel, NlpValue } from '../schemas/nlp-value.schema';
|
||||
import { NlpEntityService } from '../services/nlp-entity.service';
|
||||
import { NlpValueService } from '../services/nlp-value.service';
|
||||
|
||||
describe('NlpValueController', () => {
|
||||
let nlpValueController: NlpValueController;
|
||||
let nlpValueService: NlpValueService;
|
||||
let nlpEntityService: NlpEntityService;
|
||||
let jhonNlpValue: NlpValue;
|
||||
let positiveValue: NlpValue;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [NlpValueController],
|
||||
imports: [
|
||||
rootMongooseTestModule(installNlpValueFixtures),
|
||||
MongooseModule.forFeature([
|
||||
NlpValueModel,
|
||||
NlpSampleEntityModel,
|
||||
NlpEntityModel,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
LoggerService,
|
||||
NlpValueRepository,
|
||||
NlpValueService,
|
||||
NlpSampleEntityRepository,
|
||||
NlpEntityService,
|
||||
NlpEntityRepository,
|
||||
EventEmitter2,
|
||||
],
|
||||
}).compile();
|
||||
nlpValueController = module.get<NlpValueController>(NlpValueController);
|
||||
nlpValueService = module.get<NlpValueService>(NlpValueService);
|
||||
nlpEntityService = module.get<NlpEntityService>(NlpEntityService);
|
||||
jhonNlpValue = await nlpValueService.findOne({ value: 'jhon' });
|
||||
positiveValue = await nlpValueService.findOne({ value: 'positive' });
|
||||
});
|
||||
afterAll(async () => {
|
||||
await closeInMongodConnection();
|
||||
});
|
||||
|
||||
afterEach(jest.clearAllMocks);
|
||||
|
||||
describe('findPage', () => {
|
||||
it('should find nlp Values, and foreach nlp value populate the corresponding entity', async () => {
|
||||
const pageQuery = getPageQuery<NlpValue>({
|
||||
sort: ['value', 'desc'],
|
||||
});
|
||||
const result = await nlpValueController.findPage(
|
||||
pageQuery,
|
||||
['entity'],
|
||||
{},
|
||||
);
|
||||
|
||||
const nlpValueFixturesWithEntities = nlpValueFixtures.reduce(
|
||||
(acc, curr) => {
|
||||
acc.push({
|
||||
...curr,
|
||||
entity: nlpEntityFixtures[parseInt(curr.entity)],
|
||||
});
|
||||
return acc;
|
||||
},
|
||||
[],
|
||||
);
|
||||
expect(result).toEqualPayload(nlpValueFixturesWithEntities);
|
||||
});
|
||||
|
||||
it('should find nlp Values', async () => {
|
||||
const pageQuery = getPageQuery<NlpValue>({
|
||||
sort: ['value', 'desc'],
|
||||
});
|
||||
const result = await nlpValueController.findPage(
|
||||
pageQuery,
|
||||
['invalidCriteria'],
|
||||
{},
|
||||
);
|
||||
const nlpEntities = await nlpEntityService.findAll();
|
||||
const nlpValueFixturesWithEntities = nlpValueFixtures.reduce(
|
||||
(acc, curr) => {
|
||||
const ValueWithEntities = {
|
||||
...curr,
|
||||
entity: nlpEntities[parseInt(curr.entity)].id,
|
||||
};
|
||||
acc.push(ValueWithEntities);
|
||||
return acc;
|
||||
},
|
||||
[],
|
||||
);
|
||||
expect(result).toEqualPayload(nlpValueFixturesWithEntities);
|
||||
});
|
||||
});
|
||||
|
||||
describe('count', () => {
|
||||
it('should count the nlp Values', async () => {
|
||||
const result = await nlpValueController.filterCount();
|
||||
const count = nlpValueFixtures.length;
|
||||
expect(result).toEqual({ count });
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create nlp Value', async () => {
|
||||
const nlpEntities = await nlpEntityService.findAll();
|
||||
const value: NlpValueCreateDto = {
|
||||
entity: nlpEntities[0].id,
|
||||
value: 'valuetest',
|
||||
expressions: ['synonym1', 'synonym2'],
|
||||
metadata: { firstkey: 'firstvalue', secondKey: 1995 },
|
||||
builtin: false,
|
||||
};
|
||||
const result = await nlpValueController.create(value);
|
||||
expect(result).toEqualPayload(value);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteOne', () => {
|
||||
it('should delete a nlp Value', async () => {
|
||||
const result = await nlpValueController.deleteOne(jhonNlpValue.id);
|
||||
expect(result.deletedCount).toEqual(1);
|
||||
});
|
||||
|
||||
it('should throw exception when nlp Value id not found', async () => {
|
||||
await expect(
|
||||
nlpValueController.deleteOne(jhonNlpValue.id),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOne', () => {
|
||||
it('should get a nlp Value', async () => {
|
||||
const result = await nlpValueController.findOne(positiveValue.id, [
|
||||
'invalidCreteria',
|
||||
]);
|
||||
const intentNlpEntity = await nlpEntityService.findOne({
|
||||
name: 'intent',
|
||||
});
|
||||
const valueWithEntity = {
|
||||
...nlpValueFixtures[0],
|
||||
entity: intentNlpEntity.id,
|
||||
};
|
||||
|
||||
expect(result).toEqualPayload(valueWithEntity);
|
||||
});
|
||||
|
||||
it('should get a nlp Value with populate', async () => {
|
||||
const intentNlpEntity = await nlpEntityService.findOne({
|
||||
name: 'intent',
|
||||
});
|
||||
const result = await nlpValueController.findOne(positiveValue.id, [
|
||||
'entity',
|
||||
]);
|
||||
const valueWithEntity = {
|
||||
...nlpValueFixtures[0],
|
||||
entity: intentNlpEntity,
|
||||
};
|
||||
expect(result).toEqualPayload(valueWithEntity);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException when Id does not exist', async () => {
|
||||
await expect(
|
||||
nlpValueController.findOne(jhonNlpValue.id, ['entity']),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateOne', () => {
|
||||
it('should update a nlp Value', async () => {
|
||||
const intentNlpEntity = await nlpEntityService.findOne({
|
||||
name: 'intent',
|
||||
});
|
||||
const updatedValue = {
|
||||
entity: intentNlpEntity.id,
|
||||
value: 'updated',
|
||||
expressions: [],
|
||||
builtin: true,
|
||||
};
|
||||
const result = await nlpValueController.updateOne(
|
||||
positiveValue.id,
|
||||
updatedValue,
|
||||
);
|
||||
expect(result).toEqualPayload(updatedValue);
|
||||
});
|
||||
|
||||
it('should throw exception when nlp value id not found', async () => {
|
||||
const intentNlpEntity = await nlpEntityService.findOne({
|
||||
name: 'intent',
|
||||
});
|
||||
await expect(
|
||||
nlpValueController.updateOne(jhonNlpValue.id, {
|
||||
entity: intentNlpEntity.id,
|
||||
value: 'updated',
|
||||
expressions: [],
|
||||
builtin: true,
|
||||
}),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
});
|
||||
187
api/src/nlp/controllers/nlp-value.controller.ts
Normal file
187
api/src/nlp/controllers/nlp-value.controller.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Patch,
|
||||
HttpCode,
|
||||
Query,
|
||||
NotFoundException,
|
||||
UseInterceptors,
|
||||
} from '@nestjs/common';
|
||||
import { CsrfCheck } from '@tekuconcept/nestjs-csrf';
|
||||
import { TFilterQuery } from 'mongoose';
|
||||
|
||||
import { CsrfInterceptor } from '@/interceptors/csrf.interceptor';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { BaseController } from '@/utils/generics/base-controller';
|
||||
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
|
||||
import { PageQueryPipe } from '@/utils/pagination/pagination-query.pipe';
|
||||
import { PopulatePipe } from '@/utils/pipes/populate.pipe';
|
||||
import { SearchFilterPipe } from '@/utils/pipes/search-filter.pipe';
|
||||
|
||||
import { NlpValueCreateDto, NlpValueUpdateDto } from '../dto/nlp-value.dto';
|
||||
import { NlpValue, NlpValueStub } from '../schemas/nlp-value.schema';
|
||||
import { NlpEntityService } from '../services/nlp-entity.service';
|
||||
import { NlpValueService } from '../services/nlp-value.service';
|
||||
|
||||
@UseInterceptors(CsrfInterceptor)
|
||||
@Controller('nlpvalue')
|
||||
export class NlpValueController extends BaseController<NlpValue, NlpValueStub> {
|
||||
constructor(
|
||||
private readonly nlpValueService: NlpValueService,
|
||||
private readonly nlpEntityService: NlpEntityService,
|
||||
private readonly logger: LoggerService,
|
||||
) {
|
||||
super(nlpValueService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new NLP value.
|
||||
*
|
||||
* Validates the input DTO and ensures the entity ID exists.
|
||||
*
|
||||
* @param createNlpValueDto - Data transfer object for creating NLP values.
|
||||
*
|
||||
* @returns A promise resolving to the created NLP value.
|
||||
*/
|
||||
@CsrfCheck(true)
|
||||
@Post()
|
||||
async create(
|
||||
@Body() createNlpValueDto: NlpValueCreateDto,
|
||||
): Promise<NlpValue> {
|
||||
this.validate({
|
||||
dto: createNlpValueDto,
|
||||
allowedIds: {
|
||||
entity: (await this.nlpEntityService.findOne(createNlpValueDto.entity))
|
||||
?.id,
|
||||
},
|
||||
});
|
||||
return await this.nlpValueService.create(createNlpValueDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the filtered count of NLP values.
|
||||
*
|
||||
* This endpoint supports filtering based on allowed fields.
|
||||
*
|
||||
* @returns A promise resolving to an object representing the count of filtered NLP values.
|
||||
*/
|
||||
@Get('count')
|
||||
async filterCount(
|
||||
@Query(
|
||||
new SearchFilterPipe<NlpValue>({ allowedFields: ['entity', 'value'] }),
|
||||
)
|
||||
filters?: TFilterQuery<NlpValue>,
|
||||
) {
|
||||
return await this.count(filters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a single NLP value by ID.
|
||||
*
|
||||
* Optionally populates related entities based on query parameters.
|
||||
*
|
||||
* @param id - The ID of the NLP value to retrieve.
|
||||
* @param populate - An array of related entities to populate.
|
||||
*
|
||||
* @returns A promise resolving to the found NLP value.
|
||||
*/
|
||||
@Get(':id')
|
||||
async findOne(
|
||||
@Param('id') id: string,
|
||||
@Query(PopulatePipe) populate: string[],
|
||||
) {
|
||||
const doc = this.canPopulate(populate, ['entity'])
|
||||
? await this.nlpValueService.findOneAndPopulate(id)
|
||||
: await this.nlpValueService.findOne(id);
|
||||
if (!doc) {
|
||||
this.logger.warn(`Unable to find NLP Value by id ${id}`);
|
||||
throw new NotFoundException(`NLP Value with ID ${id} not found`);
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a paginated list of NLP values.
|
||||
*
|
||||
* Supports filtering, pagination, and optional population of related entities.
|
||||
*
|
||||
* @param pageQuery - The pagination query parameters.
|
||||
* @param populate - An array of related entities to populate.
|
||||
* @param filters - Filters to apply when retrieving the NLP values.
|
||||
*
|
||||
* @returns A promise resolving to a paginated list of NLP values.
|
||||
*/
|
||||
@Get()
|
||||
async findPage(
|
||||
@Query(PageQueryPipe) pageQuery: PageQueryDto<NlpValue>,
|
||||
@Query(PopulatePipe) populate: string[],
|
||||
@Query(
|
||||
new SearchFilterPipe<NlpValue>({
|
||||
allowedFields: ['entity', 'value'],
|
||||
}),
|
||||
)
|
||||
filters: TFilterQuery<NlpValue>,
|
||||
) {
|
||||
return this.canPopulate(populate, ['entity'])
|
||||
? await this.nlpValueService.findPageAndPopulate(filters, pageQuery)
|
||||
: await this.nlpValueService.findPage(filters, pageQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing NLP value by ID.
|
||||
*
|
||||
* Validates the input DTO and ensures the NLP value with the specified ID exists.
|
||||
*
|
||||
* @param id - The ID of the NLP value to update.
|
||||
* @param updateNlpValueDto - Data transfer object for updating NLP values.
|
||||
*
|
||||
* @returns A promise resolving to the updated NLP value.
|
||||
*/
|
||||
@CsrfCheck(true)
|
||||
@Patch(':id')
|
||||
async updateOne(
|
||||
@Param('id') id: string,
|
||||
@Body() updateNlpValueDto: NlpValueUpdateDto,
|
||||
): Promise<NlpValue> {
|
||||
const result = await this.nlpValueService.updateOne(id, updateNlpValueDto);
|
||||
if (!result) {
|
||||
this.logger.warn(`Unable to update NLP Value by id ${id}`);
|
||||
throw new NotFoundException(`NLP Value with ID ${id} not found`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an NLP value by ID.
|
||||
*
|
||||
* Ensures that the NLP value with the specified ID exists before deletion.
|
||||
*
|
||||
* @param id - The ID of the NLP value to delete.
|
||||
*
|
||||
* @returns A promise resolving to the result of the deletion operation.
|
||||
*/
|
||||
@CsrfCheck(true)
|
||||
@Delete(':id')
|
||||
@HttpCode(204)
|
||||
async deleteOne(@Param('id') id: string) {
|
||||
const result = await this.nlpValueService.deleteCascadeOne(id);
|
||||
if (result.deletedCount === 0) {
|
||||
this.logger.warn(`Unable to delete NLP Value by id ${id}`);
|
||||
throw new NotFoundException(`NLP Value with ID ${id} not found`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
31
api/src/nlp/controllers/nlp.controller.ts
Normal file
31
api/src/nlp/controllers/nlp.controller.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
|
||||
import { NlpService } from '../services/nlp.service';
|
||||
|
||||
@Controller('nlp')
|
||||
export class NlpController {
|
||||
constructor(private readonly nlpService: NlpService) {}
|
||||
|
||||
/**
|
||||
* Retrieves a list of NLP helpers.
|
||||
*
|
||||
* @returns An array of objects containing the name of each NLP helper.
|
||||
*/
|
||||
@Get()
|
||||
getNlpHelpers(): { name: string }[] {
|
||||
return this.nlpService.getAll().map((helper) => {
|
||||
return {
|
||||
name: helper.getName(),
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
49
api/src/nlp/dto/nlp-entity.dto.ts
Normal file
49
api/src/nlp/dto/nlp-entity.dto.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import {
|
||||
IsString,
|
||||
IsOptional,
|
||||
IsBoolean,
|
||||
IsArray,
|
||||
Matches,
|
||||
IsIn,
|
||||
IsNotEmpty,
|
||||
} from 'class-validator';
|
||||
|
||||
export type Lookup = 'keywords' | 'trait' | 'free-text';
|
||||
|
||||
export class NlpEntityCreateDto {
|
||||
@ApiProperty({ description: 'Name of the nlp entity', type: String })
|
||||
@Matches(/^[a-zA-Z0-9_]+$/, {
|
||||
message: 'Only alphanumeric characters and underscores are allowed.',
|
||||
})
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
isArray: true,
|
||||
enum: ['keywords', 'trait', 'free-text'],
|
||||
})
|
||||
@IsArray()
|
||||
@IsIn(['keywords', 'trait', 'free-text'], { each: true })
|
||||
@IsOptional()
|
||||
lookups?: Lookup[];
|
||||
|
||||
@ApiPropertyOptional({ type: String })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
doc?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Nlp entity is builtin', type: Boolean })
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
builtin?: boolean;
|
||||
}
|
||||
43
api/src/nlp/dto/nlp-sample-entity.dto.ts
Normal file
43
api/src/nlp/dto/nlp-sample-entity.dto.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
import { IsObjectId } from '@/utils/validation-rules/is-object-id';
|
||||
|
||||
export class NlpSampleEntityCreateDto {
|
||||
@ApiPropertyOptional({ type: Number })
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
start?: number;
|
||||
|
||||
@ApiPropertyOptional({ type: Number })
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
end?: number;
|
||||
|
||||
@ApiProperty({ type: String })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@IsObjectId({ message: 'Entity must be a valid ObjectId' })
|
||||
entity: string;
|
||||
|
||||
@ApiProperty({ type: String })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@IsObjectId({ message: 'Value must be a valid ObjectId' })
|
||||
value: string;
|
||||
|
||||
@ApiProperty({ type: String })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@IsObjectId({ message: 'Sample must be a valid ObjectId' })
|
||||
sample: string;
|
||||
}
|
||||
50
api/src/nlp/dto/nlp-sample.dto.ts
Normal file
50
api/src/nlp/dto/nlp-sample.dto.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { ApiProperty, ApiPropertyOptional, PartialType } from '@nestjs/swagger';
|
||||
import {
|
||||
IsBoolean,
|
||||
IsIn,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
} from 'class-validator';
|
||||
|
||||
import { NlpSampleEntityValue, NlpSampleState } from '../schemas/types';
|
||||
|
||||
export class NlpSampleCreateDto {
|
||||
@ApiProperty({ description: 'nlp sample text', type: String })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
text: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'nlp sample is trained', type: Boolean })
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
trained?: boolean;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'nlp sample type',
|
||||
enum: Object.values(NlpSampleState),
|
||||
})
|
||||
@IsString()
|
||||
@IsIn(Object.values(NlpSampleState))
|
||||
@IsOptional()
|
||||
type?: NlpSampleState;
|
||||
}
|
||||
|
||||
export class NlpSampleDto extends NlpSampleCreateDto {
|
||||
@ApiPropertyOptional({
|
||||
description: 'nlp sample entities',
|
||||
})
|
||||
@IsOptional()
|
||||
entities?: NlpSampleEntityValue[];
|
||||
}
|
||||
|
||||
export class NlpSampleUpdateDto extends PartialType(NlpSampleCreateDto) {}
|
||||
55
api/src/nlp/dto/nlp-value.dto.ts
Normal file
55
api/src/nlp/dto/nlp-value.dto.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { PartialType } from '@nestjs/mapped-types';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import {
|
||||
IsString,
|
||||
IsBoolean,
|
||||
IsArray,
|
||||
IsObject,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
} from 'class-validator';
|
||||
|
||||
import { IsObjectId } from '@/utils/validation-rules/is-object-id';
|
||||
|
||||
export class NlpValueCreateDto {
|
||||
@ApiProperty({ description: 'Nlp value', type: String })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
value: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Nlp value expressions',
|
||||
isArray: true,
|
||||
type: Array,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
expressions?: string[];
|
||||
|
||||
@ApiPropertyOptional({ description: 'Nlp value metadata', type: Object })
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
metadata?: Record<string, any>;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Nlp value is builtin', type: Boolean })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
builtin?: boolean;
|
||||
|
||||
@ApiProperty({ description: 'Nlp value entity', type: String })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@IsObjectId({ message: 'Entity must be a valid ObjectId' })
|
||||
entity: string;
|
||||
}
|
||||
|
||||
export class NlpValueUpdateDto extends PartialType(NlpValueCreateDto) {}
|
||||
195
api/src/nlp/lib/BaseNlpHelper.ts
Normal file
195
api/src/nlp/lib/BaseNlpHelper.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file NlpAdapter is an abstract class for define an NLP provider adapter
|
||||
* @author Hexastack <contact@hexastack.com>
|
||||
*/
|
||||
|
||||
/**
|
||||
* @module Services/NLP
|
||||
*
|
||||
* NlpAdapter is an abstract class from which each NLP provider adapter should extend from.
|
||||
*/
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { NlpEntity, NlpEntityFull } from '@/nlp/schemas/nlp-entity.schema';
|
||||
import { NlpSample, NlpSampleFull } from '@/nlp/schemas/nlp-sample.schema';
|
||||
import { NlpValue, NlpValueFull } from '@/nlp/schemas/nlp-value.schema';
|
||||
import { Settings } from '@/setting/schemas/types';
|
||||
|
||||
import { Nlp } from './types';
|
||||
import { NlpEntityService } from '../services/nlp-entity.service';
|
||||
import { NlpSampleService } from '../services/nlp-sample.service';
|
||||
import { NlpService } from '../services/nlp.service';
|
||||
|
||||
export default abstract class BaseNlpHelper {
|
||||
protected settings: Settings['nlp_settings'];
|
||||
|
||||
constructor(
|
||||
protected readonly logger: LoggerService,
|
||||
protected readonly nlpService: NlpService,
|
||||
protected readonly nlpSampleService: NlpSampleService,
|
||||
protected readonly nlpEntityService: NlpEntityService,
|
||||
) {}
|
||||
|
||||
setSettings(settings: Settings['nlp_settings']) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the helper's name
|
||||
*
|
||||
* @returns Helper's name
|
||||
*/
|
||||
abstract getName(): string;
|
||||
|
||||
/**
|
||||
* Updates an entity
|
||||
*
|
||||
* @param entity - The updated entity
|
||||
*
|
||||
* @returns The updated entity otherwise an error
|
||||
*/
|
||||
async updateEntity(entity: NlpEntity): Promise<NlpEntity> {
|
||||
return entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an entity
|
||||
*
|
||||
* @param entity - The entity to add
|
||||
* @returns The added entity otherwise an error
|
||||
*/
|
||||
addEntity(_entity: NlpEntity): Promise<string> {
|
||||
return new Promise((resolve, _reject) => {
|
||||
return resolve(uuidv4());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an entity
|
||||
*
|
||||
* @param entityId - The entity ID to delete
|
||||
*
|
||||
* @return The deleted entity otherwise an error
|
||||
*/
|
||||
async deleteEntity(entityId: string): Promise<any> {
|
||||
return entityId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an entity value
|
||||
*
|
||||
* @param value - The updated update
|
||||
*
|
||||
* @returns The updated value otherwise it should throw an error
|
||||
*/
|
||||
async updateValue(value: NlpValue): Promise<NlpValue> {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an entity value
|
||||
*
|
||||
* @param value - The value to add
|
||||
*
|
||||
* @returns The added value otherwise it should throw an error
|
||||
*/
|
||||
addValue(_value: NlpValue): Promise<string> {
|
||||
return new Promise((resolve, _reject) => {
|
||||
return resolve(uuidv4());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an entity value
|
||||
*
|
||||
* @param value - The value to delete
|
||||
*
|
||||
* @returns The deleted value otherwise an error
|
||||
*/
|
||||
async deleteValue(value: NlpValueFull): Promise<NlpValueFull> {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns training dataset in NLP provider compatible format
|
||||
*
|
||||
* @param samples - Sample to train
|
||||
* @param entities - All available entities
|
||||
*
|
||||
* @returns The formatted NLP training set
|
||||
*/
|
||||
abstract format(samples: NlpSampleFull[], entities: NlpEntityFull[]): unknown;
|
||||
|
||||
/**
|
||||
* Perform training request
|
||||
*
|
||||
* @param samples - Samples to train
|
||||
* @param entities - All available entities
|
||||
*
|
||||
* @returns Training result
|
||||
*/
|
||||
abstract train(
|
||||
samples: NlpSampleFull[],
|
||||
entities: NlpEntityFull[],
|
||||
): Promise<any>;
|
||||
|
||||
/**
|
||||
* Perform evaluation request
|
||||
*
|
||||
* @param samples - Samples to evaluate
|
||||
* @param entities - All available entities
|
||||
*
|
||||
* @returns NLP evaluation result
|
||||
*/
|
||||
abstract evaluate(
|
||||
samples: NlpSampleFull[],
|
||||
entities: NlpEntityFull[],
|
||||
): Promise<any>;
|
||||
|
||||
/**
|
||||
* Delete/Forget a sample
|
||||
*
|
||||
* @param sample - The sample to delete/forget
|
||||
*
|
||||
* @returns The deleted sample otherwise an error
|
||||
*/
|
||||
async forget(sample: NlpSample): Promise<NlpSample> {
|
||||
return sample;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns only the entities that have strong confidence (> than the threshold), can return an empty result
|
||||
*
|
||||
* @param nlp - The nlp provider parse returned result
|
||||
* @param threshold - Whenever to apply threshold filter or not
|
||||
*
|
||||
* @returns NLP Parsed entities
|
||||
*/
|
||||
abstract bestGuess(nlp: any, threshold: boolean): Nlp.ParseEntities;
|
||||
|
||||
/**
|
||||
* Returns only the entities that have strong confidence (> than the threshold), can return an empty result
|
||||
*
|
||||
* @param text - The text to parse
|
||||
* @param threshold - Whenever to apply threshold filter or not
|
||||
* @param project - Whenever to request a specific model
|
||||
*
|
||||
* @returns NLP Parsed entities
|
||||
*/
|
||||
abstract parse(
|
||||
text: string,
|
||||
threshold?: boolean,
|
||||
project?: string,
|
||||
): Promise<Nlp.ParseEntities>;
|
||||
}
|
||||
27
api/src/nlp/lib/types.ts
Normal file
27
api/src/nlp/lib/types.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
export namespace Nlp {
|
||||
export interface Config {
|
||||
endpoint?: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface ParseEntity {
|
||||
entity: string; // Entity name
|
||||
value: string; // Value name
|
||||
confidence: number;
|
||||
start?: number;
|
||||
end?: number;
|
||||
}
|
||||
|
||||
export interface ParseEntities {
|
||||
entities: ParseEntity[];
|
||||
}
|
||||
}
|
||||
70
api/src/nlp/nlp.module.ts
Normal file
70
api/src/nlp/nlp.module.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { InjectDynamicProviders } from 'nestjs-dynamic-providers';
|
||||
|
||||
import { AttachmentModule } from '@/attachment/attachment.module';
|
||||
|
||||
import { NlpEntityController } from './controllers/nlp-entity.controller';
|
||||
import { NlpSampleController } from './controllers/nlp-sample.controller';
|
||||
import { NlpValueController } from './controllers/nlp-value.controller';
|
||||
import { NlpController } from './controllers/nlp.controller';
|
||||
import { NlpEntityRepository } from './repositories/nlp-entity.repository';
|
||||
import { NlpSampleEntityRepository } from './repositories/nlp-sample-entity.repository';
|
||||
import { NlpSampleRepository } from './repositories/nlp-sample.repository';
|
||||
import { NlpValueRepository } from './repositories/nlp-value.repository';
|
||||
import { NlpEntityModel } from './schemas/nlp-entity.schema';
|
||||
import { NlpSampleEntityModel } from './schemas/nlp-sample-entity.schema';
|
||||
import { NlpSampleModel } from './schemas/nlp-sample.schema';
|
||||
import { NlpValueModel } from './schemas/nlp-value.schema';
|
||||
import { NlpEntitySeeder } from './seeds/nlp-entity.seed';
|
||||
import { NlpValueSeeder } from './seeds/nlp-value.seed';
|
||||
import { NlpEntityService } from './services/nlp-entity.service';
|
||||
import { NlpSampleEntityService } from './services/nlp-sample-entity.service';
|
||||
import { NlpSampleService } from './services/nlp-sample.service';
|
||||
import { NlpValueService } from './services/nlp-value.service';
|
||||
import { NlpService } from './services/nlp.service';
|
||||
|
||||
@InjectDynamicProviders('dist/**/*.nlp.helper.js')
|
||||
@Module({
|
||||
imports: [
|
||||
MongooseModule.forFeature([
|
||||
NlpEntityModel,
|
||||
NlpValueModel,
|
||||
NlpSampleModel,
|
||||
NlpSampleEntityModel,
|
||||
]),
|
||||
AttachmentModule,
|
||||
HttpModule,
|
||||
],
|
||||
controllers: [
|
||||
NlpEntityController,
|
||||
NlpValueController,
|
||||
NlpSampleController,
|
||||
NlpController,
|
||||
],
|
||||
providers: [
|
||||
NlpEntityRepository,
|
||||
NlpValueRepository,
|
||||
NlpSampleRepository,
|
||||
NlpSampleEntityRepository,
|
||||
NlpEntityService,
|
||||
NlpValueService,
|
||||
NlpSampleService,
|
||||
NlpSampleEntityService,
|
||||
NlpService,
|
||||
NlpEntitySeeder,
|
||||
NlpValueSeeder,
|
||||
],
|
||||
exports: [NlpService, NlpSampleService, NlpEntityService, NlpValueService],
|
||||
})
|
||||
export class NlpModule {}
|
||||
117
api/src/nlp/repositories/nlp-entity.repository.spec.ts
Normal file
117
api/src/nlp/repositories/nlp-entity.repository.spec.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { nlpEntityFixtures } from '@/utils/test/fixtures/nlpentity';
|
||||
import { installNlpValueFixtures } from '@/utils/test/fixtures/nlpvalue';
|
||||
import { getPageQuery } from '@/utils/test/pagination';
|
||||
import {
|
||||
closeInMongodConnection,
|
||||
rootMongooseTestModule,
|
||||
} from '@/utils/test/test';
|
||||
|
||||
import { NlpEntityRepository } from './nlp-entity.repository';
|
||||
import { NlpSampleEntityRepository } from './nlp-sample-entity.repository';
|
||||
import { NlpValueRepository } from './nlp-value.repository';
|
||||
import { NlpEntityModel, NlpEntity } from '../schemas/nlp-entity.schema';
|
||||
import { NlpSampleEntityModel } from '../schemas/nlp-sample-entity.schema';
|
||||
import { NlpValueModel } from '../schemas/nlp-value.schema';
|
||||
|
||||
describe('NlpEntityRepository', () => {
|
||||
let nlpEntityRepository: NlpEntityRepository;
|
||||
let nlpValueRepository: NlpValueRepository;
|
||||
let firstNameNlpEntity: NlpEntity;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [
|
||||
rootMongooseTestModule(installNlpValueFixtures),
|
||||
MongooseModule.forFeature([
|
||||
NlpEntityModel,
|
||||
NlpValueModel,
|
||||
NlpSampleEntityModel,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
NlpEntityRepository,
|
||||
NlpValueRepository,
|
||||
NlpSampleEntityRepository,
|
||||
EventEmitter2,
|
||||
],
|
||||
}).compile();
|
||||
nlpEntityRepository = module.get<NlpEntityRepository>(NlpEntityRepository);
|
||||
nlpValueRepository = module.get<NlpValueRepository>(NlpValueRepository);
|
||||
firstNameNlpEntity = await nlpEntityRepository.findOne({
|
||||
name: 'first_name',
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await closeInMongodConnection();
|
||||
});
|
||||
|
||||
afterEach(jest.clearAllMocks);
|
||||
|
||||
describe('The deleteCascadeOne function', () => {
|
||||
it('should delete a nlp entity', async () => {
|
||||
const intentNlpEntity = await nlpEntityRepository.findOne({
|
||||
name: 'intent',
|
||||
});
|
||||
const result = await nlpEntityRepository.deleteOne(intentNlpEntity.id);
|
||||
|
||||
expect(result.deletedCount).toEqual(1);
|
||||
|
||||
const intentNlpValues = await nlpValueRepository.find({
|
||||
entity: intentNlpEntity.id,
|
||||
});
|
||||
|
||||
expect(intentNlpValues.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOneAndPopulate', () => {
|
||||
it('should return a nlp entity with populate', async () => {
|
||||
const firstNameValues = await nlpValueRepository.find({
|
||||
entity: firstNameNlpEntity.id,
|
||||
});
|
||||
const result = await nlpEntityRepository.findOneAndPopulate(
|
||||
firstNameNlpEntity.id,
|
||||
);
|
||||
expect(result).toEqualPayload({
|
||||
...nlpEntityFixtures[1],
|
||||
values: firstNameValues,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('findPageAndPopulate', () => {
|
||||
it('should return all nlp entities with populate', async () => {
|
||||
const pageQuery = getPageQuery<NlpEntity>({
|
||||
sort: ['name', 'desc'],
|
||||
});
|
||||
const firstNameValues = await nlpValueRepository.find({
|
||||
entity: firstNameNlpEntity.id,
|
||||
});
|
||||
const result = await nlpEntityRepository.findPageAndPopulate(
|
||||
{ _id: firstNameNlpEntity.id },
|
||||
pageQuery,
|
||||
);
|
||||
expect(result).toEqualPayload([
|
||||
{
|
||||
id: firstNameNlpEntity.id,
|
||||
...nlpEntityFixtures[1],
|
||||
values: firstNameValues,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
148
api/src/nlp/repositories/nlp-entity.repository.ts
Normal file
148
api/src/nlp/repositories/nlp-entity.repository.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { Document, Model, Query, TFilterQuery, Types } from 'mongoose';
|
||||
|
||||
import { BaseRepository, DeleteResult } from '@/utils/generics/base-repository';
|
||||
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
|
||||
|
||||
import { NlpSampleEntityRepository } from './nlp-sample-entity.repository';
|
||||
import { NlpValueRepository } from './nlp-value.repository';
|
||||
import { NlpEntity, NlpEntityFull } from '../schemas/nlp-entity.schema';
|
||||
|
||||
@Injectable()
|
||||
export class NlpEntityRepository extends BaseRepository<NlpEntity, 'values'> {
|
||||
constructor(
|
||||
@InjectModel(NlpEntity.name) readonly model: Model<NlpEntity>,
|
||||
private readonly nlpValueRepository: NlpValueRepository,
|
||||
private readonly nlpSampleEntityRepository: NlpSampleEntityRepository,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
) {
|
||||
super(model, NlpEntity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Post-create hook that triggers after an NLP entity is created.
|
||||
* Emits an event to notify other parts of the system about the creation.
|
||||
* Bypasses built-in entities.
|
||||
*
|
||||
* @param created - The newly created NLP entity document.
|
||||
*/
|
||||
async postCreate(
|
||||
_created: Document<unknown, object, NlpEntity> &
|
||||
NlpEntity & { _id: Types.ObjectId },
|
||||
): Promise<void> {
|
||||
if (!_created.builtin) {
|
||||
// Bypass builtin entities (probably fixtures)
|
||||
this.eventEmitter.emit('hook:nlp:entity:create', _created);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Post-update hook that triggers after an NLP entity is updated.
|
||||
* Emits an event to notify other parts of the system about the update.
|
||||
* Bypasses built-in entities.
|
||||
*
|
||||
* @param query - The query used to find and update the entity.
|
||||
* @param updated - The updated NLP entity document.
|
||||
*/
|
||||
async postUpdate(
|
||||
_query: Query<
|
||||
Document<NlpEntity, any, any>,
|
||||
Document<NlpEntity, any, any>,
|
||||
unknown,
|
||||
NlpEntity,
|
||||
'findOneAndUpdate'
|
||||
>,
|
||||
updated: NlpEntity,
|
||||
): Promise<void> {
|
||||
if (!updated?.builtin) {
|
||||
// Bypass builtin entities (probably fixtures)
|
||||
this.eventEmitter.emit('hook:nlp:entity:update', updated);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-delete hook that triggers before an NLP entity is deleted.
|
||||
* Deletes related NLP values and sample entities before the entity deletion.
|
||||
* Emits an event to notify other parts of the system about the deletion.
|
||||
* Bypasses built-in entities.
|
||||
*
|
||||
* @param query The query used to delete the entity.
|
||||
* @param criteria The filter criteria used to find the entity for deletion.
|
||||
*/
|
||||
async preDelete(
|
||||
_query: Query<
|
||||
DeleteResult,
|
||||
Document<NlpEntity, any, any>,
|
||||
unknown,
|
||||
NlpEntity,
|
||||
'deleteOne' | 'deleteMany'
|
||||
>,
|
||||
criteria: TFilterQuery<NlpEntity>,
|
||||
): Promise<void> {
|
||||
if (criteria._id) {
|
||||
await this.nlpValueRepository.deleteMany({ entity: criteria._id });
|
||||
await this.nlpSampleEntityRepository.deleteMany({ entity: criteria._id });
|
||||
|
||||
const entities = await this.find(
|
||||
typeof criteria === 'string' ? { _id: criteria } : criteria,
|
||||
);
|
||||
entities
|
||||
.filter((e) => !e.builtin)
|
||||
.map((e) => {
|
||||
this.eventEmitter.emit('hook:nlp:entity:delete', e);
|
||||
});
|
||||
} else {
|
||||
throw new Error('Attempted to delete NLP entity using unknown criteria');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all NLP entities and populates related `values`.
|
||||
*
|
||||
* @returns Promise containing an array of fully populated NLP entities.
|
||||
*/
|
||||
async findAllAndPopulate() {
|
||||
const query = this.findAllQuery().populate(['values']);
|
||||
return await this.execute(query, NlpEntityFull);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a paginated list of NLP entities based on filter criteria,
|
||||
* and populates related `values`.
|
||||
*
|
||||
* @param filter Filter criteria for NLP entities.
|
||||
* @param pageQuery Pagination query.
|
||||
*
|
||||
* @returns Promise containing the paginated result of fully populated NLP entities.
|
||||
*/
|
||||
async findPageAndPopulate(
|
||||
filter: TFilterQuery<NlpEntity>,
|
||||
pageQuery: PageQueryDto<NlpEntity>,
|
||||
) {
|
||||
const query = this.findPageQuery(filter, pageQuery).populate(['values']);
|
||||
return await this.execute(query, NlpEntityFull);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a single NLP entity by its ID and populates related `values`.
|
||||
*
|
||||
* @param id The ID of the NLP entity.
|
||||
*
|
||||
* @returns Promise containing the fully populated NLP entity.
|
||||
*/
|
||||
async findOneAndPopulate(id: string) {
|
||||
const query = this.findOneQuery(id).populate(['values']);
|
||||
return await this.executeOne(query, NlpEntityFull);
|
||||
}
|
||||
}
|
||||
137
api/src/nlp/repositories/nlp-sample-entity.repository.spec.ts
Normal file
137
api/src/nlp/repositories/nlp-sample-entity.repository.spec.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { nlpSampleFixtures } from '@/utils/test/fixtures/nlpsample';
|
||||
import {
|
||||
installNlpSampleEntityFixtures,
|
||||
nlpSampleEntityFixtures,
|
||||
} from '@/utils/test/fixtures/nlpsampleentity';
|
||||
import { nlpValueFixtures } from '@/utils/test/fixtures/nlpvalue';
|
||||
import { getPageQuery } from '@/utils/test/pagination';
|
||||
import {
|
||||
closeInMongodConnection,
|
||||
rootMongooseTestModule,
|
||||
} from '@/utils/test/test';
|
||||
|
||||
import { NlpEntityRepository } from './nlp-entity.repository';
|
||||
import { NlpSampleEntityRepository } from './nlp-sample-entity.repository';
|
||||
import { NlpValueRepository } from './nlp-value.repository';
|
||||
import { NlpEntityModel, NlpEntity } from '../schemas/nlp-entity.schema';
|
||||
import {
|
||||
NlpSampleEntityModel,
|
||||
NlpSampleEntity,
|
||||
} from '../schemas/nlp-sample-entity.schema';
|
||||
import { NlpSampleModel } from '../schemas/nlp-sample.schema';
|
||||
import { NlpValueModel } from '../schemas/nlp-value.schema';
|
||||
|
||||
describe('NlpSampleEntityRepository', () => {
|
||||
let nlpSampleEntityRepository: NlpSampleEntityRepository;
|
||||
let nlpEntityRepository: NlpEntityRepository;
|
||||
let nlpSampleEntities: NlpSampleEntity[];
|
||||
let nlpEntities: NlpEntity[];
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [
|
||||
rootMongooseTestModule(installNlpSampleEntityFixtures),
|
||||
MongooseModule.forFeature([
|
||||
NlpSampleEntityModel,
|
||||
NlpEntityModel,
|
||||
NlpValueModel,
|
||||
NlpSampleModel,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
NlpSampleEntityRepository,
|
||||
NlpEntityRepository,
|
||||
NlpValueRepository,
|
||||
EventEmitter2,
|
||||
],
|
||||
}).compile();
|
||||
nlpSampleEntityRepository = module.get<NlpSampleEntityRepository>(
|
||||
NlpSampleEntityRepository,
|
||||
);
|
||||
nlpEntityRepository = module.get<NlpEntityRepository>(NlpEntityRepository);
|
||||
nlpSampleEntities = await nlpSampleEntityRepository.findAll();
|
||||
nlpEntities = await nlpEntityRepository.findAll();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await closeInMongodConnection();
|
||||
});
|
||||
|
||||
afterEach(jest.clearAllMocks);
|
||||
|
||||
describe('findOneAndPopulate', () => {
|
||||
it('should return a nlp SampleEntity with populate', async () => {
|
||||
const result = await nlpSampleEntityRepository.findOneAndPopulate(
|
||||
nlpSampleEntities[0].id,
|
||||
);
|
||||
expect(result).toEqualPayload({
|
||||
...nlpSampleEntityFixtures[0],
|
||||
entity: nlpEntities[0],
|
||||
value: { ...nlpValueFixtures[0], entity: nlpEntities[0].id },
|
||||
sample: nlpSampleFixtures[0],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('findPageAndPopulate', () => {
|
||||
it('should return all nlp entities with populate', async () => {
|
||||
const pageQuery = getPageQuery<NlpSampleEntity>({
|
||||
sort: ['value', 'asc'],
|
||||
});
|
||||
const result = await nlpSampleEntityRepository.findPageAndPopulate(
|
||||
{},
|
||||
pageQuery,
|
||||
);
|
||||
const nlpValueFixturesWithEntities = nlpValueFixtures.reduce(
|
||||
(acc, curr) => {
|
||||
const ValueWithEntities = {
|
||||
...curr,
|
||||
entity: nlpEntities[0].id,
|
||||
};
|
||||
acc.push(ValueWithEntities);
|
||||
return acc;
|
||||
},
|
||||
[],
|
||||
);
|
||||
nlpValueFixturesWithEntities[2] = {
|
||||
...nlpValueFixturesWithEntities[2],
|
||||
entity: nlpEntities[1].id,
|
||||
};
|
||||
|
||||
const nlpSampleEntityFixturesWithPopulate =
|
||||
nlpSampleEntityFixtures.reduce((acc, curr) => {
|
||||
const sampleEntityWithPopulate = {
|
||||
...curr,
|
||||
entity: nlpEntities[curr.entity],
|
||||
value: nlpValueFixturesWithEntities[curr.value],
|
||||
sample: nlpSampleFixtures[curr.sample],
|
||||
};
|
||||
acc.push(sampleEntityWithPopulate);
|
||||
return acc;
|
||||
}, []);
|
||||
expect(result).toEqualPayload(nlpSampleEntityFixturesWithPopulate);
|
||||
});
|
||||
});
|
||||
|
||||
describe('The deleteCascadeOne function', () => {
|
||||
it('should delete a nlp SampleEntity', async () => {
|
||||
const result = await nlpSampleEntityRepository.deleteOne(
|
||||
nlpSampleEntities[1].id,
|
||||
);
|
||||
expect(result.deletedCount).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
63
api/src/nlp/repositories/nlp-sample-entity.repository.ts
Normal file
63
api/src/nlp/repositories/nlp-sample-entity.repository.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { TFilterQuery, Model } from 'mongoose';
|
||||
|
||||
import { BaseRepository } from '@/utils/generics/base-repository';
|
||||
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
|
||||
|
||||
import {
|
||||
NlpSampleEntity,
|
||||
NlpSampleEntityFull,
|
||||
} from '../schemas/nlp-sample-entity.schema';
|
||||
|
||||
@Injectable()
|
||||
export class NlpSampleEntityRepository extends BaseRepository<
|
||||
NlpSampleEntity,
|
||||
'entity' | 'value' | 'sample'
|
||||
> {
|
||||
constructor(
|
||||
@InjectModel(NlpSampleEntity.name) readonly model: Model<NlpSampleEntity>,
|
||||
) {
|
||||
super(model, NlpSampleEntity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a paginated set of `NlpSampleEntity` documents based on the provided filter
|
||||
* and page query. Also populates related fields such as `entity`, `value`, and `sample`.
|
||||
*
|
||||
* @param filter - The query filter for retrieving documents.
|
||||
* @param pageQuery - The pagination options.
|
||||
* @returns The paginated results with populated fields.
|
||||
*/
|
||||
async findPageAndPopulate(
|
||||
filter: TFilterQuery<NlpSampleEntity>,
|
||||
pageQuery: PageQueryDto<NlpSampleEntity>,
|
||||
) {
|
||||
const query = this.findPageQuery(filter, pageQuery).populate([
|
||||
'entity',
|
||||
'value',
|
||||
'sample',
|
||||
]);
|
||||
return await this.execute(query, NlpSampleEntityFull);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a single `NlpSampleEntity` document by ID and populates the related fields `entity`, `value`, and `sample`.
|
||||
*
|
||||
* @param id - The ID of the document to retrieve.
|
||||
* @returns The document with populated fields.
|
||||
*/
|
||||
async findOneAndPopulate(id: string) {
|
||||
const query = this.findOneQuery(id).populate(['entity', 'value', 'sample']);
|
||||
return await this.executeOne(query, NlpSampleEntityFull);
|
||||
}
|
||||
}
|
||||
128
api/src/nlp/repositories/nlp-sample.repository.spec.ts
Normal file
128
api/src/nlp/repositories/nlp-sample.repository.spec.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { nlpSampleFixtures } from '@/utils/test/fixtures/nlpsample';
|
||||
import { installNlpSampleEntityFixtures } from '@/utils/test/fixtures/nlpsampleentity';
|
||||
import { getPageQuery } from '@/utils/test/pagination';
|
||||
import {
|
||||
closeInMongodConnection,
|
||||
rootMongooseTestModule,
|
||||
} from '@/utils/test/test';
|
||||
|
||||
import { NlpSampleEntityRepository } from './nlp-sample-entity.repository';
|
||||
import { NlpSampleRepository } from './nlp-sample.repository';
|
||||
import {
|
||||
NlpSampleEntityModel,
|
||||
NlpSampleEntity,
|
||||
} from '../schemas/nlp-sample-entity.schema';
|
||||
import { NlpSampleModel, NlpSample } from '../schemas/nlp-sample.schema';
|
||||
|
||||
describe('NlpSampleRepository', () => {
|
||||
let nlpSampleRepository: NlpSampleRepository;
|
||||
let nlpSampleEntityRepository: NlpSampleEntityRepository;
|
||||
let nlpSampleEntity: NlpSampleEntity;
|
||||
let noNlpSample: NlpSample;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [
|
||||
rootMongooseTestModule(installNlpSampleEntityFixtures),
|
||||
MongooseModule.forFeature([NlpSampleModel, NlpSampleEntityModel]),
|
||||
],
|
||||
providers: [
|
||||
NlpSampleRepository,
|
||||
NlpSampleEntityRepository,
|
||||
EventEmitter2,
|
||||
],
|
||||
}).compile();
|
||||
nlpSampleRepository = module.get<NlpSampleRepository>(NlpSampleRepository);
|
||||
nlpSampleEntityRepository = module.get<NlpSampleEntityRepository>(
|
||||
NlpSampleEntityRepository,
|
||||
);
|
||||
noNlpSample = await nlpSampleRepository.findOne({ text: 'No' });
|
||||
nlpSampleEntity = await nlpSampleEntityRepository.findOne({
|
||||
sample: noNlpSample.id,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await closeInMongodConnection();
|
||||
});
|
||||
|
||||
afterEach(jest.clearAllMocks);
|
||||
|
||||
describe('findOneAndPopulate', () => {
|
||||
it('should return a nlp Sample with populate', async () => {
|
||||
const result = await nlpSampleRepository.findOneAndPopulate(
|
||||
noNlpSample.id,
|
||||
);
|
||||
expect(result).toEqualPayload({
|
||||
...nlpSampleFixtures[1],
|
||||
entities: [nlpSampleEntity],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('findPageAndPopulate', () => {
|
||||
it('should return all nlp samples with populate', async () => {
|
||||
const pageQuery = getPageQuery<NlpSample>({
|
||||
sort: ['text', 'desc'],
|
||||
});
|
||||
const result = await nlpSampleRepository.findPageAndPopulate(
|
||||
{},
|
||||
pageQuery,
|
||||
);
|
||||
const nlpSamples = await nlpSampleRepository.findAll();
|
||||
const nlpSampleEntities = await nlpSampleEntityRepository.findAll();
|
||||
|
||||
const nlpSampleFixturesWithEntities = nlpSamples.reduce(
|
||||
(acc, currSample) => {
|
||||
const sampleWithEntities = {
|
||||
...currSample,
|
||||
entities: nlpSampleEntities.filter((currSampleEntity) => {
|
||||
return currSampleEntity.sample === currSample.id;
|
||||
}),
|
||||
};
|
||||
acc.push(sampleWithEntities);
|
||||
return acc;
|
||||
},
|
||||
[],
|
||||
);
|
||||
expect(result).toEqualPayload(nlpSampleFixturesWithEntities);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateMany', () => {
|
||||
it('should update many nlp samples', async () => {
|
||||
const result = await nlpSampleRepository.updateMany(
|
||||
{},
|
||||
{
|
||||
trained: false,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.modifiedCount).toEqual(nlpSampleFixtures.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe('The deleteCascadeOne function', () => {
|
||||
it('should delete a nlp Sample', async () => {
|
||||
const result = await nlpSampleRepository.deleteOne(noNlpSample.id);
|
||||
expect(result.deletedCount).toEqual(1);
|
||||
const sampleEntities = await nlpSampleEntityRepository.find({
|
||||
sample: noNlpSample.id,
|
||||
});
|
||||
expect(sampleEntities.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
107
api/src/nlp/repositories/nlp-sample.repository.ts
Normal file
107
api/src/nlp/repositories/nlp-sample.repository.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { Document, Model, Query, TFilterQuery } from 'mongoose';
|
||||
|
||||
import { BaseRepository, DeleteResult } from '@/utils/generics/base-repository';
|
||||
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
|
||||
|
||||
import { NlpSampleEntityRepository } from './nlp-sample-entity.repository';
|
||||
import { NlpSample, NlpSampleFull } from '../schemas/nlp-sample.schema';
|
||||
|
||||
@Injectable()
|
||||
export class NlpSampleRepository extends BaseRepository<NlpSample, 'entities'> {
|
||||
constructor(
|
||||
@InjectModel(NlpSample.name) readonly model: Model<NlpSample>,
|
||||
private readonly nlpSampleEntityRepository: NlpSampleEntityRepository,
|
||||
) {
|
||||
super(model, NlpSample);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes NLP sample entities associated with the provided criteria before deleting the sample itself.
|
||||
*
|
||||
* @param query - The query object used for deletion.
|
||||
* @param criteria - Criteria to identify the sample(s) to delete.
|
||||
*/
|
||||
async preDelete(
|
||||
_query: Query<
|
||||
DeleteResult,
|
||||
Document<NlpSample, any, any>,
|
||||
unknown,
|
||||
NlpSample,
|
||||
'deleteOne' | 'deleteMany'
|
||||
>,
|
||||
criteria: TFilterQuery<NlpSample>,
|
||||
) {
|
||||
if (criteria._id) {
|
||||
await this.nlpSampleEntityRepository.deleteMany({
|
||||
sample: criteria._id,
|
||||
});
|
||||
} else {
|
||||
throw new Error(
|
||||
'Attempted to delete a NLP sample using unknown criteria',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a paginated list of NLP samples and populates the related entities.
|
||||
*
|
||||
* @param filter Query filter used to retrieve NLP samples.
|
||||
* @param pageQuery Pagination details for the query.
|
||||
*
|
||||
* @returns A promise that resolves to a paginated list of `NlpSampleFull` objects.
|
||||
*/
|
||||
async findPageAndPopulate(
|
||||
filter: TFilterQuery<NlpSample>,
|
||||
pageQuery: PageQueryDto<NlpSample>,
|
||||
): Promise<NlpSampleFull[]> {
|
||||
const query = this.findPageQuery(filter, pageQuery).populate(['entities']);
|
||||
return await this.execute(query, NlpSampleFull);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all NLP samples that match the filter and populates related entities.
|
||||
*
|
||||
* @param filter Query filter used to retrieve NLP samples.
|
||||
*
|
||||
* @returns A promise that resolves to a list of `NlpSampleFull` objects.
|
||||
*/
|
||||
async findAndPopulate(
|
||||
filter: TFilterQuery<NlpSample>,
|
||||
): Promise<NlpSampleFull[]> {
|
||||
const query = this.findQuery(filter).populate(['entities']);
|
||||
return await this.execute(query, NlpSampleFull);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds an NLP sample by its ID and populates related entities.
|
||||
*
|
||||
* @param id The ID of the NLP sample to retrieve.
|
||||
*
|
||||
* @returns A promise that resolves to the `NlpSampleFull` object.
|
||||
*/
|
||||
async findOneAndPopulate(id: string): Promise<NlpSampleFull> {
|
||||
const query = this.findOneQuery(id).populate(['entities']);
|
||||
return await this.executeOne(query, NlpSampleFull);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all NLP samples and populates related entities.
|
||||
*
|
||||
* @returns A promise that resolves to a list of all `NlpSampleFull` objects.
|
||||
*/
|
||||
async findAllAndPopulate() {
|
||||
const query = this.findAllQuery().populate(['entities']);
|
||||
return await this.execute(query, NlpSampleFull);
|
||||
}
|
||||
}
|
||||
105
api/src/nlp/repositories/nlp-value.repository.spec.ts
Normal file
105
api/src/nlp/repositories/nlp-value.repository.spec.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { nlpEntityFixtures } from '@/utils/test/fixtures/nlpentity';
|
||||
import { installNlpSampleEntityFixtures } from '@/utils/test/fixtures/nlpsampleentity';
|
||||
import { nlpValueFixtures } from '@/utils/test/fixtures/nlpvalue';
|
||||
import { getPageQuery } from '@/utils/test/pagination';
|
||||
import {
|
||||
closeInMongodConnection,
|
||||
rootMongooseTestModule,
|
||||
} from '@/utils/test/test';
|
||||
|
||||
import { NlpSampleEntityRepository } from './nlp-sample-entity.repository';
|
||||
import { NlpValueRepository } from './nlp-value.repository';
|
||||
import { NlpEntityModel } from '../schemas/nlp-entity.schema';
|
||||
import { NlpSampleEntityModel } from '../schemas/nlp-sample-entity.schema';
|
||||
import { NlpValue, NlpValueModel } from '../schemas/nlp-value.schema';
|
||||
|
||||
describe('NlpValueRepository', () => {
|
||||
let nlpValueRepository: NlpValueRepository;
|
||||
let nlpSampleEntityRepository: NlpSampleEntityRepository;
|
||||
let nlpValues: NlpValue[];
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [
|
||||
rootMongooseTestModule(installNlpSampleEntityFixtures),
|
||||
MongooseModule.forFeature([
|
||||
NlpValueModel,
|
||||
NlpSampleEntityModel,
|
||||
NlpEntityModel,
|
||||
]),
|
||||
],
|
||||
providers: [NlpValueRepository, NlpSampleEntityRepository, EventEmitter2],
|
||||
}).compile();
|
||||
nlpValueRepository = module.get<NlpValueRepository>(NlpValueRepository);
|
||||
nlpSampleEntityRepository = module.get<NlpSampleEntityRepository>(
|
||||
NlpSampleEntityRepository,
|
||||
);
|
||||
nlpValues = await nlpValueRepository.findAll();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await closeInMongodConnection();
|
||||
});
|
||||
|
||||
afterEach(jest.clearAllMocks);
|
||||
|
||||
describe('findOneAndPopulate', () => {
|
||||
it('should return a nlp value with populate', async () => {
|
||||
const result = await nlpValueRepository.findOneAndPopulate(
|
||||
nlpValues[1].id,
|
||||
);
|
||||
expect(result).toEqualPayload({
|
||||
...nlpValueFixtures[1],
|
||||
entity: nlpEntityFixtures[0],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('findPageAndPopulate', () => {
|
||||
it('should return all nlp entities with populate', async () => {
|
||||
const pageQuery = getPageQuery<NlpValue>({
|
||||
sort: ['value', 'desc'],
|
||||
});
|
||||
const result = await nlpValueRepository.findPageAndPopulate(
|
||||
{},
|
||||
pageQuery,
|
||||
);
|
||||
const nlpValueFixturesWithEntities = nlpValueFixtures.reduce(
|
||||
(acc, curr) => {
|
||||
const ValueWithEntities = {
|
||||
...curr,
|
||||
entity: nlpEntityFixtures[parseInt(curr.entity)],
|
||||
};
|
||||
acc.push(ValueWithEntities);
|
||||
return acc;
|
||||
},
|
||||
[],
|
||||
);
|
||||
expect(result).toEqualPayload(nlpValueFixturesWithEntities);
|
||||
});
|
||||
});
|
||||
|
||||
describe('The deleteCascadeOne function', () => {
|
||||
it('should delete a nlp Value', async () => {
|
||||
const result = await nlpValueRepository.deleteOne(nlpValues[1].id);
|
||||
expect(result.deletedCount).toEqual(1);
|
||||
const sampleEntities = await nlpSampleEntityRepository.find({
|
||||
value: nlpValues[1].id,
|
||||
});
|
||||
expect(sampleEntities.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
132
api/src/nlp/repositories/nlp-value.repository.ts
Normal file
132
api/src/nlp/repositories/nlp-value.repository.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { Document, Model, Query, TFilterQuery } from 'mongoose';
|
||||
|
||||
import { BaseRepository, DeleteResult } from '@/utils/generics/base-repository';
|
||||
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
|
||||
|
||||
import { NlpSampleEntityRepository } from './nlp-sample-entity.repository';
|
||||
import {
|
||||
NlpValue,
|
||||
NlpValueDocument,
|
||||
NlpValueFull,
|
||||
} from '../schemas/nlp-value.schema';
|
||||
|
||||
@Injectable()
|
||||
export class NlpValueRepository extends BaseRepository<NlpValue, 'entity'> {
|
||||
constructor(
|
||||
@InjectModel(NlpValue.name) readonly model: Model<NlpValue>,
|
||||
private readonly nlpSampleEntityRepository: NlpSampleEntityRepository,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
) {
|
||||
super(model, NlpValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits an event after a new NLP value is created, bypassing built-in values.
|
||||
*
|
||||
* @param created - The newly created NLP value document.
|
||||
*/
|
||||
async postCreate(created: NlpValueDocument): Promise<void> {
|
||||
if (!created.builtin) {
|
||||
// Bypass builtin entities (probably fixtures)
|
||||
this.eventEmitter.emit('hook:nlp:value:create', created);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits an event after an NLP value is updated, bypassing built-in values.
|
||||
*
|
||||
* @param query - The query that was used to update the NLP value.
|
||||
* @param updated - The updated NLP value document.
|
||||
*/
|
||||
async postUpdate(
|
||||
_query: Query<
|
||||
Document<NlpValue, any, any>,
|
||||
Document<NlpValue, any, any>,
|
||||
unknown,
|
||||
NlpValue,
|
||||
'findOneAndUpdate'
|
||||
>,
|
||||
updated: NlpValue,
|
||||
): Promise<void> {
|
||||
if (!updated?.builtin) {
|
||||
// Bypass builtin entities (probably fixtures)
|
||||
this.eventEmitter.emit('hook:nlp:value:update', updated);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles deletion of NLP values and associated entities. If the criteria includes an ID,
|
||||
* emits an event for each deleted entity.
|
||||
*
|
||||
* @param _query - The query used to delete the NLP value(s).
|
||||
* @param criteria - The filter criteria used to identify the NLP value(s) to delete.
|
||||
*/
|
||||
async preDelete(
|
||||
_query: Query<
|
||||
DeleteResult,
|
||||
Document<NlpValue, any, any>,
|
||||
unknown,
|
||||
NlpValue,
|
||||
'deleteOne' | 'deleteMany'
|
||||
>,
|
||||
criteria: TFilterQuery<NlpValue>,
|
||||
): Promise<void> {
|
||||
if (criteria._id) {
|
||||
await this.nlpSampleEntityRepository.deleteMany({ value: criteria._id });
|
||||
|
||||
const entities = await this.find(
|
||||
typeof criteria === 'string' ? { _id: criteria } : criteria,
|
||||
);
|
||||
entities
|
||||
.filter((e) => !e.builtin)
|
||||
.map((e) => {
|
||||
this.eventEmitter.emit('hook:nlp:value:delete', e);
|
||||
});
|
||||
} else if (criteria.entity) {
|
||||
// Do nothing : cascading deletes coming from Nlp Sample Entity
|
||||
} else {
|
||||
throw new Error('Attempted to delete a NLP value using unknown criteria');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and paginates NLP values based on the provided filter and page query,
|
||||
* populating related entities.
|
||||
*
|
||||
* @param filter - The filter query used to search for NLP values.
|
||||
* @param pageQuery - The pagination query details.
|
||||
*
|
||||
* @returns A list of populated NLP values for the requested page.
|
||||
*/
|
||||
async findPageAndPopulate(
|
||||
filter: TFilterQuery<NlpValue>,
|
||||
pageQuery: PageQueryDto<NlpValue>,
|
||||
): Promise<NlpValueFull[]> {
|
||||
const query = this.findPageQuery(filter, pageQuery).populate(['entity']);
|
||||
return await this.execute(query, NlpValueFull);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and populates a single NLP value by its ID.
|
||||
*
|
||||
* @param id - The ID of the NLP value to find.
|
||||
*
|
||||
* @returns The populated NLP value document.
|
||||
*/
|
||||
async findOneAndPopulate(id: string): Promise<NlpValueFull> {
|
||||
const query = this.findOneQuery(id).populate(['entity']);
|
||||
return await this.executeOne(query, NlpValueFull);
|
||||
}
|
||||
}
|
||||
97
api/src/nlp/schemas/nlp-entity.schema.ts
Normal file
97
api/src/nlp/schemas/nlp-entity.schema.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { Prop, Schema, SchemaFactory, ModelDefinition } from '@nestjs/mongoose';
|
||||
import { Exclude, Type } from 'class-transformer';
|
||||
import { THydratedDocument } from 'mongoose';
|
||||
|
||||
import { BaseSchema } from '@/utils/generics/base-schema';
|
||||
import { LifecycleHookManager } from '@/utils/generics/lifecycle-hook-manager';
|
||||
|
||||
import { NlpValue } from './nlp-value.schema';
|
||||
import { NlpEntityMap } from './types';
|
||||
import { Lookup } from '../dto/nlp-entity.dto';
|
||||
|
||||
@Schema({ timestamps: true })
|
||||
export class NlpEntityStub extends BaseSchema {
|
||||
/**
|
||||
* This entity foreign id (nlp provider).
|
||||
*/
|
||||
@Prop({ type: String, required: false, unique: false })
|
||||
foreign_id?: string;
|
||||
|
||||
/**
|
||||
* The entity name.
|
||||
*/
|
||||
@Prop({
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
match: /^[a-zA-Z0-9_]+$/,
|
||||
})
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Lookup strategy can contain : keywords, trait, free-text
|
||||
*/
|
||||
@Prop({ type: [String], default: ['keywords'] })
|
||||
lookups?: Lookup[];
|
||||
|
||||
/**
|
||||
* Description of the entity purpose.
|
||||
*/
|
||||
@Prop({ type: String })
|
||||
doc?: string;
|
||||
|
||||
/**
|
||||
* Either or not this entity a built-in (either fixtures or shipped along with the 3rd party ai).
|
||||
*/
|
||||
@Prop({ type: Boolean, default: false })
|
||||
builtin?: boolean;
|
||||
|
||||
/**
|
||||
* Returns a map object for entities
|
||||
* @param entities - Array of entities
|
||||
* @returns {NlpEntityMap} - Object that contains entities identified by key=entity.id
|
||||
|
||||
*/
|
||||
static getEntityMap<T extends NlpEntityStub>(entities: T[]) {
|
||||
return entities.reduce((acc, curr: T) => {
|
||||
acc[curr.id] = curr;
|
||||
return acc;
|
||||
}, {} as NlpEntityMap<T>);
|
||||
}
|
||||
}
|
||||
|
||||
@Schema({ timestamps: true })
|
||||
export class NlpEntity extends NlpEntityStub {
|
||||
@Exclude()
|
||||
values?: never;
|
||||
}
|
||||
|
||||
@Schema({ timestamps: true })
|
||||
export class NlpEntityFull extends NlpEntityStub {
|
||||
@Type(() => NlpValue)
|
||||
values: NlpValue[];
|
||||
}
|
||||
|
||||
export type NlpEntityDocument = THydratedDocument<NlpEntity>;
|
||||
|
||||
export const NlpEntityModel: ModelDefinition = LifecycleHookManager.attach({
|
||||
name: NlpEntity.name,
|
||||
schema: SchemaFactory.createForClass(NlpEntityStub),
|
||||
});
|
||||
|
||||
NlpEntityModel.schema.virtual('values', {
|
||||
ref: 'NlpValue',
|
||||
localField: '_id',
|
||||
foreignField: 'entity',
|
||||
});
|
||||
|
||||
export default NlpEntityModel.schema;
|
||||
108
api/src/nlp/schemas/nlp-sample-entity.schema.ts
Normal file
108
api/src/nlp/schemas/nlp-sample-entity.schema.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { Prop, Schema, SchemaFactory, ModelDefinition } from '@nestjs/mongoose';
|
||||
import { Transform, Type } from 'class-transformer';
|
||||
import { THydratedDocument, Schema as MongooseSchema } from 'mongoose';
|
||||
|
||||
import { BaseSchema } from '@/utils/generics/base-schema';
|
||||
|
||||
import { NlpEntity } from './nlp-entity.schema';
|
||||
import { NlpSample } from './nlp-sample.schema';
|
||||
import { NlpValue } from './nlp-value.schema';
|
||||
|
||||
@Schema({ timestamps: true })
|
||||
export class NlpSampleEntityStub extends BaseSchema {
|
||||
/**
|
||||
* The index indicating the start of the text to be used for the training.
|
||||
*/
|
||||
@Prop({ type: Number })
|
||||
start?: number;
|
||||
|
||||
/**
|
||||
* The index marking the end of the text to be used for the training.
|
||||
*/
|
||||
@Prop({ type: Number })
|
||||
end?: number;
|
||||
|
||||
/**
|
||||
* The nlp entity involved for this training.
|
||||
*/
|
||||
@Prop({
|
||||
type: MongooseSchema.Types.ObjectId,
|
||||
ref: 'NlpEntity',
|
||||
required: true,
|
||||
})
|
||||
entity: unknown;
|
||||
|
||||
/**
|
||||
* The value of the above nlp entity.
|
||||
*/
|
||||
@Prop({
|
||||
type: MongooseSchema.Types.ObjectId,
|
||||
ref: 'NlpValue',
|
||||
required: true,
|
||||
})
|
||||
value: unknown;
|
||||
|
||||
/**
|
||||
* The sample of the training (text).
|
||||
*/
|
||||
@Prop({
|
||||
type: MongooseSchema.Types.ObjectId,
|
||||
ref: 'NlpSample',
|
||||
required: true,
|
||||
})
|
||||
sample: unknown;
|
||||
}
|
||||
|
||||
@Schema({ timestamps: true })
|
||||
export class NlpSampleEntity extends NlpSampleEntityStub {
|
||||
@Transform(({ obj }) => (obj.start > -1 ? obj.start : undefined))
|
||||
start?: number;
|
||||
|
||||
@Transform(({ obj }) => (obj.end > -1 ? obj.end : undefined))
|
||||
end?: number;
|
||||
|
||||
@Transform(({ obj }) => obj.entity.toString())
|
||||
entity: string;
|
||||
|
||||
@Transform(({ obj }) => obj.value.toString())
|
||||
value: string;
|
||||
|
||||
@Transform(({ obj }) => obj.sample.toString())
|
||||
sample: string;
|
||||
}
|
||||
|
||||
@Schema({ timestamps: true })
|
||||
export class NlpSampleEntityFull extends NlpSampleEntityStub {
|
||||
@Transform(({ obj }) => (obj.start > -1 ? obj.start : undefined))
|
||||
start?: number;
|
||||
|
||||
@Transform(({ obj }) => (obj.end > -1 ? obj.end : undefined))
|
||||
end?: number;
|
||||
|
||||
@Type(() => NlpEntity)
|
||||
entity: NlpEntity;
|
||||
|
||||
@Type(() => NlpValue)
|
||||
value: NlpValue;
|
||||
|
||||
@Type(() => NlpSample)
|
||||
sample: NlpSample;
|
||||
}
|
||||
|
||||
export type NlpSampleEntityDocument = THydratedDocument<NlpSampleEntity>;
|
||||
|
||||
export const NlpSampleEntityModel: ModelDefinition = {
|
||||
name: NlpSampleEntity.name,
|
||||
schema: SchemaFactory.createForClass(NlpSampleEntityStub),
|
||||
};
|
||||
|
||||
export default NlpSampleEntityModel.schema;
|
||||
70
api/src/nlp/schemas/nlp-sample.schema.ts
Normal file
70
api/src/nlp/schemas/nlp-sample.schema.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { Prop, Schema, SchemaFactory, ModelDefinition } from '@nestjs/mongoose';
|
||||
import { Exclude, Type } from 'class-transformer';
|
||||
import { THydratedDocument } from 'mongoose';
|
||||
|
||||
import { BaseSchema } from '@/utils/generics/base-schema';
|
||||
import { LifecycleHookManager } from '@/utils/generics/lifecycle-hook-manager';
|
||||
|
||||
import { NlpSampleEntity } from './nlp-sample-entity.schema';
|
||||
import { NlpSampleState } from './types';
|
||||
|
||||
@Schema({ timestamps: true })
|
||||
export class NlpSampleStub extends BaseSchema {
|
||||
/**
|
||||
* The content of the sample.
|
||||
*/
|
||||
@Prop({ type: String, required: true, unique: true })
|
||||
text: string;
|
||||
|
||||
/**
|
||||
* Either or not this sample was used for traning already.
|
||||
*/
|
||||
@Prop({ type: Boolean, default: false })
|
||||
trained?: boolean;
|
||||
|
||||
/**
|
||||
* From where this sample was provided.
|
||||
*/
|
||||
@Prop({
|
||||
type: String,
|
||||
enum: Object.values(NlpSampleState),
|
||||
default: NlpSampleState.train,
|
||||
})
|
||||
type?: keyof typeof NlpSampleState;
|
||||
}
|
||||
|
||||
@Schema({ timestamps: true })
|
||||
export class NlpSample extends NlpSampleStub {
|
||||
@Exclude()
|
||||
entities?: never;
|
||||
}
|
||||
|
||||
@Schema({ timestamps: true })
|
||||
export class NlpSampleFull extends NlpSampleStub {
|
||||
@Type(() => NlpSampleEntity)
|
||||
entities: NlpSampleEntity[];
|
||||
}
|
||||
|
||||
export type NlpSampleDocument = THydratedDocument<NlpSample>;
|
||||
|
||||
export const NlpSampleModel: ModelDefinition = LifecycleHookManager.attach({
|
||||
name: NlpSample.name,
|
||||
schema: SchemaFactory.createForClass(NlpSampleStub),
|
||||
});
|
||||
|
||||
NlpSampleModel.schema.virtual('entities', {
|
||||
ref: 'NlpSampleEntity',
|
||||
localField: '_id',
|
||||
foreignField: 'sample',
|
||||
});
|
||||
|
||||
export default NlpSampleModel.schema;
|
||||
107
api/src/nlp/schemas/nlp-value.schema.ts
Normal file
107
api/src/nlp/schemas/nlp-value.schema.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { Prop, Schema, SchemaFactory, ModelDefinition } from '@nestjs/mongoose';
|
||||
import { Transform, Type } from 'class-transformer';
|
||||
import { THydratedDocument, Schema as MongooseSchema } from 'mongoose';
|
||||
|
||||
import { BaseSchema } from '@/utils/generics/base-schema';
|
||||
import { LifecycleHookManager } from '@/utils/generics/lifecycle-hook-manager';
|
||||
|
||||
import { NlpEntity, NlpEntityFull } from './nlp-entity.schema';
|
||||
import { NlpValueMap } from './types';
|
||||
|
||||
@Schema({ timestamps: true })
|
||||
export class NlpValueStub extends BaseSchema {
|
||||
/**
|
||||
* This value content.
|
||||
*/
|
||||
@Prop({ type: String, required: false, unique: false })
|
||||
foreign_id?: string;
|
||||
|
||||
/**
|
||||
* This value content.
|
||||
*/
|
||||
@Prop({ type: String, required: true, unique: true })
|
||||
value: string;
|
||||
|
||||
/**
|
||||
* An array of synonyms or equivalent words that fits this value.
|
||||
*/
|
||||
@Prop({ type: [String], default: [] })
|
||||
expressions?: string[];
|
||||
|
||||
/**
|
||||
* Metadata are additional data that can be associated to this values, most of the time, the metadata contains system values or ids (e.g: value: "coffee", metadata: "item_11") .
|
||||
*/
|
||||
@Prop({ type: JSON, default: {} })
|
||||
metadata?: Record<string, any>;
|
||||
|
||||
/**
|
||||
* Either or not this value a built-in (either fixtures or shipped along with the 3rd party ai).
|
||||
*/
|
||||
@Prop({ type: Boolean, default: false })
|
||||
builtin?: boolean;
|
||||
|
||||
/**
|
||||
* The entity to which this value belongs to.
|
||||
*/
|
||||
@Prop({
|
||||
type: MongooseSchema.Types.ObjectId,
|
||||
ref: 'NlpEntity',
|
||||
required: true,
|
||||
})
|
||||
entity: unknown;
|
||||
|
||||
/**
|
||||
* Returns array of values of all the provided entities
|
||||
* @param entities - Array of entities
|
||||
* @returns {NlpValue[]} - Array that contains all entities values
|
||||
|
||||
*/
|
||||
static getValuesFromEntities(entities: NlpEntityFull[]): NlpValue[] {
|
||||
return entities.reduce((acc, curr: NlpEntityFull) => {
|
||||
return acc.concat(curr.values);
|
||||
}, [] as NlpValue[]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a map object for values
|
||||
* @param values - Array of values
|
||||
* @returns {NlpValueMap} - Object that contains values identified by key=value.id
|
||||
|
||||
*/
|
||||
static getValueMap<T extends NlpValueStub>(values: T[]): NlpValueMap<T> {
|
||||
return values.reduce((acc, curr) => {
|
||||
acc[curr.id] = curr;
|
||||
return acc;
|
||||
}, {} as NlpValueMap<T>);
|
||||
}
|
||||
}
|
||||
|
||||
@Schema({ timestamps: true })
|
||||
export class NlpValue extends NlpValueStub {
|
||||
@Transform(({ obj }) => obj.entity.toString())
|
||||
entity: string;
|
||||
}
|
||||
|
||||
@Schema({ timestamps: true })
|
||||
export class NlpValueFull extends NlpValueStub {
|
||||
@Type(() => NlpEntity)
|
||||
entity: NlpEntity;
|
||||
}
|
||||
|
||||
export type NlpValueDocument = THydratedDocument<NlpValue>;
|
||||
|
||||
export const NlpValueModel: ModelDefinition = LifecycleHookManager.attach({
|
||||
name: NlpValue.name,
|
||||
schema: SchemaFactory.createForClass(NlpValueStub),
|
||||
});
|
||||
|
||||
export default NlpValueModel.schema;
|
||||
28
api/src/nlp/schemas/types.ts
Normal file
28
api/src/nlp/schemas/types.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { NlpEntityStub } from './nlp-entity.schema';
|
||||
import { NlpValueStub } from './nlp-value.schema';
|
||||
|
||||
export interface NlpSampleEntityValue {
|
||||
entity: string; // entity name
|
||||
value: string; // entity value
|
||||
start?: number;
|
||||
end?: number;
|
||||
}
|
||||
|
||||
export type NlpEntityMap<T extends NlpEntityStub> = { [entityId: string]: T };
|
||||
|
||||
export type NlpValueMap<T extends NlpValueStub> = { [valueId: string]: T };
|
||||
|
||||
export enum NlpSampleState {
|
||||
train = 'train',
|
||||
test = 'test',
|
||||
inbox = 'inbox',
|
||||
}
|
||||
25
api/src/nlp/seeds/nlp-entity.seed-model.ts
Normal file
25
api/src/nlp/seeds/nlp-entity.seed-model.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { NlpEntityCreateDto } from '../dto/nlp-entity.dto';
|
||||
|
||||
export const nlpEntityModels: NlpEntityCreateDto[] = [
|
||||
{
|
||||
name: 'language',
|
||||
lookups: ['trait'],
|
||||
doc: `"language" refers to the language of the text sent by the end user`,
|
||||
builtin: true,
|
||||
},
|
||||
{
|
||||
name: 'intent',
|
||||
lookups: ['trait'],
|
||||
doc: `"intent" refers to the underlying purpose or goal that a piece of text aims to convey. Identifying the intent involves determining what action or response the text is prompting. For instance, in customer service chatbots, recognizing the intent behind a user's message, such as "book a flight" or "check account balance," is crucial to provide accurate and relevant responses`,
|
||||
builtin: true,
|
||||
},
|
||||
];
|
||||
22
api/src/nlp/seeds/nlp-entity.seed.ts
Normal file
22
api/src/nlp/seeds/nlp-entity.seed.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { BaseSeeder } from '@/utils/generics/base-seeder';
|
||||
|
||||
import { NlpEntityRepository } from '../repositories/nlp-entity.repository';
|
||||
import { NlpEntity } from '../schemas/nlp-entity.schema';
|
||||
|
||||
@Injectable()
|
||||
export class NlpEntitySeeder extends BaseSeeder<NlpEntity> {
|
||||
constructor(nlpEntityRepository: NlpEntityRepository) {
|
||||
super(nlpEntityRepository);
|
||||
}
|
||||
}
|
||||
22
api/src/nlp/seeds/nlp-value.seed-model.ts
Normal file
22
api/src/nlp/seeds/nlp-value.seed-model.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { config } from '@/config';
|
||||
|
||||
import { NlpValueCreateDto } from '../dto/nlp-value.dto';
|
||||
|
||||
export const nlpValueModels: NlpValueCreateDto[] = [
|
||||
...config.chatbot.lang.available.map((lang: string) => {
|
||||
return {
|
||||
entity: 'language',
|
||||
value: lang,
|
||||
builtin: true,
|
||||
};
|
||||
}),
|
||||
];
|
||||
40
api/src/nlp/seeds/nlp-value.seed.ts
Normal file
40
api/src/nlp/seeds/nlp-value.seed.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { BaseSchema } from '@/utils/generics/base-schema';
|
||||
import { BaseSeeder } from '@/utils/generics/base-seeder';
|
||||
|
||||
import { NlpEntityRepository } from '../repositories/nlp-entity.repository';
|
||||
import { NlpValueRepository } from '../repositories/nlp-value.repository';
|
||||
import { NlpValue } from '../schemas/nlp-value.schema';
|
||||
|
||||
@Injectable()
|
||||
export class NlpValueSeeder extends BaseSeeder<NlpValue> {
|
||||
constructor(
|
||||
nlpValueRepository: NlpValueRepository,
|
||||
private readonly nlpEntityRepository: NlpEntityRepository,
|
||||
) {
|
||||
super(nlpValueRepository);
|
||||
}
|
||||
|
||||
async seed(models: Omit<NlpValue, keyof BaseSchema>[]): Promise<boolean> {
|
||||
if (await this.isEmpty()) {
|
||||
const entities = await this.nlpEntityRepository.findAll();
|
||||
const modelDtos = models.map((v) => ({
|
||||
...v,
|
||||
entity: entities.find(({ name }) => name === v.entity)?.id,
|
||||
}));
|
||||
await this.repository.createMany(modelDtos);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
154
api/src/nlp/services/nlp-entity.service.spec.ts
Normal file
154
api/src/nlp/services/nlp-entity.service.spec.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { nlpEntityFixtures } from '@/utils/test/fixtures/nlpentity';
|
||||
import { installNlpValueFixtures } from '@/utils/test/fixtures/nlpvalue';
|
||||
import { getPageQuery } from '@/utils/test/pagination';
|
||||
import {
|
||||
closeInMongodConnection,
|
||||
rootMongooseTestModule,
|
||||
} from '@/utils/test/test';
|
||||
|
||||
import { NlpEntityService } from './nlp-entity.service';
|
||||
import { NlpValueService } from './nlp-value.service';
|
||||
import { NlpEntityRepository } from '../repositories/nlp-entity.repository';
|
||||
import { NlpSampleEntityRepository } from '../repositories/nlp-sample-entity.repository';
|
||||
import { NlpValueRepository } from '../repositories/nlp-value.repository';
|
||||
import { NlpEntityModel, NlpEntity } from '../schemas/nlp-entity.schema';
|
||||
import { NlpSampleEntityModel } from '../schemas/nlp-sample-entity.schema';
|
||||
import { NlpValueModel } from '../schemas/nlp-value.schema';
|
||||
|
||||
describe('nlpEntityService', () => {
|
||||
let nlpEntityService: NlpEntityService;
|
||||
let nlpEntityRepository: NlpEntityRepository;
|
||||
let nlpValueRepository: NlpValueRepository;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [
|
||||
rootMongooseTestModule(installNlpValueFixtures),
|
||||
MongooseModule.forFeature([
|
||||
NlpEntityModel,
|
||||
NlpValueModel,
|
||||
NlpSampleEntityModel,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
NlpEntityService,
|
||||
NlpEntityRepository,
|
||||
NlpValueService,
|
||||
NlpValueRepository,
|
||||
NlpSampleEntityRepository,
|
||||
EventEmitter2,
|
||||
],
|
||||
}).compile();
|
||||
nlpEntityService = module.get<NlpEntityService>(NlpEntityService);
|
||||
nlpEntityRepository = module.get<NlpEntityRepository>(NlpEntityRepository);
|
||||
nlpValueRepository = module.get<NlpValueRepository>(NlpValueRepository);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await closeInMongodConnection();
|
||||
});
|
||||
|
||||
afterEach(jest.clearAllMocks);
|
||||
|
||||
describe('The deleteCascadeOne function', () => {
|
||||
it('should delete a nlp entity', async () => {
|
||||
const intentNlpEntity = await nlpEntityRepository.findOne({
|
||||
name: 'intent',
|
||||
});
|
||||
const result = await nlpEntityService.deleteCascadeOne(
|
||||
intentNlpEntity.id,
|
||||
);
|
||||
expect(result.deletedCount).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOneAndPopulate', () => {
|
||||
it('should return a nlp entity with populate', async () => {
|
||||
const firstNameNlpEntity = await nlpEntityRepository.findOne({
|
||||
name: 'first_name',
|
||||
});
|
||||
const result = await nlpEntityService.findOneAndPopulate(
|
||||
firstNameNlpEntity.id,
|
||||
);
|
||||
const firstNameValues = await nlpValueRepository.findOne({
|
||||
entity: firstNameNlpEntity.id,
|
||||
});
|
||||
const entityWithValues = {
|
||||
id: firstNameNlpEntity.id,
|
||||
...nlpEntityFixtures[1],
|
||||
values: [firstNameValues],
|
||||
};
|
||||
expect(result).toEqualPayload(entityWithValues);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findPageAndPopulate', () => {
|
||||
it('should return all nlp entities with populate', async () => {
|
||||
const pageQuery = getPageQuery<NlpEntity>({ sort: ['name', 'desc'] });
|
||||
const firstNameNlpEntity = await nlpEntityRepository.findOne({
|
||||
name: 'first_name',
|
||||
});
|
||||
const result = await nlpEntityService.findPageAndPopulate(
|
||||
{ _id: firstNameNlpEntity.id },
|
||||
pageQuery,
|
||||
);
|
||||
const firstNameValues = await nlpValueRepository.findOne({
|
||||
entity: firstNameNlpEntity.id,
|
||||
});
|
||||
const entitiesWithValues = [
|
||||
{
|
||||
id: firstNameNlpEntity.id,
|
||||
...nlpEntityFixtures[1],
|
||||
values: [firstNameValues],
|
||||
},
|
||||
];
|
||||
expect(result).toEqualPayload(entitiesWithValues);
|
||||
});
|
||||
});
|
||||
|
||||
describe('storeNewEntities', () => {
|
||||
it('should store new entities', async () => {
|
||||
const result = await nlpEntityService.storeNewEntities(
|
||||
'Mein Name ist Hexabot',
|
||||
[
|
||||
{ entity: 'intent', value: 'Name' },
|
||||
{ entity: 'language', value: 'de' },
|
||||
],
|
||||
['trait'],
|
||||
);
|
||||
const intentEntity = await nlpEntityRepository.findOne({
|
||||
name: 'intent',
|
||||
});
|
||||
const languageEntity = await nlpEntityRepository.findOne({
|
||||
name: 'language',
|
||||
});
|
||||
const nameValue = await nlpValueRepository.findOne({ value: 'Name' });
|
||||
const deValue = await nlpValueRepository.findOne({ value: 'de' });
|
||||
const storedEntites = [
|
||||
{
|
||||
entity: intentEntity.id,
|
||||
value: nameValue.id,
|
||||
},
|
||||
{
|
||||
entity: languageEntity.id,
|
||||
value: deValue.id,
|
||||
},
|
||||
];
|
||||
|
||||
expect(result).toEqualPayload(storedEntites);
|
||||
});
|
||||
});
|
||||
});
|
||||
128
api/src/nlp/services/nlp-entity.service.ts
Normal file
128
api/src/nlp/services/nlp-entity.service.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { TFilterQuery } from 'mongoose';
|
||||
|
||||
import { BaseService } from '@/utils/generics/base-service';
|
||||
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
|
||||
|
||||
import { NlpValueService } from './nlp-value.service';
|
||||
import { Lookup } from '../dto/nlp-entity.dto';
|
||||
import { NlpEntityRepository } from '../repositories/nlp-entity.repository';
|
||||
import { NlpEntity } from '../schemas/nlp-entity.schema';
|
||||
import { NlpSampleEntityValue } from '../schemas/types';
|
||||
|
||||
@Injectable()
|
||||
export class NlpEntityService extends BaseService<NlpEntity> {
|
||||
constructor(
|
||||
readonly repository: NlpEntityRepository,
|
||||
private readonly nlpValueService: NlpValueService,
|
||||
) {
|
||||
super(repository);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an entity by its ID.
|
||||
*
|
||||
* @param id - The ID of the entity to delete.
|
||||
*
|
||||
* @returns A promise that resolves when the entity is deleted.
|
||||
*/
|
||||
async deleteCascadeOne(id: string) {
|
||||
return await this.repository.deleteOne(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds an entity by its ID and populates related data.
|
||||
*
|
||||
* @param id - The ID of the entity to find.
|
||||
*
|
||||
* @returns A promise that resolves with the populated entity.
|
||||
*/
|
||||
async findOneAndPopulate(id: string) {
|
||||
return await this.repository.findOneAndPopulate(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all entities and populates related data.
|
||||
*
|
||||
* @returns A promise that resolves with the populated list of entities.
|
||||
*/
|
||||
async findAllAndPopulate() {
|
||||
return await this.repository.findAllAndPopulate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds entities based on the specified filters and pagination, and populates related data.
|
||||
*
|
||||
* @param filters - The filters to apply to the query.
|
||||
* @param pageQuery - The pagination and sorting options for the query.
|
||||
*
|
||||
* @returns A promise that resolves with the populated page of entities.
|
||||
*/
|
||||
async findPageAndPopulate(
|
||||
filters: TFilterQuery<NlpEntity>,
|
||||
pageQuery: PageQueryDto<NlpEntity>,
|
||||
) {
|
||||
return await this.repository.findPageAndPopulate(filters, pageQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores new entities based on the sample text and sample entities.
|
||||
* Deletes all values relative to this entity before deleting the entity itself.
|
||||
* This is typically used during an import process.
|
||||
*
|
||||
* @param sampleText - The text sample containing NLP entities.
|
||||
* @param sampleEntities - The list of sample entity values to be processed.
|
||||
* @param lookups - Optional list of lookups used to classify the entities.
|
||||
*
|
||||
* @returns A promise that resolves when the entities and their values are stored.
|
||||
*/
|
||||
async storeNewEntities(
|
||||
sampleText: string,
|
||||
sampleEntities: NlpSampleEntityValue[],
|
||||
lookups: Lookup[] = ['keywords'],
|
||||
) {
|
||||
// Extract entity names from sampleEntities
|
||||
const entities = sampleEntities.map((e) => e.entity);
|
||||
|
||||
// Retrieve stored entities
|
||||
let storedEntities = (await this.find({ name: { $in: entities } })) || [];
|
||||
// Find newly added entities
|
||||
const entitiesToAdd = entities
|
||||
.filter((e) => storedEntities.findIndex((se) => se.name === e) === -1)
|
||||
.filter((e, idx, self) => self.indexOf(e) === idx)
|
||||
.map((e) => ({ name: e, lookups }));
|
||||
// Create new entities
|
||||
const newEntities = await this.createMany(entitiesToAdd);
|
||||
// Add new entities to the storedEntities array
|
||||
storedEntities = storedEntities.concat(newEntities);
|
||||
return await this.nlpValueService.storeNewValues(
|
||||
sampleText,
|
||||
sampleEntities,
|
||||
storedEntities,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores only new entities based on the provided sample entities and returns them.
|
||||
* It creates new entities if they do not already exist.
|
||||
*
|
||||
* @param sampleEntities - The list of sample entity values to process.
|
||||
*
|
||||
* @returns A promise that resolves with the list of stored entities, including their IDs.
|
||||
*/
|
||||
storeEntities(sampleEntities: NlpSampleEntityValue[]): Promise<NlpEntity[]> {
|
||||
const findOrCreate = sampleEntities.map((e: NlpSampleEntityValue) =>
|
||||
this.findOneOrCreate({ name: e.entity }, { name: e.entity }),
|
||||
);
|
||||
return Promise.all(findOrCreate);
|
||||
}
|
||||
}
|
||||
196
api/src/nlp/services/nlp-sample-entity.service.spec.ts
Normal file
196
api/src/nlp/services/nlp-sample-entity.service.spec.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { nlpSampleFixtures } from '@/utils/test/fixtures/nlpsample';
|
||||
import {
|
||||
installNlpSampleEntityFixtures,
|
||||
nlpSampleEntityFixtures,
|
||||
} from '@/utils/test/fixtures/nlpsampleentity';
|
||||
import { nlpValueFixtures } from '@/utils/test/fixtures/nlpvalue';
|
||||
import { getPageQuery } from '@/utils/test/pagination';
|
||||
import {
|
||||
closeInMongodConnection,
|
||||
rootMongooseTestModule,
|
||||
} from '@/utils/test/test';
|
||||
|
||||
import { NlpEntityService } from './nlp-entity.service';
|
||||
import { NlpSampleEntityService } from './nlp-sample-entity.service';
|
||||
import { NlpValueService } from './nlp-value.service';
|
||||
import { NlpEntityRepository } from '../repositories/nlp-entity.repository';
|
||||
import { NlpSampleEntityRepository } from '../repositories/nlp-sample-entity.repository';
|
||||
import { NlpValueRepository } from '../repositories/nlp-value.repository';
|
||||
import { NlpEntityModel, NlpEntity } from '../schemas/nlp-entity.schema';
|
||||
import {
|
||||
NlpSampleEntityModel,
|
||||
NlpSampleEntity,
|
||||
} from '../schemas/nlp-sample-entity.schema';
|
||||
import { NlpSample, NlpSampleModel } from '../schemas/nlp-sample.schema';
|
||||
import { NlpValue, NlpValueModel } from '../schemas/nlp-value.schema';
|
||||
|
||||
describe('NlpSampleEntityService', () => {
|
||||
let nlpSampleEntityService: NlpSampleEntityService;
|
||||
let nlpSampleEntityRepository: NlpSampleEntityRepository;
|
||||
let nlpSampleEntities: NlpSampleEntity[];
|
||||
let nlpEntityRepository: NlpEntityRepository;
|
||||
let nlpEntities: NlpEntity[];
|
||||
let nlpEntityService: NlpEntityService;
|
||||
let nlpValueService: NlpValueService;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [
|
||||
rootMongooseTestModule(installNlpSampleEntityFixtures),
|
||||
MongooseModule.forFeature([
|
||||
NlpSampleEntityModel,
|
||||
NlpEntityModel,
|
||||
NlpSampleModel,
|
||||
NlpValueModel,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
NlpSampleEntityRepository,
|
||||
NlpEntityRepository,
|
||||
NlpValueRepository,
|
||||
NlpSampleEntityService,
|
||||
NlpEntityService,
|
||||
NlpValueService,
|
||||
EventEmitter2,
|
||||
],
|
||||
}).compile();
|
||||
nlpSampleEntityService = module.get<NlpSampleEntityService>(
|
||||
NlpSampleEntityService,
|
||||
);
|
||||
nlpSampleEntityRepository = module.get<NlpSampleEntityRepository>(
|
||||
NlpSampleEntityRepository,
|
||||
);
|
||||
nlpEntityRepository = module.get<NlpEntityRepository>(NlpEntityRepository);
|
||||
nlpSampleEntityService = module.get<NlpSampleEntityService>(
|
||||
NlpSampleEntityService,
|
||||
);
|
||||
nlpEntityService = module.get<NlpEntityService>(NlpEntityService);
|
||||
nlpValueService = module.get<NlpValueService>(NlpValueService);
|
||||
nlpSampleEntities = await nlpSampleEntityRepository.findAll();
|
||||
nlpEntities = await nlpEntityRepository.findAll();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await closeInMongodConnection();
|
||||
});
|
||||
|
||||
afterEach(jest.clearAllMocks);
|
||||
|
||||
describe('findOneAndPopulate', () => {
|
||||
it('should return a nlp SampleEntity with populate', async () => {
|
||||
const result = await nlpSampleEntityService.findOneAndPopulate(
|
||||
nlpSampleEntities[0].id,
|
||||
);
|
||||
const sampleEntityWithPopulate = {
|
||||
...nlpSampleEntityFixtures[0],
|
||||
entity: nlpEntities[0],
|
||||
value: { ...nlpValueFixtures[0], entity: nlpEntities[0].id },
|
||||
sample: nlpSampleFixtures[0],
|
||||
};
|
||||
expect(result).toEqualPayload(sampleEntityWithPopulate);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findPageAndPopulate', () => {
|
||||
it('should return all nlp sample entities with populate', async () => {
|
||||
const pageQuery = getPageQuery<NlpSampleEntity>({
|
||||
sort: ['value', 'asc'],
|
||||
});
|
||||
const result = await nlpSampleEntityService.findPageAndPopulate(
|
||||
{},
|
||||
pageQuery,
|
||||
);
|
||||
const nlpValueFixturesWithEntities = nlpValueFixtures.reduce(
|
||||
(acc, curr) => {
|
||||
const ValueWithEntities = {
|
||||
...curr,
|
||||
entity: nlpEntities[0].id,
|
||||
};
|
||||
acc.push(ValueWithEntities);
|
||||
return acc;
|
||||
},
|
||||
[],
|
||||
);
|
||||
nlpValueFixturesWithEntities[2] = {
|
||||
...nlpValueFixturesWithEntities[2],
|
||||
entity: nlpEntities[1].id,
|
||||
};
|
||||
|
||||
const nlpSampleEntityFixturesWithPopulate =
|
||||
nlpSampleEntityFixtures.reduce((acc, curr) => {
|
||||
const sampleEntityWithPopulate = {
|
||||
...curr,
|
||||
entity: nlpEntities[curr.entity],
|
||||
value: nlpValueFixturesWithEntities[curr.value],
|
||||
sample: nlpSampleFixtures[curr.sample],
|
||||
};
|
||||
acc.push(sampleEntityWithPopulate);
|
||||
return acc;
|
||||
}, []);
|
||||
expect(result).toEqualPayload(nlpSampleEntityFixturesWithPopulate);
|
||||
});
|
||||
});
|
||||
|
||||
describe('storeSampleEntities', () => {
|
||||
it('should store sample entities correctly', async () => {
|
||||
const sample = { id: '1', text: 'Hello world' } as NlpSample;
|
||||
const entities = [
|
||||
{ entity: 'greeting', value: 'Hello', start: 0, end: 5 },
|
||||
];
|
||||
|
||||
jest
|
||||
.spyOn(nlpEntityService, 'storeEntities')
|
||||
.mockResolvedValue([{ id: '10', name: 'greeting' } as NlpEntity]);
|
||||
|
||||
jest
|
||||
.spyOn(nlpValueService, 'storeValues')
|
||||
.mockResolvedValue([{ id: '20', value: 'Hello' } as NlpValue]);
|
||||
|
||||
jest
|
||||
.spyOn(nlpSampleEntityService, 'createMany')
|
||||
.mockResolvedValue('Expected Result' as any);
|
||||
|
||||
const result = await nlpSampleEntityService.storeSampleEntities(
|
||||
sample,
|
||||
entities,
|
||||
);
|
||||
|
||||
expect(nlpEntityService.storeEntities).toHaveBeenCalledWith(entities);
|
||||
expect(nlpValueService.storeValues).toHaveBeenCalledWith(
|
||||
sample.text,
|
||||
entities,
|
||||
);
|
||||
expect(nlpSampleEntityService.createMany).toHaveBeenCalledWith([
|
||||
{ sample: sample.id, entity: '10', value: '20', start: 0, end: 5 },
|
||||
]);
|
||||
expect(result).toEqual('Expected Result');
|
||||
});
|
||||
|
||||
it('should throw an error if stored entity or value cannot be found', async () => {
|
||||
const sample = { id: 1, text: 'Hello world' } as any as NlpSample;
|
||||
const entities = [
|
||||
{ entity: 'greeting', value: 'Hello', start: 0, end: 5 },
|
||||
];
|
||||
|
||||
jest.spyOn(nlpEntityService, 'storeEntities').mockResolvedValue([]);
|
||||
jest.spyOn(nlpValueService, 'storeValues').mockResolvedValue([]);
|
||||
|
||||
await expect(
|
||||
nlpSampleEntityService.storeSampleEntities(sample, entities),
|
||||
).rejects.toThrow('Unable to find the stored entity or value');
|
||||
});
|
||||
});
|
||||
});
|
||||
101
api/src/nlp/services/nlp-sample-entity.service.ts
Normal file
101
api/src/nlp/services/nlp-sample-entity.service.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { TFilterQuery } from 'mongoose';
|
||||
|
||||
import { BaseService } from '@/utils/generics/base-service';
|
||||
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
|
||||
|
||||
import { NlpEntityService } from './nlp-entity.service';
|
||||
import { NlpValueService } from './nlp-value.service';
|
||||
import { NlpSampleEntityRepository } from '../repositories/nlp-sample-entity.repository';
|
||||
import { NlpSampleEntity } from '../schemas/nlp-sample-entity.schema';
|
||||
import { NlpSample } from '../schemas/nlp-sample.schema';
|
||||
import { NlpSampleEntityValue } from '../schemas/types';
|
||||
|
||||
@Injectable()
|
||||
export class NlpSampleEntityService extends BaseService<NlpSampleEntity> {
|
||||
constructor(
|
||||
readonly repository: NlpSampleEntityRepository,
|
||||
private readonly nlpEntityService: NlpEntityService,
|
||||
private readonly nlpValueService: NlpValueService,
|
||||
) {
|
||||
super(repository);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a single NLP sample entity by its ID and populates related
|
||||
* entities for the retrieved sample.
|
||||
*
|
||||
* @param id The ID of the NLP sample entity to find and populate.
|
||||
*
|
||||
* @returns The populated NLP sample entity.
|
||||
*/
|
||||
async findOneAndPopulate(id: string) {
|
||||
return await this.repository.findOneAndPopulate(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a paginated list of NLP sample entities based on filters, and
|
||||
* populates related entities for each retrieved sample entity.
|
||||
*
|
||||
* @param filters Filters to apply when searching for the entities.
|
||||
* @param pageQuery Query parameters for pagination.
|
||||
*
|
||||
* @returns A paginated list of populated NLP sample entities.
|
||||
*/
|
||||
async findPageAndPopulate(
|
||||
filters: TFilterQuery<NlpSampleEntity>,
|
||||
pageQuery: PageQueryDto<NlpSampleEntity>,
|
||||
) {
|
||||
return await this.repository.findPageAndPopulate(filters, pageQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds new sample entities to the corresponding training sample and returns
|
||||
* the created sample entities. It handles the storage of new entities and
|
||||
* their values and links them with the sample.
|
||||
*
|
||||
* @param sample The training sample to which the entities belong.
|
||||
* @param entities The array of entity values to store and link to the sample.
|
||||
*
|
||||
* @returns The newly created sample entities.
|
||||
*/
|
||||
async storeSampleEntities(
|
||||
sample: NlpSample,
|
||||
entities: NlpSampleEntityValue[],
|
||||
): Promise<NlpSampleEntity[]> {
|
||||
// Store any new entity/value
|
||||
const storedEntities = await this.nlpEntityService.storeEntities(entities);
|
||||
|
||||
const storedValues = await this.nlpValueService.storeValues(
|
||||
sample.text,
|
||||
entities,
|
||||
);
|
||||
|
||||
// Store sample entities
|
||||
const sampleEntities = entities.map((e) => {
|
||||
const storedEntity = storedEntities.find((se) => se.name === e.entity);
|
||||
const storedValue = storedValues.find((sv) => sv.value === e.value);
|
||||
if (!storedEntity || !storedValue) {
|
||||
throw new Error('Unable to find the stored entity or value');
|
||||
}
|
||||
return {
|
||||
sample: sample.id,
|
||||
entity: storedEntity.id, // replace entity name by id
|
||||
value: storedValue.id, // replace value by id
|
||||
start: 'start' in e ? e.start : undefined,
|
||||
end: 'end' in e ? e.end : undefined,
|
||||
} as NlpSampleEntity;
|
||||
});
|
||||
|
||||
return this.createMany(sampleEntities);
|
||||
}
|
||||
}
|
||||
142
api/src/nlp/services/nlp-sample.service.spec.ts
Normal file
142
api/src/nlp/services/nlp-sample.service.spec.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { nlpSampleFixtures } from '@/utils/test/fixtures/nlpsample';
|
||||
import { installNlpSampleEntityFixtures } from '@/utils/test/fixtures/nlpsampleentity';
|
||||
import { getPageQuery } from '@/utils/test/pagination';
|
||||
import {
|
||||
closeInMongodConnection,
|
||||
rootMongooseTestModule,
|
||||
} from '@/utils/test/test';
|
||||
|
||||
import { NlpEntityService } from './nlp-entity.service';
|
||||
import { NlpSampleEntityService } from './nlp-sample-entity.service';
|
||||
import { NlpSampleService } from './nlp-sample.service';
|
||||
import { NlpValueService } from './nlp-value.service';
|
||||
import { NlpEntityRepository } from '../repositories/nlp-entity.repository';
|
||||
import { NlpSampleEntityRepository } from '../repositories/nlp-sample-entity.repository';
|
||||
import { NlpSampleRepository } from '../repositories/nlp-sample.repository';
|
||||
import { NlpValueRepository } from '../repositories/nlp-value.repository';
|
||||
import { NlpEntityModel } from '../schemas/nlp-entity.schema';
|
||||
import {
|
||||
NlpSampleEntityModel,
|
||||
NlpSampleEntity,
|
||||
} from '../schemas/nlp-sample-entity.schema';
|
||||
import { NlpSampleModel, NlpSample } from '../schemas/nlp-sample.schema';
|
||||
import { NlpValueModel } from '../schemas/nlp-value.schema';
|
||||
|
||||
describe('NlpSampleService', () => {
|
||||
let nlpSampleService: NlpSampleService;
|
||||
let nlpSampleEntityRepository: NlpSampleEntityRepository;
|
||||
let nlpSampleRepository: NlpSampleRepository;
|
||||
let noNlpSample: NlpSample;
|
||||
let nlpSampleEntity: NlpSampleEntity;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [
|
||||
rootMongooseTestModule(installNlpSampleEntityFixtures),
|
||||
MongooseModule.forFeature([
|
||||
NlpSampleModel,
|
||||
NlpSampleEntityModel,
|
||||
NlpValueModel,
|
||||
NlpEntityModel,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
NlpSampleRepository,
|
||||
NlpSampleEntityRepository,
|
||||
NlpEntityRepository,
|
||||
NlpValueRepository,
|
||||
NlpSampleService,
|
||||
NlpSampleEntityService,
|
||||
NlpEntityService,
|
||||
NlpValueService,
|
||||
EventEmitter2,
|
||||
],
|
||||
}).compile();
|
||||
nlpSampleService = module.get<NlpSampleService>(NlpSampleService);
|
||||
nlpSampleRepository = module.get<NlpSampleRepository>(NlpSampleRepository);
|
||||
nlpSampleEntityRepository = module.get<NlpSampleEntityRepository>(
|
||||
NlpSampleEntityRepository,
|
||||
);
|
||||
nlpSampleEntityRepository = module.get<NlpSampleEntityRepository>(
|
||||
NlpSampleEntityRepository,
|
||||
);
|
||||
noNlpSample = await nlpSampleService.findOne({ text: 'No' });
|
||||
nlpSampleEntity = await nlpSampleEntityRepository.findOne({
|
||||
sample: noNlpSample.id,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await closeInMongodConnection();
|
||||
});
|
||||
|
||||
afterEach(jest.clearAllMocks);
|
||||
|
||||
describe('findOneAndPopulate', () => {
|
||||
it('should return a nlp Sample with populate', async () => {
|
||||
const result = await nlpSampleService.findOneAndPopulate(noNlpSample.id);
|
||||
const sampleWithEntities = {
|
||||
...nlpSampleFixtures[1],
|
||||
entities: [nlpSampleEntity],
|
||||
};
|
||||
expect(result).toEqualPayload(sampleWithEntities);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findPageAndPopulate', () => {
|
||||
it('should return all nlp samples with populate', async () => {
|
||||
const pageQuery = getPageQuery<NlpSample>({ sort: ['text', 'desc'] });
|
||||
const result = await nlpSampleService.findPageAndPopulate({}, pageQuery);
|
||||
const nlpSamples = await nlpSampleRepository.findAll();
|
||||
const nlpSampleEntities = await nlpSampleEntityRepository.findAll();
|
||||
|
||||
const nlpSampleFixturesWithEntities = nlpSamples.reduce(
|
||||
(acc, currSample) => {
|
||||
const sampleWithEntities = {
|
||||
...currSample,
|
||||
entities: nlpSampleEntities.filter((currSampleEntity) => {
|
||||
return currSampleEntity.sample === currSample.id;
|
||||
}),
|
||||
};
|
||||
acc.push(sampleWithEntities);
|
||||
return acc;
|
||||
},
|
||||
[],
|
||||
);
|
||||
expect(result).toEqualPayload(nlpSampleFixturesWithEntities);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateMany', () => {
|
||||
it('should update many nlp samples', async () => {
|
||||
const result = await nlpSampleService.updateMany(
|
||||
{},
|
||||
{
|
||||
trained: false,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.modifiedCount).toEqual(nlpSampleFixtures.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe('The deleteCascadeOne function', () => {
|
||||
it('should delete a nlp Sample', async () => {
|
||||
const result = await nlpSampleService.deleteOne(noNlpSample.id);
|
||||
expect(result.deletedCount).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
166
api/src/nlp/services/nlp-sample.service.ts
Normal file
166
api/src/nlp/services/nlp-sample.service.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { TFilterQuery } from 'mongoose';
|
||||
|
||||
import {
|
||||
CommonExample,
|
||||
DatasetType,
|
||||
EntitySynonym,
|
||||
ExampleEntity,
|
||||
LookupTable,
|
||||
} from '@/extensions/helpers/nlp/default/types';
|
||||
import { BaseService } from '@/utils/generics/base-service';
|
||||
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
|
||||
|
||||
import { NlpSampleRepository } from '../repositories/nlp-sample.repository';
|
||||
import { NlpEntity, NlpEntityFull } from '../schemas/nlp-entity.schema';
|
||||
import { NlpSample, NlpSampleFull } from '../schemas/nlp-sample.schema';
|
||||
import { NlpValue } from '../schemas/nlp-value.schema';
|
||||
|
||||
@Injectable()
|
||||
export class NlpSampleService extends BaseService<NlpSample> {
|
||||
constructor(readonly repository: NlpSampleRepository) {
|
||||
super(repository);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an NLP sample by its ID and cascades the operation if needed.
|
||||
*
|
||||
* @param id - The unique identifier of the NLP sample to delete.
|
||||
*
|
||||
* @returns A promise resolving when the sample is deleted.
|
||||
*/
|
||||
async deleteCascadeOne(id: string) {
|
||||
return await this.repository.deleteOne(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a single NLP sample by its ID and populates related data.
|
||||
*
|
||||
* @param id - The unique identifier of the NLP sample to find.
|
||||
*
|
||||
* @returns A promise resolving to the found sample with populated fields.
|
||||
*/
|
||||
async findOneAndPopulate(id: string) {
|
||||
return await this.repository.findOneAndPopulate(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a page of NLP samples based on filters and populates related data.
|
||||
*
|
||||
* @param filters - Query filters to apply when searching for samples.
|
||||
* @param pageQuery - Pagination and sorting options for the query.
|
||||
*
|
||||
* @returns A promise resolving to the paginated results with populated fields.
|
||||
*/
|
||||
async findPageAndPopulate(
|
||||
filters: TFilterQuery<NlpSample>,
|
||||
pageQuery: PageQueryDto<NlpSample>,
|
||||
) {
|
||||
return await this.repository.findPageAndPopulate(filters, pageQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds multiple NLP samples based on filters and populates related data.
|
||||
*
|
||||
* @param filters - Query filters to apply when searching for samples.
|
||||
*
|
||||
* @returns A promise resolving to the found samples with populated fields.
|
||||
*/
|
||||
async findAndPopulate(filters: TFilterQuery<NlpSample>) {
|
||||
return await this.repository.findAndPopulate(filters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all NLP samples and populates related data.
|
||||
*
|
||||
* @returns A promise resolving to all samples with populated fields.
|
||||
*/
|
||||
async findAllAndPopulate() {
|
||||
return await this.repository.findAllAndPopulate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a set of NLP samples into the Rasa NLU-compatible training dataset format.
|
||||
*
|
||||
* @param samples - The NLP samples to format.
|
||||
* @param entities - The NLP entities available in the dataset.
|
||||
*
|
||||
* @returns The formatted Rasa NLU training dataset.
|
||||
*/
|
||||
formatRasaNlu(
|
||||
samples: NlpSampleFull[],
|
||||
entities: NlpEntityFull[],
|
||||
): DatasetType {
|
||||
const entityMap = NlpEntity.getEntityMap(entities);
|
||||
const valueMap = NlpValue.getValueMap(
|
||||
NlpValue.getValuesFromEntities(entities),
|
||||
);
|
||||
|
||||
const common_examples: CommonExample[] = samples
|
||||
.filter((s) => s.entities.length > 0)
|
||||
.map((s) => {
|
||||
const intent = s.entities.find(
|
||||
(e) => entityMap[e.entity].name === 'intent',
|
||||
);
|
||||
if (!intent) {
|
||||
throw new Error('Unable to find the `intent` nlp entity.');
|
||||
}
|
||||
const sampleEntities: ExampleEntity[] = s.entities
|
||||
.filter((e) => entityMap[<string>e.entity].name !== 'intent')
|
||||
.map((e) => {
|
||||
const res: ExampleEntity = {
|
||||
entity: entityMap[<string>e.entity].name,
|
||||
value: valueMap[<string>e.value].value,
|
||||
};
|
||||
if ('start' in e && 'end' in e) {
|
||||
Object.assign(res, {
|
||||
start: e.start,
|
||||
end: e.end,
|
||||
});
|
||||
}
|
||||
return res;
|
||||
});
|
||||
return {
|
||||
text: s.text,
|
||||
intent: valueMap[intent.value].value,
|
||||
entities: sampleEntities,
|
||||
};
|
||||
});
|
||||
const lookup_tables: LookupTable[] = entities.map((e) => {
|
||||
return {
|
||||
name: e.name,
|
||||
elements: e.values.map((v) => {
|
||||
return v.value;
|
||||
}),
|
||||
};
|
||||
});
|
||||
const entity_synonyms = entities
|
||||
.reduce((acc, e) => {
|
||||
const synonyms = e.values.map((v) => {
|
||||
return {
|
||||
value: v.value,
|
||||
synonyms: v.expressions,
|
||||
};
|
||||
});
|
||||
return acc.concat(synonyms);
|
||||
}, [] as EntitySynonym[])
|
||||
.filter((s) => {
|
||||
return s.synonyms.length > 0;
|
||||
});
|
||||
return {
|
||||
common_examples,
|
||||
regex_features: [],
|
||||
lookup_tables,
|
||||
entity_synonyms,
|
||||
};
|
||||
}
|
||||
}
|
||||
201
api/src/nlp/services/nlp-value.service.spec.ts
Normal file
201
api/src/nlp/services/nlp-value.service.spec.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { nlpEntityFixtures } from '@/utils/test/fixtures/nlpentity';
|
||||
import {
|
||||
installNlpValueFixtures,
|
||||
nlpValueFixtures,
|
||||
} from '@/utils/test/fixtures/nlpvalue';
|
||||
import { getPageQuery } from '@/utils/test/pagination';
|
||||
import {
|
||||
closeInMongodConnection,
|
||||
rootMongooseTestModule,
|
||||
} from '@/utils/test/test';
|
||||
|
||||
import { NlpEntityService } from './nlp-entity.service';
|
||||
import { NlpValueService } from './nlp-value.service';
|
||||
import { NlpEntityRepository } from '../repositories/nlp-entity.repository';
|
||||
import { NlpSampleEntityRepository } from '../repositories/nlp-sample-entity.repository';
|
||||
import { NlpValueRepository } from '../repositories/nlp-value.repository';
|
||||
import { NlpEntity, NlpEntityModel } from '../schemas/nlp-entity.schema';
|
||||
import { NlpSampleEntityModel } from '../schemas/nlp-sample-entity.schema';
|
||||
import { NlpValue, NlpValueModel } from '../schemas/nlp-value.schema';
|
||||
|
||||
describe('NlpValueService', () => {
|
||||
let nlpEntityService: NlpEntityService;
|
||||
let nlpValueService: NlpValueService;
|
||||
let nlpValueRepository: NlpValueRepository;
|
||||
let nlpEntityRepository: NlpEntityRepository;
|
||||
let nlpValues: NlpValue[];
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [
|
||||
rootMongooseTestModule(installNlpValueFixtures),
|
||||
MongooseModule.forFeature([
|
||||
NlpValueModel,
|
||||
NlpSampleEntityModel,
|
||||
NlpEntityModel,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
NlpValueRepository,
|
||||
NlpSampleEntityRepository,
|
||||
NlpEntityRepository,
|
||||
NlpValueService,
|
||||
NlpEntityService,
|
||||
EventEmitter2,
|
||||
],
|
||||
}).compile();
|
||||
nlpValueService = module.get<NlpValueService>(NlpValueService);
|
||||
nlpEntityService = module.get<NlpEntityService>(NlpEntityService);
|
||||
nlpValueRepository = module.get<NlpValueRepository>(NlpValueRepository);
|
||||
nlpEntityRepository = module.get<NlpEntityRepository>(NlpEntityRepository);
|
||||
nlpValues = await nlpValueRepository.findAll();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await closeInMongodConnection();
|
||||
});
|
||||
|
||||
afterEach(jest.clearAllMocks);
|
||||
|
||||
describe('findOneAndPopulate', () => {
|
||||
it('should return a nlp Value with populate', async () => {
|
||||
const result = await nlpValueService.findOneAndPopulate(nlpValues[1].id);
|
||||
const valueWithEntity = {
|
||||
...nlpValueFixtures[1],
|
||||
entity: nlpEntityFixtures[0],
|
||||
};
|
||||
expect(result).toEqualPayload(valueWithEntity);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findPageAndPopulate', () => {
|
||||
it('should return all nlp entities with populate', async () => {
|
||||
const pageQuery = getPageQuery<NlpValue>({ sort: ['value', 'desc'] });
|
||||
const result = await nlpValueService.findPageAndPopulate({}, pageQuery);
|
||||
const nlpValueFixturesWithEntities = nlpValueFixtures.reduce(
|
||||
(acc, curr) => {
|
||||
const ValueWithEntities = {
|
||||
...curr,
|
||||
entity: nlpEntityFixtures[parseInt(curr.entity)],
|
||||
};
|
||||
acc.push(ValueWithEntities);
|
||||
return acc;
|
||||
},
|
||||
[],
|
||||
);
|
||||
expect(result).toEqualPayload(nlpValueFixturesWithEntities);
|
||||
});
|
||||
});
|
||||
|
||||
describe('The deleteCascadeOne function', () => {
|
||||
it('should delete a nlp Value', async () => {
|
||||
const result = await nlpValueService.deleteOne(nlpValues[1].id);
|
||||
expect(result.deletedCount).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('storeNewValues', () => {
|
||||
it('should store new values', async () => {
|
||||
const storedEntities = await nlpEntityRepository.findAll();
|
||||
const result = await nlpValueService.storeNewValues(
|
||||
'Hello do you see me',
|
||||
[
|
||||
{ entity: 'intent', value: 'greeting' },
|
||||
{ entity: 'first_name', value: 'jhon' },
|
||||
],
|
||||
storedEntities,
|
||||
);
|
||||
const intentEntity = await nlpEntityRepository.findOne({
|
||||
name: 'intent',
|
||||
});
|
||||
const firstNameEntity = await nlpEntityRepository.findOne({
|
||||
name: 'first_name',
|
||||
});
|
||||
const greetingValue = await nlpValueRepository.findOne({
|
||||
value: 'greeting',
|
||||
});
|
||||
const jhonValue = await nlpValueRepository.findOne({ value: 'jhon' });
|
||||
const storedValues = [
|
||||
{
|
||||
entity: intentEntity.id,
|
||||
value: greetingValue.id,
|
||||
},
|
||||
{
|
||||
entity: firstNameEntity.id,
|
||||
value: jhonValue.id,
|
||||
},
|
||||
];
|
||||
|
||||
expect(result).toEqual(storedValues);
|
||||
});
|
||||
});
|
||||
|
||||
describe('storeValues', () => {
|
||||
it('should store new values correctly', async () => {
|
||||
const sampleText = 'This is a test';
|
||||
const sampleEntities = [
|
||||
{ entity: 'testEntity', value: 'testValue', start: 10, end: 14 },
|
||||
];
|
||||
const expectedEntities = [{ name: 'testEntity', id: '9'.repeat(24) }];
|
||||
const mockFindResults = expectedEntities;
|
||||
|
||||
jest
|
||||
.spyOn(nlpEntityService, 'find')
|
||||
.mockResolvedValue(mockFindResults as NlpEntity[]);
|
||||
jest
|
||||
.spyOn(nlpValueService, 'findOneOrCreate')
|
||||
.mockImplementation((_condition, newValue) =>
|
||||
Promise.resolve({
|
||||
...newValue,
|
||||
expressions: [],
|
||||
} as unknown as NlpValue),
|
||||
);
|
||||
jest
|
||||
.spyOn(nlpValueService, 'updateOne')
|
||||
.mockImplementation((_condition, newValue) =>
|
||||
Promise.resolve({
|
||||
...newValue,
|
||||
expressions: [],
|
||||
} as unknown as NlpValue),
|
||||
);
|
||||
|
||||
const result = await nlpValueService.storeValues(
|
||||
sampleText,
|
||||
sampleEntities,
|
||||
);
|
||||
|
||||
expect(nlpEntityService.find).toHaveBeenCalledWith({
|
||||
name: { $in: ['testEntity'] },
|
||||
});
|
||||
expect(nlpValueService.findOneOrCreate).toHaveBeenCalledTimes(1);
|
||||
expect(nlpValueService.updateOne).toHaveBeenCalledTimes(1);
|
||||
expect(result).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should throw an error if entity not found', async () => {
|
||||
const sampleText = 'This is a test';
|
||||
const sampleEntities = [
|
||||
{ entity: 'unknownEntity', value: 'testValue', start: 10, end: 14 },
|
||||
];
|
||||
|
||||
jest.spyOn(nlpEntityService, 'find').mockResolvedValue([]);
|
||||
|
||||
await expect(
|
||||
nlpValueService.storeValues(sampleText, sampleEntities),
|
||||
).rejects.toThrow('Unable to find the stored entity unknownEntity');
|
||||
});
|
||||
});
|
||||
});
|
||||
237
api/src/nlp/services/nlp-value.service.ts
Normal file
237
api/src/nlp/services/nlp-value.service.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { forwardRef, Inject, Injectable } from '@nestjs/common';
|
||||
import { TFilterQuery } from 'mongoose';
|
||||
|
||||
import { BaseService } from '@/utils/generics/base-service';
|
||||
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
|
||||
|
||||
import { NlpEntityService } from './nlp-entity.service';
|
||||
import { NlpValueCreateDto, NlpValueUpdateDto } from '../dto/nlp-value.dto';
|
||||
import { NlpValueRepository } from '../repositories/nlp-value.repository';
|
||||
import { NlpEntity } from '../schemas/nlp-entity.schema';
|
||||
import { NlpValue } from '../schemas/nlp-value.schema';
|
||||
import { NlpSampleEntityValue } from '../schemas/types';
|
||||
|
||||
@Injectable()
|
||||
export class NlpValueService extends BaseService<NlpValue> {
|
||||
constructor(
|
||||
readonly repository: NlpValueRepository,
|
||||
@Inject(forwardRef(() => NlpEntityService))
|
||||
private readonly nlpEntityService: NlpEntityService,
|
||||
) {
|
||||
super(repository);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an NLP value by its ID, cascading any dependent data.
|
||||
*
|
||||
* @param id The ID of the NLP value to delete.
|
||||
*
|
||||
* @returns A promise that resolves when the deletion is complete.
|
||||
*/
|
||||
async deleteCascadeOne(id: string) {
|
||||
return await this.repository.deleteOne(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds an NLP value by its ID and populates related entities.
|
||||
*
|
||||
* @param id The ID of the NLP value to find.
|
||||
*
|
||||
* @returns A promise that resolves with the populated NLP value.
|
||||
*/
|
||||
async findOneAndPopulate(id: string) {
|
||||
return await this.repository.findOneAndPopulate(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a page of NLP values based on filters, and populates related entities.
|
||||
*
|
||||
* @param filters The filters to apply when searching for NLP values.
|
||||
* @param pageQuery Pagination information such as page number and size.
|
||||
*
|
||||
* @returns A promise that resolves with a page of populated NLP values.
|
||||
*/
|
||||
async findPageAndPopulate(
|
||||
filters: TFilterQuery<NlpValue>,
|
||||
pageQuery: PageQueryDto<NlpValue>,
|
||||
) {
|
||||
return await this.repository.findPageAndPopulate(filters, pageQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds new NLP values or updates existing ones based on the provided training sample.
|
||||
* This method handles both the creation of new values and the addition of synonyms.
|
||||
*
|
||||
* @param sampleText The original text from the training sample.
|
||||
* @param sampleEntities The entities and values extracted from the sample.
|
||||
* @param storedEntities The stored NLP entities to be updated with new values.
|
||||
*
|
||||
* @returns A promise that resolves with the updated sample entities containing their IDs.
|
||||
*/
|
||||
async storeNewValues(
|
||||
sampleText: string,
|
||||
sampleEntities: NlpSampleEntityValue[],
|
||||
storedEntities: NlpEntity[],
|
||||
) {
|
||||
const eMap: Record<string, NlpEntity> = storedEntities.reduce(
|
||||
(acc, curr) => {
|
||||
if (curr.name) acc[curr?.name] = curr;
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
// Extract entity values from sampleEntities
|
||||
const values = sampleEntities.map((e) => e.value);
|
||||
|
||||
// Retrieve stored values
|
||||
let storedValues = await this.find({
|
||||
value: {
|
||||
$in: values,
|
||||
},
|
||||
});
|
||||
|
||||
// Compute the values to be added
|
||||
const valuesToAdd = sampleEntities
|
||||
// Remove already stored values
|
||||
.filter((e) => storedValues.findIndex((v) => v.value === e.value) === -1)
|
||||
// Filter unique values to avoid duplicates
|
||||
.filter(
|
||||
(e, idx, self) =>
|
||||
self.findIndex(({ value }) => e.value === value) === idx,
|
||||
)
|
||||
// Build the value dto object
|
||||
.map((e) => {
|
||||
const newValue: NlpValueCreateDto = {
|
||||
entity: eMap[e.entity].id,
|
||||
value: e.value,
|
||||
expressions: [],
|
||||
};
|
||||
// Deal with synonym case
|
||||
if ('start' in e && 'end' in e) {
|
||||
const word = sampleText.slice(e.start, e.end);
|
||||
if (word !== e.value) {
|
||||
newValue.expressions = [word];
|
||||
}
|
||||
}
|
||||
return newValue;
|
||||
});
|
||||
|
||||
// Store new values
|
||||
const newValues = await this.createMany(valuesToAdd);
|
||||
|
||||
storedValues = storedValues.concat(newValues);
|
||||
|
||||
const vMap: Record<string, NlpValue> = storedValues.reduce((acc, curr) => {
|
||||
acc[curr.value] = curr;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// Store new synonyms for existing values
|
||||
const synonymsToAdd = sampleEntities
|
||||
.filter((e) => {
|
||||
if ('start' in e && 'end' in e) {
|
||||
const word = sampleText.slice(e.start, e.end);
|
||||
return (
|
||||
word !== e.value && vMap[e.value].expressions.indexOf(word) === -1
|
||||
);
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.map((e) => {
|
||||
return this.updateOne(vMap[e.value].id, {
|
||||
...vMap[e.value],
|
||||
expressions: vMap[e.value].expressions.concat([
|
||||
sampleText.slice(e.start, e.end),
|
||||
]),
|
||||
} as NlpValueUpdateDto);
|
||||
});
|
||||
|
||||
await Promise.all(synonymsToAdd);
|
||||
|
||||
// Replace entities/values with ids
|
||||
const result: NlpSampleEntityValue[] = sampleEntities.map((e) => {
|
||||
return {
|
||||
...e,
|
||||
entity: eMap[e.entity].id,
|
||||
value: vMap[e.value].id,
|
||||
};
|
||||
});
|
||||
|
||||
// Return sample entities with ids
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds new NLP values or updates existing ones based on the training sample.
|
||||
* Handles the addition of synonyms and ensures that each value has the correct entity association.
|
||||
*
|
||||
* @param sampleText The original text from the training sample.
|
||||
* @param sampleEntities The entities and values extracted from the sample.
|
||||
*
|
||||
* @returns A promise that resolves with the stored NLP values.
|
||||
*/
|
||||
async storeValues(
|
||||
sampleText: string,
|
||||
sampleEntities: NlpSampleEntityValue[],
|
||||
): Promise<NlpValue[]> {
|
||||
const entities = sampleEntities.map((e) => e.entity);
|
||||
// Get all used entities from database
|
||||
const storedEntities = await this.nlpEntityService.find({
|
||||
name: { $in: entities },
|
||||
});
|
||||
|
||||
// Prepare values objects for storage
|
||||
const valuesToAdd: NlpValueCreateDto[] = sampleEntities.map((e) => {
|
||||
let expressions: string[] = [];
|
||||
// Deal with synonym case
|
||||
if (
|
||||
'start' in e &&
|
||||
e.start &&
|
||||
e.start >= 0 &&
|
||||
'end' in e &&
|
||||
e.end &&
|
||||
e.end > 0
|
||||
) {
|
||||
const word = sampleText.slice(e.start, e.end);
|
||||
if (word !== e.value) {
|
||||
expressions = [word];
|
||||
}
|
||||
}
|
||||
const storedEntity = storedEntities.find((se) => se.name === e.entity);
|
||||
if (!storedEntity) {
|
||||
throw new Error(`Unable to find the stored entity ${e.entity}`);
|
||||
}
|
||||
return {
|
||||
entity: storedEntity.id,
|
||||
value: e.value,
|
||||
expressions,
|
||||
};
|
||||
});
|
||||
// Find or create values
|
||||
const promises = valuesToAdd.map(async (v) => {
|
||||
const createdOrFound = await this.findOneOrCreate({ value: v.value }, v);
|
||||
// If value is found in database, then update it's synonyms
|
||||
const expressions = createdOrFound.expressions
|
||||
.concat(v.expressions) // Add new synonyms
|
||||
.filter((v, i, a) => a.indexOf(v) === i); // Filter unique values
|
||||
|
||||
// Update expressions
|
||||
const result = await this.updateOne({ value: v.value }, { expressions });
|
||||
|
||||
if (!result) throw new Error(`Unable to update NLP value ${v.value}`);
|
||||
|
||||
return result;
|
||||
});
|
||||
return Promise.all(promises);
|
||||
}
|
||||
}
|
||||
218
api/src/nlp/services/nlp.service.ts
Normal file
218
api/src/nlp/services/nlp.service.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { SettingService } from '@/setting/services/setting.service';
|
||||
|
||||
import { NlpEntityService } from './nlp-entity.service';
|
||||
import { NlpSampleService } from './nlp-sample.service';
|
||||
import { NlpValueService } from './nlp-value.service';
|
||||
import BaseNlpHelper from '../lib/BaseNlpHelper';
|
||||
import { NlpEntity } from '../schemas/nlp-entity.schema';
|
||||
import { NlpValue } from '../schemas/nlp-value.schema';
|
||||
|
||||
@Injectable()
|
||||
export class NlpService {
|
||||
private registry: Map<string, BaseNlpHelper> = new Map();
|
||||
|
||||
private nlp: BaseNlpHelper;
|
||||
|
||||
constructor(
|
||||
private readonly settingService: SettingService,
|
||||
private readonly logger: LoggerService,
|
||||
protected readonly nlpSampleService: NlpSampleService,
|
||||
protected readonly nlpEntityService: NlpEntityService,
|
||||
protected readonly nlpValueService: NlpValueService,
|
||||
) {
|
||||
this.initNLP();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a helper with a specific name in the registry.
|
||||
*
|
||||
* @param name - The name of the helper to register.
|
||||
* @param helper - The NLP helper to be associated with the given name.
|
||||
* @typeParam C - The type of the helper, which must extend `BaseNlpHelper`.
|
||||
*/
|
||||
public setHelper<C extends BaseNlpHelper>(name: string, helper: C) {
|
||||
this.registry.set(name, helper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all registered helpers.
|
||||
*
|
||||
* @returns An array of all helpers currently registered.
|
||||
*/
|
||||
public getAll() {
|
||||
return Array.from(this.registry.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the appropriate helper based on the helper name.
|
||||
*
|
||||
* @param helperName - The name of the helper (messenger, offline, ...).
|
||||
*
|
||||
* @returns The specified helper.
|
||||
*/
|
||||
public getHelper<C extends BaseNlpHelper>(name: string): C {
|
||||
const handler = this.registry.get(name);
|
||||
if (!handler) {
|
||||
throw new Error(`NLP Helper ${name} not found`);
|
||||
}
|
||||
return handler as C;
|
||||
}
|
||||
|
||||
async initNLP() {
|
||||
try {
|
||||
const settings = await this.settingService.getSettings();
|
||||
const nlpSettings = settings.nlp_settings;
|
||||
const helper = this.getHelper(nlpSettings.provider);
|
||||
|
||||
if (helper) {
|
||||
this.nlp = helper;
|
||||
this.nlp.setSettings(nlpSettings);
|
||||
} else {
|
||||
throw new Error(`Undefined NLP Helper ${nlpSettings.provider}`);
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.error('NLP Service : Unable to instantiate NLP Helper !', e);
|
||||
// throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the currently active NLP helper.
|
||||
*
|
||||
* @returns The current NLP helper.
|
||||
*/
|
||||
getNLP() {
|
||||
return this.nlp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the event triggered when NLP settings are updated. Re-initializes the NLP service.
|
||||
*/
|
||||
@OnEvent('hook:settings:nlp_settings:*')
|
||||
async handleSettingsUpdate() {
|
||||
this.initNLP();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the event triggered when a new NLP entity is created. Synchronizes the entity with the external NLP provider.
|
||||
*
|
||||
* @param entity - The NLP entity to be created.
|
||||
* @returns The updated entity after synchronization.
|
||||
*/
|
||||
@OnEvent('hook:nlp:entity:create')
|
||||
async handleEntityCreate(entity: NlpEntity) {
|
||||
// Synchonize new entity with NLP
|
||||
try {
|
||||
const foreignId = await this.getNLP().addEntity(entity);
|
||||
this.logger.debug('New entity successfully synced!', foreignId);
|
||||
return await this.nlpEntityService.updateOne(entity.id, {
|
||||
foreign_id: foreignId,
|
||||
});
|
||||
} catch (err) {
|
||||
this.logger.error('Unable to sync a new entity', err);
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the event triggered when an NLP entity is updated. Synchronizes the updated entity with the external NLP provider.
|
||||
*
|
||||
* @param entity - The NLP entity to be updated.
|
||||
*/
|
||||
@OnEvent('hook:nlp:entity:update')
|
||||
async handleEntityUpdate(entity: NlpEntity) {
|
||||
// Synchonize new entity with NLP provider
|
||||
try {
|
||||
await this.getNLP().updateEntity(entity);
|
||||
this.logger.debug('Updated entity successfully synced!', entity);
|
||||
} catch (err) {
|
||||
this.logger.error('Unable to sync updated entity', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the event triggered when an NLP entity is deleted. Synchronizes the deletion with the external NLP provider.
|
||||
*
|
||||
* @param entity - The NLP entity to be deleted.
|
||||
*/
|
||||
@OnEvent('hook:nlp:entity:delete')
|
||||
async handleEntityDelete(entity: NlpEntity) {
|
||||
// Synchonize new entity with NLP provider
|
||||
try {
|
||||
await this.getNLP().deleteEntity(entity.foreign_id);
|
||||
this.logger.debug('Deleted entity successfully synced!', entity);
|
||||
} catch (err) {
|
||||
this.logger.error('Unable to sync deleted entity', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the event triggered when a new NLP value is created. Synchronizes the value with the external NLP provider.
|
||||
*
|
||||
* @param value - The NLP value to be created.
|
||||
*
|
||||
* @returns The updated value after synchronization.
|
||||
*/
|
||||
@OnEvent('hook:nlp:value:create')
|
||||
async handleValueCreate(value: NlpValue) {
|
||||
// Synchonize new value with NLP provider
|
||||
try {
|
||||
const foreignId = await this.getNLP().addValue(value);
|
||||
this.logger.debug('New value successfully synced!', foreignId);
|
||||
return await this.nlpValueService.updateOne(value.id, {
|
||||
foreign_id: foreignId,
|
||||
});
|
||||
} catch (err) {
|
||||
this.logger.error('Unable to sync a new value', err);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the event triggered when an NLP value is updated. Synchronizes the updated value with the external NLP provider.
|
||||
*
|
||||
* @param value - The NLP value to be updated.
|
||||
*/
|
||||
@OnEvent('hook:nlp:value:update')
|
||||
async handleValueUpdate(value: NlpValue) {
|
||||
// Synchonize new value with NLP provider
|
||||
try {
|
||||
await this.getNLP().updateValue(value);
|
||||
this.logger.debug('Updated value successfully synced!', value);
|
||||
} catch (err) {
|
||||
this.logger.error('Unable to sync updated value', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the event triggered when an NLP value is deleted. Synchronizes the deletion with the external NLP provider.
|
||||
*
|
||||
* @param value - The NLP value to be deleted.
|
||||
*/
|
||||
@OnEvent('hook:nlp:value:delete')
|
||||
async handleValueDelete(value: NlpValue) {
|
||||
// Synchonize new value with NLP provider
|
||||
try {
|
||||
const populatedValue = await this.nlpValueService.findOneAndPopulate(
|
||||
value.id,
|
||||
);
|
||||
await this.getNLP().deleteValue(populatedValue);
|
||||
this.logger.debug('Deleted value successfully synced!', value);
|
||||
} catch (err) {
|
||||
this.logger.error('Unable to sync deleted value', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user