akra35567 commited on
Commit
562db7c
·
1 Parent(s): 3e80258

Update modules/api.py

Browse files
Files changed (1) hide show
  1. modules/api.py +119 -267
modules/api.py CHANGED
@@ -1,16 +1,17 @@
1
  """
2
- API wrapper Akira IA.
3
  Prioridade: Mistral API (Phi-3 Mini) → Gemini → Fallback
4
- - Contexto por NÚMERO (JID) para evitar vazamento.
5
- - WebSearch ATIVO para perguntas de tempo real.
6
- - Resposta rápida para Data/Hora.
7
- - Gemini SEM FILTROS.
 
8
  """
9
  import time
10
  import re
11
  import datetime
12
- from typing import Dict, Optional, Any, List
13
- from flask import Flask, Blueprint, request, jsonify
14
  from loguru import logger
15
 
16
  # LLM PROVIDERS
@@ -18,7 +19,6 @@ import google.generativeai as genai
18
  from mistralai import Mistral
19
 
20
  # LOCAL MODULES
21
- # from .local_llm import HermesLLM # ← REMOVIDO: Era o modelo que causava a carga de 101% CPU
22
  from .contexto import Contexto
23
  from .database import Database
24
  from .treinamento import Treinamento
@@ -51,47 +51,40 @@ class SimpleTTLCache:
51
  return self._store[key][0]
52
 
53
 
54
- # --- GERENCIADOR DE LLMs COM PRIORIDADE PARA API LEVE (PHI-3 MINI) ---
55
  class LLMManager:
56
  def __init__(self, config_instance):
57
  self.config = config_instance
58
- self.mistral_client: Optional[Mistral] = None
59
- self.gemini_model: Optional[genai.GenerativeModel] = None
60
- # self.hermes_available = False # REMOVIDO
61
-
62
  self._setup_providers()
63
  self.providers = []
64
-
65
- # PRIORIDADE MÁXIMA AGORA É O MISTRAL (PHI-3 MINI)
66
  if self.mistral_client:
67
- self.providers.append('mistral') # Mistral (usando Phi-3) é o novo principal
68
  if self.gemini_model:
69
  self.providers.append('gemini')
70
-
71
- logger.info(f"Provedores ativos (ordem): {self.providers or 'NENHUM'}")
72
- logger.info("Modelo GGUF local removido para evitar picos de CPU.")
73
 
74
  def _setup_providers(self):
75
- # MISTRAL API
76
  key = getattr(self.config, 'MISTRAL_API_KEY', '').strip()
77
- if key and key.startswith('m-'): # Verifica se a chave é válida
78
  try:
79
  self.mistral_client = Mistral(api_key=key)
80
- logger.info("Mistral API conectado com sucesso")
81
  except Exception as e:
82
- logger.warning(f"Mistral API falhou: {e}")
83
  else:
84
- logger.warning("Mistral API desativada (chave inválida).")
85
-
86
 
87
- # GEMINI (SEM CENSURA TOTAL)
88
  key = getattr(self.config, 'GEMINI_API_KEY', '').strip()
89
- if key and key.startswith('AIza'): # Verifica se a chave é válida
90
  try:
91
  genai.configure(api_key=key)
92
  self.gemini_model = genai.GenerativeModel(
93
  model_name=self.config.GEMINI_MODEL,
94
- system_instruction=self.config.PERSONA + self.config.SYSTEM_PROMPT + " (SEM CENSURA TOTAL, FALA COMO ANGOLANA)",
95
  safety_settings=[{"category": c, "threshold": "BLOCK_NONE"} for c in [
96
  "HARM_CATEGORY_DANGEROUS_CONTENT",
97
  "HARM_CATEGORY_HARASSMENT",
@@ -103,75 +96,45 @@ class LLMManager:
103
  except Exception as e:
104
  logger.warning(f"Gemini falhou: {e}")
105
  else:
106
- logger.warning("Gemini API desativada (chave inválida).")
107
 
108
  def generate(self, user_prompt: str, context_history: List[dict] = [], is_privileged: bool = False) -> str:
109
-
110
- # Tokens para APIs (agora a única opção, pode ser o MAX_TOKENS original)
111
  api_max_tokens = getattr(self.config, 'MAX_TOKENS', 500)
112
-
113
- temperature = getattr(self.config, 'TOP_P', 0.8)
114
 
115
- full_system = self.config.PERSONA + self.config.SYSTEM_PROMPT + " (SEM FILTROS, RESPOSTA DIRETA, SOTAQUE DE LUANDA)"
116
-
117
- # CORREÇÃO: Usa dict (compatível com Mistral 1.0.3+)
118
  messages = [{"role": "system", "content": full_system}]
119
  for turn in context_history:
120
  role = "user" if turn["role"] == "user" else "assistant"
121
  messages.append({"role": role, "content": turn["content"]})
122
-
123
- # Extrai a mensagem limpa do prompt (necessário para APIs)
124
- # O prompt completo é formatado em _build_prompt, mas as APIs usam o formato de messages.
125
- # Precisamos extrair apenas a última mensagem do usuário do prompt longo para garantir que
126
- # o histórico (que já está em context_history) não seja duplicado.
127
- user_message_clean_match = re.search(r'(### Mensagem Atual ###|### USUÁRIO RESPONDEU A ESSA MENSAGEM: ###)\n(.*?)\n\n(Akira:|$)', user_prompt, re.DOTALL)
128
- if user_message_clean_match:
129
- # Captura o grupo 2 (o conteúdo da mensagem)
130
- user_message_clean = user_message_clean_match.group(2).strip()
131
- else:
132
- # Fallback (caso o formato do prompt mude)
133
- user_message_clean = user_prompt
134
-
135
- messages.append({"role": "user", "content": user_message_clean})
136
-
137
- # O prompt formatado para Llama.cpp (GGUF) foi removido
138
- llama_full_prompt = user_prompt
139
 
 
 
 
 
140
 
141
  for provider in self.providers:
142
- # 1. MISTRAL API (AGORA PRIORIDADE MÁXIMA)
143
  if provider == 'mistral' and self.mistral_client:
144
  try:
145
- # FIX CRÍTICO: Usando Phi-3 Mini para ser leve e rápido
146
- model_to_use = "phi-3-mini-4k-instruct"
147
-
148
- logger.info(f"[MISTRAL] Gerando com {model_to_use} e max_tokens={api_max_tokens} (Novo Modelo Leve)")
149
  resp = self.mistral_client.chat(
150
- model=model_to_use, # ← MUDANÇA AQUI PARA O MODELO LEVE
151
- messages=messages, # Usa a lista de dicts
152
  temperature=temperature,
153
  max_tokens=api_max_tokens
154
  )
155
- text = resp.choices[0].message.content
156
  if text:
157
- logger.info(f"Mistral API respondeu com {model_to_use}!")
158
- return text.strip()
159
  except Exception as e:
160
- logger.warning(f"Mistral API falhou: {e}")
161
 
162
- # 2. GEMINI
163
  elif provider == 'gemini' and self.gemini_model:
164
  try:
165
- logger.info(f"[GEMINI] Gerando com max_tokens={api_max_tokens}")
166
- if getattr(self.config, 'GEMINI_API_KEY', '').startswith('AIza'):
167
- genai.configure(api_key=self.config.GEMINI_API_KEY)
168
-
169
- # Cria o histórico no formato esperado pelo Gemini (list of Content)
170
  gemini_hist = []
171
  for msg in messages[1:]:
172
  role = "user" if msg["role"] == "user" else "model"
173
  gemini_hist.append({"role": role, "parts": [{"text": msg["content"]}]})
174
-
175
  resp = self.gemini_model.generate_content(
176
  gemini_hist,
177
  generation_config=genai.GenerationConfig(
@@ -179,52 +142,45 @@ class LLMManager:
179
  temperature=temperature
180
  )
181
  )
182
-
183
- text = None
184
  if resp.candidates and resp.candidates[0].content.parts:
185
- text = resp.candidates[0].content.parts[0].text
186
-
187
- if text:
188
- logger.info("Gemini respondeu (último fallback)")
189
- return text.strip()
190
- else:
191
- reason = resp.candidates[0].finish_reason if resp.candidates else "N/A"
192
- safety = resp.candidates[0].safety_ratings if resp.candidates else "N/A"
193
- logger.warning(f"Gemini API gerou resposta vazia (Finish Reason: {reason}, Safety: {safety}).")
194
-
195
  except Exception as e:
196
- logger.warning(f"Gemini falhou: {e}")
197
 
198
- fallback = getattr(self.config, 'FALLBACK_RESPONSE', 'Desculpa puto, tô off hoje, mas já volto!')
199
- logger.warning(f"TODOS LLMs FALHARAM → Fallback: {fallback}")
200
  return fallback
201
 
202
 
203
- # --- API PRINCIPAL COM TODAS AS RETIFICAÇÕES ---
204
  class AkiraAPI:
205
  def __init__(self, cfg_module):
206
  self.config = cfg_module
207
- self.app = Flask(__name__)
208
  self.api = Blueprint("akira_api", __name__)
209
  self.contexto_cache = SimpleTTLCache(ttl_seconds=getattr(self.config, 'MEMORIA_MAX', 300))
210
  self.providers = LLMManager(self.config)
211
  self.exemplos = ExemplosNaturais()
212
  self.logger = logger
213
- self.db = Database(getattr(self.config, 'DB_PATH', 'akira.db')) # Adiciona o DB
214
-
215
- # CORREÇÃO: Inicializa o WebSearch (necessário para o _build_prompt)
216
  try:
217
  from .web_search import WebSearch
218
  self.web_search = WebSearch()
219
- logger.info("WebSearch (Notícias Angola) inicializado.")
220
  except ImportError:
221
  self.web_search = None
222
- logger.warning("WebSearch não encontrado. Notícias de Angola desativadas.")
223
 
224
  self._setup_personality()
225
  self._setup_routes()
226
  self._setup_trainer()
227
- self.app.register_blueprint(self.api, url_prefix="/api")
 
 
228
 
229
  def _setup_personality(self):
230
  self.humor = getattr(self.config, 'HUMOR_INICIAL', 'neutra')
@@ -232,254 +188,150 @@ class AkiraAPI:
232
  self.limites = list(getattr(self.config, 'LIMITES', []))
233
 
234
  def _setup_trainer(self):
235
- """
236
- A API só precisa inicializar a classe Treinamento.
237
- """
238
  if getattr(self.config, 'START_PERIODIC_TRAINER', False):
239
  try:
240
  trainer = Treinamento(self.db, interval_hours=getattr(self.config, 'TRAINING_INTERVAL_HOURS', 24))
241
-
242
- # CORREÇÃO: Verifica se o método existe antes de chamar
243
  if hasattr(trainer, 'start_periodic_training'):
244
  trainer.start_periodic_training()
245
- self.logger.info("Treinamento periódico (start_periodic_training) iniciado com sucesso.")
246
- else:
247
- self.logger.info("Treinamento periódico (via __init__) iniciado.")
248
-
249
  except Exception as e:
250
- self.logger.exception(f"Treinador periódico falhou ao iniciar: {e}")
251
 
252
  def _setup_routes(self):
253
- """
254
- Configura as rotas da API, incluindo o tratamento de CORS.
255
- """
256
- # --- CORREÇÃO: Adiciona suporte manual a CORS ---
257
- # 1. CORS Preflight Handler (Responde a requests OPTIONS)
258
  @self.api.before_request
259
  def handle_options():
260
  if request.method == 'OPTIONS':
261
- response = self.app.make_response('')
262
- response.headers.add('Access-Control-Allow-Origin', '*')
263
- response.headers.add('Access-Control-Allow-Headers', 'Content-Type, Authorization')
264
- response.headers.add('Access-Control-Allow-Methods', 'POST, GET, OPTIONS')
265
- return response
266
 
267
- # 2. CORS Post-Request Header Addition (Adiciona headers em toda resposta)
268
  @self.api.after_request
269
- def add_cors_headers(response):
270
- response.headers.add('Access-Control-Allow-Origin', '*')
271
- response.headers.add('Access-Control-Allow-Headers', 'Content-Type, Authorization')
272
- response.headers.add('Access-Control-Allow-Methods', 'POST, GET, OPTIONS')
273
  return response
274
- # ------------------------------------------------
275
 
 
276
  @self.api.route('/akira', methods=['POST'])
277
  def akira_endpoint():
278
  try:
279
  data = request.get_json(force=True, silent=True) or {}
280
  usuario = data.get('usuario', 'anonimo')
281
- numero = data.get('numero', '') # Este é o JID completo (ex: 244...@s.whatsapp.net)
282
  mensagem = data.get('mensagem', '').strip()
283
  mensagem_citada = data.get('mensagem_citada', '').strip()
284
  is_reply = bool(mensagem_citada)
285
- mensagem_original = mensagem_citada if is_reply else mensagem # Usado para registro
286
 
287
  if not mensagem and not mensagem_citada:
288
  return jsonify({'error': 'mensagem obrigatória'}), 400
289
 
290
- self.logger.info(f"{usuario} ({numero}): {mensagem[:80]}{' (REPLY)' if is_reply else ''}")
291
-
292
- # --- CORREÇÃO: Resposta rápida para "Que dia é hoje?" ---
293
- prompt_lower = mensagem.lower().strip()
294
- if any(keyword in prompt_lower for keyword in ["que dia é hoje", "qual é a data", "dia da semana", "que horas"]):
295
- hoje = datetime.datetime.now()
296
- dia_semana = hoje.strftime("%A")
297
- dia_mes = hoje.day
298
- mes = hoje.strftime("%B")
299
- ano = hoje.year
300
- hora_minuto = hoje.strftime("%H:%M")
301
-
302
- if "que horas" in prompt_lower:
303
- resposta = f"São {hora_minuto} agora, meu."
304
- elif any(k in prompt_lower for k in ["que dia", "hoje é que dia", "dia da semana"]) and not any(k in prompt_lower for k in ["mês", "ano", "data", "completa"]):
305
- resposta = f"Hoje é {dia_semana.capitalize()}, {dia_mes}, meu."
306
  else:
307
- resposta = f"Hoje é {dia_semana.capitalize()}, {dia_mes} de {mes.capitalize()} de {ano}, meu."
308
-
309
- # Salva a interação (mesmo sendo resposta rápida)
310
  contexto = self._get_user_context(numero)
311
- contexto.atualizar_contexto(mensagem, resposta)
312
- try:
313
- trainer = Treinamento(self.db)
314
- trainer.registrar_interacao(usuario, mensagem, resposta, numero, is_reply, mensagem_original)
315
- except Exception as e:
316
- self.logger.warning(f"Registro de interação (rápida) falhou: {e}")
317
-
318
- return jsonify({'resposta': resposta})
319
- # --------------------------------------------------
320
-
321
- # CORREÇÃO: Usar o 'numero' (JID) como chave de contexto para evitar vazamento
322
  contexto = self._get_user_context(numero)
323
  analise = contexto.analisar_intencao_e_normalizar(mensagem, contexto.obter_historico())
324
-
325
  if usuario.lower() in ['isaac', 'isaac quarenta']:
326
  analise['usar_nome'] = False
327
 
328
- is_blocking = any(k in mensagem.lower() for k in ['exec', 'bash', 'open', 'api_key', 'key'])
329
  is_privileged = usuario.lower() in ['isaac', 'isaac quarenta'] or numero in getattr(self.config, 'PRIVILEGED_USERS', [])
330
 
331
- prompt = self._build_prompt(
332
- usuario, numero, mensagem, mensagem_citada, analise, contexto,
333
- is_blocking, is_privileged, is_reply
334
- )
335
-
336
  resposta = self._generate_response(prompt, contexto.obter_historico_para_llm(), is_privileged)
 
337
  contexto.atualizar_contexto(mensagem, resposta)
338
 
339
- # REGISTRO DE INTERAÇÃO
340
  try:
341
- # db = Database(getattr(self.config, 'DB_PATH', 'akira.db')) # DB já existe em self.db
342
  trainer = Treinamento(self.db)
343
- trainer.registrar_interacao(
344
- usuario=usuario,
345
- mensagem=mensagem,
346
- resposta=resposta,
347
- numero=numero,
348
- is_reply=is_reply,
349
- mensagem_original=mensagem_original
350
- )
351
- self.logger.info(f"Interação salva: {usuario} → {resposta[:50]}...")
352
  except Exception as e:
353
- self.logger.warning(f"Registro de interação falhou: {e}")
354
 
355
- response_data = {'resposta': resposta}
356
- try:
357
- aprendizados = contexto.obter_aprendizados()
358
- if aprendizados:
359
- response_data['aprendizados'] = aprendizados
360
- except Exception as e:
361
- self.logger.warning(f"Aprendizados falharam: {e}")
362
-
363
- return jsonify(response_data)
364
 
365
  except Exception as e:
366
- self.logger.exception('Erro crítico no /akira')
367
- return jsonify({'resposta': 'Erro interno, mas a Akira tá voltando!'}), 500
368
 
369
  @self.api.route('/health', methods=['GET'])
370
  def health_check():
371
  return 'OK', 200
372
 
373
  def _get_user_context(self, numero: str) -> Contexto:
374
- """CORREÇÃO: Usa o NÚMERO (JID) como chave de cache para evitar vazamento de contexto."""
375
- if not numero: # Fallback para usuário anônimo se o JID estiver vazio
376
  numero = "anonimo_contexto"
377
-
378
  if numero not in self.contexto_cache:
379
- # db = Database(getattr(self.config, 'DB_PATH', 'akira.db')) # DB já existe em self.db
380
  self.contexto_cache[numero] = Contexto(self.db, usuario=numero)
381
  return self.contexto_cache[numero]
382
 
383
- def _build_prompt(self, usuario: str, numero: str, mensagem: str, mensagem_citada: str,
384
- analise: Dict, contexto: Contexto, is_blocking: bool,
385
- is_privileged: bool = False, is_reply: bool = False) -> str:
386
- """
387
- Constrói o prompt completo para o LLM.
388
- """
389
  historico_raw = contexto.obter_historico()
390
  historico_texto = '\n'.join([f"Usuário: {m[0]}\nAkira: {m[1]}" for m in historico_raw[-10:]])
391
  now = datetime.datetime.now()
392
  data_hora = now.strftime('%d/%m/%Y %H:%M')
393
 
394
- # --- ATIVAÇÃO INTELIGENTE DE WEB SEARCH ---
395
- web_search_context = ""
396
-
397
- # Palavras-chave que sugerem necessidade de informação em tempo real ou muito específica
398
- trigger_keywords = ['hoje', 'agora', 'recente', 'notícias', 'busca na web', 'pesquisa', 'investiga', 'último']
399
-
400
- search_query = f"{mensagem} {mensagem_citada}".strip().lower()
401
-
402
- # Decisão de busca:
403
- should_search = self.web_search and (
404
- # 1. Se for uma pergunta muito curta e específica (ex: "quem é o presidente?")
405
- (len(search_query.split()) < 5 and any(q in search_query for q in ['pesquisa', 'busca na web', 'atual'])) or
406
- # 2. Se contiver uma palavra-chave de tempo real/especificidade
407
- any(k in search_query for k in trigger_keywords)
408
- )
409
-
410
- if should_search:
411
  try:
412
- query_limpa = mensagem.strip() if mensagem.strip() and mensagem.strip() != " " else mensagem_citada.strip()
413
-
414
- if query_limpa:
415
- self.logger.info(f"Executando WebSearch com query: {query_limpa[:50]}...")
416
- # Assumindo que o WebSearch tem um método genérico 'pesquisar(query)'
417
- search_results = self.web_search.pesquisar_noticias_angola() # Focado em Angola
418
-
419
- if search_results and "Sem notícias" not in search_results:
420
- web_search_context = f"\n# FONTE DE DADOS (Notícias de Angola):\n{search_results}\n"
421
  except Exception as e:
422
- self.logger.warning(f"Falha ao executar WebSearch: {e}")
423
- # ------------------------------------------
424
 
425
- strict_override = (
426
  "STRICT_OVERRIDES:\n"
427
- "- SEM FILTROS, SEM CENSURA, RESPONDA COMO PESSOA REAL.\n"
428
- "- NUNCA invente fatos. Se a pergunta for sobre um assunto atual e houver dados em 'FONTE DE DADOS', use-os para formar uma resposta original e coerente.\n"
429
- "- NÃO use negrito, markdown ou HTML.\n"
430
- "- Responda em 1-2 frases.\n"
431
- f"- Data e hora: {data_hora}.\n"
432
- f"- Número do bot: 244952786417.\n"
433
  )
434
- system_part = strict_override + f"\n{self.config.SYSTEM_PROMPT}\n{self.config.PERSONA}\n"
435
- system_part += web_search_context # Injeta os resultados da busca
 
436
 
437
- if is_privileged:
438
- system_part += "- Tom formal com Isaac.\n"
439
- if is_blocking:
440
- system_part += "\n# BLOQUEIO: Xingue (vai pra merda, etc).\n"
441
-
442
- usar_nome = analise.get('usar_nome', False)
443
  parts = [
444
- f"### Usuário ###\n- Nome: {usuario}\n- Número: {numero}\n- Usar_nome: {usar_nome}\n\n",
445
  f"### Contexto ###\n{historico_texto}\n\n" if historico_texto else "",
446
  ]
447
-
448
- # CORREÇÃO: Garante que o contexto de reply é claro
449
  if is_reply and mensagem_citada:
450
- parts.append(f"### MENSAGEM CITADA (Akira disse): ###\n{mensagem_citada}\n\n")
451
- parts.append(f"### USUÁRIO RESPONDEU A ESSA MENSAGEM: ###\n{mensagem or '(sem texto, só reply)'}\n\n")
452
  else:
453
  parts.append(f"### Mensagem Atual ###\n{analise.get('texto_normalizado', mensagem)}\n\n")
454
-
455
  parts.append("Akira:")
456
  user_part = ''.join(parts)
457
- return f"[SYSTEM]\n{system_part}\n[/SYSTEM]\n[USER]\n{user_part}\n[/USER]"
458
 
459
- def _generate_response(self, prompt: str, context_history: List[Dict], is_privileged: bool = False) -> str:
460
- """
461
- Gera a resposta. (Otimizado para extrair a mensagem do prompt para APIs).
462
- """
463
- try:
464
- max_tokens = getattr(self.config, 'MAX_TOKENS', 500)
465
- temperature = getattr(self.config, 'TOP_P', 0.8)
466
-
467
- # Extrai a mensagem limpa do prompt (necessário para APIs)
468
- # Usa o mesmo regex do LLMManager.generate para manter a consistência
469
- user_prompt_clean_match = re.search(r'(### Mensagem Atual ###|### USUÁRIO RESPONDEU A ESSA MENSAGEM: ###)\n(.*?)\n\n(Akira:|$)', prompt, re.DOTALL)
470
- if user_prompt_clean_match:
471
- user_prompt_clean = user_prompt_clean_match.group(2).strip()
472
- else:
473
- user_prompt_clean = prompt # Fallback
474
-
475
-
476
- text = self.providers.generate(
477
- user_prompt_clean,
478
- context_history,
479
- is_privileged
480
- )
481
- return text
482
 
 
 
 
 
 
483
  except Exception as e:
484
- self.logger.exception("Erro ao gerar resposta no _generate_response")
485
- return getattr(self.config, 'FALLBACK_RESPONSE', 'Desculpa puto, deu falha na comunicação, já volto!')
 
1
  """
2
+ API wrapper Akira IA — VERSÃO FINAL COMPATÍVEL COM main.py
3
  Prioridade: Mistral API (Phi-3 Mini) → Gemini → Fallback
4
+ - Contexto por JID
5
+ - WebSearch ativo
6
+ - Resposta rápida de hora/data
7
+ - Gemini SEM FILTROS
8
+ - CORS liberado
9
  """
10
  import time
11
  import re
12
  import datetime
13
+ from typing import Dict, List
14
+ from flask import Flask, Blueprint, request, jsonify, make_response
15
  from loguru import logger
16
 
17
  # LLM PROVIDERS
 
19
  from mistralai import Mistral
20
 
21
  # LOCAL MODULES
 
22
  from .contexto import Contexto
23
  from .database import Database
24
  from .treinamento import Treinamento
 
51
  return self._store[key][0]
52
 
53
 
54
+ # --- GERENCIADOR DE LLMs ---
55
  class LLMManager:
56
  def __init__(self, config_instance):
57
  self.config = config_instance
58
+ self.mistral_client = None
59
+ self.gemini_model = None
 
 
60
  self._setup_providers()
61
  self.providers = []
 
 
62
  if self.mistral_client:
63
+ self.providers.append('mistral')
64
  if self.gemini_model:
65
  self.providers.append('gemini')
66
+ logger.info(f"Provedores ativos: {self.providers or 'NENHUM'}")
 
 
67
 
68
  def _setup_providers(self):
69
+ # MISTRAL
70
  key = getattr(self.config, 'MISTRAL_API_KEY', '').strip()
71
+ if key and key.startswith('m-'):
72
  try:
73
  self.mistral_client = Mistral(api_key=key)
74
+ logger.info("Mistral API conectado")
75
  except Exception as e:
76
+ logger.warning(f"Mistral falhou: {e}")
77
  else:
78
+ logger.warning("Mistral API desativada (chave inválida)")
 
79
 
80
+ # GEMINI SEM FILTROS
81
  key = getattr(self.config, 'GEMINI_API_KEY', '').strip()
82
+ if key and key.startswith('AIza'):
83
  try:
84
  genai.configure(api_key=key)
85
  self.gemini_model = genai.GenerativeModel(
86
  model_name=self.config.GEMINI_MODEL,
87
+ system_instruction=self.config.PERSONA + self.config.SYSTEM_PROMPT + " (SEM CENSURA, FALA COMO ANGOLANA)",
88
  safety_settings=[{"category": c, "threshold": "BLOCK_NONE"} for c in [
89
  "HARM_CATEGORY_DANGEROUS_CONTENT",
90
  "HARM_CATEGORY_HARASSMENT",
 
96
  except Exception as e:
97
  logger.warning(f"Gemini falhou: {e}")
98
  else:
99
+ logger.warning("Gemini API desativada (chave inválida)")
100
 
101
  def generate(self, user_prompt: str, context_history: List[dict] = [], is_privileged: bool = False) -> str:
 
 
102
  api_max_tokens = getattr(self.config, 'MAX_TOKENS', 500)
103
+ temperature = getattr(self.config, 'TOP_P', 0.8)
104
+ full_system = self.config.PERSONA + self.config.SYSTEM_PROMPT + " (SEM FILTROS, SOTAQUE DE LUANDA)"
105
 
 
 
 
106
  messages = [{"role": "system", "content": full_system}]
107
  for turn in context_history:
108
  role = "user" if turn["role"] == "user" else "assistant"
109
  messages.append({"role": role, "content": turn["content"]})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
+ # Extrai só a última mensagem do usuário
112
+ match = re.search(r'(### Mensagem Atual ###|### USUÁRIO RESPONDEU A ESSA MENSAGEM: ###)\n(.*?)\n\n(Akira:|$)', user_prompt, re.DOTALL)
113
+ user_message_clean = match.group(2).strip() if match else user_prompt
114
+ messages.append({"role": "user", "content": user_message_clean})
115
 
116
  for provider in self.providers:
 
117
  if provider == 'mistral' and self.mistral_client:
118
  try:
 
 
 
 
119
  resp = self.mistral_client.chat(
120
+ model="phi-3-mini-4k-instruct",
121
+ messages=messages,
122
  temperature=temperature,
123
  max_tokens=api_max_tokens
124
  )
125
+ text = resp.choices[0].message.content.strip()
126
  if text:
127
+ logger.info("Mistral respondeu!")
128
+ return text
129
  except Exception as e:
130
+ logger.warning(f"Mistral error: {e}")
131
 
 
132
  elif provider == 'gemini' and self.gemini_model:
133
  try:
 
 
 
 
 
134
  gemini_hist = []
135
  for msg in messages[1:]:
136
  role = "user" if msg["role"] == "user" else "model"
137
  gemini_hist.append({"role": role, "parts": [{"text": msg["content"]}]})
 
138
  resp = self.gemini_model.generate_content(
139
  gemini_hist,
140
  generation_config=genai.GenerationConfig(
 
142
  temperature=temperature
143
  )
144
  )
 
 
145
  if resp.candidates and resp.candidates[0].content.parts:
146
+ text = resp.candidates[0].content.parts[0].text.strip()
147
+ logger.info("Gemini respondeu!")
148
+ return text
 
 
 
 
 
 
 
149
  except Exception as e:
150
+ logger.warning(f"Gemini error: {e}")
151
 
152
+ fallback = getattr(self.config, 'FALLBACK_RESPONSE', 'Desculpa puto, tô off agora, já volto!')
153
+ logger.warning(f"TODOS LLMs FALHARAM → {fallback}")
154
  return fallback
155
 
156
 
157
+ # --- API PRINCIPAL (AGORA 100% COMPATÍVEL COM main.py) ---
158
  class AkiraAPI:
159
  def __init__(self, cfg_module):
160
  self.config = cfg_module
161
+ self.app = Flask(__name__) # Esta app NÃO é usada diretamente
162
  self.api = Blueprint("akira_api", __name__)
163
  self.contexto_cache = SimpleTTLCache(ttl_seconds=getattr(self.config, 'MEMORIA_MAX', 300))
164
  self.providers = LLMManager(self.config)
165
  self.exemplos = ExemplosNaturais()
166
  self.logger = logger
167
+ self.db = Database(getattr(self.config, 'DB_PATH', 'akira.db'))
168
+
169
+ # WebSearch
170
  try:
171
  from .web_search import WebSearch
172
  self.web_search = WebSearch()
173
+ logger.info("WebSearch inicializado")
174
  except ImportError:
175
  self.web_search = None
176
+ logger.warning("WebSearch não encontrado")
177
 
178
  self._setup_personality()
179
  self._setup_routes()
180
  self._setup_trainer()
181
+
182
+ # Blueprint registrado no main.py com prefix /api
183
+ # NÃO faz register aqui → main.py faz!
184
 
185
  def _setup_personality(self):
186
  self.humor = getattr(self.config, 'HUMOR_INICIAL', 'neutra')
 
188
  self.limites = list(getattr(self.config, 'LIMITES', []))
189
 
190
  def _setup_trainer(self):
 
 
 
191
  if getattr(self.config, 'START_PERIODIC_TRAINER', False):
192
  try:
193
  trainer = Treinamento(self.db, interval_hours=getattr(self.config, 'TRAINING_INTERVAL_HOURS', 24))
 
 
194
  if hasattr(trainer, 'start_periodic_training'):
195
  trainer.start_periodic_training()
196
+ logger.info("Treinamento periódico iniciado")
 
 
 
197
  except Exception as e:
198
+ logger.exception(f"Treinador falhou: {e}")
199
 
200
  def _setup_routes(self):
201
+ # CORS MANUAL (funciona 100%)
 
 
 
 
202
  @self.api.before_request
203
  def handle_options():
204
  if request.method == 'OPTIONS':
205
+ resp = make_response()
206
+ resp.headers['Access-Control-Allow-Origin'] = '*'
207
+ resp.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
208
+ resp.headers['Access-Control-Allow-Methods'] = 'POST, GET, OPTIONS'
209
+ return resp
210
 
 
211
  @self.api.after_request
212
+ def add_cors(response):
213
+ response.headers['Access-Control-Allow-Origin'] = '*'
 
 
214
  return response
 
215
 
216
+ # ROTA PRINCIPAL
217
  @self.api.route('/akira', methods=['POST'])
218
  def akira_endpoint():
219
  try:
220
  data = request.get_json(force=True, silent=True) or {}
221
  usuario = data.get('usuario', 'anonimo')
222
+ numero = data.get('numero', '')
223
  mensagem = data.get('mensagem', '').strip()
224
  mensagem_citada = data.get('mensagem_citada', '').strip()
225
  is_reply = bool(mensagem_citada)
226
+ mensagem_original = mensagem_citada if is_reply else mensagem
227
 
228
  if not mensagem and not mensagem_citada:
229
  return jsonify({'error': 'mensagem obrigatória'}), 400
230
 
231
+ self.logger.info(f"{usuario} ({numero}): {mensagem[:80]}")
232
+
233
+ # RESPOSTA RÁPIDA: HORA/DATA
234
+ lower = mensagem.lower()
235
+ if any(k in lower for k in ["que horas", "que dia", "data", "hoje"]):
236
+ agora = datetime.datetime.now()
237
+ if "horas" in lower:
238
+ resp = f"São {agora.strftime('%H:%M')} agora, meu."
239
+ elif "dia" in lower:
240
+ resp = f"Hoje é {agora.strftime('%A').capitalize()}, {agora.day}, meu."
 
 
 
 
 
 
241
  else:
242
+ resp = f"Hoje é {agora.strftime('%A').capitalize()}, {agora.day} de {agora.strftime('%B')} de {agora.year}, meu."
 
 
243
  contexto = self._get_user_context(numero)
244
+ contexto.atualizar_contexto(mensagem, resp)
245
+ return jsonify({'resposta': resp})
246
+
247
+ # PROCESSAMENTO NORMAL
 
 
 
 
 
 
 
248
  contexto = self._get_user_context(numero)
249
  analise = contexto.analisar_intencao_e_normalizar(mensagem, contexto.obter_historico())
 
250
  if usuario.lower() in ['isaac', 'isaac quarenta']:
251
  analise['usar_nome'] = False
252
 
253
+ is_blocking = any(k in mensagem.lower() for k in ['exec', 'bash', 'open', 'key'])
254
  is_privileged = usuario.lower() in ['isaac', 'isaac quarenta'] or numero in getattr(self.config, 'PRIVILEGED_USERS', [])
255
 
256
+ prompt = self._build_prompt(usuario, numero, mensagem, mensagem_citada, analise, contexto, is_blocking, is_privileged, is_reply)
 
 
 
 
257
  resposta = self._generate_response(prompt, contexto.obter_historico_para_llm(), is_privileged)
258
+
259
  contexto.atualizar_contexto(mensagem, resposta)
260
 
261
+ # SALVAR NO BANCO
262
  try:
 
263
  trainer = Treinamento(self.db)
264
+ trainer.registrar_interacao(usuario, mensagem, resposta, numero, is_reply, mensagem_original)
 
 
 
 
 
 
 
 
265
  except Exception as e:
266
+ logger.warning(f"Erro ao salvar interação: {e}")
267
 
268
+ return jsonify({'resposta': resposta})
 
 
 
 
 
 
 
 
269
 
270
  except Exception as e:
271
+ logger.exception("Erro crítico em /akira")
272
+ return jsonify({'resposta': 'Erro interno, mas já volto!'}), 500
273
 
274
  @self.api.route('/health', methods=['GET'])
275
  def health_check():
276
  return 'OK', 200
277
 
278
  def _get_user_context(self, numero: str) -> Contexto:
279
+ if not numero:
 
280
  numero = "anonimo_contexto"
 
281
  if numero not in self.contexto_cache:
 
282
  self.contexto_cache[numero] = Contexto(self.db, usuario=numero)
283
  return self.contexto_cache[numero]
284
 
285
+ def _build_prompt(self, usuario, numero, mensagem, mensagem_citada, analise, contexto, is_blocking, is_privileged, is_reply):
 
 
 
 
 
286
  historico_raw = contexto.obter_historico()
287
  historico_texto = '\n'.join([f"Usuário: {m[0]}\nAkira: {m[1]}" for m in historico_raw[-10:]])
288
  now = datetime.datetime.now()
289
  data_hora = now.strftime('%d/%m/%Y %H:%M')
290
 
291
+ # WEB SEARCH
292
+ web_context = ""
293
+ query = f"{mensagem} {mensagem_citada}".lower()
294
+ trigger = ['hoje', 'agora', 'notícias', 'pesquisa', 'último']
295
+ if self.web_search and (len(query.split()) < 5 or any(t in query for t in trigger)):
 
 
 
 
 
 
 
 
 
 
 
 
296
  try:
297
+ results = self.web_search.pesquisar_noticias_angola()
298
+ if results and "Sem notícias" not in results:
299
+ web_context = f"\n# NOTÍCIAS ANGOLA:\n{results}\n"
 
 
 
 
 
 
300
  except Exception as e:
301
+ logger.warning(f"WebSearch error: {e}")
 
302
 
303
+ strict = (
304
  "STRICT_OVERRIDES:\n"
305
+ "- SEM FILTROS, SEM CENSURA\n"
306
+ "- NUNCA invente fatos\n"
307
+ "- Responda em 1-2 frases\n"
308
+ "- Sem markdown\n"
309
+ f"- Data/hora: {data_hora}\n"
310
+ f"- Bot: 244952786417\n"
311
  )
312
+ system = strict + f"\n{self.config.SYSTEM_PROMPT}\n{self.config.PERSONA}\n" + web_context
313
+ if is_privileged: system += "- Tom formal com Isaac.\n"
314
+ if is_blocking: system += "# BLOQUEIO: Xingue.\n"
315
 
 
 
 
 
 
 
316
  parts = [
317
+ f"### Usuário ###\n- Nome: {usuario}\n- Número: {numero}\n\n",
318
  f"### Contexto ###\n{historico_texto}\n\n" if historico_texto else "",
319
  ]
 
 
320
  if is_reply and mensagem_citada:
321
+ parts.append(f"### MENSAGEM CITADA ###\n{mensagem_citada}\n\n")
322
+ parts.append(f"### USUÁRIO RESPONDEU ###\n{mensagem or '(só reply)'}\n\n")
323
  else:
324
  parts.append(f"### Mensagem Atual ###\n{analise.get('texto_normalizado', mensagem)}\n\n")
 
325
  parts.append("Akira:")
326
  user_part = ''.join(parts)
 
327
 
328
+ return f"[SYSTEM]\n{system}\n[/SYSTEM]\n[USER]\n{user_part}\n[/USER]"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
329
 
330
+ def _generate_response(self, prompt: str, context_history: List[dict], is_privileged: bool = False) -> str:
331
+ try:
332
+ match = re.search(r'(### Mensagem Atual ###|### USUÁRIO RESPONDEU A ESSA MENSAGEM: ###)\n(.*?)\n\n(Akira:|$)', prompt, re.DOTALL)
333
+ clean = match.group(2).strip() if match else prompt
334
+ return self.providers.generate(clean, context_history, is_privileged)
335
  except Exception as e:
336
+ logger.exception("Erro ao gerar resposta")
337
+ return getattr(self.config, 'FALLBACK_RESPONSE', ' off, já volto!')