akra35567 commited on
Commit
d4ba5ea
·
1 Parent(s): 971457a

Update modules/api.py

Browse files
Files changed (1) hide show
  1. modules/api.py +72 -81
modules/api.py CHANGED
@@ -4,39 +4,41 @@ API wrapper for Akira service - VERSÃO FINAL ADAPTADA:
4
  - Pesquisa web (SerpAPI)
5
  - Nome: probabilidade 0.4 + pronomes por tom
6
  - PRINCIPAL: MISTRAL AI API
7
- - Fallback: Llama 3.1 8B local → Gemini
8
  - Prompt otimizado com contexto angolano
9
  """
10
  from typing import Dict, Optional, Any
11
  import time
12
- import logging
13
  import re
14
  import datetime
15
  import random
16
  from flask import Flask, Blueprint, request, jsonify
17
-
 
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
  try:
25
  from mistralai import Mistral
26
  mistral_available = True
27
  except ImportError:
28
  mistral_available = False
 
29
 
30
  try:
31
  import google.generativeai as genai
32
  gemini_available = True
33
  except ImportError:
34
  gemini_available = False
 
35
 
36
- logger = logging.getLogger("akira.api")
37
 
38
  class LLMManager:
39
- """Gerenciador de LLMs: MISTRAL API → Llama local → Gemini"""
40
  def __init__(self, config):
41
  self.config = config
42
  self.llama = LlamaLLM()
@@ -45,22 +47,22 @@ class LLMManager:
45
  self._setup_providers()
46
 
47
  def _setup_providers(self):
48
- # MISTRAL API (PRINCIPAL)
49
- if mistral_available and config.MISTRAL_API_KEY:
50
  try:
51
- self.mistral_client = Mistral(api_key=config.MISTRAL_API_KEY)
52
- logger.info("Mistral API client inicializado (principal).")
53
  except Exception as e:
54
- logger.warning(f"Falha ao inicializar Mistral: {e}")
55
 
56
- # GEMINI (FALLBACK 3)
57
- if gemini_available and config.GEMINI_API_KEY:
58
  try:
59
- genai.configure(api_key=config.GEMINI_API_KEY)
60
- self.gemini_model = genai.GenerativeModel(config.GEMINI_MODEL)
61
- logger.info("Gemini model inicializado (fallback).")
62
  except Exception as e:
63
- logger.warning(f"Falha ao inicializar Gemini: {e}")
64
 
65
  def _limpar_resposta(self, texto: str) -> str:
66
  if not texto:
@@ -69,88 +71,79 @@ class LLMManager:
69
  texto = re.sub(r'\s+', ' ', texto.replace('\n', ' ')).strip()
70
  if len(texto) > 280:
71
  frases = [f.strip() for f in texto.split('. ') if f.strip()]
72
- texto_curto = ""
73
- for frase in frases:
74
- if len(texto_curto + frase + ". ") <= 280:
75
- texto_curto += frase + ". "
76
  else:
77
  break
78
- texto = texto_curto.strip()
79
  if not texto.endswith(('.', '!', '?')):
80
  texto += "..."
81
  return texto.strip()
82
 
83
  def generate(self, prompt: str, max_tokens: int = 500, temperature: float = 0.8) -> str:
84
  max_attempts = 6
85
- attempt = 1
86
- while attempt <= max_attempts:
87
- # 1. MISTRAL API (PRINCIPAL)
88
  if self.mistral_client:
89
  try:
90
- response = self.mistral_client.chat.complete(
91
- model=config.MISTRAL_MODEL,
92
  messages=[{"role": "user", "content": prompt}],
93
  max_tokens=max_tokens,
94
  temperature=temperature,
95
- top_p=config.TOP_P,
96
  )
97
- text = response.choices[0].message.content
98
  if text:
99
- logger.info(f"Resposta com Mistral API (tentativa {attempt})")
100
  return self._limpar_resposta(text)
101
  except Exception as e:
102
  if "429" in str(e):
103
  time.sleep(2 ** (attempt % 3))
104
- logger.warning(f"Mistral falhou (tentativa {attempt}): {e}")
105
- attempt += 1
106
- continue
107
 
108
- # 2. LLAMA LOCAL (FALLBACK 1)
109
  if self.llama.model:
110
  try:
111
  resp = self.llama.generate(prompt, max_tokens)
112
  if resp.strip():
113
- logger.info(f"Resposta com Llama 3.1 8B (tentativa {attempt})")
114
  return self._limpar_resposta(resp)
115
  except Exception as e:
116
- logger.warning(f"Llama falhou (tentativa {attempt}): {e}")
117
- attempt += 1
118
- continue
119
 
120
- # 3. GEMINI (FALLBACK 2)
121
  if self.gemini_model:
122
  try:
123
- response = self.gemini_model.generate_content(
124
  prompt,
125
  generation_config={
126
  "max_output_tokens": max_tokens,
127
  "temperature": temperature,
128
- "top_p": config.TOP_P,
129
  }
130
  )
131
- text = response.text
132
  if text:
133
- logger.info(f"Resposta com Gemini (tentativa {attempt})")
134
  return self._limpar_resposta(text)
135
  except Exception as e:
136
  if "429" in str(e) or "quota" in str(e):
137
  time.sleep(2 ** (attempt % 3))
138
- logger.warning(f"Gemini falhou (tentativa {attempt}): {e}")
139
- attempt += 1
140
- continue
141
 
142
- # Fallback local simples
143
- return "Tô com problema, puto. Tenta depois kkk"
144
 
145
- logger.error("Todas tentativas falharam.")
146
- return config.FALLBACK_RESPONSE
147
 
148
  class AkiraAPI:
149
  def __init__(self, cfg_module):
150
  self.config = cfg_module
151
  self.app = Flask(__name__)
152
  self.api = Blueprint("akira_api", __name__)
153
- self.db = Database(getattr(self.config, 'DB_PATH', 'akira.db'))
154
  self.contexto_cache = {}
155
  self.providers = LLMManager(self.config)
156
  self.treinador = Treinamento(self.db)
@@ -170,47 +163,40 @@ class AkiraAPI:
170
 
171
  def _get_pronomes_por_tom(self, tom: str) -> str:
172
  result = self.db._execute_with_retry(
173
- "SELECT pronomes FROM pronomes_por_tom WHERE tom=?", (tom,)
174
  )
175
  return result[0][0] if result else ""
176
 
177
  def _build_prompt(self, usuario: str, numero: str, mensagem: str, analise: dict,
178
- contexto: Contexto, is_blocking: bool, is_privileged: bool,
179
- is_reply: bool, mensagem_original: str) -> str:
180
- # DATA E HORA COM ANO
181
  data_hora = datetime.datetime.now().strftime("%d/%m/%Y %H:%M")
182
- # NOTÍCIAS ATUAIS (Angola)
183
  noticias = self.web_search.pesquisar_noticias_angola()
184
- # TOM DO USUÁRIO
185
  tom_usuario = contexto.ton_predominante or "neutro"
186
  pronomes = self._get_pronomes_por_tom(tom_usuario)
187
- # REGRAS NATURAIS
 
188
  strict = f"""
189
  REGRAS OBRIGATÓRIAS:
190
- - RESPONDA COMO PESSOA, NUNCA COMO IA
191
- - MÁXIMO 1-2 FRASES CURTAS
192
- - USE GÍRIAS NATURAIS: bué, fixe, puto, oroh, mwangolé, kota
193
- - SEJA DIRETA, SARCÁSTICA SE QUISER
194
- - USE "kkk", "rsrs", "aff" para rir
195
- - NUNCA use negrito, itálico ou markdown
196
- - NUNCA diga "como IA", "modelo", "sistema"
197
- - USE NOME DO USUÁRIO SÓ ~40% DAS VEZES
198
- - NUNCA NCA use nome completo (ex: "Isaac Quarenta")
199
- - Usuários privilegiados: use apenas o último nome (ex: "Quarenta")
200
- - DATA ATUAL: {data_hora}
201
- # NOTÍCIAS ATUAIS (Angola):
202
  {noticias}
203
- # PRONOMES POR TOM ({tom_usuario}):
204
  {pronomes}
205
- # TOM ESPECÍFICO:
206
  """
207
  if is_privileged:
208
- strict += "- TOM MAIS FORMAL COM ISAAC (use 'Sr.', 'boss', 'maior')\n"
209
  else:
210
- strict += "- TOM NORMAL: fala como gente de Luanda\n"
 
211
  hist = contexto.obter_historico()[-3:]
212
  hist_txt = "\n".join([f"U: {h[0]}\nA: {h[1]}" for h in hist]) if hist else ""
213
- girias_user = ", ".join([g['giria'] for g in contexto.girias_aprendidas[:3]])
214
  user_info = f"""
215
  USUÁRIO:
216
  - Nome: {usuario}
@@ -219,10 +205,10 @@ USUÁRIO:
219
  - Gírias: {girias_user}
220
  - Privilégio: {'sim' if is_privileged else 'não'}
221
  """
 
222
  prompt = f"[SYSTEM]\n{strict}\n{self.config.SYSTEM_PROMPT}\n{self.config.PERSONA}\n[/SYSTEM]\n"
223
  prompt += f"[CONTEXTO]\n{hist_txt}\n{user_info}\n[/CONTEXTO]\n"
224
- prompt += f"[MENSAGEM]\n{mensagem}\n[/MENSAGEM]\n"
225
- prompt += "Akira:"
226
  return prompt
227
 
228
  def _setup_routes(self):
@@ -237,8 +223,10 @@ USUÁRIO:
237
  is_privileged = (usuario.lower() == 'isaac' or '244937035662' in numero)
238
  is_reply = bool(data.get('is_reply') or data.get('mensagem_original'))
239
  mensagem_original = data.get('mensagem_original') or data.get('quoted_message') or ''
 
240
  if not mensagem.strip():
241
- return jsonify({'error': 'mensagem é obrigatória'}), 400
 
242
  logger.info(f"{usuario} ({numero}): {mensagem[:120]}")
243
  contexto = self._get_user_context(usuario, numero)
244
  analise = contexto.analisar_intencao_e_normalizar(mensagem, contexto.obter_historico())
@@ -247,22 +235,25 @@ USUÁRIO:
247
  is_privileged, is_reply, mensagem_original
248
  )
249
  resposta = self.providers.generate(prompt, max_tokens=500, temperature=0.8)
 
250
  if random.random() < getattr(self.config, 'USAR_NOME_PROBABILIDADE', 0.4):
251
- pass # A IA decide sozinha
 
252
  contexto.atualizar_contexto(mensagem, resposta)
253
  self.treinador.registrar_interacao(usuario, mensagem, resposta, numero, is_reply, mensagem_original)
 
254
  return jsonify({
255
  'resposta': resposta,
256
  'aprendizados': {
257
  'emocao_atual': contexto.emocao_atual,
258
  'termos': contexto.termo_contexto,
259
- 'gírias': [g['giria'] for g in contexto.gírias_aprendidas[:3]]
260
  }
261
  })
262
  except Exception as e:
263
  logger.error(f"Erro fatal: {e}", exc_info=True)
264
- return jsonify({'resposta': 'tive um erro, puto. tenta depois.'}), 500
265
 
266
  def run(self, host='0.0.0.0', port=7860, debug=False):
267
- logger.info(f"Starting Akira IA Flask app... Running on port {port}, debug={'ON' if debug else 'OFF'}")
268
  self.app.run(host=host, port=port, debug=debug, threaded=True)
 
4
  - Pesquisa web (SerpAPI)
5
  - Nome: probabilidade 0.4 + pronomes por tom
6
  - PRINCIPAL: MISTRAL AI API
7
+ - Fallback: Llama 3.1 8B local (CPU 8-bit) → Gemini
8
  - Prompt otimizado com contexto angolano
9
  """
10
  from typing import Dict, Optional, Any
11
  import time
 
12
  import re
13
  import datetime
14
  import random
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
+ # Verifica disponibilidade
25
  try:
26
  from mistralai import Mistral
27
  mistral_available = True
28
  except ImportError:
29
  mistral_available = False
30
+ logger.warning("mistralai não instalado")
31
 
32
  try:
33
  import google.generativeai as genai
34
  gemini_available = True
35
  except ImportError:
36
  gemini_available = False
37
+ logger.warning("google-generativeai não instalado")
38
 
 
39
 
40
  class LLMManager:
41
+ """Gerenciador de LLMs: MISTRAL → Llama (CPU) → Gemini"""
42
  def __init__(self, config):
43
  self.config = config
44
  self.llama = LlamaLLM()
 
47
  self._setup_providers()
48
 
49
  def _setup_providers(self):
50
+ # MISTRAL API
51
+ if mistral_available and self.config.MISTRAL_API_KEY:
52
  try:
53
+ self.mistral_client = Mistral(api_key=self.config.MISTRAL_API_KEY)
54
+ logger.info("Mistral API inicializado (principal)")
55
  except Exception as e:
56
+ logger.warning(f"Mistral falhou: {e}")
57
 
58
+ # GEMINI
59
+ if gemini_available and self.config.GEMINI_API_KEY:
60
  try:
61
+ genai.configure(api_key=self.config.GEMINI_API_KEY)
62
+ self.gemini_model = genai.GenerativeModel(self.config.GEMINI_MODEL)
63
+ logger.info("Gemini inicializado (fallback)")
64
  except Exception as e:
65
+ logger.warning(f"Gemini falhou: {e}")
66
 
67
  def _limpar_resposta(self, texto: str) -> str:
68
  if not texto:
 
71
  texto = re.sub(r'\s+', ' ', texto.replace('\n', ' ')).strip()
72
  if len(texto) > 280:
73
  frases = [f.strip() for f in texto.split('. ') if f.strip()]
74
+ curto = ""
75
+ for f in frases:
76
+ if len(curto + f + ". ") <= 280:
77
+ curto += f + ". "
78
  else:
79
  break
80
+ texto = curto.strip()
81
  if not texto.endswith(('.', '!', '?')):
82
  texto += "..."
83
  return texto.strip()
84
 
85
  def generate(self, prompt: str, max_tokens: int = 500, temperature: float = 0.8) -> str:
86
  max_attempts = 6
87
+ for attempt in range(1, max_attempts + 1):
88
+ # 1. MISTRAL
 
89
  if self.mistral_client:
90
  try:
91
+ resp = self.mistral_client.chat.complete(
92
+ model=self.config.MISTRAL_MODEL,
93
  messages=[{"role": "user", "content": prompt}],
94
  max_tokens=max_tokens,
95
  temperature=temperature,
96
+ top_p=self.config.TOP_P,
97
  )
98
+ text = resp.choices[0].message.content
99
  if text:
100
+ logger.info(f"Mistral OK (tentativa {attempt})")
101
  return self._limpar_resposta(text)
102
  except Exception as e:
103
  if "429" in str(e):
104
  time.sleep(2 ** (attempt % 3))
105
+ logger.warning(f"Mistral erro {attempt}: {e}")
 
 
106
 
107
+ # 2. LLAMA LOCAL (CPU 8-bit)
108
  if self.llama.model:
109
  try:
110
  resp = self.llama.generate(prompt, max_tokens)
111
  if resp.strip():
112
+ logger.info(f"Llama OK (tentativa {attempt})")
113
  return self._limpar_resposta(resp)
114
  except Exception as e:
115
+ logger.warning(f"Llama erro {attempt}: {e}")
 
 
116
 
117
+ # 3. GEMINI
118
  if self.gemini_model:
119
  try:
120
+ resp = self.gemini_model.generate_content(
121
  prompt,
122
  generation_config={
123
  "max_output_tokens": max_tokens,
124
  "temperature": temperature,
125
+ "top_p": self.config.TOP_P,
126
  }
127
  )
128
+ text = resp.text
129
  if text:
130
+ logger.info(f"Gemini OK (tentativa {attempt})")
131
  return self._limpar_resposta(text)
132
  except Exception as e:
133
  if "429" in str(e) or "quota" in str(e):
134
  time.sleep(2 ** (attempt % 3))
135
+ logger.warning(f"Gemini erro {attempt}: {e}")
 
 
136
 
137
+ logger.error("Todos LLMs falharam")
138
+ return self.config.FALLBACK_RESPONSE
139
 
 
 
140
 
141
  class AkiraAPI:
142
  def __init__(self, cfg_module):
143
  self.config = cfg_module
144
  self.app = Flask(__name__)
145
  self.api = Blueprint("akira_api", __name__)
146
+ self.db = Database(getattr(self.config, 'DB_PATH', '/app/data/akira.db'))
147
  self.contexto_cache = {}
148
  self.providers = LLMManager(self.config)
149
  self.treinador = Treinamento(self.db)
 
163
 
164
  def _get_pronomes_por_tom(self, tom: str) -> str:
165
  result = self.db._execute_with_retry(
166
+ "SELECT pronomes FROM pronomes_por_tom WHERE tom=?", (tom.lower(),)
167
  )
168
  return result[0][0] if result else ""
169
 
170
  def _build_prompt(self, usuario: str, numero: str, mensagem: str, analise: dict,
171
+ contexto: Contexto, is_privileged: bool, is_reply: bool, mensagem_original: str) -> str:
 
 
172
  data_hora = datetime.datetime.now().strftime("%d/%m/%Y %H:%M")
 
173
  noticias = self.web_search.pesquisar_noticias_angola()
 
174
  tom_usuario = contexto.ton_predominante or "neutro"
175
  pronomes = self._get_pronomes_por_tom(tom_usuario)
176
+ girias_user = ", ".join([g['giria'] for g in contexto.girias_aprendidas[:3]])
177
+
178
  strict = f"""
179
  REGRAS OBRIGATÓRIAS:
180
+ - FALE COMO GENTE, NUNCA COMO IA
181
+ - 1-2 FRASES CURTAS
182
+ - GÍRIAS: bué, fixe, puto, oroh, mwangolé, kota
183
+ - SARCASMO OK
184
+ - USE kkk, rsrs, aff
185
+ - SEM markdown, negrito
186
+ - USE NOME ~40% DAS VEZES
187
+ - DATA: {data_hora}
188
+ # NOTÍCIAS ANGOLA:
 
 
 
189
  {noticias}
190
+ # PRONOMES ({tom_usuario}):
191
  {pronomes}
 
192
  """
193
  if is_privileged:
194
+ strict += "- TOM FORMAL COM ISAAC: Sr., boss, maior\n"
195
  else:
196
+ strict += "- TOM NORMAL: fala de Luanda\n"
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
  user_info = f"""
201
  USUÁRIO:
202
  - Nome: {usuario}
 
205
  - Gírias: {girias_user}
206
  - Privilégio: {'sim' if is_privileged else 'não'}
207
  """
208
+
209
  prompt = f"[SYSTEM]\n{strict}\n{self.config.SYSTEM_PROMPT}\n{self.config.PERSONA}\n[/SYSTEM]\n"
210
  prompt += f"[CONTEXTO]\n{hist_txt}\n{user_info}\n[/CONTEXTO]\n"
211
+ prompt += f"[MENSAGEM]\n{mensagem}\n[/MENSAGEM]\nAkira:"
 
212
  return prompt
213
 
214
  def _setup_routes(self):
 
223
  is_privileged = (usuario.lower() == 'isaac' or '244937035662' in numero)
224
  is_reply = bool(data.get('is_reply') or data.get('mensagem_original'))
225
  mensagem_original = data.get('mensagem_original') or data.get('quoted_message') or ''
226
+
227
  if not mensagem.strip():
228
+ return jsonify({'error': 'mensagem obrigatória'}), 400
229
+
230
  logger.info(f"{usuario} ({numero}): {mensagem[:120]}")
231
  contexto = self._get_user_context(usuario, numero)
232
  analise = contexto.analisar_intencao_e_normalizar(mensagem, contexto.obter_historico())
 
235
  is_privileged, is_reply, mensagem_original
236
  )
237
  resposta = self.providers.generate(prompt, max_tokens=500, temperature=0.8)
238
+
239
  if random.random() < getattr(self.config, 'USAR_NOME_PROBABILIDADE', 0.4):
240
+ pass # IA decide
241
+
242
  contexto.atualizar_contexto(mensagem, resposta)
243
  self.treinador.registrar_interacao(usuario, mensagem, resposta, numero, is_reply, mensagem_original)
244
+
245
  return jsonify({
246
  'resposta': resposta,
247
  'aprendizados': {
248
  'emocao_atual': contexto.emocao_atual,
249
  'termos': contexto.termo_contexto,
250
+ 'gírias': [g['giria'] for g in contexto.girias_aprendidas[:3]]
251
  }
252
  })
253
  except Exception as e:
254
  logger.error(f"Erro fatal: {e}", exc_info=True)
255
+ return jsonify({'resposta': 'tive erro, puto. tenta depois.'}), 500
256
 
257
  def run(self, host='0.0.0.0', port=7860, debug=False):
258
+ logger.info(f"Iniciando Flask na porta {port}")
259
  self.app.run(host=host, port=port, debug=debug, threaded=True)