akra35567 commited on
Commit
0b27f09
·
verified ·
1 Parent(s): 9a70944

Update modules/api.py

Browse files
Files changed (1) hide show
  1. modules/api.py +337 -177
modules/api.py CHANGED
@@ -1,11 +1,14 @@
1
- # modules/api.py — AKIRA V20 ULTIMATE (Dezembro 2025)
2
- """API Flask com 6 provedores de IA em fallback cascata
3
- - Mistral → Gemini → Groq → Cohere → Together → HuggingFace
4
- - Controle estilístico avançado
5
- - Transições graduais de humor
6
- - Reply context tracking
7
- - Fuso horário corrigido (+1h)
8
- - Isolamento total PV/Grupo
 
 
 
9
  """
10
  import time
11
  import datetime
@@ -13,6 +16,8 @@ import requests
13
  import os
14
  import json
15
  import random
 
 
16
  from flask import Blueprint, request, jsonify, make_response
17
  from loguru import logger
18
  from .contexto import Contexto
@@ -48,8 +53,14 @@ class SimpleTTLCache:
48
  raise KeyError(key)
49
  return self._store[key][0]
50
 
 
 
 
 
 
 
51
  # ============================================================================
52
- # GERENCIADOR MULTI-API
53
  # ============================================================================
54
 
55
  class MultiAPIManager:
@@ -62,18 +73,31 @@ class MultiAPIManager:
62
  def _verificar_apis(self):
63
  """Verifica quais APIs estão configuradas"""
64
  apis = []
65
- if config.MISTRAL_API_KEY and config.MISTRAL_API_KEY.strip():
 
 
66
  apis.append("mistral")
67
- if config.GEMINI_API_KEY and config.GEMINI_API_KEY.strip():
 
 
68
  apis.append("gemini")
69
- if config.GROQ_API_KEY and config.GROQ_API_KEY.strip():
 
 
70
  apis.append("groq")
71
- if config.COHERE_API_KEY and config.COHERE_API_KEY.strip():
 
 
72
  apis.append("cohere")
73
- if config.TOGETHER_API_KEY and config.TOGETHER_API_KEY.strip():
 
 
74
  apis.append("together")
75
- if config.HF_API_KEY and config.HF_API_KEY.strip():
 
 
76
  apis.append("huggingface")
 
77
  return apis
78
 
79
  def _construir_prompt(
@@ -81,14 +105,11 @@ class MultiAPIManager:
81
  mensagem: str,
82
  historico: list,
83
  mensagem_citada: str,
84
- humor: str,
85
- tom_usuario: str,
86
- modo_resposta: str,
87
  usuario: str,
88
- usar_nome: bool,
89
  tipo_conversa: str
90
  ) -> str:
91
- """Constrói prompt otimizado com controle estilístico"""
92
 
93
  # === DATA E HORA ATUAL (CORRIGIDA +1H) ===
94
  from datetime import datetime, timedelta
@@ -111,16 +132,20 @@ class MultiAPIManager:
111
  msg_lower = mensagem.lower()
112
 
113
  if any(p in msg_lower for p in ["criou", "criador", "quem fez", "desenvolveu", "softedge", "isaac"]):
114
- info_context = f"\n[INFO IMPORTANTE]: {empresa_info.get_resposta_sobre_empresa(mensagem, tom_usuario == 'formal')}\n"
115
 
116
  # === CONTEXTO DE REPLY ===
117
  reply_context = ""
 
 
118
  if mensagem_citada:
119
  # Verifica se é reply à própria Akira
120
  if mensagem_citada.startswith("[Respondendo à Akira:"):
121
- reply_context = f"\n[USUÁRIO ESTÁ RESPONDENDO À SUA MENSAGEM ANTERIOR]: {mensagem_citada}\n"
 
122
  else:
123
- reply_context = f"\n[USUÁRIO ESTÁ RESPONDENDO A OUTRA MENSAGEM]: {mensagem_citada}\n"
 
124
 
125
  # === HISTÓRICO FORMATADO ===
126
  historico_texto = ""
@@ -132,29 +157,62 @@ class MultiAPIManager:
132
  historico_texto += f"{role.upper()}: {content}\n"
133
 
134
  # === CONFIGURAÇÃO DO MODO DE RESPOSTA ===
 
135
  modo_config = config.MODOS_RESPOSTA.get(modo_resposta, config.MODOS_RESPOSTA["normal_ironico"])
136
 
137
  regras_modo = f"""
138
  MODO ATIVO: {modo_resposta}
139
  - Descrição: {modo_config['desc']}
140
  - Exemplo: {modo_config['exemplo']}
141
- - Usar gírias: {'SIM' if modo_config['usa_girias'] else 'NÃO'}
 
 
142
  - Tamanho máximo: {modo_config['max_chars']} caracteres
143
  """
144
 
145
- # === NOME DO USUÁRIO ===
146
- probabilidade_nome = int(config.USAR_NOME_PROBABILIDADE * 100)
147
- usar_nome_str = f"SIM (use '{usuario}')" if usar_nome else f"NÃO (probabilidade {probabilidade_nome}% não ativada)"
 
 
 
 
 
 
 
 
 
 
 
 
 
148
 
149
  # === TIPO DE ISOLAMENTO ===
150
- tipo_isolamento = "GRUPO (histórico isolado de conversas privadas)" if tipo_conversa == "grupo" else "PRIVADO (histórico isolado de grupos)"
 
 
 
 
 
 
 
 
 
 
151
 
152
- # === PROMPT FINAL ===
153
- prompt = f"""{config.PERSONA_BASE.format(humor=humor, tom_usuario=tom_usuario, modo_resposta=modo_resposta)}
 
 
 
 
 
154
 
155
  {config.SYSTEM_PROMPT.format(
156
- humor=humor,
 
157
  tom_usuario=tom_usuario,
 
158
  modo_resposta=modo_resposta,
159
  tipo_conversa=tipo_conversa,
160
  mensagem_citada=mensagem_citada or "nenhuma",
@@ -162,27 +220,39 @@ MODO ATIVO: {modo_resposta}
162
  max_chars=modo_config['max_chars'],
163
  usa_girias='SIM' if modo_config['usa_girias'] else 'NÃO',
164
  usa_emojis='SIM' if modo_config['usa_emojis'] else 'NÃO',
 
 
165
  reply_context=reply_context,
 
166
  tipo_isolamento=tipo_isolamento,
167
- usuario=usuario,
168
- prob_nome=probabilidade_nome
 
 
 
 
 
 
169
  )}
170
 
171
  {config.REGRAS_ADAPTATIVAS}
172
 
173
- DATA E HORA ATUAL: {data_hora_atual}
174
  {info_context}
175
 
176
- CONTEXTO DA CONVERSA:
177
  {historico_texto}
178
 
179
- USUÁRIO ({usuario}): {mensagem}
180
 
181
  AKIRA (responda {modo_config['desc'].lower()}, máximo {modo_config['max_chars']} caracteres):"""
182
 
 
 
 
183
  return prompt
184
 
185
- # === CHAMADAS ÀS APIS (MANTIDAS IGUAIS) ===
186
 
187
  def _chamar_mistral(self, prompt: str) -> str:
188
  """Chama Mistral AI"""
@@ -192,7 +262,11 @@ AKIRA (responda {modo_config['desc'].lower()}, máximo {modo_config['max_chars']
192
  "model": config.MISTRAL_MODEL,
193
  "messages": [{"role": "user", "content": prompt}],
194
  "max_tokens": config.MAX_TOKENS,
195
- "temperature": config.TEMPERATURE
 
 
 
 
196
  }
197
  resp = requests.post(
198
  "https://api.mistral.ai/v1/chat/completions",
@@ -204,7 +278,7 @@ AKIRA (responda {modo_config['desc'].lower()}, máximo {modo_config['max_chars']
204
  if resp.status_code == 200:
205
  return resp.json()["choices"][0]["message"]["content"].strip()
206
  else:
207
- logger.warning(f"Mistral erro: {resp.status_code}")
208
  return None
209
  except Exception as e:
210
  logger.warning(f"Mistral falhou: {e}")
@@ -218,7 +292,9 @@ AKIRA (responda {modo_config['desc'].lower()}, máximo {modo_config['max_chars']
218
  "contents": [{"parts": [{"text": prompt}]}],
219
  "generationConfig": {
220
  "maxOutputTokens": config.MAX_TOKENS,
221
- "temperature": config.TEMPERATURE
 
 
222
  }
223
  }
224
  resp = requests.post(url, json=payload, timeout=self.timeout)
@@ -226,7 +302,7 @@ AKIRA (responda {modo_config['desc'].lower()}, máximo {modo_config['max_chars']
226
  if resp.status_code == 200:
227
  return resp.json()["candidates"][0]["content"]["parts"][0]["text"].strip()
228
  else:
229
- logger.warning(f"Gemini erro: {resp.status_code}")
230
  return None
231
  except Exception as e:
232
  logger.warning(f"Gemini falhou: {e}")
@@ -240,7 +316,9 @@ AKIRA (responda {modo_config['desc'].lower()}, máximo {modo_config['max_chars']
240
  "model": config.GROQ_MODEL,
241
  "messages": [{"role": "user", "content": prompt}],
242
  "max_tokens": config.MAX_TOKENS,
243
- "temperature": config.TEMPERATURE
 
 
244
  }
245
  resp = requests.post(
246
  "https://api.groq.com/openai/v1/chat/completions",
@@ -252,7 +330,7 @@ AKIRA (responda {modo_config['desc'].lower()}, máximo {modo_config['max_chars']
252
  if resp.status_code == 200:
253
  return resp.json()["choices"][0]["message"]["content"].strip()
254
  else:
255
- logger.warning(f"Groq erro: {resp.status_code}")
256
  return None
257
  except Exception as e:
258
  logger.warning(f"Groq falhou: {e}")
@@ -266,7 +344,9 @@ AKIRA (responda {modo_config['desc'].lower()}, máximo {modo_config['max_chars']
266
  "model": config.COHERE_MODEL,
267
  "message": prompt,
268
  "max_tokens": config.MAX_TOKENS,
269
- "temperature": config.TEMPERATURE
 
 
270
  }
271
  resp = requests.post(
272
  "https://api.cohere.ai/v1/chat",
@@ -278,7 +358,7 @@ AKIRA (responda {modo_config['desc'].lower()}, máximo {modo_config['max_chars']
278
  if resp.status_code == 200:
279
  return resp.json()["text"].strip()
280
  else:
281
- logger.warning(f"Cohere erro: {resp.status_code}")
282
  return None
283
  except Exception as e:
284
  logger.warning(f"Cohere falhou: {e}")
@@ -292,7 +372,9 @@ AKIRA (responda {modo_config['desc'].lower()}, máximo {modo_config['max_chars']
292
  "model": config.TOGETHER_MODEL,
293
  "messages": [{"role": "user", "content": prompt}],
294
  "max_tokens": config.MAX_TOKENS,
295
- "temperature": config.TEMPERATURE
 
 
296
  }
297
  resp = requests.post(
298
  "https://api.together.xyz/v1/chat/completions",
@@ -304,7 +386,7 @@ AKIRA (responda {modo_config['desc'].lower()}, máximo {modo_config['max_chars']
304
  if resp.status_code == 200:
305
  return resp.json()["choices"][0]["message"]["content"].strip()
306
  else:
307
- logger.warning(f"Together erro: {resp.status_code}")
308
  return None
309
  except Exception as e:
310
  logger.warning(f"Together falhou: {e}")
@@ -314,7 +396,16 @@ AKIRA (responda {modo_config['desc'].lower()}, máximo {modo_config['max_chars']
314
  """Chama HuggingFace Inference API"""
315
  try:
316
  headers = {"Authorization": f"Bearer {config.HF_API_KEY}"}
317
- payload = {"inputs": prompt, "parameters": {"max_new_tokens": config.MAX_TOKENS}}
 
 
 
 
 
 
 
 
 
318
  resp = requests.post(
319
  f"https://api-inference.huggingface.co/models/{config.HF_MODEL}",
320
  json=payload,
@@ -323,10 +414,11 @@ AKIRA (responda {modo_config['desc'].lower()}, máximo {modo_config['max_chars']
323
  )
324
  logger.debug(f"HF response: {resp.status_code}")
325
  if resp.status_code == 200:
326
- return resp.json()[0]["generated_text"].split("AKIRA:")[-1].strip()
327
- else:
328
- logger.warning(f"HF erro: {resp.status_code}")
329
- return None
 
330
  except Exception as e:
331
  logger.warning(f"HuggingFace falhou: {e}")
332
  return None
@@ -338,17 +430,13 @@ AKIRA (responda {modo_config['desc'].lower()}, máximo {modo_config['max_chars']
338
  mensagem: str,
339
  historico: list,
340
  mensagem_citada: str,
341
- humor: str,
342
- tom_usuario: str,
343
- modo_resposta: str,
344
  usuario: str,
345
- usar_nome: bool,
346
  tipo_conversa: str
347
  ) -> str:
348
  """Tenta gerar resposta usando fallback cascata"""
349
  prompt = self._construir_prompt(
350
- mensagem, historico, mensagem_citada, humor,
351
- tom_usuario, modo_resposta, usuario, usar_nome, tipo_conversa
352
  )
353
 
354
  max_loops = 2
@@ -363,6 +451,8 @@ AKIRA (responda {modo_config['desc'].lower()}, máximo {modo_config['max_chars']
363
  logger.info(f"Tentando {api_name.upper()} (retry {retry+1}/2)...")
364
 
365
  try:
 
 
366
  if api_name == "mistral":
367
  resposta = self._chamar_mistral(prompt)
368
  elif api_name == "gemini":
@@ -377,8 +467,9 @@ AKIRA (responda {modo_config['desc'].lower()}, máximo {modo_config['max_chars']
377
  resposta = self._chamar_huggingface(prompt)
378
 
379
  if resposta:
 
380
  logger.success(f"✓ Resposta gerada via {api_name.upper()}")
381
- return self._limpar_resposta(resposta)
382
 
383
  time.sleep(1)
384
 
@@ -387,30 +478,75 @@ AKIRA (responda {modo_config['desc'].lower()}, máximo {modo_config['max_chars']
387
 
388
  time.sleep(2)
389
 
390
- return "Barra no bardeado"
 
 
 
 
 
 
 
 
391
 
392
  def _limpar_resposta(self, resposta: str) -> str:
393
- """Remove markdown e limita tamanho"""
394
- # Remove markdown
 
 
 
395
  resposta = resposta.replace("**", "").replace("*", "")
396
  resposta = resposta.replace("```", "").replace("`", "")
 
397
 
398
- # Remove prefixos
399
- prefixos = ["AKIRA:", "Akira:", "RESPOSTA:", "Resposta:", "Assistant:"]
400
  for p in prefixos:
401
  if resposta.startswith(p):
402
  resposta = resposta[len(p):].strip()
403
 
404
- # Limita tamanho
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
405
  if len(resposta) > 400:
406
  resposta = resposta[:397] + "..."
407
 
 
 
 
 
408
  return resposta.strip()
409
 
410
  # ============================================================================
411
- # CLASSE PRINCIPAL DA API (CONTINUA NA PARTE 2)
412
  # ============================================================================
413
- # CONTINUAÇÃO DO api.py (PARTE 2/2)
414
 
415
  class AkiraAPI:
416
  def __init__(self, cfg_module):
@@ -422,16 +558,74 @@ class AkiraAPI:
422
  self.web_search = get_web_search()
423
  self._setup_routes()
424
  self._setup_trainer()
 
 
425
 
426
  def _setup_trainer(self):
427
  """Inicializa treinamento forçado"""
428
  if getattr(self.config, 'START_PERIODIC_TRAINER', False):
429
  try:
430
- Treinamento(self.db, interval_hours=config.TRAINING_INTERVAL_HOURS).start_periodic_training()
 
431
  logger.info("✅ Treinamento periódico INICIADO")
432
  except Exception as e:
433
  logger.error(f"❌ Treinador falhou: {e}")
434
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
435
  def _setup_routes(self):
436
  """Configura rotas Flask"""
437
 
@@ -441,7 +635,7 @@ class AkiraAPI:
441
  resp = make_response()
442
  resp.headers['Access-Control-Allow-Origin'] = '*'
443
  resp.headers['Access-Control-Allow-Headers'] = 'Content-Type'
444
- resp.headers['Access-Control-Allow-Methods'] = 'POST, GET'
445
  return resp
446
 
447
  @self.api.after_request
@@ -454,18 +648,33 @@ class AkiraAPI:
454
  try:
455
  data = request.get_json() or {}
456
  usuario = data.get('usuario', 'anonimo')
457
- numero = data.get('numero', '').strip()
458
  mensagem = data.get('mensagem', '').strip()
459
  mensagem_citada = data.get('mensagem_citada', '').strip()
 
 
 
460
 
 
 
 
 
 
461
  if not mensagem and not mensagem_citada:
462
  return jsonify({'error': 'mensagem obrigatória'}), 400
463
 
464
- logger.info(f"[{usuario}] ({numero}): {mensagem[:60]}")
 
 
465
 
466
- # === DETECTA TIPO DE CONVERSA ===
467
- tipo_conversa = "grupo" if ("@g.us" in numero or "120363" in numero) else "pv"
468
- logger.info(f"Tipo de conversa: {tipo_conversa}")
 
 
 
 
 
469
 
470
  # === RESPOSTA RÁPIDA PARA HORA ===
471
  if any(k in mensagem.lower() for k in ["hora", "horas", "que horas"]):
@@ -474,59 +683,55 @@ class AkiraAPI:
474
  return jsonify({'resposta': f"São {agora.strftime('%H:%M')} em Luanda, puto."})
475
 
476
  # === BUSCA WEB (SE NECESSÁRIO) ===
477
- intencao_busca = WebSearch.detectar_intencao_busca(mensagem)
478
  contexto_web = ""
479
-
480
- if intencao_busca == "noticias":
481
- logger.info("Buscando notícias de Angola...")
482
- contexto_web = self.web_search.pesquisar_noticias_angola()
483
- elif intencao_busca == "clima":
484
- logger.info("Buscando clima...")
485
- cidade = "Luanda"
486
- for palavra in mensagem.split():
487
- if len(palavra) > 4 and palavra[0].isupper():
488
- cidade = palavra
489
- break
490
- contexto_web = self.web_search.buscar_clima(cidade)
491
- elif intencao_busca == "busca_geral":
492
- logger.info("Buscando informações gerais...")
493
- contexto_web = self.web_search.buscar_geral(mensagem)
 
 
494
 
495
  # === CONTEXTO DO USUÁRIO (ISOLADO) ===
496
- contexto = self._get_user_context(numero)
497
  historico = contexto.obter_historico_para_llm()
498
 
499
- # === ANÁLISE COMPLETA (TOM, HUMOR, MODO) ===
500
- analise = contexto.analisar_intencao_e_normalizar(mensagem, historico)
 
 
501
 
502
- tom_usuario = analise.get("tom_usuario", "neutro")
503
- humor_atual = analise.get("humor_atualizado", "normal")
504
- modo_resposta = analise.get("modo_resposta", "casual_amigavel")
505
- usar_nome = analise.get("usar_nome", False)
506
 
507
- logger.info(f"Análise: tom={tom_usuario}, humor={humor_atual}, modo={modo_resposta}, usar_nome={usar_nome}")
 
 
508
 
509
- # === USUÁRIOS PRIVILEGIADOS ===
510
- if numero in config.USUARIOS_PRIVILEGIADOS:
511
- # Começa formal mas pode adaptar
512
- if humor_atual == "normal" and tom_usuario == "neutro":
513
- tom_usuario = "formal"
514
- logger.info(f"Usuário privilegiado detectado: {config.USUARIOS_PRIVILEGIADOS[numero]}")
 
515
 
516
- # === DETECTA SE REPLY É À AKIRA ===
517
- reply_to_bot = False
518
- if mensagem_citada:
519
- # Verifica se no histórico recente a Akira disse algo similar
520
- historico_recente = historico[-5:] # Últimas 5
521
- for msg in historico_recente:
522
- if msg.get("role") == "assistant":
523
- conteudo_bot = msg.get("content", "")
524
- # Se mensagem citada está contida na resposta da Akira
525
- if mensagem_citada[:30].lower() in conteudo_bot.lower():
526
- reply_to_bot = True
527
- mensagem_citada = f"[Respondendo à Akira: '{mensagem_citada[:50]}...']"
528
- logger.info("✅ Reply à Akira detectado!")
529
- break
530
 
531
  # === ADICIONA CONTEXTO WEB AO HISTÓRICO ===
532
  historico_com_web = historico.copy()
@@ -541,24 +746,22 @@ class AkiraAPI:
541
  mensagem=mensagem,
542
  historico=historico_com_web,
543
  mensagem_citada=mensagem_citada,
544
- humor=humor_atual,
545
- tom_usuario=tom_usuario,
546
- modo_resposta=modo_resposta,
547
  usuario=usuario,
548
- usar_nome=usar_nome,
549
  tipo_conversa=tipo_conversa
550
  )
551
 
552
  logger.success(f"✅ Resposta: {resposta[:100]}...")
553
 
554
- # === SALVAR NO BANCO + CONTEXTO (ISOLADO) ===
 
555
  contexto.atualizar_contexto(
556
  mensagem=mensagem,
557
  resposta=resposta,
558
  numero=numero,
559
  is_reply=bool(mensagem_citada),
560
  mensagem_original=mensagem_citada,
561
- reply_to_bot=reply_to_bot
562
  )
563
 
564
  # === REGISTRAR PARA TREINAMENTO ===
@@ -572,59 +775,16 @@ class AkiraAPI:
572
  is_reply=bool(mensagem_citada),
573
  mensagem_original=mensagem_citada,
574
  contexto={
575
- "humor": humor_atual,
576
- "modo": modo_resposta,
577
- "tom": tom_usuario
578
- }
 
 
 
 
 
579
  )
580
  except Exception as e:
581
  logger.warning(f"Erro ao treinar: {e}")
582
-
583
- return jsonify({'resposta': resposta})
584
-
585
- except Exception as e:
586
- logger.exception("Erro em /akira")
587
- return jsonify({'resposta': 'Erro interno, já volto!'}), 500
588
-
589
- @self.api.route('/health', methods=['GET'])
590
- def health_check():
591
- return 'OK', 200
592
-
593
- @self.api.route('/stats', methods=['GET'])
594
- def stats():
595
- """Endpoint de estatísticas"""
596
- try:
597
- # Conta mensagens totais
598
- total_msgs = self.db._execute_with_retry("SELECT COUNT(*) FROM mensagens")
599
- total = total_msgs[0][0] if total_msgs else 0
600
-
601
- # Conta por tipo
602
- tipos = self.db._execute_with_retry(
603
- "SELECT tipo_contexto, COUNT(*) FROM mensagens GROUP BY tipo_contexto"
604
- )
605
-
606
- stats_data = {
607
- "total_mensagens": total,
608
- "por_tipo": {t[0]: t[1] for t in (tipos or [])},
609
- "humores_disponiveis": config.HUMORES_BASE,
610
- "modos_resposta": list(config.MODOS_RESPOSTA.keys()),
611
- "apis_ativas": self.llm_manager.apis_disponiveis
612
- }
613
-
614
- return jsonify(stats_data)
615
- except Exception as e:
616
- logger.error(f"Erro em /stats: {e}")
617
- return jsonify({"error": str(e)}), 500
618
-
619
- def _get_user_context(self, numero: str) -> Contexto:
620
- """Retorna contexto do usuário (com cache)"""
621
- if not numero:
622
- numero = "anonimo"
623
-
624
- # Cache isolado por número
625
- cache_key = numero
626
-
627
- if cache_key not in self.contexto_cache:
628
- self.contexto_cache[cache_key] = Contexto(self.db, usuario=numero)
629
-
630
- return self.contexto_cache[cache_key]
 
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 robusta
9
+ ✅ Detecção automática de PV/Grupo via WhatsApp
10
+ ✅ Rota /reset exclusiva para root
11
+ ✅ Fuso horário corrigido (+1h)
12
  """
13
  import time
14
  import datetime
 
16
  import os
17
  import json
18
  import random
19
+ import re
20
+ from typing import Dict, Any
21
  from flask import Blueprint, request, jsonify, make_response
22
  from loguru import logger
23
  from .contexto import Contexto
 
53
  raise KeyError(key)
54
  return self._store[key][0]
55
 
56
+ def get(self, key, default=None):
57
+ try:
58
+ return self[key]
59
+ except KeyError:
60
+ return default
61
+
62
  # ============================================================================
63
+ # GERENCIADOR MULTI-API (ATUALIZADO)
64
  # ============================================================================
65
 
66
  class MultiAPIManager:
 
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(
 
105
  mensagem: str,
106
  historico: list,
107
  mensagem_citada: str,
108
+ analise: Dict[str, Any],
 
 
109
  usuario: str,
 
110
  tipo_conversa: str
111
  ) -> str:
112
+ """Constrói prompt otimizado com todas as regras"""
113
 
114
  # === DATA E HORA ATUAL (CORRIGIDA +1H) ===
115
  from datetime import datetime, timedelta
 
132
  msg_lower = mensagem.lower()
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
  # === CONTEXTO DE REPLY ===
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 do usuário."
149
 
150
  # === HISTÓRICO FORMATADO ===
151
  historico_texto = ""
 
157
  historico_texto += f"{role.upper()}: {content}\n"
158
 
159
  # === CONFIGURAÇÃO DO MODO DE RESPOSTA ===
160
+ modo_resposta = analise.get("modo_resposta", "normal_ironico")
161
  modo_config = config.MODOS_RESPOSTA.get(modo_resposta, config.MODOS_RESPOSTA["normal_ironico"])
162
 
163
  regras_modo = f"""
164
  MODO ATIVO: {modo_resposta}
165
  - Descrição: {modo_config['desc']}
166
  - Exemplo: {modo_config['exemplo']}
167
+ - Usar gírias: {'SIM' if modo_config['usa_girias'] else 'NÃO'} (probabilidade: {config.GIRIA_PROBABILIDADE*100}%)
168
+ - Usar emojis: {'SIM' if modo_config['usa_emojis'] else 'NÃO'} (probabilidade: {modo_config['prob_emoji']*100}%)
169
+ - Tonalidade: {modo_config['tonalidade']}
170
  - Tamanho máximo: {modo_config['max_chars']} caracteres
171
  """
172
 
173
+ # === INFORMAÇÕES DO USUÁRIO ===
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
+ pode_dar_ordens = user_data.get('pode_dar_ordens', False)
185
+
186
+ # Probabilidade de usar nome
187
+ usar_nome = analise.get("usar_nome", False)
188
+ usar_nome_str = f"SIM (use '{nome_usuario}')" if usar_nome else f"NÃO (probabilidade {int(config.USAR_NOME_PROBABILIDADE*100)}% não ativada)"
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=humor_atual,
206
+ humor_desc=humor_desc,
207
+ tom_usuario=tom_usuario,
208
+ modo_resposta=modo_resposta
209
+ )}
210
 
211
  {config.SYSTEM_PROMPT.format(
212
+ humor=humor_atual,
213
+ humor_desc=humor_desc,
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",
 
220
  max_chars=modo_config['max_chars'],
221
  usa_girias='SIM' if modo_config['usa_girias'] else 'NÃO',
222
  usa_emojis='SIM' if modo_config['usa_emojis'] else 'NÃO',
223
+ prob_emoji=int(modo_config['prob_emoji']*100),
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
  {config.REGRAS_ADAPTATIVAS}
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
  AKIRA (responda {modo_config['desc'].lower()}, máximo {modo_config['max_chars']} caracteres):"""
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 _chamar_mistral(self, prompt: str) -> str:
258
  """Chama Mistral AI"""
 
262
  "model": config.MISTRAL_MODEL,
263
  "messages": [{"role": "user", "content": prompt}],
264
  "max_tokens": config.MAX_TOKENS,
265
+ "temperature": config.TEMPERATURE,
266
+ "top_p": config.TOP_P,
267
+ "frequency_penalty": config.FREQUENCY_PENALTY,
268
+ "presence_penalty": config.PRESENCE_PENALTY,
269
+ "stop": config.STOP_SEQUENCES
270
  }
271
  resp = requests.post(
272
  "https://api.mistral.ai/v1/chat/completions",
 
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}")
 
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)
 
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}")
 
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",
 
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}")
 
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",
 
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}")
 
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",
 
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}")
 
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,
 
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
 
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
 
451
  logger.info(f"Tentando {api_name.upper()} (retry {retry+1}/2)...")
452
 
453
  try:
454
+ resposta = None
455
+
456
  if api_name == "mistral":
457
  resposta = self._chamar_mistral(prompt)
458
  elif api_name == "gemini":
 
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
 
 
478
 
479
  time.sleep(2)
480
 
481
+ # Fallback final
482
+ fallbacks = [
483
+ "Barra no bardeado",
484
+ "Tás a falar com a parede?",
485
+ "Nem ligo, puto.",
486
+ "Ah, tá...",
487
+ "Foda-se."
488
+ ]
489
+ return random.choice(fallbacks)
490
 
491
  def _limpar_resposta(self, resposta: str) -> str:
492
+ """Remove markdown, limita tamanho e aplica regras de estilo"""
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):
 
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("✅ Treinamento periódico INICIADO")
571
  except Exception as e:
572
  logger.error(f"❌ Treinador falhou: {e}")
573
 
574
+ def _get_user_context(self, numero: str, tipo_conversa: str, grupo_nome: str = '', grupo_id: str = ''):
575
+ """Obtém contexto isolado por tipo de conversa"""
576
+ # Cria chave única para isolamento
577
+ if tipo_conversa == 'grupo' and grupo_id:
578
+ cache_key = f"grupo_{grupo_id}"
579
+ else:
580
+ cache_key = f"pv_{numero}"
581
+
582
+ if cache_key in self.contexto_cache:
583
+ return self.contexto_cache[cache_key]
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
 
 
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, GET, DELETE'
639
  return resp
640
 
641
  @self.api.after_request
 
648
  try:
649
  data = request.get_json() or {}
650
  usuario = data.get('usuario', 'anonimo')
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') # 'pv' ou 'grupo'
655
+ grupo_nome = data.get('grupo_nome', '')
656
+ grupo_id = data.get('grupo_id', '')
657
 
658
+ # Log inicial
659
+ logger.info(f"[{usuario}] ({numero}): {mensagem[:60]}...")
660
+ logger.info(f"Tipo de conversa: {tipo_conversa} {f'({grupo_nome})' if grupo_nome else ''}")
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
+ # === DETECTA TIPO DE CONVERSA (AGORA VIA WHATSAPP) ===
671
+ # O WhatsApp envia 'tipo_conversa', mas valida
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"]):
 
683
  return jsonify({'resposta': f"São {agora.strftime('%H:%M')} em Luanda, puto."})
684
 
685
  # === BUSCA WEB (SE NECESSÁRIO) ===
 
686
  contexto_web = ""
687
+ if tipo_conversa == "pv": # Apenas em PV para evitar spam
688
+ intencao_busca = WebSearch.detectar_intencao_busca(mensagem)
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 intencao_busca == "clima":
694
+ logger.info("Buscando clima...")
695
+ cidade = "Luanda"
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
  # === CONTEXTO DO USUÁRIO (ISOLADO) ===
706
+ contexto = self._get_user_context(numero, tipo_conversa, grupo_nome, grupo_id)
707
  historico = contexto.obter_historico_para_llm()
708
 
709
+ # === VERIFICA SE É USUÁRIO PRIVILEGIADO ===
710
+ usuario_privilegiado = self.db.is_usuario_privilegiado(numero)
711
+ if usuario_privilegiado:
712
+ logger.info(f"👑 Usuário privilegiado detectado: {numero}")
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
+ # === AJUSTE PARA USUÁRIOS PRIVILEGIADOS ===
722
+ if usuario_privilegiado:
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()
 
746
  mensagem=mensagem,
747
  historico=historico_com_web,
748
  mensagem_citada=mensagem_citada,
749
+ analise=analise,
 
 
750
  usuario=usuario,
 
751
  tipo_conversa=tipo_conversa
752
  )
753
 
754
  logger.success(f"✅ Resposta: {resposta[:100]}...")
755
 
756
+ # === SALVAR NO BANCO + CONTEXTO ===
757
+ reply_info = analise.get('reply_info', {})
758
  contexto.atualizar_contexto(
759
  mensagem=mensagem,
760
  resposta=resposta,
761
  numero=numero,
762
  is_reply=bool(mensagem_citada),
763
  mensagem_original=mensagem_citada,
764
+ reply_to_bot=reply_info.get('reply_to_bot', False)
765
  )
766
 
767
  # === REGISTRAR PARA TREINAMENTO ===
 
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 treinar: {e}")
790
+