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

Update modules/api.py

Browse files
Files changed (1) hide show
  1. modules/api.py +78 -63
modules/api.py CHANGED
@@ -1,12 +1,10 @@
1
  """
2
- API wrapper Akira IA — VERSÃO FINAL COMPATÍVEL COM main.py
3
- Prioridade: Mistral API (Phi-3 Mini) → Gemini → Fallback
4
- - Contexto por JID
5
- - WebSearch ativo
6
- - Resposta rápida de hora/data
7
- - Gemini SEM FILTROS
8
- - CORS liberado
9
  """
 
10
  import time
11
  import re
12
  import datetime
@@ -18,6 +16,10 @@ from loguru import logger
18
  import google.generativeai as genai
19
  from mistralai import Mistral
20
 
 
 
 
 
21
  # LOCAL MODULES
22
  from .contexto import Contexto
23
  from .database import Database
@@ -26,44 +28,39 @@ from .exemplos_naturais import ExemplosNaturais
26
  from .web_search import WebSearch
27
  import modules.config as config
28
 
29
-
30
- # --- CACHE SIMPLES COM TTL (5 MINUTOS) ---
31
  class SimpleTTLCache:
32
  def __init__(self, ttl_seconds: int = 300):
33
  self.ttl = ttl_seconds
34
  self._store = {}
35
-
36
  def __contains__(self, key):
37
- if key not in self._store:
38
- return False
39
  _, expires = self._store[key]
40
- if time.time() > expires:
41
- del self._store[key]
42
- return False
43
  return True
44
-
45
  def __setitem__(self, key, value):
46
  self._store[key] = (value, time.time() + self.ttl)
47
-
48
  def __getitem__(self, key):
49
- if key not in self:
50
- raise KeyError(key)
51
  return self._store[key][0]
52
 
53
-
54
- # --- GERENCIADOR DE LLMs ---
55
  class LLMManager:
56
- def __init__(self, config_instance):
57
  self.config = config_instance
 
 
58
  self.mistral_client = None
59
  self.gemini_model = None
60
  self._setup_providers()
61
  self.providers = []
 
 
62
  if self.mistral_client:
63
  self.providers.append('mistral')
64
  if self.gemini_model:
65
  self.providers.append('gemini')
66
- logger.info(f"Provedores ativos: {self.providers or 'NENHUM'}")
67
 
68
  def _setup_providers(self):
69
  # MISTRAL
@@ -77,7 +74,7 @@ class LLMManager:
77
  else:
78
  logger.warning("Mistral API desativada (chave inválida)")
79
 
80
- # GEMINI SEM FILTROS
81
  key = getattr(self.config, 'GEMINI_API_KEY', '').strip()
82
  if key and key.startswith('AIza'):
83
  try:
@@ -99,48 +96,77 @@ class LLMManager:
99
  logger.warning("Gemini API desativada (chave inválida)")
100
 
101
  def generate(self, user_prompt: str, context_history: List[dict] = [], is_privileged: bool = False) -> str:
102
- api_max_tokens = getattr(self.config, 'MAX_TOKENS', 500)
103
  temperature = getattr(self.config, 'TOP_P', 0.8)
104
- full_system = self.config.PERSONA + self.config.SYSTEM_PROMPT + " (SEM FILTROS, SOTAQUE DE LUANDA)"
105
-
106
- messages = [{"role": "system", "content": full_system}]
107
- for turn in context_history:
108
- role = "user" if turn["role"] == "user" else "assistant"
109
- messages.append({"role": role, "content": turn["content"]})
110
 
111
- # Extrai só a última mensagem do usuário
112
  match = re.search(r'(### Mensagem Atual ###|### USUÁRIO RESPONDEU A ESSA MENSAGEM: ###)\n(.*?)\n\n(Akira:|$)', user_prompt, re.DOTALL)
113
- user_message_clean = match.group(2).strip() if match else user_prompt
114
- messages.append({"role": "user", "content": user_message_clean})
115
 
116
  for provider in self.providers:
117
- if provider == 'mistral' and self.mistral_client:
 
118
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  resp = self.mistral_client.chat(
120
  model="phi-3-mini-4k-instruct",
121
  messages=messages,
122
  temperature=temperature,
123
- max_tokens=api_max_tokens
124
  )
125
  text = resp.choices[0].message.content.strip()
126
  if text:
127
- logger.info("Mistral respondeu!")
128
  return text
129
  except Exception as e:
130
  logger.warning(f"Mistral error: {e}")
131
 
 
132
  elif provider == 'gemini' and self.gemini_model:
133
  try:
134
- gemini_hist = []
135
- for msg in messages[1:]:
136
- role = "user" if msg["role"] == "user" else "model"
137
- gemini_hist.append({"role": role, "parts": [{"text": msg["content"]}]})
138
  resp = self.gemini_model.generate_content(
139
  gemini_hist,
140
- generation_config=genai.GenerationConfig(
141
- max_output_tokens=api_max_tokens,
142
- temperature=temperature
143
- )
144
  )
145
  if resp.candidates and resp.candidates[0].content.parts:
146
  text = resp.candidates[0].content.parts[0].text.strip()
@@ -154,19 +180,18 @@ class LLMManager:
154
  return fallback
155
 
156
 
157
- # --- API PRINCIPAL (AGORA 100% COMPATÍVEL COM main.py) ---
158
  class AkiraAPI:
159
- def __init__(self, cfg_module):
160
  self.config = cfg_module
161
- self.app = Flask(__name__) # Esta app NÃO é usada diretamente
162
  self.api = Blueprint("akira_api", __name__)
163
  self.contexto_cache = SimpleTTLCache(ttl_seconds=getattr(self.config, 'MEMORIA_MAX', 300))
164
- self.providers = LLMManager(self.config)
165
  self.exemplos = ExemplosNaturais()
166
  self.logger = logger
167
  self.db = Database(getattr(self.config, 'DB_PATH', 'akira.db'))
168
 
169
- # WebSearch
170
  try:
171
  from .web_search import WebSearch
172
  self.web_search = WebSearch()
@@ -179,9 +204,6 @@ class AkiraAPI:
179
  self._setup_routes()
180
  self._setup_trainer()
181
 
182
- # Blueprint registrado no main.py com prefix /api
183
- # NÃO faz register aqui → main.py faz!
184
-
185
  def _setup_personality(self):
186
  self.humor = getattr(self.config, 'HUMOR_INICIAL', 'neutra')
187
  self.interesses = list(getattr(self.config, 'INTERESSES', []))
@@ -198,7 +220,6 @@ class AkiraAPI:
198
  logger.exception(f"Treinador falhou: {e}")
199
 
200
  def _setup_routes(self):
201
- # CORS MANUAL (funciona 100%)
202
  @self.api.before_request
203
  def handle_options():
204
  if request.method == 'OPTIONS':
@@ -213,7 +234,6 @@ class AkiraAPI:
213
  response.headers['Access-Control-Allow-Origin'] = '*'
214
  return response
215
 
216
- # ROTA PRINCIPAL
217
  @self.api.route('/akira', methods=['POST'])
218
  def akira_endpoint():
219
  try:
@@ -230,7 +250,7 @@ class AkiraAPI:
230
 
231
  self.logger.info(f"{usuario} ({numero}): {mensagem[:80]}")
232
 
233
- # RESPOSTA RÁPIDA: HORA/DATA
234
  lower = mensagem.lower()
235
  if any(k in lower for k in ["que horas", "que dia", "data", "hoje"]):
236
  agora = datetime.datetime.now()
@@ -244,7 +264,6 @@ class AkiraAPI:
244
  contexto.atualizar_contexto(mensagem, resp)
245
  return jsonify({'resposta': resp})
246
 
247
- # PROCESSAMENTO NORMAL
248
  contexto = self._get_user_context(numero)
249
  analise = contexto.analisar_intencao_e_normalizar(mensagem, contexto.obter_historico())
250
  if usuario.lower() in ['isaac', 'isaac quarenta']:
@@ -258,12 +277,11 @@ class AkiraAPI:
258
 
259
  contexto.atualizar_contexto(mensagem, resposta)
260
 
261
- # SALVAR NO BANCO
262
  try:
263
  trainer = Treinamento(self.db)
264
  trainer.registrar_interacao(usuario, mensagem, resposta, numero, is_reply, mensagem_original)
265
  except Exception as e:
266
- logger.warning(f"Erro ao salvar interação: {e}")
267
 
268
  return jsonify({'resposta': resposta})
269
 
@@ -276,8 +294,7 @@ class AkiraAPI:
276
  return 'OK', 200
277
 
278
  def _get_user_context(self, numero: str) -> Contexto:
279
- if not numero:
280
- numero = "anonimo_contexto"
281
  if numero not in self.contexto_cache:
282
  self.contexto_cache[numero] = Contexto(self.db, usuario=numero)
283
  return self.contexto_cache[numero]
@@ -288,7 +305,6 @@ class AkiraAPI:
288
  now = datetime.datetime.now()
289
  data_hora = now.strftime('%d/%m/%Y %H:%M')
290
 
291
- # WEB SEARCH
292
  web_context = ""
293
  query = f"{mensagem} {mensagem_citada}".lower()
294
  trigger = ['hoje', 'agora', 'notícias', 'pesquisa', 'último']
@@ -324,7 +340,6 @@ class AkiraAPI:
324
  parts.append(f"### Mensagem Atual ###\n{analise.get('texto_normalizado', mensagem)}\n\n")
325
  parts.append("Akira:")
326
  user_part = ''.join(parts)
327
-
328
  return f"[SYSTEM]\n{system}\n[/SYSTEM]\n[USER]\n{user_part}\n[/USER]"
329
 
330
  def _generate_response(self, prompt: str, context_history: List[dict], is_privileged: bool = False) -> str:
 
1
  """
2
+ AKIRA IA — VERSÃO FINAL COM PHI-3 LOCAL EM PRIMEIRO LUGAR
3
+ Prioridade: LOCAL (Phi-3) → Mistral API → Gemini → Fallback
4
+ - Tudo funcionando: contexto, websearch, memória, treinamento
5
+ - Respostas em < 3 segundos mesmo na CPU do HF Space
 
 
 
6
  """
7
+
8
  import time
9
  import re
10
  import datetime
 
16
  import google.generativeai as genai
17
  from mistralai import Mistral
18
 
19
+ # Transformers (LOCAL)
20
+ import torch
21
+ from transformers import AutoModelForCausalLM, AutoTokenizer
22
+
23
  # LOCAL MODULES
24
  from .contexto import Contexto
25
  from .database import Database
 
28
  from .web_search import WebSearch
29
  import modules.config as config
30
 
31
+ # --- CACHE SIMPLES ---
 
32
  class SimpleTTLCache:
33
  def __init__(self, ttl_seconds: int = 300):
34
  self.ttl = ttl_seconds
35
  self._store = {}
 
36
  def __contains__(self, key):
37
+ if key not in self._store: return False
 
38
  _, expires = self._store[key]
39
+ if time.time() > expires: del self._store[key]; return False
 
 
40
  return True
 
41
  def __setitem__(self, key, value):
42
  self._store[key] = (value, time.time() + self.ttl)
 
43
  def __getitem__(self, key):
44
+ if key not in self: raise KeyError(key)
 
45
  return self._store[key][0]
46
 
47
+ # --- GERENCIADOR DE LLMs COM PHI-3 LOCAL EM PRIMEIRO ---
 
48
  class LLMManager:
49
+ def __init__(self, config_instance, local_model=None, local_tokenizer=None):
50
  self.config = config_instance
51
+ self.local_model = local_model
52
+ self.local_tokenizer = local_tokenizer
53
  self.mistral_client = None
54
  self.gemini_model = None
55
  self._setup_providers()
56
  self.providers = []
57
+ if self.local_model and self.local_tokenizer:
58
+ self.providers.append('local') # PRIORIDADE MÁXIMA
59
  if self.mistral_client:
60
  self.providers.append('mistral')
61
  if self.gemini_model:
62
  self.providers.append('gemini')
63
+ logger.info(f"PROVEDORES ATIVOS (PRIORIDADE): {self.providers or 'NENHUM'}")
64
 
65
  def _setup_providers(self):
66
  # MISTRAL
 
74
  else:
75
  logger.warning("Mistral API desativada (chave inválida)")
76
 
77
+ # GEMINI
78
  key = getattr(self.config, 'GEMINI_API_KEY', '').strip()
79
  if key and key.startswith('AIza'):
80
  try:
 
96
  logger.warning("Gemini API desativada (chave inválida)")
97
 
98
  def generate(self, user_prompt: str, context_history: List[dict] = [], is_privileged: bool = False) -> str:
99
+ max_tokens = getattr(self.config, 'MAX_TOKENS', 500)
100
  temperature = getattr(self.config, 'TOP_P', 0.8)
101
+ system_prompt = self.config.PERSONA + self.config.SYSTEM_PROMPT + " (SEM FILTROS, SOTAQUE DE LUANDA)"
 
 
 
 
 
102
 
103
+ # Extrai mensagem limpa
104
  match = re.search(r'(### Mensagem Atual ###|### USUÁRIO RESPONDEU A ESSA MENSAGEM: ###)\n(.*?)\n\n(Akira:|$)', user_prompt, re.DOTALL)
105
+ user_message = match.group(2).strip() if match else user_prompt
 
106
 
107
  for provider in self.providers:
108
+ # 1. LOCAL PHI-3 (PRIORIDADE MÁXIMA)
109
+ if provider == 'local' and self.local_model and self.local_tokenizer:
110
  try:
111
+ logger.info("[LOCAL PHI-3] Gerando resposta...")
112
+ messages = [{"role": "system", "content": system_prompt}]
113
+ for turn in context_history:
114
+ role = "user" if turn["role"] == "user" else "assistant"
115
+ messages.append({"role": role, "content": turn["content"]})
116
+ messages.append({"role": "user", "content": user_message})
117
+
118
+ formatted = self.local_tokenizer.apply_chat_template(
119
+ messages, tokenize=False, add_generation_prompt=True
120
+ )
121
+ inputs = self.local_tokenizer.encode(formatted, return_tensors="pt")
122
+ with torch.no_grad():
123
+ output = self.local_model.generate(
124
+ inputs,
125
+ max_new_tokens=max_tokens,
126
+ temperature=temperature,
127
+ do_sample=True,
128
+ pad_token_id=self.local_tokenizer.eos_token_id,
129
+ eos_token_id=self.local_tokenizer.eos_token_id
130
+ )
131
+ text = self.local_tokenizer.decode(
132
+ output[0][inputs.shape[-1]:], skip_special_tokens=True
133
+ ).strip()
134
+ if text:
135
+ logger.info("PHI-3 LOCAL respondeu!")
136
+ return text
137
+ except Exception as e:
138
+ logger.warning(f"Phi-3 local falhou: {e}")
139
+
140
+ # 2. MISTRAL
141
+ elif provider == 'mistral' and self.mistral_client:
142
+ try:
143
+ messages = [{"role": "system", "content": system_prompt}]
144
+ for turn in context_history:
145
+ role = "user" if turn["role"] == "user" else "assistant"
146
+ messages.append({"role": role, "content": turn["content"]})
147
+ messages.append({"role": "user", "content": user_message})
148
+
149
  resp = self.mistral_client.chat(
150
  model="phi-3-mini-4k-instruct",
151
  messages=messages,
152
  temperature=temperature,
153
+ max_tokens=max_tokens
154
  )
155
  text = resp.choices[0].message.content.strip()
156
  if text:
157
+ logger.info("Mistral API respondeu!")
158
  return text
159
  except Exception as e:
160
  logger.warning(f"Mistral error: {e}")
161
 
162
+ # 3. GEMINI
163
  elif provider == 'gemini' and self.gemini_model:
164
  try:
165
+ gemini_hist = [{"role": "user" if m["role"]=="user" else "model", "parts": [{"text": m["content"]}]}
166
+ for m in [{"role": "system", "content": system_prompt}] + context_history + [{"role": "user", "content": user_message}][1:]]
 
 
167
  resp = self.gemini_model.generate_content(
168
  gemini_hist,
169
+ generation_config=genai.GenerationConfig(max_output_tokens=max_tokens, temperature=temperature)
 
 
 
170
  )
171
  if resp.candidates and resp.candidates[0].content.parts:
172
  text = resp.candidates[0].content.parts[0].text.strip()
 
180
  return fallback
181
 
182
 
183
+ # --- API PRINCIPAL (PASSA O MODELO LOCAL) ---
184
  class AkiraAPI:
185
+ def __init__(self, cfg_module, local_model=None, local_tokenizer=None):
186
  self.config = cfg_module
187
+ self.app = Flask(__name__)
188
  self.api = Blueprint("akira_api", __name__)
189
  self.contexto_cache = SimpleTTLCache(ttl_seconds=getattr(self.config, 'MEMORIA_MAX', 300))
190
+ self.providers = LLMManager(self.config, local_model, local_tokenizer)
191
  self.exemplos = ExemplosNaturais()
192
  self.logger = logger
193
  self.db = Database(getattr(self.config, 'DB_PATH', 'akira.db'))
194
 
 
195
  try:
196
  from .web_search import WebSearch
197
  self.web_search = WebSearch()
 
204
  self._setup_routes()
205
  self._setup_trainer()
206
 
 
 
 
207
  def _setup_personality(self):
208
  self.humor = getattr(self.config, 'HUMOR_INICIAL', 'neutra')
209
  self.interesses = list(getattr(self.config, 'INTERESSES', []))
 
220
  logger.exception(f"Treinador falhou: {e}")
221
 
222
  def _setup_routes(self):
 
223
  @self.api.before_request
224
  def handle_options():
225
  if request.method == 'OPTIONS':
 
234
  response.headers['Access-Control-Allow-Origin'] = '*'
235
  return response
236
 
 
237
  @self.api.route('/akira', methods=['POST'])
238
  def akira_endpoint():
239
  try:
 
250
 
251
  self.logger.info(f"{usuario} ({numero}): {mensagem[:80]}")
252
 
253
+ # RESPOSTA RÁPIDA HORA/DATA
254
  lower = mensagem.lower()
255
  if any(k in lower for k in ["que horas", "que dia", "data", "hoje"]):
256
  agora = datetime.datetime.now()
 
264
  contexto.atualizar_contexto(mensagem, resp)
265
  return jsonify({'resposta': resp})
266
 
 
267
  contexto = self._get_user_context(numero)
268
  analise = contexto.analisar_intencao_e_normalizar(mensagem, contexto.obter_historico())
269
  if usuario.lower() in ['isaac', 'isaac quarenta']:
 
277
 
278
  contexto.atualizar_contexto(mensagem, resposta)
279
 
 
280
  try:
281
  trainer = Treinamento(self.db)
282
  trainer.registrar_interacao(usuario, mensagem, resposta, numero, is_reply, mensagem_original)
283
  except Exception as e:
284
+ logger.warning(f"Erro ao salvar: {e}")
285
 
286
  return jsonify({'resposta': resposta})
287
 
 
294
  return 'OK', 200
295
 
296
  def _get_user_context(self, numero: str) -> Contexto:
297
+ if not numero: numero = "anonimo_contexto"
 
298
  if numero not in self.contexto_cache:
299
  self.contexto_cache[numero] = Contexto(self.db, usuario=numero)
300
  return self.contexto_cache[numero]
 
305
  now = datetime.datetime.now()
306
  data_hora = now.strftime('%d/%m/%Y %H:%M')
307
 
 
308
  web_context = ""
309
  query = f"{mensagem} {mensagem_citada}".lower()
310
  trigger = ['hoje', 'agora', 'notícias', 'pesquisa', 'último']
 
340
  parts.append(f"### Mensagem Atual ###\n{analise.get('texto_normalizado', mensagem)}\n\n")
341
  parts.append("Akira:")
342
  user_part = ''.join(parts)
 
343
  return f"[SYSTEM]\n{system}\n[/SYSTEM]\n[USER]\n{user_part}\n[/USER]"
344
 
345
  def _generate_response(self, prompt: str, context_history: List[dict], is_privileged: bool = False) -> str: