diff --git a/config.py b/config.py index 735dda9..2edfc62 100644 --- a/config.py +++ b/config.py @@ -39,6 +39,8 @@ class Settings: """Inicializa as configurações.""" logger.debug("Carregando configurações do Redis...") + self.ACTIVE_LLM_PROVIDER = self.get_redis_value("ACTIVE_LLM_PROVIDER", "groq") + self.OPENAI_API_KEY = self.get_redis_value("OPENAI_API_KEY", "") self.GROQ_API_KEY = self.get_redis_value("GROQ_API_KEY", "gsk_default_key") self.BUSINESS_MESSAGE = self.get_redis_value("BUSINESS_MESSAGE", "*Impacte AI* Premium Services") self.PROCESS_GROUP_MESSAGES = self.get_redis_value("PROCESS_GROUP_MESSAGES", "false").lower() == "true" diff --git a/manager.py b/manager.py index ea679d9..61b4273 100644 --- a/manager.py +++ b/manager.py @@ -252,7 +252,7 @@ def login_page(): # Modificar a função de logout no dashboard def dashboard(): # Versão do sistema - APP_VERSION = "2.3.2" + APP_VERSION = "2.3.3" show_logo() st.sidebar.markdown('', unsafe_allow_html=True) @@ -728,9 +728,10 @@ def manage_settings(): st.title("⚙️ Configurações") # Criar tabs para melhor organização - tab1, tab2, tab3, tab4 = st.tabs([ - "🔑 Chaves API", - "🌐 Configurações Gerais", + tab1, tab2, tab3, tab4, tab5 = st.tabs([ + "🔑 Chaves API", + "🤖 Provedor LLM", + "🌐 Configurações Gerais", "📝 Formatação de Mensagens", "🗣️ Idiomas e Transcrição" ]) @@ -787,6 +788,46 @@ def manage_settings(): pass with tab2: + st.subheader("Configuração do Provedor LLM") + + # Select provider + current_provider = storage.get_llm_provider() + provider = st.selectbox( + "Provedor de Serviço", + options=["groq", "openai"], + format_func=lambda x: "Groq (Open Source)" if x == "groq" else "OpenAI (API Paga)", + index=0 if current_provider == "groq" else 1 + ) + + if provider == "openai": + st.info(""" + A OpenAI é um serviço pago que requer uma chave API válida. + Obtenha sua chave em https://platform.openai.com + """) + + # OpenAI Key Management + openai_key = st.text_input( + "OpenAI API Key", + type="password", + help="Chave que começa com 'sk-'" + ) + + if st.button("Adicionar Chave OpenAI"): + if openai_key and openai_key.startswith("sk-"): + storage.add_openai_key(openai_key) + st.success("✅ Chave OpenAI adicionada com sucesso!") + else: + st.error("Chave inválida! Deve começar com 'sk-'") + + # Save provider selection + if st.button("💾 Salvar Configuração do Provedor"): + try: + storage.set_llm_provider(provider) + st.success(f"Provedor alterado para: {provider}") + except Exception as e: + st.error(f"Erro ao salvar provedor: {str(e)}") + + with tab3: st.subheader("Configurações do Sistema") # Business Message @@ -850,7 +891,7 @@ def manage_settings(): ) pass - with tab3: + with tab4: st.subheader("Formatação de Mensagens") # Headers personalizados @@ -935,7 +976,7 @@ def manage_settings(): st.error(f"Erro ao salvar configurações: {str(e)}") - with tab4: + with tab5: st.subheader("Idiomas e Transcrição") # Adicionar estatísticas no topo diff --git a/openai_handler.py b/openai_handler.py new file mode 100644 index 0000000..76486e4 --- /dev/null +++ b/openai_handler.py @@ -0,0 +1,74 @@ +import aiohttp +import json +from datetime import datetime +import logging +from storage import StorageHandler + +logger = logging.getLogger("OpenAIHandler") +logger.setLevel(logging.DEBUG) +handler = logging.StreamHandler() +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) +logger.addHandler(handler) + +async def test_openai_key(key: str) -> bool: + """Test if an OpenAI key is valid and working.""" + url = "https://api.openai.com/v1/models" + headers = {"Authorization": f"Bearer {key}"} + + try: + async with aiohttp.ClientSession() as session: + async with session.get(url, headers=headers) as response: + if response.status == 200: + data = await response.json() + return len(data.get("data", [])) > 0 + return False + except Exception as e: + logger.error(f"Error testing OpenAI key: {e}") + return False + +async def handle_openai_request( + url: str, + headers: dict, + data: any, + storage: StorageHandler, + is_form_data: bool = False +) -> tuple[bool, dict, str]: + """Handle requests to OpenAI API with retries.""" + max_retries = 3 + + for attempt in range(max_retries): + try: + async with aiohttp.ClientSession() as session: + if is_form_data: + async with session.post(url, headers=headers, data=data) as response: + response_data = await response.json() + if response.status == 200: + if is_form_data and response_data.get("text"): + return True, response_data, "" + elif not is_form_data and response_data.get("choices"): + return True, response_data, "" + else: + async with session.post(url, headers=headers, json=data) as response: + response_data = await response.json() + if response.status == 200 and response_data.get("choices"): + return True, response_data, "" + + error_msg = response_data.get("error", {}).get("message", "") + + if "invalid_api_key" in error_msg or "invalid authorization" in error_msg.lower(): + logger.error(f"OpenAI API key invalid or expired") + return False, response_data, error_msg + + if attempt < max_retries - 1: + continue + + return False, response_data, error_msg + + except Exception as e: + logger.error(f"Error in request: {str(e)}") + if attempt < max_retries - 1: + continue + return False, {}, f"Request failed: {str(e)}" + + return False, {}, "All retries failed" \ No newline at end of file diff --git a/services.py b/services.py index 40c75bf..9ad8943 100644 --- a/services.py +++ b/services.py @@ -47,6 +47,7 @@ async def summarize_text_if_needed(text): storage.add_log("DEBUG", "Iniciando processo de resumo", { "text_length": len(text) }) + provider = storage.get_llm_provider() # Obter idioma configurado language = redis_client.get("TRANSCRIPTION_LANGUAGE") or "pt" @@ -54,12 +55,20 @@ async def summarize_text_if_needed(text): "language": language, "redis_value": redis_client.get("TRANSCRIPTION_LANGUAGE") }) - url_completions = "https://api.groq.com/openai/v1/chat/completions" - groq_key = await get_working_groq_key(storage) - if not groq_key: - raise Exception("Nenhuma chave GROQ disponível") + + if provider == "openai": + api_key = storage.get_openai_keys()[0] + url = "https://api.openai.com/v1/chat/completions" + model = "gpt-4o-mini" + else: # groq + url = "https://api.groq.com/openai/v1/chat/completions" + api_key = await get_working_groq_key(storage) + if not api_key: + raise Exception("Nenhuma chave GROQ disponível") + model = "llama-3.3-70b-versatile" + headers = { - "Authorization": f"Bearer {groq_key}", + "Authorization": f"Bearer {api_key}", "Content-Type": "application/json", } @@ -143,11 +152,11 @@ async def summarize_text_if_needed(text): "role": "user", "content": f"{base_prompt}\n\nTexto para resumir: {text}", }], - "model": "llama-3.3-70b-versatile", + "model": model, } try: - success, response_data, error = await handle_groq_request(url_completions, headers, json_data, storage, is_form_data=False) + success, response_data, error = await handle_groq_request(url, headers, json_data, storage, is_form_data=False) if not success: raise Exception(error) @@ -195,12 +204,20 @@ async def transcribe_audio(audio_source, apikey=None, remote_jid=None, from_me=F "from_me": from_me, "remote_jid": remote_jid }) + provider = storage.get_llm_provider() - url = "https://api.groq.com/openai/v1/audio/transcriptions" - groq_key = await get_working_groq_key(storage) - if not groq_key: - raise Exception("Nenhuma chave GROQ disponível") - groq_headers = {"Authorization": f"Bearer {groq_key}"} + if provider == "openai": + api_key = storage.get_openai_keys()[0] # Get first OpenAI key + url = "https://api.openai.com/v1/audio/transcriptions" + model = "whisper-1" + else: # groq + api_key = await get_working_groq_key(storage) + if not api_key: + raise Exception("Nenhuma chave GROQ disponível") + url = "https://api.groq.com/openai/v1/audio/transcriptions" + model = "whisper-large-v3" + + headers = {"Authorization": f"Bearer {api_key}"} # Inicializar variáveis contact_language = None @@ -238,9 +255,9 @@ async def transcribe_audio(audio_source, apikey=None, remote_jid=None, from_me=F with open(audio_source, 'rb') as audio_file: data = aiohttp.FormData() data.add_field('file', audio_file, filename='audio.mp3') - data.add_field('model', 'whisper-large-v3') + data.add_field('model', model) - success, response_data, error = await handle_groq_request(url, groq_headers, data, storage, is_form_data=True) + success, response_data, error = await handle_groq_request(url, headers, data, storage, is_form_data=True) if success: initial_text = response_data.get("text", "") @@ -311,14 +328,14 @@ async def transcribe_audio(audio_source, apikey=None, remote_jid=None, from_me=F with open(audio_source, 'rb') as audio_file: data = aiohttp.FormData() data.add_field('file', audio_file, filename='audio.mp3') - data.add_field('model', 'whisper-large-v3') + data.add_field('model', model) data.add_field('language', transcription_language) if use_timestamps: data.add_field('response_format', 'verbose_json') # Usar handle_groq_request para ter retry e validação - success, response_data, error = await handle_groq_request(url, groq_headers, data, storage, is_form_data=True) + success, response_data, error = await handle_groq_request(url, headers, data, storage, is_form_data=True) if not success: raise Exception(f"Erro na transcrição: {error}") @@ -428,6 +445,7 @@ async def detect_language(text: str) -> str: Returns: str: Código ISO 639-1 do idioma detectado """ + provider = storage.get_llm_provider() storage.add_log("DEBUG", "Iniciando detecção de idioma", { "text_length": len(text) }) @@ -437,14 +455,19 @@ async def detect_language(text: str) -> str: "pt", "en", "es", "fr", "de", "it", "ja", "ko", "zh", "ro", "ru", "ar", "hi", "nl", "pl", "tr" } - - url_completions = "https://api.groq.com/openai/v1/chat/completions" - groq_key = await get_working_groq_key(storage) - if not groq_key: - raise Exception("Nenhuma chave GROQ disponível") - + if provider == "openai": + api_key = storage.get_openai_keys()[0] + url = "https://api.openai.com/v1/chat/completions" + model = "gpt-4o-mini" + else: # groq + url = "https://api.groq.com/openai/v1/chat/completions" + api_key = await get_working_groq_key(storage) + if not api_key: + raise Exception("Nenhuma chave GROQ disponível") + model = "llama-3.3-70b-versatile" + headers = { - "Authorization": f"Bearer {groq_key}", + "Authorization": f"Bearer {api_key}", "Content-Type": "application/json", } @@ -473,12 +496,12 @@ async def detect_language(text: str) -> str: "role": "user", "content": f"{prompt}\n\n{text[:500]}" # Limitando para os primeiros 500 caracteres }], - "model": "llama-3.3-70b-versatile", + "model": model, "temperature": 0.1 } try: - success, response_data, error = await handle_groq_request(url_completions, headers, json_data, storage, is_form_data=False) + success, response_data, error = await handle_groq_request(url, headers, json_data, storage, is_form_data=False) if not success: raise Exception(f"Falha na detecção de idioma: {error}") @@ -640,97 +663,104 @@ async def format_message(transcription_text, summary_text=None): return "\n\n".join(message_parts) async def translate_text(text: str, source_language: str, target_language: str) -> str: - """ - Traduz o texto usando a API GROQ - - Args: - text: Texto para traduzir - source_language: Código ISO 639-1 do idioma de origem - target_language: Código ISO 639-1 do idioma de destino - - Returns: - str: Texto traduzido - """ - storage.add_log("DEBUG", "Iniciando tradução", { + """ + Traduz o texto usando a API GROQ + + Args: + text: Texto para traduzir + source_language: Código ISO 639-1 do idioma de origem + target_language: Código ISO 639-1 do idioma de destino + + Returns: + str: Texto traduzido + """ + provider = storage.get_llm_provider() + storage.add_log("DEBUG", "Iniciando tradução", { "source_language": source_language, "target_language": target_language, "text_length": len(text) - }) + }) - # Se os idiomas forem iguais, retorna o texto original - if source_language == target_language: - return text + # Se os idiomas forem iguais, retorna o texto original + if source_language == target_language: + return text - url_completions = "https://api.groq.com/openai/v1/chat/completions" - groq_key = await get_working_groq_key(storage) - if not groq_key: - raise Exception("Nenhuma chave GROQ disponível") + if provider == "openai": + api_key = storage.get_openai_keys()[0] + url = "https://api.openai.com/v1/chat/completions" + model = "gpt-4o-mini" + else: # groq + url = "https://api.groq.com/openai/v1/chat/completions" + api_key = await get_working_groq_key(storage) + if not api_key: + raise Exception("Nenhuma chave GROQ disponível") + model = "llama-3.3-70b-versatile" + + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + } - headers = { - "Authorization": f"Bearer {groq_key}", - "Content-Type": "application/json", - } - - prompt = f""" - Você é um tradutor profissional especializado em manter o tom e estilo do texto original. - - Instruções: - 1. Traduza o texto de {source_language} para {target_language} - 2. Preserve todas as formatações (negrito, itálico, emojis) - 3. Mantenha os mesmos parágrafos e quebras de linha - 4. Preserve números, datas e nomes próprios - 5. Não adicione ou remova informações - 6. Não inclua notas ou explicações - 7. Mantenha o mesmo nível de formalidade - - Texto para tradução: - {text} - """ - - json_data = { - "messages": [{ - "role": "system", - "content": "Você é um tradutor profissional que mantém o estilo e formatação do texto original." - }, { - "role": "user", - "content": prompt - }], - "model": "llama-3.3-70b-versatile", - "temperature": 0.3 - } + prompt = f""" + Você é um tradutor profissional especializado em manter o tom e estilo do texto original. + + Instruções: + 1. Traduza o texto de {source_language} para {target_language} + 2. Preserve todas as formatações (negrito, itálico, emojis) + 3. Mantenha os mesmos parágrafos e quebras de linha + 4. Preserve números, datas e nomes próprios + 5. Não adicione ou remova informações + 6. Não inclua notas ou explicações + 7. Mantenha o mesmo nível de formalidade + + Texto para tradução: + {text} + """ + + json_data = { + "messages": [{ + "role": "system", + "content": "Você é um tradutor profissional que mantém o estilo e formatação do texto original." + }, { + "role": "user", + "content": prompt + }], + "model": model, + "temperature": 0.3 + } - try: - success, response_data, error = await handle_groq_request(url_completions, headers, json_data, storage, is_form_data=False) - if not success: - raise Exception(f"Falha na tradução: {error}") - - translated_text = response_data["choices"][0]["message"]["content"].strip() - - # Verificar se a tradução manteve aproximadamente o mesmo tamanho - length_ratio = len(translated_text) / len(text) - if not (0.5 <= length_ratio <= 1.5): - storage.add_log("WARNING", "Possível erro na tradução - diferença significativa no tamanho", { - "original_length": len(text), - "translated_length": len(translated_text), - "ratio": length_ratio - }) - - # Validar se a tradução não está vazia - if not await validate_transcription_response(translated_text): - storage.add_log("ERROR", "Tradução vazia ou inválida recebida") - raise Exception("Tradução vazia ou inválida recebida") - - storage.add_log("INFO", "Tradução concluída com sucesso", { - "original_length": len(text), - "translated_length": len(translated_text), - "ratio": length_ratio - }) - - return translated_text + try: + success, response_data, error = await handle_groq_request(url, headers, json_data, storage, is_form_data=False) + if not success: + raise Exception(f"Falha na tradução: {error}") + + translated_text = response_data["choices"][0]["message"]["content"].strip() + + # Verificar se a tradução manteve aproximadamente o mesmo tamanho + length_ratio = len(translated_text) / len(text) + if not (0.5 <= length_ratio <= 1.5): + storage.add_log("WARNING", "Possível erro na tradução - diferença significativa no tamanho", { + "original_length": len(text), + "translated_length": len(translated_text), + "ratio": length_ratio + }) + + # Validar se a tradução não está vazia + if not await validate_transcription_response(translated_text): + storage.add_log("ERROR", "Tradução vazia ou inválida recebida") + raise Exception("Tradução vazia ou inválida recebida") + + storage.add_log("INFO", "Tradução concluída com sucesso", { + "original_length": len(text), + "translated_length": len(translated_text), + "ratio": length_ratio + }) + + return translated_text - except Exception as e: - storage.add_log("ERROR", "Erro no processo de tradução", { - "error": str(e), - "type": type(e).__name__ - }) - raise \ No newline at end of file + except Exception as e: + storage.add_log("ERROR", "Erro no processo de tradução", { + "error": str(e), + "type": type(e).__name__ + }) + raise \ No newline at end of file diff --git a/storage.py b/storage.py index 259c8f1..ad89693 100644 --- a/storage.py +++ b/storage.py @@ -693,4 +693,25 @@ class StorageHandler: } self.redis.lpush(key, json.dumps(failed_delivery)) # Manter apenas as últimas 100 falhas - self.redis.ltrim(key, 0, 99) \ No newline at end of file + self.redis.ltrim(key, 0, 99) + + def get_llm_provider(self) -> str: + """Returns active LLM provider (groq or openai)""" + return self.redis.get(self._get_redis_key("active_llm_provider")) or "groq" + + def set_llm_provider(self, provider: str): + """Sets active LLM provider""" + if provider not in ["groq", "openai"]: + raise ValueError("Provider must be 'groq' or 'openai'") + self.redis.set(self._get_redis_key("active_llm_provider"), provider) + + def get_openai_keys(self) -> List[str]: + """Get stored OpenAI API keys""" + return list(self.redis.smembers(self._get_redis_key("openai_keys"))) + + def add_openai_key(self, key: str): + """Add OpenAI API key""" + if key and key.startswith("sk-"): + self.redis.sadd(self._get_redis_key("openai_keys"), key) + return True + return False \ No newline at end of file