feat: Integration with Dify

Adds support for Dify integration, including new routes, services, and controllers. The configuration for Dify has been added to the environment file, and the necessary changes have been made to the `.env.example` file. Additionally, the WhatsApp Baileys service has been updated to handle Dify notifications.

Modified files:
- `.env.example`
- `package.json`
- `src/api/integrations/openai/services/openai.service.ts`
- `src/api/routes/index.router.ts`
- `src/api/server.module.ts`
- `src/api/services/channel.service.ts`
- `src/api/services/channels/whatsapp.baileys.service.ts`
- `src/config/env.config.ts`
- `src/validate/validate.schema.ts`

Introduced files:
- `src/api/integrations/dify/`
- `src/api/integrations/dify/controllers/dify.controller.ts`
- `src/api/integrations/dify/dto/dify.dto.ts`
- `src/api/integrations/dify/routes/dify.router.ts`
- `src/api/integrations/dify/services/dify.service.ts`
- `src/api/integrations/dify/validate/dify.schema.ts`
This commit is contained in:
Davidson Gomes 2024-07-30 13:34:35 -03:00
parent b604e4ecc7
commit a5d72a0dfd
14 changed files with 1698 additions and 2 deletions

View File

@ -122,6 +122,8 @@ CHATWOOT_IMPORT_PLACEHOLDER_MEDIA_MESSAGE=false
OPENAI_ENABLED=false
OPENAI_API_KEY_GLOBAL=
DIFY_ENABLED=false
CACHE_REDIS_ENABLED=true
CACHE_REDIS_URI=redis://localhost:6379/6
CACHE_REDIS_PREFIX_KEY=evolution

View File

@ -1,6 +1,6 @@
{
"name": "evolution-api",
"version": "2.0.4-beta",
"version": "2.0.4-rc",
"description": "Rest api for communication with WhatsApp",
"main": "./dist/src/main.js",
"scripts": {

View File

@ -0,0 +1,69 @@
import { configService, Dify } from '../../../../config/env.config';
import { BadRequestException } from '../../../../exceptions';
import { InstanceDto } from '../../../dto/instance.dto';
import { DifyDto, DifyIgnoreJidDto } from '../dto/dify.dto';
import { DifyService } from '../services/dify.service';
export class DifyController {
constructor(private readonly difyService: DifyService) {}
public async createDify(instance: InstanceDto, data: DifyDto) {
if (!configService.get<Dify>('DIFY').ENABLED) throw new BadRequestException('Dify is disabled');
return this.difyService.create(instance, data);
}
public async findDify(instance: InstanceDto) {
if (!configService.get<Dify>('DIFY').ENABLED) throw new BadRequestException('Dify is disabled');
return this.difyService.find(instance);
}
public async fetchDify(instance: InstanceDto, difyId: string) {
if (!configService.get<Dify>('DIFY').ENABLED) throw new BadRequestException('Dify is disabled');
return this.difyService.fetch(instance, difyId);
}
public async updateDify(instance: InstanceDto, difyId: string, data: DifyDto) {
if (!configService.get<Dify>('DIFY').ENABLED) throw new BadRequestException('Dify is disabled');
return this.difyService.update(instance, difyId, data);
}
public async deleteDify(instance: InstanceDto, difyId: string) {
if (!configService.get<Dify>('DIFY').ENABLED) throw new BadRequestException('Dify is disabled');
return this.difyService.delete(instance, difyId);
}
public async settings(instance: InstanceDto, data: any) {
if (!configService.get<Dify>('DIFY').ENABLED) throw new BadRequestException('Dify is disabled');
return this.difyService.setDefaultSettings(instance, data);
}
public async fetchSettings(instance: InstanceDto) {
if (!configService.get<Dify>('DIFY').ENABLED) throw new BadRequestException('Dify is disabled');
return this.difyService.fetchDefaultSettings(instance);
}
public async changeStatus(instance: InstanceDto, data: any) {
if (!configService.get<Dify>('DIFY').ENABLED) throw new BadRequestException('Dify is disabled');
return this.difyService.changeStatus(instance, data);
}
public async fetchSessions(instance: InstanceDto, difyId: string) {
if (!configService.get<Dify>('DIFY').ENABLED) throw new BadRequestException('Dify is disabled');
return this.difyService.fetchSessions(instance, difyId);
}
public async ignoreJid(instance: InstanceDto, data: DifyIgnoreJidDto) {
if (!configService.get<Dify>('DIFY').ENABLED) throw new BadRequestException('Dify is disabled');
return this.difyService.ignoreJid(instance, data);
}
}

View File

@ -0,0 +1,46 @@
import { $Enums, TriggerOperator, TriggerType } from '@prisma/client';
export class Session {
remoteJid?: string;
sessionId?: string;
status?: string;
createdAt?: number;
updateAt?: number;
}
export class DifyDto {
enabled?: boolean;
botType?: $Enums.DifyBotType;
apiUrl?: string;
apiKey?: string;
expire?: number;
keywordFinish?: string;
delayMessage?: number;
unknownMessage?: string;
listeningFromMe?: boolean;
stopBotFromMe?: boolean;
keepOpen?: boolean;
debounceTime?: number;
triggerType?: TriggerType;
triggerOperator?: TriggerOperator;
triggerValue?: string;
ignoreJids?: any;
}
export class DifySettingDto {
expire?: number;
keywordFinish?: string;
delayMessage?: number;
unknownMessage?: string;
listeningFromMe?: boolean;
stopBotFromMe?: boolean;
keepOpen?: boolean;
debounceTime?: number;
difyIdFallback?: string;
ignoreJids?: any;
}
export class DifyIgnoreJidDto {
remoteJid?: string;
action?: string;
}

View File

@ -0,0 +1,123 @@
import { RequestHandler, Router } from 'express';
import {
difyIgnoreJidSchema,
difySchema,
difySettingSchema,
difyStatusSchema,
instanceSchema,
} from '../../../../validate/validate.schema';
import { RouterBroker } from '../../../abstract/abstract.router';
import { InstanceDto } from '../../../dto/instance.dto';
import { HttpStatus } from '../../../routes/index.router';
import { difyController } from '../../../server.module';
import { DifyDto, DifyIgnoreJidDto, DifySettingDto } from '../dto/dify.dto';
export class DifyRouter extends RouterBroker {
constructor(...guards: RequestHandler[]) {
super();
this.router
.post(this.routerPath('create'), ...guards, async (req, res) => {
const response = await this.dataValidate<DifyDto>({
request: req,
schema: difySchema,
ClassRef: DifyDto,
execute: (instance, data) => difyController.createDify(instance, data),
});
res.status(HttpStatus.CREATED).json(response);
})
.get(this.routerPath('find'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => difyController.findDify(instance),
});
res.status(HttpStatus.OK).json(response);
})
.get(this.routerPath('fetch/:difyId'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => difyController.fetchDify(instance, req.params.difyId),
});
res.status(HttpStatus.OK).json(response);
})
.put(this.routerPath('update/:difyId'), ...guards, async (req, res) => {
const response = await this.dataValidate<DifyDto>({
request: req,
schema: difySchema,
ClassRef: DifyDto,
execute: (instance, data) => difyController.updateDify(instance, req.params.difyId, data),
});
res.status(HttpStatus.OK).json(response);
})
.delete(this.routerPath('delete/:difyId'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => difyController.deleteDify(instance, req.params.difyId),
});
res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('settings'), ...guards, async (req, res) => {
const response = await this.dataValidate<DifySettingDto>({
request: req,
schema: difySettingSchema,
ClassRef: DifySettingDto,
execute: (instance, data) => difyController.settings(instance, data),
});
res.status(HttpStatus.OK).json(response);
})
.get(this.routerPath('fetchSettings'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => difyController.fetchSettings(instance),
});
res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('changeStatus'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: difyStatusSchema,
ClassRef: InstanceDto,
execute: (instance, data) => difyController.changeStatus(instance, data),
});
res.status(HttpStatus.OK).json(response);
})
.get(this.routerPath('fetchSessions/:difyId'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => difyController.fetchSessions(instance, req.params.difyId),
});
res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('ignoreJid'), ...guards, async (req, res) => {
const response = await this.dataValidate<DifyIgnoreJidDto>({
request: req,
schema: difyIgnoreJidSchema,
ClassRef: DifyIgnoreJidDto,
execute: (instance, data) => difyController.ignoreJid(instance, data),
});
res.status(HttpStatus.OK).json(response);
});
}
public readonly router = Router();
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,107 @@
import { JSONSchema7 } from 'json-schema';
import { v4 } from 'uuid';
const isNotEmpty = (...propertyNames: string[]): JSONSchema7 => {
const properties = {};
propertyNames.forEach(
(property) =>
(properties[property] = {
minLength: 1,
description: `The "${property}" cannot be empty`,
}),
);
return {
if: {
propertyNames: {
enum: [...propertyNames],
},
},
then: { properties },
};
};
export const difySchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
enabled: { type: 'boolean' },
botType: { type: 'string', enum: ['chatBot', 'textGenerator', 'agent', 'workflow'] },
apiUrl: { type: 'string' },
apiKey: { type: 'string' },
triggerType: { type: 'string', enum: ['all', 'keyword', 'none'] },
triggerOperator: { type: 'string', enum: ['equals', 'contains', 'startsWith', 'endsWith', 'regex'] },
triggerValue: { type: 'string' },
expire: { type: 'integer' },
keywordFinish: { type: 'string' },
delayMessage: { type: 'integer' },
unknownMessage: { type: 'string' },
listeningFromMe: { type: 'boolean' },
stopBotFromMe: { type: 'boolean' },
keepOpen: { type: 'boolean' },
debounceTime: { type: 'integer' },
ignoreJids: { type: 'array', items: { type: 'string' } },
},
required: ['enabled', 'botType', 'triggerType'],
...isNotEmpty('enabled', 'botType', 'triggerType'),
};
export const difyStatusSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
remoteJid: { type: 'string' },
status: { type: 'string', enum: ['opened', 'closed', 'paused', 'delete'] },
},
required: ['remoteJid', 'status'],
...isNotEmpty('remoteJid', 'status'),
};
export const difySettingSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
expire: { type: 'integer' },
keywordFinish: { type: 'string' },
delayMessage: { type: 'integer' },
unknownMessage: { type: 'string' },
listeningFromMe: { type: 'boolean' },
stopBotFromMe: { type: 'boolean' },
keepOpen: { type: 'boolean' },
debounceTime: { type: 'integer' },
ignoreJids: { type: 'array', items: { type: 'string' } },
difyIdFallback: { type: 'string' },
},
required: [
'expire',
'keywordFinish',
'delayMessage',
'unknownMessage',
'listeningFromMe',
'stopBotFromMe',
'keepOpen',
'debounceTime',
'ignoreJids',
],
...isNotEmpty(
'expire',
'keywordFinish',
'delayMessage',
'unknownMessage',
'listeningFromMe',
'stopBotFromMe',
'keepOpen',
'debounceTime',
'ignoreJids',
),
};
export const difyIgnoreJidSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
remoteJid: { type: 'string' },
action: { type: 'string', enum: ['add', 'remove'] },
},
required: ['remoteJid', 'action'],
...isNotEmpty('remoteJid', 'action'),
};

View File

@ -730,7 +730,7 @@ export class OpenaiService {
})
.then((instance) => instance.id);
const defaultSettingCheck = await this.prismaRepository.typebotSetting.findFirst({
const defaultSettingCheck = await this.prismaRepository.openaiSetting.findFirst({
where: {
instanceId,
},

View File

@ -8,6 +8,7 @@ import { authGuard } from '../guards/auth.guard';
import { instanceExistsGuard, instanceLoggedGuard } from '../guards/instance.guard';
import Telemetry from '../guards/telemetry.guard';
import { ChatwootRouter } from '../integrations/chatwoot/routes/chatwoot.router';
import { DifyRouter } from '../integrations/dify/routes/dify.router';
import { OpenaiRouter } from '../integrations/openai/routes/openai.router';
import { RabbitmqRouter } from '../integrations/rabbitmq/routes/rabbitmq.router';
import { S3Router } from '../integrations/s3/routes/s3.router';
@ -88,6 +89,7 @@ router
.use('/label', new LabelRouter(...guards).router)
.use('/s3', new S3Router(...guards).router)
.use('/openai', new OpenaiRouter(...guards).router)
.use('/dify', new DifyRouter(...guards).router)
.get('/webhook/meta', async (req, res) => {
if (req.query['hub.verify_token'] === configService.get<WaBusiness>('WA_BUSINESS').TOKEN_WEBHOOK)
res.send(req.query['hub.challenge']);

View File

@ -13,6 +13,8 @@ import { TemplateController } from './controllers/template.controller';
import { WebhookController } from './controllers/webhook.controller';
import { ChatwootController } from './integrations/chatwoot/controllers/chatwoot.controller';
import { ChatwootService } from './integrations/chatwoot/services/chatwoot.service';
import { DifyController } from './integrations/dify/controllers/dify.controller';
import { DifyService } from './integrations/dify/services/dify.service';
import { OpenaiController } from './integrations/openai/controllers/openai.controller';
import { OpenaiService } from './integrations/openai/services/openai.service';
import { RabbitmqController } from './integrations/rabbitmq/controllers/rabbitmq.controller';
@ -70,6 +72,9 @@ export const typebotController = new TypebotController(typebotService);
const openaiService = new OpenaiService(waMonitor, configService, prismaRepository);
export const openaiController = new OpenaiController(openaiService);
const difyService = new DifyService(waMonitor, configService, prismaRepository);
export const difyController = new DifyController(difyService);
const s3Service = new S3Service(prismaRepository);
export const s3Controller = new S3Controller(s3Service);

View File

@ -26,6 +26,7 @@ import { SettingsDto } from '../dto/settings.dto';
import { WebhookDto } from '../dto/webhook.dto';
import { ChatwootDto } from '../integrations/chatwoot/dto/chatwoot.dto';
import { ChatwootService } from '../integrations/chatwoot/services/chatwoot.service';
import { DifyService } from '../integrations/dify/services/dify.service';
import { OpenaiService } from '../integrations/openai/services/openai.service';
import { RabbitmqDto } from '../integrations/rabbitmq/dto/rabbitmq.dto';
import { getAMQP, removeQueues } from '../integrations/rabbitmq/libs/amqp.server';
@ -71,6 +72,8 @@ export class ChannelStartupService {
public openaiService = new OpenaiService(waMonitor, this.configService, this.prismaRepository);
public difyService = new DifyService(waMonitor, this.configService, this.prismaRepository);
public setInstance(instance: InstanceDto) {
this.logger.setInstance(instance.instanceName);

View File

@ -68,6 +68,7 @@ import {
configService,
ConfigSessionPhone,
Database,
Dify,
Log,
Openai,
ProviderSession,
@ -1187,6 +1188,17 @@ export class BaileysStartupService extends ChannelStartupService {
}
}
if (this.configService.get<Dify>('DIFY').ENABLED) {
if (type === 'notify') {
if (messageRaw.messageType !== 'reactionMessage')
await this.difyService.sendDify(
{ instanceName: this.instance.name, instanceId: this.instanceId },
messageRaw.key.remoteJid,
messageRaw,
);
}
}
const contact = await this.prismaRepository.contact.findFirst({
where: { remoteJid: received.key.remoteJid, instanceId: this.instanceId },
});

View File

@ -192,6 +192,7 @@ export type Chatwoot = {
};
};
export type Openai = { ENABLED: boolean; API_KEY_GLOBAL?: string };
export type Dify = { ENABLED: boolean };
export type S3 = {
ACCESS_KEY: string;
@ -226,6 +227,7 @@ export interface Env {
TYPEBOT: Typebot;
CHATWOOT: Chatwoot;
OPENAI: Openai;
DIFY: Dify;
CACHE: CacheConf;
S3?: S3;
AUTHENTICATION: Auth;
@ -437,6 +439,9 @@ export class ConfigService {
ENABLED: process.env?.OPENAI_ENABLED === 'true',
API_KEY_GLOBAL: process.env?.OPENAI_API_KEY_GLOBAL || null,
},
DIFY: {
ENABLED: process.env?.DIFY_ENABLED === 'true',
},
CACHE: {
REDIS: {
ENABLED: process.env?.CACHE_REDIS_ENABLED === 'true',

View File

@ -1,5 +1,6 @@
// Integrations Schema
export * from '../api/integrations/chatwoot/validate/chatwoot.schema';
export * from '../api/integrations/dify/validate/dify.schema';
export * from '../api/integrations/openai/validate/openai.schema';
export * from '../api/integrations/rabbitmq/validate/rabbitmq.schema';
export * from '../api/integrations/sqs/validate/sqs.schema';