melhoria no sistema de chaves groq
This commit is contained in:
parent
3cd75903fc
commit
be82707ccc
98
groq_handler.py
Normal file
98
groq_handler.py
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import aiohttp
|
||||||
|
import json
|
||||||
|
from typing import Optional, Tuple
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
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:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def validate_transcription_response(response_text: str) -> bool:
|
||||||
|
"""Valide se a resposta da transcrição é significativa."""
|
||||||
|
try:
|
||||||
|
# Remove common whitespace and punctuation
|
||||||
|
cleaned_text = response_text.strip()
|
||||||
|
# Check minimum content length (adjustable threshold)
|
||||||
|
return len(cleaned_text) >= 10
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def get_working_groq_key(storage) -> Optional[str]:
|
||||||
|
"""Obtenha uma chave GROQ funcional do pool disponível."""
|
||||||
|
keys = storage.get_groq_keys()
|
||||||
|
|
||||||
|
for _ in range(len(keys)): # Try each key once
|
||||||
|
key = storage.get_next_groq_key()
|
||||||
|
if key and await test_groq_key(key):
|
||||||
|
return key
|
||||||
|
|
||||||
|
storage.add_log("ERROR", "No working GROQ keys available")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def handle_groq_request(url: str, headers: dict, data: dict, storage) -> Tuple[bool, dict, str]:
|
||||||
|
"""
|
||||||
|
Handle GROQ API request with retries and key rotation.
|
||||||
|
Returns: (success, response_data, error_message)
|
||||||
|
"""
|
||||||
|
max_retries = len(storage.get_groq_keys())
|
||||||
|
|
||||||
|
for attempt in range(max_retries):
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.post(url, headers=headers, json=data) as response:
|
||||||
|
response_data = await response.json()
|
||||||
|
|
||||||
|
if response.status == 200:
|
||||||
|
# Validate response content
|
||||||
|
if "choices" in response_data and response_data["choices"]:
|
||||||
|
content = response_data["choices"][0].get("message", {}).get("content")
|
||||||
|
if content and await validate_transcription_response(content):
|
||||||
|
return True, response_data, ""
|
||||||
|
|
||||||
|
# Handle specific error cases
|
||||||
|
error_msg = response_data.get("error", {}).get("message", "")
|
||||||
|
if "organization_restricted" in error_msg or "invalid_api_key" in error_msg:
|
||||||
|
# Try next key
|
||||||
|
new_key = await get_working_groq_key(storage)
|
||||||
|
if new_key:
|
||||||
|
headers["Authorization"] = f"Bearer {new_key}"
|
||||||
|
storage.add_log("INFO", "Tentando nova chave GROQ após erro", {
|
||||||
|
"error": error_msg,
|
||||||
|
"attempt": attempt + 1
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
|
||||||
|
return False, {}, f"API Error: {error_msg}"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Tratamento específico para erros de conexão
|
||||||
|
if "Connection" in str(e) and attempt < max_retries - 1:
|
||||||
|
storage.add_log("WARNING", "Erro de conexão, tentando novamente", {
|
||||||
|
"error": str(e),
|
||||||
|
"attempt": attempt + 1
|
||||||
|
})
|
||||||
|
await asyncio.sleep(1) # Espera 1 segundo antes de retry
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Se for última tentativa ou outro tipo de erro
|
||||||
|
if attempt == max_retries - 1:
|
||||||
|
storage.add_log("ERROR", "Todas tentativas falharam", {
|
||||||
|
"error": str(e),
|
||||||
|
"total_attempts": max_retries
|
||||||
|
})
|
||||||
|
return False, {}, f"Request failed: {str(e)}"
|
||||||
|
continue
|
||||||
|
|
||||||
|
storage.add_log("ERROR", "Todas as chaves GROQ falharam")
|
||||||
|
return False, {}, "All GROQ keys exhausted"
|
@ -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.2"
|
||||||
|
|
||||||
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)
|
||||||
|
347
services.py
347
services.py
@ -7,7 +7,7 @@ from storage import StorageHandler
|
|||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import tempfile
|
import tempfile
|
||||||
|
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()
|
||||||
|
|
||||||
@ -54,7 +54,9 @@ async def summarize_text_if_needed(text):
|
|||||||
"redis_value": redis_client.get("TRANSCRIPTION_LANGUAGE")
|
"redis_value": redis_client.get("TRANSCRIPTION_LANGUAGE")
|
||||||
})
|
})
|
||||||
url_completions = "https://api.groq.com/openai/v1/chat/completions"
|
url_completions = "https://api.groq.com/openai/v1/chat/completions"
|
||||||
groq_key = await get_groq_key()
|
groq_key = await get_working_groq_key(storage)
|
||||||
|
if not groq_key:
|
||||||
|
raise Exception("Nenhuma chave GROQ disponível")
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": f"Bearer {groq_key}",
|
"Authorization": f"Bearer {groq_key}",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@ -144,25 +146,29 @@ async def summarize_text_if_needed(text):
|
|||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with aiohttp.ClientSession() as session:
|
success, response_data, error = await handle_groq_request(url_completions, headers, json_data, storage)
|
||||||
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),
|
||||||
@ -190,7 +196,9 @@ async def transcribe_audio(audio_source, apikey=None, remote_jid=None, from_me=F
|
|||||||
})
|
})
|
||||||
|
|
||||||
url = "https://api.groq.com/openai/v1/audio/transcriptions"
|
url = "https://api.groq.com/openai/v1/audio/transcriptions"
|
||||||
groq_key = await get_groq_key()
|
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}"}
|
groq_headers = {"Authorization": f"Bearer {groq_key}"}
|
||||||
|
|
||||||
# Inicializar variáveis
|
# Inicializar variáveis
|
||||||
@ -230,25 +238,23 @@ async def transcribe_audio(audio_source, apikey=None, remote_jid=None, from_me=F
|
|||||||
data.add_field('file', open(audio_source, 'rb'), filename='audio.mp3')
|
data.add_field('file', open(audio_source, 'rb'), filename='audio.mp3')
|
||||||
data.add_field('model', 'whisper-large-v3')
|
data.add_field('model', 'whisper-large-v3')
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
success, response_data, error = await handle_groq_request(url, groq_headers, data, storage)
|
||||||
async with session.post(url, headers=groq_headers, data=data) as response:
|
if success:
|
||||||
if response.status == 200:
|
initial_text = response_data.get("text", "")
|
||||||
initial_result = await response.json()
|
|
||||||
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),
|
||||||
@ -307,23 +313,19 @@ async def transcribe_audio(audio_source, apikey=None, remote_jid=None, from_me=F
|
|||||||
|
|
||||||
if use_timestamps:
|
if use_timestamps:
|
||||||
data.add_field('response_format', 'verbose_json')
|
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)
|
||||||
|
if not success:
|
||||||
|
raise Exception(f"Erro na transcrição: {error}")
|
||||||
|
|
||||||
|
transcription = format_timestamped_result(response_data) if use_timestamps else response_data.get("text", "")
|
||||||
|
|
||||||
# Realizar transcrição
|
# Validar o conteúdo da transcrição
|
||||||
async with aiohttp.ClientSession() as session:
|
if not await validate_transcription_response(transcription):
|
||||||
async with session.post(url, headers=groq_headers, data=data) as response:
|
storage.add_log("ERROR", "Transcrição vazia ou inválida recebida")
|
||||||
if response.status != 200:
|
raise Exception("Transcrição vazia ou inválida recebida")
|
||||||
error_text = await response.text()
|
|
||||||
storage.add_log("ERROR", "Erro na transcrição", {
|
|
||||||
"error": error_text,
|
|
||||||
"status": response.status
|
|
||||||
})
|
|
||||||
raise Exception(f"Erro na transcrição: {error_text}")
|
|
||||||
|
|
||||||
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
|
# Detecção automática para novos contatos
|
||||||
if (is_private and storage.get_auto_language_detection() and
|
if (is_private and storage.get_auto_language_detection() and
|
||||||
not from_me and not contact_language):
|
not from_me and not contact_language):
|
||||||
@ -434,7 +436,10 @@ async def detect_language(text: str) -> str:
|
|||||||
}
|
}
|
||||||
|
|
||||||
url_completions = "https://api.groq.com/openai/v1/chat/completions"
|
url_completions = "https://api.groq.com/openai/v1/chat/completions"
|
||||||
groq_key = await get_groq_key()
|
groq_key = await get_working_groq_key(storage)
|
||||||
|
if not groq_key:
|
||||||
|
raise Exception("Nenhuma chave GROQ disponível")
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": f"Bearer {groq_key}",
|
"Authorization": f"Bearer {groq_key}",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@ -470,32 +475,25 @@ async def detect_language(text: str) -> str:
|
|||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with aiohttp.ClientSession() as session:
|
success, response_data, error = await handle_groq_request(url_completions, headers, json_data, storage)
|
||||||
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(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),
|
||||||
@ -639,96 +637,97 @@ async def format_message(transcription_text, summary_text=None):
|
|||||||
return "\n\n".join(message_parts)
|
return "\n\n".join(message_parts)
|
||||||
|
|
||||||
async def translate_text(text: str, source_language: str, target_language: str) -> str:
|
async def translate_text(text: str, source_language: str, target_language: str) -> str:
|
||||||
"""
|
"""
|
||||||
Traduz o texto usando a API GROQ
|
Traduz o texto usando a API GROQ
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
text: Texto para traduzir
|
text: Texto para traduzir
|
||||||
source_language: Código ISO 639-1 do idioma de origem
|
source_language: Código ISO 639-1 do idioma de origem
|
||||||
target_language: Código ISO 639-1 do idioma de destino
|
target_language: Código ISO 639-1 do idioma de destino
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: Texto traduzido
|
str: Texto traduzido
|
||||||
"""
|
"""
|
||||||
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"
|
url_completions = "https://api.groq.com/openai/v1/chat/completions"
|
||||||
groq_key = await get_groq_key()
|
groq_key = await get_working_groq_key(storage)
|
||||||
headers = {
|
if not groq_key:
|
||||||
"Authorization": f"Bearer {groq_key}",
|
raise Exception("Nenhuma chave GROQ disponível")
|
||||||
"Content-Type": "application/json",
|
|
||||||
}
|
headers = {
|
||||||
|
"Authorization": f"Bearer {groq_key}",
|
||||||
# Prompt melhorado com contexto e instruções específicas
|
"Content-Type": "application/json",
|
||||||
prompt = f"""
|
}
|
||||||
Você é um tradutor profissional especializado em manter o tom e estilo do texto original.
|
|
||||||
|
prompt = f"""
|
||||||
Instruções:
|
Você é um tradutor profissional especializado em manter o tom e estilo do texto original.
|
||||||
1. Traduza o texto de {source_language} para {target_language}
|
|
||||||
2. Preserve todas as formatações (negrito, itálico, emojis)
|
Instruções:
|
||||||
3. Mantenha os mesmos parágrafos e quebras de linha
|
1. Traduza o texto de {source_language} para {target_language}
|
||||||
4. Preserve números, datas e nomes próprios
|
2. Preserve todas as formatações (negrito, itálico, emojis)
|
||||||
5. Não adicione ou remova informações
|
3. Mantenha os mesmos parágrafos e quebras de linha
|
||||||
6. Não inclua notas ou explicações
|
4. Preserve números, datas e nomes próprios
|
||||||
7. Mantenha o mesmo nível de formalidade
|
5. Não adicione ou remova informações
|
||||||
|
6. Não inclua notas ou explicações
|
||||||
Texto para tradução:
|
7. Mantenha o mesmo nível de formalidade
|
||||||
{text}
|
|
||||||
"""
|
Texto para tradução:
|
||||||
|
{text}
|
||||||
json_data = {
|
"""
|
||||||
"messages": [{
|
|
||||||
"role": "system",
|
json_data = {
|
||||||
"content": "Você é um tradutor profissional que mantém o estilo e formatação do texto original."
|
"messages": [{
|
||||||
}, {
|
"role": "system",
|
||||||
"role": "user",
|
"content": "Você é um tradutor profissional que mantém o estilo e formatação do texto original."
|
||||||
"content": prompt
|
}, {
|
||||||
}],
|
"role": "user",
|
||||||
"model": "llama-3.3-70b-versatile",
|
"content": prompt
|
||||||
"temperature": 0.3
|
}],
|
||||||
}
|
"model": "llama-3.3-70b-versatile",
|
||||||
|
"temperature": 0.3
|
||||||
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with aiohttp.ClientSession() as session:
|
success, response_data, error = await handle_groq_request(url_completions, headers, json_data, storage)
|
||||||
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(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
|
|
Loading…
Reference in New Issue
Block a user