diff --git a/api/src/nlp/controllers/nlp-value.controller.ts b/api/src/nlp/controllers/nlp-value.controller.ts index a8c3c6be..42cb334f 100644 --- a/api/src/nlp/controllers/nlp-value.controller.ts +++ b/api/src/nlp/controllers/nlp-value.controller.ts @@ -125,6 +125,22 @@ export class NlpValueController extends BaseController< return doc; } + @Get('') + async findAndPopulateNlpValuesWithCount( + @Query(PageQueryPipe) pageQuery: PageQueryDto, + @Query(PopulatePipe) populate: string[], + @Query( + new SearchFilterPipe({ allowedFields: ['entity', 'value'] }), + ) + filters?: TFilterQuery, + ) { + return await this.nlpValueService.findAndPopulateNlpValuesWithCount( + populate, + filters, + pageQuery, + ); + } + /** * Retrieves a paginated list of NLP values. * @@ -136,7 +152,7 @@ export class NlpValueController extends BaseController< * * @returns A promise resolving to a paginated list of NLP values. */ - @Get() + // @Get('') disabled async findPage( @Query(PageQueryPipe) pageQuery: PageQueryDto, @Query(PopulatePipe) populate: string[], diff --git a/api/src/nlp/services/nlp-value.service.ts b/api/src/nlp/services/nlp-value.service.ts index f246ad61..c9ec6309 100644 --- a/api/src/nlp/services/nlp-value.service.ts +++ b/api/src/nlp/services/nlp-value.service.ts @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Hexastack. All rights reserved. + * Copyright © 2025 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. @@ -7,11 +7,15 @@ */ import { forwardRef, Inject, Injectable } from '@nestjs/common'; +import { Types } from 'mongoose'; import { DeleteResult } from '@/utils/generics/base-repository'; import { BaseService } from '@/utils/generics/base-service'; +import { PageQueryDto } from '@/utils/pagination/pagination-query.dto'; +import { TFilterQuery } from '@/utils/types/filter.types'; import { NlpValueCreateDto, NlpValueDto } from '../dto/nlp-value.dto'; +import { NlpSampleEntityRepository } from '../repositories/nlp-sample-entity.repository'; import { NlpValueRepository } from '../repositories/nlp-value.repository'; import { NlpEntity } from '../schemas/nlp-entity.schema'; import { @@ -34,6 +38,7 @@ export class NlpValueService extends BaseService< readonly repository: NlpValueRepository, @Inject(forwardRef(() => NlpEntityService)) private readonly nlpEntityService: NlpEntityService, + private readonly nlpSampleEntityRepository: NlpSampleEntityRepository, ) { super(repository); } @@ -218,4 +223,121 @@ export class NlpValueService extends BaseService< }); return Promise.all(promises); } + + async findAndPopulateNlpValuesWithCount( + populate: string[], + filters?: TFilterQuery, + pageQuery?: PageQueryDto, + ) { + const { $and = [], ...rest } = filters || ({} as TFilterQuery); + + const entityValueModel = this.getRepository().model; + + return entityValueModel + .aggregate([ + { + // support filters + $match: { + ...rest, + ...($and?.length && { + $and: + $and?.map((v) => { + if (v.entity) { + return { + ...v, + entity: new Types.ObjectId(String(v.entity)), + }; + } + + return v; + }) || [], + }), + }, + }, + // support pageQuery + { + $skip: pageQuery?.skip || 0, + }, + { + $limit: pageQuery?.limit || 10, + }, + { + $sort: { + [pageQuery?.sort?.[0] || 'createdAt']: + pageQuery?.sort?.[1] === 'desc' ? -1 : 1, + }, + }, + { + $lookup: { + from: 'nlpsampleentities', + localField: '_id', + foreignField: 'value', + as: 'sampleEntities', + }, + }, + { + $unwind: { + path: '$sampleEntities', + preserveNullAndEmptyArrays: true, + }, + }, + { + $lookup: { + from: 'nlpsamples', + localField: 'sampleEntities.sample', + foreignField: '_id', + as: 'samples', + }, + }, + { + $lookup: { + from: 'nlpentities', + localField: 'entity', + foreignField: '_id', + as: 'entities', + }, + }, + { + $group: { + _id: '$_id', + value: { $first: '$value' }, + expressions: { $first: '$expressions' }, + builtin: { $first: '$builtin' }, + metadata: { $first: '$metadata' }, + createdAt: { $first: '$createdAt' }, + updatedAt: { $first: '$updatedAt' }, + entity: { + // support populate + $first: populate.some((p) => + this.getRepository() + .getPopulate() + .map((p) => p.toString()) + .includes(p), + ) + ? '$entities' + : '$entity', + }, + //TODO when samples is empty array we need to return 0 not 1 + nlpSamplesCount: { + $sum: { $cond: [{ $ifNull: ['samples', false] }, 1, 0] }, + }, + }, + }, + { + $project: { + id: '$_id', + _id: 0, + value: 1, + expressions: 1, + builtin: 1, + entity: 1, + metadata: 1, + createdAt: 1, + updatedAt: 1, + nlpSamplesCount: 1, + }, + }, + ]) + .exec(); + } } diff --git a/frontend/public/locales/en/translation.json b/frontend/public/locales/en/translation.json index 93781e15..b243c3ae 100644 --- a/frontend/public/locales/en/translation.json +++ b/frontend/public/locales/en/translation.json @@ -334,6 +334,7 @@ "nlp": "NLU", "nlp_entity": "Entity", "nlp_entity_value": "Value", + "nlp_samples_count": "Nlp Samples count", "value": "Value", "synonyms": "Synonyms", "lookups": "Lookups", diff --git a/frontend/public/locales/fr/translation.json b/frontend/public/locales/fr/translation.json index 208dfc25..cd32248c 100644 --- a/frontend/public/locales/fr/translation.json +++ b/frontend/public/locales/fr/translation.json @@ -334,6 +334,7 @@ "nlp": "NLU", "nlp_entity": "Entité NLU", "nlp_entity_value": "Valeur NLU", + "nlp_samples_count": "Nombre des échantillons", "value": "Valeur", "lookups": "Stratégies", "lookup_strategies": "Stratégie de recherche", diff --git a/frontend/src/components/nlp/components/NlpValue.tsx b/frontend/src/components/nlp/components/NlpValue.tsx index 12c462e8..92cc7d04 100644 --- a/frontend/src/components/nlp/components/NlpValue.tsx +++ b/frontend/src/components/nlp/components/NlpValue.tsx @@ -55,7 +55,7 @@ export const NlpValues = ({ entityId }: { entityId: string }) => { const canHaveSynonyms = nlpEntity?.lookups?.[0] === NlpLookups.keywords; const { onSearch, searchPayload } = useSearch({ $eq: [{ entity: entityId }], - $or: ["doc", "value"] + $or: ["doc", "value"], }); const { dataGridProps } = useFind( { entity: EntityType.NLP_VALUE }, @@ -103,7 +103,7 @@ export const NlpValues = ({ entityId }: { entityId: string }) => { ], t("label.operations"), ); - const synonymsColumn = { + const synonymsColumn = { flex: 3, field: "synonyms", headerName: t("label.synonyms"), @@ -125,6 +125,21 @@ export const NlpValues = ({ entityId }: { entityId: string }) => { disableColumnMenu: true, renderHeader, }, + { + flex: 3, + field: "nlpSamplesCount", + headerName: t("label.nlp_samples_count"), + sortable: true, + disableColumnMenu: true, + renderHeader, + renderCell: ({ row }) => ( + + ), + }, { flex: 3, field: "doc", diff --git a/frontend/src/types/nlp-value.types.ts b/frontend/src/types/nlp-value.types.ts index 7b7a1e5e..4986ee8d 100644 --- a/frontend/src/types/nlp-value.types.ts +++ b/frontend/src/types/nlp-value.types.ts @@ -19,6 +19,7 @@ export interface INlpValueAttributes { expressions?: string[]; metadata?: Record; builtin?: boolean; + nlpSamplesCount?: number; } export interface INlpValueStub extends IBaseSchema, INlpValueAttributes {}