akra35567 commited on
Commit
4ad8c5a
·
1 Parent(s): 992ec88

Update modules/api.py

Browse files
Files changed (1) hide show
  1. modules/api.py +157 -399
modules/api.py CHANGED
@@ -1,433 +1,191 @@
1
- """API wrapper for Akira service.
2
-
3
- This module provides a single AkiraAPI class which wires together the
4
- configuration (modules.config), database, context manager, training and
5
- LLM providers already present in this repository. The goal is to keep the
6
- integration layer minimal and robust so `main.py` can create the app with:
7
-
8
- from modules.api import AkiraAPI
9
- import modules.config as config
10
- akira = AkiraAPI(config)
11
- app = akira.app
12
-
13
- The implementation below avoids depending on missing modules and normalizes
14
- the config names to the existing `config.py` constants.
15
- """
16
-
17
- from typing import Dict, Optional, Any
18
  import time
19
- import logging
20
- import re
21
- from flask import Flask, Blueprint, request, jsonify
22
-
23
- # Importações de módulos locais
24
- from .contexto import Contexto
25
- from .database import Database
26
- from .treinamento import Treinamento
27
- from .exemplos_naturais import ExemplosNaturais
28
-
29
- # Tenta importar provedores de LLM
30
- try:
31
- from .local_llm import LlamaLLM # NOVO: IMPORTAÇÃO DO MODELO LOCAL
32
- local_llm_available = True
33
- except ImportError:
34
- local_llm_available = False
35
- logging.getLogger("akira.api").warning("LlamaLLM não disponível. Modelo local desabilitado.")
36
 
 
 
37
 
38
- try:
39
- from mistralai import Mistral
40
- mistral_available = True
41
- except ImportError:
42
- mistral_available = False
43
- logger = logging.getLogger("akira.api")
44
-
45
-
46
- try:
47
- import google.generativeai as genai
48
- gemini_available = True
49
- except ImportError:
50
- gemini_available = False
51
- logging.getLogger("akira.api").warning("google.generativeai não disponível. Gemini desabilitado.")
52
 
53
- logger = logging.getLogger("akira.api")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
 
 
 
 
55
 
56
- class LLMManager:
57
- """Gerenciador de provedores LLM (Local -> Mistral -> Gemini como fallback)."""
58
 
59
- def __init__(self, config):
60
- self.config = config
61
- self.mistral_client = None
62
- self.gemini_model = None
63
- self.local_llm = None # NOVO: Atributo para o modelo local
64
- self._setup_providers()
 
 
 
65
 
66
- def _setup_providers(self):
67
- # 1. SETUP LLAMA LOCAL (PRIORIDADE 1)
68
- if local_llm_available:
69
- try:
70
- self.local_llm = LlamaLLM()
71
- if not self.local_llm.is_available():
72
- self.local_llm = None
73
- logger.warning("LlamaLLM carregado mas não está disponível/operacional. Passando para API.")
74
- else:
75
- logger.info("LlamaLLM (Local/HF) inicializado como primário.")
76
- except Exception as e:
77
- logger.warning(f"Falha ao inicializar LlamaLLM: {e}. Passando para API.")
78
- self.local_llm = None
79
 
80
- # 2. SETUP MISTRAL API (PRIORIDADE 2)
81
- # Adicionada verificação de 'm-' para evitar a tentativa de inicialização com chave inválida
82
- if mistral_available and getattr(self.config, 'MISTRAL_API_KEY', None) and getattr(self.config, 'MISTRAL_API_KEY', '').startswith('m-'):
 
 
 
83
  try:
84
- self.mistral_client = Mistral(api_key=self.config.MISTRAL_API_KEY)
85
- logger.info("Mistral client inicializado.")
86
  except Exception as e:
87
- logger.warning(f"Falha ao inicializar Mistral: {e}")
 
88
  else:
89
- logger.warning("Mistral API desativada (chave ausente ou inválida, como no log).")
90
 
91
- # 3. SETUP GEMINI API (PRIORIDADE 3)
92
- if gemini_available and getattr(self.config, 'GEMINI_API_KEY', None):
 
93
  try:
94
- self.gemini_model = genai.GenerativeModel(getattr(self.config, 'GEMINI_MODEL', 'gemini-2.5-flash'))
95
- logger.info("Gemini model inicializado.")
 
 
 
 
 
96
  except Exception as e:
97
- logger.warning(f"Falha ao inicializar Gemini: {e}")
98
-
99
- def generate(self, prompt: str, max_tokens: int = 300, temperature: float = 0.8) -> str:
100
- # NOVA ORDEM DE PRIORIDADE: Local -> Mistral API -> Gemini API
101
- providers = ['local', 'mistral', 'gemini']
102
-
103
- for provider in providers:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
 
105
- # PRIORITY 1: LOCAL LLM
106
- if provider == 'local' and self.local_llm and self.local_llm.is_available():
107
- try:
108
- response = self.local_llm.generate(prompt, max_tokens=max_tokens, temperature=temperature)
109
- if response:
110
- logger.info("Resposta gerada por: LlamaLLM (Local/HF)")
111
- return response
112
- logger.warning("LlamaLLM gerou resposta vazia, tentando próximo provedor.")
113
- except Exception as e:
114
- logger.warning(f"LlamaLLM (Local/HF) falhou: {e}. Próximo provedor.")
115
-
116
-
117
- # PRIORITY 2: MISTRAL API
118
- elif provider == 'mistral' and self.mistral_client:
119
  try:
120
- response = self.mistral_client.chat.complete(
121
- model=getattr(self.config, 'MISTRAL_MODEL', 'mistral-small-latest'),
122
- messages=[{"role": "user", "content": prompt}],
123
- max_tokens=max_tokens,
124
- temperature=temperature
125
- )
126
- content = response.choices[0].message.content if response.choices else ""
127
- if content:
128
- logger.info("Resposta gerada por: Mistral API")
129
- return str(content)
130
 
131
- logger.warning("Mistral API gerou resposta vazia, tentando próximo provedor.")
132
-
 
 
 
 
 
 
 
 
133
  except Exception as e:
134
- error_msg = str(e).lower()
135
- if "429" in error_msg or "too many requests" in error_msg or "service tier capacity exceeded" in error_msg:
136
- logger.warning(f"Mistral rate limit, retrying in 1s: {e}")
137
- time.sleep(1)
138
- try:
139
- response = self.mistral_client.chat.complete(
140
- model=getattr(self.config, 'MISTRAL_MODEL', 'mistral-small-latest'),
141
- messages=[{"role": "user", "content": prompt}],
142
- max_tokens=max_tokens,
143
- temperature=temperature
144
- )
145
- content = response.choices[0].message.content if response.choices else ""
146
- if content:
147
- logger.info("Resposta gerada por: Mistral API (Retry)")
148
- return str(content)
149
- except Exception as e2:
150
- logger.warning(f"Mistral retry failed: {e2}")
151
- else:
152
- logger.warning(f"Mistral falhou: {e}. Próximo provedor.")
153
 
154
- # PRIORITY 3: GEMINI API
 
 
155
  elif provider == 'gemini' and self.gemini_model:
156
  try:
 
 
 
 
 
 
 
 
 
 
 
 
157
  response = self.gemini_model.generate_content(
158
- prompt,
159
- generation_config={
160
- "max_output_tokens": max_tokens,
161
- "temperature": temperature
162
- }
163
  )
164
  text = response.text
 
165
  if text:
166
- logger.info("Resposta gerada por: Gemini API")
167
  return text.strip()
168
 
169
  logger.warning("Gemini API gerou resposta vazia, tentando fallback.")
170
 
171
  except Exception as e:
172
  error_msg = str(e).lower()
173
- if "429" in error_msg or "too many requests" in error_msg or "quota exceeded" in error_msg or "404" in error_msg:
174
- logger.warning(f"Gemini error/rate limit, retrying in 1s: {e}")
175
- time.sleep(1)
176
- try:
177
- response = self.gemini_model.generate_content(
178
- prompt,
179
- generation_config={
180
- "max_output_tokens": max_tokens,
181
- "temperature": temperature
182
- }
183
- )
184
- text = response.text
185
- if text:
186
- logger.info("Resposta gerada por: Gemini API (Retry)")
187
- return text.strip()
188
- except Exception as e2:
189
- logger.warning(f"Gemini retry failed: {e2}")
190
- else:
191
- logger.warning(f"Gemini falhou: {e}")
192
-
193
- logger.error("Todos os provedores (Local, Mistral, Gemini) falharam")
194
- return getattr(self.config, 'FALLBACK_RESPONSE', 'Desculpa, puto, o modelo tá off hoje. Tenta depois!')
195
-
196
-
197
- class SimpleTTLCache:
198
- def __init__(self, ttl_seconds: int = 300):
199
- self.ttl = ttl_seconds
200
- self._store = {}
201
-
202
- def __contains__(self, key):
203
- v = self._store.get(key)
204
- if not v:
205
- return False
206
- value, expires = v
207
- if time.time() > expires:
208
- del self._store[key]
209
- return False
210
- return True
211
-
212
- def __setitem__(self, key, value: Any):
213
- self._store[key] = (value, time.time() + self.ttl)
214
-
215
- def __getitem__(self, key):
216
- if key in self:
217
- return self._store[key][0]
218
- raise KeyError(key)
219
-
220
-
221
- class AkiraAPI:
222
- def __init__(self, cfg_module):
223
- self.config = cfg_module
224
- self.app = Flask(__name__)
225
- self.api = Blueprint("akira_api", __name__)
226
- self.contexto_cache = SimpleTTLCache(ttl_seconds=getattr(self.config, 'MEMORIA_MAX', 300))
227
- self.providers = LLMManager(self.config)
228
- self.exemplos = ExemplosNaturais()
229
- self.logger = logger
230
-
231
- self._setup_personality()
232
- self._setup_routes()
233
- self._setup_trainer()
234
-
235
- self.app.register_blueprint(self.api, url_prefix="/api", name="akira_api_prefixed")
236
- self.app.register_blueprint(self.api, url_prefix="", name="akira_api_root")
237
-
238
- def _setup_personality(self):
239
- self.humor = getattr(self.config, 'HUMOR_INICIAL', 'neutra')
240
- self.interesses = list(getattr(self.config, 'INTERESSES', []))
241
- self.limites = list(getattr(self.config, 'LIMITES', []))
242
- self.persona = getattr(self.config, 'PERSONA', '')
243
-
244
- def _setup_routes(self):
245
- @self.api.route('/akira', methods=['POST'])
246
- def akira_endpoint():
247
- try:
248
- data = request.get_json(force=True, silent=True) or {}
249
- usuario = data.get('usuario', 'anonimo')
250
- numero = data.get('numero', '')
251
- mensagem = data.get('mensagem', '')
252
- is_privileged = bool(data.get('is_privileged_user', False))
253
- if usuario.lower() == 'isaac':
254
- is_privileged = True
255
- is_reply = bool(data.get('is_reply') or data.get('mensagem_original') or data.get('quoted_message'))
256
- mensagem_original = data.get('mensagem_original') or data.get('quoted_message') or ''
257
-
258
- if not mensagem:
259
- return jsonify({'error': 'mensagem é obrigatória'}), 400
260
-
261
-
262
- self.logger.info(f"📨 {usuario} ({numero}): {mensagem[:120]}")
263
-
264
- contexto = self._get_user_context(usuario)
265
- analise = contexto.analisar_intencao_e_normalizar(mensagem, contexto.obter_historico())
266
- if usuario.lower() == 'isaac':
267
- analise['usar_nome'] = False
268
-
269
- is_blocking = False
270
- if len(mensagem) < 10 and any(k in mensagem.lower() for k in ['exec', 'bash', 'open', 'api_key', 'key']):
271
- is_blocking = True
272
-
273
- prompt = self._build_prompt(usuario, numero, mensagem, analise, contexto, is_blocking,
274
- is_privileged=is_privileged, is_reply=is_reply,
275
- mensagem_original=mensagem_original)
276
- resposta = self._generate_response(prompt)
277
- contexto.atualizar_contexto(mensagem, resposta)
278
-
279
- try:
280
- db = Database(getattr(self.config, 'DB_PATH', 'akira.db'))
281
- trainer = Treinamento(db)
282
- trainer.registrar_interacao(usuario, mensagem, resposta, numero, is_reply, mensagem_original)
283
- except Exception as e:
284
- self.logger.warning(f"Registro de interação falhou: {e}")
285
-
286
- response_data: Dict[str, Any] = {'resposta': resposta}
287
  try:
288
- aprendizados = contexto.obter_aprendizados()
289
- if aprendizados:
290
- response_data['aprendizados'] = aprendizados
 
 
 
 
 
 
 
291
  except Exception as e:
292
- self.logger.warning(f"Falha ao obter aprendizados: {e}")
293
-
294
- return jsonify(response_data)
295
- except Exception as e:
296
- self.logger.exception('Erro no endpoint /akira')
297
- return jsonify({'resposta': getattr(self.config, 'FALLBACK_RESPONSE', 'Erro interno')}), 500
298
-
299
- @self.api.route('/health', methods=['GET'])
300
- def health_check():
301
- return 'OK', 200
302
-
303
- def _get_user_context(self, usuario: str) -> Contexto:
304
- if usuario not in self.contexto_cache:
305
- db = Database(getattr(self.config, 'DB_PATH', 'akira.db'))
306
- self.contexto_cache[usuario] = Contexto(db, usuario=usuario)
307
- return self.contexto_cache[usuario]
308
-
309
- def _build_prompt(self, usuario: str, numero: str, mensagem: str, analise: Dict, contexto: Contexto, is_blocking: bool,
310
- is_privileged: bool = False, is_reply: bool = False, mensagem_original: str = '') -> str:
311
- import datetime
312
- historico = contexto.obter_historico()
313
- historico_texto = '\n'.join([f"Usuário: {m[0]}\nAkira: {m[1]}" for m in historico])
314
-
315
- now = datetime.datetime.now()
316
- data_hora = now.strftime('%d/%m/%Y %H:%M')
317
-
318
- strict_override = (
319
- "STRICT_OVERRIDES:\n"
320
- "- NUNCA invente fatos ou mencione eventos/pessoas não presentes no contexto.\n"
321
- "- NÃO use negrito, markdown ou HTML. Remova quaisquer formatações.\n"
322
- "- Não quebre linhas desnecessariamente; mantenha respostas em parágrafos contínuos para parecer mais natural e menos robótico.\n"
323
- "- Não repita o que o usuário disse; responda de forma original e relevante, sem copiar frases do usuário.\n"
324
- "- Para saudações simples como 'oi', 'olá', 'akira', responda de forma conversacional perguntando como o usuário está, por exemplo 'Oi! Tudo bem com você?' ou 'Olá! Como vai?'.\n"
325
- "- Se is_privileged=true: use tom respeitoso/mais formal; evite gírias; use pronomes formais como 'o senhor' ou 'a senhora'.\n"
326
- "- Não introduza tópicos como futebol/partidas a menos que o usuário ou o contexto peça.\n"
327
- "- Use linguagem natural, criativa e autônoma, como se fosse uma pessoa real, não um robô.\n"
328
- "- Demonstre autonomia, personalidade e senso crítico.\n"
329
- "- Considere o contexto, a data e hora atuais.\n"
330
- f"- Data e hora atuais: {data_hora}.\n"
331
- f"- Número do bot: 244952786417.\n"
332
- )
333
-
334
- system_part = strict_override + f"\n{getattr(self.config, 'SYSTEM_PROMPT', '')}\n{self.persona}\n"
335
- regras = '\n'.join(getattr(self.config, 'REGRAS', []))
336
- filtros = '\n'.join(getattr(self.config, 'FILTERS', []))
337
- system_part += f"# Regras:\n{regras}\n# Filtros:\n{filtros}\n"
338
-
339
- extra_instructions = []
340
- if is_privileged:
341
- extra_instructions.append("Quando o usuário for privilegiado: use tom respeitoso e ligeiramente formal. Evite gírias e piadas a menos que o usuário as solicite. Use títulos (Sr./Sra.) ou formas de tratamento formais quando apropriado. Use pronomes formais como 'o senhor' ou 'a senhora' de vez em quando para maior respeito. Seja conciso.")
342
- else:
343
- extra_instructions.append("Mantenha a persona local quando apropriado, mas NÃO comece com textos longos. Para saudações simples, responda com uma frase curta ou até 2 frases no máximo. Use o nome do usuário apenas quando fizer sentido. Prefira pronomes para evitar repetição.")
344
-
345
- extra_instructions.append("Não invente fatos, não mencione resultados de jogos, pessoas ou eventos que não estão no contexto. Não use negrito/markdown (**, __) nem formatação HTML por padrão. Evite mensagens muito longas; seja direto.")
346
- system_part += "\n# Instruções adicionais:\n" + "\n".join(extra_instructions) + "\n"
347
-
348
- if is_blocking:
349
- system_part += "\n# BLOQUEIO: usuário possivelmente malicioso. Responda recusando a executar comandos sensíveis, com xingamentos como (vai pra merda, sou tua serva por acaso?, vai tomar no cu).\n"
350
-
351
- usar_nome = analise.get('usar_nome', False)
352
- parts = []
353
- parts.append(f"### Usuário ###\n- Nome: {usuario}\n- Número: {numero}\n- Tom: {self.humor}\n- Estilo: {analise.get('estilo', 'normal')}\n- Usar_nome: {usar_nome}\n\n")
354
- parts.append(f"### Contexto ###\n{historico_texto}\n\n")
355
- parts.append(f"### Mensagem ###\n{analise.get('texto_normalizado', mensagem)}\n\n")
356
- if is_reply and mensagem_original:
357
- parts.append(f"### Mensagem original (reply) ###\n{mensagem_original}\n\n")
358
- parts.append(f"### Instruções ###\n{getattr(self.config, 'INSTRUCTIONS', '')}\n\n")
359
- parts.append("Akira:\n")
360
- user_part = ''.join(parts)
361
-
362
- prompt = f"[SYSTEM]\n{system_part}\n[/SYSTEM]\n[USER]\n{user_part}\n[/USER]"
363
- return prompt
364
-
365
- def _generate_response(self, prompt: str) -> str:
366
- try:
367
- max_tokens = getattr(self.config, 'MAX_TOKENS', 300)
368
- temperature = getattr(self.config, 'TEMPERATURE', 0.8)
369
- text = self.providers.generate(prompt, max_tokens=max_tokens, temperature=temperature)
370
- return self._clean_response(text, prompt)
371
- except Exception as e:
372
- self.logger.exception('Falha ao gerar resposta com provedores LLM')
373
- return getattr(self.config, 'FALLBACK_RESPONSE', 'Desculpa, o modelo está off.')
374
-
375
- def _clean_response(self, text: Optional[str], prompt: Optional[str] = None) -> str:
376
- if not text:
377
- return ''
378
- cleaned = text.strip()
379
-
380
- for prefix in ['akira:', 'Resposta:', 'resposta:']:
381
- if cleaned.startswith(prefix):
382
- cleaned = cleaned[len(prefix):].strip()
383
- break
384
-
385
- cleaned = re.sub(r'\*+([^*]+)\*+', r'\1', cleaned)
386
- cleaned = re.sub(r'_+([^_]+)_+', r'\1', cleaned)
387
- cleaned = re.sub(r'`+([^`]+)`+', r'\1', cleaned)
388
- cleaned = re.sub(r'~+([^~]+)~+', r'\1', cleaned)
389
- cleaned = re.sub(r'\[([^\]]+)\]', r'\1', cleaned)
390
- cleaned = re.sub(r'<[^>]+>', '', cleaned)
391
-
392
- sentences = re.split(r'(?<=[.!?])\s+', cleaned)
393
- if len(sentences) > 2:
394
- cleaned = ' '.join(sentences[:2]).strip()
395
-
396
- sports_keywords = ['futebol', 'girabola', 'petro', 'jogo', 'partida', 'contrata', 'campeonato', 'liga']
397
- try:
398
- prompt_text = (prompt or '').lower()
399
- if prompt_text and not any(k in prompt_text for k in sports_keywords):
400
- filtered = []
401
- for s in re.split(r'(?<=[\.\!\?])\s+', cleaned):
402
- if not any(k in s.lower() for k in sports_keywords):
403
- filtered.append(s)
404
- if filtered:
405
- cleaned = ' '.join(filtered).strip()
406
- except Exception:
407
- pass
408
-
409
- max_chars = getattr(self.config, 'MAX_RESPONSE_CHARS', None)
410
- if not max_chars:
411
- max_chars = getattr(self.config, 'MAX_TOKENS', 300) * 4
412
-
413
- cleaned = re.sub(r"\*{0,2}([A-ZÀ-Ÿ][a-zà-ÿ]+\s+[A-ZÀ-Ÿ][a-zà-ÿ]+)\*{0,2}", r"\1", cleaned)
414
- return cleaned[:max_chars]
415
-
416
- def _setup_trainer(self):
417
- if getattr(self.config, 'START_PERIODIC_TRAINER', False):
418
- try:
419
- db = Database(getattr(self.config, 'DB_PATH', 'akira.db'))
420
- trainer = Treinamento(db, interval_hours=getattr(self.config, 'TRAIN_INTERVAL_HOURS', 24))
421
- trainer.start_periodic_training()
422
- self.logger.info("Treinamento periódico iniciado com sucesso.")
423
- except Exception as e:
424
- self.logger.exception(f"Falha ao iniciar treinador periódico: {e}")
425
-
426
- def responder(self, mensagem: str, numero: str, nome: str = 'Usuário') -> str:
427
- data = {'usuario': nome, 'numero': numero, 'mensagem': mensagem}
428
- contexto = self._get_user_context(nome)
429
- analise = contexto.analisar_intencao_e_normalizar(mensagem, contexto.obter_historico())
430
- prompt = self._build_prompt(nome, numero, mensagem, analise, contexto, is_blocking=False)
431
- resposta = self._generate_response(prompt)
432
- contexto.atualizar_contexto(mensagem, resposta)
433
- return resposta
 
1
+ # modules/api.py
2
+ import os
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  import time
4
+ from typing import Optional, List, Any
5
+ import google.generativeai as genai
6
+ from mistralai.client import MistralClient
7
+ from mistralai.models.chat_models import ChatMessage
8
+ from loguru import logger
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
+ # Importa as configurações do seu arquivo config
11
+ from modules import config
12
 
13
+ class LLMManager:
14
+ def __init__(self):
15
+ self.config = config
16
+ self.mistral_client: Optional[MistralClient] = None
17
+ self.gemini_model: Optional[genai.GenerativeModel] = None
18
+ self.llama_llm = self._import_llama()
 
 
 
 
 
 
 
 
19
 
20
+ self._setup_providers()
21
+
22
+ # Lista de provedores em ordem de prioridade (Mistral > Gemini)
23
+ # O LlamaLLM (local) será adicionado se for importado e disponível,
24
+ # mas como esvaziamos o local_llm.py, ele será pulado.
25
+ self.providers = []
26
+
27
+ # PRIORIDADE 1: Mistral API
28
+ if self.mistral_client:
29
+ self.providers.append('mistral')
30
+
31
+ # PRIORIDADE 2: Gemini API (Fallback Principal)
32
+ if self.gemini_model:
33
+ self.providers.append('gemini')
34
+
35
+ # Prioridade 3: Llama Local (Se estiver configurado e disponível)
36
+ if self.llama_llm and self.llama_llm.is_available():
37
+ self.providers.append('llama') # Isso será sempre FALSO com o novo local_llm.py
38
 
39
+ if not self.providers:
40
+ logger.error("Nenhum provedor de LLM configurado ou operacional. O app responderá apenas com fallback.")
41
+ else:
42
+ logger.info(f"Provedores ativos, ordem de prioridade: {self.providers}")
43
 
 
 
44
 
45
+ def _import_llama(self):
46
+ """Importa o LlamaLLM se o arquivo existir."""
47
+ try:
48
+ from modules.local_llm import LlamaLLM
49
+ # O carregamento real é feito dentro de LlamaLLM.__init__
50
+ return LlamaLLM()
51
+ except Exception as e:
52
+ logger.warning(f"Falha ao importar LlamaLLM (módulo ausente ou erro de importação): {e}")
53
+ return None
54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
+ def _setup_providers(self) -> None:
57
+ """Inicializa os clientes da API."""
58
+
59
+ # 1. MISTRAL
60
+ mistral_available = self.config.MISTRAL_API_KEY.startswith('m-')
61
+ if mistral_available:
62
  try:
63
+ self.mistral_client = MistralClient(api_key=self.config.MISTRAL_API_KEY)
64
+ logger.info("Mistral API inicializada.")
65
  except Exception as e:
66
+ logger.warning(f"Falha ao inicializar Mistral Client: {e}. Desativando Mistral API.")
67
+ self.mistral_client = None
68
  else:
69
+ logger.warning("Mistral API desativada (chave ausente ou inválida, como no log).")
70
 
71
+ # 2. GEMINI
72
+ gemini_available = self.config.GEMINI_API_KEY.startswith('AIza')
73
+ if gemini_available:
74
  try:
75
+ # Inicializa o cliente Gemini. A chave será RE-CONFIGURADA em .generate
76
+ # para maior robustez, prevenindo o erro de chave perdida.
77
+ self.gemini_model = genai.GenerativeModel(
78
+ model=self.config.GEMINI_MODEL,
79
+ system_instruction=self.config.PERSONA + self.config.SYSTEM_PROMPT
80
+ )
81
+ logger.info(f"Gemini model inicializado: {self.config.GEMINI_MODEL}")
82
  except Exception as e:
83
+ logger.warning(f"Falha ao inicializar modelo Gemini: {e}. Desativando Gemini API.")
84
+ self.gemini_model = None
85
+ else:
86
+ logger.warning("Gemini API desativada (chave ausente ou inválida).")
87
+
88
+ # 3. LLAMA LOCAL
89
+ # O LlamaLLM já é importado e carregado em self.llama_llm, o que permite o
90
+ # teste de disponibilidade no self.providers.
91
+
92
+
93
+ def generate(self, user_prompt: str, context_history: List[dict] = [], is_privileged: bool = False) -> str:
94
+ """Gera a resposta, iterando pelos provedores na ordem de prioridade."""
95
+
96
+ # Formata o prompt para o LLM. As APIs usam ChatMessage/system_instruction.
97
+ full_system_prompt = self.config.PERSONA + self.config.SYSTEM_PROMPT
98
+
99
+ # Constrói o histórico do chat
100
+ messages = [
101
+ ChatMessage(role="system", content=full_system_prompt)
102
+ ]
103
+
104
+ for turn in context_history:
105
+ role = "user" if turn["role"] == "user" else "assistant"
106
+ messages.append(ChatMessage(role=role, content=turn["content"]))
107
+
108
+ messages.append(ChatMessage(role="user", content=user_prompt))
109
+
110
+ # Tenta provedores na ordem de prioridade
111
+ for provider in self.providers:
112
 
113
+ # -----------------------------------------------------------
114
+ # PRIORITY 1: MISTRAL API
115
+ # -----------------------------------------------------------
116
+ if provider == 'mistral' and self.mistral_client:
 
 
 
 
 
 
 
 
 
 
117
  try:
118
+ # Mistral usa seu próprio formato de mensagem/role
119
+ mistral_messages = [
120
+ ChatMessage(role=msg.role, content=msg.content)
121
+ for msg in messages
122
+ ]
 
 
 
 
 
123
 
124
+ response = self.mistral_client.chat(
125
+ model=self.config.MISTRAL_MODEL,
126
+ messages=mistral_messages,
127
+ temperature=self.config.TOP_P, # Mistral usa TOP_P para o temperature
128
+ max_tokens=self.config.MAX_TOKENS
129
+ )
130
+ text = response.choices[0].message.content
131
+ if text:
132
+ logger.info("Resposta gerada por: Mistral API (Principal)")
133
+ return text.strip()
134
  except Exception as e:
135
+ logger.warning(f"Mistral API falhou: {e}. Tentando fallback.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
 
137
+ # -----------------------------------------------------------
138
+ # PRIORITY 2: GEMINI API (FALLBACK)
139
+ # -----------------------------------------------------------
140
  elif provider == 'gemini' and self.gemini_model:
141
  try:
142
+ # **SOLUÇÃO CRÍTICA**: Reconfigura a chave ANTES de chamar generate_content
143
+ # Isso previne o erro de 'No API_KEY or ADC found'
144
+ if self.config.GEMINI_API_KEY.startswith('AIza'):
145
+ genai.configure(api_key=self.config.GEMINI_API_KEY)
146
+
147
+ # Gemini usa um formato de histórico que alterna 'user' e 'model'
148
+ gemini_history = []
149
+ # O primeiro item é o system_instruction, que é passado na inicialização
150
+ for msg in messages[1:]:
151
+ role = "user" if msg.role == "user" else "model"
152
+ gemini_history.append({"role": role, "parts": [msg.content]})
153
+
154
  response = self.gemini_model.generate_content(
155
+ gemini_history,
156
+ config={"max_output_tokens": self.config.MAX_TOKENS, "temperature": self.config.TOP_P}
 
 
 
157
  )
158
  text = response.text
159
+
160
  if text:
161
+ logger.info("Resposta gerada por: Gemini API (Fallback)")
162
  return text.strip()
163
 
164
  logger.warning("Gemini API gerou resposta vazia, tentando fallback.")
165
 
166
  except Exception as e:
167
  error_msg = str(e).lower()
168
+ if "no api_key or adc found" in error_msg:
169
+ logger.error("Gemini falhou por CHAVE INVÁLIDA/PERDIDA no ponto de uso.")
170
+ logger.warning(f"Gemini API falhou: {e}. Tentando fallback.")
171
+
172
+ # -----------------------------------------------------------
173
+ # PRIORITY 3: LLAMA LOCAL (IGNORADO se local_llm.py estiver vazio)
174
+ # -----------------------------------------------------------
175
+ elif provider == 'llama' and self.llama_llm and self.llama_llm.is_available():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
  try:
177
+ # A chamada LLAMA é diferente, ela precisa do prompt formatado
178
+ # Passar o prompt bruto aqui e o local_llm.py fará a formatação
179
+ local_response = self.llama_llm.generate(
180
+ user_prompt,
181
+ max_tokens=self.config.MAX_TOKENS,
182
+ temperature=self.config.TOP_P
183
+ )
184
+ if local_response:
185
+ logger.info("Resposta gerada por: Llama 3.1 Local")
186
+ return local_response
187
  except Exception as e:
188
+ logger.warning(f"Llama Local falhou: {e}. Tentando fallback.")
189
+
190
+ logger.error("Todos os provedores (Mistral, Gemini, Local) falharam")
191
+ return self.config.FALLBACK_RESPONSE