feat: initial commit

This commit is contained in:
Mohamed Marrouchi
2024-09-10 10:50:11 +01:00
commit 30e5766487
879 changed files with 122820 additions and 0 deletions

View 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);
});
});
});

View 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;
}
}

View 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 });
});
});
});

View 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 };
}
}

View 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);
});
});
});

View 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;
}
}

View 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(),
};
});
}
}

View 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;
}

View 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;
}

View 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) {}

View 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) {}

View 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
View 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
View 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 {}

View 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,
},
]);
});
});
});

View 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);
}
}

View 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);
});
});
});

View 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);
}
}

View 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);
});
});
});

View 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);
}
}

View 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);
});
});
});

View 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);
}
}

View 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;

View 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;

View 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;

View 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;

View 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',
}

View 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,
},
];

View 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);
}
}

View 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,
};
}),
];

View 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;
}
}

View 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);
});
});
});

View 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);
}
}

View 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');
});
});
});

View 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);
}
}

View 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);
});
});
});

View 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,
};
}
}

View 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');
});
});
});

View 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);
}
}

View 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);
}
}
}