mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-07-13 07:04:50 -06:00
refactor: enhance OpenAI controller and service for better credential management
This commit refactors the OpenAIController and OpenAIService to improve credential handling and error management. Key changes include: - Added checks to prevent duplicate API keys and names during bot creation. - Updated the getModels method to accept an optional credential ID for more flexible credential usage. - Enhanced error handling with specific BadRequestException messages for better clarity. - Removed unused methods and streamlined the speech-to-text functionality to utilize instance-specific settings. These improvements enhance the maintainability and usability of the OpenAI integration.
This commit is contained in:
parent
22e99f7934
commit
39aaf29d54
@ -117,7 +117,7 @@ export class OpenaiController extends BaseChatbotController<OpenaiBot, OpenaiDto
|
||||
}
|
||||
}
|
||||
|
||||
// Bots
|
||||
// Override createBot to handle OpenAI-specific credential logic
|
||||
public async createBot(instance: InstanceDto, data: OpenaiDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Openai is disabled');
|
||||
|
||||
@ -206,58 +206,6 @@ export class OpenaiController extends BaseChatbotController<OpenaiBot, OpenaiDto
|
||||
return super.createBot(instance, data);
|
||||
}
|
||||
|
||||
public async findBot(instance: InstanceDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Openai is disabled');
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bots = await this.botRepository.findMany({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bots.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return bots;
|
||||
}
|
||||
|
||||
public async fetchBot(instance: InstanceDto, botId: string) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Openai is disabled');
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
throw new Error('Openai Bot not found');
|
||||
}
|
||||
|
||||
if (bot.instanceId !== instanceId) {
|
||||
throw new Error('Openai Bot not found');
|
||||
}
|
||||
|
||||
return bot;
|
||||
}
|
||||
|
||||
// Process OpenAI-specific bot logic
|
||||
protected async processBot(
|
||||
instance: any,
|
||||
@ -284,8 +232,31 @@ export class OpenaiController extends BaseChatbotController<OpenaiBot, OpenaiDto
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
if (!data.apiKey) throw new Error('API Key is required');
|
||||
if (!data.name) throw new Error('Name is required');
|
||||
if (!data.apiKey) throw new BadRequestException('API Key is required');
|
||||
if (!data.name) throw new BadRequestException('Name is required');
|
||||
|
||||
// Check if API key already exists
|
||||
const existingApiKey = await this.credsRepository.findFirst({
|
||||
where: {
|
||||
apiKey: data.apiKey,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingApiKey) {
|
||||
throw new BadRequestException('This API key is already registered. Please use a different API key.');
|
||||
}
|
||||
|
||||
// Check if name already exists for this instance
|
||||
const existingName = await this.credsRepository.findFirst({
|
||||
where: {
|
||||
name: data.name,
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingName) {
|
||||
throw new BadRequestException('This credential name is already in use. Please choose a different name.');
|
||||
}
|
||||
|
||||
try {
|
||||
const creds = await this.credsRepository.create({
|
||||
@ -449,7 +420,7 @@ export class OpenaiController extends BaseChatbotController<OpenaiBot, OpenaiDto
|
||||
}
|
||||
|
||||
// Models - OpenAI specific functionality
|
||||
public async getModels(instance: InstanceDto) {
|
||||
public async getModels(instance: InstanceDto, openaiCredsId?: string) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Openai is disabled');
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
@ -462,21 +433,40 @@ export class OpenaiController extends BaseChatbotController<OpenaiBot, OpenaiDto
|
||||
|
||||
if (!instanceId) throw new Error('Instance not found');
|
||||
|
||||
const defaultSettings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
include: {
|
||||
OpenaiCreds: true,
|
||||
},
|
||||
});
|
||||
let apiKey: string;
|
||||
|
||||
if (!defaultSettings) throw new Error('Settings not found');
|
||||
if (openaiCredsId) {
|
||||
// Use specific credential ID if provided
|
||||
const creds = await this.credsRepository.findFirst({
|
||||
where: {
|
||||
id: openaiCredsId,
|
||||
instanceId: instanceId, // Ensure the credential belongs to this instance
|
||||
},
|
||||
});
|
||||
|
||||
if (!defaultSettings.OpenaiCreds)
|
||||
throw new Error('OpenAI credentials not found. Please create credentials and associate them with the settings.');
|
||||
if (!creds) throw new Error('OpenAI credentials not found for the provided ID');
|
||||
|
||||
const { apiKey } = defaultSettings.OpenaiCreds;
|
||||
apiKey = creds.apiKey;
|
||||
} else {
|
||||
// Use default credentials from settings if no ID provided
|
||||
const defaultSettings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
include: {
|
||||
OpenaiCreds: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!defaultSettings) throw new Error('Settings not found');
|
||||
|
||||
if (!defaultSettings.OpenaiCreds)
|
||||
throw new Error(
|
||||
'OpenAI credentials not found. Please create credentials and associate them with the settings.',
|
||||
);
|
||||
|
||||
apiKey = defaultSettings.OpenaiCreds.apiKey;
|
||||
}
|
||||
|
||||
try {
|
||||
this.client = new OpenAI({ apiKey });
|
||||
|
@ -153,7 +153,7 @@ export class OpenaiRouter extends RouterBroker {
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => openaiController.getModels(instance),
|
||||
execute: (instance) => openaiController.getModels(instance, req.query.openaiCredsId as string),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
|
@ -1,11 +1,8 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { Integration } from '@api/types/wa.types';
|
||||
import { ConfigService, Language } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { IntegrationSession, OpenaiBot, OpenaiCreds, OpenaiSetting } from '@prisma/client';
|
||||
import { ConfigService, Language, Openai as OpenaiConfig } from '@config/env.config';
|
||||
import { IntegrationSession, OpenaiBot, OpenaiSetting } from '@prisma/client';
|
||||
import { sendTelemetry } from '@utils/sendTelemetry';
|
||||
import axios from 'axios';
|
||||
import { downloadMediaMessage } from 'baileys';
|
||||
@ -33,13 +30,6 @@ export class OpenaiService extends BaseChatbotService<OpenaiBot, OpenaiSetting>
|
||||
return 'openai';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new session specific to OpenAI
|
||||
*/
|
||||
public async createNewSession(instance: InstanceDto, data: any) {
|
||||
return super.createNewSession(instance, data, 'openai');
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the OpenAI client with the provided API key
|
||||
*/
|
||||
@ -82,7 +72,7 @@ export class OpenaiService extends BaseChatbotService<OpenaiBot, OpenaiSetting>
|
||||
this.initClient(creds.apiKey);
|
||||
|
||||
// Transcribe the audio
|
||||
const transcription = await this.speechToText(msg);
|
||||
const transcription = await this.speechToText(msg, instance);
|
||||
|
||||
if (transcription) {
|
||||
this.logger.log(`Audio transcribed: ${transcription}`);
|
||||
@ -149,6 +139,7 @@ export class OpenaiService extends BaseChatbotService<OpenaiBot, OpenaiSetting>
|
||||
const createSession = await this.createNewSession(
|
||||
{ instanceName: instance.instanceName, instanceId: instance.instanceId },
|
||||
data,
|
||||
this.getBotType(),
|
||||
);
|
||||
|
||||
await this.initNewSession(
|
||||
@ -650,159 +641,67 @@ export class OpenaiService extends BaseChatbotService<OpenaiBot, OpenaiSetting>
|
||||
|
||||
/**
|
||||
* Implementation of speech-to-text transcription for audio messages
|
||||
* This overrides the base class implementation with extra functionality
|
||||
* Can be called directly with a message object or with an audio buffer
|
||||
*/
|
||||
public async speechToText(msgOrBuffer: any, updateMediaMessage?: any): Promise<string | null> {
|
||||
try {
|
||||
this.logger.log('Starting speechToText transcription');
|
||||
public async speechToText(msg: any, instance: any): Promise<string | null> {
|
||||
const settings = await this.prismaRepository.openaiSetting.findFirst({
|
||||
where: {
|
||||
instanceId: instance.instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
// Handle direct calls with message object
|
||||
if (msgOrBuffer && (msgOrBuffer.key || msgOrBuffer.message)) {
|
||||
this.logger.log('Processing message object for audio transcription');
|
||||
const audioBuffer = await this.getAudioBufferFromMsg(msgOrBuffer, updateMediaMessage);
|
||||
|
||||
if (!audioBuffer) {
|
||||
this.logger.error('Failed to get audio buffer from message');
|
||||
return null;
|
||||
}
|
||||
|
||||
this.logger.log(`Got audio buffer of size: ${audioBuffer.length} bytes`);
|
||||
|
||||
// Process the audio buffer with the base implementation
|
||||
return this.processAudioTranscription(audioBuffer);
|
||||
}
|
||||
|
||||
// Handle calls with a buffer directly (base implementation)
|
||||
this.logger.log('Processing buffer directly for audio transcription');
|
||||
return this.processAudioTranscription(msgOrBuffer);
|
||||
} catch (err) {
|
||||
this.logger.error(`Error in speechToText: ${err}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to process audio buffer for transcription
|
||||
*/
|
||||
private async processAudioTranscription(audioBuffer: Buffer): Promise<string | null> {
|
||||
if (!this.configService) {
|
||||
this.logger.error('ConfigService not available for speech-to-text transcription');
|
||||
if (!settings) {
|
||||
this.logger.error(`OpenAI settings not found. InstanceId: ${instance.instanceId}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// Use the initialized client's API key if available
|
||||
let apiKey;
|
||||
const creds = await this.prismaRepository.openaiCreds.findUnique({
|
||||
where: { id: settings.openaiCredsId },
|
||||
});
|
||||
|
||||
if (this.client) {
|
||||
// Extract the API key from the initialized client if possible
|
||||
// OpenAI client doesn't expose the API key directly, so we need to use environment or config
|
||||
apiKey = this.configService.get<any>('OPENAI')?.API_KEY || process.env.OPENAI_API_KEY;
|
||||
} else {
|
||||
this.logger.log('No OpenAI client initialized, using config API key');
|
||||
apiKey = this.configService.get<any>('OPENAI')?.API_KEY || process.env.OPENAI_API_KEY;
|
||||
}
|
||||
if (!creds) {
|
||||
this.logger.error(`OpenAI credentials not found. CredsId: ${settings.openaiCredsId}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!apiKey) {
|
||||
this.logger.error('No OpenAI API key set for Whisper transcription');
|
||||
return null;
|
||||
}
|
||||
let audio: Buffer;
|
||||
|
||||
const lang = this.configService.get<Language>('LANGUAGE').includes('pt')
|
||||
? 'pt'
|
||||
: this.configService.get<Language>('LANGUAGE');
|
||||
|
||||
this.logger.log(`Sending audio for transcription with language: ${lang}`);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', audioBuffer, 'audio.ogg');
|
||||
formData.append('model', 'whisper-1');
|
||||
formData.append('language', lang);
|
||||
|
||||
this.logger.log('Making API request to OpenAI Whisper transcription');
|
||||
|
||||
const response = await axios.post('https://api.openai.com/v1/audio/transcriptions', formData, {
|
||||
headers: {
|
||||
...formData.getHeaders(),
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
if (msg.message.mediaUrl) {
|
||||
audio = await axios.get(msg.message.mediaUrl, { responseType: 'arraybuffer' }).then((response) => {
|
||||
return Buffer.from(response.data, 'binary');
|
||||
});
|
||||
|
||||
this.logger.log(`Transcription completed: ${response?.data?.text || 'No text returned'}`);
|
||||
return response?.data?.text || null;
|
||||
} catch (err) {
|
||||
this.logger.error(`Whisper transcription failed: ${JSON.stringify(err.response?.data || err.message || err)}`);
|
||||
return null;
|
||||
} else if (msg.message.base64) {
|
||||
audio = Buffer.from(msg.message.base64, 'base64');
|
||||
} else {
|
||||
// Fallback for raw WhatsApp audio messages that need downloadMediaMessage
|
||||
audio = await downloadMediaMessage(
|
||||
{ key: msg.key, message: msg?.message },
|
||||
'buffer',
|
||||
{},
|
||||
{
|
||||
logger: P({ level: 'error' }) as any,
|
||||
reuploadRequest: instance,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to convert message to audio buffer
|
||||
*/
|
||||
private async getAudioBufferFromMsg(msg: any, updateMediaMessage: any): Promise<Buffer | null> {
|
||||
try {
|
||||
this.logger.log('Getting audio buffer from message');
|
||||
this.logger.log(`Message type: ${msg.messageType}, has media URL: ${!!msg?.message?.mediaUrl}`);
|
||||
const lang = this.configService.get<Language>('LANGUAGE').includes('pt')
|
||||
? 'pt'
|
||||
: this.configService.get<Language>('LANGUAGE');
|
||||
|
||||
let audio;
|
||||
const formData = new FormData();
|
||||
formData.append('file', audio, 'audio.ogg');
|
||||
formData.append('model', 'whisper-1');
|
||||
formData.append('language', lang);
|
||||
|
||||
if (msg?.message?.mediaUrl) {
|
||||
this.logger.log(`Getting audio from media URL: ${msg.message.mediaUrl}`);
|
||||
audio = await axios.get(msg.message.mediaUrl, { responseType: 'arraybuffer' }).then((response) => {
|
||||
return Buffer.from(response.data, 'binary');
|
||||
});
|
||||
} else if (msg?.message?.audioMessage) {
|
||||
// Handle WhatsApp audio messages
|
||||
this.logger.log('Getting audio from audioMessage');
|
||||
audio = await downloadMediaMessage(
|
||||
{ key: msg.key, message: msg?.message },
|
||||
'buffer',
|
||||
{},
|
||||
{
|
||||
logger: P({ level: 'error' }) as any,
|
||||
reuploadRequest: updateMediaMessage,
|
||||
},
|
||||
);
|
||||
} else if (msg?.message?.pttMessage) {
|
||||
// Handle PTT voice messages
|
||||
this.logger.log('Getting audio from pttMessage');
|
||||
audio = await downloadMediaMessage(
|
||||
{ key: msg.key, message: msg?.message },
|
||||
'buffer',
|
||||
{},
|
||||
{
|
||||
logger: P({ level: 'error' }) as any,
|
||||
reuploadRequest: updateMediaMessage,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
this.logger.log('No recognized audio format found');
|
||||
audio = await downloadMediaMessage(
|
||||
{ key: msg.key, message: msg?.message },
|
||||
'buffer',
|
||||
{},
|
||||
{
|
||||
logger: P({ level: 'error' }) as any,
|
||||
reuploadRequest: updateMediaMessage,
|
||||
},
|
||||
);
|
||||
}
|
||||
const apiKey = creds?.apiKey || this.configService.get<OpenaiConfig>('OPENAI').API_KEY_GLOBAL;
|
||||
|
||||
if (audio) {
|
||||
this.logger.log(`Successfully obtained audio buffer of size: ${audio.length} bytes`);
|
||||
} else {
|
||||
this.logger.error('Failed to obtain audio buffer');
|
||||
}
|
||||
const response = await axios.post('https://api.openai.com/v1/audio/transcriptions', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
});
|
||||
|
||||
return audio;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error getting audio buffer: ${error.message || JSON.stringify(error)}`);
|
||||
if (error.response) {
|
||||
this.logger.error(`API response status: ${error.response.status}`);
|
||||
this.logger.error(`API response data: ${JSON.stringify(error.response.data || {})}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return response?.data?.text;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user