Spaces:
Running
Running
Update modules/api.py
Browse files- modules/api.py +94 -112
modules/api.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
| 1 |
# modules/api.py — AKIRA V19 ULTIMATE (Dezembro 2025)
|
| 2 |
-
"""
|
| 3 |
-
API Flask com 6 provedores de IA em fallback cascata
|
| 4 |
- Mistral → Gemini → Groq → Cohere → Together → HuggingFace
|
| 5 |
- Respostas em <5s (média)
|
| 6 |
- Contexto de reply estruturado
|
|
@@ -52,37 +51,33 @@ class SimpleTTLCache:
|
|
| 52 |
|
| 53 |
class MultiAPIManager:
|
| 54 |
"""Gerencia chamadas para 6 APIs com fallback automático"""
|
| 55 |
-
|
| 56 |
def __init__(self):
|
| 57 |
self.timeout = config.API_TIMEOUT
|
| 58 |
self.apis_disponiveis = self._verificar_apis()
|
| 59 |
logger.info(f"APIs disponíveis: {', '.join(self.apis_disponiveis)}")
|
| 60 |
-
|
| 61 |
def _verificar_apis(self):
|
| 62 |
-
"""Verifica quais APIs estão configuradas"""
|
| 63 |
apis = []
|
| 64 |
-
if config.MISTRAL_API_KEY:
|
| 65 |
apis.append("mistral")
|
| 66 |
-
if config.GEMINI_API_KEY:
|
| 67 |
apis.append("gemini")
|
| 68 |
-
if config.GROQ_API_KEY:
|
| 69 |
apis.append("groq")
|
| 70 |
-
if config.COHERE_API_KEY:
|
| 71 |
apis.append("cohere")
|
| 72 |
-
if config.TOGETHER_API_KEY:
|
| 73 |
apis.append("together")
|
| 74 |
-
if config.HF_API_KEY:
|
| 75 |
apis.append("huggingface")
|
| 76 |
return apis
|
| 77 |
-
|
| 78 |
-
def _construir_prompt(self, mensagem: str, historico: list, mensagem_citada: str,
|
| 79 |
-
humor: str, tom_usuario: str) -> str:
|
| 80 |
"""Constrói prompt otimizado com contexto"""
|
| 81 |
-
|
| 82 |
# === INFORMAÇÕES DA EMPRESA (SOFTEDGE) ===
|
| 83 |
empresa_info = EmpresaInfo()
|
| 84 |
info_context = ""
|
| 85 |
-
|
| 86 |
# Detecta se usuário pergunta sobre criador/empresa
|
| 87 |
msg_lower = mensagem.lower()
|
| 88 |
if any(palavra in msg_lower for palavra in ["criou", "criador", "quem fez", "desenvolveu", "softedge", "isaac"]):
|
|
@@ -91,26 +86,22 @@ class MultiAPIManager:
|
|
| 91 |
info_context = f"\n[INFO IMPORTANTE]: {empresa_info.get_resposta_sobre_empresa(mensagem, tom_formal)}\n"
|
| 92 |
elif "softedge" in msg_lower or "empresa" in msg_lower:
|
| 93 |
info_context = f"\n[INFO IMPORTANTE]: Softedge é empresa angolana de IA fundada por Isaac Quarenta em 2024. WhatsApp: {empresa_info.get_canal_whatsapp()}\n"
|
| 94 |
-
|
| 95 |
# === DATA E HORA ATUAL ===
|
| 96 |
from datetime import datetime
|
| 97 |
agora = datetime.now()
|
| 98 |
data_hora_atual = agora.strftime("%d de %B de %Y, %H:%M")
|
| 99 |
# Traduz mês para português
|
| 100 |
meses = {
|
| 101 |
-
"January": "janeiro", "February": "fevereiro", "March": "março",
|
| 102 |
-
"
|
| 103 |
-
"
|
| 104 |
-
"October": "outubro", "November": "novembro", "December": "dezembro"
|
| 105 |
}
|
| 106 |
for en, pt in meses.items():
|
| 107 |
data_hora_atual = data_hora_atual.replace(en, pt)
|
| 108 |
-
|
| 109 |
# Contexto de reply (se existir)
|
| 110 |
reply_context = ""
|
| 111 |
if mensagem_citada:
|
| 112 |
reply_context = f"\n[USUÁRIO ESTÁ RESPONDENDO A]: \"{mensagem_citada[:100]}...\"\n"
|
| 113 |
-
|
| 114 |
# Histórico formatado
|
| 115 |
historico_texto = ""
|
| 116 |
if historico:
|
|
@@ -119,25 +110,10 @@ class MultiAPIManager:
|
|
| 119 |
role = msg.get("role", "user")
|
| 120 |
content = msg.get("content", "")
|
| 121 |
historico_texto += f"{role.upper()}: {content}\n"
|
| 122 |
-
|
| 123 |
# Prompt final
|
| 124 |
-
prompt = f"""{config.PERSONA.format(humor=humor, tom_usuario=tom_usuario)}
|
| 125 |
-
|
| 126 |
-
{config.SYSTEM_PROMPT.format(mensagem_citada=mensagem_citada or "nenhuma", humor=humor)}
|
| 127 |
-
|
| 128 |
-
DATA E HORA ATUAL: {data_hora_atual}
|
| 129 |
-
{info_context}
|
| 130 |
-
|
| 131 |
-
CONTEXTO DA CONVERSA:
|
| 132 |
-
{historico_texto}
|
| 133 |
-
{reply_context}
|
| 134 |
-
|
| 135 |
-
USUÁRIO: {mensagem}
|
| 136 |
-
|
| 137 |
-
AKIRA (responda EM 1-2 LINHAS curtas, sem markdown):"""
|
| 138 |
-
|
| 139 |
return prompt
|
| 140 |
-
|
| 141 |
# === API 1: MISTRAL ===
|
| 142 |
def _chamar_mistral(self, prompt: str) -> str:
|
| 143 |
"""Chama Mistral AI"""
|
|
@@ -155,12 +131,16 @@ AKIRA (responda EM 1-2 LINHAS curtas, sem markdown):"""
|
|
| 155 |
headers=headers,
|
| 156 |
timeout=self.timeout
|
| 157 |
)
|
|
|
|
| 158 |
if resp.status_code == 200:
|
| 159 |
return resp.json()["choices"][0]["message"]["content"].strip()
|
|
|
|
|
|
|
|
|
|
| 160 |
except Exception as e:
|
| 161 |
logger.warning(f"Mistral falhou: {e}")
|
| 162 |
-
|
| 163 |
-
|
| 164 |
# === API 2: GEMINI ===
|
| 165 |
def _chamar_gemini(self, prompt: str) -> str:
|
| 166 |
"""Chama Google Gemini"""
|
|
@@ -174,12 +154,16 @@ AKIRA (responda EM 1-2 LINHAS curtas, sem markdown):"""
|
|
| 174 |
}
|
| 175 |
}
|
| 176 |
resp = requests.post(url, json=payload, timeout=self.timeout)
|
|
|
|
| 177 |
if resp.status_code == 200:
|
| 178 |
return resp.json()["candidates"][0]["content"]["parts"][0]["text"].strip()
|
|
|
|
|
|
|
|
|
|
| 179 |
except Exception as e:
|
| 180 |
logger.warning(f"Gemini falhou: {e}")
|
| 181 |
-
|
| 182 |
-
|
| 183 |
# === API 3: GROQ ===
|
| 184 |
def _chamar_groq(self, prompt: str) -> str:
|
| 185 |
"""Chama Groq (ultra-rápido)"""
|
|
@@ -197,12 +181,16 @@ AKIRA (responda EM 1-2 LINHAS curtas, sem markdown):"""
|
|
| 197 |
headers=headers,
|
| 198 |
timeout=self.timeout
|
| 199 |
)
|
|
|
|
| 200 |
if resp.status_code == 200:
|
| 201 |
return resp.json()["choices"][0]["message"]["content"].strip()
|
|
|
|
|
|
|
|
|
|
| 202 |
except Exception as e:
|
| 203 |
logger.warning(f"Groq falhou: {e}")
|
| 204 |
-
|
| 205 |
-
|
| 206 |
# === API 4: COHERE ===
|
| 207 |
def _chamar_cohere(self, prompt: str) -> str:
|
| 208 |
"""Chama Cohere"""
|
|
@@ -220,12 +208,16 @@ AKIRA (responda EM 1-2 LINHAS curtas, sem markdown):"""
|
|
| 220 |
headers=headers,
|
| 221 |
timeout=self.timeout
|
| 222 |
)
|
|
|
|
| 223 |
if resp.status_code == 200:
|
| 224 |
return resp.json()["text"].strip()
|
|
|
|
|
|
|
|
|
|
| 225 |
except Exception as e:
|
| 226 |
logger.warning(f"Cohere falhou: {e}")
|
| 227 |
-
|
| 228 |
-
|
| 229 |
# === API 5: TOGETHER AI ===
|
| 230 |
def _chamar_together(self, prompt: str) -> str:
|
| 231 |
"""Chama Together AI"""
|
|
@@ -243,12 +235,16 @@ AKIRA (responda EM 1-2 LINHAS curtas, sem markdown):"""
|
|
| 243 |
headers=headers,
|
| 244 |
timeout=self.timeout
|
| 245 |
)
|
|
|
|
| 246 |
if resp.status_code == 200:
|
| 247 |
return resp.json()["choices"][0]["message"]["content"].strip()
|
|
|
|
|
|
|
|
|
|
| 248 |
except Exception as e:
|
| 249 |
logger.warning(f"Together falhou: {e}")
|
| 250 |
-
|
| 251 |
-
|
| 252 |
# === API 6: HUGGING FACE ===
|
| 253 |
def _chamar_huggingface(self, prompt: str) -> str:
|
| 254 |
"""Chama HuggingFace Inference API"""
|
|
@@ -261,67 +257,68 @@ AKIRA (responda EM 1-2 LINHAS curtas, sem markdown):"""
|
|
| 261 |
headers=headers,
|
| 262 |
timeout=self.timeout
|
| 263 |
)
|
|
|
|
| 264 |
if resp.status_code == 200:
|
| 265 |
return resp.json()[0]["generated_text"].split("AKIRA:")[-1].strip()
|
|
|
|
|
|
|
|
|
|
| 266 |
except Exception as e:
|
| 267 |
logger.warning(f"HuggingFace falhou: {e}")
|
| 268 |
-
|
| 269 |
-
|
| 270 |
# === MÉTODO PRINCIPAL DE GERAÇÃO ===
|
| 271 |
-
def gerar_resposta(self, mensagem: str, historico: list, mensagem_citada: str,
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
|
|
|
|
|
|
| 275 |
"""
|
| 276 |
prompt = self._construir_prompt(mensagem, historico, mensagem_citada, humor, tom_usuario)
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
return "Barra no bardeado."
|
| 308 |
-
|
| 309 |
def _limpar_resposta(self, resposta: str) -> str:
|
| 310 |
"""Remove markdown e limita tamanho"""
|
| 311 |
# Remove markdown
|
| 312 |
resposta = resposta.replace("**", "").replace("*", "")
|
| 313 |
resposta = resposta.replace("```", "").replace("`", "")
|
| 314 |
-
|
| 315 |
# Remove prefixos comuns de IA
|
| 316 |
prefixos = ["AKIRA:", "Akira:", "RESPOSTA:", "Resposta:"]
|
| 317 |
for p in prefixos:
|
| 318 |
if resposta.startswith(p):
|
| 319 |
resposta = resposta[len(p):].strip()
|
| 320 |
-
|
| 321 |
# Limita tamanho (máximo 300 caracteres)
|
| 322 |
if len(resposta) > 300:
|
| 323 |
resposta = resposta[:297] + "..."
|
| 324 |
-
|
| 325 |
return resposta.strip()
|
| 326 |
|
| 327 |
# ============================================================================
|
|
@@ -338,7 +335,7 @@ class AkiraAPI:
|
|
| 338 |
self.web_search = get_web_search() # Instância de WebSearch
|
| 339 |
self._setup_routes()
|
| 340 |
self._setup_trainer()
|
| 341 |
-
|
| 342 |
def _setup_trainer(self):
|
| 343 |
"""Inicializa treinamento (desativado por padrão)"""
|
| 344 |
if getattr(self.config, 'START_PERIODIC_TRAINER', False):
|
|
@@ -347,10 +344,9 @@ class AkiraAPI:
|
|
| 347 |
logger.info("Treinamento periódico INICIADO")
|
| 348 |
except Exception as e:
|
| 349 |
logger.error(f"Treinador falhou: {e}")
|
| 350 |
-
|
| 351 |
def _setup_routes(self):
|
| 352 |
"""Configura rotas Flask"""
|
| 353 |
-
|
| 354 |
@self.api.before_request
|
| 355 |
def handle_options():
|
| 356 |
if request.method == 'OPTIONS':
|
|
@@ -359,12 +355,12 @@ class AkiraAPI:
|
|
| 359 |
resp.headers['Access-Control-Allow-Headers'] = 'Content-Type'
|
| 360 |
resp.headers['Access-Control-Allow-Methods'] = 'POST, GET'
|
| 361 |
return resp
|
| 362 |
-
|
| 363 |
@self.api.after_request
|
| 364 |
def add_cors(response):
|
| 365 |
response.headers['Access-Control-Allow-Origin'] = '*'
|
| 366 |
return response
|
| 367 |
-
|
| 368 |
@self.api.route('/akira', methods=['POST'])
|
| 369 |
def akira_endpoint():
|
| 370 |
try:
|
|
@@ -373,21 +369,16 @@ class AkiraAPI:
|
|
| 373 |
numero = data.get('numero', '').strip()
|
| 374 |
mensagem = data.get('mensagem', '').strip()
|
| 375 |
mensagem_citada = data.get('mensagem_citada', '').strip()
|
| 376 |
-
|
| 377 |
if not mensagem and not mensagem_citada:
|
| 378 |
return jsonify({'error': 'mensagem obrigatória'}), 400
|
| 379 |
-
|
| 380 |
logger.info(f"[{usuario}] ({numero}): {mensagem[:60]}")
|
| 381 |
-
|
| 382 |
# === RESPOSTA RÁPIDA PARA HORA ===
|
| 383 |
if any(k in mensagem.lower() for k in ["hora", "horas", "que horas"]):
|
| 384 |
agora = datetime.datetime.now()
|
| 385 |
return jsonify({'resposta': f"São {agora.strftime('%H:%M')} em Luanda, puto."})
|
| 386 |
-
|
| 387 |
# === DETECTAR INTENÇÃO DE BUSCA WEB ===
|
| 388 |
intencao_busca = WebSearch.detectar_intencao_busca(mensagem)
|
| 389 |
contexto_web = ""
|
| 390 |
-
|
| 391 |
if intencao_busca == "noticias":
|
| 392 |
logger.info("Buscando notícias de Angola...")
|
| 393 |
contexto_web = self.web_search.pesquisar_noticias_angola()
|
|
@@ -404,21 +395,17 @@ class AkiraAPI:
|
|
| 404 |
elif intencao_busca == "busca_geral":
|
| 405 |
logger.info("Buscando informações gerais...")
|
| 406 |
contexto_web = self.web_search.buscar_geral(mensagem)
|
| 407 |
-
|
| 408 |
# === CONTEXTO DO USUÁRIO ===
|
| 409 |
contexto = self._get_user_context(numero)
|
| 410 |
historico = contexto.obter_historico_para_llm()
|
| 411 |
-
|
| 412 |
# === ANÁLISE DE TOM E HUMOR ===
|
| 413 |
analise = contexto.analisar_intencao_e_normalizar(mensagem, historico)
|
| 414 |
tom_usuario = analise.get("estilo", "casual")
|
| 415 |
humor_atual = contexto.obter_emocao_atual()
|
| 416 |
-
|
| 417 |
# === VERIFICAR SE É USUÁRIO PRIVILEGIADO ===
|
| 418 |
if numero in config.USUARIOS_PRIVILEGIADOS:
|
| 419 |
tom_usuario = "formal"
|
| 420 |
logger.info(f"Usuário privilegiado detectado: {config.USUARIOS_PRIVILEGIADOS[numero]}")
|
| 421 |
-
|
| 422 |
# === GERAR RESPOSTA VIA MULTI-API ===
|
| 423 |
# Adiciona contexto web ao histórico se disponível
|
| 424 |
historico_com_web = historico.copy()
|
|
@@ -427,7 +414,6 @@ class AkiraAPI:
|
|
| 427 |
"role": "system",
|
| 428 |
"content": f"CONTEXTO ADICIONAL (busca web):\n{contexto_web}"
|
| 429 |
})
|
| 430 |
-
|
| 431 |
resposta = self.llm_manager.gerar_resposta(
|
| 432 |
mensagem=mensagem,
|
| 433 |
historico=historico_com_web,
|
|
@@ -435,10 +421,8 @@ class AkiraAPI:
|
|
| 435 |
humor=humor_atual,
|
| 436 |
tom_usuario=tom_usuario
|
| 437 |
)
|
| 438 |
-
|
| 439 |
# === SALVAR NO BANCO + CONTEXTO ===
|
| 440 |
contexto.atualizar_contexto(mensagem, resposta)
|
| 441 |
-
|
| 442 |
try:
|
| 443 |
trainer = Treinamento(self.db)
|
| 444 |
trainer.registrar_interacao(
|
|
@@ -451,17 +435,15 @@ class AkiraAPI:
|
|
| 451 |
)
|
| 452 |
except Exception as e:
|
| 453 |
logger.warning(f"Erro ao treinar: {e}")
|
| 454 |
-
|
| 455 |
return jsonify({'resposta': resposta})
|
| 456 |
-
|
| 457 |
except Exception as e:
|
| 458 |
logger.exception("Erro em /akira")
|
| 459 |
return jsonify({'resposta': 'Erro interno, já volto!'}), 500
|
| 460 |
-
|
| 461 |
@self.api.route('/health', methods=['GET'])
|
| 462 |
def health_check():
|
| 463 |
return 'OK', 200
|
| 464 |
-
|
| 465 |
def _get_user_context(self, numero: str) -> Contexto:
|
| 466 |
"""Retorna contexto do usuário (com cache)"""
|
| 467 |
if not numero:
|
|
|
|
| 1 |
# modules/api.py — AKIRA V19 ULTIMATE (Dezembro 2025)
|
| 2 |
+
"""API Flask com 6 provedores de IA em fallback cascata
|
|
|
|
| 3 |
- Mistral → Gemini → Groq → Cohere → Together → HuggingFace
|
| 4 |
- Respostas em <5s (média)
|
| 5 |
- Contexto de reply estruturado
|
|
|
|
| 51 |
|
| 52 |
class MultiAPIManager:
|
| 53 |
"""Gerencia chamadas para 6 APIs com fallback automático"""
|
|
|
|
| 54 |
def __init__(self):
|
| 55 |
self.timeout = config.API_TIMEOUT
|
| 56 |
self.apis_disponiveis = self._verificar_apis()
|
| 57 |
logger.info(f"APIs disponíveis: {', '.join(self.apis_disponiveis)}")
|
| 58 |
+
|
| 59 |
def _verificar_apis(self):
|
| 60 |
+
"""Verifica quais APIs estão configuradas (ignora vazias)"""
|
| 61 |
apis = []
|
| 62 |
+
if config.MISTRAL_API_KEY and config.MISTRAL_API_KEY.strip():
|
| 63 |
apis.append("mistral")
|
| 64 |
+
if config.GEMINI_API_KEY and config.GEMINI_API_KEY.strip():
|
| 65 |
apis.append("gemini")
|
| 66 |
+
if config.GROQ_API_KEY and config.GROQ_API_KEY.strip():
|
| 67 |
apis.append("groq")
|
| 68 |
+
if config.COHERE_API_KEY and config.COHERE_API_KEY.strip():
|
| 69 |
apis.append("cohere")
|
| 70 |
+
if config.TOGETHER_API_KEY and config.TOGETHER_API_KEY.strip():
|
| 71 |
apis.append("together")
|
| 72 |
+
if config.HF_API_KEY and config.HF_API_KEY.strip():
|
| 73 |
apis.append("huggingface")
|
| 74 |
return apis
|
| 75 |
+
|
| 76 |
+
def _construir_prompt(self, mensagem: str, historico: list, mensagem_citada: str, humor: str, tom_usuario: str) -> str:
|
|
|
|
| 77 |
"""Constrói prompt otimizado com contexto"""
|
|
|
|
| 78 |
# === INFORMAÇÕES DA EMPRESA (SOFTEDGE) ===
|
| 79 |
empresa_info = EmpresaInfo()
|
| 80 |
info_context = ""
|
|
|
|
| 81 |
# Detecta se usuário pergunta sobre criador/empresa
|
| 82 |
msg_lower = mensagem.lower()
|
| 83 |
if any(palavra in msg_lower for palavra in ["criou", "criador", "quem fez", "desenvolveu", "softedge", "isaac"]):
|
|
|
|
| 86 |
info_context = f"\n[INFO IMPORTANTE]: {empresa_info.get_resposta_sobre_empresa(mensagem, tom_formal)}\n"
|
| 87 |
elif "softedge" in msg_lower or "empresa" in msg_lower:
|
| 88 |
info_context = f"\n[INFO IMPORTANTE]: Softedge é empresa angolana de IA fundada por Isaac Quarenta em 2024. WhatsApp: {empresa_info.get_canal_whatsapp()}\n"
|
|
|
|
| 89 |
# === DATA E HORA ATUAL ===
|
| 90 |
from datetime import datetime
|
| 91 |
agora = datetime.now()
|
| 92 |
data_hora_atual = agora.strftime("%d de %B de %Y, %H:%M")
|
| 93 |
# Traduz mês para português
|
| 94 |
meses = {
|
| 95 |
+
"January": "janeiro", "February": "fevereiro", "March": "março", "April": "abril",
|
| 96 |
+
"May": "maio", "June": "junho", "July": "julho", "August": "agosto",
|
| 97 |
+
"September": "setembro", "October": "outubro", "November": "novembro", "December": "dezembro"
|
|
|
|
| 98 |
}
|
| 99 |
for en, pt in meses.items():
|
| 100 |
data_hora_atual = data_hora_atual.replace(en, pt)
|
|
|
|
| 101 |
# Contexto de reply (se existir)
|
| 102 |
reply_context = ""
|
| 103 |
if mensagem_citada:
|
| 104 |
reply_context = f"\n[USUÁRIO ESTÁ RESPONDENDO A]: \"{mensagem_citada[:100]}...\"\n"
|
|
|
|
| 105 |
# Histórico formatado
|
| 106 |
historico_texto = ""
|
| 107 |
if historico:
|
|
|
|
| 110 |
role = msg.get("role", "user")
|
| 111 |
content = msg.get("content", "")
|
| 112 |
historico_texto += f"{role.upper()}: {content}\n"
|
|
|
|
| 113 |
# Prompt final
|
| 114 |
+
prompt = f"""{config.PERSONA.format(humor=humor, tom_usuario=tom_usuario)}{config.SYSTEM_PROMPT.format(mensagem_citada=mensagem_citada or "nenhuma", humor=humor)}DATA E HORA ATUAL: {data_hora_atual}{info_context}CONTEXTO DA CONVERSA:{historico_texto}{reply_context}USUÁRIO: {mensagem}AKIRA (responda EM 1-2 LINHAS curtas, sem markdown):"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
return prompt
|
| 116 |
+
|
| 117 |
# === API 1: MISTRAL ===
|
| 118 |
def _chamar_mistral(self, prompt: str) -> str:
|
| 119 |
"""Chama Mistral AI"""
|
|
|
|
| 131 |
headers=headers,
|
| 132 |
timeout=self.timeout
|
| 133 |
)
|
| 134 |
+
logger.debug(f"Mistral response: {resp.status_code} - {resp.text[:100]}...")
|
| 135 |
if resp.status_code == 200:
|
| 136 |
return resp.json()["choices"][0]["message"]["content"].strip()
|
| 137 |
+
else:
|
| 138 |
+
logger.warning(f"Mistral erro: {resp.status_code} - {resp.text}")
|
| 139 |
+
return None
|
| 140 |
except Exception as e:
|
| 141 |
logger.warning(f"Mistral falhou: {e}")
|
| 142 |
+
return None
|
| 143 |
+
|
| 144 |
# === API 2: GEMINI ===
|
| 145 |
def _chamar_gemini(self, prompt: str) -> str:
|
| 146 |
"""Chama Google Gemini"""
|
|
|
|
| 154 |
}
|
| 155 |
}
|
| 156 |
resp = requests.post(url, json=payload, timeout=self.timeout)
|
| 157 |
+
logger.debug(f"Gemini response: {resp.status_code} - {resp.text[:100]}...")
|
| 158 |
if resp.status_code == 200:
|
| 159 |
return resp.json()["candidates"][0]["content"]["parts"][0]["text"].strip()
|
| 160 |
+
else:
|
| 161 |
+
logger.warning(f"Gemini erro: {resp.status_code} - {resp.text}")
|
| 162 |
+
return None
|
| 163 |
except Exception as e:
|
| 164 |
logger.warning(f"Gemini falhou: {e}")
|
| 165 |
+
return None
|
| 166 |
+
|
| 167 |
# === API 3: GROQ ===
|
| 168 |
def _chamar_groq(self, prompt: str) -> str:
|
| 169 |
"""Chama Groq (ultra-rápido)"""
|
|
|
|
| 181 |
headers=headers,
|
| 182 |
timeout=self.timeout
|
| 183 |
)
|
| 184 |
+
logger.debug(f"Groq response: {resp.status_code} - {resp.text[:100]}...")
|
| 185 |
if resp.status_code == 200:
|
| 186 |
return resp.json()["choices"][0]["message"]["content"].strip()
|
| 187 |
+
else:
|
| 188 |
+
logger.warning(f"Groq erro: {resp.status_code} - {resp.text}")
|
| 189 |
+
return None
|
| 190 |
except Exception as e:
|
| 191 |
logger.warning(f"Groq falhou: {e}")
|
| 192 |
+
return None
|
| 193 |
+
|
| 194 |
# === API 4: COHERE ===
|
| 195 |
def _chamar_cohere(self, prompt: str) -> str:
|
| 196 |
"""Chama Cohere"""
|
|
|
|
| 208 |
headers=headers,
|
| 209 |
timeout=self.timeout
|
| 210 |
)
|
| 211 |
+
logger.debug(f"Cohere response: {resp.status_code} - {resp.text[:100]}...")
|
| 212 |
if resp.status_code == 200:
|
| 213 |
return resp.json()["text"].strip()
|
| 214 |
+
else:
|
| 215 |
+
logger.warning(f"Cohere erro: {resp.status_code} - {resp.text}")
|
| 216 |
+
return None
|
| 217 |
except Exception as e:
|
| 218 |
logger.warning(f"Cohere falhou: {e}")
|
| 219 |
+
return None
|
| 220 |
+
|
| 221 |
# === API 5: TOGETHER AI ===
|
| 222 |
def _chamar_together(self, prompt: str) -> str:
|
| 223 |
"""Chama Together AI"""
|
|
|
|
| 235 |
headers=headers,
|
| 236 |
timeout=self.timeout
|
| 237 |
)
|
| 238 |
+
logger.debug(f"Together response: {resp.status_code} - {resp.text[:100]}...")
|
| 239 |
if resp.status_code == 200:
|
| 240 |
return resp.json()["choices"][0]["message"]["content"].strip()
|
| 241 |
+
else:
|
| 242 |
+
logger.warning(f"Together erro: {resp.status_code} - {resp.text}")
|
| 243 |
+
return None
|
| 244 |
except Exception as e:
|
| 245 |
logger.warning(f"Together falhou: {e}")
|
| 246 |
+
return None
|
| 247 |
+
|
| 248 |
# === API 6: HUGGING FACE ===
|
| 249 |
def _chamar_huggingface(self, prompt: str) -> str:
|
| 250 |
"""Chama HuggingFace Inference API"""
|
|
|
|
| 257 |
headers=headers,
|
| 258 |
timeout=self.timeout
|
| 259 |
)
|
| 260 |
+
logger.debug(f"HF response: {resp.status_code} - {resp.text[:100]}...")
|
| 261 |
if resp.status_code == 200:
|
| 262 |
return resp.json()[0]["generated_text"].split("AKIRA:")[-1].strip()
|
| 263 |
+
else:
|
| 264 |
+
logger.warning(f"HF erro: {resp.status_code} - {resp.text}")
|
| 265 |
+
return None
|
| 266 |
except Exception as e:
|
| 267 |
logger.warning(f"HuggingFace falhou: {e}")
|
| 268 |
+
return None
|
| 269 |
+
|
| 270 |
# === MÉTODO PRINCIPAL DE GERAÇÃO ===
|
| 271 |
+
def gerar_resposta(self, mensagem: str, historico: list, mensagem_citada: str, humor: str, tom_usuario: str) -> str:
|
| 272 |
+
"""
|
| 273 |
+
Tenta gerar resposta usando todas as APIs na ordem configurada.
|
| 274 |
+
- 2 retries por API
|
| 275 |
+
- Até 2 loops full se todas falharem
|
| 276 |
+
- Logs detalhados
|
| 277 |
"""
|
| 278 |
prompt = self._construir_prompt(mensagem, historico, mensagem_citada, humor, tom_usuario)
|
| 279 |
+
max_loops = 2
|
| 280 |
+
for loop in range(max_loops):
|
| 281 |
+
logger.info(f"Fallback loop {loop+1}/{max_loops}")
|
| 282 |
+
for api_name in config.API_FALLBACK_ORDER:
|
| 283 |
+
if api_name not in self.apis_disponiveis:
|
| 284 |
+
continue
|
| 285 |
+
for retry in range(2): # 2 tentativas por API
|
| 286 |
+
logger.info(f"Tentando {api_name.upper()} (retry {retry+1}/2)...")
|
| 287 |
+
try:
|
| 288 |
+
if api_name == "mistral":
|
| 289 |
+
resposta = self._chamar_mistral(prompt)
|
| 290 |
+
elif api_name == "gemini":
|
| 291 |
+
resposta = self._chamar_gemini(prompt)
|
| 292 |
+
elif api_name == "groq":
|
| 293 |
+
resposta = self._chamar_groq(prompt)
|
| 294 |
+
elif api_name == "cohere":
|
| 295 |
+
resposta = self._chamar_cohere(prompt)
|
| 296 |
+
elif api_name == "together":
|
| 297 |
+
resposta = self._chamar_together(prompt)
|
| 298 |
+
elif api_name == "huggingface":
|
| 299 |
+
resposta = self._chamar_huggingface(prompt)
|
| 300 |
+
if resposta:
|
| 301 |
+
logger.success(f"✓ Resposta gerada via {api_name.upper()}")
|
| 302 |
+
return self._limpar_resposta(resposta)
|
| 303 |
+
time.sleep(1) # Sleep entre retries
|
| 304 |
+
except Exception as e:
|
| 305 |
+
logger.error(f"{api_name} erro crítico (retry {retry+1}): {e}")
|
| 306 |
+
time.sleep(2) # Sleep entre loops full
|
| 307 |
+
return "Barra no bardeado"
|
| 308 |
+
|
|
|
|
|
|
|
| 309 |
def _limpar_resposta(self, resposta: str) -> str:
|
| 310 |
"""Remove markdown e limita tamanho"""
|
| 311 |
# Remove markdown
|
| 312 |
resposta = resposta.replace("**", "").replace("*", "")
|
| 313 |
resposta = resposta.replace("```", "").replace("`", "")
|
|
|
|
| 314 |
# Remove prefixos comuns de IA
|
| 315 |
prefixos = ["AKIRA:", "Akira:", "RESPOSTA:", "Resposta:"]
|
| 316 |
for p in prefixos:
|
| 317 |
if resposta.startswith(p):
|
| 318 |
resposta = resposta[len(p):].strip()
|
|
|
|
| 319 |
# Limita tamanho (máximo 300 caracteres)
|
| 320 |
if len(resposta) > 300:
|
| 321 |
resposta = resposta[:297] + "..."
|
|
|
|
| 322 |
return resposta.strip()
|
| 323 |
|
| 324 |
# ============================================================================
|
|
|
|
| 335 |
self.web_search = get_web_search() # Instância de WebSearch
|
| 336 |
self._setup_routes()
|
| 337 |
self._setup_trainer()
|
| 338 |
+
|
| 339 |
def _setup_trainer(self):
|
| 340 |
"""Inicializa treinamento (desativado por padrão)"""
|
| 341 |
if getattr(self.config, 'START_PERIODIC_TRAINER', False):
|
|
|
|
| 344 |
logger.info("Treinamento periódico INICIADO")
|
| 345 |
except Exception as e:
|
| 346 |
logger.error(f"Treinador falhou: {e}")
|
| 347 |
+
|
| 348 |
def _setup_routes(self):
|
| 349 |
"""Configura rotas Flask"""
|
|
|
|
| 350 |
@self.api.before_request
|
| 351 |
def handle_options():
|
| 352 |
if request.method == 'OPTIONS':
|
|
|
|
| 355 |
resp.headers['Access-Control-Allow-Headers'] = 'Content-Type'
|
| 356 |
resp.headers['Access-Control-Allow-Methods'] = 'POST, GET'
|
| 357 |
return resp
|
| 358 |
+
|
| 359 |
@self.api.after_request
|
| 360 |
def add_cors(response):
|
| 361 |
response.headers['Access-Control-Allow-Origin'] = '*'
|
| 362 |
return response
|
| 363 |
+
|
| 364 |
@self.api.route('/akira', methods=['POST'])
|
| 365 |
def akira_endpoint():
|
| 366 |
try:
|
|
|
|
| 369 |
numero = data.get('numero', '').strip()
|
| 370 |
mensagem = data.get('mensagem', '').strip()
|
| 371 |
mensagem_citada = data.get('mensagem_citada', '').strip()
|
|
|
|
| 372 |
if not mensagem and not mensagem_citada:
|
| 373 |
return jsonify({'error': 'mensagem obrigatória'}), 400
|
|
|
|
| 374 |
logger.info(f"[{usuario}] ({numero}): {mensagem[:60]}")
|
|
|
|
| 375 |
# === RESPOSTA RÁPIDA PARA HORA ===
|
| 376 |
if any(k in mensagem.lower() for k in ["hora", "horas", "que horas"]):
|
| 377 |
agora = datetime.datetime.now()
|
| 378 |
return jsonify({'resposta': f"São {agora.strftime('%H:%M')} em Luanda, puto."})
|
|
|
|
| 379 |
# === DETECTAR INTENÇÃO DE BUSCA WEB ===
|
| 380 |
intencao_busca = WebSearch.detectar_intencao_busca(mensagem)
|
| 381 |
contexto_web = ""
|
|
|
|
| 382 |
if intencao_busca == "noticias":
|
| 383 |
logger.info("Buscando notícias de Angola...")
|
| 384 |
contexto_web = self.web_search.pesquisar_noticias_angola()
|
|
|
|
| 395 |
elif intencao_busca == "busca_geral":
|
| 396 |
logger.info("Buscando informações gerais...")
|
| 397 |
contexto_web = self.web_search.buscar_geral(mensagem)
|
|
|
|
| 398 |
# === CONTEXTO DO USUÁRIO ===
|
| 399 |
contexto = self._get_user_context(numero)
|
| 400 |
historico = contexto.obter_historico_para_llm()
|
|
|
|
| 401 |
# === ANÁLISE DE TOM E HUMOR ===
|
| 402 |
analise = contexto.analisar_intencao_e_normalizar(mensagem, historico)
|
| 403 |
tom_usuario = analise.get("estilo", "casual")
|
| 404 |
humor_atual = contexto.obter_emocao_atual()
|
|
|
|
| 405 |
# === VERIFICAR SE É USUÁRIO PRIVILEGIADO ===
|
| 406 |
if numero in config.USUARIOS_PRIVILEGIADOS:
|
| 407 |
tom_usuario = "formal"
|
| 408 |
logger.info(f"Usuário privilegiado detectado: {config.USUARIOS_PRIVILEGIADOS[numero]}")
|
|
|
|
| 409 |
# === GERAR RESPOSTA VIA MULTI-API ===
|
| 410 |
# Adiciona contexto web ao histórico se disponível
|
| 411 |
historico_com_web = historico.copy()
|
|
|
|
| 414 |
"role": "system",
|
| 415 |
"content": f"CONTEXTO ADICIONAL (busca web):\n{contexto_web}"
|
| 416 |
})
|
|
|
|
| 417 |
resposta = self.llm_manager.gerar_resposta(
|
| 418 |
mensagem=mensagem,
|
| 419 |
historico=historico_com_web,
|
|
|
|
| 421 |
humor=humor_atual,
|
| 422 |
tom_usuario=tom_usuario
|
| 423 |
)
|
|
|
|
| 424 |
# === SALVAR NO BANCO + CONTEXTO ===
|
| 425 |
contexto.atualizar_contexto(mensagem, resposta)
|
|
|
|
| 426 |
try:
|
| 427 |
trainer = Treinamento(self.db)
|
| 428 |
trainer.registrar_interacao(
|
|
|
|
| 435 |
)
|
| 436 |
except Exception as e:
|
| 437 |
logger.warning(f"Erro ao treinar: {e}")
|
|
|
|
| 438 |
return jsonify({'resposta': resposta})
|
|
|
|
| 439 |
except Exception as e:
|
| 440 |
logger.exception("Erro em /akira")
|
| 441 |
return jsonify({'resposta': 'Erro interno, já volto!'}), 500
|
| 442 |
+
|
| 443 |
@self.api.route('/health', methods=['GET'])
|
| 444 |
def health_check():
|
| 445 |
return 'OK', 200
|
| 446 |
+
|
| 447 |
def _get_user_context(self, numero: str) -> Contexto:
|
| 448 |
"""Retorna contexto do usuário (com cache)"""
|
| 449 |
if not numero:
|