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

Update modules/api.py

Browse files
Files changed (1) hide show
  1. 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
- 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
@@ -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 (ATUALIZADO)
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
- """Constrói prompt otimizado com todas as regras"""
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
- # Traduz mês para português
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
- # === INFORMAÇÕES DA EMPRESA (SE RELEVANTE) ===
130
  empresa_info = EmpresaInfo()
131
  info_context = ""
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 = ""
152
  if historico:
153
- ultimas = historico[-8:] # Últimas 8 mensagens
154
- for msg in ultimas:
155
- role = msg.get("role", "user")
156
  content = msg.get("content", "")
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",
219
  regras_modo=regras_modo,
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"""
259
- try:
260
- headers = {"Authorization": f"Bearer {config.MISTRAL_API_KEY}"}
261
- payload = {
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",
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
- resposta = None
455
-
456
- if api_name == "mistral":
457
- resposta = self._chamar_mistral(prompt)
458
- elif api_name == "gemini":
459
- resposta = self._chamar_gemini(prompt)
460
- elif api_name == "groq":
461
- resposta = self._chamar_groq(prompt)
462
- elif api_name == "cohere":
463
- resposta = self._chamar_cohere(prompt)
464
- elif api_name == "together":
465
- resposta = self._chamar_together(prompt)
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
- 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):
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("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
-
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, GET, DELETE'
639
  return resp
640
 
641
  @self.api.after_request
642
- def add_cors(response):
643
- response.headers['Access-Control-Allow-Origin'] = '*'
644
- return response
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', '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"]):
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
- # === 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()
738
  if contexto_web:
739
- historico_com_web.append({
740
- "role": "system",
741
- "content": f"CONTEXTO ADICIONAL (busca web):\n{contexto_web}"
742
- })
743
-
744
- # === GERAR RESPOSTA VIA MULTI-API ===
745
  resposta = self.llm_manager.gerar_resposta(
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,
@@ -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
- mensagem=mensagem,
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 treinar: {e}")
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()