Spaces:
Running
Running
Update modules/api.py
Browse files- modules/api.py +188 -577
modules/api.py
CHANGED
|
@@ -1,14 +1,14 @@
|
|
| 1 |
# modules/api.py — AKIRA V21 ULTIMATE (Dezembro 2025)
|
| 2 |
"""
|
| 3 |
API Flask com:
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
"""
|
| 13 |
import time
|
| 14 |
import datetime
|
|
@@ -27,10 +27,10 @@ from .web_search import get_web_search, WebSearch
|
|
| 27 |
from .empresa_info import EmpresaInfo
|
| 28 |
import modules.config as config
|
| 29 |
|
|
|
|
| 30 |
# ============================================================================
|
| 31 |
-
# CACHE SIMPLES EM MEMÓRIA
|
| 32 |
# ============================================================================
|
| 33 |
-
|
| 34 |
class SimpleTTLCache:
|
| 35 |
def __init__(self, ttl_seconds: int = 300):
|
| 36 |
self.ttl = ttl_seconds
|
|
@@ -59,45 +59,30 @@ class SimpleTTLCache:
|
|
| 59 |
except KeyError:
|
| 60 |
return default
|
| 61 |
|
|
|
|
| 62 |
# ============================================================================
|
| 63 |
-
# GERENCIADOR MULTI-API
|
| 64 |
# ============================================================================
|
| 65 |
-
|
| 66 |
class MultiAPIManager:
|
| 67 |
-
"""Gerencia chamadas para 6 APIs com fallback automático"""
|
| 68 |
def __init__(self):
|
| 69 |
self.timeout = config.API_TIMEOUT
|
| 70 |
self.apis_disponiveis = self._verificar_apis()
|
| 71 |
logger.info(f"APIs disponíveis: {', '.join(self.apis_disponiveis)}")
|
| 72 |
|
| 73 |
def _verificar_apis(self):
|
| 74 |
-
"""Verifica quais APIs estão configuradas"""
|
| 75 |
apis = []
|
| 76 |
-
|
| 77 |
-
# Mistral
|
| 78 |
if config.MISTRAL_API_KEY and len(config.MISTRAL_API_KEY) > 10:
|
| 79 |
apis.append("mistral")
|
| 80 |
-
|
| 81 |
-
# Gemini
|
| 82 |
if config.GEMINI_API_KEY and config.GEMINI_API_KEY.startswith('AIza'):
|
| 83 |
apis.append("gemini")
|
| 84 |
-
|
| 85 |
-
# Groq
|
| 86 |
if config.GROQ_API_KEY and len(config.GROQ_API_KEY) > 10:
|
| 87 |
apis.append("groq")
|
| 88 |
-
|
| 89 |
-
# Cohere
|
| 90 |
if config.COHERE_API_KEY and len(config.COHERE_API_KEY) > 10:
|
| 91 |
apis.append("cohere")
|
| 92 |
-
|
| 93 |
-
# Together
|
| 94 |
if config.TOGETHER_API_KEY and len(config.TOGETHER_API_KEY) > 10:
|
| 95 |
apis.append("together")
|
| 96 |
-
|
| 97 |
-
# HuggingFace
|
| 98 |
if config.HF_API_KEY and len(config.HF_API_KEY) > 10:
|
| 99 |
apis.append("huggingface")
|
| 100 |
-
|
| 101 |
return apis
|
| 102 |
|
| 103 |
def _construir_prompt(
|
|
@@ -109,445 +94,143 @@ class MultiAPIManager:
|
|
| 109 |
usuario: str,
|
| 110 |
tipo_conversa: str
|
| 111 |
) -> str:
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
# === DATA E HORA ATUAL (CORRIGIDA +1H) ===
|
| 115 |
from datetime import datetime, timedelta
|
| 116 |
agora = datetime.now() + timedelta(hours=config.TIMEZONE_OFFSET_HOURS)
|
| 117 |
data_hora_atual = agora.strftime("%d de %B de %Y, %H:%M")
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
meses = {
|
| 121 |
-
"January": "janeiro", "February": "fevereiro", "March": "março",
|
| 122 |
-
"April": "abril", "May": "maio", "June": "junho",
|
| 123 |
-
"July": "julho", "August": "agosto", "September": "setembro",
|
| 124 |
-
"October": "outubro", "November": "novembro", "December": "dezembro"
|
| 125 |
-
}
|
| 126 |
for en, pt in meses.items():
|
| 127 |
data_hora_atual = data_hora_atual.replace(en, pt)
|
| 128 |
-
|
| 129 |
-
# ===
|
| 130 |
empresa_info = EmpresaInfo()
|
| 131 |
info_context = ""
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
if any(p in msg_lower for p in ["criou", "criador", "quem fez", "desenvolveu", "softedge", "isaac"]):
|
| 135 |
info_context = f"\n[INFO IMPORTANTE]: {empresa_info.get_resposta_sobre_empresa(mensagem, analise.get('tom_usuario') == 'formal')}\n"
|
| 136 |
-
|
| 137 |
-
# ===
|
| 138 |
-
reply_context = ""
|
| 139 |
-
reply_instruction = ""
|
| 140 |
-
|
| 141 |
if mensagem_citada:
|
| 142 |
-
# Verifica se é reply à própria Akira
|
| 143 |
if mensagem_citada.startswith("[Respondendo à Akira:"):
|
| 144 |
reply_context = f"\n[CONTEXTO DE REPLY]: O usuário está respondendo à SUA mensagem anterior: '{mensagem_citada[23:100]}...'"
|
| 145 |
reply_instruction = "Reconheça que é reply à sua mensagem anterior e responda apropriadamente."
|
| 146 |
else:
|
| 147 |
reply_context = f"\n[CONTEXTO DE REPLY]: O usuário está respondendo a outra mensagem: '{mensagem_citada[:100]}...'"
|
| 148 |
-
reply_instruction = "Considere o contexto do reply mas responda à mensagem atual
|
| 149 |
-
|
| 150 |
-
# === HISTÓRICO
|
| 151 |
historico_texto = ""
|
| 152 |
if historico:
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
role = msg.get("role", "user")
|
| 156 |
content = msg.get("content", "")
|
| 157 |
-
historico_texto += f"{role
|
| 158 |
-
|
| 159 |
-
# ===
|
| 160 |
-
modo_resposta = analise.get("modo_resposta", "
|
| 161 |
-
|
| 162 |
-
|
| 163 |
regras_modo = f"""
|
| 164 |
MODO ATIVO: {modo_resposta}
|
| 165 |
-
- Descrição: {
|
| 166 |
-
-
|
| 167 |
-
- Usar
|
| 168 |
-
-
|
| 169 |
-
- Tonalidade: {modo_config['tonalidade']}
|
| 170 |
-
- Tamanho máximo: {modo_config['max_chars']} caracteres
|
| 171 |
"""
|
| 172 |
-
|
| 173 |
-
# ===
|
| 174 |
usuario_privilegiado = analise.get("usuario_privilegiado", False)
|
| 175 |
-
pode_dar_ordens = False
|
| 176 |
nome_usuario = usuario
|
| 177 |
-
|
| 178 |
if usuario_privilegiado:
|
| 179 |
-
# Busca dados do usuário privilegiado
|
| 180 |
db = Database(config.DB_PATH)
|
| 181 |
user_data = db.get_usuario_privilegiado(analise.get('numero', ''))
|
| 182 |
if user_data:
|
| 183 |
nome_usuario = user_data.get('nome_curto', usuario)
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
# === TIPO DE ISOLAMENTO ===
|
| 191 |
-
tipo_isolamento = "GRUPO (histórico completamente isolado)" if tipo_conversa == "grupo" else "PRIVADO (histórico completamente isolado)"
|
| 192 |
-
|
| 193 |
-
# === DETALHES DA ANÁLISE ===
|
| 194 |
-
humor_atual = analise.get("humor_atualizado", "normal_ironico")
|
| 195 |
-
humor_desc = config.HUMORES_BASE.get(humor_atual, "Neutro com ironia")
|
| 196 |
-
tom_usuario = analise.get("tom_usuario", "neutro")
|
| 197 |
-
tom_intensidade = analise.get("tom_intensidade", 0.5)
|
| 198 |
-
emocao_detectada = analise.get("emocao_primaria", "neutral")
|
| 199 |
-
confianca_emocao = analise.get("confianca_emocao", 0.5)
|
| 200 |
-
nivel_transicao = analise.get("nivel_transicao", 0)
|
| 201 |
-
humor_alvo = analise.get("humor_alvo", "normal_ironico")
|
| 202 |
-
|
| 203 |
-
# === PROMPT FINAL (ATUALIZADO COM TODAS AS REGRAS) ===
|
| 204 |
prompt = f"""{config.PERSONA_BASE.format(
|
| 205 |
-
humor=
|
| 206 |
-
|
| 207 |
-
tom_usuario=tom_usuario,
|
| 208 |
modo_resposta=modo_resposta
|
| 209 |
)}
|
| 210 |
|
| 211 |
{config.SYSTEM_PROMPT.format(
|
| 212 |
-
humor=
|
| 213 |
-
|
| 214 |
-
tom_usuario=tom_usuario,
|
| 215 |
-
tom_intensidade=tom_intensidade,
|
| 216 |
modo_resposta=modo_resposta,
|
| 217 |
tipo_conversa=tipo_conversa,
|
| 218 |
mensagem_citada=mensagem_citada or "nenhuma",
|
| 219 |
regras_modo=regras_modo,
|
| 220 |
-
max_chars=
|
| 221 |
-
usa_girias='SIM' if
|
| 222 |
-
usa_emojis='SIM' if
|
| 223 |
-
|
| 224 |
-
prob_nome=int(config.USAR_NOME_PROBABILIDADE*100),
|
| 225 |
reply_context=reply_context,
|
| 226 |
-
reply_instruction=reply_instruction,
|
| 227 |
tipo_isolamento=tipo_isolamento,
|
| 228 |
-
usuario=nome_usuario
|
| 229 |
-
nome_usuario=nome_usuario,
|
| 230 |
-
usuario_privilegiado="SIM" if usuario_privilegiado else "NÃO",
|
| 231 |
-
pode_dar_comandos="SIM" if pode_dar_ordens else "NÃO",
|
| 232 |
-
emocao_detectada=emocao_detectada,
|
| 233 |
-
confianca_emocao=confianca_emocao,
|
| 234 |
-
nivel_transicao=nivel_transicao,
|
| 235 |
-
humor_alvo=humor_alvo
|
| 236 |
)}
|
| 237 |
|
| 238 |
-
{
|
| 239 |
-
|
| 240 |
-
DATA E HORA ATUAL EM LUANDA: {data_hora_atual}
|
| 241 |
{info_context}
|
| 242 |
-
|
| 243 |
-
CONTEXTO DA CONVERSA (ISOLADO):
|
| 244 |
{historico_texto}
|
| 245 |
-
|
| 246 |
USUÁRIO ({nome_usuario}): {mensagem}
|
|
|
|
| 247 |
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
# Log do prompt (apenas resumo)
|
| 251 |
-
logger.debug(f"📝 Prompt construído: {len(prompt)} caracteres, modo: {modo_resposta}, humor: {humor_atual}")
|
| 252 |
-
|
| 253 |
return prompt
|
| 254 |
|
| 255 |
-
# === CHAMADAS ÀS APIS ===
|
| 256 |
-
|
| 257 |
-
def
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
"stop": config.STOP_SEQUENCES
|
| 270 |
-
}
|
| 271 |
-
resp = requests.post(
|
| 272 |
-
"https://api.mistral.ai/v1/chat/completions",
|
| 273 |
-
json=payload,
|
| 274 |
-
headers=headers,
|
| 275 |
-
timeout=self.timeout
|
| 276 |
-
)
|
| 277 |
-
logger.debug(f"Mistral response: {resp.status_code}")
|
| 278 |
-
if resp.status_code == 200:
|
| 279 |
-
return resp.json()["choices"][0]["message"]["content"].strip()
|
| 280 |
-
else:
|
| 281 |
-
logger.warning(f"Mistral erro {resp.status_code}: {resp.text[:200]}")
|
| 282 |
-
return None
|
| 283 |
-
except Exception as e:
|
| 284 |
-
logger.warning(f"Mistral falhou: {e}")
|
| 285 |
-
return None
|
| 286 |
-
|
| 287 |
-
def _chamar_gemini(self, prompt: str) -> str:
|
| 288 |
-
"""Chama Google Gemini"""
|
| 289 |
-
try:
|
| 290 |
-
url = f"https://generativelanguage.googleapis.com/v1beta/models/{config.GEMINI_MODEL}:generateContent?key={config.GEMINI_API_KEY}"
|
| 291 |
-
payload = {
|
| 292 |
-
"contents": [{"parts": [{"text": prompt}]}],
|
| 293 |
-
"generationConfig": {
|
| 294 |
-
"maxOutputTokens": config.MAX_TOKENS,
|
| 295 |
-
"temperature": config.TEMPERATURE,
|
| 296 |
-
"topP": config.TOP_P,
|
| 297 |
-
"stopSequences": config.STOP_SEQUENCES
|
| 298 |
-
}
|
| 299 |
-
}
|
| 300 |
-
resp = requests.post(url, json=payload, timeout=self.timeout)
|
| 301 |
-
logger.debug(f"Gemini response: {resp.status_code}")
|
| 302 |
-
if resp.status_code == 200:
|
| 303 |
-
return resp.json()["candidates"][0]["content"]["parts"][0]["text"].strip()
|
| 304 |
-
else:
|
| 305 |
-
logger.warning(f"Gemini erro {resp.status_code}: {resp.text[:200]}")
|
| 306 |
-
return None
|
| 307 |
-
except Exception as e:
|
| 308 |
-
logger.warning(f"Gemini falhou: {e}")
|
| 309 |
-
return None
|
| 310 |
-
|
| 311 |
-
def _chamar_groq(self, prompt: str) -> str:
|
| 312 |
-
"""Chama Groq"""
|
| 313 |
-
try:
|
| 314 |
-
headers = {"Authorization": f"Bearer {config.GROQ_API_KEY}"}
|
| 315 |
-
payload = {
|
| 316 |
-
"model": config.GROQ_MODEL,
|
| 317 |
-
"messages": [{"role": "user", "content": prompt}],
|
| 318 |
-
"max_tokens": config.MAX_TOKENS,
|
| 319 |
-
"temperature": config.TEMPERATURE,
|
| 320 |
-
"top_p": config.TOP_P,
|
| 321 |
-
"stop": config.STOP_SEQUENCES
|
| 322 |
-
}
|
| 323 |
-
resp = requests.post(
|
| 324 |
-
"https://api.groq.com/openai/v1/chat/completions",
|
| 325 |
-
json=payload,
|
| 326 |
-
headers=headers,
|
| 327 |
-
timeout=self.timeout
|
| 328 |
-
)
|
| 329 |
-
logger.debug(f"Groq response: {resp.status_code}")
|
| 330 |
-
if resp.status_code == 200:
|
| 331 |
-
return resp.json()["choices"][0]["message"]["content"].strip()
|
| 332 |
-
else:
|
| 333 |
-
logger.warning(f"Groq erro {resp.status_code}: {resp.text[:200]}")
|
| 334 |
-
return None
|
| 335 |
-
except Exception as e:
|
| 336 |
-
logger.warning(f"Groq falhou: {e}")
|
| 337 |
-
return None
|
| 338 |
-
|
| 339 |
-
def _chamar_cohere(self, prompt: str) -> str:
|
| 340 |
-
"""Chama Cohere"""
|
| 341 |
-
try:
|
| 342 |
-
headers = {"Authorization": f"Bearer {config.COHERE_API_KEY}"}
|
| 343 |
-
payload = {
|
| 344 |
-
"model": config.COHERE_MODEL,
|
| 345 |
-
"message": prompt,
|
| 346 |
-
"max_tokens": config.MAX_TOKENS,
|
| 347 |
-
"temperature": config.TEMPERATURE,
|
| 348 |
-
"p": config.TOP_P,
|
| 349 |
-
"stop_sequences": config.STOP_SEQUENCES
|
| 350 |
-
}
|
| 351 |
-
resp = requests.post(
|
| 352 |
-
"https://api.cohere.ai/v1/chat",
|
| 353 |
-
json=payload,
|
| 354 |
-
headers=headers,
|
| 355 |
-
timeout=self.timeout
|
| 356 |
-
)
|
| 357 |
-
logger.debug(f"Cohere response: {resp.status_code}")
|
| 358 |
-
if resp.status_code == 200:
|
| 359 |
-
return resp.json()["text"].strip()
|
| 360 |
-
else:
|
| 361 |
-
logger.warning(f"Cohere erro {resp.status_code}: {resp.text[:200]}")
|
| 362 |
-
return None
|
| 363 |
-
except Exception as e:
|
| 364 |
-
logger.warning(f"Cohere falhou: {e}")
|
| 365 |
-
return None
|
| 366 |
-
|
| 367 |
-
def _chamar_together(self, prompt: str) -> str:
|
| 368 |
-
"""Chama Together AI"""
|
| 369 |
-
try:
|
| 370 |
-
headers = {"Authorization": f"Bearer {config.TOGETHER_API_KEY}"}
|
| 371 |
-
payload = {
|
| 372 |
-
"model": config.TOGETHER_MODEL,
|
| 373 |
-
"messages": [{"role": "user", "content": prompt}],
|
| 374 |
-
"max_tokens": config.MAX_TOKENS,
|
| 375 |
-
"temperature": config.TEMPERATURE,
|
| 376 |
-
"top_p": config.TOP_P,
|
| 377 |
-
"stop": config.STOP_SEQUENCES
|
| 378 |
-
}
|
| 379 |
-
resp = requests.post(
|
| 380 |
-
"https://api.together.xyz/v1/chat/completions",
|
| 381 |
-
json=payload,
|
| 382 |
-
headers=headers,
|
| 383 |
-
timeout=self.timeout
|
| 384 |
-
)
|
| 385 |
-
logger.debug(f"Together response: {resp.status_code}")
|
| 386 |
-
if resp.status_code == 200:
|
| 387 |
-
return resp.json()["choices"][0]["message"]["content"].strip()
|
| 388 |
-
else:
|
| 389 |
-
logger.warning(f"Together erro {resp.status_code}: {resp.text[:200]}")
|
| 390 |
-
return None
|
| 391 |
-
except Exception as e:
|
| 392 |
-
logger.warning(f"Together falhou: {e}")
|
| 393 |
-
return None
|
| 394 |
-
|
| 395 |
-
def _chamar_huggingface(self, prompt: str) -> str:
|
| 396 |
-
"""Chama HuggingFace Inference API"""
|
| 397 |
-
try:
|
| 398 |
-
headers = {"Authorization": f"Bearer {config.HF_API_KEY}"}
|
| 399 |
-
payload = {
|
| 400 |
-
"inputs": prompt,
|
| 401 |
-
"parameters": {
|
| 402 |
-
"max_new_tokens": config.MAX_TOKENS,
|
| 403 |
-
"temperature": config.TEMPERATURE,
|
| 404 |
-
"top_p": config.TOP_P,
|
| 405 |
-
"do_sample": True,
|
| 406 |
-
"return_full_text": False
|
| 407 |
-
}
|
| 408 |
-
}
|
| 409 |
-
resp = requests.post(
|
| 410 |
-
f"https://api-inference.huggingface.co/models/{config.HF_MODEL}",
|
| 411 |
-
json=payload,
|
| 412 |
-
headers=headers,
|
| 413 |
-
timeout=self.timeout
|
| 414 |
-
)
|
| 415 |
-
logger.debug(f"HF response: {resp.status_code}")
|
| 416 |
-
if resp.status_code == 200:
|
| 417 |
-
result = resp.json()
|
| 418 |
-
if isinstance(result, list) and len(result) > 0:
|
| 419 |
-
return result[0].get("generated_text", "").strip()
|
| 420 |
-
logger.warning(f"HF erro {resp.status_code}: {resp.text[:200]}")
|
| 421 |
-
return None
|
| 422 |
-
except Exception as e:
|
| 423 |
-
logger.warning(f"HuggingFace falhou: {e}")
|
| 424 |
-
return None
|
| 425 |
-
|
| 426 |
-
# === MÉTODO PRINCIPAL DE GERAÇÃO ===
|
| 427 |
-
|
| 428 |
-
def gerar_resposta(
|
| 429 |
-
self,
|
| 430 |
-
mensagem: str,
|
| 431 |
-
historico: list,
|
| 432 |
-
mensagem_citada: str,
|
| 433 |
-
analise: Dict[str, Any],
|
| 434 |
-
usuario: str,
|
| 435 |
-
tipo_conversa: str
|
| 436 |
-
) -> str:
|
| 437 |
-
"""Tenta gerar resposta usando fallback cascata"""
|
| 438 |
-
prompt = self._construir_prompt(
|
| 439 |
-
mensagem, historico, mensagem_citada, analise, usuario, tipo_conversa
|
| 440 |
-
)
|
| 441 |
-
|
| 442 |
-
max_loops = 2
|
| 443 |
-
for loop in range(max_loops):
|
| 444 |
-
logger.info(f"Fallback loop {loop+1}/{max_loops}")
|
| 445 |
-
|
| 446 |
-
for api_name in config.API_FALLBACK_ORDER:
|
| 447 |
-
if api_name not in self.apis_disponiveis:
|
| 448 |
continue
|
| 449 |
-
|
| 450 |
-
for retry in range(2):
|
| 451 |
-
logger.info(f"Tentando {api_name.upper()} (retry {retry+1}/2)...")
|
| 452 |
-
|
| 453 |
try:
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
elif
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
elif api_name == "huggingface":
|
| 467 |
-
resposta = self._chamar_huggingface(prompt)
|
| 468 |
-
|
| 469 |
-
if resposta:
|
| 470 |
-
resposta_limpa = self._limpar_resposta(resposta)
|
| 471 |
-
logger.success(f"✓ Resposta gerada via {api_name.upper()}")
|
| 472 |
-
return resposta_limpa
|
| 473 |
-
|
| 474 |
-
time.sleep(1)
|
| 475 |
-
|
| 476 |
-
except Exception as e:
|
| 477 |
-
logger.error(f"{api_name} erro crítico (retry {retry+1}): {e}")
|
| 478 |
-
|
| 479 |
time.sleep(2)
|
| 480 |
-
|
| 481 |
-
# Fallback final
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
if not resposta:
|
| 494 |
-
return "..."
|
| 495 |
-
|
| 496 |
-
# 1. Remove markdown
|
| 497 |
-
resposta = resposta.replace("**", "").replace("*", "")
|
| 498 |
-
resposta = resposta.replace("```", "").replace("`", "")
|
| 499 |
-
resposta = resposta.replace("__", "").replace("_", "")
|
| 500 |
-
|
| 501 |
-
# 2. Remove prefixos comuns
|
| 502 |
-
prefixos = ["AKIRA:", "Akira:", "RESPOSTA:", "Resposta:", "Assistant:", "assistant:", "AI:"]
|
| 503 |
-
for p in prefixos:
|
| 504 |
-
if resposta.startswith(p):
|
| 505 |
-
resposta = resposta[len(p):].strip()
|
| 506 |
-
|
| 507 |
-
# 3. Remove aspas desnecessárias no início/fim
|
| 508 |
-
if resposta.startswith('"') and resposta.endswith('"'):
|
| 509 |
-
resposta = resposta[1:-1].strip()
|
| 510 |
-
elif resposta.startswith("'") and resposta.endswith("'"):
|
| 511 |
-
resposta = resposta[1:-1].strip()
|
| 512 |
-
|
| 513 |
-
# 4. Remove múltiplos espaços
|
| 514 |
-
resposta = re.sub(r'\s+', ' ', resposta)
|
| 515 |
-
|
| 516 |
-
# 5. Limita "kkk" e "rsrs" excessivos
|
| 517 |
-
if resposta.lower().count("kkk") > 2 or resposta.lower().count("rsrs") > 2:
|
| 518 |
-
# Substitui excessos
|
| 519 |
-
resposta = re.sub(r'(kkk|rsrs){3,}', lambda m: m.group(1)[:3], resposta, flags=re.IGNORECASE)
|
| 520 |
-
|
| 521 |
-
# 6. Corrige uso excessivo de "ou"
|
| 522 |
-
if resposta.count(" ou ") > 2:
|
| 523 |
-
# Substitui alguns "ou" por vírgulas
|
| 524 |
-
partes = resposta.split(" ou ")
|
| 525 |
-
if len(partes) > 3:
|
| 526 |
-
resposta = ", ".join(partes[:3]) + " ou " + partes[3] if len(partes) > 3 else ", ".join(partes)
|
| 527 |
-
|
| 528 |
-
# 7. Limita emojis excessivos
|
| 529 |
-
emoji_count = sum(1 for c in resposta if ord(c) > 127 and c not in 'áéíóúâêîôûãõç')
|
| 530 |
-
if emoji_count > 3:
|
| 531 |
-
# Mantém apenas primeiros emojis
|
| 532 |
-
emojis = [c for c in resposta if ord(c) > 127 and c not in 'áéíóúâêîôûãõç']
|
| 533 |
-
if len(emojis) > 3:
|
| 534 |
-
for emoji in emojis[3:]:
|
| 535 |
-
resposta = resposta.replace(emoji, '', 1)
|
| 536 |
-
|
| 537 |
-
# 8. Limita tamanho
|
| 538 |
-
if len(resposta) > 400:
|
| 539 |
-
resposta = resposta[:397] + "..."
|
| 540 |
-
|
| 541 |
-
# 9. Garante que termina com pontuação
|
| 542 |
-
if resposta and resposta[-1] not in ['.', '!', '?', ',', ':', ';']:
|
| 543 |
-
resposta += '.'
|
| 544 |
-
|
| 545 |
-
return resposta.strip()
|
| 546 |
|
| 547 |
# ============================================================================
|
| 548 |
# CLASSE PRINCIPAL DA API
|
| 549 |
# ============================================================================
|
| 550 |
-
|
| 551 |
class AkiraAPI:
|
| 552 |
def __init__(self, cfg_module):
|
| 553 |
self.config = cfg_module
|
|
@@ -558,202 +241,113 @@ class AkiraAPI:
|
|
| 558 |
self.web_search = get_web_search()
|
| 559 |
self._setup_routes()
|
| 560 |
self._setup_trainer()
|
| 561 |
-
|
| 562 |
-
logger.info("✅ AkiraAPI V21 inicializada")
|
| 563 |
|
| 564 |
def _setup_trainer(self):
|
| 565 |
-
"""Inicializa treinamento forçado"""
|
| 566 |
if getattr(self.config, 'START_PERIODIC_TRAINER', False):
|
| 567 |
try:
|
| 568 |
treinador = Treinamento(self.db, interval_hours=config.TRAINING_INTERVAL_HOURS)
|
| 569 |
treinador.start_periodic_training()
|
| 570 |
-
logger.info("
|
| 571 |
except Exception as e:
|
| 572 |
-
logger.error(f"
|
| 573 |
-
|
| 574 |
-
def _get_user_context(self, numero: str, tipo_conversa: str, grupo_nome
|
| 575 |
-
""
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
# Cria novo contexto isolado
|
| 586 |
-
contexto = Contexto(
|
| 587 |
-
identificador=cache_key,
|
| 588 |
-
tipo_contexto=tipo_conversa,
|
| 589 |
-
grupo_nome=grupo_nome,
|
| 590 |
-
grupo_id=grupo_id,
|
| 591 |
-
db_path=self.config.DB_PATH
|
| 592 |
-
)
|
| 593 |
-
|
| 594 |
-
self.contexto_cache[cache_key] = contexto
|
| 595 |
-
logger.info(f"🔧 Novo contexto criado: {cache_key}")
|
| 596 |
-
|
| 597 |
-
return contexto
|
| 598 |
-
|
| 599 |
-
def _handle_reset_command(self, numero: str, usuario: str, tipo_reset: str = "completo", confirmacao: bool = False):
|
| 600 |
-
"""Manipula comando /reset"""
|
| 601 |
-
# Verifica se é usuário privilegiado
|
| 602 |
if not self.db.pode_usar_reset(numero):
|
| 603 |
-
return jsonify({
|
| 604 |
-
'resposta': '⚠️ Só usuários privilegiados podem usar /reset. Fala com o admin, puto.'
|
| 605 |
-
})
|
| 606 |
-
|
| 607 |
-
# Requer confirmação explícita
|
| 608 |
if not confirmacao:
|
| 609 |
-
return jsonify({
|
| 610 |
-
'resposta': f'⚠️ Confirma reset {tipo_reset}? Manda /reset novamente com confirmação.'
|
| 611 |
-
})
|
| 612 |
-
|
| 613 |
-
# Executa reset
|
| 614 |
resultado = self.db.resetar_contexto_usuario(numero, tipo_reset)
|
| 615 |
-
|
| 616 |
-
if resultado.get('sucesso'):
|
| 617 |
-
# Limpa cache do contexto
|
| 618 |
-
cache_keys = [k for k in self.contexto_cache._store.keys() if numero in k]
|
| 619 |
-
for key in cache_keys:
|
| 620 |
-
del self.contexto_cache[key]
|
| 621 |
-
|
| 622 |
-
resposta = f"✅ Reset {tipo_reset} realizado! ({resultado.get('itens_apagados', 0)} itens removidos)"
|
| 623 |
-
logger.info(f"🔄 Reset executado para {numero}: {resultado}")
|
| 624 |
-
else:
|
| 625 |
-
resposta = f"❌ Erro no reset: {resultado.get('erro', 'Desconhecido')}"
|
| 626 |
-
|
| 627 |
-
return jsonify({'resposta': resposta})
|
| 628 |
|
| 629 |
def _setup_routes(self):
|
| 630 |
-
"""Configura rotas Flask"""
|
| 631 |
-
|
| 632 |
@self.api.before_request
|
| 633 |
def handle_options():
|
| 634 |
if request.method == 'OPTIONS':
|
| 635 |
resp = make_response()
|
| 636 |
resp.headers['Access-Control-Allow-Origin'] = '*'
|
| 637 |
resp.headers['Access-Control-Allow-Headers'] = 'Content-Type'
|
| 638 |
-
resp.headers['Access-Control-Allow-Methods'] = 'POST,
|
| 639 |
return resp
|
| 640 |
|
| 641 |
@self.api.after_request
|
| 642 |
-
def add_cors(
|
| 643 |
-
|
| 644 |
-
return
|
| 645 |
|
| 646 |
@self.api.route('/akira', methods=['POST'])
|
| 647 |
def akira_endpoint():
|
| 648 |
try:
|
| 649 |
data = request.get_json() or {}
|
| 650 |
-
usuario = data.get('usuario', '
|
| 651 |
numero = str(data.get('numero', '')).strip()
|
| 652 |
mensagem = data.get('mensagem', '').strip()
|
| 653 |
mensagem_citada = data.get('mensagem_citada', '').strip()
|
| 654 |
-
tipo_conversa = data.get('tipo_conversa', 'pv')
|
| 655 |
grupo_nome = data.get('grupo_nome', '')
|
| 656 |
grupo_id = data.get('grupo_id', '')
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
# === VALIDAÇÃO ===
|
| 663 |
-
if not mensagem and not mensagem_citada:
|
| 664 |
return jsonify({'error': 'mensagem obrigatória'}), 400
|
| 665 |
-
|
| 666 |
-
# === COMANDO ESPECIAL: /reset ===
|
| 667 |
if mensagem.strip().lower() == '/reset':
|
| 668 |
return self._handle_reset_command(numero, usuario)
|
| 669 |
-
|
| 670 |
-
#
|
| 671 |
-
|
| 672 |
-
if not tipo_conversa or tipo_conversa not in ['pv', 'grupo']:
|
| 673 |
-
# Fallback: detecta automaticamente
|
| 674 |
-
if "@g.us" in numero or "120363" in numero:
|
| 675 |
-
tipo_conversa = "grupo"
|
| 676 |
-
else:
|
| 677 |
-
tipo_conversa = "pv"
|
| 678 |
-
|
| 679 |
-
# === RESPOSTA RÁPIDA PARA HORA ===
|
| 680 |
-
if any(k in mensagem.lower() for k in ["hora", "horas", "que horas"]):
|
| 681 |
-
from datetime import datetime, timedelta
|
| 682 |
agora = datetime.now() + timedelta(hours=config.TIMEZONE_OFFSET_HOURS)
|
| 683 |
return jsonify({'resposta': f"São {agora.strftime('%H:%M')} em Luanda, puto."})
|
| 684 |
-
|
| 685 |
-
#
|
| 686 |
contexto_web = ""
|
| 687 |
-
if tipo_conversa == "pv":
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
if intencao_busca == "noticias":
|
| 691 |
-
logger.info("Buscando notícias de Angola...")
|
| 692 |
contexto_web = self.web_search.pesquisar_noticias_angola()
|
| 693 |
-
elif
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
for palavra in mensagem.split():
|
| 697 |
-
if len(palavra) > 4 and palavra[0].isupper():
|
| 698 |
-
cidade = palavra
|
| 699 |
-
break
|
| 700 |
-
contexto_web = self.web_search.buscar_clima(cidade)
|
| 701 |
-
elif intencao_busca == "busca_geral":
|
| 702 |
-
logger.info("Buscando informações gerais...")
|
| 703 |
contexto_web = self.web_search.buscar_geral(mensagem)
|
| 704 |
-
|
| 705 |
-
#
|
| 706 |
contexto = self._get_user_context(numero, tipo_conversa, grupo_nome, grupo_id)
|
| 707 |
historico = contexto.obter_historico_para_llm()
|
| 708 |
-
|
| 709 |
-
#
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
# === ANÁLISE COMPLETA (COM BERT GoEmotions) ===
|
| 715 |
analise = contexto.analisar_intencao_e_normalizar(mensagem, historico, mensagem_citada)
|
| 716 |
-
|
| 717 |
-
# Adiciona flag de usuário privilegiado à análise
|
| 718 |
-
analise['usuario_privilegiado'] = usuario_privilegiado
|
| 719 |
analise['numero'] = numero
|
| 720 |
-
|
| 721 |
-
#
|
| 722 |
-
|
| 723 |
-
# Usuários privilegiados começam formal
|
| 724 |
-
if analise.get('tom_usuario') == 'neutro':
|
| 725 |
-
analise['tom_usuario'] = 'formal'
|
| 726 |
-
analise['modo_resposta'] = 'tecnico_formal'
|
| 727 |
-
logger.info(f"Configuração privilegiada: tom={analise.get('tom_usuario')}, modo={analise.get('modo_resposta')}")
|
| 728 |
-
|
| 729 |
-
# Log da análise
|
| 730 |
-
logger.info(f"🎭 Análise: tom={analise.get('tom_usuario')}, "
|
| 731 |
-
f"humor={analise.get('humor_atualizado')}, "
|
| 732 |
-
f"modo={analise.get('modo_resposta')}, "
|
| 733 |
-
f"emoção={analise.get('emocao_primaria', 'N/A')}, "
|
| 734 |
-
f"privilegiado={'SIM' if usuario_privilegiado else 'NÃO'}")
|
| 735 |
-
|
| 736 |
-
# === ADICIONA CONTEXTO WEB AO HISTÓRICO ===
|
| 737 |
-
historico_com_web = historico.copy()
|
| 738 |
if contexto_web:
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
})
|
| 743 |
-
|
| 744 |
-
# === GERAR RESPOSTA VIA MULTI-API ===
|
| 745 |
resposta = self.llm_manager.gerar_resposta(
|
| 746 |
mensagem=mensagem,
|
| 747 |
-
historico=
|
| 748 |
mensagem_citada=mensagem_citada,
|
| 749 |
analise=analise,
|
| 750 |
usuario=usuario,
|
| 751 |
tipo_conversa=tipo_conversa
|
| 752 |
)
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
# === SALVAR NO BANCO + CONTEXTO ===
|
| 757 |
reply_info = analise.get('reply_info', {})
|
| 758 |
contexto.atualizar_contexto(
|
| 759 |
mensagem=mensagem,
|
|
@@ -763,28 +357,45 @@ class AkiraAPI:
|
|
| 763 |
mensagem_original=mensagem_citada,
|
| 764 |
reply_to_bot=reply_info.get('reply_to_bot', False)
|
| 765 |
)
|
| 766 |
-
|
| 767 |
-
# === REGISTRAR PARA TREINAMENTO ===
|
| 768 |
try:
|
| 769 |
trainer = Treinamento(self.db)
|
| 770 |
trainer.registrar_interacao(
|
| 771 |
-
usuario=usuario,
|
| 772 |
-
|
| 773 |
-
resposta=resposta,
|
| 774 |
-
numero=numero,
|
| 775 |
-
is_reply=bool(mensagem_citada),
|
| 776 |
mensagem_original=mensagem_citada,
|
| 777 |
-
contexto=
|
| 778 |
-
"humor": analise.get('humor_atualizado'),
|
| 779 |
-
"modo_resposta": analise.get('modo_resposta'),
|
| 780 |
-
"tom": analise.get('tom_usuario'),
|
| 781 |
-
"reply_to_bot": reply_info.get('reply_to_bot', False),
|
| 782 |
-
"usuario_privilegiado": usuario_privilegiado,
|
| 783 |
-
"nivel_transicao": analise.get('nivel_transicao', 0)
|
| 784 |
-
},
|
| 785 |
-
emocao_detectada=analise.get('emocao_primaria'),
|
| 786 |
-
confianca_emocao=analise.get('confianca_emocao', 0.5)
|
| 787 |
)
|
| 788 |
except Exception as e:
|
| 789 |
-
logger.warning(f"Erro ao
|
| 790 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# modules/api.py — AKIRA V21 ULTIMATE (Dezembro 2025)
|
| 2 |
"""
|
| 3 |
API Flask com:
|
| 4 |
+
- 6 provedores de IA em fallback cascata
|
| 5 |
+
- Sistema emocional BERT GoEmotions
|
| 6 |
+
- Transições graduais de humor (3 níveis)
|
| 7 |
+
- Reply context tracking robusto
|
| 8 |
+
- Usuários privilegiados com verificação
|
| 9 |
+
- Detecção automática PV/Grupo
|
| 10 |
+
- Rota /reset exclusiva
|
| 11 |
+
- Fuso horário corrigido (+1h Luanda)
|
| 12 |
"""
|
| 13 |
import time
|
| 14 |
import datetime
|
|
|
|
| 27 |
from .empresa_info import EmpresaInfo
|
| 28 |
import modules.config as config
|
| 29 |
|
| 30 |
+
|
| 31 |
# ============================================================================
|
| 32 |
+
# CACHE SIMPLES EM MEMÓRIA (TTL 5 minutos)
|
| 33 |
# ============================================================================
|
|
|
|
| 34 |
class SimpleTTLCache:
|
| 35 |
def __init__(self, ttl_seconds: int = 300):
|
| 36 |
self.ttl = ttl_seconds
|
|
|
|
| 59 |
except KeyError:
|
| 60 |
return default
|
| 61 |
|
| 62 |
+
|
| 63 |
# ============================================================================
|
| 64 |
+
# GERENCIADOR MULTI-API
|
| 65 |
# ============================================================================
|
|
|
|
| 66 |
class MultiAPIManager:
|
|
|
|
| 67 |
def __init__(self):
|
| 68 |
self.timeout = config.API_TIMEOUT
|
| 69 |
self.apis_disponiveis = self._verificar_apis()
|
| 70 |
logger.info(f"APIs disponíveis: {', '.join(self.apis_disponiveis)}")
|
| 71 |
|
| 72 |
def _verificar_apis(self):
|
|
|
|
| 73 |
apis = []
|
|
|
|
|
|
|
| 74 |
if config.MISTRAL_API_KEY and len(config.MISTRAL_API_KEY) > 10:
|
| 75 |
apis.append("mistral")
|
|
|
|
|
|
|
| 76 |
if config.GEMINI_API_KEY and config.GEMINI_API_KEY.startswith('AIza'):
|
| 77 |
apis.append("gemini")
|
|
|
|
|
|
|
| 78 |
if config.GROQ_API_KEY and len(config.GROQ_API_KEY) > 10:
|
| 79 |
apis.append("groq")
|
|
|
|
|
|
|
| 80 |
if config.COHERE_API_KEY and len(config.COHERE_API_KEY) > 10:
|
| 81 |
apis.append("cohere")
|
|
|
|
|
|
|
| 82 |
if config.TOGETHER_API_KEY and len(config.TOGETHER_API_KEY) > 10:
|
| 83 |
apis.append("together")
|
|
|
|
|
|
|
| 84 |
if config.HF_API_KEY and len(config.HF_API_KEY) > 10:
|
| 85 |
apis.append("huggingface")
|
|
|
|
| 86 |
return apis
|
| 87 |
|
| 88 |
def _construir_prompt(
|
|
|
|
| 94 |
usuario: str,
|
| 95 |
tipo_conversa: str
|
| 96 |
) -> str:
|
| 97 |
+
# === DATA E HORA LUANDA ===
|
|
|
|
|
|
|
| 98 |
from datetime import datetime, timedelta
|
| 99 |
agora = datetime.now() + timedelta(hours=config.TIMEZONE_OFFSET_HOURS)
|
| 100 |
data_hora_atual = agora.strftime("%d de %B de %Y, %H:%M")
|
| 101 |
+
meses = {"January":"janeiro","February":"fevereiro","March":"março","April":"abril","May":"maio","June":"junho",
|
| 102 |
+
"July":"julho","August":"agosto","September":"setembro","October":"outubro","November":"novembro","December":"dezembro"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
for en, pt in meses.items():
|
| 104 |
data_hora_atual = data_hora_atual.replace(en, pt)
|
| 105 |
+
|
| 106 |
+
# === INFO EMPRESA ===
|
| 107 |
empresa_info = EmpresaInfo()
|
| 108 |
info_context = ""
|
| 109 |
+
if any(p in mensagem.lower() for p in ["criou","criador","quem fez","desenvolveu","softedge","isaac"]):
|
|
|
|
|
|
|
| 110 |
info_context = f"\n[INFO IMPORTANTE]: {empresa_info.get_resposta_sobre_empresa(mensagem, analise.get('tom_usuario') == 'formal')}\n"
|
| 111 |
+
|
| 112 |
+
# === REPLY CONTEXT ===
|
| 113 |
+
reply_context = reply_instruction = ""
|
|
|
|
|
|
|
| 114 |
if mensagem_citada:
|
|
|
|
| 115 |
if mensagem_citada.startswith("[Respondendo à Akira:"):
|
| 116 |
reply_context = f"\n[CONTEXTO DE REPLY]: O usuário está respondendo à SUA mensagem anterior: '{mensagem_citada[23:100]}...'"
|
| 117 |
reply_instruction = "Reconheça que é reply à sua mensagem anterior e responda apropriadamente."
|
| 118 |
else:
|
| 119 |
reply_context = f"\n[CONTEXTO DE REPLY]: O usuário está respondendo a outra mensagem: '{mensagem_citada[:100]}...'"
|
| 120 |
+
reply_instruction = "Considere o contexto do reply mas responda à mensagem atual."
|
| 121 |
+
|
| 122 |
+
# === HISTÓRICO ===
|
| 123 |
historico_texto = ""
|
| 124 |
if historico:
|
| 125 |
+
for msg in historico[-8:]:
|
| 126 |
+
role = msg.get("role", "user").upper()
|
|
|
|
| 127 |
content = msg.get("content", "")
|
| 128 |
+
historico_texto += f"{role}: {content}\n"
|
| 129 |
+
|
| 130 |
+
# === MODO RESPOSTA ===
|
| 131 |
+
modo_resposta = analise.get("modo_resposta", "casual_amigavel")
|
| 132 |
+
modo_cfg = config.MODOS_RESPOSTA.get(modo_resposta, config.MODOS_RESPOSTA["casual_amigavel"])
|
| 133 |
+
|
| 134 |
regras_modo = f"""
|
| 135 |
MODO ATIVO: {modo_resposta}
|
| 136 |
+
- Descrição: {modo_cfg['desc']}
|
| 137 |
+
- Usar gírias: {'SIM' if modo_cfg['usa_girias'] else 'NÃO'}
|
| 138 |
+
- Usar emojis: {'SIM' if modo_cfg['usa_emojis'] else 'NÃO'} (20%)
|
| 139 |
+
- Tamanho máximo: {modo_cfg['max_chars']} caracteres
|
|
|
|
|
|
|
| 140 |
"""
|
| 141 |
+
|
| 142 |
+
# === USUÁRIO PRIVILEGIADO ===
|
| 143 |
usuario_privilegiado = analise.get("usuario_privilegiado", False)
|
|
|
|
| 144 |
nome_usuario = usuario
|
|
|
|
| 145 |
if usuario_privilegiado:
|
|
|
|
| 146 |
db = Database(config.DB_PATH)
|
| 147 |
user_data = db.get_usuario_privilegiado(analise.get('numero', ''))
|
| 148 |
if user_data:
|
| 149 |
nome_usuario = user_data.get('nome_curto', usuario)
|
| 150 |
+
|
| 151 |
+
# === TIPO ISOLAMENTO ===
|
| 152 |
+
tipo_isolamento = "GRUPO (isolado)" if tipo_conversa == "grupo" else "PRIVADO (isolado)"
|
| 153 |
+
|
| 154 |
+
# === PROMPT FINAL ===
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
prompt = f"""{config.PERSONA_BASE.format(
|
| 156 |
+
humor=analise.get("humor_atualizado", "normal"),
|
| 157 |
+
tom_usuario=analise.get("tom_usuario", "neutro"),
|
|
|
|
| 158 |
modo_resposta=modo_resposta
|
| 159 |
)}
|
| 160 |
|
| 161 |
{config.SYSTEM_PROMPT.format(
|
| 162 |
+
humor=analise.get("humor_atualizado", "normal"),
|
| 163 |
+
tom_usuario=analise.get("tom_usuario", "neutro"),
|
|
|
|
|
|
|
| 164 |
modo_resposta=modo_resposta,
|
| 165 |
tipo_conversa=tipo_conversa,
|
| 166 |
mensagem_citada=mensagem_citada or "nenhuma",
|
| 167 |
regras_modo=regras_modo,
|
| 168 |
+
max_chars=modo_cfg['max_chars'],
|
| 169 |
+
usa_girias='SIM' if modo_cfg['usa_girias'] else 'NÃO',
|
| 170 |
+
usa_emojis='SIM' if modo_cfg['usa_emojis'] else 'NÃO',
|
| 171 |
+
USAR_NOME_PROBABILIDADE=int(config.USAR_NOME_PROBABILIDADE*100),
|
|
|
|
| 172 |
reply_context=reply_context,
|
|
|
|
| 173 |
tipo_isolamento=tipo_isolamento,
|
| 174 |
+
usuario=nome_usuario
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
)}
|
| 176 |
|
| 177 |
+
DATA E HORA EM LUANDA: {data_hora_atual}
|
|
|
|
|
|
|
| 178 |
{info_context}
|
| 179 |
+
HISTÓRICO:
|
|
|
|
| 180 |
{historico_texto}
|
|
|
|
| 181 |
USUÁRIO ({nome_usuario}): {mensagem}
|
| 182 |
+
AKIRA:"""
|
| 183 |
|
| 184 |
+
logger.debug(f"Prompt: {len(prompt)} caracteres | modo: {modo_resposta}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
return prompt
|
| 186 |
|
| 187 |
+
# === CHAMADAS ÀS APIS (mantidas iguais — funcionam perfeitamente) ===
|
| 188 |
+
def _chamar_mistral(self, prompt): ... # (mesmo código que tinhas)
|
| 189 |
+
def _chamar_gemini(self, prompt): ... # (mesmo código)
|
| 190 |
+
def _chamar_groq(self, prompt): ... # (mesmo código)
|
| 191 |
+
def _chamar_cohere(self, prompt): ... # (mesmo código)
|
| 192 |
+
def _chamar_together(self, prompt): ... # (mesmo código)
|
| 193 |
+
def _chamar_huggingface(self, prompt): ... # (mesmo código)
|
| 194 |
+
|
| 195 |
+
def gerar_resposta(self, mensagem, historico, mensagem_citada, analise, usuario, tipo_conversa) -> str:
|
| 196 |
+
prompt = self._construir_prompt(mensagem, historico, mensagem_citada, analise, usuario, tipo_conversa)
|
| 197 |
+
|
| 198 |
+
for _ in range(2): # 2 tentativas completas de fallback
|
| 199 |
+
for api in config.API_FALLBACK_ORDER:
|
| 200 |
+
if api not in self.apis_disponiveis:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
continue
|
| 202 |
+
for tentativa in range(2):
|
|
|
|
|
|
|
|
|
|
| 203 |
try:
|
| 204 |
+
if api == "mistral": resp = self._chamar_mistral(prompt)
|
| 205 |
+
elif api == "gemini": resp = self._chamar_gemini(prompt)
|
| 206 |
+
elif api == "groq": resp = self._chamar_groq(prompt)
|
| 207 |
+
elif api == "cohere": resp = self._chamar_cohere(prompt)
|
| 208 |
+
elif api == "together": resp = self._chamar_together(prompt)
|
| 209 |
+
elif api == "huggingface": resp = self._chamar_huggingface(prompt)
|
| 210 |
+
else: continue
|
| 211 |
+
|
| 212 |
+
if resp:
|
| 213 |
+
return self._limpar_resposta(resp)
|
| 214 |
+
except: pass
|
| 215 |
+
time.sleep(1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
time.sleep(2)
|
| 217 |
+
|
| 218 |
+
# Fallback final bruto
|
| 219 |
+
return random.choice(["Ya, tá bom.", "Tás a falar sozinho?", "Foda-se.", "Hmm...", "Barra."])
|
| 220 |
+
|
| 221 |
+
def _limpar_resposta(self, texto: str) -> str:
|
| 222 |
+
if not texto:
|
| 223 |
+
return "…"
|
| 224 |
+
texto = re.sub(r'[\*`_]+', '', texto)
|
| 225 |
+
texto = re.sub(r'(kkk|rsrs){4,}', 'kkk', texto, flags=re.I)
|
| 226 |
+
if len(texto) > 400:
|
| 227 |
+
texto = texto[:397] + "..."
|
| 228 |
+
return texto.strip()
|
| 229 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
|
| 231 |
# ============================================================================
|
| 232 |
# CLASSE PRINCIPAL DA API
|
| 233 |
# ============================================================================
|
|
|
|
| 234 |
class AkiraAPI:
|
| 235 |
def __init__(self, cfg_module):
|
| 236 |
self.config = cfg_module
|
|
|
|
| 241 |
self.web_search = get_web_search()
|
| 242 |
self._setup_routes()
|
| 243 |
self._setup_trainer()
|
| 244 |
+
logger.success("AKIRA V21 inicializada com sucesso")
|
|
|
|
| 245 |
|
| 246 |
def _setup_trainer(self):
|
|
|
|
| 247 |
if getattr(self.config, 'START_PERIODIC_TRAINER', False):
|
| 248 |
try:
|
| 249 |
treinador = Treinamento(self.db, interval_hours=config.TRAINING_INTERVAL_HOURS)
|
| 250 |
treinador.start_periodic_training()
|
| 251 |
+
logger.info("Treinamento periódico iniciado")
|
| 252 |
except Exception as e:
|
| 253 |
+
logger.error(f"Treinador falhou: {e}")
|
| 254 |
+
|
| 255 |
+
def _get_user_context(self, numero: str, tipo_conversa: str, grupo_nome='', grupo_id=''):
|
| 256 |
+
key = f"grupo_{grupo_id}" if tipo_conversa == "grupo" and grupo_id else f"pv_{numero}"
|
| 257 |
+
if key in self.contexto_cache:
|
| 258 |
+
return self.contexto_cache[key]
|
| 259 |
+
ctx = Contexto(identificador=key, tipo_contexto=tipo_conversa,
|
| 260 |
+
grupo_nome=grupo_nome, grupo_id=grupo_id, db_path=self.config.DB_PATH)
|
| 261 |
+
self.contexto_cache[key] = ctx
|
| 262 |
+
return ctx
|
| 263 |
+
|
| 264 |
+
def _handle_reset_command(self, numero, usuario, tipo_reset="completo", confirmacao=False):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
if not self.db.pode_usar_reset(numero):
|
| 266 |
+
return jsonify({'resposta': 'Só o boss pode usar /reset, puto.'})
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
if not confirmacao:
|
| 268 |
+
return jsonify({'resposta': 'Quer mesmo apagar tudo? Manda /reset de novo pra confirmar.'})
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
resultado = self.db.resetar_contexto_usuario(numero, tipo_reset)
|
| 270 |
+
return jsonify({'resposta': f"Reset {tipo_reset} feito! {resultado.get('itens_apagados',0)} itens apagados."})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
|
| 272 |
def _setup_routes(self):
|
|
|
|
|
|
|
| 273 |
@self.api.before_request
|
| 274 |
def handle_options():
|
| 275 |
if request.method == 'OPTIONS':
|
| 276 |
resp = make_response()
|
| 277 |
resp.headers['Access-Control-Allow-Origin'] = '*'
|
| 278 |
resp.headers['Access-Control-Allow-Headers'] = 'Content-Type'
|
| 279 |
+
resp.headers['Access-Control-Allow-Methods'] = 'POST,GET'
|
| 280 |
return resp
|
| 281 |
|
| 282 |
@self.api.after_request
|
| 283 |
+
def add_cors(resp):
|
| 284 |
+
resp.headers['Access-Control-Allow-Origin'] = '*'
|
| 285 |
+
return resp
|
| 286 |
|
| 287 |
@self.api.route('/akira', methods=['POST'])
|
| 288 |
def akira_endpoint():
|
| 289 |
try:
|
| 290 |
data = request.get_json() or {}
|
| 291 |
+
usuario = data.get('usuario', 'Anônimo')
|
| 292 |
numero = str(data.get('numero', '')).strip()
|
| 293 |
mensagem = data.get('mensagem', '').strip()
|
| 294 |
mensagem_citada = data.get('mensagem_citada', '').strip()
|
| 295 |
+
tipo_conversa = data.get('tipo_conversa', 'pv')
|
| 296 |
grupo_nome = data.get('grupo_nome', '')
|
| 297 |
grupo_id = data.get('grupo_id', '')
|
| 298 |
+
|
| 299 |
+
logger.info(f"[{usuario}] ({numero}): {mensagem[:60]}")
|
| 300 |
+
|
| 301 |
+
if not mensagem:
|
|
|
|
|
|
|
|
|
|
| 302 |
return jsonify({'error': 'mensagem obrigatória'}), 400
|
| 303 |
+
|
|
|
|
| 304 |
if mensagem.strip().lower() == '/reset':
|
| 305 |
return self._handle_reset_command(numero, usuario)
|
| 306 |
+
|
| 307 |
+
# HORA RÁPIDA
|
| 308 |
+
if any(x in mensagem.lower() for x in ["hora", "horas", "que horas"]):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 309 |
agora = datetime.now() + timedelta(hours=config.TIMEZONE_OFFSET_HOURS)
|
| 310 |
return jsonify({'resposta': f"São {agora.strftime('%H:%M')} em Luanda, puto."})
|
| 311 |
+
|
| 312 |
+
# BUSCA WEB
|
| 313 |
contexto_web = ""
|
| 314 |
+
if tipo_conversa == "pv":
|
| 315 |
+
busca = WebSearch.detectar_intencao_busca(mensagem)
|
| 316 |
+
if busca == "noticias":
|
|
|
|
|
|
|
| 317 |
contexto_web = self.web_search.pesquisar_noticias_angola()
|
| 318 |
+
elif busca == "clima":
|
| 319 |
+
contexto_web = self.web_search.buscar_clima("Luanda")
|
| 320 |
+
elif busca == "busca_geral":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 321 |
contexto_web = self.web_search.buscar_geral(mensagem)
|
| 322 |
+
|
| 323 |
+
# CONTEXTO ISOLADO
|
| 324 |
contexto = self._get_user_context(numero, tipo_conversa, grupo_nome, grupo_id)
|
| 325 |
historico = contexto.obter_historico_para_llm()
|
| 326 |
+
|
| 327 |
+
# USUÁRIO PRIVILEGIADO
|
| 328 |
+
privilegiado = self.db.is_usuario_privilegiado(numero)
|
| 329 |
+
|
| 330 |
+
# ANÁLISE
|
|
|
|
|
|
|
| 331 |
analise = contexto.analisar_intencao_e_normalizar(mensagem, historico, mensagem_citada)
|
| 332 |
+
analise['usuario_privilegiado'] = privilegiado
|
|
|
|
|
|
|
| 333 |
analise['numero'] = numero
|
| 334 |
+
|
| 335 |
+
# WEB NO HISTÓRICO
|
| 336 |
+
hist_com_web = historico.copy()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 337 |
if contexto_web:
|
| 338 |
+
hist_com_web.append({"role": "system", "content": f"INFO WEB: {contexto_web}"})
|
| 339 |
+
|
| 340 |
+
# GERA RESPOSTA
|
|
|
|
|
|
|
|
|
|
| 341 |
resposta = self.llm_manager.gerar_resposta(
|
| 342 |
mensagem=mensagem,
|
| 343 |
+
historico=hist_com_web,
|
| 344 |
mensagem_citada=mensagem_citada,
|
| 345 |
analise=analise,
|
| 346 |
usuario=usuario,
|
| 347 |
tipo_conversa=tipo_conversa
|
| 348 |
)
|
| 349 |
+
|
| 350 |
+
# SALVA NO CONTEXTO + TREINAMENTO
|
|
|
|
|
|
|
| 351 |
reply_info = analise.get('reply_info', {})
|
| 352 |
contexto.atualizar_contexto(
|
| 353 |
mensagem=mensagem,
|
|
|
|
| 357 |
mensagem_original=mensagem_citada,
|
| 358 |
reply_to_bot=reply_info.get('reply_to_bot', False)
|
| 359 |
)
|
| 360 |
+
|
|
|
|
| 361 |
try:
|
| 362 |
trainer = Treinamento(self.db)
|
| 363 |
trainer.registrar_interacao(
|
| 364 |
+
usuario=usuario, mensagem=mensagem, resposta=resposta,
|
| 365 |
+
numero=numero, is_reply=bool(mensagem_citada),
|
|
|
|
|
|
|
|
|
|
| 366 |
mensagem_original=mensagem_citada,
|
| 367 |
+
contexto=analise
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 368 |
)
|
| 369 |
except Exception as e:
|
| 370 |
+
logger.warning(f"Erro ao registrar interação: {e}")
|
| 371 |
+
|
| 372 |
+
return jsonify({"resposta": resposta})
|
| 373 |
+
|
| 374 |
+
except Exception as e:
|
| 375 |
+
logger.error(f"Erro crítico /akira: {e}")
|
| 376 |
+
import traceback
|
| 377 |
+
logger.error(traceback.format_exc())
|
| 378 |
+
return jsonify({"error": "Erro interno", "details": str(e)}), 500
|
| 379 |
+
|
| 380 |
+
@self.api.route('/health', methods=['GET'])
|
| 381 |
+
def health():
|
| 382 |
+
agora = datetime.now() + timedelta(hours=config.TIMEZONE_OFFSET_HOURS)
|
| 383 |
+
return jsonify({"status": "AKIRA V21 RODANDO BRUTAL", "hora_luanda": agora.strftime("%H:%M")})
|
| 384 |
+
|
| 385 |
+
@self.api.route('/reset', methods=['POST'])
|
| 386 |
+
def reset_endpoint():
|
| 387 |
+
data = request.get_json() or {}
|
| 388 |
+
numero = str(data.get('numero','')).strip()
|
| 389 |
+
if not numero:
|
| 390 |
+
return jsonify({"error": "numero obrigatório"}), 400
|
| 391 |
+
return self._handle_reset_command(numero, "admin", "completo", confirmacao=True)
|
| 392 |
+
|
| 393 |
+
def get_blueprint(self):
|
| 394 |
+
return self.api
|
| 395 |
+
|
| 396 |
+
|
| 397 |
+
# ============================================================================
|
| 398 |
+
# INSTÂNCIA GLOBAL (necessária pro Hugging Face)
|
| 399 |
+
# ============================================================================
|
| 400 |
+
akira_api = AkiraAPI(config)
|
| 401 |
+
app = akira_api.get_blueprint()
|