akra35567 commited on
Commit
aea5255
·
1 Parent(s): 87619ad

Update modules/api.py

Browse files
Files changed (1) hide show
  1. modules/api.py +89 -107
modules/api.py CHANGED
@@ -25,23 +25,21 @@ from .database import Database
25
  from .treinamento import Treinamento
26
  from .exemplos_naturais import ExemplosNaturais
27
 
28
- # --- NOVOS IMPORTS PARA WEBSERVICE ---
29
  try:
30
- # Assumindo que o web_search está no mesmo diretório de módulos
31
- from .web_search import WebSearch
32
- websearch_available = True
33
  except ImportError:
34
- websearch_available = False
35
- logging.warning("WebSearch não disponível. Funcionalidades de busca limitadas.")
36
- # --------------------------------------
37
 
38
  try:
39
  from mistralai import Mistral
40
  mistral_available = True
41
  except ImportError:
42
  mistral_available = False
 
43
 
44
- logger = logging.getLogger("akira.api")
45
 
46
  try:
47
  import google.generativeai as genai
@@ -52,17 +50,30 @@ except ImportError:
52
 
53
 
54
  class LLMManager:
55
- """Gerenciador de provedores LLM (Mistral + Gemini como fallback)."""
56
 
57
  def __init__(self, config):
58
  self.config = config
59
  self.mistral_client = None
60
  self.gemini_model = None
 
61
  self._setup_providers()
62
 
63
  def _setup_providers(self):
64
- # O código local de LLM que exige GPU está fora deste arquivo,
65
- # focamos apenas nos providers de API externa (Mistral e Gemini)
 
 
 
 
 
 
 
 
 
 
 
 
66
  if mistral_available and getattr(self.config, 'MISTRAL_API_KEY', None):
67
  try:
68
  self.mistral_client = Mistral(api_key=self.config.MISTRAL_API_KEY)
@@ -70,20 +81,36 @@ class LLMManager:
70
  except Exception as e:
71
  logger.warning(f"Falha ao inicializar Mistral: {e}")
72
 
 
73
  if gemini_available and getattr(self.config, 'GEMINI_API_KEY', None):
74
  try:
75
  genai.configure(api_key=self.config.GEMINI_API_KEY)
76
- self.gemini_model = genai.GenerativeModel(getattr(self.config, 'GEMINI_MODEL', 'gemini-1.5-flash')) # type: ignore[reportAttributeAccessIssue]
 
77
  logger.info("Gemini model inicializado.")
78
  except Exception as e:
79
  logger.warning(f"Falha ao inicializar Gemini: {e}")
80
 
81
  def generate(self, prompt: str, max_tokens: int = 300, temperature: float = 0.8) -> str:
82
- # A ordem garante que Gemini seja o fallback
83
- providers = ['mistral', 'gemini']
84
 
85
  for provider in providers:
86
- if provider == 'mistral' and self.mistral_client:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  try:
88
  response = self.mistral_client.chat.complete(
89
  model=getattr(self.config, 'MISTRAL_MODEL', 'mistral-small-latest'),
@@ -92,7 +119,13 @@ class LLMManager:
92
  temperature=temperature
93
  )
94
  content = response.choices[0].message.content if response.choices else ""
95
- return str(content) if content else ""
 
 
 
 
 
 
96
  except Exception as e:
97
  error_msg = str(e).lower()
98
  if "429" in error_msg or "too many requests" in error_msg or "service tier capacity exceeded" in error_msg:
@@ -106,12 +139,15 @@ class LLMManager:
106
  temperature=temperature
107
  )
108
  content = response.choices[0].message.content if response.choices else ""
109
- return str(content) if content else ""
 
 
110
  except Exception as e2:
111
  logger.warning(f"Mistral retry failed: {e2}")
112
  else:
113
  logger.warning(f"Mistral falhou: {e}")
114
 
 
115
  elif provider == 'gemini' and self.gemini_model:
116
  try:
117
  response = self.gemini_model.generate_content(
@@ -122,11 +158,17 @@ class LLMManager:
122
  }
123
  )
124
  text = response.text
125
- return text.strip() if text else ""
 
 
 
 
 
 
126
  except Exception as e:
127
  error_msg = str(e).lower()
128
- if "429" in error_msg or "too many requests" in error_msg or "quota exceeded" in error_msg:
129
- logger.warning(f"Gemini rate limit, retrying in 1s: {e}")
130
  time.sleep(1)
131
  try:
132
  response = self.gemini_model.generate_content(
@@ -137,17 +179,20 @@ class LLMManager:
137
  }
138
  )
139
  text = response.text
140
- return text.strip() if text else ""
 
 
141
  except Exception as e2:
142
  logger.warning(f"Gemini retry failed: {e2}")
143
  else:
144
  logger.warning(f"Gemini falhou: {e}")
145
 
146
- logger.error("Ambos os providers falharam")
147
  return getattr(self.config, 'FALLBACK_RESPONSE', 'Desculpa, puto, o modelo tá off hoje. Tenta depois!')
148
 
149
 
150
  class SimpleTTLCache:
 
151
  def __init__(self, ttl_seconds: int = 300):
152
  self.ttl = ttl_seconds
153
  self._store = {}
@@ -176,14 +221,11 @@ class AkiraAPI:
176
  self.config = cfg_module
177
  self.app = Flask(__name__)
178
  self.api = Blueprint("akira_api", __name__)
 
179
  self.contexto_cache = SimpleTTLCache(ttl_seconds=getattr(self.config, 'MEMORIA_MAX', 300))
180
- self.providers = LLMManager(self.config)
181
  self.exemplos = ExemplosNaturais()
182
  self.logger = logger
183
-
184
- # --- NOVO: Inicialização do WebSearch ---
185
- self.web_search = WebSearch() if websearch_available else None
186
- # ------------------------------------------
187
 
188
  self._setup_personality()
189
  self._setup_routes()
@@ -192,6 +234,7 @@ class AkiraAPI:
192
  self.app.register_blueprint(self.api, url_prefix="/api", name="akira_api_prefixed")
193
  self.app.register_blueprint(self.api, url_prefix="", name="akira_api_root")
194
 
 
195
  def _setup_personality(self):
196
  self.humor = getattr(self.config, 'HUMOR_INICIAL', 'neutra')
197
  self.interesses = list(getattr(self.config, 'INTERESSES', []))
@@ -209,11 +252,8 @@ class AkiraAPI:
209
  is_privileged = bool(data.get('is_privileged_user', False))
210
  if usuario.lower() == 'isaac':
211
  is_privileged = True
212
-
213
- # --- NOVO: Extração da mensagem citada (inclui o novo campo do index.js) ---
214
- mensagem_citada = data.get('mensagem_citada') or data.get('mensagem_original') or data.get('quoted_message') or ''
215
- is_reply = bool(mensagem_citada)
216
- # ----------------------------------------------------------------------------
217
 
218
  if not mensagem:
219
  return jsonify({'error': 'mensagem é obrigatória'}), 400
@@ -230,20 +270,16 @@ class AkiraAPI:
230
  if len(mensagem) < 10 and any(k in mensagem.lower() for k in ['exec', 'bash', 'open', 'api_key', 'key']):
231
  is_blocking = True
232
 
233
- # --- NOVO: passagem do campo mensagem_citada para o build_prompt ---
234
  prompt = self._build_prompt(usuario, numero, mensagem, analise, contexto, is_blocking,
235
- is_privileged=is_privileged, is_reply=is_reply,
236
- mensagem_citada=mensagem_citada)
237
- # ------------------------------------------------------------------
238
-
239
  resposta = self._generate_response(prompt)
240
  contexto.atualizar_contexto(mensagem, resposta)
241
 
242
  try:
243
  db = Database(getattr(self.config, 'DB_PATH', 'akira.db'))
244
  trainer = Treinamento(db)
245
- # Passagem da mensagem citada para o registro
246
- trainer.registrar_interacao(usuario, mensagem, resposta, numero, is_reply, mensagem_citada)
247
  except Exception as e:
248
  self.logger.warning(f"Registro de interação falhou: {e}")
249
 
@@ -270,52 +306,14 @@ class AkiraAPI:
270
  self.contexto_cache[usuario] = Contexto(db, usuario=usuario)
271
  return self.contexto_cache[usuario]
272
 
273
- # --- FUNÇÃO _build_prompt ATUALIZADA (Lógica de Busca Inteligente) ---
274
  def _build_prompt(self, usuario: str, numero: str, mensagem: str, analise: Dict, contexto: Contexto, is_blocking: bool,
275
- is_privileged: bool = False, is_reply: bool = False, mensagem_citada: str = '') -> str:
276
  import datetime
277
  historico = contexto.obter_historico()
278
  historico_texto = '\n'.join([f"Usuário: {m[0]}\nAkira: {m[1]}" for m in historico])
279
 
280
  now = datetime.datetime.now()
281
  data_hora = now.strftime('%d/%m/%Y %H:%M')
282
-
283
- # --- RETIFICADO: LÓGICA DE ATIVAÇÃO INTELIGENTE DE BUSCA ---
284
- web_search_context = ""
285
-
286
- # Keywords que sugerem necessidade de informação em tempo real ou muito específica
287
- trigger_keywords = ['hoje', 'agora', 'recente', 'último', 'presidente', 'notícias', 'quem é', 'o que é', 'onde está', 'quando termina']
288
-
289
- # Combina a mensagem atual e a citada para a decisão de busca e query
290
- # Normaliza para minúsculas
291
- search_query = f"{mensagem} {mensagem_citada}".strip().lower()
292
-
293
- # Decisão de busca:
294
- should_search = self.web_search and (
295
- # 1. Se for uma pergunta muito curta e específica (ex: "quem é o presidente?" - até 5 palavras)
296
- (len(search_query.split()) < 5 and any(q in search_query for q in ['quem', 'o que é', 'onde'])) or
297
- # 2. Se contiver uma palavra-chave de tempo real/especificidade
298
- any(k in search_query for k in trigger_keywords)
299
- )
300
-
301
- if should_search:
302
- try:
303
- # Usa a mensagem atual (ou a combinada) como query para a pesquisa genérica
304
- # Usa a mensagem original (sem a citada) para a query ser mais limpa, se houver
305
- query_limpa = mensagem.strip() if mensagem.strip() else mensagem_citada.strip()
306
-
307
- if query_limpa:
308
- self.logger.info(f"Executando WebSearch com query: {query_limpa[:50]}...")
309
- # **MUDANÇA AQUI:** Chama o método genérico `pesquisar(query)`
310
- search_results = self.web_search.pesquisar(query_limpa)
311
-
312
- if search_results:
313
- # **MUDANÇA AQUI:** Injeta os resultados com o novo rótulo
314
- web_search_context = f"\n# FONTE DE DADOS EM TEMPO REAL:\n{search_results}\n"
315
- except Exception as e:
316
- self.logger.warning(f"Falha ao executar WebSearch: {e}")
317
- # -------------------------------------------------------------
318
-
319
 
320
  strict_override = (
321
  "STRICT_OVERRIDES:\n"
@@ -337,8 +335,7 @@ class AkiraAPI:
337
  regras = '\n'.join(getattr(self.config, 'REGRAS', []))
338
  filtros = '\n'.join(getattr(self.config, 'FILTERS', []))
339
  system_part += f"# Regras:\n{regras}\n# Filtros:\n{filtros}\n"
340
- system_part += web_search_context # Injeta os resultados da busca com o novo rótulo
341
-
342
  extra_instructions = []
343
  if is_privileged:
344
  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.")
@@ -356,23 +353,19 @@ class AkiraAPI:
356
  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")
357
  parts.append(f"### Contexto ###\n{historico_texto}\n\n")
358
  parts.append(f"### Mensagem ###\n{analise.get('texto_normalizado', mensagem)}\n\n")
359
-
360
- # --- NOVO: Adiciona o contexto da mensagem citada ---
361
- if is_reply and mensagem_citada:
362
- parts.append(f"### Mensagem original (reply) ###\n{mensagem_citada}\n\n")
363
- # ---------------------------------------------------
364
-
365
  parts.append(f"### Instruções ###\n{getattr(self.config, 'INSTRUCTIONS', '')}\n\n")
366
  parts.append("Akira:\n")
367
  user_part = ''.join(parts)
368
 
369
  prompt = f"[SYSTEM]\n{system_part}\n[/SYSTEM]\n[USER]\n{user_part}\n[/USER]"
370
  return prompt
371
- # --------------------------------------
372
 
373
  def _generate_response(self, prompt: str) -> str:
374
  try:
375
- max_tokens = getattr(self.config, 'MAX_TOKENS', 300)
 
376
  temperature = getattr(self.config, 'TEMPERATURE', 0.8)
377
  text = self.providers.generate(prompt, max_tokens=max_tokens, temperature=temperature)
378
  return self._clean_response(text, prompt)
@@ -397,15 +390,11 @@ class AkiraAPI:
397
  cleaned = re.sub(r'\[([^\]]+)\]', r'\1', cleaned)
398
  cleaned = re.sub(r'<[^>]+>', '', cleaned)
399
 
400
- # Remove linhas longas que parecem lixo ou repetição, mantendo apenas as primeiras 2-3 sentenças
401
  sentences = re.split(r'(?<=[.!?])\s+', cleaned)
 
 
402
 
403
- # O clean_response no início do ficheiro original já tinha uma lógica mais complexa,
404
- # vamos garantir que ela seja mantida e aprimorada
405
- # Se houver mais de 3 frases, vamos limitar a 3 (para manter a resposta concisa como as regras pedem)
406
- if len(sentences) > 3:
407
- cleaned = ' '.join(sentences[:3]).strip()
408
-
409
  sports_keywords = ['futebol', 'girabola', 'petro', 'jogo', 'partida', 'contrata', 'campeonato', 'liga']
410
  try:
411
  prompt_text = (prompt or '').lower()
@@ -421,9 +410,9 @@ class AkiraAPI:
421
 
422
  max_chars = getattr(self.config, 'MAX_RESPONSE_CHARS', None)
423
  if not max_chars:
424
- max_chars = getattr(self.config, 'MAX_TOKENS', 300) * 4
 
425
 
426
- # Remove negrito restante de palavras únicas/nomes próprios para evitar formatação
427
  cleaned = re.sub(r"\*{0,2}([A-ZÀ-Ÿ][a-zà-ÿ]+\s+[A-ZÀ-Ÿ][a-zà-ÿ]+)\*{0,2}", r"\1", cleaned)
428
  return cleaned[:max_chars]
429
 
@@ -437,18 +426,11 @@ class AkiraAPI:
437
  except Exception as e:
438
  self.logger.exception(f"Falha ao iniciar treinador periódico: {e}")
439
 
440
- # A função 'responder' também foi atualizada para aceitar mensagem_citada
441
- def responder(self, mensagem: str, numero: str, nome: str = 'Usuário', mensagem_citada: str = '') -> str:
442
  contexto = self._get_user_context(nome)
443
  analise = contexto.analisar_intencao_e_normalizar(mensagem, contexto.obter_historico())
444
-
445
- # Passa a mensagem citada para o build_prompt
446
- # is_reply é true se mensagem_citada não for vazia
447
- is_reply = bool(mensagem_citada.strip())
448
-
449
- prompt = self._build_prompt(nome, numero, mensagem, analise, contexto, is_blocking=False,
450
- is_reply=is_reply, mensagem_citada=mensagem_citada)
451
-
452
  resposta = self._generate_response(prompt)
453
  contexto.atualizar_contexto(mensagem, resposta)
454
  return resposta
 
25
  from .treinamento import Treinamento
26
  from .exemplos_naturais import ExemplosNaturais
27
 
 
28
  try:
29
+ from .local_llm import LlamaLLM # IMPORTADO: LlamaLLM (Modelo Local/HF)
30
+ local_llm_available = True
 
31
  except ImportError:
32
+ local_llm_available = False
33
+ logger.warning("LlamaLLM não disponível. Modelo local desabilitado.")
34
+
35
 
36
  try:
37
  from mistralai import Mistral
38
  mistral_available = True
39
  except ImportError:
40
  mistral_available = False
41
+ logger = logging.getLogger("akira.api")
42
 
 
43
 
44
  try:
45
  import google.generativeai as genai
 
50
 
51
 
52
  class LLMManager:
53
+ """Gerenciador de provedores LLM (Local -> Mistral -> Gemini como fallback)."""
54
 
55
  def __init__(self, config):
56
  self.config = config
57
  self.mistral_client = None
58
  self.gemini_model = None
59
+ self.local_llm = None # NOVO: Atributo para o modelo local
60
  self._setup_providers()
61
 
62
  def _setup_providers(self):
63
+ # 1. SETUP LLAMA LOCAL (PRIORIDADE 1)
64
+ if local_llm_available:
65
+ try:
66
+ self.local_llm = LlamaLLM()
67
+ if not self.local_llm.is_available():
68
+ self.local_llm = None
69
+ logger.warning("LlamaLLM carregado mas não está disponível/operacional.")
70
+ else:
71
+ logger.info("LlamaLLM (Local/HF) inicializado como primário.")
72
+ except Exception as e:
73
+ logger.warning(f"Falha ao inicializar LlamaLLM: {e}")
74
+ self.local_llm = None
75
+
76
+ # 2. SETUP MISTRAL API (PRIORIDADE 2)
77
  if mistral_available and getattr(self.config, 'MISTRAL_API_KEY', None):
78
  try:
79
  self.mistral_client = Mistral(api_key=self.config.MISTRAL_API_KEY)
 
81
  except Exception as e:
82
  logger.warning(f"Falha ao inicializar Mistral: {e}")
83
 
84
+ # 3. SETUP GEMINI API (PRIORIDADE 3)
85
  if gemini_available and getattr(self.config, 'GEMINI_API_KEY', None):
86
  try:
87
  genai.configure(api_key=self.config.GEMINI_API_KEY)
88
+ # CORRIGIDO: O modelo agora é lido da configuração (gemini-2.5-flash)
89
+ self.gemini_model = genai.GenerativeModel(getattr(self.config, 'GEMINI_MODEL', 'gemini-2.5-flash')) # type: ignore[reportAttributeAccessIssue]
90
  logger.info("Gemini model inicializado.")
91
  except Exception as e:
92
  logger.warning(f"Falha ao inicializar Gemini: {e}")
93
 
94
  def generate(self, prompt: str, max_tokens: int = 300, temperature: float = 0.8) -> str:
95
+ # NOVA ORDEM DE PRIORIDADE
96
+ providers = ['local', 'mistral', 'gemini']
97
 
98
  for provider in providers:
99
+
100
+ # PRIORITY 1: LOCAL LLM (Llama/Mistral-7B)
101
+ if provider == 'local' and self.local_llm and self.local_llm.is_available():
102
+ try:
103
+ response = self.local_llm.generate(prompt, max_tokens=max_tokens, temperature=temperature)
104
+ if response:
105
+ logger.info("Resposta gerada por: LlamaLLM (Local)")
106
+ return response
107
+ logger.warning("LlamaLLM gerou resposta vazia, tentando próximo provedor.")
108
+ except Exception as e:
109
+ logger.warning(f"LlamaLLM (Local) falhou: {e}")
110
+
111
+
112
+ # PRIORITY 2: MISTRAL API
113
+ elif provider == 'mistral' and self.mistral_client:
114
  try:
115
  response = self.mistral_client.chat.complete(
116
  model=getattr(self.config, 'MISTRAL_MODEL', 'mistral-small-latest'),
 
119
  temperature=temperature
120
  )
121
  content = response.choices[0].message.content if response.choices else ""
122
+ if content:
123
+ logger.info("Resposta gerada por: Mistral API")
124
+ return str(content)
125
+
126
+ # Lógica de Retry
127
+ logger.warning("Mistral API gerou resposta vazia, tentando próximo provedor.")
128
+
129
  except Exception as e:
130
  error_msg = str(e).lower()
131
  if "429" in error_msg or "too many requests" in error_msg or "service tier capacity exceeded" in error_msg:
 
139
  temperature=temperature
140
  )
141
  content = response.choices[0].message.content if response.choices else ""
142
+ if content:
143
+ logger.info("Resposta gerada por: Mistral API (Retry)")
144
+ return str(content)
145
  except Exception as e2:
146
  logger.warning(f"Mistral retry failed: {e2}")
147
  else:
148
  logger.warning(f"Mistral falhou: {e}")
149
 
150
+ # PRIORITY 3: GEMINI API
151
  elif provider == 'gemini' and self.gemini_model:
152
  try:
153
  response = self.gemini_model.generate_content(
 
158
  }
159
  )
160
  text = response.text
161
+ if text:
162
+ logger.info("Resposta gerada por: Gemini API")
163
+ return text.strip()
164
+
165
+ # Lógica de Retry
166
+ logger.warning("Gemini API gerou resposta vazia, tentando fallback.")
167
+
168
  except Exception as e:
169
  error_msg = str(e).lower()
170
+ if "429" in error_msg or "too many requests" in error_msg or "quota exceeded" in error_msg or "404" in error_msg:
171
+ logger.warning(f"Gemini error/rate limit, retrying in 1s: {e}")
172
  time.sleep(1)
173
  try:
174
  response = self.gemini_model.generate_content(
 
179
  }
180
  )
181
  text = response.text
182
+ if text:
183
+ logger.info("Resposta gerada por: Gemini API (Retry)")
184
+ return text.strip()
185
  except Exception as e2:
186
  logger.warning(f"Gemini retry failed: {e2}")
187
  else:
188
  logger.warning(f"Gemini falhou: {e}")
189
 
190
+ logger.error("Todos os provedores (Local, Mistral, Gemini) falharam")
191
  return getattr(self.config, 'FALLBACK_RESPONSE', 'Desculpa, puto, o modelo tá off hoje. Tenta depois!')
192
 
193
 
194
  class SimpleTTLCache:
195
+ # ... (restante da classe SimpleTTLCache, inalterada)
196
  def __init__(self, ttl_seconds: int = 300):
197
  self.ttl = ttl_seconds
198
  self._store = {}
 
221
  self.config = cfg_module
222
  self.app = Flask(__name__)
223
  self.api = Blueprint("akira_api", __name__)
224
+ # Memoria MAX também é usado como TTL para o cache
225
  self.contexto_cache = SimpleTTLCache(ttl_seconds=getattr(self.config, 'MEMORIA_MAX', 300))
226
+ self.providers = LLMManager(self.config) # Usa o novo LLMManager com prioridades
227
  self.exemplos = ExemplosNaturais()
228
  self.logger = logger
 
 
 
 
229
 
230
  self._setup_personality()
231
  self._setup_routes()
 
234
  self.app.register_blueprint(self.api, url_prefix="/api", name="akira_api_prefixed")
235
  self.app.register_blueprint(self.api, url_prefix="", name="akira_api_root")
236
 
237
+ # ... (restante da classe AkiraAPI, inalterada)
238
  def _setup_personality(self):
239
  self.humor = getattr(self.config, 'HUMOR_INICIAL', 'neutra')
240
  self.interesses = list(getattr(self.config, 'INTERESSES', []))
 
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
 
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
 
 
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"
 
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.")
 
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 agora é 1000 pelo config.py
368
+ max_tokens = getattr(self.config, 'MAX_TOKENS', 300)
369
  temperature = getattr(self.config, 'TEMPERATURE', 0.8)
370
  text = self.providers.generate(prompt, max_tokens=max_tokens, temperature=temperature)
371
  return self._clean_response(text, prompt)
 
390
  cleaned = re.sub(r'\[([^\]]+)\]', r'\1', cleaned)
391
  cleaned = re.sub(r'<[^>]+>', '', cleaned)
392
 
393
+ # Lógica de limite de sentenças (mantida 2 sentenças como regra de persona)
394
  sentences = re.split(r'(?<=[.!?])\s+', cleaned)
395
+ if len(sentences) > 2:
396
+ cleaned = ' '.join(sentences[:2]).strip()
397
 
 
 
 
 
 
 
398
  sports_keywords = ['futebol', 'girabola', 'petro', 'jogo', 'partida', 'contrata', 'campeonato', 'liga']
399
  try:
400
  prompt_text = (prompt or '').lower()
 
410
 
411
  max_chars = getattr(self.config, 'MAX_RESPONSE_CHARS', None)
412
  if not max_chars:
413
+ # Usa o novo MAX_TOKENS (1000) * 4 para limite de caracteres, garantindo que a resposta não seja cortada
414
+ max_chars = getattr(self.config, 'MAX_TOKENS', 300) * 4
415
 
 
416
  cleaned = re.sub(r"\*{0,2}([A-ZÀ-Ÿ][a-zà-ÿ]+\s+[A-ZÀ-Ÿ][a-zà-ÿ]+)\*{0,2}", r"\1", cleaned)
417
  return cleaned[:max_chars]
418
 
 
426
  except Exception as e:
427
  self.logger.exception(f"Falha ao iniciar treinador periódico: {e}")
428
 
429
+ def responder(self, mensagem: str, numero: str, nome: str = 'Usuário') -> str:
430
+ data = {'usuario': nome, 'numero': numero, 'mensagem': mensagem}
431
  contexto = self._get_user_context(nome)
432
  analise = contexto.analisar_intencao_e_normalizar(mensagem, contexto.obter_historico())
433
+ prompt = self._build_prompt(nome, numero, mensagem, analise, contexto, is_blocking=False)
 
 
 
 
 
 
 
434
  resposta = self._generate_response(prompt)
435
  contexto.atualizar_contexto(mensagem, resposta)
436
  return resposta