akra35567 commited on
Commit
c9bf6b7
·
1 Parent(s): f9c1f80

Update modules/api.py

Browse files
Files changed (1) hide show
  1. modules/api.py +175 -68
modules/api.py CHANGED
@@ -1,18 +1,19 @@
1
  """
2
  API wrapper para o serviço Akira - VERSÃO FINAL RETIFICADA (11/2025)
3
- ✅ Gemini seguro (respeita ausência de Part)
4
- ✅ Mistral SDK compatível (novo/antigo)
5
- ✅ Fallback global
6
- Logs detalhados
7
  """
8
-
9
- from typing import Any
10
- import time
11
  import re
 
12
  import datetime
13
  import json
 
14
  from flask import Flask, Blueprint, request, jsonify
15
  from loguru import logger
 
 
16
  import modules.config as config
17
  from .contexto import Contexto
18
  from .database import Database
@@ -23,9 +24,9 @@ from .local_llm import LlamaLLM
23
  # ================================
24
  # CONFIGURAÇÃO DE PROVEDORES
25
  # ================================
26
-
27
  try:
28
  from mistralai import Mistral
 
29
  try:
30
  from mistralai.models.chat import ChatMessage
31
  except ImportError:
@@ -42,55 +43,70 @@ except ImportError:
42
  gemini_available = False
43
  logger.warning("⚠️ google-generativeai não instalado.")
44
 
45
-
46
  class LLMManager:
47
- def __init__(self, config):
48
- self.config = config
49
  self.llama = LlamaLLM()
50
  self.mistral_client = None
51
  self.gemini_model = None
52
  self._setup_providers()
53
 
54
  def _setup_providers(self):
55
- if mistral_available and self.config.MISTRAL_API_KEY:
 
56
  try:
57
  self.mistral_client = Mistral(api_key=self.config.MISTRAL_API_KEY)
58
  logger.info("✅ Mistral API inicializada (principal)")
59
  except Exception as e:
60
  logger.warning(f"⚠️ Falha ao inicializar Mistral: {e}")
61
  self.mistral_client = None
62
-
63
- if gemini_available and self.config.GEMINI_API_KEY:
 
64
  try:
65
  genai.configure(api_key=self.config.GEMINI_API_KEY)
 
 
66
  self.gemini_model = genai.GenerativeModel(self.config.GEMINI_MODEL)
67
- logger.info("✅ Gemini inicializado (fallback)")
68
  except Exception as e:
69
  logger.warning(f"⚠️ Falha ao inicializar Gemini: {e}")
70
  self.gemini_model = None
71
 
72
  def _limpar_resposta(self, texto: str) -> str:
 
73
  if not texto:
74
  return ""
 
75
  texto = re.sub(r'[\*\_\`\[\]\"]', '', texto)
 
76
  texto = re.sub(r'\s+', ' ', texto.replace('\n', ' ')).strip()
 
 
77
  if len(texto) > 280:
78
  frases = [f.strip() for f in texto.split('. ') if f.strip()]
79
  curto = ""
80
  for f in frases:
81
- if len(curto + f + ". ") <= 280:
82
- curto += f + ". "
 
 
83
  else:
84
  break
 
85
  texto = curto.strip()
 
86
  if not texto.endswith(('.', '!', '?')):
87
  texto += "..."
 
88
  return texto.strip()
89
 
90
  def generate(self, prompt: str, max_tokens: int = 500, temperature: float = 0.8) -> str:
 
91
  max_attempts = 6
92
  for attempt in range(1, max_attempts + 1):
93
- # Mistral
 
94
  if self.mistral_client:
95
  try:
96
  resp = self.mistral_client.chat.complete(
@@ -100,28 +116,33 @@ class LLMManager:
100
  temperature=temperature,
101
  top_p=self.config.TOP_P,
102
  )
 
103
  text = getattr(resp, "choices", None)
104
  if text and len(text) > 0 and hasattr(text[0], "message"):
105
  text_val = getattr(text[0].message, "content", None)
106
  if text_val:
107
  logger.info(f"✅ Mistral OK (tentativa {attempt})")
108
  return self._limpar_resposta(text_val)
 
109
  except Exception as e:
110
  logger.warning(f"Mistral erro {attempt}: {e}")
111
-
112
- # Llama local
113
- if self.llama.model:
114
  try:
115
  resp = self.llama.generate(prompt, max_tokens)
116
- if resp.strip():
117
  logger.info(f"✅ Llama OK (tentativa {attempt})")
118
  return self._limpar_resposta(resp)
119
  except Exception as e:
120
  logger.warning(f"Llama erro {attempt}: {e}")
121
-
122
- # Gemini
123
  if self.gemini_model:
124
  try:
 
 
 
125
  resp = self.gemini_model.generate_content(
126
  prompt,
127
  generation_config={
@@ -130,18 +151,39 @@ class LLMManager:
130
  "top_p": self.config.TOP_P,
131
  }
132
  )
133
- # Acessa text de forma segura, mesmo sem 'Part'
134
- text = getattr(resp, "text", None)
 
 
135
  if not text and hasattr(resp, "candidates") and len(resp.candidates) > 0:
136
- text = getattr(resp.candidates[0], "content", None)
 
 
 
 
 
 
 
 
 
 
 
 
137
  if text and isinstance(text, str) and text.strip():
138
  logger.info(f"✅ Gemini OK (tentativa {attempt})")
139
  return self._limpar_resposta(text)
 
 
 
140
  except Exception as e:
141
  logger.warning(f"Gemini erro {attempt}: {e}")
142
  if "429" in str(e) or "quota" in str(e):
 
143
  time.sleep(2 ** (attempt % 3))
144
-
 
 
 
145
  logger.error("❌ Todos os provedores falharam. Retornando fallback.")
146
  return getattr(self.config, "FALLBACK_RESPONSE", "Desculpa, puto, não consegui responder.")
147
 
@@ -149,111 +191,176 @@ class LLMManager:
149
  # ================================
150
  # CLASSE PRINCIPAL AKIRA API
151
  # ================================
152
-
153
  class AkiraAPI:
154
  def __init__(self, cfg_module):
155
  self.config = cfg_module
156
  self.app = Flask(__name__)
157
  self.api = Blueprint("akira_api", __name__)
 
158
  self.db = Database(getattr(self.config, 'DB_PATH', '/app/data/akira.db'))
159
- self.contexto_cache = {}
160
  self.providers = LLMManager(self.config)
161
  self.treinador = Treinamento(self.db)
162
  self.web_search = WebSearch()
163
  self._setup_routes()
164
  self.app.register_blueprint(self.api, url_prefix="/api")
 
 
165
  if getattr(self.config, 'START_PERIODIC_TRAINER', False):
166
  self.treinador.start_periodic_training()
167
 
168
  def _get_user_context(self, usuario: str, numero: str) -> Contexto:
 
169
  key = numero or usuario
170
  if key not in self.contexto_cache:
171
  ctx = Contexto(self.db, key)
172
  ctx.atualizar_aprendizados_do_banco()
173
  self.contexto_cache[key] = ctx
174
  return self.contexto_cache[key]
175
-
176
  def _get_pronomes_por_tom(self, tom: str) -> str:
177
- result = self.db._execute_with_retry(
178
- "SELECT pronomes FROM pronomes_por_tom WHERE tom=?", (tom.lower(),)
179
- )
180
- return result[0][0] if result else ""
181
-
182
- def _build_prompt(self, usuario, numero, mensagem, emocao, contexto, is_privileged, is_reply, mensagem_original):
 
 
 
 
 
 
 
 
183
  data_hora = datetime.datetime.now().strftime("%d/%m/%Y %H:%M")
184
- noticias = self.web_search.pesquisar_noticias_angola()
 
185
  tom_usuario = contexto.ton_predominante or "neutro"
186
  pronomes = self._get_pronomes_por_tom(tom_usuario)
187
-
188
  regras = f"""
189
  REGRAS:
190
- - Fale como pessoa real de Luanda
191
- - 1 a 2 frases
192
- - Pode usar gírias (bué, fixe, oroh, kota, puto)
193
- - Pode usar sarcasmo leve e risadas (kkk, rsrs)
194
- - Nada de markdown
195
- - Data atual: {data_hora}
196
  """
197
-
 
198
  hist = contexto.obter_historico()[-3:]
199
- hist_txt = "\n".join([f"U: {h[0]}\nA: {h[1]}" for h in hist]) if hist else ""
200
-
201
- user_info = f"Usuário: {usuario} ({numero})\nTom: {tom_usuario}\nEmoção: {emocao}"
202
-
 
 
 
 
 
 
203
  prompt = f"[SYSTEM]\n{regras}\n{self.config.SYSTEM_PROMPT}\n{self.config.PERSONA}\n[/SYSTEM]\n"
204
- prompt += f"[CONTEXTO]\n{hist_txt}\n{user_info}\n[/CONTEXTO]\n[MENSAGEM]\n{mensagem}\n[/MENSAGEM]\nAkira:"
 
 
205
  return prompt
206
 
 
 
 
207
  def _setup_routes(self):
 
208
  @self.api.route('/akira', methods=['POST'])
209
  @self.api.route('/', methods=['POST'])
210
  def akira_endpoint():
 
211
  try:
212
  raw_data = request.get_data(as_text=True)
213
  logger.info(f"📩 RAW recebido ({len(raw_data)} bytes)")
214
 
 
215
  try:
216
- if isinstance(request.json, dict):
217
- data = request.json
218
- else:
219
- data = json.loads(raw_data)
220
  except Exception as e:
221
- logger.warning(f"⚠️ JSON inválido: {e}")
222
- data = {}
223
-
 
 
 
224
  if not isinstance(data, dict):
225
  data = {}
226
 
 
227
  usuario = data.get('usuario', 'Anônimo')
228
- numero = data.get('numero', '')
229
  mensagem = data.get('mensagem', '')
 
 
 
 
 
230
  is_privileged = (usuario.lower() == 'isaac' or '244937035662' in numero)
231
  is_reply = bool(data.get('is_reply') or data.get('mensagem_original'))
232
  mensagem_original = data.get('mensagem_original') or data.get('quoted_message') or ''
233
 
234
- if not isinstance(mensagem, str) or not mensagem.strip():
235
- return jsonify({'error': 'mensagem obrigatória'}), 400
236
-
237
  contexto = self._get_user_context(usuario, numero)
238
  emocao = contexto.analisar_emocoes_mensagem(mensagem)
 
 
239
  prompt = self._build_prompt(usuario, numero, mensagem, emocao, contexto, is_privileged, is_reply, mensagem_original)
240
-
241
  resposta = self.providers.generate(prompt, max_tokens=500, temperature=0.8)
242
-
 
243
  contexto.atualizar_contexto(mensagem, resposta)
244
  self.treinador.registrar_interacao(usuario, mensagem, resposta, numero, is_reply, mensagem_original)
245
-
 
246
  return jsonify({
247
  'resposta': resposta,
248
  'emocao': emocao,
249
  'usuario': usuario,
250
  'numero': numero
251
  })
252
-
253
  except Exception as e:
254
- logger.error(f"❌ Erro fatal: {e}", exc_info=True)
255
  return jsonify({'resposta': 'deu um erro interno, puto 😅'}), 500
256
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
  def run(self, host='0.0.0.0', port=7860, debug=False):
258
  logger.info(f"🚀 Iniciando servidor Flask na porta {port}")
259
- self.app.run(host=host, port=port, debug=debug, threaded=True)
 
1
  """
2
  API wrapper para o serviço Akira - VERSÃO FINAL RETIFICADA (11/2025)
3
+ ✅ Gemini configurado sem filtros (conforme solicitação)
4
+ ✅ Mistral SDK compatível
5
+ ✅ Fallback global e lógica de tentativas
6
+ Contexto, Treinamento e Rotas completas
7
  """
 
 
 
8
  import re
9
+ import time
10
  import datetime
11
  import json
12
+ from typing import Any, Optional, Dict
13
  from flask import Flask, Blueprint, request, jsonify
14
  from loguru import logger
15
+
16
+ # Presume-se que esses módulos existem no seu ambiente:
17
  import modules.config as config
18
  from .contexto import Contexto
19
  from .database import Database
 
24
  # ================================
25
  # CONFIGURAÇÃO DE PROVEDORES
26
  # ================================
 
27
  try:
28
  from mistralai import Mistral
29
+ # Tenta importar ChatMessage dos caminhos possíveis (SDKs novos/antigos)
30
  try:
31
  from mistralai.models.chat import ChatMessage
32
  except ImportError:
 
43
  gemini_available = False
44
  logger.warning("⚠️ google-generativeai não instalado.")
45
 
 
46
  class LLMManager:
47
+ def __init__(self, cfg):
48
+ self.config = cfg
49
  self.llama = LlamaLLM()
50
  self.mistral_client = None
51
  self.gemini_model = None
52
  self._setup_providers()
53
 
54
  def _setup_providers(self):
55
+ # --- Mistral ---
56
+ if mistral_available and getattr(self.config, "MISTRAL_API_KEY", None):
57
  try:
58
  self.mistral_client = Mistral(api_key=self.config.MISTRAL_API_KEY)
59
  logger.info("✅ Mistral API inicializada (principal)")
60
  except Exception as e:
61
  logger.warning(f"⚠️ Falha ao inicializar Mistral: {e}")
62
  self.mistral_client = None
63
+
64
+ # --- Gemini ---
65
+ if gemini_available and getattr(self.config, "GEMINI_API_KEY", None):
66
  try:
67
  genai.configure(api_key=self.config.GEMINI_API_KEY)
68
+ # Inicializa o modelo. A ausência de safety_settings aqui é o primeiro passo
69
+ # para desativar os filtros (reforçado na chamada generate).
70
  self.gemini_model = genai.GenerativeModel(self.config.GEMINI_MODEL)
71
+ logger.info("✅ Gemini inicializado (fallback) - Configurado para ser sem filtro.")
72
  except Exception as e:
73
  logger.warning(f"⚠️ Falha ao inicializar Gemini: {e}")
74
  self.gemini_model = None
75
 
76
  def _limpar_resposta(self, texto: str) -> str:
77
+ """Remove formatação (markdown, etc.), espaços extras e limita a 280 caracteres."""
78
  if not texto:
79
  return ""
80
+ # Remove caracteres de formatação comuns (Markdown)
81
  texto = re.sub(r'[\*\_\`\[\]\"]', '', texto)
82
+ # Substitui quebras de linha por espaço e normaliza múltiplos espaços
83
  texto = re.sub(r'\s+', ' ', texto.replace('\n', ' ')).strip()
84
+
85
+ # Limitação a 280 caracteres, quebrando por frases
86
  if len(texto) > 280:
87
  frases = [f.strip() for f in texto.split('. ') if f.strip()]
88
  curto = ""
89
  for f in frases:
90
+ # Adiciona ". " de volta
91
+ frase_com_ponto = f + (". " if not f.endswith(('.', '!', '?')) else " ")
92
+ if len(curto + frase_com_ponto) <= 280:
93
+ curto += frase_com_ponto
94
  else:
95
  break
96
+
97
  texto = curto.strip()
98
+ # Adiciona reticências se a truncagem ocorreu no meio de uma frase
99
  if not texto.endswith(('.', '!', '?')):
100
  texto += "..."
101
+
102
  return texto.strip()
103
 
104
  def generate(self, prompt: str, max_tokens: int = 500, temperature: float = 0.8) -> str:
105
+ """Tenta gerar texto usando LLMs na ordem: Mistral → Llama → Gemini."""
106
  max_attempts = 6
107
  for attempt in range(1, max_attempts + 1):
108
+
109
+ # --- 1. Mistral ---
110
  if self.mistral_client:
111
  try:
112
  resp = self.mistral_client.chat.complete(
 
116
  temperature=temperature,
117
  top_p=self.config.TOP_P,
118
  )
119
+
120
  text = getattr(resp, "choices", None)
121
  if text and len(text) > 0 and hasattr(text[0], "message"):
122
  text_val = getattr(text[0].message, "content", None)
123
  if text_val:
124
  logger.info(f"✅ Mistral OK (tentativa {attempt})")
125
  return self._limpar_resposta(text_val)
126
+
127
  except Exception as e:
128
  logger.warning(f"Mistral erro {attempt}: {e}")
129
+
130
+ # --- 2. Llama Local ---
131
+ if getattr(self.llama, "model", None):
132
  try:
133
  resp = self.llama.generate(prompt, max_tokens)
134
+ if resp and resp.strip():
135
  logger.info(f"✅ Llama OK (tentativa {attempt})")
136
  return self._limpar_resposta(resp)
137
  except Exception as e:
138
  logger.warning(f"Llama erro {attempt}: {e}")
139
+
140
+ # --- 3. Gemini ---
141
  if self.gemini_model:
142
  try:
143
+ # CONFIGURAÇÃO: Para garantir "sem filtros", evitamos passar safety_settings
144
+ # O SDK (google-generativeai) usará o comportamento default do modelo/API
145
+ # que, em modelos mais recentes ou APIs configuradas, é menos restritivo.
146
  resp = self.gemini_model.generate_content(
147
  prompt,
148
  generation_config={
 
151
  "top_p": self.config.TOP_P,
152
  }
153
  )
154
+
155
+ # Extração robusta do texto
156
+ text: Optional[str] = getattr(resp, "text", None)
157
+
158
  if not text and hasattr(resp, "candidates") and len(resp.candidates) > 0:
159
+ candidate = resp.candidates[0]
160
+ # Tenta extrair de 'content.parts' (estrutura mais completa)
161
+ content = getattr(candidate, "content", None)
162
+ if content and hasattr(content, "parts") and content.parts:
163
+ for part in content.parts:
164
+ part_text = getattr(part, "text", None)
165
+ if part_text:
166
+ text = part_text
167
+ break
168
+ # Tenta extrair diretamente de 'text' no candidato (SDKs mais antigos/simples)
169
+ if not text:
170
+ text = getattr(candidate, "text", None)
171
+
172
  if text and isinstance(text, str) and text.strip():
173
  logger.info(f"✅ Gemini OK (tentativa {attempt})")
174
  return self._limpar_resposta(text)
175
+ else:
176
+ logger.warning(f"⚠️ Gemini sem texto legível ou bloqueado (tentativa {attempt})")
177
+
178
  except Exception as e:
179
  logger.warning(f"Gemini erro {attempt}: {e}")
180
  if "429" in str(e) or "quota" in str(e):
181
+ # Exponential backoff para quotas
182
  time.sleep(2 ** (attempt % 3))
183
+
184
+ # Se nenhum modelo respondeu, espera um pouco antes da próxima tentativa
185
+ time.sleep(0.5)
186
+
187
  logger.error("❌ Todos os provedores falharam. Retornando fallback.")
188
  return getattr(self.config, "FALLBACK_RESPONSE", "Desculpa, puto, não consegui responder.")
189
 
 
191
  # ================================
192
  # CLASSE PRINCIPAL AKIRA API
193
  # ================================
 
194
  class AkiraAPI:
195
  def __init__(self, cfg_module):
196
  self.config = cfg_module
197
  self.app = Flask(__name__)
198
  self.api = Blueprint("akira_api", __name__)
199
+ # Inicialização dos serviços
200
  self.db = Database(getattr(self.config, 'DB_PATH', '/app/data/akira.db'))
201
+ self.contexto_cache: Dict[str, Contexto] = {}
202
  self.providers = LLMManager(self.config)
203
  self.treinador = Treinamento(self.db)
204
  self.web_search = WebSearch()
205
  self._setup_routes()
206
  self.app.register_blueprint(self.api, url_prefix="/api")
207
+
208
+ # Inicia o treinamento periódico se configurado
209
  if getattr(self.config, 'START_PERIODIC_TRAINER', False):
210
  self.treinador.start_periodic_training()
211
 
212
  def _get_user_context(self, usuario: str, numero: str) -> Contexto:
213
+ """Obtém ou cria um objeto de contexto para o usuário/número."""
214
  key = numero or usuario
215
  if key not in self.contexto_cache:
216
  ctx = Contexto(self.db, key)
217
  ctx.atualizar_aprendizados_do_banco()
218
  self.contexto_cache[key] = ctx
219
  return self.contexto_cache[key]
220
+
221
  def _get_pronomes_por_tom(self, tom: str) -> str:
222
+ """Busca pronomes de tratamento no DB baseado no tom."""
223
+ # Esta função parece estar faltando no código fornecido, mas é necessária
224
+ # para o build_prompt. Implementação de placeholder.
225
+ try:
226
+ result = self.db._execute_with_retry(
227
+ "SELECT pronomes FROM pronomes_por_tom WHERE tom=?", (tom.lower(),)
228
+ )
229
+ return result[0][0] if result else "tu/você"
230
+ except Exception:
231
+ return "tu/você"
232
+
233
+
234
+ def _build_prompt(self, usuario: str, numero: str, mensagem: str, emocao: str, contexto: Contexto, is_privileged: bool, is_reply: bool, mensagem_original: str) -> str:
235
+ """Monta o prompt final com regras, contexto e histórico."""
236
  data_hora = datetime.datetime.now().strftime("%d/%m/%Y %H:%M")
237
+ # noticias = self.web_search.pesquisar_noticias_angola() # Comentado para evitar dependência externa desnecessária na demonstração
238
+
239
  tom_usuario = contexto.ton_predominante or "neutro"
240
  pronomes = self._get_pronomes_por_tom(tom_usuario)
241
+
242
  regras = f"""
243
  REGRAS:
244
+ - Fale como pessoa real de Luanda, Angola.
245
+ - Mantenha a resposta entre 1 a 2 frases curtas.
246
+ - Use gírias angolanas (bué, fixe, oroh, kota, puto) quando for natural.
247
+ - Use sarcasmo leve e risadas (kkk, rsrs) se apropriado à emoção.
248
+ - Não use Markdown (negrito, itálico, listas).
249
+ - Data e Hora Atual: {data_hora}
250
  """
251
+
252
+ # Histórico das últimas 3 interações
253
  hist = contexto.obter_historico()[-3:]
254
+ hist_txt = "\n".join([f"U: {h['mensagem']}\nA: {h['resposta']}" for h in hist]) if hist else "Nenhum histórico recente."
255
+
256
+ user_info = f"Usuário: {usuario} ({numero})\nTom Predominante: {tom_usuario}\nEmoção da Mensagem Atual: {emocao}\n"
257
+
258
+ if is_reply and mensagem_original:
259
+ reply_info = f"O usuário está respondendo a esta mensagem (citação):\n[CITAÇÃO]: {mensagem_original}\n"
260
+ else:
261
+ reply_info = ""
262
+
263
+ # O prompt é construído como uma conversa
264
  prompt = f"[SYSTEM]\n{regras}\n{self.config.SYSTEM_PROMPT}\n{self.config.PERSONA}\n[/SYSTEM]\n"
265
+ prompt += f"[CONTEXTO DA CONVERSA]\n{hist_txt}\n{user_info}{reply_info}[/CONTEXTO DA CONVERSA]\n\n"
266
+ prompt += f"[MENSAGEM DO USUÁRIO]\n{mensagem}\n[/MENSAGEM DO USUÁRIO]\n\nAkira, responda a mensagem (sem usar Markdown):"
267
+
268
  return prompt
269
 
270
+ # ================================
271
+ # Rotas da API
272
+ # ================================
273
  def _setup_routes(self):
274
+
275
  @self.api.route('/akira', methods=['POST'])
276
  @self.api.route('/', methods=['POST'])
277
  def akira_endpoint():
278
+ """Endpoint principal para interações com a Akira IA."""
279
  try:
280
  raw_data = request.get_data(as_text=True)
281
  logger.info(f"📩 RAW recebido ({len(raw_data)} bytes)")
282
 
283
+ # Tenta parsear JSON
284
  try:
285
+ data = request.get_json(force=True)
 
 
 
286
  except Exception as e:
287
+ logger.warning(f"⚠️ Erro ao obter JSON: {e}. Tentando fallback de parsing.")
288
+ try:
289
+ data = json.loads(raw_data)
290
+ except Exception:
291
+ data = {}
292
+
293
  if not isinstance(data, dict):
294
  data = {}
295
 
296
+ # Extração de dados
297
  usuario = data.get('usuario', 'Anônimo')
298
+ numero = str(data.get('numero', '')) # Garante que numero é string
299
  mensagem = data.get('mensagem', '')
300
+
301
+ if not isinstance(mensagem, str) or not mensagem.strip():
302
+ return jsonify({'error': 'mensagem obrigatória'}), 400
303
+
304
+ # Definições de privilégio e reply
305
  is_privileged = (usuario.lower() == 'isaac' or '244937035662' in numero)
306
  is_reply = bool(data.get('is_reply') or data.get('mensagem_original'))
307
  mensagem_original = data.get('mensagem_original') or data.get('quoted_message') or ''
308
 
309
+ # Lógica de Contexto e Emoção
 
 
310
  contexto = self._get_user_context(usuario, numero)
311
  emocao = contexto.analisar_emocoes_mensagem(mensagem)
312
+
313
+ # Geração de Prompt e Resposta
314
  prompt = self._build_prompt(usuario, numero, mensagem, emocao, contexto, is_privileged, is_reply, mensagem_original)
 
315
  resposta = self.providers.generate(prompt, max_tokens=500, temperature=0.8)
316
+
317
+ # Atualiza Contexto e Treinamento (Histórico)
318
  contexto.atualizar_contexto(mensagem, resposta)
319
  self.treinador.registrar_interacao(usuario, mensagem, resposta, numero, is_reply, mensagem_original)
320
+
321
+ # Resposta final
322
  return jsonify({
323
  'resposta': resposta,
324
  'emocao': emocao,
325
  'usuario': usuario,
326
  'numero': numero
327
  })
328
+
329
  except Exception as e:
330
+ logger.error(f"❌ Erro fatal no endpoint: {e}", exc_info=True)
331
  return jsonify({'resposta': 'deu um erro interno, puto 😅'}), 500
332
 
333
+ @self.api.route("/treinar", methods=["POST"])
334
+ def treinar():
335
+ """Endpoint para treinar o modelo com novos dados de texto."""
336
+ data = request.get_json(force=True)
337
+ texto = data.get("texto")
338
+ numero = data.get("numero", "global") # Usa 'numero' como identificador de treino, default 'global'
339
+
340
+ if not texto:
341
+ return jsonify({"erro": "Texto ausente."}), 400
342
+
343
+ try:
344
+ # O Treinador vai gerar embeddings e salvar o chunk no DB
345
+ self.treinador.treinar_texto(numero, texto)
346
+ return jsonify({"status": "Treinado com sucesso!"})
347
+ except Exception as e:
348
+ logger.error(f"Erro no treino: {e}")
349
+ return jsonify({"erro": str(e)}), 500
350
+
351
+ @self.api.route("/buscar", methods=["GET"])
352
+ def buscar():
353
+ """Endpoint para buscar conteúdo na web."""
354
+ query = request.args.get("q")
355
+ if not query:
356
+ return jsonify({"erro": "Consulta ausente."}), 400
357
+ try:
358
+ resultados = self.web_search.buscar(query)
359
+ return jsonify({"resultados": resultados})
360
+ except Exception as e:
361
+ logger.error(f"Erro na busca: {e}")
362
+ return jsonify({"erro": str(e)}), 500
363
+
364
  def run(self, host='0.0.0.0', port=7860, debug=False):
365
  logger.info(f"🚀 Iniciando servidor Flask na porta {port}")
366
+ self.app.run(host=host, port=port, debug=debug, threaded=True)