From 6e8a2a3d48941691d9bd4dcd41007ef6e881c867 Mon Sep 17 00:00:00 2001 From: medtaher Date: Mon, 23 Sep 2024 00:27:15 +0100 Subject: [PATCH 01/10] feat: add permanent option context var (unit tests fail) --- api/src/chat/schemas/context-var.schema.ts | 6 +++ api/src/chat/schemas/subscriber.schema.ts | 7 ++++ .../chat/schemas/types/subscriberContext.ts | 3 ++ api/src/chat/services/conversation.service.ts | 37 +++++++++++++++++-- 4 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 api/src/chat/schemas/types/subscriberContext.ts diff --git a/api/src/chat/schemas/context-var.schema.ts b/api/src/chat/schemas/context-var.schema.ts index 4757391c..60fcc7c8 100644 --- a/api/src/chat/schemas/context-var.schema.ts +++ b/api/src/chat/schemas/context-var.schema.ts @@ -28,6 +28,12 @@ export class ContextVar extends BaseSchema { match: /^[a-z_0-9]+$/, }) name: string; + + @Prop({ + type: Boolean, + default: false, + }) + permanent?: boolean; } export const ContextVarModel: ModelDefinition = { diff --git a/api/src/chat/schemas/subscriber.schema.ts b/api/src/chat/schemas/subscriber.schema.ts index 1cee129d..f0e451b4 100644 --- a/api/src/chat/schemas/subscriber.schema.ts +++ b/api/src/chat/schemas/subscriber.schema.ts @@ -19,6 +19,7 @@ import { TFilterPopulateFields } from '@/utils/types/filter.types'; import { Label } from './label.schema'; import { ChannelData } from './types/channel'; +import { SubscriberContext } from './types/subscriberContext'; @Schema({ timestamps: true }) export class SubscriberStub extends BaseSchema { @@ -107,6 +108,12 @@ export class SubscriberStub extends BaseSchema { default: null, }) avatar?: unknown; + + @Prop({ + type: Object, + default: { vars: {} }, //TODO: add this to the migration + }) + context?: SubscriberContext; } @Schema({ timestamps: true }) diff --git a/api/src/chat/schemas/types/subscriberContext.ts b/api/src/chat/schemas/types/subscriberContext.ts new file mode 100644 index 00000000..18108d3b --- /dev/null +++ b/api/src/chat/schemas/types/subscriberContext.ts @@ -0,0 +1,3 @@ +export interface SubscriberContext { + [key: string]: any; +} diff --git a/api/src/chat/services/conversation.service.ts b/api/src/chat/services/conversation.service.ts index ecf45fc5..319d31e2 100644 --- a/api/src/chat/services/conversation.service.ts +++ b/api/src/chat/services/conversation.service.ts @@ -7,16 +7,19 @@ * 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 { Injectable, Logger } from '@nestjs/common'; import { FilterQuery } from 'mongoose'; import EventWrapper from '@/channel/lib/EventWrapper'; import { LoggerService } from '@/logger/logger.service'; import { BaseService } from '@/utils/generics/base-service'; +import { ContextVarService } from './context-var.service'; +import { SubscriberService } from './subscriber.service'; import { VIEW_MORE_PAYLOAD } from '../helpers/constants'; import { ConversationRepository } from '../repositories/conversation.repository'; import { Block, BlockFull } from '../schemas/block.schema'; +import { ContextVar } from '../schemas/context-var.schema'; import { Conversation, ConversationFull, @@ -34,6 +37,8 @@ export class ConversationService extends BaseService< constructor( readonly repository: ConversationRepository, private readonly logger: LoggerService, + private readonly contextVarService: ContextVarService, + private readonly subscriberService: SubscriberService, ) { super(repository); } @@ -79,6 +84,7 @@ export class ConversationService extends BaseService< captureVars: boolean = false, ) { const msgType = event.getMessageType(); + const profile = event.getSender(); // Capture channel specific context data convo.context.channel = event.getHandler().getChannel(); convo.context.text = event.getText(); @@ -86,6 +92,18 @@ export class ConversationService extends BaseService< convo.context.nlp = event.getNLP(); convo.context.vars = convo.context.vars || {}; + const contextVars = ( + await this.contextVarService.find({ + name: { $in: next.capture_vars.map((c) => c.context_var) }, + }) + ).reduce( + (acc, cv) => { + acc[cv.name] = cv; + return acc; + }, + {} as Record, + ); + // Capture user entry in context vars if (captureVars && next.capture_vars && next.capture_vars.length > 0) { next.capture_vars.forEach((capture) => { @@ -121,12 +139,18 @@ export class ConversationService extends BaseService< contextValue = typeof contextValue === 'string' ? contextValue.trim() : contextValue; - convo.context.vars[capture.context_var] = contextValue; + if (contextVars[capture.context_var]?.permanent) { + Logger.debug( + `Adding context var to subscriber: ${capture.context_var} = ${contextValue}`, + ); + profile.context.vars[capture.context_var] = contextValue; + } else { + convo.context.vars[capture.context_var] = contextValue; + } }); } // Store user infos - const profile = event.getSender(); if (profile) { // @ts-expect-error : id needs to remain readonly convo.context.user.id = profile.id; @@ -182,6 +206,13 @@ export class ConversationService extends BaseService< 'Conversation Model : No conversation has been updated', ); } + + //TODO: add check if nothing changed don't update + + this.subscriberService.updateOne(convo.sender, { + context: profile.context, + }); + return updatedConversation; } catch (err) { this.logger.error('Conversation Model : Unable to store context', err); From 92003c471eaa7061de2f0ebc186e16a4ccfce280 Mon Sep 17 00:00:00 2001 From: medtaher Date: Mon, 23 Sep 2024 14:54:16 +0100 Subject: [PATCH 02/10] refactor: move getContextVarsByBlock to ContextVarService --- api/src/chat/services/context-var.service.ts | 14 ++++++++++++++ api/src/chat/services/conversation.service.ts | 14 ++------------ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/api/src/chat/services/context-var.service.ts b/api/src/chat/services/context-var.service.ts index a347bb9b..00578aff 100644 --- a/api/src/chat/services/context-var.service.ts +++ b/api/src/chat/services/context-var.service.ts @@ -12,6 +12,7 @@ import { Injectable } from '@nestjs/common'; import { BaseService } from '@/utils/generics/base-service'; import { ContextVarRepository } from '../repositories/context-var.repository'; +import { Block, BlockFull } from '../schemas/block.schema'; import { ContextVar } from '../schemas/context-var.schema'; @Injectable() @@ -19,4 +20,17 @@ export class ContextVarService extends BaseService { constructor(readonly repository: ContextVarRepository) { super(repository); } + + async getContextVarsByBlock( + block: Block | BlockFull, + ): Promise> { + return ( + await this.find({ + name: { $in: block.capture_vars.map((cv) => cv.context_var) }, + }) + ).reduce((acc, cv) => { + acc[cv.name] = cv; + return acc; + }, {}); + } } diff --git a/api/src/chat/services/conversation.service.ts b/api/src/chat/services/conversation.service.ts index 319d31e2..9fde8b25 100644 --- a/api/src/chat/services/conversation.service.ts +++ b/api/src/chat/services/conversation.service.ts @@ -19,7 +19,6 @@ import { SubscriberService } from './subscriber.service'; import { VIEW_MORE_PAYLOAD } from '../helpers/constants'; import { ConversationRepository } from '../repositories/conversation.repository'; import { Block, BlockFull } from '../schemas/block.schema'; -import { ContextVar } from '../schemas/context-var.schema'; import { Conversation, ConversationFull, @@ -92,17 +91,8 @@ export class ConversationService extends BaseService< convo.context.nlp = event.getNLP(); convo.context.vars = convo.context.vars || {}; - const contextVars = ( - await this.contextVarService.find({ - name: { $in: next.capture_vars.map((c) => c.context_var) }, - }) - ).reduce( - (acc, cv) => { - acc[cv.name] = cv; - return acc; - }, - {} as Record, - ); + const contextVars = + await this.contextVarService.getContextVarsByBlock(next); // Capture user entry in context vars if (captureVars && next.capture_vars && next.capture_vars.length > 0) { From 982200cc3a2dfbe96a9d493b56c1b5cc66987e6f Mon Sep 17 00:00:00 2001 From: medtaher Date: Mon, 23 Sep 2024 23:55:58 +0100 Subject: [PATCH 03/10] fix: fix old unit tests --- api/src/chat/controllers/context-var.controller.spec.ts | 1 + api/src/chat/dto/context-var.dto.ts | 3 +++ api/src/chat/services/bot.service.spec.ts | 6 ++++++ api/src/utils/test/fixtures/contextvar.ts | 2 ++ api/src/utils/test/fixtures/subscriber.ts | 3 +++ 5 files changed, 15 insertions(+) diff --git a/api/src/chat/controllers/context-var.controller.spec.ts b/api/src/chat/controllers/context-var.controller.spec.ts index 77aadde6..a06eb4a0 100644 --- a/api/src/chat/controllers/context-var.controller.spec.ts +++ b/api/src/chat/controllers/context-var.controller.spec.ts @@ -100,6 +100,7 @@ describe('ContextVarController', () => { const contextVarCreateDto: ContextVarCreateDto = { label: 'contextVarLabel2', name: 'test_add', + permanent: false, }; const result = await contextVarController.create(contextVarCreateDto); diff --git a/api/src/chat/dto/context-var.dto.ts b/api/src/chat/dto/context-var.dto.ts index 715efd89..3819f5c2 100644 --- a/api/src/chat/dto/context-var.dto.ts +++ b/api/src/chat/dto/context-var.dto.ts @@ -20,6 +20,9 @@ export class ContextVarCreateDto { @IsNotEmpty() @IsString() name: string; + + @ApiProperty({ description: 'Is context var permanent', type: Boolean }) + permanent?: boolean; } export class ContextVarUpdateDto extends PartialType(ContextVarCreateDto) {} diff --git a/api/src/chat/services/bot.service.spec.ts b/api/src/chat/services/bot.service.spec.ts index 3625a1a9..6f6f8f5a 100644 --- a/api/src/chat/services/bot.service.spec.ts +++ b/api/src/chat/services/bot.service.spec.ts @@ -59,15 +59,18 @@ import { CategoryRepository } from './../repositories/category.repository'; import { BlockService } from './block.service'; import { BotService } from './bot.service'; import { CategoryService } from './category.service'; +import { ContextVarService } from './context-var.service'; import { ConversationService } from './conversation.service'; import { MessageService } from './message.service'; import { SubscriberService } from './subscriber.service'; import { BlockRepository } from '../repositories/block.repository'; +import { ContextVarRepository } from '../repositories/context-var.repository'; import { ConversationRepository } from '../repositories/conversation.repository'; import { MessageRepository } from '../repositories/message.repository'; import { SubscriberRepository } from '../repositories/subscriber.repository'; import { BlockFull, BlockModel } from '../schemas/block.schema'; import { CategoryModel } from '../schemas/category.schema'; +import { ContextVarModel } from '../schemas/context-var.schema'; import { Conversation, ConversationFull, @@ -107,6 +110,7 @@ describe('BlockService', () => { NlpEntityModel, NlpSampleEntityModel, NlpSampleModel, + ContextVarModel, ]), ], providers: [ @@ -143,6 +147,8 @@ describe('BlockService', () => { NlpSampleEntityService, NlpSampleService, NlpService, + ContextVarService, + ContextVarRepository, { provide: PluginService, useValue: {}, diff --git a/api/src/utils/test/fixtures/contextvar.ts b/api/src/utils/test/fixtures/contextvar.ts index a3ffc896..4ccea4bb 100644 --- a/api/src/utils/test/fixtures/contextvar.ts +++ b/api/src/utils/test/fixtures/contextvar.ts @@ -18,10 +18,12 @@ const contextVars: ContextVarCreateDto[] = [ { label: 'test context var 1', name: 'test1', + permanent: false, }, { label: 'test context var 2', name: 'test2', + permanent: false, }, ]; diff --git a/api/src/utils/test/fixtures/subscriber.ts b/api/src/utils/test/fixtures/subscriber.ts index a45e41b9..04aecd65 100644 --- a/api/src/utils/test/fixtures/subscriber.ts +++ b/api/src/utils/test/fixtures/subscriber.ts @@ -91,6 +91,9 @@ export const subscriberDefaultValues: TFixturesDefaultValues = { lastvisit: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), retainedFrom: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), avatar: null, + /*context: { + vars: {}, + }*/ // TODO: add unit tests taking into account the context field }; export const subscriberFixtures = getFixturesWithDefaultValues({ From 5d19889a7af8de7903584a077f4701cc444f7f3f Mon Sep 17 00:00:00 2001 From: medtaher Date: Tue, 24 Sep 2024 11:18:47 +0100 Subject: [PATCH 04/10] feat: relace tokens with contextVars --- api/src/chat/services/block.service.spec.ts | 23 +++++++- api/src/chat/services/block.service.ts | 62 +++++++++++++++------ api/src/chat/services/bot.service.ts | 1 + 3 files changed, 67 insertions(+), 19 deletions(-) diff --git a/api/src/chat/services/block.service.spec.ts b/api/src/chat/services/block.service.spec.ts index 43af4afe..806c5d1b 100644 --- a/api/src/chat/services/block.service.spec.ts +++ b/api/src/chat/services/block.service.spec.ts @@ -65,6 +65,7 @@ import { LabelModel } from '../schemas/label.schema'; import { FileType } from '../schemas/types/attachment'; import { Context } from '../schemas/types/context'; import { PayloadType, StdOutgoingListMessage } from '../schemas/types/message'; +import { SubscriberContext } from '../schemas/types/subscriberContext'; describe('BlockService', () => { let blockRepository: BlockRepository; @@ -421,6 +422,7 @@ describe('BlockService', () => { ...contextBlankInstance, skip: { [blockProductListMock.id]: 0 }, }, + { vars: {} }, //TODO: to correct false, 'conv_id', ); @@ -454,6 +456,7 @@ describe('BlockService', () => { ...contextBlankInstance, skip: { [blockProductListMock.id]: 2 }, }, + { vars: {} }, //TODO: to correct false, 'conv_id', ); @@ -498,9 +501,19 @@ describe('BlockService', () => { skip: { '1': 0 }, attempt: 0, }; + const subscriberContext: SubscriberContext = { + vars: { + phone: '123456789', + }, + }; it('should process empty text', () => { - const result = blockService.processText('', context, settings); + const result = blockService.processText( + '', + context, + subscriberContext, + settings, + ); expect(result).toEqual(''); }); @@ -509,6 +522,7 @@ describe('BlockService', () => { const result = blockService.processText( translation.en, context, + subscriberContext, settings, ); expect(result).toEqual(translation.fr); @@ -518,6 +532,7 @@ describe('BlockService', () => { const result = blockService.processText( '{context.user.first_name} {context.user.last_name}, email : {context.vars.email}', contextEmailVarInstance, + subscriberContext, settings, ); expect(result).toEqual('John Doe, email : email@example.com'); @@ -525,17 +540,19 @@ describe('BlockService', () => { it('should process text replacements with context vars', () => { const result = blockService.processText( - '{context.user.first_name} {context.user.last_name}, email : {context.vars.email}', + '{context.user.first_name} {context.user.last_name}, phone : {context.vars.phone}', contextEmailVarInstance, + subscriberContext, settings, ); - expect(result).toEqual('John Doe, email : email@example.com'); + expect(result).toEqual('John Doe, phone : 123456789'); }); it('should process text replacements with settings contact infos', () => { const result = blockService.processText( 'Trying the settings : the name of company is <<{contact.company_name}>>', contextBlankInstance, + subscriberContext, settings, ); expect(result).toEqual( diff --git a/api/src/chat/services/block.service.ts b/api/src/chat/services/block.service.ts index 91e0f07b..af85e15f 100644 --- a/api/src/chat/services/block.service.ts +++ b/api/src/chat/services/block.service.ts @@ -33,6 +33,7 @@ import { } from '../schemas/types/message'; import { NlpPattern, Pattern, PayloadPattern } from '../schemas/types/pattern'; import { Payload, StdQuickReply } from '../schemas/types/quick-reply'; +import { SubscriberContext } from '../schemas/types/subscriberContext'; @Injectable() export class BlockService extends BaseService { @@ -301,22 +302,19 @@ export class BlockService extends BaseService { processTokenReplacements( text: string, context: Context, + subscriberContext: SubscriberContext, settings: Settings, ): string { + const vars = { ...subscriberContext.vars, ...context.vars }; // Replace context tokens with their values - Object.keys(context.vars || {}).forEach((key) => { - if ( - typeof context.vars[key] === 'string' && - context.vars[key].indexOf(':') !== -1 - ) { - const tmp = context.vars[key].split(':'); - context.vars[key] = tmp[1]; + Object.keys(vars).forEach((key) => { + if (typeof vars[key] === 'string' && vars[key].indexOf(':') !== -1) { + const tmp = vars[key].split(':'); + vars[key] = tmp[1]; } text = text.replace( '{context.vars.' + key + '}', - typeof context.vars[key] === 'string' - ? context.vars[key] - : JSON.stringify(context.vars[key]), + typeof vars[key] === 'string' ? vars[key] : JSON.stringify(vars[key]), ); }); @@ -368,7 +366,12 @@ export class BlockService extends BaseService { * * @returns The text message translated and tokens being replaces with values */ - processText(text: string, context: Context, settings: Settings): string { + processText( + text: string, + context: Context, + subscriberContext: SubscriberContext, + settings: Settings, + ): string { const lang = context && context.user && context.user.language ? context.user.language @@ -376,7 +379,12 @@ export class BlockService extends BaseService { // Translate text = this.i18n.t(text, { lang, defaultValue: text }); // Replace context tokens - text = this.processTokenReplacements(text, context, settings); + text = this.processTokenReplacements( + text, + context, + subscriberContext, + settings, + ); return text; } @@ -423,6 +431,7 @@ export class BlockService extends BaseService { async processMessage( block: Block | BlockFull, context: Context, + subscriberContext: SubscriberContext, fallback = false, conversationId?: string, ): Promise { @@ -440,6 +449,7 @@ export class BlockService extends BaseService { const text = this.processText( this.getRandom(blockMessage), context, + subscriberContext, settings, ); const envelope: StdOutgoingEnvelope = { @@ -456,12 +466,22 @@ export class BlockService extends BaseService { const envelope: StdOutgoingEnvelope = { format: OutgoingMessageFormat.quickReplies, message: { - text: this.processText(blockMessage.text, context, settings), + text: this.processText( + blockMessage.text, + context, + subscriberContext, + settings, + ), quickReplies: blockMessage.quickReplies.map((qr: StdQuickReply) => { return qr.title ? { ...qr, - title: this.processText(qr.title, context, settings), + title: this.processText( + qr.title, + context, + subscriberContext, + settings, + ), } : qr; }), @@ -476,12 +496,22 @@ export class BlockService extends BaseService { const envelope: StdOutgoingEnvelope = { format: OutgoingMessageFormat.buttons, message: { - text: this.processText(blockMessage.text, context, settings), + text: this.processText( + blockMessage.text, + context, + subscriberContext, + settings, + ), buttons: blockMessage.buttons.map((btn) => { return btn.title ? { ...btn, - title: this.processText(btn.title, context, settings), + title: this.processText( + btn.title, + context, + subscriberContext, + settings, + ), } : btn; }), diff --git a/api/src/chat/services/bot.service.ts b/api/src/chat/services/bot.service.ts index bdd07650..9a8ca91b 100644 --- a/api/src/chat/services/bot.service.ts +++ b/api/src/chat/services/bot.service.ts @@ -74,6 +74,7 @@ export class BotService { await this.blockService.processMessage( block, context, + event.getSender().context, fallback, conservationId, ); From d7838951e5662714f1ae445da03a387e7f1737da Mon Sep 17 00:00:00 2001 From: medtaher Date: Tue, 24 Sep 2024 14:54:23 +0100 Subject: [PATCH 05/10] refactor: refactor spec code --- api/src/chat/services/block.service.spec.ts | 6 ++++-- api/src/utils/test/mocks/conversation.ts | 5 +++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/api/src/chat/services/block.service.spec.ts b/api/src/chat/services/block.service.spec.ts index 806c5d1b..b5f152f4 100644 --- a/api/src/chat/services/block.service.spec.ts +++ b/api/src/chat/services/block.service.spec.ts @@ -48,6 +48,7 @@ import { contextBlankInstance, contextEmailVarInstance, contextGetStartedInstance, + subscriberContextBlankInstance, } from '@/utils/test/mocks/conversation'; import { nlpEntitiesGreeting } from '@/utils/test/mocks/nlp'; import { @@ -422,7 +423,7 @@ describe('BlockService', () => { ...contextBlankInstance, skip: { [blockProductListMock.id]: 0 }, }, - { vars: {} }, //TODO: to correct + subscriberContextBlankInstance, false, 'conv_id', ); @@ -456,7 +457,7 @@ describe('BlockService', () => { ...contextBlankInstance, skip: { [blockProductListMock.id]: 2 }, }, - { vars: {} }, //TODO: to correct + subscriberContextBlankInstance, false, 'conv_id', ); @@ -502,6 +503,7 @@ describe('BlockService', () => { attempt: 0, }; const subscriberContext: SubscriberContext = { + ...subscriberContextBlankInstance, vars: { phone: '123456789', }, diff --git a/api/src/utils/test/mocks/conversation.ts b/api/src/utils/test/mocks/conversation.ts index c08fbbf4..7063f7d8 100644 --- a/api/src/utils/test/mocks/conversation.ts +++ b/api/src/utils/test/mocks/conversation.ts @@ -10,6 +10,7 @@ import { Block, BlockStub } from '@/chat/schemas/block.schema'; import { ConversationFull } from '@/chat/schemas/conversation.schema'; import { Context } from '@/chat/schemas/types/context'; +import { SubscriberContext } from '@/chat/schemas/types/subscriberContext'; import { quickRepliesBlock, textBlock } from './block'; import { modelInstance } from './misc'; @@ -30,6 +31,10 @@ export const contextBlankInstance: Context = { attempt: 1, }; +export const subscriberContextBlankInstance: SubscriberContext = { + vars: {}, +}; + export const contextEmailVarInstance: Context = { ...contextBlankInstance, vars: { From dec568cbfa6a401f9ae383d0234228825dd156d1 Mon Sep 17 00:00:00 2001 From: medtaher Date: Tue, 24 Sep 2024 17:40:14 +0100 Subject: [PATCH 06/10] feat: add permanent option in frontend --- .../src/components/context-vars/index.tsx | 32 ++++++++++++++++++- frontend/src/i18n/en/translation.json | 1 + frontend/src/i18n/fr/translation.json | 1 + frontend/src/types/context-var.types.ts | 2 ++ 4 files changed, 35 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/context-vars/index.tsx b/frontend/src/components/context-vars/index.tsx index 68a095f1..c6458f07 100644 --- a/frontend/src/components/context-vars/index.tsx +++ b/frontend/src/components/context-vars/index.tsx @@ -9,7 +9,7 @@ import { faAsterisk } from "@fortawesome/free-solid-svg-icons"; import AddIcon from "@mui/icons-material/Add"; -import { Button, Grid, Paper } from "@mui/material"; +import { Button, Grid, Paper, Switch } from "@mui/material"; import { GridColDef } from "@mui/x-data-grid"; import React from "react"; import { useTranslation } from "react-i18next"; @@ -24,6 +24,7 @@ import { renderHeader } from "@/app-components/tables/columns/renderHeader"; import { DataGrid } from "@/app-components/tables/DataGrid"; import { useDelete } from "@/hooks/crud/useDelete"; import { useFind } from "@/hooks/crud/useFind"; +import { useUpdate } from "@/hooks/crud/useUpdate"; import { getDisplayDialogs, useDialog } from "@/hooks/useDialog"; import { useHasPermission } from "@/hooks/useHasPermission"; import { useSearch } from "@/hooks/useSearch"; @@ -52,6 +53,14 @@ export const ContextVars = () => { params: searchPayload, }, ); + const { mutateAsync: updateContextVar } = useUpdate(EntityType.CONTEXT_VAR, { + onError: () => { + toast.error(t("message.internal_server_error")); + }, + onSuccess() { + toast.success(t("message.success_save")); + }, + }); const { mutateAsync: deleteContextVar } = useDelete(EntityType.CONTEXT_VAR, { onError: () => { toast.error(t("message.internal_server_error")); @@ -87,6 +96,27 @@ export const ContextVars = () => { renderHeader, headerAlign: "left", }, + { + maxWidth: 120, + field: "permanent", + headerName: t("label.permanent"), + disableColumnMenu: true, + renderHeader, + headerAlign: "left", + renderCell: (params) => ( + { + updateContextVar({ + id: params.row.id, + params: { permanent: !params.value }, + }); + }} + /> + ), + }, { maxWidth: 140, field: "createdAt", diff --git a/frontend/src/i18n/en/translation.json b/frontend/src/i18n/en/translation.json index f5a0dd08..e7d7855e 100644 --- a/frontend/src/i18n/en/translation.json +++ b/frontend/src/i18n/en/translation.json @@ -288,6 +288,7 @@ "assign_labels": "Assign labels", "replacement_tokens": "Replacement Tokens", "built_in": "Built-in", + "permanent": "Permanent", "assign_to": "Takeover By", "assigned_to": "Assigned To", "user_first_name": "First Name", diff --git a/frontend/src/i18n/fr/translation.json b/frontend/src/i18n/fr/translation.json index 180dddb6..2b012a34 100644 --- a/frontend/src/i18n/fr/translation.json +++ b/frontend/src/i18n/fr/translation.json @@ -289,6 +289,7 @@ "assign_labels": "Affecter des étiquettes", "replacement_tokens": "Jetons de remplacement", "built_in": "Intégré", + "permanent": "Permanent", "assign_to": "Assigner à", "assigned_to": "Assigné(e) à", "user_first_name": "Prénom", diff --git a/frontend/src/types/context-var.types.ts b/frontend/src/types/context-var.types.ts index fa388266..35b48a71 100644 --- a/frontend/src/types/context-var.types.ts +++ b/frontend/src/types/context-var.types.ts @@ -14,6 +14,7 @@ import { IBaseSchema, IFormat, OmitPopulate } from "./base.types"; export interface IContextVarAttributes { name: string; label: string; + permanent: boolean; } export interface IContextVarStub @@ -21,6 +22,7 @@ export interface IContextVarStub OmitPopulate { name: string; label: string; + permanent: boolean; } export interface IContextVar extends IContextVarStub, IFormat {} From 106045d2c0a89fa8755a54a24a6c91c0cec6d66d Mon Sep 17 00:00:00 2001 From: medtaher Date: Tue, 24 Sep 2024 18:09:14 +0100 Subject: [PATCH 07/10] refactor: remove comment --- api/src/utils/test/fixtures/subscriber.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/api/src/utils/test/fixtures/subscriber.ts b/api/src/utils/test/fixtures/subscriber.ts index 04aecd65..a45e41b9 100644 --- a/api/src/utils/test/fixtures/subscriber.ts +++ b/api/src/utils/test/fixtures/subscriber.ts @@ -91,9 +91,6 @@ export const subscriberDefaultValues: TFixturesDefaultValues = { lastvisit: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), retainedFrom: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), avatar: null, - /*context: { - vars: {}, - }*/ // TODO: add unit tests taking into account the context field }; export const subscriberFixtures = getFixturesWithDefaultValues({ From 7771969deb344d88469b80ecdf938884e41162a6 Mon Sep 17 00:00:00 2001 From: medtaher Date: Sat, 28 Sep 2024 15:13:29 +0100 Subject: [PATCH 08/10] docs: add context var docs --- api/src/chat/schemas/context-var.schema.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/api/src/chat/schemas/context-var.schema.ts b/api/src/chat/schemas/context-var.schema.ts index 60fcc7c8..9530f8d7 100644 --- a/api/src/chat/schemas/context-var.schema.ts +++ b/api/src/chat/schemas/context-var.schema.ts @@ -29,6 +29,11 @@ export class ContextVar extends BaseSchema { }) name: string; + /** + * The permanent attribute allows the chatbot to know where to store the context variable. + * If the context variable is not permanent, it will be stored in the converation context, which is temporary. + * If the context variable is permanent, it will be stored in the subscriber context, which is permanent. + */ @Prop({ type: Boolean, default: false, From 4248df0f67178f65ca32aa3d0e741e0126655215 Mon Sep 17 00:00:00 2001 From: medtaher Date: Sat, 28 Sep 2024 15:15:26 +0100 Subject: [PATCH 09/10] fix: add await in request --- api/src/chat/services/conversation.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/chat/services/conversation.service.ts b/api/src/chat/services/conversation.service.ts index 9fde8b25..dae0eea8 100644 --- a/api/src/chat/services/conversation.service.ts +++ b/api/src/chat/services/conversation.service.ts @@ -199,7 +199,7 @@ export class ConversationService extends BaseService< //TODO: add check if nothing changed don't update - this.subscriberService.updateOne(convo.sender, { + await this.subscriberService.updateOne(convo.sender, { context: profile.context, }); From 2ef011ed8e7f78d32aa7e524511d2dcc8c556154 Mon Sep 17 00:00:00 2001 From: Mohamed Marrouchi Date: Sun, 29 Sep 2024 09:33:41 +0100 Subject: [PATCH 10/10] fix: minor adjustments --- api/src/chat/dto/context-var.dto.ts | 4 ++- api/src/chat/services/bot.service.ts | 4 +-- api/src/chat/services/context-var.service.ts | 15 +++++++---- .../context-vars/ContextVarDialog.tsx | 25 +++++++++++++++++-- frontend/src/i18n/en/translation.json | 3 ++- frontend/src/i18n/fr/translation.json | 3 ++- frontend/src/types/context-var.types.ts | 6 +---- 7 files changed, 43 insertions(+), 17 deletions(-) diff --git a/api/src/chat/dto/context-var.dto.ts b/api/src/chat/dto/context-var.dto.ts index 3819f5c2..7bed0ac3 100644 --- a/api/src/chat/dto/context-var.dto.ts +++ b/api/src/chat/dto/context-var.dto.ts @@ -8,7 +8,7 @@ */ import { ApiProperty, PartialType } from '@nestjs/swagger'; -import { IsNotEmpty, IsString } from 'class-validator'; +import { IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator'; export class ContextVarCreateDto { @ApiProperty({ description: 'Context var label', type: String }) @@ -22,6 +22,8 @@ export class ContextVarCreateDto { name: string; @ApiProperty({ description: 'Is context var permanent', type: Boolean }) + @IsOptional() + @IsBoolean() permanent?: boolean; } diff --git a/api/src/chat/services/bot.service.ts b/api/src/chat/services/bot.service.ts index 9a8ca91b..01c4b635 100644 --- a/api/src/chat/services/bot.service.ts +++ b/api/src/chat/services/bot.service.ts @@ -70,11 +70,12 @@ export class BotService { event.getSenderForeignId(), ); // Process message : Replace tokens with context data and then send the message + const recipient = event.getSender(); const envelope: StdOutgoingEnvelope = await this.blockService.processMessage( block, context, - event.getSender().context, + recipient.context, fallback, conservationId, ); @@ -88,7 +89,6 @@ export class BotService { this.eventEmitter.emit('hook:stats:entry', 'all_messages', 'All Messages'); // Trigger sent message event - const recipient = event.getSender(); const sentMessage: MessageCreateDto = { mid: response && 'mid' in response ? response.mid : '', message: envelope.message, diff --git a/api/src/chat/services/context-var.service.ts b/api/src/chat/services/context-var.service.ts index 00578aff..99356b2e 100644 --- a/api/src/chat/services/context-var.service.ts +++ b/api/src/chat/services/context-var.service.ts @@ -21,14 +21,19 @@ export class ContextVarService extends BaseService { super(repository); } + /** + * Retrieves a mapping of context variable names to their corresponding `ContextVar` objects for a given block. + * + * @param {Block | BlockFull} block - The block containing the capture variables to retrieve context variables for. + * @returns {Promise>} A promise that resolves to a record mapping context variable names to `ContextVar` objects. + */ async getContextVarsByBlock( block: Block | BlockFull, ): Promise> { - return ( - await this.find({ - name: { $in: block.capture_vars.map((cv) => cv.context_var) }, - }) - ).reduce((acc, cv) => { + const vars = await this.find({ + name: { $in: block.capture_vars.map(({ context_var }) => context_var) }, + }); + return vars.reduce((acc, cv) => { acc[cv.name] = cv; return acc; }, {}); diff --git a/frontend/src/components/context-vars/ContextVarDialog.tsx b/frontend/src/components/context-vars/ContextVarDialog.tsx index 5a396187..3b973684 100644 --- a/frontend/src/components/context-vars/ContextVarDialog.tsx +++ b/frontend/src/components/context-vars/ContextVarDialog.tsx @@ -7,9 +7,16 @@ * 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 { Dialog, DialogActions, DialogContent } from "@mui/material"; +import { + Dialog, + DialogActions, + DialogContent, + FormControlLabel, + FormHelperText, + Switch, +} from "@mui/material"; import { FC, useEffect } from "react"; -import { useForm } from "react-hook-form"; +import { Controller, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import DialogButtons from "@/app-components/buttons/DialogButtons"; @@ -58,6 +65,7 @@ export const ContextVarDialog: FC = ({ setValue, handleSubmit, formState: { errors }, + control, } = useForm({ defaultValues: { name: data?.name || "", label: data?.label || "" }, }); @@ -129,6 +137,19 @@ export const ContextVarDialog: FC = ({ InputLabelProps={{ shrink: true }} /> + + ( + } + label={t("label.permanent")} + /> + )} + /> + {t("help.permanent")} + diff --git a/frontend/src/i18n/en/translation.json b/frontend/src/i18n/en/translation.json index a3b4a5ca..69fc736f 100644 --- a/frontend/src/i18n/en/translation.json +++ b/frontend/src/i18n/en/translation.json @@ -699,7 +699,8 @@ "supported_message_type_others": "Only generic template messages are supported.", "notification_type_regular": "Sound/Vibration", "notification_type_silent_push": "On-screen notification only", - "notification_type_no_push": "No notification" + "notification_type_no_push": "No notification", + "permanent": "When enabled, the variable value will be stored in the subscriber's profile and retained for future conversations." }, "charts": { "messages": "Messages", diff --git a/frontend/src/i18n/fr/translation.json b/frontend/src/i18n/fr/translation.json index 89c47a9e..a23bf751 100644 --- a/frontend/src/i18n/fr/translation.json +++ b/frontend/src/i18n/fr/translation.json @@ -697,7 +697,8 @@ "supported_message_type_others": "Seuls les messages de modèle générique sont pris en charge.", "notification_type_regular": "Son/Vibration", "notification_type_silent_push": "Notification à l'écran uniquement", - "notification_type_no_push": "Aucune notification" + "notification_type_no_push": "Aucune notification", + "permanent": "Lorsqu'elle est activée, cette variable sera stockée dans le profil de l'abonné(e) et conservée pour les futures conversations." }, "charts": { "messages": "Messages", diff --git a/frontend/src/types/context-var.types.ts b/frontend/src/types/context-var.types.ts index 35b48a71..0cf795ee 100644 --- a/frontend/src/types/context-var.types.ts +++ b/frontend/src/types/context-var.types.ts @@ -19,11 +19,7 @@ export interface IContextVarAttributes { export interface IContextVarStub extends IBaseSchema, - OmitPopulate { - name: string; - label: string; - permanent: boolean; -} + OmitPopulate {} export interface IContextVar extends IContextVarStub, IFormat {}