feat: refactor extensions as npm packages (be brave 1)

This commit is contained in:
Mohamed Marrouchi 2024-10-22 11:01:44 +01:00
parent 4cf5c57053
commit 35cf78c523
87 changed files with 394 additions and 331 deletions

View File

@ -14,3 +14,4 @@ test
*.mock.ts
__mock__
__test__
.hexabot

1
api/.gitignore vendored
View File

@ -1,3 +1,4 @@
.hexabot/
node_modules/
dist/
coverage/

5
api/package-lock.json generated
View File

@ -11355,6 +11355,11 @@
"he": "bin/he"
}
},
"node_modules/hexabot-plugin-medmar": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/hexabot-plugin-medmar/-/hexabot-plugin-medmar-2.0.1.tgz",
"integrity": "sha512-qT2wG+vPjgBD9PZYl1EqzZPqVpoOrpldRUEDiwuzIKkIHEdfL6Q5Tr3rcgnYqFIPimVQhn/0tMgd1yQLD1dCPQ=="
},
"node_modules/hexoid": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz",

View File

@ -8,12 +8,18 @@
"scripts": {
"preinstall": "node merge-extensions-deps.js",
"postinstall": "patch-package",
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"build:clean": "rm -rf src/.hexabot",
"build:channels": "mkdir -p src/.hexabot/channels && cp -R node_modules/hexabot-channel-* src/.hexabot/channels/",
"build:helpers": "mkdir -p src/.hexabot/helpers && cp -R node_modules/hexabot-helper-* src/.hexabot/helpers/",
"build:plugins": "mkdir -p src/.hexabot/plugins && cp -R node_modules/hexabot-plugin-* src/.hexabot/plugins/",
"build:extensions": "npm run build:channels && npm run build:helpers && npm run build:plugins",
"build:prepare": "npm run build:clean && npm run build:extensions",
"build": "npm run build:prepare && nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" \"libs/**/*.ts\"",
"start": "nest start",
"doc": "npx @compodoc/compodoc --hideGenerator -p tsconfig.doc.json -s -r 9003 -w",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug 0.0.0.0:9229 --watch",
"start:dev": "npm run build:prepare && nest start --watch",
"start:debug": "npm run build:prepare && nest start --debug 0.0.0.0:9229 --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\"",
"lint:fix": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",

View File

@ -108,13 +108,13 @@ export class AttachmentService extends BaseService<Attachment> {
await this.getStoragePlugin().uploadAvatar(picture);
this.logger.log(
`Profile picture uploaded successfully to ${
this.getStoragePlugin().id
this.getStoragePlugin().name
}`,
);
} catch (err) {
this.logger.error(
`Error while uploading profile picture to ${
this.getStoragePlugin().id
this.getStoragePlugin().name
}`,
err,
);

View File

@ -23,7 +23,7 @@ export class ChannelController {
getChannels(): { name: string }[] {
return this.channelService.getAll().map((handler) => {
return {
name: handler.getChannel(),
name: handler.getName(),
};
});
}

View File

@ -7,7 +7,7 @@
*/
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { NextFunction, Request, Response } from 'express';
import { ChannelService } from './channel.service';
@ -20,7 +20,9 @@ export class ChannelMiddleware implements NestMiddleware {
try {
const [_, path, channelName] = req.path.split('/');
if (path === 'webhook' && channelName) {
const channel = this.channelService.getChannelHandler(channelName);
const channel = this.channelService.getChannelHandler(
`${channelName}-channel`,
);
if (channel) {
return await channel.middleware(req, res, next);
}

View File

@ -7,7 +7,12 @@
*/
import { HttpModule } from '@nestjs/axios';
import { MiddlewareConsumer, Module, RequestMethod } from '@nestjs/common';
import {
Global,
MiddlewareConsumer,
Module,
RequestMethod,
} from '@nestjs/common';
import { InjectDynamicProviders } from 'nestjs-dynamic-providers';
import { AttachmentModule } from '@/attachment/attachment.module';
@ -23,6 +28,7 @@ export interface ChannelModuleOptions {
folder: string;
}
@Global()
@InjectDynamicProviders('dist/extensions/**/*.channel.js')
@Module({
controllers: [WebhookController, ChannelController],

View File

@ -23,10 +23,11 @@ import { SocketRequest } from '@/websocket/utils/socket-request';
import { SocketResponse } from '@/websocket/utils/socket-response';
import ChannelHandler from './lib/Handler';
import { ChannelName } from './types';
@Injectable()
export class ChannelService {
private registry: Map<string, ChannelHandler<string>> = new Map();
private registry: Map<string, ChannelHandler<ChannelName>> = new Map();
constructor(
private readonly logger: LoggerService,
@ -40,7 +41,7 @@ export class ChannelService {
* @param channel - The channel handler associated with the channel name.
* @typeParam C The channel handler's type that extends `ChannelHandler`.
*/
public setChannel<T extends string, C extends ChannelHandler<T>>(
public setChannel<T extends ChannelName, C extends ChannelHandler<T>>(
name: T,
channel: C,
) {
@ -62,9 +63,9 @@ export class ChannelService {
* @param name - The name of the channel to find.
* @returns The channel handler associated with the specified name, or undefined if the channel is not found.
*/
public findChannel(name: string) {
public findChannel(name: ChannelName) {
return this.getAll().find((c) => {
return c.getChannel() === name;
return c.getName() === name;
});
}
@ -74,7 +75,7 @@ export class ChannelService {
* @param channelName - The name of the channel (messenger, offline, ...).
* @returns The handler for the specified channel.
*/
public getChannelHandler<T extends string, C extends ChannelHandler<T>>(
public getChannelHandler<T extends ChannelName, C extends ChannelHandler<T>>(
name: T,
): C {
const handler = this.registry.get(name);
@ -93,7 +94,7 @@ export class ChannelService {
* @returns A promise that resolves when the handler has processed the request.
*/
async handle(channel: string, req: Request, res: Response): Promise<void> {
const handler = this.getChannelHandler(channel);
const handler = this.getChannelHandler(`${channel}-channel`);
handler.handle(req, res);
}

View File

@ -55,7 +55,7 @@ export default abstract class EventWrapper<
toString() {
return JSON.stringify(
{
handler: this._handler.getChannel(),
handler: this._handler.getName(),
channelData: this.getChannelData(),
sender: this.getSender(),
recipient: this.getRecipientForeignId(),

View File

@ -6,7 +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 { Injectable } from '@nestjs/common';
import path from 'path';
import { Injectable, OnModuleInit } from '@nestjs/common';
import { NextFunction, Request, Response } from 'express';
import { Attachment } from '@/attachment/schemas/attachment.schema';
@ -17,35 +19,39 @@ import {
} from '@/chat/schemas/types/message';
import { LoggerService } from '@/logger/logger.service';
import { SettingService } from '@/setting/services/setting.service';
import { hyphenToUnderscore } from '@/utils/helpers/misc';
import { Extension } from '@/utils/generics/extension';
import { SocketRequest } from '@/websocket/utils/socket-request';
import { SocketResponse } from '@/websocket/utils/socket-response';
import { ChannelService } from '../channel.service';
import { ChannelSetting } from '../types';
import { ChannelName, ChannelSetting } from '../types';
import EventWrapper from './EventWrapper';
@Injectable()
export default abstract class ChannelHandler<N extends string = string> {
private readonly name: N;
export default abstract class ChannelHandler<
N extends ChannelName = ChannelName,
>
extends Extension
implements OnModuleInit
{
private readonly settings: ChannelSetting<N>[];
constructor(
name: N,
settings: ChannelSetting<N>[],
protected readonly settingService: SettingService,
private readonly channelService: ChannelService,
protected readonly logger: LoggerService,
) {
this.name = name;
this.settings = settings;
super(name);
// eslint-disable-next-line @typescript-eslint/no-var-requires
this.settings = require(path.join(this.getPath(), 'settings')).default;
}
onModuleInit() {
async onModuleInit() {
await super.onModuleInit();
this.channelService.setChannel(
this.getChannel(),
this.getName() as ChannelName,
this as unknown as ChannelHandler<N>,
);
this.setup();
@ -53,7 +59,7 @@ export default abstract class ChannelHandler<N extends string = string> {
async setup() {
await this.settingService.seedIfNotExist(
this.getChannel(),
this.getName(),
this.settings.map((s, i) => ({
...s,
weight: i + 1,
@ -62,22 +68,6 @@ export default abstract class ChannelHandler<N extends string = string> {
this.init();
}
/**
* Returns the channel's name
* @returns Channel's name
*/
getChannel() {
return this.name;
}
/**
* Returns the channel's group
* @returns Channel's group
*/
protected getGroup() {
return hyphenToUnderscore(this.getChannel()) as ChannelSetting<N>['group'];
}
/**
* Returns the channel's settings
* @returns Channel's settings
@ -85,7 +75,7 @@ export default abstract class ChannelHandler<N extends string = string> {
async getSettings<S extends string = HyphenToUnderscore<N>>() {
const settings = await this.settingService.getSettings();
// @ts-expect-error workaround typing
return settings[this.getGroup() as keyof Settings] as Settings[S];
return settings[this.getNamespace() as keyof Settings] as Settings[S];
}
/**

View File

@ -25,7 +25,7 @@ export const subscriberInstance: Subscriber = {
lastvisit: new Date(),
retainedFrom: new Date(),
channel: {
name: 'offline',
name: 'offline-channel',
},
labels: [],
...modelInstance,

View File

@ -1,5 +1,7 @@
import { SettingCreateDto } from '@/setting/dto/setting.dto';
export type ChannelName = `${string}-channel`;
export type ChannelSetting<N extends string = string> = Omit<
SettingCreateDto,
'group' | 'weight'

View File

@ -27,7 +27,7 @@ import { CsrfInterceptor } from '@/interceptors/csrf.interceptor';
import { LoggerService } from '@/logger/logger.service';
import { BaseBlockPlugin } from '@/plugins/base-block-plugin';
import { PluginService } from '@/plugins/plugins.service';
import { PluginType } from '@/plugins/types';
import { PluginName, PluginType } from '@/plugins/types';
import { UserService } from '@/user/services/user.service';
import { BaseController } from '@/utils/generics/base-controller';
import { DeleteResult } from '@/utils/generics/base-repository';
@ -85,20 +85,23 @@ export class BlockController extends BaseController<
/**
* Retrieves a custom block settings for a specific plugin.
*
* @param pluginId - The name of the plugin for which settings are to be retrieved.
* @param pluginName - The name of the plugin for which settings are to be retrieved.
*
* @returns An array containing the settings of the specified plugin.
*/
@Get('customBlocks/settings')
findSettings(@Query('plugin') pluginId: string) {
findSettings(@Query('plugin') pluginName: PluginName) {
try {
if (!pluginId) {
if (!pluginName) {
throw new BadRequestException(
'Plugin id must be supplied as a query param',
);
}
const plugin = this.pluginsService.getPlugin(PluginType.block, pluginId);
const plugin = this.pluginsService.getPlugin(
PluginType.block,
pluginName,
);
if (!plugin) {
throw new NotFoundException('Plugin Not Found');
@ -122,11 +125,12 @@ export class BlockController extends BaseController<
const plugins = this.pluginsService
.getAllByType(PluginType.block)
.map((p) => ({
id: p.id,
id: p.getName(),
namespace: p.getNamespace(),
template: {
...p.template,
message: {
plugin: p.id,
plugin: p.name,
args: p.settings.reduce(
(acc, setting) => {
acc[setting.label] = setting.value;

View File

@ -6,8 +6,10 @@
* 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 { ChannelName } from '@/channel/types';
interface BaseChannelData {
name: string; // channel name
name: ChannelName; // channel name
isSocket?: boolean;
type?: any; //TODO: type has to be checked
}

View File

@ -6,6 +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 { ChannelName } from '@/channel/types';
import { Nlp } from '@/helper/types';
import { Subscriber } from '../subscriber.schema';
@ -13,7 +14,7 @@ import { Subscriber } from '../subscriber.schema';
import { Payload } from './quick-reply';
export interface Context {
channel?: string;
channel?: ChannelName;
text?: string;
payload?: Payload | string;
nlp?: Nlp.ParseEntities | null;

View File

@ -222,7 +222,7 @@ describe('BlockService', () => {
describe('match', () => {
const handlerMock = {
getChannel: jest.fn(() => OFFLINE_CHANNEL_NAME),
getName: jest.fn(() => OFFLINE_CHANNEL_NAME),
} as any as OfflineHandler;
const offlineEventGreeting = new OfflineEventWrapper(
handlerMock,
@ -502,7 +502,7 @@ describe('BlockService', () => {
describe('processText', () => {
const context: Context = {
...contextGetStartedInstance,
channel: 'offline',
channel: 'offline-channel',
text: '',
payload: undefined,
nlp: { entities: [] },

View File

@ -17,7 +17,7 @@ import { I18nService } from '@/i18n/services/i18n.service';
import { LanguageService } from '@/i18n/services/language.service';
import { LoggerService } from '@/logger/logger.service';
import { PluginService } from '@/plugins/plugins.service';
import { PluginType } from '@/plugins/types';
import { PluginName, PluginType } from '@/plugins/types';
import { SettingService } from '@/setting/services/setting.service';
import { BaseService } from '@/utils/generics/base-service';
import { getRandom } from '@/utils/helpers/safeRandom';
@ -71,7 +71,7 @@ export class BlockService extends BaseService<Block, BlockPopulate, BlockFull> {
const payload = event.getPayload();
// Perform a filter on the specific channels
const channel = event.getHandler().getChannel();
const channel = event.getHandler().getName();
blocks = blocks.filter((b) => {
return (
!b.trigger_channels ||
@ -593,7 +593,7 @@ export class BlockService extends BaseService<Block, BlockPopulate, BlockFull> {
} else if (blockMessage && 'plugin' in blockMessage) {
const plugin = this.pluginService.findPlugin(
PluginType.block,
blockMessage.plugin,
blockMessage.plugin as PluginName,
);
// Process custom plugin block
try {

View File

@ -224,7 +224,7 @@ describe('BlockService', () => {
nlp: null,
payload: null,
attempt: 0,
channel: 'offline',
channel: 'offline-channel',
text: offlineEventText.data.text,
},
});
@ -283,7 +283,7 @@ describe('BlockService', () => {
nlp: null,
payload: null,
attempt: 0,
channel: 'offline',
channel: 'offline-channel',
text: offlineEventText.data.text,
},
});

View File

@ -246,7 +246,7 @@ export class ChatService {
this.eventEmitter.emit('hook:stats:entry', 'new_users', 'New users');
subscriberData.channel = {
...event.getChannelData(),
name: handler.getChannel(),
name: handler.getName(),
};
subscriber = await this.subscriberService.create(subscriberData);
} else {

View File

@ -9,6 +9,7 @@
import { Injectable, Logger } from '@nestjs/common';
import EventWrapper from '@/channel/lib/EventWrapper';
import { ChannelName } from '@/channel/types';
import { LoggerService } from '@/logger/logger.service';
import { BaseService } from '@/utils/generics/base-service';
@ -69,7 +70,7 @@ export class ConversationService extends BaseService<
const msgType = event.getMessageType();
const profile = event.getSender();
// Capture channel specific context data
convo.context.channel = event.getHandler().getChannel();
convo.context.channel = event.getHandler().getName() as ChannelName;
convo.context.text = event.getText();
convo.context.payload = event.getPayload();
convo.context.nlp = event.getNLP();

View File

@ -1,3 +1,3 @@
{
"live_chat_tester": "Live Chat Tester"
"live_chat_tester_channel": "Live Chat Tester"
}

View File

@ -1,3 +1,3 @@
{
"live_chat_tester": "Testeur Live Chat"
"live_chat_tester_channel": "Testeur Live Chat"
}

View File

@ -21,10 +21,7 @@ import { WebsocketGateway } from '@/websocket/websocket.gateway';
import BaseWebChannelHandler from '../offline/base-web-channel';
import {
DEFAULT_LIVE_CHAT_TEST_SETTINGS,
LIVE_CHAT_TEST_CHANNEL_NAME,
} from './settings';
import { LIVE_CHAT_TEST_CHANNEL_NAME } from './settings';
@Injectable()
export default class LiveChatTesterHandler extends BaseWebChannelHandler<
@ -44,7 +41,6 @@ export default class LiveChatTesterHandler extends BaseWebChannelHandler<
) {
super(
LIVE_CHAT_TEST_CHANNEL_NAME,
DEFAULT_LIVE_CHAT_TEST_SETTINGS,
settingService,
channelService,
logger,
@ -57,4 +53,8 @@ export default class LiveChatTesterHandler extends BaseWebChannelHandler<
websocketGateway,
);
}
getPath(): string {
return __dirname;
}
}

View File

@ -1,5 +1,4 @@
import {
DEFAULT_LIVE_CHAT_TEST_SETTINGS,
import DEFAULT_LIVE_CHAT_TEST_SETTINGS, {
LIVE_CHAT_TEST_GROUP_NAME,
} from './settings';

View File

@ -0,0 +1,7 @@
{
"name": "hexabot-channel-live-chat-tester",
"version": "2.0.0",
"description": "The Web Channel Extension for Hexabot Chatbot / Agent Builder for website integration",
"author": "Hexastack",
"license": "AGPL-3.0-only"
}

View File

@ -12,11 +12,11 @@ import { SettingType } from '@/setting/schemas/types';
import { Offline } from '../offline/types';
export const LIVE_CHAT_TEST_CHANNEL_NAME = 'live-chat-tester';
export const LIVE_CHAT_TEST_CHANNEL_NAME = 'live-chat-tester-channel';
export const LIVE_CHAT_TEST_GROUP_NAME = 'live_chat_tester';
export const LIVE_CHAT_TEST_GROUP_NAME = 'live_chat_tester_channel';
export const DEFAULT_LIVE_CHAT_TEST_SETTINGS = [
export default [
{
group: LIVE_CHAT_TEST_GROUP_NAME,
label: Offline.SettingLabel.verification_token,

View File

@ -131,7 +131,7 @@ describe('Offline Handler', () => {
it('should have correct name', () => {
expect(handler).toBeDefined();
expect(handler.getChannel()).toEqual('offline');
expect(handler.getName()).toEqual('offline-channel');
});
it('should format text properly', () => {
@ -192,7 +192,7 @@ describe('Offline Handler', () => {
agent: req.headers['user-agent'],
ipAddress: '0.0.0.0',
isSocket: false,
name: 'offline',
name: 'offline-channel',
},
country: '',
first_name: req.query.first_name,

View File

@ -22,7 +22,7 @@ import { AttachmentService } from '@/attachment/services/attachment.service';
import { ChannelService } from '@/channel/channel.service';
import EventWrapper from '@/channel/lib/EventWrapper';
import ChannelHandler from '@/channel/lib/Handler';
import { ChannelSetting } from '@/channel/types';
import { ChannelName } from '@/channel/types';
import { MessageCreateDto } from '@/chat/dto/message.dto';
import { SubscriberCreateDto } from '@/chat/dto/subscriber.dto';
import { VIEW_MORE_PAYLOAD } from '@/chat/helpers/constants';
@ -63,12 +63,11 @@ import { Offline } from './types';
import OfflineEventWrapper from './wrapper';
@Injectable()
export default class BaseWebChannelHandler<
N extends string,
export default abstract class BaseWebChannelHandler<
N extends ChannelName,
> extends ChannelHandler<N> {
constructor(
name: N,
settings: ChannelSetting<N>[],
settingService: SettingService,
channelService: ChannelService,
logger: LoggerService,
@ -80,7 +79,7 @@ export default class BaseWebChannelHandler<
protected readonly menuService: MenuService,
private readonly websocketGateway: WebsocketGateway,
) {
super(name, settings, settingService, channelService, logger);
super(name, settingService, channelService, logger);
}
/**
@ -102,7 +101,7 @@ export default class BaseWebChannelHandler<
const settings = await this.getSettings();
const handshake = client.handshake;
const { channel } = handshake.query;
if (channel !== this.getChannel()) {
if (channel !== this.getName()) {
return;
}
try {
@ -464,7 +463,7 @@ export default class BaseWebChannelHandler<
retainedFrom: new Date(),
channel: {
...channelData,
name: this.getChannel(),
name: this.getName() as ChannelName,
},
language: '',
locale: '',

View File

@ -1,3 +1,3 @@
{
"offline": "Canal Web"
"offline_channel": "Web Channel"
}

View File

@ -1,3 +1,3 @@
{
"live_chat_tester": "Testeur Live Chat"
"offline_channel": "Canal Web"
}

View File

@ -20,7 +20,7 @@ import { SettingService } from '@/setting/services/setting.service';
import { WebsocketGateway } from '@/websocket/websocket.gateway';
import BaseWebChannelHandler from './base-web-channel';
import { DEFAULT_OFFLINE_SETTINGS, OFFLINE_CHANNEL_NAME } from './settings';
import { OFFLINE_CHANNEL_NAME } from './settings';
@Injectable()
export default class OfflineHandler extends BaseWebChannelHandler<
@ -40,7 +40,6 @@ export default class OfflineHandler extends BaseWebChannelHandler<
) {
super(
OFFLINE_CHANNEL_NAME,
DEFAULT_OFFLINE_SETTINGS,
settingService,
channelService,
logger,
@ -53,4 +52,8 @@ export default class OfflineHandler extends BaseWebChannelHandler<
websocketGateway,
);
}
getPath(): string {
return __dirname;
}
}

View File

@ -1,4 +1,4 @@
import { DEFAULT_OFFLINE_SETTINGS, OFFLINE_GROUP_NAME } from './settings';
import DEFAULT_OFFLINE_SETTINGS, { OFFLINE_GROUP_NAME } from './settings';
declare global {
interface Settings extends SettingTree<typeof DEFAULT_OFFLINE_SETTINGS> {}

View File

@ -0,0 +1,7 @@
{
"name": "hexabot-channel-offline",
"version": "2.0.0",
"description": "The Web Channel Extension for Hexabot Chatbot / Agent Builder for website integration",
"author": "Hexastack",
"license": "AGPL-3.0-only"
}

View File

@ -11,11 +11,11 @@ import { SettingType } from '@/setting/schemas/types';
import { Offline } from './types';
export const OFFLINE_CHANNEL_NAME = 'offline' as const;
export const OFFLINE_CHANNEL_NAME = 'offline-channel' as const;
export const OFFLINE_GROUP_NAME = OFFLINE_CHANNEL_NAME;
export const OFFLINE_GROUP_NAME = 'offline_channel';
export const DEFAULT_OFFLINE_SETTINGS = [
export default [
{
group: OFFLINE_GROUP_NAME,
label: Offline.SettingLabel.verification_token,

View File

@ -7,6 +7,7 @@
*/
import EventWrapper from '@/channel/lib/EventWrapper';
import { ChannelName } from '@/channel/types';
import {
AttachmentForeignKey,
AttachmentPayload,
@ -67,7 +68,8 @@ type OfflineEventAdapter =
};
export default class OfflineEventWrapper<
T extends BaseWebChannelHandler<string> = BaseWebChannelHandler<string>,
T extends
BaseWebChannelHandler<ChannelName> = BaseWebChannelHandler<ChannelName>,
> extends EventWrapper<OfflineEventAdapter, Offline.Event> {
/**
* Constructor : channel's event wrapper

View File

@ -58,7 +58,7 @@ describe('Core NLU Helper', () => {
provide: SettingService,
useValue: {
getSettings: jest.fn(() => ({
core_nlu: {
core_nlu_helper: {
endpoint: 'path',
token: 'token',
threshold: '0.5',
@ -121,7 +121,7 @@ describe('Core NLU Helper', () => {
true,
);
const settings = await settingService.getSettings();
const threshold = settings.core_nlu.threshold;
const threshold = settings.core_nlu_helper.threshold;
const thresholdGuess = {
entities: nlpBestGuess.entities.filter(
(g) =>

View File

@ -1,3 +1,3 @@
{
"core_nlu": "Core NLU Engine"
"core_nlu_helper": "Core NLU Engine"
}

View File

@ -1,3 +1,3 @@
{
"core_nlu": "Core NLU Engine"
"core_nlu_helper": "Core NLU Engine"
}

View File

@ -1,4 +1,4 @@
import { CORE_NLU_HELPER_GROUP, CORE_NLU_HELPER_SETTINGS } from './settings';
import CORE_NLU_HELPER_SETTINGS, { CORE_NLU_HELPER_GROUP } from './settings';
declare global {
interface Settings extends SettingTree<typeof CORE_NLU_HELPER_SETTINGS> {}

View File

@ -20,7 +20,7 @@ import { NlpValue } from '@/nlp/schemas/nlp-value.schema';
import { SettingService } from '@/setting/services/setting.service';
import { buildURL } from '@/utils/helpers/URL';
import { CORE_NLU_HELPER_NAME, CORE_NLU_HELPER_SETTINGS } from './settings';
import { CORE_NLU_HELPER_NAME } from './settings';
import { NlpParseResultType, RasaNlu } from './types';
@Injectable()
@ -34,13 +34,11 @@ export default class CoreNluHelper extends BaseNlpHelper<
private readonly httpService: HttpService,
private readonly languageService: LanguageService,
) {
super(
CORE_NLU_HELPER_NAME,
CORE_NLU_HELPER_SETTINGS,
settingService,
helperService,
logger,
);
super(CORE_NLU_HELPER_NAME, settingService, helperService, logger);
}
getPath() {
return __dirname;
}
/**

View File

@ -1,5 +1,5 @@
{
"name": "hexabot-core-nlu",
"name": "hexabot-helper-core-nlu",
"version": "2.0.0",
"description": "The Core NLU Helper Extension for Hexabot Chatbot / Agent Builder to enable the Intent Classification and Language Detection",
"dependencies": {},

View File

@ -1,11 +1,11 @@
import { HelperSetting } from '@/helper/types';
import { SettingType } from '@/setting/schemas/types';
export const CORE_NLU_HELPER_NAME = 'core-nlu';
export const CORE_NLU_HELPER_NAME = 'core-nlu-helper';
export const CORE_NLU_HELPER_GROUP = 'core_nlu';
export const CORE_NLU_HELPER_GROUP = 'core_nlu_helper';
export const CORE_NLU_HELPER_SETTINGS = [
export default [
{
group: CORE_NLU_HELPER_GROUP,
label: 'endpoint',

View File

@ -1,3 +1,3 @@
{
"ollama": "Ollama"
"ollama_helper": "Ollama"
}

View File

@ -1,3 +1,3 @@
{
"ollama": "Ollama"
"ollama_helper": "Ollama"
}

View File

@ -1,4 +1,4 @@
import { OLLAMA_HELPER_GROUP, OLLAMA_HELPER_SETTINGS } from './settings';
import OLLAMA_HELPER_SETTINGS, { OLLAMA_HELPER_GROUP } from './settings';
declare global {
interface Settings extends SettingTree<typeof OLLAMA_HELPER_SETTINGS> {}

View File

@ -17,7 +17,7 @@ import { LoggerService } from '@/logger/logger.service';
import { Setting } from '@/setting/schemas/setting.schema';
import { SettingService } from '@/setting/services/setting.service';
import { OLLAMA_HELPER_NAME, OLLAMA_HELPER_SETTINGS } from './settings';
import { OLLAMA_HELPER_NAME } from './settings';
@Injectable()
export default class OllamaLlmHelper
@ -36,13 +36,11 @@ export default class OllamaLlmHelper
helperService: HelperService,
protected readonly logger: LoggerService,
) {
super(
OLLAMA_HELPER_NAME,
OLLAMA_HELPER_SETTINGS,
settingService,
helperService,
logger,
);
super('ollama-helper', settingService, helperService, logger);
}
getPath(): string {
return __dirname;
}
async onApplicationBootstrap() {
@ -51,7 +49,7 @@ export default class OllamaLlmHelper
this.client = new Ollama({ host: settings.api_url });
}
@OnEvent('hook:ollama:api_url')
@OnEvent('hook:ollama_helper:api_url')
handleApiUrlChange(setting: Setting) {
this.client = new Ollama({ host: setting.value });
}

View File

@ -1,11 +1,13 @@
import { HelperSetting } from '@/helper/types';
import { SettingType } from '@/setting/schemas/types';
export const OLLAMA_HELPER_NAME = 'ollama';
export const OLLAMA_HELPER_NAME = 'ollama-helper';
export const OLLAMA_HELPER_GROUP = 'ollama';
export const OLLAMA_HELPER_GROUP: HyphenToUnderscore<
typeof OLLAMA_HELPER_NAME
> = 'ollama_helper';
export const OLLAMA_HELPER_SETTINGS = [
export default [
{
label: 'api_url',
group: OLLAMA_HELPER_GROUP,

View File

@ -1,3 +1,3 @@
{
"ollama": "Ollama"
"ollama_plugin": "Ollama Plugin"
}

View File

@ -1,3 +1,3 @@
{
"ollama": "Ollama"
"ollama_plugin": "Ollama Plugin"
}

View File

@ -14,14 +14,13 @@ import { HelperType } from '@/helper/types';
import { LoggerService } from '@/logger/logger.service';
import { BaseBlockPlugin } from '@/plugins/base-block-plugin';
import { PluginService } from '@/plugins/plugins.service';
import { PluginBlockTemplate } from '@/plugins/types';
import { OLLAMA_PLUGIN_SETTINGS } from './settings';
import SETTINGS from './settings';
@Injectable()
export class OllamaPlugin extends BaseBlockPlugin<
typeof OLLAMA_PLUGIN_SETTINGS
> {
public readonly settings = OLLAMA_PLUGIN_SETTINGS;
export class OllamaPlugin extends BaseBlockPlugin<typeof SETTINGS> {
template: PluginBlockTemplate = { name: 'Ollama Plugin' };
constructor(
pluginService: PluginService,
@ -30,12 +29,11 @@ export class OllamaPlugin extends BaseBlockPlugin<
private contentService: ContentService,
private messageService: MessageService,
) {
super('ollama', OLLAMA_PLUGIN_SETTINGS, pluginService);
super('ollama-plugin', pluginService);
}
this.template = { name: 'Ollama Plugin' };
this.effects = {
onStoreContextData: () => {},
};
getPath(): string {
return __dirname;
}
async process(block: Block, context: Context, _convId: string) {

View File

@ -1,9 +1,8 @@
{
"name": "hexabot-ollama",
"name": "hexabot-plugin-ollama",
"version": "2.0.0",
"description": "The Ollama Plugin Extension for Hexabot Chatbot / Agent Builder that provides a custom block for Generative AI + RAG",
"dependencies": {},
"extensions": {
"dependencies": {
"hexabot-helper-ollama": "2.0.0"
},
"author": "Hexastack",

View File

@ -1,7 +1,7 @@
import { PluginSetting } from '@/plugins/types';
import { SettingType } from '@/setting/schemas/types';
export const OLLAMA_PLUGIN_SETTINGS = [
export default [
{
label: 'model',
group: 'default',

View File

@ -12,7 +12,7 @@ import { LoggerService } from '@/logger/logger.service';
import { SettingService } from '@/setting/services/setting.service';
import BaseHelper from './lib/base-helper';
import { HelperRegistry, HelperType, TypeOfHelper } from './types';
import { HelperName, HelperRegistry, HelperType, TypeOfHelper } from './types';
@Injectable()
export class HelperService {
@ -33,7 +33,7 @@ export class HelperService {
*
* @param name - The helper to be registered.
*/
public register<H extends BaseHelper<string>>(helper: H) {
public register<H extends BaseHelper>(helper: H) {
const helpers = this.registry.get(helper.getType());
helpers.set(helper.getName(), helper);
this.logger.log(`Helper "${helper.getName()}" has been registered!`);
@ -47,7 +47,7 @@ export class HelperService {
*
* @returns - The helper
*/
public get<T extends HelperType>(type: T, name: string) {
public get<T extends HelperType>(type: T, name: HelperName) {
const helpers = this.registry.get(type);
if (!helpers.has(name)) {
@ -67,6 +67,16 @@ export class HelperService {
return Array.from(helpers.values());
}
/**
* Retrieves all registered helpers as an array.
*
* @returns An array containing all the registered helpers.
*/
public getAll(): BaseHelper[] {
return Array.from(this.registry.values()) // Get all the inner maps
.flatMap((innerMap) => Array.from(innerMap.values())); // Flatten and get the values from each inner map
}
/**
* Get a helper by class.
*
@ -100,7 +110,7 @@ export class HelperService {
const defaultHelper = this.get(
HelperType.NLU,
settings.chatbot_settings.default_nlu_helper,
settings.chatbot_settings.default_nlu_helper as HelperName,
);
if (!defaultHelper) {
@ -120,7 +130,7 @@ export class HelperService {
const defaultHelper = this.get(
HelperType.LLM,
settings.chatbot_settings.default_llm_helper,
settings.chatbot_settings.default_llm_helper as HelperName,
);
if (!defaultHelper) {

View File

@ -6,33 +6,37 @@
* 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 { LoggerService } from '@nestjs/common';
import path from 'path';
import { LoggerService, OnModuleInit } from '@nestjs/common';
import { SettingService } from '@/setting/services/setting.service';
import { hyphenToUnderscore } from '@/utils/helpers/misc';
import { Extension } from '@/utils/generics/extension';
import { HelperService } from '../helper.service';
import { HelperSetting, HelperType } from '../types';
export default abstract class BaseHelper<N extends string = string> {
protected readonly name: N;
import { HelperName, HelperSetting, HelperType } from '../types';
export default abstract class BaseHelper<N extends HelperName = HelperName>
extends Extension
implements OnModuleInit
{
protected readonly settings: HelperSetting<N>[] = [];
protected abstract type: HelperType;
constructor(
name: N,
settings: HelperSetting<N>[],
protected readonly settingService: SettingService,
protected readonly helperService: HelperService,
protected readonly logger: LoggerService,
) {
this.name = name;
this.settings = settings;
super(name);
// eslint-disable-next-line @typescript-eslint/no-var-requires
this.settings = require(path.join(this.getPath(), 'settings')).default;
}
onModuleInit() {
async onModuleInit() {
await super.onModuleInit();
this.helperService.register(this);
this.setup();
}
@ -47,23 +51,6 @@ export default abstract class BaseHelper<N extends string = string> {
);
}
/**
* Returns the helper's name
*
* @returns Helper's name
*/
public getName() {
return this.name;
}
/**
* Returns the helper's group
* @returns Helper's group
*/
protected getGroup() {
return hyphenToUnderscore(this.getName()) as HelperSetting<N>['group'];
}
/**
* Get the helper's type
*
@ -81,6 +68,6 @@ export default abstract class BaseHelper<N extends string = string> {
async getSettings<S extends string = HyphenToUnderscore<N>>() {
const settings = await this.settingService.getSettings();
// @ts-expect-error workaround typing
return settings[this.getGroup() as keyof Settings] as Settings[S];
return settings[this.getNamespace() as keyof Settings] as Settings[S];
}
}

View File

@ -11,23 +11,22 @@ import { LoggerService } from '@/logger/logger.service';
import { SettingService } from '@/setting/services/setting.service';
import { HelperService } from '../helper.service';
import { HelperSetting, HelperType } from '../types';
import { HelperName, HelperType } from '../types';
import BaseHelper from './base-helper';
export default abstract class BaseLlmHelper<
N extends string,
N extends HelperName = HelperName,
> extends BaseHelper<N> {
protected readonly type: HelperType = HelperType.LLM;
constructor(
name: N,
settings: HelperSetting<N>[],
settingService: SettingService,
helperService: HelperService,
logger: LoggerService,
) {
super(name, settings, settingService, helperService, logger);
super(name, settingService, helperService, logger);
}
/**

View File

@ -23,23 +23,23 @@ import {
import { SettingService } from '@/setting/services/setting.service';
import { HelperService } from '../helper.service';
import { HelperSetting, HelperType, Nlp } from '../types';
import { HelperName, HelperType, Nlp } from '../types';
import BaseHelper from './base-helper';
// eslint-disable-next-line prettier/prettier
export default abstract class BaseNlpHelper<
N extends string,
N extends HelperName = HelperName,
> extends BaseHelper<N> {
protected readonly type: HelperType = HelperType.NLU;
constructor(
name: N,
settings: HelperSetting<N>[],
settingService: SettingService,
helperService: HelperService,
logger: LoggerService,
) {
super(name, settings, settingService, helperService, logger);
super(name, settingService, helperService, logger);
}
/**

View File

@ -29,10 +29,12 @@ export enum HelperType {
UTIL = 'util',
}
export type HelperName = `${string}-helper`;
export type TypeOfHelper<T extends HelperType> = T extends HelperType.LLM
? BaseLlmHelper<string>
? BaseLlmHelper<HelperName>
: T extends HelperType.NLU
? BaseNlpHelper<string>
? BaseNlpHelper<HelperName>
: BaseHelper;
export type HelperRegistry<H extends BaseHelper = BaseHelper> = Map<
@ -40,7 +42,7 @@ export type HelperRegistry<H extends BaseHelper = BaseHelper> = Map<
Map<string, H>
>;
export type HelperSetting<N extends string = string> = Omit<
export type HelperSetting<N extends HelperName = HelperName> = Omit<
SettingCreateDto,
'group' | 'weight'
> & {

View File

@ -8,14 +8,19 @@
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { ChannelService } from '@/channel/channel.service';
import { HelperService } from '@/helper/helper.service';
import { CsrfInterceptor } from '@/interceptors/csrf.interceptor';
import { I18nService } from '../services/i18n.service';
import { PluginService } from '@/plugins/plugins.service';
@UseInterceptors(CsrfInterceptor)
@Controller('i18n')
export class I18nController {
constructor(private readonly i18nService: I18nService) {}
constructor(
private readonly pluginService: PluginService,
private readonly helperService: HelperService,
private readonly channelService: ChannelService,
) {}
/**
* Retrieves translations of all the installed extensions.
@ -23,6 +28,12 @@ export class I18nController {
*/
@Get()
getTranslations() {
return this.i18nService.getExtensionI18nTranslations();
const plugins = this.pluginService.getAll();
const helpers = this.helperService.getAll();
const channels = this.channelService.getAll();
return [...plugins, ...helpers, ...channels].reduce((acc, curr) => {
acc[curr.getNamespace()] = curr.getTranslations();
return acc;
}, {});
}
}

View File

@ -6,13 +6,8 @@
* 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 { existsSync, promises as fs } from 'fs';
import * as path from 'path';
import { Injectable, OnModuleInit } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import {
I18nJsonLoader,
I18nTranslation,
I18nService as NativeI18nService,
Path,
PathValue,
@ -22,25 +17,13 @@ import { IfAnyOrNever } from 'nestjs-i18n/dist/types';
import { config } from '@/config';
import { Translation } from '@/i18n/schemas/translation.schema';
import { hyphenToUnderscore } from '@/utils/helpers/misc';
@Injectable()
export class I18nService<K = Record<string, unknown>>
extends NativeI18nService<K>
implements OnModuleInit
{
export class I18nService<
K = Record<string, unknown>,
> extends NativeI18nService<K> {
private dynamicTranslations: Record<string, Record<string, string>> = {};
private extensionTranslations: I18nTranslation = {};
onModuleInit() {
this.loadExtensionI18nTranslations();
}
getExtensionI18nTranslations() {
return this.extensionTranslations;
}
t<P extends Path<K> = any, R = PathValue<K, P>>(
key: P,
options?: TranslateOptions,
@ -83,52 +66,4 @@ export class I18nService<K = Record<string, unknown>>
return acc;
}, this.dynamicTranslations);
}
async loadExtensionI18nTranslations() {
const baseDir = path.join(__dirname, '..', '..', 'extensions');
const extensionTypes = ['channels', 'helpers', 'plugins'];
try {
for (const type of extensionTypes) {
const extensionsDir = path.join(baseDir, type);
if (!existsSync(extensionsDir)) {
continue;
}
const extensionFolders = await fs.readdir(extensionsDir, {
withFileTypes: true,
});
for (const folder of extensionFolders) {
if (folder.isDirectory()) {
const i18nPath = path.join(extensionsDir, folder.name, 'i18n');
const namespace = hyphenToUnderscore(folder.name);
try {
// Check if the i18n directory exists
await fs.access(i18nPath);
// Load and merge translations
const i18nLoader = new I18nJsonLoader({ path: i18nPath });
const translations = await i18nLoader.load();
for (const lang in translations) {
if (!this.extensionTranslations[lang]) {
this.extensionTranslations[lang] = {
[namespace]: translations[lang],
};
} else {
this.extensionTranslations[lang][namespace] =
translations[lang];
}
}
} catch (error) {
// If the i18n folder does not exist or error in reading, skip this folder
}
}
}
}
} catch (error) {
throw new Error(`Failed to read extensions directory: ${error.message}`);
}
}
}

View File

@ -6,6 +6,8 @@
* 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 path from 'path';
import { Injectable } from '@nestjs/common';
import { Block, BlockFull } from '@/chat/schemas/block.schema';
@ -17,6 +19,7 @@ import { PluginService } from './plugins.service';
import {
PluginBlockTemplate,
PluginEffects,
PluginName,
PluginSetting,
PluginType,
} from './types';
@ -29,16 +32,13 @@ export abstract class BaseBlockPlugin<
public readonly settings: T;
constructor(
id: string,
settings: T,
pluginService: PluginService<BasePlugin>,
) {
super(id, pluginService);
this.settings = settings;
constructor(name: PluginName, pluginService: PluginService<BasePlugin>) {
super(name, pluginService);
// eslint-disable-next-line @typescript-eslint/no-var-requires
this.settings = require(path.join(this.getPath(), 'settings')).default;
}
template: PluginBlockTemplate;
abstract template: PluginBlockTemplate;
effects?: PluginEffects;

View File

@ -10,13 +10,13 @@ import { Injectable } from '@nestjs/common';
import { BasePlugin } from './base-plugin.service';
import { PluginService } from './plugins.service';
import { PluginType } from './types';
import { PluginName, PluginType } from './types';
@Injectable()
export abstract class BaseEventPlugin extends BasePlugin {
public readonly type: PluginType = PluginType.event;
constructor(id: string, pluginService: PluginService<BasePlugin>) {
super(id, pluginService);
constructor(name: PluginName, pluginService: PluginService<BasePlugin>) {
super(name, pluginService);
}
}

View File

@ -8,19 +8,24 @@
import { Injectable, OnModuleInit } from '@nestjs/common';
import { Extension } from '@/utils/generics/extension';
import { PluginService } from './plugins.service';
import { PluginType } from './types';
import { PluginName, PluginType } from './types';
@Injectable()
export abstract class BasePlugin implements OnModuleInit {
export abstract class BasePlugin extends Extension implements OnModuleInit {
public readonly type: PluginType;
constructor(
public readonly id: string,
public readonly name: PluginName,
private pluginService: PluginService<BasePlugin>,
) {}
) {
super(name);
}
onModuleInit() {
this.pluginService.setPlugin(this.type, this.id, this);
async onModuleInit() {
await super.onModuleInit();
this.pluginService.setPlugin(this.type, this.name, this);
}
}

View File

@ -13,14 +13,14 @@ import { Attachment } from '@/attachment/schemas/attachment.schema';
import { BasePlugin } from './base-plugin.service';
import { PluginService } from './plugins.service';
import { PluginType } from './types';
import { PluginName, PluginType } from './types';
@Injectable()
export abstract class BaseStoragePlugin extends BasePlugin {
public readonly type: PluginType = PluginType.storage;
constructor(id: string, pluginService: PluginService<BasePlugin>) {
super(id, pluginService);
constructor(name: PluginName, pluginService: PluginService<BasePlugin>) {
super(name, pluginService);
}
abstract fileExists(attachment: Attachment): Promise<boolean>;

View File

@ -20,7 +20,12 @@ import { ContentModel } from '@/cms/schemas/content.schema';
import { PluginService } from './plugins.service';
@InjectDynamicProviders('dist/extensions/**/*.plugin.js')
@InjectDynamicProviders(
// Core & under dev plugins
'dist/extensions/**/*.plugin.js',
// Installed plugins via npm
'dist/.hexabot/plugins/**/*.plugin.js',
)
@Global()
@Module({
imports: [

View File

@ -23,7 +23,7 @@ describe('PluginsService', () => {
imports: [LoggerModule],
}).compile();
pluginsService = module.get<PluginService>(PluginService);
module.get<DummyPlugin>(DummyPlugin).onModuleInit();
await module.get<DummyPlugin>(DummyPlugin).onModuleInit();
});
afterAll(async () => {
jest.clearAllMocks();
@ -37,7 +37,7 @@ describe('PluginsService', () => {
describe('getPlugin', () => {
it('should return the required plugin', () => {
const result = pluginsService.getPlugin(PluginType.block, 'dummy');
const result = pluginsService.getPlugin(PluginType.block, 'dummy-plugin');
expect(result).toBeInstanceOf(DummyPlugin);
});
});

View File

@ -10,7 +10,7 @@ import { Injectable } from '@nestjs/common';
import { BasePlugin } from './base-plugin.service';
import { PluginInstance } from './map-types';
import { PluginType } from './types';
import { PluginName, PluginType } from './types';
/**
* @summary Service for managing and retrieving plugins.
@ -38,18 +38,18 @@ export class PluginService<T extends BasePlugin = BasePlugin> {
constructor() {}
/**
* Registers a plugin with a given key.
* Registers a plugin with a given name.
*
* @param key The unique identifier for the plugin.
* @param name The unique identifier for the plugin.
* @param plugin The plugin instance to register.
*/
public setPlugin(type: PluginType, key: string, plugin: T) {
public setPlugin(type: PluginType, name: PluginName, plugin: T) {
const registry = this.registry.get(type);
registry.set(key, plugin);
registry.set(name, plugin);
}
/**
* Retrieves all registered plugins as an array.
* Retrieves all registered plugins by as an array.
*
* @returns An array containing all the registered plugins.
*/
@ -58,29 +58,39 @@ export class PluginService<T extends BasePlugin = BasePlugin> {
return Array.from(registry.values()) as PluginInstance<PT>[];
}
/**
* Retrieves all registered plugins as an array.
*
* @returns An array containing all the registered plugins.
*/
public getAll(): T[] {
return Array.from(this.registry.values()) // Get all the inner maps
.flatMap((innerMap) => Array.from(innerMap.values())); // Flatten and get the values from each inner map
}
/**
* Retrieves a plugin based on its key.
*
* @param id The key used to register the plugin.
* @param name The key used to register the plugin.
*
* @returns The plugin associated with the given key, or `undefined` if not found.
*/
public getPlugin<PT extends PluginType>(type: PT, id: string) {
public getPlugin<PT extends PluginType>(type: PT, name: PluginName) {
const registry = this.registry.get(type);
const plugin = registry.get(id);
const plugin = registry.get(name);
return plugin ? (plugin as PluginInstance<PT>) : undefined;
}
/**
* Finds a plugin by its internal `id` property.
*
* @param pluginId The unique `id` of the plugin to find.
* @param name The unique `id` of the plugin to find.
*
* @returns The plugin with the matching `id`, or `undefined` if no plugin is found.
*/
public findPlugin<PT extends PluginType>(type: PT, pluginId: string) {
public findPlugin<PT extends PluginType>(type: PT, name: PluginName) {
return this.getAllByType(type).find((plugin) => {
return plugin.id === pluginId;
return plugin.name === name;
});
}
}

View File

@ -6,11 +6,14 @@
* 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 { ChannelEvent } from '@/channel/lib/EventWrapper';
import { BlockCreateDto } from '@/chat/dto/block.dto';
import { Block } from '@/chat/schemas/block.schema';
import { Conversation } from '@/chat/schemas/conversation.schema';
import { SettingCreateDto } from '@/setting/dto/setting.dto';
export type PluginName = `${string}-plugin`;
export enum PluginType {
event = 'event',
block = 'block',
@ -19,7 +22,6 @@ export enum PluginType {
export interface CustomBlocks {}
type ChannelEvent = any;
type BlockAttrs = Partial<BlockCreateDto> & { name: string };
export type PluginSetting = Omit<SettingCreateDto, 'weight'>;

View File

@ -13,7 +13,7 @@ export const DEFAULT_SETTINGS = [
{
group: 'chatbot_settings',
label: 'default_nlu_helper',
value: 'core-nlu',
value: 'core-nlu-helper',
type: SettingType.select,
config: {
multiple: false,
@ -27,7 +27,7 @@ export const DEFAULT_SETTINGS = [
{
group: 'chatbot_settings',
label: 'default_llm_helper',
value: 'ollama',
value: 'ollama-helper',
type: SettingType.select,
config: {
multiple: false,

View File

@ -0,0 +1,43 @@
import { promises as fs } from 'fs';
import path from 'path';
import { OnModuleInit } from '@nestjs/common';
import { I18nJsonLoader, I18nTranslation } from 'nestjs-i18n';
import { Observable } from 'rxjs';
import { ExtensionName } from '../types/extension';
export abstract class Extension implements OnModuleInit {
private translations: I18nTranslation | Observable<I18nTranslation>;
constructor(public readonly name: ExtensionName) {}
abstract getPath(): string;
getName() {
return this.name;
}
getNamespace<N extends ExtensionName = ExtensionName>() {
return this.name.replaceAll('-', '_') as HyphenToUnderscore<N>;
}
async onModuleInit() {
// Load i18n
const i18nPath = path.join(this.getPath(), 'i18n');
try {
// Check if the i18n directory exists
await fs.access(i18nPath);
// Load and merge translations
const i18nLoader = new I18nJsonLoader({ path: i18nPath });
this.translations = await i18nLoader.load();
} catch (error) {
// If the i18n folder does not exist or error in reading, skip this folder
}
}
getTranslations() {
return this.translations;
}
}

View File

@ -15,23 +15,27 @@ import {
import { LoggerService } from '@/logger/logger.service';
import { BaseBlockPlugin } from '@/plugins/base-block-plugin';
import { PluginService } from '@/plugins/plugins.service';
import { PluginSetting } from '@/plugins/types';
import { PluginBlockTemplate, PluginSetting } from '@/plugins/types';
@Injectable()
export class DummyPlugin extends BaseBlockPlugin<PluginSetting[]> {
template: PluginBlockTemplate = { name: 'Dummy Plugin' };
constructor(
pluginService: PluginService,
private logger: LoggerService,
) {
super('dummy', [], pluginService);
this.template = { name: 'Dummy Plugin' };
super('dummy-plugin', pluginService);
this.effects = {
onStoreContextData: () => {},
};
}
getPath(): string {
return __dirname;
}
async process() {
const envelope: StdOutgoingTextEnvelope = {
format: OutgoingMessageFormat.text,

View File

@ -0,0 +1 @@
export default [];

View File

@ -10,8 +10,8 @@ import mongoose from 'mongoose';
import { ConversationCreateDto } from '@/chat/dto/conversation.dto';
import {
ConversationModel,
Conversation,
ConversationModel,
} from '@/chat/schemas/conversation.schema';
import { getFixturesWithDefaultValues } from '../defaultValues';
@ -25,7 +25,7 @@ const conversations: ConversationCreateDto[] = [
sender: '0',
active: true,
context: {
channel: 'messenger',
channel: 'messenger-channel',
text: 'Hi',
payload: '',
nlp: {
@ -60,7 +60,7 @@ const conversations: ConversationCreateDto[] = [
foreign_id: '',
labels: [],
assignedTo: null,
channel: { name: 'messenger' },
channel: { name: 'messenger-channel' },
},
skip: {},
attempt: 0,
@ -71,7 +71,7 @@ const conversations: ConversationCreateDto[] = [
{
sender: '1',
context: {
channel: 'offline',
channel: 'offline-channel',
text: 'Hello',
payload: '',
nlp: {
@ -106,7 +106,7 @@ const conversations: ConversationCreateDto[] = [
foreign_id: '',
labels: [],
assignedTo: null,
channel: { name: 'offline' },
channel: { name: 'offline-channel' },
},
skip: {},
attempt: 0,

View File

@ -27,7 +27,7 @@ const subscribers: SubscriberCreateDto[] = [
gender: 'male',
country: 'FR',
channel: {
name: 'messenger',
name: 'messenger-channel',
},
labels: [],
assignedAt: null,
@ -43,7 +43,7 @@ const subscribers: SubscriberCreateDto[] = [
gender: 'male',
country: 'US',
channel: {
name: 'offline',
name: 'offline-channel',
},
labels: [],
assignedAt: null,
@ -59,7 +59,7 @@ const subscribers: SubscriberCreateDto[] = [
gender: 'male',
country: 'US',
channel: {
name: 'offline',
name: 'offline-channel',
},
labels: [],
assignedAt: null,
@ -75,7 +75,7 @@ const subscribers: SubscriberCreateDto[] = [
gender: 'male',
country: 'US',
channel: {
name: 'offline',
name: 'offline-channel',
},
labels: [],
assignedAt: null,

View File

@ -16,7 +16,7 @@ import { modelInstance } from './misc';
import { subscriberInstance } from './subscriber';
export const contextBlankInstance: Context = {
channel: 'offline',
channel: 'offline-channel',
text: '',
payload: undefined,
nlp: { entities: [] },
@ -42,7 +42,7 @@ export const contextEmailVarInstance: Context = {
};
export const contextGetStartedInstance: Context = {
channel: 'offline',
channel: 'offline-channel',
text: 'Get Started',
payload: 'GET_STARTED',
nlp: { entities: [] },

View File

@ -25,7 +25,7 @@ export const subscriberInstance: Subscriber = {
lastvisit: new Date(),
retainedFrom: new Date(),
channel: {
name: 'offline',
name: 'offline-channel',
},
labels: [],
...modelInstance,

View File

@ -0,0 +1,5 @@
import { ChannelName } from '@/channel/types';
import { HelperName } from '@/helper/types';
import { PluginName } from '@/plugins/types';
export type ExtensionName = ChannelName | HelperName | PluginName;

View File

@ -23,5 +23,11 @@
"@/*": ["src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.json", "test/**/*.ts"]
"include": [
"src/**/*.ts",
"src/**/*.json",
"test/**/*.ts",
"src/.hexabot/**/*.ts",
"src/.hexabot/**/*.json"
]
}

View File

@ -33,7 +33,7 @@ export const ChatWidget = () => {
<UiChatWidget
config={{
apiUrl,
channel: "live-chat-tester",
channel: "live-chat-tester-channel",
token: "test",
language: i18n.language,
}}

View File

@ -31,7 +31,9 @@ export const CustomBlocks = () => {
{customBlocks?.map((customBlock) => (
<Block
key={customBlock.id}
title={t(`title.${customBlock.id}`, { ns: customBlock.id })}
title={t(`title.${customBlock.namespace}`, {
ns: customBlock.namespace,
})}
Icon={PluginIcon}
blockTemplate={customBlock.template}
name={customBlock.template.name}

View File

@ -63,7 +63,8 @@ const PluginMessageForm = () => {
<SettingInput
setting={setting}
field={field}
ns={message.plugin}
// @TODO : clean this later
ns={message.plugin.replaceAll("-", "_")}
/>
</FormControl>
)}

View File

@ -22,14 +22,13 @@ export const useRemoteI18n = () => {
const fetchRemoteI18n = async () => {
try {
const additionalTranslations = await apiClient.fetchRemoteI18n();
// Assuming additionalTranslations is an object like { en: { translation: { key: 'value' } } }
Object.keys(additionalTranslations).forEach((lang) => {
Object.keys(additionalTranslations[lang]).forEach((namespace) => {
Object.keys(additionalTranslations).forEach((namespace) => {
Object.keys(additionalTranslations[namespace]).forEach((lang) => {
i18n.addResourceBundle(
lang,
namespace,
additionalTranslations[lang][namespace],
additionalTranslations[namespace][lang],
true,
true,
);

View File

@ -139,4 +139,6 @@ export interface ICustomBlockTemplateAttributes {
// @TODO : templates doe not contain base schema attributes
export interface ICustomBlockTemplate
extends IBaseSchema,
OmitPopulate<ICustomBlockTemplateAttributes, EntityType.CUSTOM_BLOCK> {}
OmitPopulate<ICustomBlockTemplateAttributes, EntityType.CUSTOM_BLOCK> {
namespace: string;
}

View File

@ -61,7 +61,7 @@ Once the widget is built, you can easily embed it into any webpage. Here's an ex
ReactDOM.render(
el(HexabotWidget, {
apiUrl: 'https://api.yourdomain.com',
channel: 'offline',
channel: 'offline-channel',
token: 'token123',
}),
domContainer,
@ -96,7 +96,7 @@ To prevent the website css from conflicting with the chat widget css, we can lev
ReactDOM.render(
React.createElement(HexabotWidget, {
apiUrl: 'https://api.yourdomain.com',
channel: 'offline',
channel: 'offline-channel',
token: 'token123',
}),
shadowContainer,

View File

@ -34,7 +34,7 @@
ReactDOM.render(
React.createElement(HexabotWidget, {
apiUrl: 'http://localhost:4000',
channel: 'offline',
channel: 'offline-channel',
token: 'token123',
}),
shadowContainer,

View File

@ -8,7 +8,7 @@
export const DEFAULT_CONFIG = {
apiUrl: process.env.REACT_APP_WIDGET_API_URL || 'http://localhost:4000',
channel: process.env.REACT_APP_WIDGET_CHANNEL || 'live-chat-tester',
channel: process.env.REACT_APP_WIDGET_CHANNEL || 'live-chat-tester-channel',
token: process.env.REACT_APP_WIDGET_TOKEN || 'test',
language: 'en',
};

View File

@ -18,7 +18,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
<ChatWidget
{...{
apiUrl: process.env.REACT_APP_WIDGET_API_URL || 'http://localhost:4000',
channel: process.env.REACT_APP_WIDGET_CHANNEL || 'offline',
channel: process.env.REACT_APP_WIDGET_CHANNEL || 'offline-channel',
token: process.env.REACT_APP_WIDGET_TOKEN || 'token123',
language: 'en',
}}