Skip to content

Feat : Highlight triggered and errored blocks #896

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 10 commits into from
Closed
6 changes: 6 additions & 0 deletions api/src/chat/controllers/block.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,15 @@ import { BlockCreateDto, BlockUpdateDto } from '../dto/block.dto';
import { BlockRepository } from '../repositories/block.repository';
import { CategoryRepository } from '../repositories/category.repository';
import { LabelRepository } from '../repositories/label.repository';
import { SubscriberRepository } from '../repositories/subscriber.repository';
import { Block, BlockModel } from '../schemas/block.schema';
import { LabelModel } from '../schemas/label.schema';
import { SubscriberModel } from '../schemas/subscriber.schema';
import { PayloadType } from '../schemas/types/button';
import { BlockService } from '../services/block.service';
import { CategoryService } from '../services/category.service';
import { LabelService } from '../services/label.service';
import { SubscriberService } from '../services/subscriber.service';

import { Category, CategoryModel } from './../schemas/category.schema';
import { BlockController } from './block.controller';
Expand Down Expand Up @@ -93,6 +96,7 @@ describe('BlockController', () => {
RoleModel,
PermissionModel,
LanguageModel,
SubscriberModel,
]),
],
providers: [
Expand All @@ -116,6 +120,8 @@ describe('BlockController', () => {
PermissionService,
LanguageService,
PluginService,
SubscriberService,
SubscriberRepository,
{
provide: I18nService,
useValue: {
Expand Down
43 changes: 43 additions & 0 deletions api/src/chat/controllers/subscriber.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,19 @@
* 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 { CACHE_MANAGER, CacheModule } from '@nestjs/cache-manager';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { MongooseModule } from '@nestjs/mongoose';

import { AttachmentRepository } from '@/attachment/repositories/attachment.repository';
import { AttachmentModel } from '@/attachment/schemas/attachment.schema';
import { AttachmentService } from '@/attachment/services/attachment.service';
import { LoggerModule } from '@/logger/logger.module';
import { SettingRepository } from '@/setting/repositories/setting.repository';
import { SettingModel } from '@/setting/schemas/setting.schema';
import { SettingSeeder } from '@/setting/seeds/setting.seed';
import { SettingService } from '@/setting/services/setting.service';
import { SettingModule } from '@/setting/setting.module';
import { InvitationRepository } from '@/user/repositories/invitation.repository';
import { RoleRepository } from '@/user/repositories/role.repository';
import { UserRepository } from '@/user/repositories/user.repository';
Expand Down Expand Up @@ -57,6 +65,29 @@ describe('SubscriberController', () => {
const { getMocks } = await buildTestingMocks({
controllers: [SubscriberController],
imports: [
CacheModule.register({
isGlobal: true,
ttl: 60 * 1000,
max: 100,
}),
EventEmitterModule.forRoot({
// set this to `true` to use wildcards
wildcard: true,
// the delimiter used to segment namespaces
delimiter: ':',
// set this to `true` if you want to emit the newListener event
newListener: false,
// set this to `true` if you want to emit the removeListener event
removeListener: false,
// the maximum amount of listeners that can be assigned to an event
maxListeners: 10,
// show event name in memory leak message when more than maximum amount of listeners is assigned
verboseMemoryLeak: false,
// disable throwing uncaughtException if an error event is emitted and it has no listeners
ignoreErrors: false,
}),
LoggerModule,
SettingModule,
rootMongooseTestModule(installSubscriberFixtures),
MongooseModule.forFeature([
SubscriberModel,
Expand All @@ -66,6 +97,7 @@ describe('SubscriberController', () => {
InvitationModel,
PermissionModel,
AttachmentModel,
SettingModel,
]),
],
providers: [
Expand All @@ -82,6 +114,17 @@ describe('SubscriberController', () => {
InvitationRepository,
AttachmentService,
AttachmentRepository,
SettingService,
SettingSeeder,
SettingRepository,
{
provide: CACHE_MANAGER,
useValue: {
del: jest.fn(),
get: jest.fn(),
set: jest.fn(),
},
},
],
});
[subscriberService, labelService, userService, subscriberController] =
Expand Down
12 changes: 9 additions & 3 deletions api/src/chat/services/block.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import {
} from '@/utils/test/mocks/block';
import {
contextBlankInstance,
subscriberContextBlankInstance,
subscriber,
} from '@/utils/test/mocks/conversation';
import { nlpEntitiesGreeting } from '@/utils/test/mocks/nlp';
import {
Expand All @@ -56,15 +56,18 @@ import {
import { buildTestingMocks } from '@/utils/test/utils';

import { BlockRepository } from '../repositories/block.repository';
import { SubscriberRepository } from '../repositories/subscriber.repository';
import { Block, BlockModel } from '../schemas/block.schema';
import { Category, CategoryModel } from '../schemas/category.schema';
import { LabelModel } from '../schemas/label.schema';
import { SubscriberModel } from '../schemas/subscriber.schema';
import { FileType } from '../schemas/types/attachment';
import { StdOutgoingListMessage } from '../schemas/types/message';

import { CategoryRepository } from './../repositories/category.repository';
import { BlockService } from './block.service';
import { CategoryService } from './category.service';
import { SubscriberService } from './subscriber.service';

describe('BlockService', () => {
let blockRepository: BlockRepository;
Expand All @@ -91,6 +94,7 @@ describe('BlockService', () => {
AttachmentModel,
LabelModel,
LanguageModel,
SubscriberModel,
]),
],
providers: [
Expand All @@ -106,6 +110,8 @@ describe('BlockService', () => {
ContentService,
AttachmentService,
LanguageService,
SubscriberService,
SubscriberRepository,
{
provide: PluginService,
useValue: {},
Expand Down Expand Up @@ -429,7 +435,7 @@ describe('BlockService', () => {
...contextBlankInstance,
skip: { [blockProductListMock.id]: 0 },
},
subscriberContextBlankInstance,
subscriber,
false,
'conv_id',
);
Expand Down Expand Up @@ -463,7 +469,7 @@ describe('BlockService', () => {
...contextBlankInstance,
skip: { [blockProductListMock.id]: 2 },
},
subscriberContextBlankInstance,
subscriber,
false,
'conv_id',
);
Expand Down
83 changes: 81 additions & 2 deletions api/src/chat/services/block.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
* 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 } from '@nestjs/common';
import {
Injectable,
InternalServerErrorException,
Optional,
} from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';

import EventWrapper from '@/channel/lib/EventWrapper';
Expand All @@ -21,6 +25,15 @@ import { PluginType } from '@/plugins/types';
import { SettingService } from '@/setting/services/setting.service';
import { BaseService } from '@/utils/generics/base-service';
import { getRandomElement } from '@/utils/helpers/safeRandom';
import {
SocketGet,
SocketPost,
} from '@/websocket/decorators/socket-method.decorator';
import { SocketReq } from '@/websocket/decorators/socket-req.decorator';
import { SocketRes } from '@/websocket/decorators/socket-res.decorator';
import { SocketRequest } from '@/websocket/utils/socket-request';
import { SocketResponse } from '@/websocket/utils/socket-response';
import { WebsocketGateway } from '@/websocket/websocket.gateway';

import { BlockDto } from '../dto/block.dto';
import { EnvelopeFactory } from '../helpers/envelope-factory';
Expand All @@ -39,22 +52,66 @@ import { NlpPattern, PayloadPattern } from '../schemas/types/pattern';
import { Payload, StdQuickReply } from '../schemas/types/quick-reply';
import { SubscriberContext } from '../schemas/types/subscriberContext';

import { SubscriberService } from './subscriber.service';

@Injectable()
export class BlockService extends BaseService<
Block,
BlockPopulate,
BlockFull,
BlockDto
> {
private readonly gateway: WebsocketGateway;

constructor(
readonly repository: BlockRepository,
private readonly contentService: ContentService,
private readonly settingService: SettingService,
private readonly pluginService: PluginService,
protected readonly i18n: I18nService,
protected readonly languageService: LanguageService,
private subscriberService: SubscriberService,
@Optional() gateway?: WebsocketGateway,
) {
super(repository);
if (gateway) {
this.gateway = gateway;
}
}

@SocketGet('/block/subscribe/')
@SocketPost('/block/subscribe/')
async subscribe(
@SocketReq() req: SocketRequest,
@SocketRes() res: SocketResponse,
) {
try {
const subscriberForeignId = req.session.passport?.user?.id;
if (!subscriberForeignId) {
this.logger.warn('Missing subscriber foreign ID in session');
throw new Error('Invalid session or user not authenticated');
}

const subscriber = await this.subscriberService.findOne({
foreign_id: subscriberForeignId,
});

if (!subscriber) {
this.logger.warn(
`Subscriber not found for foreign ID ${subscriberForeignId}`,
);
throw new Error('Subscriber not found');
}
const room = `blocks:${subscriber.id}`;
this.gateway.io.socketsJoin(room);
this.logger.log('Subscribed to socket room', room);
return res.status(200).json({
success: true,
});
} catch (e) {
this.logger.error('Websocket subscription', e);
throw new InternalServerErrorException(e);
}
}

/**
Expand Down Expand Up @@ -474,10 +531,11 @@ export class BlockService extends BaseService<
async processMessage(
block: Block | BlockFull,
context: Context,
subscriberContext: SubscriberContext,
recipient: Subscriber,
fallback = false,
conversationId?: string,
): Promise<StdOutgoingEnvelope> {
const subscriberContext = recipient.context as SubscriberContext;
const settings = await this.settingService.getSettings();
const blockMessage: BlockMessage =
fallback && block.options?.fallback
Expand Down Expand Up @@ -566,6 +624,13 @@ export class BlockService extends BaseService<
const attachmentPayload = blockMessage.attachment.payload;
if (!('id' in attachmentPayload)) {
this.checkDeprecatedAttachmentUrl(block);
this.logger.log('triggered: hook:highlight:error');
this.eventEmitter.emit('hook:highlight:block', {
userId: recipient.id,
blockId: block.id,
highlightType: fallback ? 'fallback' : 'error',
});

throw new Error(
'Remote attachments in blocks are no longer supported!',
);
Expand Down Expand Up @@ -614,6 +679,13 @@ export class BlockService extends BaseService<
};
return envelope;
} catch (err) {
this.logger.log('highlighting error');
this.eventEmitter.emit('hook:highlight:block', {
userId: recipient.id,
blockId: block.id,
highlightType: 'error',
});

this.logger.error(
'Unable to retrieve content for list template process',
err,
Expand All @@ -635,6 +707,13 @@ export class BlockService extends BaseService<

return envelope;
} catch (e) {
this.logger.log('highlighting error');
this.eventEmitter.emit('hook:highlight:block', {
userId: recipient.id,
blockId: block.id,
highlightType: 'error',
});

this.logger.error('Plugin was unable to load/process ', e);
throw new Error(`Unknown plugin - ${JSON.stringify(blockMessage)}`);
}
Expand Down
19 changes: 15 additions & 4 deletions api/src/chat/services/bot.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,11 +147,15 @@ export class BotService {
const envelope = await this.blockService.processMessage(
block,
context,
recipient?.context,
recipient,
fallback,
convo.id,
);

this.eventEmitter.emit('hook:highlight:block', {
blockId: block.id,
userId: recipient.id,
highlightType: fallback ? 'fallback' : 'highlight',
});
if (envelope.format !== OutgoingMessageFormat.system) {
await this.sendMessageToSubscriber(
envelope,
Expand Down Expand Up @@ -323,6 +327,11 @@ export class BotService {
);
await this.triggerBlock(event, updatedConversation, next, fallback);
} catch (err) {
this.eventEmitter.emit('hook:highlight:block', {
userId: convo.sender.id,
blockId: next.id!,
highlightType: fallback ? 'fallback' : 'error',
});
this.logger.error('Unable to store context data!', err);
return this.eventEmitter.emit('hook:conversation:end', convo);
}
Expand Down Expand Up @@ -522,11 +531,13 @@ export class BotService {
updatedAt: new Date(),
attachedBlock: null,
} as any as BlockFull;

const recipient = structuredClone(event.getSender());
recipient.context.vars = {};
const envelope = await this.blockService.processMessage(
globalFallbackBlock,
getDefaultConversationContext(),
{ vars: {} }, // @TODO: use subscriber ctx
recipient,
// { vars: {} }, // @TODO: use subscriber ctx
);

await this.sendMessageToSubscriber(
Expand Down
7 changes: 7 additions & 0 deletions api/src/setting/seeds/setting.seed-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@ export const DEFAULT_SETTINGS = [
weight: 6,
translatable: true,
},
{
group: 'chatbot_settings',
label: 'enable_debug',
value: false,
type: SettingType.checkbox,
weight: 7,
},
{
group: 'contact',
label: 'contact_email_recipient',
Expand Down
5 changes: 5 additions & 0 deletions api/src/setting/services/setting.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,4 +164,9 @@ export class SettingService extends BaseService<Setting> {
const settings = await this.findAll();
return this.buildTree(settings);
}

public async isHighlightEnabled(): Promise<boolean> {
const settings = await this.getSettings();
return settings.chatbot_settings.enable_debug ?? false;
}
}
Loading
Loading