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:
Guilherme Gomes 2025-05-27 14:32:10 -03:00
parent 22e99f7934
commit 39aaf29d54
3 changed files with 110 additions and 221 deletions

View File

@ -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 });

View File

@ -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);

View File

@ -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;
}
}