From d3b5070407dd37f250f21330477df8e1b130a339 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Fri, 6 Dec 2024 16:26:54 +0100 Subject: [PATCH 01/12] fix: add retro compatibility for find and findAndPopulate methods --- .../chat/controllers/message.controller.ts | 2 +- api/src/utils/generics/base-repository.ts | 27 ++++++++++++------- api/src/utils/generics/base-service.ts | 6 +++-- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/api/src/chat/controllers/message.controller.ts b/api/src/chat/controllers/message.controller.ts index bddaf5a4..8bfc633c 100644 --- a/api/src/chat/controllers/message.controller.ts +++ b/api/src/chat/controllers/message.controller.ts @@ -55,7 +55,7 @@ import { SubscriberService } from '../services/subscriber.service'; @UseInterceptors(CsrfInterceptor) @Controller('message') export class MessageController extends BaseController< - Message, + AnyMessage, MessageStub, MessagePopulate, MessageFull diff --git a/api/src/utils/generics/base-repository.ts b/api/src/utils/generics/base-repository.ts index e2bc6f68..8aed82e5 100644 --- a/api/src/utils/generics/base-repository.ts +++ b/api/src/utils/generics/base-repository.ts @@ -257,20 +257,28 @@ export abstract class BaseRepository< protected findQuery( filter: TFilterQuery, - pageQuery?: PageQueryDto, + // TODO: QuerySortDto type need to be removed + pageQuery?: QuerySortDto | PageQueryDto, projection?: ProjectionType, ) { - const { skip = 0, limit, sort = ['createdAt', 'asc'] } = pageQuery || {}; - const query = this.model.find(filter, projection); - return query - .skip(skip) - .limit(limit) - .sort([sort] as [string, SortOrder][]); + // TODO: current block need to be removed + if (Array.isArray(pageQuery)) { + const query = this.model.find(filter, projection); + return query.sort([pageQuery] as [string, SortOrder][]); + } else { + const { skip = 0, limit, sort = ['createdAt', 'asc'] } = pageQuery || {}; + const query = this.model.find(filter, projection); + return query + .skip(skip) + .limit(limit) + .sort([sort] as [string, SortOrder][]); + } } async find( filter: TFilterQuery, - pageQuery?: PageQueryDto, + // TODO: QuerySortDto type need to be removed + pageQuery?: QuerySortDto | PageQueryDto, projection?: ProjectionType, ) { const query = this.findQuery(filter, pageQuery, projection); @@ -285,7 +293,8 @@ export abstract class BaseRepository< async findAndPopulate( filters: TFilterQuery, - pageQuery?: PageQueryDto, + // TODO: QuerySortDto need to be removed + pageQuery?: QuerySortDto | PageQueryDto, projection?: ProjectionType, ) { this.ensureCanPopulate(); diff --git a/api/src/utils/generics/base-service.ts b/api/src/utils/generics/base-service.ts index fede7015..4a27e857 100644 --- a/api/src/utils/generics/base-service.ts +++ b/api/src/utils/generics/base-service.ts @@ -46,7 +46,8 @@ export abstract class BaseService< async find( filter: TFilterQuery, - pageQuery?: PageQueryDto, + // TODO: QuerySortDto type need to be removed + pageQuery?: QuerySortDto | PageQueryDto, projection?: ProjectionType, ): Promise { return await this.repository.find(filter, pageQuery, projection); @@ -54,7 +55,8 @@ export abstract class BaseService< async findAndPopulate( filters: TFilterQuery, - pageQuery?: PageQueryDto, + // TODO: QuerySortDto type need to be removed + pageQuery?: QuerySortDto | PageQueryDto, projection?: ProjectionType, ) { return await this.repository.findAndPopulate( From d912042f882ac73caadbd34c66d283f40761de09 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Fri, 6 Dec 2024 17:43:56 +0100 Subject: [PATCH 02/12] fix: use overloading to marke deprecated methods signatures --- api/src/utils/generics/base-repository.ts | 36 ++++++++++++++++--- api/src/utils/generics/base-service.ts | 44 +++++++++++++++++++++-- 2 files changed, 73 insertions(+), 7 deletions(-) diff --git a/api/src/utils/generics/base-repository.ts b/api/src/utils/generics/base-repository.ts index 8aed82e5..717c3f78 100644 --- a/api/src/utils/generics/base-repository.ts +++ b/api/src/utils/generics/base-repository.ts @@ -275,12 +275,26 @@ export abstract class BaseRepository< } } + /** + * @deprecated + */ + async find( + filter: TFilterQuery, + pageQuery?: QuerySortDto, + projection?: ProjectionType, + ): Promise; + async find( filter: TFilterQuery, - // TODO: QuerySortDto type need to be removed pageQuery?: QuerySortDto | PageQueryDto, projection?: ProjectionType, - ) { + ): Promise; + + async find( + filter: TFilterQuery, + pageQuery?: QuerySortDto | PageQueryDto, + projection?: ProjectionType, + ): Promise { const query = this.findQuery(filter, pageQuery, projection); return await this.execute(query, this.cls); } @@ -291,12 +305,26 @@ export abstract class BaseRepository< } } + /** + * @deprecated + */ + async findAndPopulate( + filters: TFilterQuery, + pageQuery?: QuerySortDto, + projection?: ProjectionType, + ): Promise; + + async findAndPopulate( + filters: TFilterQuery, + pageQuery?: PageQueryDto, + projection?: ProjectionType, + ): Promise; + async findAndPopulate( filters: TFilterQuery, - // TODO: QuerySortDto need to be removed pageQuery?: QuerySortDto | PageQueryDto, projection?: ProjectionType, - ) { + ): Promise { this.ensureCanPopulate(); const query = this.findQuery(filters, pageQuery, projection).populate( this.populate, diff --git a/api/src/utils/generics/base-service.ts b/api/src/utils/generics/base-service.ts index 4a27e857..9686eb76 100644 --- a/api/src/utils/generics/base-service.ts +++ b/api/src/utils/generics/base-service.ts @@ -44,21 +44,59 @@ export abstract class BaseService< return await this.repository.findOneAndPopulate(criteria, projection); } + /** + * @deprecated + */ + async find( + filter: TFilterQuery, + pageQuery?: QuerySortDto, + projection?: ProjectionType, + ): Promise; + + async find( + filter: TFilterQuery, + pageQuery?: PageQueryDto, + projection?: ProjectionType, + ): Promise; + async find( filter: TFilterQuery, - // TODO: QuerySortDto type need to be removed pageQuery?: QuerySortDto | PageQueryDto, projection?: ProjectionType, ): Promise { + if (Array.isArray(pageQuery)) + return await this.repository.find(filter, pageQuery, projection); + return await this.repository.find(filter, pageQuery, projection); } + /** + * @deprecated + */ + async findAndPopulate( + filters: TFilterQuery, + pageQuery?: QuerySortDto, + projection?: ProjectionType, + ): Promise; + + async findAndPopulate( + filters: TFilterQuery, + pageQuery?: PageQueryDto, + projection?: ProjectionType, + ): Promise; + async findAndPopulate( filters: TFilterQuery, - // TODO: QuerySortDto type need to be removed pageQuery?: QuerySortDto | PageQueryDto, projection?: ProjectionType, - ) { + ): Promise { + if (Array.isArray(pageQuery)) + return await this.repository.findAndPopulate( + filters, + pageQuery, + projection, + ); + return await this.repository.findAndPopulate( filters, pageQuery, From fb20456367ced455b274147da2c28210ea4664eb Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Fri, 6 Dec 2024 17:50:14 +0100 Subject: [PATCH 03/12] fix: use overloading to marke deprecated methods signatures v0.0.1 --- api/src/utils/generics/base-repository.ts | 39 +++++++++++++++++++---- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/api/src/utils/generics/base-repository.ts b/api/src/utils/generics/base-repository.ts index 717c3f78..c13a57be 100644 --- a/api/src/utils/generics/base-repository.ts +++ b/api/src/utils/generics/base-repository.ts @@ -255,13 +255,26 @@ export abstract class BaseRepository< return await this.executeOne(query, this.clsPopulate); } + /** + * @deprecated + */ + protected findQuery( + filter: TFilterQuery, + pageQuery?: QuerySortDto, + projection?: ProjectionType, + ); + + protected findQuery( + filter: TFilterQuery, + pageQuery?: PageQueryDto, + projection?: ProjectionType, + ); + protected findQuery( filter: TFilterQuery, - // TODO: QuerySortDto type need to be removed pageQuery?: QuerySortDto | PageQueryDto, projection?: ProjectionType, ) { - // TODO: current block need to be removed if (Array.isArray(pageQuery)) { const query = this.model.find(filter, projection); return query.sort([pageQuery] as [string, SortOrder][]); @@ -286,7 +299,7 @@ export abstract class BaseRepository< async find( filter: TFilterQuery, - pageQuery?: QuerySortDto | PageQueryDto, + pageQuery?: PageQueryDto, projection?: ProjectionType, ): Promise; @@ -295,6 +308,11 @@ export abstract class BaseRepository< pageQuery?: QuerySortDto | PageQueryDto, projection?: ProjectionType, ): Promise { + if (Array.isArray(pageQuery)) { + const query = this.findQuery(filter, pageQuery, projection); + return await this.execute(query, this.cls); + } + const query = this.findQuery(filter, pageQuery, projection); return await this.execute(query, this.cls); } @@ -326,10 +344,17 @@ export abstract class BaseRepository< projection?: ProjectionType, ): Promise { this.ensureCanPopulate(); - const query = this.findQuery(filters, pageQuery, projection).populate( - this.populate, - ); - return await this.execute(query, this.clsPopulate); + if (Array.isArray(pageQuery)) { + const query = this.findQuery(filters, pageQuery, projection).populate( + this.populate, + ); + return await this.execute(query, this.clsPopulate); + } else { + const query = this.findQuery(filters, pageQuery, projection).populate( + this.populate, + ); + return await this.execute(query, this.clsPopulate); + } } protected findAllQuery(sort?: QuerySortDto) { From 3acdbed03755c2c098c5e270966583b3a42e3d9c Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Fri, 6 Dec 2024 18:27:24 +0100 Subject: [PATCH 04/12] fix: update limit default values --- api/src/utils/generics/base-repository.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/api/src/utils/generics/base-repository.ts b/api/src/utils/generics/base-repository.ts index c13a57be..7b9bd87f 100644 --- a/api/src/utils/generics/base-repository.ts +++ b/api/src/utils/generics/base-repository.ts @@ -279,7 +279,11 @@ export abstract class BaseRepository< const query = this.model.find(filter, projection); return query.sort([pageQuery] as [string, SortOrder][]); } else { - const { skip = 0, limit, sort = ['createdAt', 'asc'] } = pageQuery || {}; + const { + skip = 0, + limit = 0, + sort = ['createdAt', 'asc'], + } = pageQuery || {}; const query = this.model.find(filter, projection); return query .skip(skip) @@ -358,11 +362,11 @@ export abstract class BaseRepository< } protected findAllQuery(sort?: QuerySortDto) { - return this.findQuery({}, { limit: undefined, skip: undefined, sort }); + return this.findQuery({}, { limit: 0, skip: undefined, sort }); } async findAll(sort?: QuerySortDto) { - return await this.find({}, { limit: undefined, skip: undefined, sort }); + return await this.find({}, { limit: 0, skip: undefined, sort }); } async findAllAndPopulate(sort?: QuerySortDto) { From da783c0ef81c20733cad1f7c852898374381bf55 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Fri, 6 Dec 2024 18:27:48 +0100 Subject: [PATCH 05/12] fix: update skip default values --- api/src/utils/generics/base-repository.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/utils/generics/base-repository.ts b/api/src/utils/generics/base-repository.ts index 7b9bd87f..f3bd9fed 100644 --- a/api/src/utils/generics/base-repository.ts +++ b/api/src/utils/generics/base-repository.ts @@ -362,11 +362,11 @@ export abstract class BaseRepository< } protected findAllQuery(sort?: QuerySortDto) { - return this.findQuery({}, { limit: 0, skip: undefined, sort }); + return this.findQuery({}, { limit: 0, skip: 0, sort }); } async findAll(sort?: QuerySortDto) { - return await this.find({}, { limit: 0, skip: undefined, sort }); + return await this.find({}, { limit: 0, skip: 0, sort }); } async findAllAndPopulate(sort?: QuerySortDto) { From d9a87e99bfbb9a9dc6e9d0408c8df76e424e4c67 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Fri, 6 Dec 2024 18:34:18 +0100 Subject: [PATCH 06/12] fix: base-repository.ts methods returning types --- api/src/utils/generics/base-repository.ts | 57 ++++++++++++----------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/api/src/utils/generics/base-repository.ts b/api/src/utils/generics/base-repository.ts index f3bd9fed..2b01f632 100644 --- a/api/src/utils/generics/base-repository.ts +++ b/api/src/utils/generics/base-repository.ts @@ -22,6 +22,7 @@ import { SortOrder, UpdateQuery, UpdateWithAggregationPipeline, + UpdateWriteOpResult, } from 'mongoose'; import { TFilterQuery } from '@/utils/types/filter.types'; @@ -70,7 +71,7 @@ export abstract class BaseRepository< this.registerLifeCycleHooks(); } - getPopulate() { + getPopulate(): P[] { return this.populate; } @@ -79,7 +80,7 @@ export abstract class BaseRepository< return `hook:${entity}:${suffix}` as `hook:${IHookEntities}:${TNormalizedEvents}`; } - private registerLifeCycleHooks() { + private registerLifeCycleHooks(): void { const repository = this; const hooks = LifecycleHookManager.getHooks(this.cls.name); @@ -202,7 +203,7 @@ export abstract class BaseRepository< protected async execute>( query: Query, cls: new () => R, - ) { + ): Promise { const resultSet = await query.lean(this.leanOpts).exec(); return resultSet.map((doc) => plainToClass(cls, doc, this.transformOpts)); } @@ -211,7 +212,7 @@ export abstract class BaseRepository< query: Query, cls: new () => R, options?: ClassTransformOptions, - ) { + ): Promise { const doc = await query.lean(this.leanOpts).exec(); return plainToClass(cls, doc, options ?? this.transformOpts); } @@ -219,7 +220,7 @@ export abstract class BaseRepository< protected findOneQuery( criteria: string | TFilterQuery, projection?: ProjectionType, - ) { + ): Query { if (!criteria) { // An empty criteria would return the first document that it finds throw new Error('findOneQuery() should not have an empty criteria'); @@ -247,7 +248,7 @@ export abstract class BaseRepository< async findOneAndPopulate( criteria: string | TFilterQuery, projection?: ProjectionType, - ) { + ): Promise { this.ensureCanPopulate(); const query = this.findOneQuery(criteria, projection).populate( this.populate, @@ -262,19 +263,19 @@ export abstract class BaseRepository< filter: TFilterQuery, pageQuery?: QuerySortDto, projection?: ProjectionType, - ); + ): Query; protected findQuery( filter: TFilterQuery, pageQuery?: PageQueryDto, projection?: ProjectionType, - ); + ): Query; protected findQuery( filter: TFilterQuery, pageQuery?: QuerySortDto | PageQueryDto, projection?: ProjectionType, - ) { + ): Query { if (Array.isArray(pageQuery)) { const query = this.model.find(filter, projection); return query.sort([pageQuery] as [string, SortOrder][]); @@ -321,7 +322,7 @@ export abstract class BaseRepository< return await this.execute(query, this.cls); } - private ensureCanPopulate() { + private ensureCanPopulate(): void { if (!this.populate || !this.clsPopulate) { throw new Error('Cannot populate query'); } @@ -361,15 +362,17 @@ export abstract class BaseRepository< } } - protected findAllQuery(sort?: QuerySortDto) { + protected findAllQuery( + sort?: QuerySortDto, + ): Query { return this.findQuery({}, { limit: 0, skip: 0, sort }); } - async findAll(sort?: QuerySortDto) { + async findAll(sort?: QuerySortDto): Promise { return await this.find({}, { limit: 0, skip: 0, sort }); } - async findAllAndPopulate(sort?: QuerySortDto) { + async findAllAndPopulate(sort?: QuerySortDto): Promise { this.ensureCanPopulate(); const query = this.findAllQuery(sort).populate(this.populate); return await this.execute(query, this.clsPopulate); @@ -381,7 +384,7 @@ export abstract class BaseRepository< protected findPageQuery( filters: TFilterQuery, { skip, limit, sort }: PageQueryDto, - ) { + ): Query { return this.findQuery(filters) .skip(skip) .limit(limit) @@ -405,7 +408,7 @@ export abstract class BaseRepository< async findPageAndPopulate( filters: TFilterQuery, pageQuery: PageQueryDto, - ) { + ): Promise { this.ensureCanPopulate(); const query = this.findPageQuery(filters, pageQuery).populate( this.populate, @@ -431,7 +434,7 @@ export abstract class BaseRepository< ); } - async createMany(dtoArray: U[]) { + async createMany(dtoArray: U[]): Promise { const docs = await this.model.create(dtoArray); return docs.map((doc) => @@ -460,7 +463,7 @@ export abstract class BaseRepository< async updateMany>( filter: TFilterQuery, dto: UpdateQuery, - ) { + ): Promise { return await this.model.updateMany(filter, { $set: dto, }); @@ -476,19 +479,19 @@ export abstract class BaseRepository< return await this.model.deleteMany(criteria); } - async preValidate(_doc: HydratedDocument) { + async preValidate(_doc: HydratedDocument): Promise { // Nothing ... } - async postValidate(_validated: HydratedDocument) { + async postValidate(_validated: HydratedDocument): Promise { // Nothing ... } - async preCreate(_doc: HydratedDocument) { + async preCreate(_doc: HydratedDocument): Promise { // Nothing ... } - async postCreate(_created: HydratedDocument) { + async postCreate(_created: HydratedDocument): Promise { // Nothing ... } @@ -496,7 +499,7 @@ export abstract class BaseRepository< _query: Query, _criteria: TFilterQuery, _updates: UpdateWithAggregationPipeline | UpdateQuery, - ) { + ): Promise { // Nothing ... } @@ -504,35 +507,35 @@ export abstract class BaseRepository< _query: Query, _criteria: TFilterQuery, _updates: UpdateWithAggregationPipeline | UpdateQuery, - ) { + ): Promise { // Nothing ... } async postUpdateMany( _query: Query, _updated: any, - ) { + ): Promise { // Nothing ... } async postUpdate( _query: Query, _updated: T, - ) { + ): Promise { // Nothing ... } async preDelete( _query: Query, _criteria: TFilterQuery, - ) { + ): Promise { // Nothing ... } async postDelete( _query: Query, _result: DeleteResult, - ) { + ): Promise { // Nothing ... } } From afa83649965dbc3ac1d3854c2bff7428e3ef2a81 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Sat, 7 Dec 2024 11:59:00 +0100 Subject: [PATCH 07/12] fix: add missing await --- api/src/attachment/controllers/attachment.controller.ts | 2 +- api/src/cms/controllers/menu.controller.ts | 4 ++-- api/src/cms/repositories/content.repository.ts | 2 +- api/src/cms/services/content.service.ts | 2 +- api/src/extensions/helpers/core-nlu/index.helper.ts | 2 +- api/src/i18n/controllers/translation.controller.ts | 2 +- api/src/nlp/services/nlp-sample-entity.service.ts | 2 +- api/src/user/controllers/user.controller.ts | 4 ++-- api/src/user/services/passwordReset.service.ts | 4 ++-- api/src/user/services/validate-account.service.ts | 4 ++-- frontend/src/websocket/SocketIoClient.ts | 2 +- widget/src/utils/SocketIoClient.ts | 4 ++-- 12 files changed, 17 insertions(+), 17 deletions(-) diff --git a/api/src/attachment/controllers/attachment.controller.ts b/api/src/attachment/controllers/attachment.controller.ts index fbc3717a..8a0ca6d4 100644 --- a/api/src/attachment/controllers/attachment.controller.ts +++ b/api/src/attachment/controllers/attachment.controller.ts @@ -153,7 +153,7 @@ export class AttachmentController extends BaseController { throw new NotFoundException('Attachment not found'); } - return this.attachmentService.download(attachment); + return await this.attachmentService.download(attachment); } /** diff --git a/api/src/cms/controllers/menu.controller.ts b/api/src/cms/controllers/menu.controller.ts index ab7b70a1..f2343d1d 100644 --- a/api/src/cms/controllers/menu.controller.ts +++ b/api/src/cms/controllers/menu.controller.ts @@ -164,8 +164,8 @@ export class MenuController extends BaseController< @CsrfCheck(true) @Patch(':id') async updateOne(@Body() body: MenuCreateDto, @Param('id') id: string) { - if (!id) return this.create(body); - return this.menuService.updateOne(id, body); + if (!id) return await this.create(body); + return await this.menuService.updateOne(id, body); } /** diff --git a/api/src/cms/repositories/content.repository.ts b/api/src/cms/repositories/content.repository.ts index 4dea95ce..77403b08 100644 --- a/api/src/cms/repositories/content.repository.ts +++ b/api/src/cms/repositories/content.repository.ts @@ -100,7 +100,7 @@ export class ContentRepository extends BaseRepository< * @returns A promise that resolves to the matching content documents. */ async textSearch(query: string) { - return this.find({ + return await this.find({ $text: { $search: query, $diacriticSensitive: false, diff --git a/api/src/cms/services/content.service.ts b/api/src/cms/services/content.service.ts index bdcac780..c73c5da5 100644 --- a/api/src/cms/services/content.service.ts +++ b/api/src/cms/services/content.service.ts @@ -49,7 +49,7 @@ export class ContentService extends BaseService< * @return A list of content matching the search query. */ async textSearch(query: string) { - return this.repository.textSearch(query); + return await this.repository.textSearch(query); } /** diff --git a/api/src/extensions/helpers/core-nlu/index.helper.ts b/api/src/extensions/helpers/core-nlu/index.helper.ts index 6a2ebd39..e92691b1 100644 --- a/api/src/extensions/helpers/core-nlu/index.helper.ts +++ b/api/src/extensions/helpers/core-nlu/index.helper.ts @@ -272,7 +272,7 @@ export default class CoreNluHelper extends BaseNlpHelper< }, ); - return this.filterEntitiesByConfidence(nlp, threshold); + return await this.filterEntitiesByConfidence(nlp, threshold); } catch (err) { this.logger.error('Core NLU Helper : Unable to parse nlp', err); throw err; diff --git a/api/src/i18n/controllers/translation.controller.ts b/api/src/i18n/controllers/translation.controller.ts index f4ab4e7c..9e571103 100644 --- a/api/src/i18n/controllers/translation.controller.ts +++ b/api/src/i18n/controllers/translation.controller.ts @@ -137,7 +137,7 @@ export class TranslationController extends BaseController { ); await Promise.all(queue); // Purge non existing translations - return this.translationService.deleteMany({ + return await this.translationService.deleteMany({ str: { $nin: strings }, }); } diff --git a/api/src/nlp/services/nlp-sample-entity.service.ts b/api/src/nlp/services/nlp-sample-entity.service.ts index 3ebb8df2..088bbf00 100644 --- a/api/src/nlp/services/nlp-sample-entity.service.ts +++ b/api/src/nlp/services/nlp-sample-entity.service.ts @@ -74,6 +74,6 @@ export class NlpSampleEntityService extends BaseService< } as NlpSampleEntity; }); - return this.createMany(sampleEntities); + return await this.createMany(sampleEntities); } } diff --git a/api/src/user/controllers/user.controller.ts b/api/src/user/controllers/user.controller.ts index 2a4594b5..a07b257a 100644 --- a/api/src/user/controllers/user.controller.ts +++ b/api/src/user/controllers/user.controller.ts @@ -84,7 +84,7 @@ export class ReadOnlyUserController extends BaseController< @Roles('public') @Get('bot/profile_pic') async botProfilePic(@Query('color') color: string) { - return getBotAvatar(color); + return await getBotAvatar(color); } /** @@ -103,7 +103,7 @@ export class ReadOnlyUserController extends BaseController< } catch (e) { const user = await this.userService.findOne(id); if (user) { - return generateInitialsAvatar(user); + return await generateInitialsAvatar(user); } else { throw new NotFoundException(`user with ID ${id} not found`); } diff --git a/api/src/user/services/passwordReset.service.ts b/api/src/user/services/passwordReset.service.ts index 845ddf52..704e1a9b 100644 --- a/api/src/user/services/passwordReset.service.ts +++ b/api/src/user/services/passwordReset.service.ts @@ -127,7 +127,7 @@ export class PasswordResetService { * @returns The signed JWT token. */ async sign(dto: UserRequestResetDto) { - return this.jwtService.signAsync(dto, this.jwtSignOptions); + return await this.jwtService.signAsync(dto, this.jwtSignOptions); } /** @@ -138,6 +138,6 @@ export class PasswordResetService { * @returns The decoded payload of the token. */ async verify(token: string): Promise { - return this.jwtService.verifyAsync(token, this.jwtSignOptions); + return await this.jwtService.verifyAsync(token, this.jwtSignOptions); } } diff --git a/api/src/user/services/validate-account.service.ts b/api/src/user/services/validate-account.service.ts index 3516e827..c7a7d203 100644 --- a/api/src/user/services/validate-account.service.ts +++ b/api/src/user/services/validate-account.service.ts @@ -50,7 +50,7 @@ export class ValidateAccountService { * @returns A promise that resolves to the signed JWT token. */ async sign(dto: { email: string }) { - return this.jwtService.signAsync(dto, this.jwtSignOptions); + return await this.jwtService.signAsync(dto, this.jwtSignOptions); } /** @@ -61,7 +61,7 @@ export class ValidateAccountService { * @returns A promise that resolves to an object containing the user's email. */ async verify(token: string): Promise<{ email: string }> { - return this.jwtService.verifyAsync(token, this.jwtSignOptions); + return await this.jwtService.verifyAsync(token, this.jwtSignOptions); } /** diff --git a/frontend/src/websocket/SocketIoClient.ts b/frontend/src/websocket/SocketIoClient.ts index 9c40194d..1829d4cb 100644 --- a/frontend/src/websocket/SocketIoClient.ts +++ b/frontend/src/websocket/SocketIoClient.ts @@ -149,7 +149,7 @@ export class SocketIoClient { url: string, options?: Partial>, ): Promise> { - return this.request({ + return await this.request({ method: "get", url, ...options, diff --git a/widget/src/utils/SocketIoClient.ts b/widget/src/utils/SocketIoClient.ts index 13b6d93a..a260f0ab 100644 --- a/widget/src/utils/SocketIoClient.ts +++ b/widget/src/utils/SocketIoClient.ts @@ -167,7 +167,7 @@ export class SocketIoClient { url: string, options?: Partial>, ): Promise> { - return this.request({ + return await this.request({ method: "get", url, ...options, @@ -178,7 +178,7 @@ export class SocketIoClient { url: string, options: Partial>, ): Promise> { - return this.request({ + return await this.request({ method: "post", url, ...options, From 37ae2ec9adc5feecadfce5d7003708441d22424b Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Mon, 9 Dec 2024 20:57:00 +0100 Subject: [PATCH 08/12] fix: reoder overloading signatures --- api/src/utils/generics/base-repository.ts | 36 +++++++++++------------ api/src/utils/generics/base-service.ts | 24 +++++++-------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/api/src/utils/generics/base-repository.ts b/api/src/utils/generics/base-repository.ts index 2b01f632..57a97aca 100644 --- a/api/src/utils/generics/base-repository.ts +++ b/api/src/utils/generics/base-repository.ts @@ -256,6 +256,12 @@ export abstract class BaseRepository< return await this.executeOne(query, this.clsPopulate); } + protected findQuery( + filter: TFilterQuery, + pageQuery?: PageQueryDto, + projection?: ProjectionType, + ): Query; + /** * @deprecated */ @@ -265,12 +271,6 @@ export abstract class BaseRepository< projection?: ProjectionType, ): Query; - protected findQuery( - filter: TFilterQuery, - pageQuery?: PageQueryDto, - projection?: ProjectionType, - ): Query; - protected findQuery( filter: TFilterQuery, pageQuery?: QuerySortDto | PageQueryDto, @@ -293,6 +293,12 @@ export abstract class BaseRepository< } } + async find( + filter: TFilterQuery, + pageQuery?: PageQueryDto, + projection?: ProjectionType, + ): Promise; + /** * @deprecated */ @@ -302,12 +308,6 @@ export abstract class BaseRepository< projection?: ProjectionType, ): Promise; - async find( - filter: TFilterQuery, - pageQuery?: PageQueryDto, - projection?: ProjectionType, - ): Promise; - async find( filter: TFilterQuery, pageQuery?: QuerySortDto | PageQueryDto, @@ -328,6 +328,12 @@ export abstract class BaseRepository< } } + async findAndPopulate( + filters: TFilterQuery, + pageQuery?: PageQueryDto, + projection?: ProjectionType, + ): Promise; + /** * @deprecated */ @@ -337,12 +343,6 @@ export abstract class BaseRepository< projection?: ProjectionType, ): Promise; - async findAndPopulate( - filters: TFilterQuery, - pageQuery?: PageQueryDto, - projection?: ProjectionType, - ): Promise; - async findAndPopulate( filters: TFilterQuery, pageQuery?: QuerySortDto | PageQueryDto, diff --git a/api/src/utils/generics/base-service.ts b/api/src/utils/generics/base-service.ts index 9686eb76..5faeaf4c 100644 --- a/api/src/utils/generics/base-service.ts +++ b/api/src/utils/generics/base-service.ts @@ -44,6 +44,12 @@ export abstract class BaseService< return await this.repository.findOneAndPopulate(criteria, projection); } + async find( + filter: TFilterQuery, + pageQuery?: PageQueryDto, + projection?: ProjectionType, + ): Promise; + /** * @deprecated */ @@ -53,12 +59,6 @@ export abstract class BaseService< projection?: ProjectionType, ): Promise; - async find( - filter: TFilterQuery, - pageQuery?: PageQueryDto, - projection?: ProjectionType, - ): Promise; - async find( filter: TFilterQuery, pageQuery?: QuerySortDto | PageQueryDto, @@ -70,6 +70,12 @@ export abstract class BaseService< return await this.repository.find(filter, pageQuery, projection); } + async findAndPopulate( + filters: TFilterQuery, + pageQuery?: PageQueryDto, + projection?: ProjectionType, + ): Promise; + /** * @deprecated */ @@ -79,12 +85,6 @@ export abstract class BaseService< projection?: ProjectionType, ): Promise; - async findAndPopulate( - filters: TFilterQuery, - pageQuery?: PageQueryDto, - projection?: ProjectionType, - ): Promise; - async findAndPopulate( filters: TFilterQuery, pageQuery?: QuerySortDto | PageQueryDto, From 91687f25ecb15e990811c4fd5e5d806e2ad3c6ba Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Mon, 9 Dec 2024 11:51:41 +0100 Subject: [PATCH 09/12] fix: simplify logic --- api/src/utils/generics/base-repository.ts | 32 +++++++++++------------ 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/api/src/utils/generics/base-repository.ts b/api/src/utils/generics/base-repository.ts index 57a97aca..fdb55993 100644 --- a/api/src/utils/generics/base-repository.ts +++ b/api/src/utils/generics/base-repository.ts @@ -279,18 +279,18 @@ export abstract class BaseRepository< if (Array.isArray(pageQuery)) { const query = this.model.find(filter, projection); return query.sort([pageQuery] as [string, SortOrder][]); - } else { - const { - skip = 0, - limit = 0, - sort = ['createdAt', 'asc'], - } = pageQuery || {}; - const query = this.model.find(filter, projection); - return query - .skip(skip) - .limit(limit) - .sort([sort] as [string, SortOrder][]); } + + const { + skip = 0, + limit = 0, + sort = ['createdAt', 'asc'], + } = pageQuery || {}; + const query = this.model.find(filter, projection); + return query + .skip(skip) + .limit(limit) + .sort([sort] as [string, SortOrder][]); } async find( @@ -354,12 +354,12 @@ export abstract class BaseRepository< this.populate, ); return await this.execute(query, this.clsPopulate); - } else { - const query = this.findQuery(filters, pageQuery, projection).populate( - this.populate, - ); - return await this.execute(query, this.clsPopulate); } + + const query = this.findQuery(filters, pageQuery, projection).populate( + this.populate, + ); + return await this.execute(query, this.clsPopulate); } protected findAllQuery( From c1fcdf3b73bd856eabfce2c31515f9daadcb9a68 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Mon, 9 Dec 2024 12:11:47 +0100 Subject: [PATCH 10/12] fix: replace findpage method by find method --- api/src/attachment/controllers/attachment.controller.ts | 2 +- api/src/chat/repositories/message.repository.ts | 4 ++-- api/src/chat/repositories/subscriber.repository.ts | 2 +- api/src/chat/services/message.service.ts | 2 +- api/src/cms/controllers/content.controller.ts | 5 +---- api/src/cms/services/content.service.ts | 2 +- 6 files changed, 7 insertions(+), 10 deletions(-) diff --git a/api/src/attachment/controllers/attachment.controller.ts b/api/src/attachment/controllers/attachment.controller.ts index fbc3717a..f6586742 100644 --- a/api/src/attachment/controllers/attachment.controller.ts +++ b/api/src/attachment/controllers/attachment.controller.ts @@ -94,7 +94,7 @@ export class AttachmentController extends BaseController { ) filters: TFilterQuery, ) { - return await this.attachmentService.findPage(filters, pageQuery); + return await this.attachmentService.find(filters, pageQuery); } /** diff --git a/api/src/chat/repositories/message.repository.ts b/api/src/chat/repositories/message.repository.ts index 64bd99f4..72fbe300 100644 --- a/api/src/chat/repositories/message.repository.ts +++ b/api/src/chat/repositories/message.repository.ts @@ -72,7 +72,7 @@ export class MessageRepository extends BaseRepository< until = new Date(), limit: number = 30, ) { - return await this.findPage( + return await this.find( { $or: [{ recipient: subscriber.id }, { sender: subscriber.id }], createdAt: { $lt: until }, @@ -96,7 +96,7 @@ export class MessageRepository extends BaseRepository< since = new Date(), limit: number = 30, ) { - return await this.findPage( + return await this.find( { $or: [{ recipient: subscriber.id }, { sender: subscriber.id }], createdAt: { $gt: since }, diff --git a/api/src/chat/repositories/subscriber.repository.ts b/api/src/chat/repositories/subscriber.repository.ts index 9c81cfdd..a3ee6cd1 100644 --- a/api/src/chat/repositories/subscriber.repository.ts +++ b/api/src/chat/repositories/subscriber.repository.ts @@ -106,7 +106,7 @@ export class SubscriberRepository extends BaseRepository< * @returns The constructed query object. */ findByForeignIdQuery(id: string) { - return this.findPageQuery( + return this.findQuery( { foreign_id: id }, { skip: 0, limit: 1, sort: ['lastvisit', 'desc'] }, ); diff --git a/api/src/chat/services/message.service.ts b/api/src/chat/services/message.service.ts index c66d0903..67474daa 100644 --- a/api/src/chat/services/message.service.ts +++ b/api/src/chat/services/message.service.ts @@ -126,7 +126,7 @@ export class MessageService extends BaseService< * @returns The message history since the specified date. */ async findLastMessages(subscriber: Subscriber, limit: number = 5) { - const lastMessages = await this.findPage( + const lastMessages = await this.find( { $or: [{ sender: subscriber.id }, { recipient: subscriber.id }], }, diff --git a/api/src/cms/controllers/content.controller.ts b/api/src/cms/controllers/content.controller.ts index b807b051..5f4bf0c5 100644 --- a/api/src/cms/controllers/content.controller.ts +++ b/api/src/cms/controllers/content.controller.ts @@ -283,10 +283,7 @@ export class ContentController extends BaseController< ); throw new NotFoundException(`ContentType of id ${contentType} not found`); } - return await this.contentService.findPage( - { entity: contentType }, - pageQuery, - ); + return await this.contentService.find({ entity: contentType }, pageQuery); } /** diff --git a/api/src/cms/services/content.service.ts b/api/src/cms/services/content.service.ts index bdcac780..03883828 100644 --- a/api/src/cms/services/content.service.ts +++ b/api/src/cms/services/content.service.ts @@ -170,7 +170,7 @@ export class ContentService extends BaseService< } try { - const contents = await this.findPage(query, { + const contents = await this.find(query, { skip, limit, sort: ['createdAt', 'desc'], From f9caf0f458c1d01efe1b7923815440f3e6e98d10 Mon Sep 17 00:00:00 2001 From: Mohamed Marrouchi Date: Mon, 9 Dec 2024 14:15:06 +0100 Subject: [PATCH 11/12] feat: llm nlu capability --- api/src/channel/lib/EventWrapper.ts | 1 + .../helpers/llm-nlu/i18n/en/help.json | 5 + .../helpers/llm-nlu/i18n/en/label.json | 5 + .../helpers/llm-nlu/i18n/en/title.json | 3 + .../helpers/llm-nlu/i18n/fr/help.json | 5 + .../helpers/llm-nlu/i18n/fr/label.json | 5 + .../helpers/llm-nlu/i18n/fr/title.json | 3 + api/src/extensions/helpers/llm-nlu/index.d.ts | 22 +++ .../helpers/llm-nlu/index.helper.ts | 186 ++++++++++++++++++ .../extensions/helpers/llm-nlu/package.json | 8 + .../extensions/helpers/llm-nlu/settings.ts | 47 +++++ api/src/helper/helper.module.ts | 4 +- api/src/helper/lib/base-llm-helper.ts | 22 ++- api/src/helper/lib/base-nlp-helper.ts | 16 +- api/src/helper/types.ts | 59 +++++- api/src/setting/seeds/setting.seed-model.ts | 2 +- 16 files changed, 373 insertions(+), 20 deletions(-) create mode 100644 api/src/extensions/helpers/llm-nlu/i18n/en/help.json create mode 100644 api/src/extensions/helpers/llm-nlu/i18n/en/label.json create mode 100644 api/src/extensions/helpers/llm-nlu/i18n/en/title.json create mode 100644 api/src/extensions/helpers/llm-nlu/i18n/fr/help.json create mode 100644 api/src/extensions/helpers/llm-nlu/i18n/fr/label.json create mode 100644 api/src/extensions/helpers/llm-nlu/i18n/fr/title.json create mode 100644 api/src/extensions/helpers/llm-nlu/index.d.ts create mode 100644 api/src/extensions/helpers/llm-nlu/index.helper.ts create mode 100644 api/src/extensions/helpers/llm-nlu/package.json create mode 100644 api/src/extensions/helpers/llm-nlu/settings.ts diff --git a/api/src/channel/lib/EventWrapper.ts b/api/src/channel/lib/EventWrapper.ts index 9456e200..5cb2c1f6 100644 --- a/api/src/channel/lib/EventWrapper.ts +++ b/api/src/channel/lib/EventWrapper.ts @@ -23,6 +23,7 @@ import ChannelHandler from './Handler'; export interface ChannelEvent {} +// eslint-disable-next-line prettier/prettier export default abstract class EventWrapper< A, E, diff --git a/api/src/extensions/helpers/llm-nlu/i18n/en/help.json b/api/src/extensions/helpers/llm-nlu/i18n/en/help.json new file mode 100644 index 00000000..f860ea94 --- /dev/null +++ b/api/src/extensions/helpers/llm-nlu/i18n/en/help.json @@ -0,0 +1,5 @@ +{ + "model": "Specify the name of the LLM (Large Language Model) you want to use. Leave this field empty if you prefer to use the default model specified in the LLM helper's settings.", + "language_classifier_prompt_template": "Provide the prompt template used for language detection. Use Handlebars syntax to dynamically insert variables or customize the prompt based on your requirements.", + "trait_classifier_prompt_template": "Define the prompt template for trait classification tasks, such as intent or sentiment detection. Use Handlebars syntax to structure and format the prompt appropriately." +} diff --git a/api/src/extensions/helpers/llm-nlu/i18n/en/label.json b/api/src/extensions/helpers/llm-nlu/i18n/en/label.json new file mode 100644 index 00000000..014066fa --- /dev/null +++ b/api/src/extensions/helpers/llm-nlu/i18n/en/label.json @@ -0,0 +1,5 @@ +{ + "model": "LLM Model", + "language_classifier_prompt_template": "Language Detection Prompt Template", + "trait_classifier_prompt_template": "Trait Classifier Prompt Template" +} diff --git a/api/src/extensions/helpers/llm-nlu/i18n/en/title.json b/api/src/extensions/helpers/llm-nlu/i18n/en/title.json new file mode 100644 index 00000000..47ffe14e --- /dev/null +++ b/api/src/extensions/helpers/llm-nlu/i18n/en/title.json @@ -0,0 +1,3 @@ +{ + "llm_nlu_helper": "LLM NLU Engine" +} diff --git a/api/src/extensions/helpers/llm-nlu/i18n/fr/help.json b/api/src/extensions/helpers/llm-nlu/i18n/fr/help.json new file mode 100644 index 00000000..63f54cdd --- /dev/null +++ b/api/src/extensions/helpers/llm-nlu/i18n/fr/help.json @@ -0,0 +1,5 @@ +{ + "model": "Spécifiez le nom du modèle LLM que vous souhaitez utiliser. Laissez ce champ vide si vous préférez utiliser le modèle par défaut spécifié dans les paramètres de l'assistant LLM.", + "language_classifier_prompt_template": "Fournissez le modèle de prompt utilisé pour la détection de langue. Utilisez la syntaxe Handlebars pour insérer dynamiquement des variables ou personnaliser le prompt en fonction de vos besoins.", + "trait_classifier_prompt_template": "Définissez le modèle de prompt pour les tâches de classification des traits, telles que la détection d'intention ou de sentiment. Utilisez la syntaxe Handlebars pour structurer et formater correctement le prompt." +} diff --git a/api/src/extensions/helpers/llm-nlu/i18n/fr/label.json b/api/src/extensions/helpers/llm-nlu/i18n/fr/label.json new file mode 100644 index 00000000..71f79359 --- /dev/null +++ b/api/src/extensions/helpers/llm-nlu/i18n/fr/label.json @@ -0,0 +1,5 @@ +{ + "model": "Modèle LLM", + "language_classifier_prompt_template": "Modèle de prompt de détection de langue", + "trait_classifier_prompt_template": "Modèle de prompt du classificateur de traits" +} diff --git a/api/src/extensions/helpers/llm-nlu/i18n/fr/title.json b/api/src/extensions/helpers/llm-nlu/i18n/fr/title.json new file mode 100644 index 00000000..9482e1ad --- /dev/null +++ b/api/src/extensions/helpers/llm-nlu/i18n/fr/title.json @@ -0,0 +1,3 @@ +{ + "llm_nlu_helper": "Moteur LLM NLU" +} diff --git a/api/src/extensions/helpers/llm-nlu/index.d.ts b/api/src/extensions/helpers/llm-nlu/index.d.ts new file mode 100644 index 00000000..67aefad7 --- /dev/null +++ b/api/src/extensions/helpers/llm-nlu/index.d.ts @@ -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). + */ + +import LLM_NLU_HELPER_SETTINGS, { LLM_NLU_HELPER_NAMESPACE } from './settings'; + +declare global { + interface Settings extends SettingTree {} +} + +declare module '@nestjs/event-emitter' { + interface IHookExtensionsOperationMap { + [LLM_NLU_HELPER_NAMESPACE]: TDefinition< + object, + SettingMapByType + >; + } +} diff --git a/api/src/extensions/helpers/llm-nlu/index.helper.ts b/api/src/extensions/helpers/llm-nlu/index.helper.ts new file mode 100644 index 00000000..25fdcdb1 --- /dev/null +++ b/api/src/extensions/helpers/llm-nlu/index.helper.ts @@ -0,0 +1,186 @@ +/* + * 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). + */ + +import { Injectable, OnModuleInit } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; +import Handlebars from 'handlebars'; + +import { HelperService } from '@/helper/helper.service'; +import BaseNlpHelper from '@/helper/lib/base-nlp-helper'; +import { LLM, Nlp } from '@/helper/types'; +import { LanguageService } from '@/i18n/services/language.service'; +import { LoggerService } from '@/logger/logger.service'; +import { NlpEntityFull } from '@/nlp/schemas/nlp-entity.schema'; +import { NlpEntityService } from '@/nlp/services/nlp-entity.service'; +import { SettingService } from '@/setting/services/setting.service'; + +import { LLM_NLU_HELPER_NAME } from './settings'; + +@Injectable() +export default class LlmNluHelper + extends BaseNlpHelper + implements OnModuleInit +{ + private languageClassifierPrompt: string; + + /** + * Trait prompts dictionary by id + */ + private traitClassifierPrompts: Array; + + constructor( + settingService: SettingService, + helperService: HelperService, + logger: LoggerService, + private readonly languageService: LanguageService, + private readonly nlpEntityService: NlpEntityService, + ) { + super(LLM_NLU_HELPER_NAME, settingService, helperService, logger); + } + + getPath() { + return __dirname; + } + + @OnEvent('hook:language:*') + @OnEvent('hook:llm_nlu_helper:language_classifier_prompt_template') + async buildLanguageClassifierPrompt() { + const settings = await this.getSettings(); + if (settings) { + const languages = await this.languageService.findAll(); + const delegate = Handlebars.compile( + settings.language_classifier_prompt_template, + ); + this.languageClassifierPrompt = delegate({ languages }); + } + } + + @OnEvent('hook:nlpEntity:*') + @OnEvent('hook:nlpValue:*') + @OnEvent('hook:llm_nlu_helper:trait_classifier_prompt_template') + async buildClassifiersPrompt() { + const settings = await this.getSettings(); + if (settings) { + const entities = await this.nlpEntityService.findAndPopulate({ + lookups: 'trait', + }); + const traitEntities = entities.filter(({ lookups }) => + lookups.includes('trait'), + ); + this.traitClassifierPrompts = traitEntities.map((entity) => ({ + ...entity, + prompt: Handlebars.compile(settings.trait_classifier_prompt_template)({ + entity, + }), + })); + } + } + + async onModuleInit() { + super.onModuleInit(); + + await this.buildLanguageClassifierPrompt(); + await this.buildClassifiersPrompt(); + } + + /** + * Finds entities in a given text based on their values and synonyms. + * + * This function takes a string of text and an array of entities, where each entity contains a value + * and a list of synonyms. It returns an array of objects, each representing an entity found in the text + * along with its start and end positions. + * + * @param text - The input text to search for entities. + * @param entities - An array of entities to search for, each containing a `value` and a list of `synonyms`. + * + * @returns An array of objects representing the found entities, with their `value`, `start`, and `end` positions. + */ + private findKeywordEntities( + text: string, + entity: NlpEntityFull, + ): Nlp.ParseEntity[] { + return entity.values + .flatMap(({ value, expressions }) => { + const allValues = [value, ...expressions]; + + // Filter the terms that are found in the text + return allValues + .flatMap((term) => { + const regex = new RegExp(`\\b${term}\\b`, 'g'); + const matches = [...text.matchAll(regex)]; + + // Map matches to FoundEntity format + return matches.map((match) => ({ + entity: entity.name, + value: term, + start: match.index!, + end: match.index! + term.length, + confidence: 1, + })); + }) + .shift(); + }) + .filter((v) => !!v); + } + + async predict(text: string): Promise { + const settings = await this.getSettings(); + const helper = await this.helperService.getDefaultLlmHelper(); + const defaultLanguage = await this.languageService.getDefaultLanguage(); + // Detect language + const language = await helper.generateStructuredResponse( + `input text: ${text}`, + settings.model, + this.languageClassifierPrompt, + { + type: LLM.ResponseSchemaType.STRING, + description: 'Language of the input text', + }, + ); + + const traits: Nlp.ParseEntity[] = [ + { + entity: 'language', + value: language || defaultLanguage.code, + confidence: undefined, + }, + ]; + for await (const { name, doc, prompt, values } of this + .traitClassifierPrompts) { + const allowedValues = values.map(({ value }) => value); + const result = await helper.generateStructuredResponse( + `input text: ${text}`, + settings.model, + prompt, + { + type: LLM.ResponseSchemaType.STRING, + description: `${name}${doc ? `: ${doc}` : ''}`, + enum: allowedValues.concat('unknown'), + }, + ); + const safeValue = result.toLowerCase().trim(); + const value = allowedValues.includes(safeValue) ? safeValue : ''; + traits.push({ + entity: name, + value, + confidence: undefined, + }); + } + + // Perform slot filling in a deterministic way since + // it's currently a challenging task for the LLMs. + const keywordEntities = await this.nlpEntityService.findAndPopulate({ + lookups: 'keywords', + }); + const entities = keywordEntities.flatMap((keywordEntity) => + this.findKeywordEntities(text, keywordEntity), + ); + + return { entities: traits.concat(entities) }; + } +} diff --git a/api/src/extensions/helpers/llm-nlu/package.json b/api/src/extensions/helpers/llm-nlu/package.json new file mode 100644 index 00000000..67ef402c --- /dev/null +++ b/api/src/extensions/helpers/llm-nlu/package.json @@ -0,0 +1,8 @@ +{ + "name": "hexabot-helper-llm-nlu", + "version": "2.0.0", + "description": "The LLM NLU Helper Extension for Hexabot to enable the Intent Classification and Language Detection", + "dependencies": {}, + "author": "Hexastack", + "license": "AGPL-3.0-only" +} diff --git a/api/src/extensions/helpers/llm-nlu/settings.ts b/api/src/extensions/helpers/llm-nlu/settings.ts new file mode 100644 index 00000000..9d7bb4b2 --- /dev/null +++ b/api/src/extensions/helpers/llm-nlu/settings.ts @@ -0,0 +1,47 @@ +/* + * 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). + */ + +import { HelperSetting } from '@/helper/types'; +import { SettingType } from '@/setting/schemas/types'; + +export const LLM_NLU_HELPER_NAME = 'llm-nlu-helper'; + +export const LLM_NLU_HELPER_NAMESPACE = 'llm_nlu_helper'; + +export default [ + { + group: LLM_NLU_HELPER_NAMESPACE, + label: 'model', + value: '', + type: SettingType.text, + }, + { + group: LLM_NLU_HELPER_NAMESPACE, + label: 'language_classifier_prompt_template', + value: `You are an advanced language detection assistant. Your task is to identify the language of the given input text from the following supported languages: + +{{#each languages}} +- {{title}} (code={{code}}) +{{/each}} + +Provide a concise result by stating the language code only. If the language is not in the supported list, return an empty string.`, + type: SettingType.textarea, + }, + { + group: LLM_NLU_HELPER_NAMESPACE, + label: 'trait_classifier_prompt_template', + value: `You are an advanced text classification assistant. Your task is to classify the given input text provided in the following {{entity.name}} values: + +{{#each entity.values}} +- {{value}} +{{/each}} + +Provide a concise result by stating only the value of the {{entity.name}}. Return an empty string otherwise.`, + type: SettingType.textarea, + }, +] as const satisfies HelperSetting[]; diff --git a/api/src/helper/helper.module.ts b/api/src/helper/helper.module.ts index 24892974..365e6d78 100644 --- a/api/src/helper/helper.module.ts +++ b/api/src/helper/helper.module.ts @@ -10,6 +10,8 @@ import { HttpModule } from '@nestjs/axios'; import { Global, Module } from '@nestjs/common'; import { InjectDynamicProviders } from 'nestjs-dynamic-providers'; +import { NlpModule } from '@/nlp/nlp.module'; + import { HelperController } from './helper.controller'; import { HelperService } from './helper.service'; @@ -23,7 +25,7 @@ import { HelperService } from './helper.service'; 'dist/.hexabot/custom/extensions/helpers/**/*.helper.js', ) @Module({ - imports: [HttpModule], + imports: [HttpModule, NlpModule], controllers: [HelperController], providers: [HelperService], exports: [HelperService], diff --git a/api/src/helper/lib/base-llm-helper.ts b/api/src/helper/lib/base-llm-helper.ts index 541bb453..21757c29 100644 --- a/api/src/helper/lib/base-llm-helper.ts +++ b/api/src/helper/lib/base-llm-helper.ts @@ -11,7 +11,7 @@ import { LoggerService } from '@/logger/logger.service'; import { SettingService } from '@/setting/services/setting.service'; import { HelperService } from '../helper.service'; -import { HelperName, HelperType } from '../types'; +import { HelperName, HelperType, LLM } from '../types'; import BaseHelper from './base-helper'; @@ -30,7 +30,7 @@ export default abstract class BaseLlmHelper< } /** - * Generates a response using LLM + * Generates a text response using LLM * * @param prompt - The input text from the user * @param model - The model to be used @@ -45,6 +45,24 @@ export default abstract class BaseLlmHelper< extra?: any, ): Promise; + /** + * Generates a structured response using LLM + * + * @param prompt - The input text from the user + * @param model - The model to be used + * @param systemPrompt - The input text from the system + * @param schema - The OpenAPI data schema + * @param extra - Extra options + * @returns {Promise} - The generated response from the LLM + */ + generateStructuredResponse?( + prompt: string, + model: string, + systemPrompt: string, + schema: LLM.ResponseSchema, + extra?: any, + ): Promise; + /** * Send a chat completion request with the conversation history. * You can use this same approach to start the conversation diff --git a/api/src/helper/lib/base-nlp-helper.ts b/api/src/helper/lib/base-nlp-helper.ts index ae2775b3..90e17fb8 100644 --- a/api/src/helper/lib/base-nlp-helper.ts +++ b/api/src/helper/lib/base-nlp-helper.ts @@ -119,7 +119,7 @@ export default abstract class BaseNlpHelper< * * @returns The formatted NLP training set */ - abstract format(samples: NlpSampleFull[], entities: NlpEntityFull[]): unknown; + format?(samples: NlpSampleFull[], entities: NlpEntityFull[]): unknown; /** * Perform training request @@ -129,10 +129,7 @@ export default abstract class BaseNlpHelper< * * @returns Training result */ - abstract train( - samples: NlpSampleFull[], - entities: NlpEntityFull[], - ): Promise; + train?(samples: NlpSampleFull[], entities: NlpEntityFull[]): Promise; /** * Perform evaluation request @@ -142,10 +139,7 @@ export default abstract class BaseNlpHelper< * * @returns NLP evaluation result */ - abstract evaluate( - samples: NlpSampleFull[], - entities: NlpEntityFull[], - ): Promise; + evaluate?(samples: NlpSampleFull[], entities: NlpEntityFull[]): Promise; /** * Delete/Forget a sample @@ -154,7 +148,7 @@ export default abstract class BaseNlpHelper< * * @returns The deleted sample otherwise an error */ - async forget(sample: NlpSample): Promise { + async forget?(sample: NlpSample): Promise { return sample; } @@ -166,7 +160,7 @@ export default abstract class BaseNlpHelper< * * @returns NLP Parsed entities */ - abstract filterEntitiesByConfidence( + filterEntitiesByConfidence?( nlp: any, threshold: boolean, ): Promise; diff --git a/api/src/helper/types.ts b/api/src/helper/types.ts index 2e6a7256..969120e9 100644 --- a/api/src/helper/types.ts +++ b/api/src/helper/types.ts @@ -14,11 +14,6 @@ import BaseLlmHelper from './lib/base-llm-helper'; import BaseNlpHelper from './lib/base-nlp-helper'; export namespace Nlp { - export interface Config { - endpoint?: string; - token: string; - } - export interface ParseEntity { entity: string; // Entity name value: string; // Value name @@ -32,6 +27,60 @@ export namespace Nlp { } } +export namespace LLM { + /** + * Schema is used to define the format of input/output data. + * Represents a select subset of an OpenAPI 3.0 schema object. + * More fields may be added in the future as needed. + * @public + */ + export interface ResponseSchema { + /** + * Optional. The type of the property. {@link + * SchemaType}. + */ + type?: ResponseSchemaType; + /** Optional. The format of the property. */ + format?: string; + /** Optional. The description of the property. */ + description?: string; + /** Optional. Whether the property is nullable. */ + nullable?: boolean; + /** Optional. The items of the property. */ + items?: ResponseSchema; + /** Optional. The enum of the property. */ + enum?: string[]; + /** Optional. Map of {@link Schema}. */ + properties?: { + [k: string]: ResponseSchema; + }; + /** Optional. Array of required property. */ + required?: string[]; + /** Optional. The example of the property. */ + example?: unknown; + } + + /** + * Contains the list of OpenAPI data types + * as defined by https://swagger.io/docs/specification/data-models/data-types/ + * @public + */ + export enum ResponseSchemaType { + /** String type. */ + STRING = 'string', + /** Number type. */ + NUMBER = 'number', + /** Integer type. */ + INTEGER = 'integer', + /** Boolean type. */ + BOOLEAN = 'boolean', + /** Array type. */ + ARRAY = 'array', + /** Object type. */ + OBJECT = 'object', + } +} + export enum HelperType { NLU = 'nlu', LLM = 'llm', diff --git a/api/src/setting/seeds/setting.seed-model.ts b/api/src/setting/seeds/setting.seed-model.ts index e5cfce7b..6f7d08c7 100644 --- a/api/src/setting/seeds/setting.seed-model.ts +++ b/api/src/setting/seeds/setting.seed-model.ts @@ -13,7 +13,7 @@ export const DEFAULT_SETTINGS = [ { group: 'chatbot_settings', label: 'default_nlu_helper', - value: 'core-nlu-helper', + value: 'llm-nlu-helper', type: SettingType.select, config: { multiple: false, From dcf50977505c1717633777f5d98064f0357cfd41 Mon Sep 17 00:00:00 2001 From: Mohamed Marrouchi Date: Mon, 9 Dec 2024 14:16:08 +0100 Subject: [PATCH 12/12] fix: rename nlp to nlu namespace --- api/src/channel/lib/EventWrapper.ts | 14 +++++--------- api/src/chat/schemas/types/context.ts | 4 ++-- api/src/chat/services/block.service.ts | 4 ++-- .../helpers/core-nlu/__test__/index.mock.ts | 4 ++-- .../extensions/helpers/core-nlu/index.helper.ts | 8 ++++---- api/src/extensions/helpers/llm-nlu/index.helper.ts | 8 ++++---- api/src/helper/lib/base-nlp-helper.ts | 6 +++--- api/src/helper/types.ts | 2 +- api/src/utils/test/mocks/nlp.ts | 4 ++-- 9 files changed, 25 insertions(+), 29 deletions(-) diff --git a/api/src/channel/lib/EventWrapper.ts b/api/src/channel/lib/EventWrapper.ts index 5cb2c1f6..8af15595 100644 --- a/api/src/channel/lib/EventWrapper.ts +++ b/api/src/channel/lib/EventWrapper.ts @@ -17,25 +17,21 @@ import { StdIncomingMessage, } from '@/chat/schemas/types/message'; import { Payload } from '@/chat/schemas/types/quick-reply'; -import { Nlp } from '@/helper/types'; +import { NLU } from '@/helper/types'; import ChannelHandler from './Handler'; export interface ChannelEvent {} // eslint-disable-next-line prettier/prettier -export default abstract class EventWrapper< - A, - E, - C extends ChannelHandler = ChannelHandler, -> { +export default abstract class EventWrapper { _adapter: A = {} as A; _handler: C; _profile!: Subscriber; - _nlp!: Nlp.ParseEntities; + _nlp!: NLU.ParseEntities; /** * Constructor : Class used to wrap any channel's event in order @@ -138,7 +134,7 @@ export default abstract class EventWrapper< * * @returns The parsed NLP entities, or null if not available. */ - getNLP(): Nlp.ParseEntities | null { + getNLP(): NLU.ParseEntities | null { return this._nlp; } @@ -147,7 +143,7 @@ export default abstract class EventWrapper< * * @param nlp - NLP parse results */ - setNLP(nlp: Nlp.ParseEntities) { + setNLP(nlp: NLU.ParseEntities) { this._nlp = nlp; } diff --git a/api/src/chat/schemas/types/context.ts b/api/src/chat/schemas/types/context.ts index cae1ccf6..73514097 100644 --- a/api/src/chat/schemas/types/context.ts +++ b/api/src/chat/schemas/types/context.ts @@ -7,7 +7,7 @@ */ import { ChannelName } from '@/channel/types'; -import { Nlp } from '@/helper/types'; +import { NLU } from '@/helper/types'; import { Subscriber } from '../subscriber.schema'; @@ -17,7 +17,7 @@ export interface Context { channel?: ChannelName; text?: string; payload?: Payload | string; - nlp?: Nlp.ParseEntities | null; + nlp?: NLU.ParseEntities | null; vars: { [key: string]: any }; user_location: { address?: Record; diff --git a/api/src/chat/services/block.service.ts b/api/src/chat/services/block.service.ts index 5251c932..e3dd3eaa 100644 --- a/api/src/chat/services/block.service.ts +++ b/api/src/chat/services/block.service.ts @@ -13,7 +13,7 @@ import { AttachmentService } from '@/attachment/services/attachment.service'; import EventWrapper from '@/channel/lib/EventWrapper'; import { ContentService } from '@/cms/services/content.service'; import { CONSOLE_CHANNEL_NAME } from '@/extensions/channels/console/settings'; -import { Nlp } from '@/helper/types'; +import { NLU } from '@/helper/types'; import { I18nService } from '@/i18n/services/i18n.service'; import { LanguageService } from '@/i18n/services/language.service'; import { LoggerService } from '@/logger/logger.service'; @@ -254,7 +254,7 @@ export class BlockService extends BaseService { * @returns The NLP patterns that matches */ matchNLP( - nlp: Nlp.ParseEntities, + nlp: NLU.ParseEntities, block: Block | BlockFull, ): NlpPattern[] | undefined { // No nlp entities to check against diff --git a/api/src/extensions/helpers/core-nlu/__test__/index.mock.ts b/api/src/extensions/helpers/core-nlu/__test__/index.mock.ts index 617710c3..c63e9cf3 100644 --- a/api/src/extensions/helpers/core-nlu/__test__/index.mock.ts +++ b/api/src/extensions/helpers/core-nlu/__test__/index.mock.ts @@ -6,7 +6,7 @@ * 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). */ -import { Nlp } from '@/helper/types'; +import { NLU } from '@/helper/types'; import { NlpParseResultType, RasaNlu } from '../types'; @@ -100,7 +100,7 @@ export const nlpParseResult: NlpParseResultType = { text: 'Hello Joe', }; -export const nlpBestGuess: Nlp.ParseEntities = { +export const nlpBestGuess: NLU.ParseEntities = { entities: [ { start: 5, diff --git a/api/src/extensions/helpers/core-nlu/index.helper.ts b/api/src/extensions/helpers/core-nlu/index.helper.ts index 6a2ebd39..a702475d 100644 --- a/api/src/extensions/helpers/core-nlu/index.helper.ts +++ b/api/src/extensions/helpers/core-nlu/index.helper.ts @@ -11,7 +11,7 @@ import { Injectable } from '@nestjs/common'; import { HelperService } from '@/helper/helper.service'; import BaseNlpHelper from '@/helper/lib/base-nlp-helper'; -import { Nlp } from '@/helper/types'; +import { NLU } from '@/helper/types'; import { LanguageService } from '@/i18n/services/language.service'; import { LoggerService } from '@/logger/logger.service'; import { NlpEntity, NlpEntityFull } from '@/nlp/schemas/nlp-entity.schema'; @@ -191,10 +191,10 @@ export default class CoreNluHelper extends BaseNlpHelper< async filterEntitiesByConfidence( nlp: NlpParseResultType, threshold: boolean, - ): Promise { + ): Promise { try { let minConfidence = 0; - const guess: Nlp.ParseEntities = { + const guess: NLU.ParseEntities = { entities: nlp.entities.slice(), }; if (threshold) { @@ -255,7 +255,7 @@ export default class CoreNluHelper extends BaseNlpHelper< text: string, threshold: boolean, project: string = 'current', - ): Promise { + ): Promise { try { const settings = await this.getSettings(); const { data: nlp } = diff --git a/api/src/extensions/helpers/llm-nlu/index.helper.ts b/api/src/extensions/helpers/llm-nlu/index.helper.ts index 25fdcdb1..814f24cf 100644 --- a/api/src/extensions/helpers/llm-nlu/index.helper.ts +++ b/api/src/extensions/helpers/llm-nlu/index.helper.ts @@ -12,7 +12,7 @@ import Handlebars from 'handlebars'; import { HelperService } from '@/helper/helper.service'; import BaseNlpHelper from '@/helper/lib/base-nlp-helper'; -import { LLM, Nlp } from '@/helper/types'; +import { LLM, NLU } from '@/helper/types'; import { LanguageService } from '@/i18n/services/language.service'; import { LoggerService } from '@/logger/logger.service'; import { NlpEntityFull } from '@/nlp/schemas/nlp-entity.schema'; @@ -103,7 +103,7 @@ export default class LlmNluHelper private findKeywordEntities( text: string, entity: NlpEntityFull, - ): Nlp.ParseEntity[] { + ): NLU.ParseEntity[] { return entity.values .flatMap(({ value, expressions }) => { const allValues = [value, ...expressions]; @@ -128,7 +128,7 @@ export default class LlmNluHelper .filter((v) => !!v); } - async predict(text: string): Promise { + async predict(text: string): Promise { const settings = await this.getSettings(); const helper = await this.helperService.getDefaultLlmHelper(); const defaultLanguage = await this.languageService.getDefaultLanguage(); @@ -143,7 +143,7 @@ export default class LlmNluHelper }, ); - const traits: Nlp.ParseEntity[] = [ + const traits: NLU.ParseEntity[] = [ { entity: 'language', value: language || defaultLanguage.code, diff --git a/api/src/helper/lib/base-nlp-helper.ts b/api/src/helper/lib/base-nlp-helper.ts index 90e17fb8..57c2f6cd 100644 --- a/api/src/helper/lib/base-nlp-helper.ts +++ b/api/src/helper/lib/base-nlp-helper.ts @@ -23,7 +23,7 @@ import { import { SettingService } from '@/setting/services/setting.service'; import { HelperService } from '../helper.service'; -import { HelperName, HelperType, Nlp } from '../types'; +import { HelperName, HelperType, NLU } from '../types'; import BaseHelper from './base-helper'; @@ -163,7 +163,7 @@ export default abstract class BaseNlpHelper< filterEntitiesByConfidence?( nlp: any, threshold: boolean, - ): Promise; + ): Promise; /** * Returns only the entities that have strong confidence (> than the threshold), can return an empty result @@ -178,5 +178,5 @@ export default abstract class BaseNlpHelper< text: string, threshold?: boolean, project?: string, - ): Promise; + ): Promise; } diff --git a/api/src/helper/types.ts b/api/src/helper/types.ts index 969120e9..055d18e0 100644 --- a/api/src/helper/types.ts +++ b/api/src/helper/types.ts @@ -13,7 +13,7 @@ import BaseHelper from './lib/base-helper'; import BaseLlmHelper from './lib/base-llm-helper'; import BaseNlpHelper from './lib/base-nlp-helper'; -export namespace Nlp { +export namespace NLU { export interface ParseEntity { entity: string; // Entity name value: string; // Value name diff --git a/api/src/utils/test/mocks/nlp.ts b/api/src/utils/test/mocks/nlp.ts index 5fb31634..04a6e0bd 100644 --- a/api/src/utils/test/mocks/nlp.ts +++ b/api/src/utils/test/mocks/nlp.ts @@ -6,9 +6,9 @@ * 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). */ -import { Nlp } from '@/helper/types'; +import { NLU } from '@/helper/types'; -export const nlpEntitiesGreeting: Nlp.ParseEntities = { +export const nlpEntitiesGreeting: NLU.ParseEntities = { entities: [ { entity: 'intent',