Skip to content

Commit

Permalink
Merge pull request #67 from Hexastack/48-request-context-vars-permane…
Browse files Browse the repository at this point in the history
…nt-option

Add permanent option context var
  • Loading branch information
marrouchi authored Sep 29, 2024
2 parents d47deeb + 2ef011e commit 2f2379d
Show file tree
Hide file tree
Showing 18 changed files with 216 additions and 33 deletions.
1 change: 1 addition & 0 deletions api/src/chat/controllers/context-var.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ describe('ContextVarController', () => {
const contextVarCreateDto: ContextVarCreateDto = {
label: 'contextVarLabel2',
name: 'test_add',
permanent: false,
};
const result = await contextVarController.create(contextVarCreateDto);

Expand Down
7 changes: 6 additions & 1 deletion api/src/chat/dto/context-var.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand All @@ -20,6 +20,11 @@ export class ContextVarCreateDto {
@IsNotEmpty()
@IsString()
name: string;

@ApiProperty({ description: 'Is context var permanent', type: Boolean })
@IsOptional()
@IsBoolean()
permanent?: boolean;
}

export class ContextVarUpdateDto extends PartialType(ContextVarCreateDto) {}
11 changes: 11 additions & 0 deletions api/src/chat/schemas/context-var.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@ export class ContextVar extends BaseSchema {
match: /^[a-z_0-9]+$/,
})
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,
})
permanent?: boolean;
}

export const ContextVarModel: ModelDefinition = {
Expand Down
7 changes: 7 additions & 0 deletions api/src/chat/schemas/subscriber.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 })
Expand Down
3 changes: 3 additions & 0 deletions api/src/chat/schemas/types/subscriberContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface SubscriberContext {
[key: string]: any;
}
25 changes: 22 additions & 3 deletions api/src/chat/services/block.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
contextBlankInstance,
contextEmailVarInstance,
contextGetStartedInstance,
subscriberContextBlankInstance,
} from '@/utils/test/mocks/conversation';
import { nlpEntitiesGreeting } from '@/utils/test/mocks/nlp';
import {
Expand All @@ -69,6 +70,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;
Expand Down Expand Up @@ -436,6 +438,7 @@ describe('BlockService', () => {
...contextBlankInstance,
skip: { [blockProductListMock.id]: 0 },
},
subscriberContextBlankInstance,
false,
'conv_id',
);
Expand Down Expand Up @@ -469,6 +472,7 @@ describe('BlockService', () => {
...contextBlankInstance,
skip: { [blockProductListMock.id]: 2 },
},
subscriberContextBlankInstance,
false,
'conv_id',
);
Expand Down Expand Up @@ -513,9 +517,20 @@ describe('BlockService', () => {
skip: { '1': 0 },
attempt: 0,
};
const subscriberContext: SubscriberContext = {
...subscriberContextBlankInstance,
vars: {
phone: '123456789',
},
};

it('should process empty text', () => {
const result = blockService.processText('', context, settings);
const result = blockService.processText(
'',
context,
subscriberContext,
settings,
);
expect(result).toEqual('');
});

Expand All @@ -524,6 +539,7 @@ describe('BlockService', () => {
const result = blockService.processText(
translation.en,
context,
subscriberContext,
settings,
);
expect(result).toEqual(translation.fr);
Expand All @@ -533,24 +549,27 @@ 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 protected]');
});

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 protected]');
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(
Expand Down
62 changes: 46 additions & 16 deletions api/src/chat/services/block.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,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<Block, BlockPopulate, BlockFull> {
Expand Down Expand Up @@ -300,22 +301,19 @@ export class BlockService extends BaseService<Block, BlockPopulate, BlockFull> {
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]),
);
});

Expand Down Expand Up @@ -367,14 +365,24 @@ export class BlockService extends BaseService<Block, BlockPopulate, BlockFull> {
*
* @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 {
// Translate
text = this.i18n.t(text, {
lang: context.user.language,
defaultValue: text,
});
// Replace context tokens
text = this.processTokenReplacements(text, context, settings);
text = this.processTokenReplacements(
text,
context,
subscriberContext,
settings,
);
return text;
}

Expand Down Expand Up @@ -421,6 +429,7 @@ export class BlockService extends BaseService<Block, BlockPopulate, BlockFull> {
async processMessage(
block: Block | BlockFull,
context: Context,
subscriberContext: SubscriberContext,
fallback = false,
conversationId?: string,
): Promise<StdOutgoingEnvelope> {
Expand All @@ -438,6 +447,7 @@ export class BlockService extends BaseService<Block, BlockPopulate, BlockFull> {
const text = this.processText(
this.getRandom(blockMessage),
context,
subscriberContext,
settings,
);
const envelope: StdOutgoingEnvelope = {
Expand All @@ -454,12 +464,22 @@ export class BlockService extends BaseService<Block, BlockPopulate, BlockFull> {
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;
}),
Expand All @@ -474,12 +494,22 @@ export class BlockService extends BaseService<Block, BlockPopulate, BlockFull> {
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;
}),
Expand Down
6 changes: 6 additions & 0 deletions api/src/chat/services/bot.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,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,
Expand Down Expand Up @@ -110,6 +113,7 @@ describe('BlockService', () => {
NlpEntityModel,
NlpSampleEntityModel,
NlpSampleModel,
ContextVarModel,
LanguageModel,
]),
],
Expand Down Expand Up @@ -148,6 +152,8 @@ describe('BlockService', () => {
NlpSampleEntityService,
NlpSampleService,
NlpService,
ContextVarService,
ContextVarRepository,
LanguageService,
{
provide: PluginService,
Expand Down
3 changes: 2 additions & 1 deletion api/src/chat/services/bot.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +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,
recipient.context,
fallback,
conservationId,
);
Expand All @@ -87,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,
Expand Down
19 changes: 19 additions & 0 deletions api/src/chat/services/context-var.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,30 @@ 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()
export class ContextVarService extends BaseService<ContextVar> {
constructor(readonly repository: ContextVarRepository) {
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<Record<string, ContextVar>>} A promise that resolves to a record mapping context variable names to `ContextVar` objects.
*/
async getContextVarsByBlock(
block: Block | BlockFull,
): Promise<Record<string, ContextVar>> {
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;
}, {});
}
}
Loading

0 comments on commit 2f2379d

Please sign in to comment.