Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
c42476549e | ||
![]() |
e89787e715 | ||
![]() |
f63dcc40d1 | ||
![]() |
af20510b2b | ||
![]() |
a25dc9c4e7 | ||
![]() |
a4ba9d02bc | ||
![]() |
4c7d346a3c | ||
![]() |
be82707ccc |
@ -39,6 +39,8 @@ class Settings:
|
|||||||
"""Inicializa as configurações."""
|
"""Inicializa as configurações."""
|
||||||
logger.debug("Carregando configurações do Redis...")
|
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.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.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"
|
self.PROCESS_GROUP_MESSAGES = self.get_redis_value("PROCESS_GROUP_MESSAGES", "false").lower() == "true"
|
||||||
|
111
groq_handler.py
Normal file
111
groq_handler.py
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import aiohttp
|
||||||
|
import json
|
||||||
|
from typing import Optional, Tuple, Any
|
||||||
|
from datetime import datetime
|
||||||
|
import logging
|
||||||
|
from storage import StorageHandler
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
logger = logging.getLogger("GROQHandler")
|
||||||
|
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_groq_key(key: str) -> bool:
|
||||||
|
"""Teste se uma chave GROQ é válida e está funcionando."""
|
||||||
|
url = "https://api.groq.com/openai/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 bool(data.get("data"))
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erro ao testar chave GROQ: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def validate_transcription_response(response_text: str) -> bool:
|
||||||
|
"""Valide se a resposta da transcrição é significativa."""
|
||||||
|
try:
|
||||||
|
cleaned_text = response_text.strip()
|
||||||
|
return len(cleaned_text) >= 10
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erro ao validar resposta da transcrição: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def get_working_groq_key(storage: StorageHandler) -> Optional[str]:
|
||||||
|
"""Obtenha uma chave GROQ funcional do pool disponível."""
|
||||||
|
keys = storage.get_groq_keys()
|
||||||
|
|
||||||
|
for _ in range(len(keys)):
|
||||||
|
key = storage.get_next_groq_key()
|
||||||
|
if not key:
|
||||||
|
continue
|
||||||
|
|
||||||
|
penalized_until = storage.get_penalized_until(key)
|
||||||
|
if penalized_until and penalized_until > datetime.utcnow():
|
||||||
|
continue
|
||||||
|
|
||||||
|
if await test_groq_key(key):
|
||||||
|
return key
|
||||||
|
else:
|
||||||
|
storage.penalize_key(key, penalty_duration=300)
|
||||||
|
|
||||||
|
storage.add_log("ERROR", "Nenhuma chave GROQ funcional disponível.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def handle_groq_request(
|
||||||
|
url: str,
|
||||||
|
headers: dict,
|
||||||
|
data: Any,
|
||||||
|
storage: StorageHandler,
|
||||||
|
is_form_data: bool = False
|
||||||
|
) -> Tuple[bool, dict, str]:
|
||||||
|
"""Lida com requisições para a API GROQ com suporte a retries e rotação de chaves."""
|
||||||
|
max_retries = len(storage.get_groq_keys())
|
||||||
|
|
||||||
|
for attempt in range(max_retries):
|
||||||
|
try:
|
||||||
|
storage.add_log("DEBUG", "Iniciando tentativa de requisição para GROQ", {
|
||||||
|
"url": url,
|
||||||
|
"is_form_data": is_form_data,
|
||||||
|
"attempt": attempt + 1
|
||||||
|
})
|
||||||
|
|
||||||
|
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 and response_data.get("text"):
|
||||||
|
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 "organization_restricted" in error_msg or "invalid_api_key" in error_msg:
|
||||||
|
new_key = await get_working_groq_key(storage)
|
||||||
|
if new_key:
|
||||||
|
headers["Authorization"] = f"Bearer {new_key}"
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
continue
|
||||||
|
|
||||||
|
return False, response_data, error_msg
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
storage.add_log("ERROR", "Erro na requisição", {"error": str(e)})
|
||||||
|
if attempt < max_retries - 1:
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
continue
|
||||||
|
return False, {}, f"Request failed: {str(e)}"
|
||||||
|
|
||||||
|
storage.add_log("ERROR", "Todas as chaves GROQ falharam.")
|
||||||
|
return False, {}, "All GROQ keys exhausted."
|
12
main.py
12
main.py
@ -5,6 +5,7 @@ from services import (
|
|||||||
send_message_to_whatsapp,
|
send_message_to_whatsapp,
|
||||||
get_audio_base64,
|
get_audio_base64,
|
||||||
summarize_text_if_needed,
|
summarize_text_if_needed,
|
||||||
|
download_remote_audio,
|
||||||
)
|
)
|
||||||
from models import WebhookRequest
|
from models import WebhookRequest
|
||||||
from config import logger, settings, redis_client
|
from config import logger, settings, redis_client
|
||||||
@ -151,17 +152,14 @@ async def transcreve_audios(request: Request):
|
|||||||
# Obter áudio
|
# Obter áudio
|
||||||
try:
|
try:
|
||||||
if "mediaUrl" in body["data"]["message"]:
|
if "mediaUrl" in body["data"]["message"]:
|
||||||
audio_source = body["data"]["message"]["mediaUrl"]
|
media_url = body["data"]["message"]["mediaUrl"]
|
||||||
storage.add_log("DEBUG", "Usando mediaUrl para áudio", {
|
storage.add_log("DEBUG", "Baixando áudio via URL", {"mediaUrl": media_url})
|
||||||
"mediaUrl": audio_source
|
audio_source = await download_remote_audio(media_url) # Baixa o arquivo remoto e retorna o caminho local
|
||||||
})
|
|
||||||
else:
|
else:
|
||||||
storage.add_log("DEBUG", "Obtendo áudio via base64")
|
storage.add_log("DEBUG", "Obtendo áudio via base64")
|
||||||
base64_audio = await get_audio_base64(server_url, instance, apikey, audio_key)
|
base64_audio = await get_audio_base64(server_url, instance, apikey, audio_key)
|
||||||
audio_source = await convert_base64_to_file(base64_audio)
|
audio_source = await convert_base64_to_file(base64_audio)
|
||||||
storage.add_log("DEBUG", "Áudio convertido", {
|
storage.add_log("DEBUG", "Áudio convertido", {"source": audio_source})
|
||||||
"source": audio_source
|
|
||||||
})
|
|
||||||
|
|
||||||
# Carregar configurações de formatação
|
# Carregar configurações de formatação
|
||||||
output_mode = get_config("output_mode", "both")
|
output_mode = get_config("output_mode", "both")
|
||||||
|
53
manager.py
53
manager.py
@ -252,7 +252,7 @@ def login_page():
|
|||||||
# Modificar a função de logout no dashboard
|
# Modificar a função de logout no dashboard
|
||||||
def dashboard():
|
def dashboard():
|
||||||
# Versão do sistema
|
# Versão do sistema
|
||||||
APP_VERSION = "2.3.1"
|
APP_VERSION = "2.3.3"
|
||||||
|
|
||||||
show_logo()
|
show_logo()
|
||||||
st.sidebar.markdown('<div class="sidebar-header">TranscreveZAP - Menu</div>', unsafe_allow_html=True)
|
st.sidebar.markdown('<div class="sidebar-header">TranscreveZAP - Menu</div>', unsafe_allow_html=True)
|
||||||
@ -728,9 +728,10 @@ def manage_settings():
|
|||||||
st.title("⚙️ Configurações")
|
st.title("⚙️ Configurações")
|
||||||
|
|
||||||
# Criar tabs para melhor organização
|
# Criar tabs para melhor organização
|
||||||
tab1, tab2, tab3, tab4 = st.tabs([
|
tab1, tab2, tab3, tab4, tab5 = st.tabs([
|
||||||
"🔑 Chaves API",
|
"🔑 Chaves API",
|
||||||
"🌐 Configurações Gerais",
|
"🤖 Provedor LLM",
|
||||||
|
"🌐 Configurações Gerais",
|
||||||
"📝 Formatação de Mensagens",
|
"📝 Formatação de Mensagens",
|
||||||
"🗣️ Idiomas e Transcrição"
|
"🗣️ Idiomas e Transcrição"
|
||||||
])
|
])
|
||||||
@ -787,6 +788,46 @@ def manage_settings():
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
with tab2:
|
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")
|
st.subheader("Configurações do Sistema")
|
||||||
|
|
||||||
# Business Message
|
# Business Message
|
||||||
@ -850,7 +891,7 @@ def manage_settings():
|
|||||||
)
|
)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
with tab3:
|
with tab4:
|
||||||
st.subheader("Formatação de Mensagens")
|
st.subheader("Formatação de Mensagens")
|
||||||
|
|
||||||
# Headers personalizados
|
# Headers personalizados
|
||||||
@ -935,7 +976,7 @@ def manage_settings():
|
|||||||
st.error(f"Erro ao salvar configurações: {str(e)}")
|
st.error(f"Erro ao salvar configurações: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
with tab4:
|
with tab5:
|
||||||
st.subheader("Idiomas e Transcrição")
|
st.subheader("Idiomas e Transcrição")
|
||||||
|
|
||||||
# Adicionar estatísticas no topo
|
# Adicionar estatísticas no topo
|
||||||
|
74
openai_handler.py
Normal file
74
openai_handler.py
Normal file
@ -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"
|
14
readme.md
14
readme.md
@ -11,6 +11,7 @@ Uma solução completa para automatizar e gerenciar mensagens de áudio no Whats
|
|||||||
- Transcrição automática multilíngue
|
- Transcrição automática multilíngue
|
||||||
- Resumos inteligentes de áudios
|
- Resumos inteligentes de áudios
|
||||||
- Detecção e tradução automática entre idiomas
|
- Detecção e tradução automática entre idiomas
|
||||||
|
- Seleção de plataforma LLM (GROQ ou OpenAI)
|
||||||
- Interface administrativa completa
|
- Interface administrativa completa
|
||||||
- Sistema de rodízio de chaves API
|
- Sistema de rodízio de chaves API
|
||||||
- Gestão avançada de grupos e usuários
|
- Gestão avançada de grupos e usuários
|
||||||
@ -28,10 +29,21 @@ Antes de começar, certifique-se de ter os seguintes requisitos:
|
|||||||
- Python 3.10+ instalado ([Download](https://www.python.org/downloads/))
|
- Python 3.10+ instalado ([Download](https://www.python.org/downloads/))
|
||||||
- Docker e Docker Compose instalados ([Instruções](https://docs.docker.com/get-docker/))
|
- Docker e Docker Compose instalados ([Instruções](https://docs.docker.com/get-docker/))
|
||||||
- Uma conta Evolution API com chave válida
|
- Uma conta Evolution API com chave válida
|
||||||
- No mínimo uma conta GROQ API com chave válida (começa com 'gsk_') ([Crie sua CONTA](https://console.groq.com/login))
|
- Chaves GROQ (começa com `gsk_`) e/ou chaves OpenAI (começa com `sk-`) configuradas ([Crie sua conta GROQ](https://console.groq.com/login))
|
||||||
* Em caso de uso com Proxy Reverso Aponte um Subdomínio para a API e outro para o MANAGER da aplicação
|
* Em caso de uso com Proxy Reverso Aponte um Subdomínio para a API e outro para o MANAGER da aplicação
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 🚀 **Novidade: Escolha do Provedor LLM**
|
||||||
|
Agora você pode escolher entre dois provedores para transcrições e resumos:
|
||||||
|
1. **GROQ** (open-source): Configuração padrão.
|
||||||
|
2. **OpenAI** (API paga): Integração com modelos GPT.
|
||||||
|
|
||||||
|
### Configuração:
|
||||||
|
- Acesse: **Configurações > Provedor LLM** na interface administrativa.
|
||||||
|
- Escolha entre `groq` e `openai`.
|
||||||
|
- Adicione as chaves correspondentes para cada provedor.
|
||||||
|
|
||||||
|
---
|
||||||
## 🚀 **Instalação e Configuração**
|
## 🚀 **Instalação e Configuração**
|
||||||
|
|
||||||
### 🐳 Docker Compose
|
### 🐳 Docker Compose
|
||||||
|
423
services.py
423
services.py
@ -7,7 +7,8 @@ from storage import StorageHandler
|
|||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import traceback
|
||||||
|
from groq_handler import get_working_groq_key, validate_transcription_response, handle_groq_request
|
||||||
# Inicializa o storage handler
|
# Inicializa o storage handler
|
||||||
storage = StorageHandler()
|
storage = StorageHandler()
|
||||||
|
|
||||||
@ -46,6 +47,7 @@ async def summarize_text_if_needed(text):
|
|||||||
storage.add_log("DEBUG", "Iniciando processo de resumo", {
|
storage.add_log("DEBUG", "Iniciando processo de resumo", {
|
||||||
"text_length": len(text)
|
"text_length": len(text)
|
||||||
})
|
})
|
||||||
|
provider = storage.get_llm_provider()
|
||||||
|
|
||||||
# Obter idioma configurado
|
# Obter idioma configurado
|
||||||
language = redis_client.get("TRANSCRIPTION_LANGUAGE") or "pt"
|
language = redis_client.get("TRANSCRIPTION_LANGUAGE") or "pt"
|
||||||
@ -53,10 +55,20 @@ async def summarize_text_if_needed(text):
|
|||||||
"language": language,
|
"language": language,
|
||||||
"redis_value": redis_client.get("TRANSCRIPTION_LANGUAGE")
|
"redis_value": redis_client.get("TRANSCRIPTION_LANGUAGE")
|
||||||
})
|
})
|
||||||
url_completions = "https://api.groq.com/openai/v1/chat/completions"
|
|
||||||
groq_key = await get_groq_key()
|
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 = {
|
headers = {
|
||||||
"Authorization": f"Bearer {groq_key}",
|
"Authorization": f"Bearer {api_key}",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,29 +152,33 @@ async def summarize_text_if_needed(text):
|
|||||||
"role": "user",
|
"role": "user",
|
||||||
"content": f"{base_prompt}\n\nTexto para resumir: {text}",
|
"content": f"{base_prompt}\n\nTexto para resumir: {text}",
|
||||||
}],
|
}],
|
||||||
"model": "llama-3.3-70b-versatile",
|
"model": model,
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with aiohttp.ClientSession() as session:
|
success, response_data, error = await handle_groq_request(url, headers, json_data, storage, is_form_data=False)
|
||||||
storage.add_log("DEBUG", "Enviando requisição para API GROQ")
|
if not success:
|
||||||
async with session.post(url_completions, headers=headers, json=json_data) as summary_response:
|
raise Exception(error)
|
||||||
if summary_response.status == 200:
|
|
||||||
summary_result = await summary_response.json()
|
summary_text = response_data["choices"][0]["message"]["content"]
|
||||||
summary_text = summary_result["choices"][0]["message"]["content"]
|
# Validar se o resumo não está vazio
|
||||||
storage.add_log("INFO", "Resumo gerado com sucesso", {
|
if not await validate_transcription_response(summary_text):
|
||||||
"original_length": len(text),
|
storage.add_log("ERROR", "Resumo vazio ou inválido recebido")
|
||||||
"summary_length": len(summary_text),
|
raise Exception("Resumo vazio ou inválido recebido")
|
||||||
"language": language
|
# Validar se o resumo é menor que o texto original
|
||||||
})
|
if len(summary_text) >= len(text):
|
||||||
return summary_text
|
storage.add_log("WARNING", "Resumo maior que texto original", {
|
||||||
else:
|
"original_length": len(text),
|
||||||
error_text = await summary_response.text()
|
"summary_length": len(summary_text)
|
||||||
storage.add_log("ERROR", "Erro na API GROQ", {
|
})
|
||||||
"error": error_text,
|
storage.add_log("INFO", "Resumo gerado com sucesso", {
|
||||||
"status": summary_response.status
|
"original_length": len(text),
|
||||||
})
|
"summary_length": len(summary_text),
|
||||||
raise Exception(f"Erro ao resumir o texto: {error_text}")
|
"language": language
|
||||||
|
})
|
||||||
|
|
||||||
|
return summary_text
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
storage.add_log("ERROR", "Erro no processo de resumo", {
|
storage.add_log("ERROR", "Erro no processo de resumo", {
|
||||||
"error": str(e),
|
"error": str(e),
|
||||||
@ -188,10 +204,20 @@ async def transcribe_audio(audio_source, apikey=None, remote_jid=None, from_me=F
|
|||||||
"from_me": from_me,
|
"from_me": from_me,
|
||||||
"remote_jid": remote_jid
|
"remote_jid": remote_jid
|
||||||
})
|
})
|
||||||
|
provider = storage.get_llm_provider()
|
||||||
|
|
||||||
url = "https://api.groq.com/openai/v1/audio/transcriptions"
|
if provider == "openai":
|
||||||
groq_key = await get_groq_key()
|
api_key = storage.get_openai_keys()[0] # Get first OpenAI key
|
||||||
groq_headers = {"Authorization": f"Bearer {groq_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
|
# Inicializar variáveis
|
||||||
contact_language = None
|
contact_language = None
|
||||||
@ -226,29 +252,28 @@ async def transcribe_audio(audio_source, apikey=None, remote_jid=None, from_me=F
|
|||||||
elif not from_me: # Só detecta em mensagens recebidas
|
elif not from_me: # Só detecta em mensagens recebidas
|
||||||
try:
|
try:
|
||||||
# Realizar transcrição inicial sem idioma específico
|
# Realizar transcrição inicial sem idioma específico
|
||||||
data = aiohttp.FormData()
|
with open(audio_source, 'rb') as audio_file:
|
||||||
data.add_field('file', open(audio_source, 'rb'), filename='audio.mp3')
|
data = aiohttp.FormData()
|
||||||
data.add_field('model', 'whisper-large-v3')
|
data.add_field('file', audio_file, filename='audio.mp3')
|
||||||
|
data.add_field('model', model)
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
async with session.post(url, headers=groq_headers, data=data) as response:
|
success, response_data, error = await handle_groq_request(url, headers, data, storage, is_form_data=True)
|
||||||
if response.status == 200:
|
if success:
|
||||||
initial_result = await response.json()
|
initial_text = response_data.get("text", "")
|
||||||
initial_text = initial_result.get("text", "")
|
|
||||||
|
# Detectar idioma do texto transcrito
|
||||||
# Detectar idioma do texto transcrito
|
detected_lang = await detect_language(initial_text)
|
||||||
detected_lang = await detect_language(initial_text)
|
|
||||||
|
# Salvar no cache E na configuração do contato
|
||||||
# Salvar no cache E na configuração do contato
|
storage.cache_language_detection(contact_id, detected_lang)
|
||||||
storage.cache_language_detection(contact_id, detected_lang)
|
storage.set_contact_language(contact_id, detected_lang)
|
||||||
storage.set_contact_language(contact_id, detected_lang)
|
|
||||||
|
contact_language = detected_lang
|
||||||
contact_language = detected_lang
|
storage.add_log("INFO", "Idioma detectado e configurado", {
|
||||||
storage.add_log("INFO", "Idioma detectado e configurado", {
|
"language": detected_lang,
|
||||||
"language": detected_lang,
|
"remote_jid": remote_jid,
|
||||||
"remote_jid": remote_jid,
|
"auto_detected": True
|
||||||
"auto_detected": True
|
})
|
||||||
})
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
storage.add_log("WARNING", "Erro na detecção automática de idioma", {
|
storage.add_log("WARNING", "Erro na detecção automática de idioma", {
|
||||||
"error": str(e),
|
"error": str(e),
|
||||||
@ -300,76 +325,73 @@ async def transcribe_audio(audio_source, apikey=None, remote_jid=None, from_me=F
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Realizar transcrição
|
# Realizar transcrição
|
||||||
data = aiohttp.FormData()
|
with open(audio_source, 'rb') as audio_file:
|
||||||
data.add_field('file', open(audio_source, 'rb'), filename='audio.mp3')
|
data = aiohttp.FormData()
|
||||||
data.add_field('model', 'whisper-large-v3')
|
data.add_field('file', audio_file, filename='audio.mp3')
|
||||||
data.add_field('language', transcription_language)
|
data.add_field('model', model)
|
||||||
|
data.add_field('language', transcription_language)
|
||||||
if use_timestamps:
|
|
||||||
data.add_field('response_format', 'verbose_json')
|
|
||||||
|
|
||||||
# Realizar transcrição
|
if use_timestamps:
|
||||||
async with aiohttp.ClientSession() as session:
|
data.add_field('response_format', 'verbose_json')
|
||||||
async with session.post(url, headers=groq_headers, data=data) as response:
|
|
||||||
if response.status != 200:
|
# Usar handle_groq_request para ter retry e validação
|
||||||
error_text = await response.text()
|
success, response_data, error = await handle_groq_request(url, headers, data, storage, is_form_data=True)
|
||||||
storage.add_log("ERROR", "Erro na transcrição", {
|
if not success:
|
||||||
"error": error_text,
|
raise Exception(f"Erro na transcrição: {error}")
|
||||||
"status": response.status
|
|
||||||
|
transcription = format_timestamped_result(response_data) if use_timestamps else response_data.get("text", "")
|
||||||
|
|
||||||
|
# Validar o conteúdo da transcrição
|
||||||
|
if not await validate_transcription_response(transcription):
|
||||||
|
storage.add_log("ERROR", "Transcrição vazia ou inválida recebida")
|
||||||
|
raise Exception("Transcrição vazia ou inválida recebida")
|
||||||
|
|
||||||
|
# Detecção automática para novos contatos
|
||||||
|
if (is_private and storage.get_auto_language_detection() and
|
||||||
|
not from_me and not contact_language):
|
||||||
|
try:
|
||||||
|
detected_lang = await detect_language(transcription)
|
||||||
|
storage.cache_language_detection(remote_jid, detected_lang)
|
||||||
|
contact_language = detected_lang
|
||||||
|
storage.add_log("INFO", "Idioma detectado e cacheado", {
|
||||||
|
"language": detected_lang,
|
||||||
|
"remote_jid": remote_jid
|
||||||
})
|
})
|
||||||
raise Exception(f"Erro na transcrição: {error_text}")
|
except Exception as e:
|
||||||
|
storage.add_log("WARNING", "Erro na detecção de idioma", {"error": str(e)})
|
||||||
result = await response.json()
|
|
||||||
|
|
||||||
# Processar resposta baseado no formato
|
|
||||||
transcription = format_timestamped_result(result) if use_timestamps else result.get("text", "")
|
|
||||||
|
|
||||||
# Detecção automática para novos contatos
|
# Tradução quando necessário
|
||||||
if (is_private and storage.get_auto_language_detection() and
|
need_translation = (
|
||||||
not from_me and not contact_language):
|
is_private and contact_language and
|
||||||
try:
|
(
|
||||||
detected_lang = await detect_language(transcription)
|
(from_me and transcription_language != target_language) or
|
||||||
storage.cache_language_detection(remote_jid, detected_lang)
|
(not from_me and target_language != transcription_language)
|
||||||
contact_language = detected_lang
|
)
|
||||||
storage.add_log("INFO", "Idioma detectado e cacheado", {
|
)
|
||||||
"language": detected_lang,
|
|
||||||
"remote_jid": remote_jid
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
storage.add_log("WARNING", "Erro na detecção de idioma", {"error": str(e)})
|
|
||||||
|
|
||||||
# Tradução quando necessário
|
if need_translation:
|
||||||
need_translation = (
|
try:
|
||||||
is_private and contact_language and
|
transcription = await translate_text(
|
||||||
(
|
transcription,
|
||||||
(from_me and transcription_language != target_language) or
|
transcription_language,
|
||||||
(not from_me and target_language != transcription_language)
|
target_language
|
||||||
)
|
)
|
||||||
)
|
storage.add_log("INFO", "Texto traduzido automaticamente", {
|
||||||
|
"from": transcription_language,
|
||||||
|
"to": target_language
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
storage.add_log("ERROR", "Erro na tradução", {"error": str(e)})
|
||||||
|
|
||||||
if need_translation:
|
# Registrar estatísticas de uso
|
||||||
try:
|
used_language = contact_language if contact_language else system_language
|
||||||
transcription = await translate_text(
|
storage.record_language_usage(
|
||||||
transcription,
|
used_language,
|
||||||
transcription_language,
|
from_me,
|
||||||
target_language
|
bool(contact_language and contact_language != system_language)
|
||||||
)
|
)
|
||||||
storage.add_log("INFO", "Texto traduzido automaticamente", {
|
|
||||||
"from": transcription_language,
|
|
||||||
"to": target_language
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
storage.add_log("ERROR", "Erro na tradução", {"error": str(e)})
|
|
||||||
|
|
||||||
# Registrar estatísticas de uso
|
return transcription, use_timestamps
|
||||||
used_language = contact_language if contact_language else system_language
|
|
||||||
storage.record_language_usage(
|
|
||||||
used_language,
|
|
||||||
from_me,
|
|
||||||
bool(contact_language and contact_language != system_language)
|
|
||||||
)
|
|
||||||
|
|
||||||
return transcription, use_timestamps
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
storage.add_log("ERROR", "Erro no processo de transcrição", {
|
storage.add_log("ERROR", "Erro no processo de transcrição", {
|
||||||
@ -423,6 +445,7 @@ async def detect_language(text: str) -> str:
|
|||||||
Returns:
|
Returns:
|
||||||
str: Código ISO 639-1 do idioma detectado
|
str: Código ISO 639-1 do idioma detectado
|
||||||
"""
|
"""
|
||||||
|
provider = storage.get_llm_provider()
|
||||||
storage.add_log("DEBUG", "Iniciando detecção de idioma", {
|
storage.add_log("DEBUG", "Iniciando detecção de idioma", {
|
||||||
"text_length": len(text)
|
"text_length": len(text)
|
||||||
})
|
})
|
||||||
@ -432,11 +455,19 @@ async def detect_language(text: str) -> str:
|
|||||||
"pt", "en", "es", "fr", "de", "it", "ja", "ko",
|
"pt", "en", "es", "fr", "de", "it", "ja", "ko",
|
||||||
"zh", "ro", "ru", "ar", "hi", "nl", "pl", "tr"
|
"zh", "ro", "ru", "ar", "hi", "nl", "pl", "tr"
|
||||||
}
|
}
|
||||||
|
if provider == "openai":
|
||||||
url_completions = "https://api.groq.com/openai/v1/chat/completions"
|
api_key = storage.get_openai_keys()[0]
|
||||||
groq_key = await get_groq_key()
|
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 = {
|
headers = {
|
||||||
"Authorization": f"Bearer {groq_key}",
|
"Authorization": f"Bearer {api_key}",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -465,37 +496,30 @@ async def detect_language(text: str) -> str:
|
|||||||
"role": "user",
|
"role": "user",
|
||||||
"content": f"{prompt}\n\n{text[:500]}" # Limitando para os primeiros 500 caracteres
|
"content": f"{prompt}\n\n{text[:500]}" # Limitando para os primeiros 500 caracteres
|
||||||
}],
|
}],
|
||||||
"model": "llama-3.3-70b-versatile",
|
"model": model,
|
||||||
"temperature": 0.1
|
"temperature": 0.1
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with aiohttp.ClientSession() as session:
|
success, response_data, error = await handle_groq_request(url, headers, json_data, storage, is_form_data=False)
|
||||||
storage.add_log("DEBUG", "Enviando requisição para API GROQ - Detecção de idioma")
|
if not success:
|
||||||
async with session.post(url_completions, headers=headers, json=json_data) as response:
|
raise Exception(f"Falha na detecção de idioma: {error}")
|
||||||
if response.status == 200:
|
|
||||||
result = await response.json()
|
detected_language = response_data["choices"][0]["message"]["content"].strip().lower()
|
||||||
detected_language = result["choices"][0]["message"]["content"].strip().lower()
|
|
||||||
|
# Validar o resultado
|
||||||
# Validar o resultado
|
if detected_language not in SUPPORTED_LANGUAGES:
|
||||||
if detected_language not in SUPPORTED_LANGUAGES:
|
storage.add_log("WARNING", "Idioma detectado não suportado", {
|
||||||
storage.add_log("WARNING", "Idioma detectado não suportado", {
|
"detected": detected_language,
|
||||||
"detected": detected_language,
|
"fallback": "en"
|
||||||
"fallback": "en"
|
})
|
||||||
})
|
detected_language = "en"
|
||||||
detected_language = "en"
|
|
||||||
|
storage.add_log("INFO", "Idioma detectado com sucesso", {
|
||||||
storage.add_log("INFO", "Idioma detectado com sucesso", {
|
"detected_language": detected_language
|
||||||
"detected_language": detected_language
|
})
|
||||||
})
|
return detected_language
|
||||||
return detected_language
|
|
||||||
else:
|
|
||||||
error_text = await response.text()
|
|
||||||
storage.add_log("ERROR", "Erro na detecção de idioma", {
|
|
||||||
"error": error_text,
|
|
||||||
"status": response.status
|
|
||||||
})
|
|
||||||
raise Exception(f"Erro na detecção de idioma: {error_text}")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
storage.add_log("ERROR", "Erro no processo de detecção de idioma", {
|
storage.add_log("ERROR", "Erro no processo de detecção de idioma", {
|
||||||
"error": str(e),
|
"error": str(e),
|
||||||
@ -650,24 +674,33 @@ async def translate_text(text: str, source_language: str, target_language: str)
|
|||||||
Returns:
|
Returns:
|
||||||
str: Texto traduzido
|
str: Texto traduzido
|
||||||
"""
|
"""
|
||||||
|
provider = storage.get_llm_provider()
|
||||||
storage.add_log("DEBUG", "Iniciando tradução", {
|
storage.add_log("DEBUG", "Iniciando tradução", {
|
||||||
"source_language": source_language,
|
"source_language": source_language,
|
||||||
"target_language": target_language,
|
"target_language": target_language,
|
||||||
"text_length": len(text)
|
"text_length": len(text)
|
||||||
})
|
})
|
||||||
|
|
||||||
# Se os idiomas forem iguais, retorna o texto original
|
# Se os idiomas forem iguais, retorna o texto original
|
||||||
if source_language == target_language:
|
if source_language == target_language:
|
||||||
return text
|
return text
|
||||||
|
|
||||||
url_completions = "https://api.groq.com/openai/v1/chat/completions"
|
if provider == "openai":
|
||||||
groq_key = await get_groq_key()
|
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 = {
|
headers = {
|
||||||
"Authorization": f"Bearer {groq_key}",
|
"Authorization": f"Bearer {api_key}",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Prompt melhorado com contexto e instruções específicas
|
|
||||||
prompt = f"""
|
prompt = f"""
|
||||||
Você é um tradutor profissional especializado em manter o tom e estilo do texto original.
|
Você é um tradutor profissional especializado em manter o tom e estilo do texto original.
|
||||||
|
|
||||||
@ -692,43 +725,63 @@ async def translate_text(text: str, source_language: str, target_language: str)
|
|||||||
"role": "user",
|
"role": "user",
|
||||||
"content": prompt
|
"content": prompt
|
||||||
}],
|
}],
|
||||||
"model": "llama-3.3-70b-versatile",
|
"model": model,
|
||||||
"temperature": 0.3
|
"temperature": 0.3
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with aiohttp.ClientSession() as session:
|
success, response_data, error = await handle_groq_request(url, headers, json_data, storage, is_form_data=False)
|
||||||
storage.add_log("DEBUG", "Enviando requisição de tradução")
|
if not success:
|
||||||
async with session.post(url_completions, headers=headers, json=json_data) as response:
|
raise Exception(f"Falha na tradução: {error}")
|
||||||
if response.status == 200:
|
|
||||||
result = await response.json()
|
translated_text = response_data["choices"][0]["message"]["content"].strip()
|
||||||
translated_text = result["choices"][0]["message"]["content"].strip()
|
|
||||||
|
# Verificar se a tradução manteve aproximadamente o mesmo tamanho
|
||||||
# Verificar se a tradução manteve aproximadamente o mesmo tamanho
|
length_ratio = len(translated_text) / len(text)
|
||||||
length_ratio = len(translated_text) / len(text)
|
if not (0.5 <= length_ratio <= 1.5):
|
||||||
if not (0.5 <= length_ratio <= 1.5):
|
storage.add_log("WARNING", "Possível erro na tradução - diferença significativa no tamanho", {
|
||||||
storage.add_log("WARNING", "Possível erro na tradução - diferença significativa no tamanho", {
|
"original_length": len(text),
|
||||||
"original_length": len(text),
|
"translated_length": len(translated_text),
|
||||||
"translated_length": len(translated_text),
|
"ratio": length_ratio
|
||||||
"ratio": length_ratio
|
})
|
||||||
})
|
|
||||||
|
# Validar se a tradução não está vazia
|
||||||
storage.add_log("INFO", "Tradução concluída com sucesso", {
|
if not await validate_transcription_response(translated_text):
|
||||||
"original_length": len(text),
|
storage.add_log("ERROR", "Tradução vazia ou inválida recebida")
|
||||||
"translated_length": len(translated_text),
|
raise Exception("Tradução vazia ou inválida recebida")
|
||||||
"ratio": length_ratio
|
|
||||||
})
|
storage.add_log("INFO", "Tradução concluída com sucesso", {
|
||||||
return translated_text
|
"original_length": len(text),
|
||||||
else:
|
"translated_length": len(translated_text),
|
||||||
error_text = await response.text()
|
"ratio": length_ratio
|
||||||
storage.add_log("ERROR", "Erro na tradução", {
|
})
|
||||||
"status": response.status,
|
|
||||||
"error": error_text
|
return translated_text
|
||||||
})
|
|
||||||
raise Exception(f"Erro na tradução: {error_text}")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
storage.add_log("ERROR", "Erro no processo de tradução", {
|
storage.add_log("ERROR", "Erro no processo de tradução", {
|
||||||
"error": str(e),
|
"error": str(e),
|
||||||
"type": type(e).__name__
|
"type": type(e).__name__
|
||||||
})
|
})
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
# Nova função para baixar áudio remoto
|
||||||
|
async def download_remote_audio(url: str) -> str:
|
||||||
|
"""
|
||||||
|
Baixa um arquivo de áudio remoto e salva localmente como um arquivo temporário.
|
||||||
|
Retorna o caminho para o arquivo salvo.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.get(url) as response:
|
||||||
|
if response.status == 200:
|
||||||
|
audio_data = await response.read()
|
||||||
|
# Cria um arquivo temporário para armazenar o áudio (pode ajustar o sufixo caso necessário)
|
||||||
|
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as temp_file:
|
||||||
|
temp_file.write(audio_data)
|
||||||
|
local_path = temp_file.name
|
||||||
|
return local_path
|
||||||
|
else:
|
||||||
|
raise Exception(f"Falha no download, código de status: {response.status}")
|
||||||
|
except Exception as e:
|
||||||
|
raise Exception(f"Erro ao baixar áudio remoto: {str(e)}")
|
48
storage.py
48
storage.py
@ -1,6 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from typing import List, Dict
|
from typing import List, Dict, Optional
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import traceback
|
import traceback
|
||||||
import logging
|
import logging
|
||||||
@ -209,6 +209,29 @@ class StorageHandler:
|
|||||||
|
|
||||||
return keys[counter % len(keys)]
|
return keys[counter % len(keys)]
|
||||||
|
|
||||||
|
def get_penalized_until(self, key: str) -> Optional[datetime]:
|
||||||
|
"""
|
||||||
|
Retorna o timestamp até quando a chave está penalizada, ou None se não estiver penalizada.
|
||||||
|
"""
|
||||||
|
penalized_key = self._get_redis_key(f"groq_key_penalized_{key}")
|
||||||
|
penalized_until = self.redis.get(penalized_key)
|
||||||
|
if penalized_until:
|
||||||
|
return datetime.fromisoformat(penalized_until)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def penalize_key(self, key: str, penalty_duration: int):
|
||||||
|
"""
|
||||||
|
Penaliza uma chave por um tempo determinado (em segundos).
|
||||||
|
"""
|
||||||
|
penalized_key = self._get_redis_key(f"groq_key_penalized_{key}")
|
||||||
|
penalized_until = datetime.utcnow() + timedelta(seconds=penalty_duration)
|
||||||
|
self.redis.set(penalized_key, penalized_until.isoformat())
|
||||||
|
self.redis.expire(penalized_key, penalty_duration) # Expira a chave após o tempo de penalidade
|
||||||
|
self.add_log("INFO", "Chave GROQ penalizada", {
|
||||||
|
"key": key,
|
||||||
|
"penalized_until": penalized_until.isoformat()
|
||||||
|
})
|
||||||
|
|
||||||
def get_message_settings(self):
|
def get_message_settings(self):
|
||||||
"""Obtém as configurações de mensagens."""
|
"""Obtém as configurações de mensagens."""
|
||||||
return {
|
return {
|
||||||
@ -670,4 +693,25 @@ class StorageHandler:
|
|||||||
}
|
}
|
||||||
self.redis.lpush(key, json.dumps(failed_delivery))
|
self.redis.lpush(key, json.dumps(failed_delivery))
|
||||||
# Manter apenas as últimas 100 falhas
|
# Manter apenas as últimas 100 falhas
|
||||||
self.redis.ltrim(key, 0, 99)
|
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
|
Loading…
Reference in New Issue
Block a user