akra35567 commited on
Commit
2e928fd
·
1 Parent(s): 5b9c255

Update modules/api.py

Browse files
Files changed (1) hide show
  1. modules/api.py +195 -246
modules/api.py CHANGED
@@ -1,314 +1,263 @@
1
  """
2
- API wrapper for Akira service.
3
- Integração mínima e robusta: config db contexto → LLM → resposta.
 
 
 
4
  """
 
 
5
  import time
6
  import re
7
  import datetime
8
- from typing import Dict, Optional, Any, List
 
9
  from flask import Flask, Blueprint, request, jsonify
10
  from loguru import logger
11
-
12
- # LLM PROVIDERS
13
- import google.generativeai as genai
14
- from mistralai.client import MistralClient
15
- from mistralai.types.chat import ChatMessage # CORREÇÃO: Caminho atualizado para SDK Mistral v1.0.0+
16
- from .local_llm import LlamaLLM
17
-
18
- # LOCAL MODULES
19
  from .contexto import Contexto
20
  from .database import Database
21
  from .treinamento import Treinamento
22
- from .exemplos_naturais import ExemplosNaturais
23
- import modules.config as config
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
 
26
  class LLMManager:
27
- def __init__(self, config_instance):
28
- self.config = config_instance
29
- self.mistral_client: Optional[MistralClient] = None
30
- self.gemini_model: Optional[genai.GenerativeModel] = None
31
- self.llama_llm = self._import_llama()
32
  self._setup_providers()
33
- self.providers = []
34
- if self.mistral_client: self.providers.append('mistral')
35
- if self.gemini_model: self.providers.append('gemini')
36
- if self.llama_llm and self.llama_llm.is_available(): self.providers.append('llama')
37
- if not self.providers:
38
- logger.error("Nenhum provedor LLM ativo.")
39
- else:
40
- logger.info(f"Provedores: {self.providers}")
41
-
42
- def _import_llama(self):
43
- try:
44
- return LlamaLLM()
45
- except:
46
- return None
47
 
48
  def _setup_providers(self):
49
- # MISTRAL
50
- if getattr(self.config, 'MISTRAL_API_KEY', '').startswith('m-'):
51
  try:
52
- self.mistral_client = MistralClient(api_key=self.config.MISTRAL_API_KEY)
53
- logger.info("Mistral API OK")
54
  except Exception as e:
55
- logger.warning(f"Mistral falhou: {e}")
56
  self.mistral_client = None
57
- else:
58
- logger.warning("Mistral desativado (chave inválida)")
59
 
60
- # GEMINI
61
- if getattr(self.config, 'GEMINI_API_KEY', '').startswith('AIza'):
62
  try:
63
  genai.configure(api_key=self.config.GEMINI_API_KEY)
64
- self.gemini_model = genai.GenerativeModel(
65
- model=self.config.GEMINI_MODEL,
66
- system_instruction=self.config.PERSONA + self.config.SYSTEM_PROMPT + " (NLP/Transformers otimizado)"
67
- )
68
- logger.info(f"Gemini OK: {self.config.GEMINI_MODEL}")
69
  except Exception as e:
70
- logger.warning(f"Gemini falhou: {e}")
71
  self.gemini_model = None
72
- else:
73
- logger.warning("Gemini desativado (chave inválida)")
74
-
75
- def generate(self, user_prompt: str, context_history: List[dict] = [], is_privileged: bool = False) -> str:
76
- full_system = self.config.PERSONA + self.config.SYSTEM_PROMPT + " (NLP/Transformers otimizado)"
77
- messages = [ChatMessage(role="system", content=full_system)]
78
- for turn in context_history:
79
- role = "user" if turn["role"] == "user" else "assistant"
80
- messages.append(ChatMessage(role=role, content=turn["content"]))
81
- messages.append(ChatMessage(role="user", content=user_prompt))
82
-
83
- for provider in self.providers:
84
- if provider == 'mistral' and self.mistral_client:
 
 
 
 
 
 
 
 
 
 
85
  try:
86
- resp = self.mistral_client.chat(
87
  model=self.config.MISTRAL_MODEL,
88
- messages=messages,
89
- temperature=self.config.TOP_P,
90
- max_tokens=self.config.MAX_TOKENS
 
91
  )
92
  text = resp.choices[0].message.content
93
  if text:
94
- logger.info("Mistral respondeu")
95
- return text.strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  except Exception as e:
97
- logger.warning(f"Mistral falhou: {e}")
98
 
99
- elif provider == 'gemini' and self.gemini_model:
100
  try:
101
- gemini_hist = []
102
- for msg in messages[1:]:
103
- # Note: ChatMessage from Mistral SDK doesn't have a direct 'role' attribute,
104
- # but we rely on the object's structure or convert it. Assuming 'role' and 'content' are present.
105
- role = "user" if msg.role == "user" else "model"
106
- gemini_hist.append({"role": role, "parts": [{"text": msg.content}]})
107
  resp = self.gemini_model.generate_content(
108
- gemini_hist,
109
- generation_config={"max_output_tokens": self.config.MAX_TOKENS, "temperature": self.config.TOP_P}
 
 
 
 
110
  )
111
  text = resp.text
112
  if text:
113
- logger.info("Gemini respondeu")
114
- return text.strip()
115
  except Exception as e:
116
- logger.warning(f"Gemini falhou: {e}")
 
 
117
 
118
- elif provider == 'llama' and self.llama_llm and self.llama_llm.is_available():
119
- try:
120
- local = self.llama_llm.generate(user_prompt, max_tokens=self.config.MAX_TOKENS, temperature=self.config.TOP_P)
121
- if local:
122
- logger.info("Llama respondeu")
123
- return local
124
- except Exception as e:
125
- logger.warning(f"Llama falhou: {e}")
126
-
127
- logger.error("Todos os LLMs falharam")
128
  return self.config.FALLBACK_RESPONSE
129
 
130
 
131
- # --- CACHE ---
132
- class SimpleTTLCache:
133
- def __init__(self, ttl_seconds: int = 300):
134
- self.ttl = ttl_seconds
135
- self._store = {}
136
-
137
- def __contains__(self, key):
138
- if key not in self._store: return False
139
- _, expires = self._store[key]
140
- if time.time() > expires:
141
- del self._store[key]
142
- return False
143
- return True
144
-
145
- def __setitem__(self, key, value):
146
- self._store[key] = (value, time.time() + self.ttl)
147
-
148
- def __getitem__(self, key):
149
- if key not in self: raise KeyError(key)
150
- return self._store[key][0]
151
-
152
 
153
- # --- AKIRA API ---
154
  class AkiraAPI:
155
  def __init__(self, cfg_module):
156
  self.config = cfg_module
157
  self.app = Flask(__name__)
158
  self.api = Blueprint("akira_api", __name__)
159
- self.contexto_cache = SimpleTTLCache(ttl_seconds=getattr(self.config, 'MEMORIA_MAX', 300))
 
160
  self.providers = LLMManager(self.config)
161
- self.exemplos = ExemplosNaturais()
162
- self.logger = logger
163
- self._setup_personality()
164
  self._setup_routes()
165
- self._setup_trainer()
166
- self.app.register_blueprint(self.api, url_prefix="/api", name="akira_api_prefixed")
167
- self.app.register_blueprint(self.api, url_prefix="", name="akira_api_root")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
 
169
- def _setup_personality(self):
170
- self.humor = getattr(self.config, 'HUMOR_INICIAL', 'neutra')
171
- self.interesses = list(getattr(self.config, 'INTERESSES', []))
172
- self.limites = list(getattr(self.config, 'LIMITES', []))
 
 
 
 
173
 
174
  def _setup_routes(self):
175
  @self.api.route('/akira', methods=['POST'])
 
176
  def akira_endpoint():
177
  try:
178
- data = request.get_json(force=True, silent=True) or {}
179
- usuario = data.get('usuario', 'anonimo')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  numero = data.get('numero', '')
181
  mensagem = data.get('mensagem', '')
182
- is_privileged = bool(data.get('is_privileged_user', False)) or usuario.lower() == 'isaac'
183
- is_reply = bool(data.get('is_reply') or data.get('mensagem_original') or data.get('quoted_message'))
184
  mensagem_original = data.get('mensagem_original') or data.get('quoted_message') or ''
185
 
186
- if not mensagem:
187
- return jsonify({'error': 'mensagem obrigatória'}), 400
188
 
189
- self.logger.info(f"{usuario} ({numero}): {mensagem[:120]}")
190
- contexto = self._get_user_context(usuario)
191
- analise = contexto.analisar_intencao_e_normalizar(mensagem, contexto.obter_historico())
192
- if usuario.lower() == 'isaac':
193
- analise['usar_nome'] = False
194
 
195
- is_blocking = len(mensagem) < 10 and any(k in mensagem.lower() for k in ['exec', 'bash', 'open', 'api_key', 'key'])
196
- prompt = self._build_prompt(usuario, numero, mensagem, analise, contexto, is_blocking,
197
- is_privileged=is_privileged, is_reply=is_reply, mensagem_original=mensagem_original)
198
 
199
- resposta = self._generate_response(prompt, contexto.obter_historico_para_llm(), is_privileged)
 
200
 
201
  contexto.atualizar_contexto(mensagem, resposta)
202
- try:
203
- db = Database(getattr(self.config, 'DB_PATH', 'akira.db'))
204
- trainer = Treinamento(db)
205
- trainer.registrar_interacao(usuario, mensagem, resposta, numero, is_reply, mensagem_original)
206
- except Exception as e:
207
- self.logger.warning(f"Registro falhou: {e}")
208
-
209
- response_data = {'resposta': resposta}
210
- try:
211
- aprendizados = contexto.obter_aprendizados()
212
- if aprendizados:
213
- response_data['aprendizados'] = aprendizados
214
- except Exception as e:
215
- self.logger.warning(f"Aprendizados falharam: {e}")
216
 
217
- return jsonify(response_data)
 
 
 
 
 
218
 
219
  except Exception as e:
220
- self.logger.exception('Erro no /akira')
221
- return jsonify({'resposta': getattr(self.config, 'FALLBACK_RESPONSE', 'Erro interno')}), 500
222
-
223
- @self.api.route('/health', methods=['GET'])
224
- def health_check():
225
- return 'OK', 200
226
-
227
- def _get_user_context(self, usuario: str) -> Contexto:
228
- if usuario not in self.contexto_cache:
229
- db = Database(getattr(self.config, 'DB_PATH', 'akira.db'))
230
- self.contexto_cache[usuario] = Contexto(db, user_key=usuario)
231
- return self.contexto_cache[usuario]
232
-
233
- def _build_prompt(self, usuario: str, numero: str, mensagem: str, analise: Dict, contexto: Contexto, is_blocking: bool,
234
- is_privileged: bool = False, is_reply: bool = False, mensagem_original: str = '') -> str:
235
- historico_raw = contexto.obter_historico()
236
- historico_texto = '\n'.join([f"Usuário: {m[0]}\nAkira: {m[1]}" for m in historico_raw])
237
- now = datetime.datetime.now()
238
- data_hora = now.strftime('%d/%m/%Y %H:%M')
239
-
240
- strict_override = (
241
- "STRICT_OVERRIDES:\n"
242
- "- NUNCA invente fatos ou mencione eventos/pessoas não presentes no contexto.\n"
243
- "- NÃO use negrito, markdown ou HTML.\n"
244
- "- Não quebre linhas desnecessariamente.\n"
245
- "- Não repita o usuário.\n"
246
- "- Para 'oi', 'olá', responda: 'Oi! Tudo bem?' ou 'Olá! Como vai?'\n"
247
- "- Se is_privileged=true: tom formal, evite gírias.\n"
248
- f"- Data e hora: {data_hora}.\n"
249
- f"- Número do bot: 244952786417.\n"
250
- )
251
 
252
- system_part = strict_override + f"\n{self.config.SYSTEM_PROMPT}\n{self.config.PERSONA}\n"
253
- system_part += "\n# Instruções adicionais:\n"
254
- if is_privileged:
255
- system_part += "- Tom respeitoso, use Sr./Sra., evite gírias.\n"
256
- else:
257
- system_part += "- Use nome só se fizer sentido. Prefira pronomes.\n"
258
-
259
- if is_blocking:
260
- system_part += "\n# BLOQUEIO: Responda com xingamentos (vai pra merda, etc).\n"
261
-
262
- usar_nome = analise.get('usar_nome', False)
263
- parts = [
264
- f"### Usuário ###\n- Nome: {usuario}\n- Número: {numero}\n- Usar_nome: {usar_nome}\n\n",
265
- f"### Contexto ###\n{historico_texto}\n\n",
266
- f"### Mensagem ###\n{analise.get('texto_normalizado', mensagem)}\n\n"
267
- ]
268
- if is_reply and mensagem_original:
269
- parts.append(f"### Mensagem original ###\n{mensagem_original}\n\n")
270
- parts.append("Akira:\n")
271
- user_part = ''.join(parts)
272
- return f"[SYSTEM]\n{system_part}\n[/SYSTEM]\n[USER]\n{user_part}\n[/USER]"
273
-
274
- def _generate_response(self, prompt: str, context_history: List[Dict], is_privileged: bool = False) -> str:
275
- try:
276
- text = self.providers.generate(prompt, context_history, is_privileged)
277
- return self._clean_response(text, prompt)
278
- except Exception as e:
279
- self.logger.exception('Falha ao gerar resposta')
280
- return getattr(self.config, 'FALLBACK_RESPONSE', 'Desculpa, estou off.')
281
-
282
- def _clean_response(self, text: Optional[str], prompt: Optional[str] = None) -> str:
283
- if not text: return ''
284
- cleaned = text.strip()
285
- for prefix in ['akira:', 'Resposta:', 'resposta:']:
286
- if cleaned.lower().startswith(prefix.lower()):
287
- cleaned = cleaned[len(prefix):].strip()
288
- break
289
- cleaned = re.sub(r'[\*\_`~\[\]<>]', '', cleaned)
290
- sentences = re.split(r'(?<=[.!?])\s+', cleaned)
291
- if len(sentences) > 2 and 'is_privileged=true' not in (prompt or ''):
292
- if not any(k in prompt.lower() for k in ['oi', 'olá', 'akira']) and len(prompt) > 20:
293
- cleaned = ' '.join(sentences[:2]).strip()
294
- max_chars = getattr(self.config, 'MAX_RESPONSE_CHARS', 280)
295
- return cleaned[:max_chars]
296
-
297
- def _setup_trainer(self):
298
- if getattr(self.config, 'START_PERIODIC_TRAINER', False):
299
- try:
300
- db = Database(getattr(self.config, 'DB_PATH', 'akira.db'))
301
- trainer = Treinamento(db, interval_hours=getattr(self.config, 'TRAIN_INTERVAL_HOURS', 24))
302
- trainer.start_periodic_training()
303
- self.logger.info("Treinamento periódico iniciado.")
304
- except Exception as e:
305
- self.logger.exception(f"Treinador falhou: {e}")
306
-
307
- def responder(self, mensagem: str, numero: str, nome: str = 'Usuário') -> str:
308
- data = {'usuario': nome, 'numero': numero, 'mensagem': mensagem}
309
- contexto = self._get_user_context(nome)
310
- analise = contexto.analisar_intencao_e_normalizar(mensagem, contexto.obter_historico())
311
- prompt = self._build_prompt(nome, numero, mensagem, analise, contexto, is_blocking=False)
312
- resposta = self._generate_response(prompt, contexto.obter_historico_para_llm())
313
- contexto.atualizar_contexto(mensagem, resposta)
314
- return resposta
 
1
  """
2
+ API wrapper para o serviço Akira - VERSÃO FINAL (11/2025)
3
+ Corrige erro 'str' object has no attribute get'
4
+ ✅ Tolerante a JSON malformado ou encoding incorreto (ex: Baileys)
5
+ ✅ Compatível com Mistral SDK novo e antigo
6
+ ✅ Logs detalhados
7
  """
8
+
9
+ from typing import Any
10
  import time
11
  import re
12
  import datetime
13
+ import random
14
+ import json
15
  from flask import Flask, Blueprint, request, jsonify
16
  from loguru import logger
17
+ import modules.config as config
 
 
 
 
 
 
 
18
  from .contexto import Contexto
19
  from .database import Database
20
  from .treinamento import Treinamento
21
+ from .web_search import WebSearch
22
+ from .local_llm import LlamaLLM
23
+
24
+
25
+ # ================================
26
+ # CONFIGURAÇÃO DE PROVEDORES
27
+ # ================================
28
+
29
+ try:
30
+ from mistralai import Mistral
31
+ try:
32
+ # Compatível com todas as versões do SDK
33
+ from mistralai.models.chat import ChatMessage
34
+ except ImportError:
35
+ from mistralai.types import ChatMessage
36
+ mistral_available = True
37
+ except ImportError:
38
+ mistral_available = False
39
+ logger.warning("⚠️ Mistral SDK não instalado.")
40
+
41
+ try:
42
+ import google.generativeai as genai
43
+ gemini_available = True
44
+ except ImportError:
45
+ gemini_available = False
46
+ logger.warning("⚠️ google-generativeai não instalado.")
47
 
48
 
49
  class LLMManager:
50
+ def __init__(self, config):
51
+ self.config = config
52
+ self.llama = LlamaLLM()
53
+ self.mistral_client = None
54
+ self.gemini_model = None
55
  self._setup_providers()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
 
57
  def _setup_providers(self):
58
+ if mistral_available and self.config.MISTRAL_API_KEY:
 
59
  try:
60
+ self.mistral_client = Mistral(api_key=self.config.MISTRAL_API_KEY)
61
+ logger.info("Mistral API inicializada (principal)")
62
  except Exception as e:
63
+ logger.warning(f"⚠️ Falha ao inicializar Mistral: {e}")
64
  self.mistral_client = None
 
 
65
 
66
+ if gemini_available and self.config.GEMINI_API_KEY:
 
67
  try:
68
  genai.configure(api_key=self.config.GEMINI_API_KEY)
69
+ self.gemini_model = genai.GenerativeModel(self.config.GEMINI_MODEL)
70
+ logger.info("✅ Gemini inicializado (fallback)")
 
 
 
71
  except Exception as e:
72
+ logger.warning(f"⚠️ Falha ao inicializar Gemini: {e}")
73
  self.gemini_model = None
74
+
75
+ def _limpar_resposta(self, texto: str) -> str:
76
+ if not texto:
77
+ return ""
78
+ texto = re.sub(r'[\*\_\`\[\]\"]', '', texto)
79
+ texto = re.sub(r'\s+', ' ', texto.replace('\n', ' ')).strip()
80
+ if len(texto) > 280:
81
+ frases = [f.strip() for f in texto.split('. ') if f.strip()]
82
+ curto = ""
83
+ for f in frases:
84
+ if len(curto + f + ". ") <= 280:
85
+ curto += f + ". "
86
+ else:
87
+ break
88
+ texto = curto.strip()
89
+ if not texto.endswith(('.', '!', '?')):
90
+ texto += "..."
91
+ return texto.strip()
92
+
93
+ def generate(self, prompt: str, max_tokens: int = 500, temperature: float = 0.8) -> str:
94
+ max_attempts = 6
95
+ for attempt in range(1, max_attempts + 1):
96
+ if self.mistral_client:
97
  try:
98
+ resp = self.mistral_client.chat.complete(
99
  model=self.config.MISTRAL_MODEL,
100
+ messages=[{"role": "user", "content": prompt}],
101
+ max_tokens=max_tokens,
102
+ temperature=temperature,
103
+ top_p=self.config.TOP_P,
104
  )
105
  text = resp.choices[0].message.content
106
  if text:
107
+ logger.info(f"Mistral OK (tentativa {attempt})")
108
+ return self._limpar_resposta(text)
109
+ except Exception as e:
110
+ if "401" in str(e) or "Unauthorized" in str(e):
111
+ logger.error("❌ MISTRAL: Chave inválida!")
112
+ elif "429" in str(e):
113
+ time.sleep(2 ** (attempt % 3))
114
+ logger.warning(f"Mistral erro {attempt}: {e}")
115
+
116
+ if self.llama.model:
117
+ try:
118
+ resp = self.llama.generate(prompt, max_tokens)
119
+ if resp.strip():
120
+ logger.info(f"✅ Llama OK (tentativa {attempt})")
121
+ return self._limpar_resposta(resp)
122
  except Exception as e:
123
+ logger.warning(f"Llama erro {attempt}: {e}")
124
 
125
+ if self.gemini_model:
126
  try:
 
 
 
 
 
 
127
  resp = self.gemini_model.generate_content(
128
+ prompt,
129
+ generation_config={
130
+ "max_output_tokens": max_tokens,
131
+ "temperature": temperature,
132
+ "top_p": self.config.TOP_P,
133
+ }
134
  )
135
  text = resp.text
136
  if text:
137
+ logger.info(f"Gemini OK (tentativa {attempt})")
138
+ return self._limpar_resposta(text)
139
  except Exception as e:
140
+ if "429" in str(e) or "quota" in str(e):
141
+ time.sleep(2 ** (attempt % 3))
142
+ logger.warning(f"Gemini erro {attempt}: {e}")
143
 
144
+ logger.error("❌ Todos os provedores falharam.")
 
 
 
 
 
 
 
 
 
145
  return self.config.FALLBACK_RESPONSE
146
 
147
 
148
+ # ================================
149
+ # CLASSE PRINCIPAL AKIRA API
150
+ # ================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
 
 
152
  class AkiraAPI:
153
  def __init__(self, cfg_module):
154
  self.config = cfg_module
155
  self.app = Flask(__name__)
156
  self.api = Blueprint("akira_api", __name__)
157
+ self.db = Database(getattr(self.config, 'DB_PATH', '/app/data/akira.db'))
158
+ self.contexto_cache = {}
159
  self.providers = LLMManager(self.config)
160
+ self.treinador = Treinamento(self.db)
161
+ self.web_search = WebSearch()
 
162
  self._setup_routes()
163
+ self.app.register_blueprint(self.api, url_prefix="/api")
164
+ if getattr(self.config, 'START_PERIODIC_TRAINER', False):
165
+ self.treinador.start_periodic_training()
166
+
167
+ def _get_user_context(self, usuario: str, numero: str) -> Contexto:
168
+ key = numero or usuario
169
+ if key not in self.contexto_cache:
170
+ ctx = Contexto(self.db, key)
171
+ ctx.atualizar_aprendizados_do_banco()
172
+ self.contexto_cache[key] = ctx
173
+ return self.contexto_cache[key]
174
+
175
+ def _get_pronomes_por_tom(self, tom: str) -> str:
176
+ result = self.db._execute_with_retry(
177
+ "SELECT pronomes FROM pronomes_por_tom WHERE tom=?", (tom.lower(),)
178
+ )
179
+ return result[0][0] if result else ""
180
+
181
+ def _build_prompt(self, usuario, numero, mensagem, emocao, contexto, is_privileged, is_reply, mensagem_original):
182
+ data_hora = datetime.datetime.now().strftime("%d/%m/%Y %H:%M")
183
+ noticias = self.web_search.pesquisar_noticias_angola()
184
+ tom_usuario = contexto.ton_predominante or "neutro"
185
+ pronomes = self._get_pronomes_por_tom(tom_usuario)
186
+
187
+ regras = f"""
188
+ REGRAS:
189
+ - Fale como pessoa real de Luanda
190
+ - 1 a 2 frases
191
+ - Pode usar gírias (bué, fixe, oroh, kota, puto)
192
+ - Pode usar sarcasmo leve e risadas (kkk, rsrs)
193
+ - Nada de markdown
194
+ - Data atual: {data_hora}
195
+ """
196
 
197
+ hist = contexto.obter_historico()[-3:]
198
+ hist_txt = "\n".join([f"U: {h[0]}\nA: {h[1]}" for h in hist]) if hist else ""
199
+
200
+ user_info = f"Usuário: {usuario} ({numero})\nTom: {tom_usuario}\nEmoção: {emocao}"
201
+
202
+ prompt = f"[SYSTEM]\n{regras}\n{self.config.SYSTEM_PROMPT}\n{self.config.PERSONA}\n[/SYSTEM]\n"
203
+ prompt += f"[CONTEXTO]\n{hist_txt}\n{user_info}\n[/CONTEXTO]\n[MENSAGEM]\n{mensagem}\n[/MENSAGEM]\nAkira:"
204
+ return prompt
205
 
206
  def _setup_routes(self):
207
  @self.api.route('/akira', methods=['POST'])
208
+ @self.api.route('/', methods=['POST'])
209
  def akira_endpoint():
210
  try:
211
+ raw_data = request.get_data(as_text=True)
212
+ logger.info(f"📩 RAW recebido ({len(raw_data)} bytes): {raw_data[:500]}")
213
+
214
+ # Decodificação robusta
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
+ logger.warning(f"🚨 Corpo não é dict: {type(data)}")
226
+ data = {}
227
+
228
+ usuario = data.get('usuario', 'Anônimo')
229
  numero = data.get('numero', '')
230
  mensagem = data.get('mensagem', '')
231
+ is_privileged = (usuario.lower() == 'isaac' or '244937035662' in numero)
232
+ is_reply = bool(data.get('is_reply') or data.get('mensagem_original'))
233
  mensagem_original = data.get('mensagem_original') or data.get('quoted_message') or ''
234
 
235
+ logger.info(f"👤 {usuario} ({numero}) disse: {mensagem}")
 
236
 
237
+ if not isinstance(mensagem, str) or not mensagem.strip():
238
+ return jsonify({'error': 'mensagem obrigatória'}), 400
 
 
 
239
 
240
+ contexto = self._get_user_context(usuario, numero)
241
+ emocao = contexto.analisar_emocoes_mensagem(mensagem)
242
+ prompt = self._build_prompt(usuario, numero, mensagem, emocao, contexto, is_privileged, is_reply, mensagem_original)
243
 
244
+ resposta = self.providers.generate(prompt, max_tokens=500, temperature=0.8)
245
+ logger.info(f"💬 Resposta: {resposta}")
246
 
247
  contexto.atualizar_contexto(mensagem, resposta)
248
+ self.treinador.registrar_interacao(usuario, mensagem, resposta, numero, is_reply, mensagem_original)
 
 
 
 
 
 
 
 
 
 
 
 
 
249
 
250
+ return jsonify({
251
+ 'resposta': resposta,
252
+ 'emocao': emocao,
253
+ 'usuario': usuario,
254
+ 'numero': numero
255
+ })
256
 
257
  except Exception as e:
258
+ logger.error(f"❌ Erro fatal: {e}", exc_info=True)
259
+ return jsonify({'resposta': 'deu um erro interno, puto 😅'}), 500
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
 
261
+ def run(self, host='0.0.0.0', port=7860, debug=False):
262
+ logger.info(f"🚀 Iniciando servidor Flask na porta {port}")
263
+ self.app.run(host=host, port=port, debug=debug, threaded=True)