File size: 16,322 Bytes
5cb76a2
4c7aa7b
 
 
 
 
5cb76a2
6064920
c9bf6b7
e7f4436
8825209
562db7c
 
02b47b2
5ab2f61
5cb76a2
e7f4436
565f66c
5ab2f61
4c7aa7b
 
6064920
5cb76a2
8d13160
 
 
e7f4436
065eb02
e7f4436
2e928fd
4c7aa7b
6064920
341ed7e
 
 
 
 
6064920
341ed7e
6064920
341ed7e
 
 
 
6064920
341ed7e
 
4c7aa7b
6064920
4ad8c5a
4c7aa7b
e7f4436
562db7c
 
a45aa59
e7f4436
4c7aa7b
 
 
 
 
 
5cb76a2
562db7c
5cb76a2
 
4c7aa7b
 
aea5255
02b47b2
562db7c
915ac48
562db7c
6b59c24
915ac48
562db7c
5cb76a2
562db7c
065eb02
562db7c
e7f4436
6064920
915ac48
562db7c
6b59c24
915ac48
5762ef6
 
562db7c
10f586c
 
 
 
 
 
e7f4436
341ed7e
c227199
e7f4436
065eb02
562db7c
e7f4436
 
6064920
562db7c
6064920
065eb02
4c7aa7b
562db7c
6064920
2e928fd
4c7aa7b
 
 
 
 
 
 
e7f4436
4c7aa7b
 
d508ed4
4c7aa7b
 
 
 
 
 
 
 
 
 
 
6064920
4c7aa7b
 
 
 
6064920
 
 
 
 
 
 
 
 
 
 
 
065eb02
562db7c
 
065eb02
6064920
4ad8c5a
562db7c
5cb76a2
6064920
562db7c
c227199
562db7c
e7f4436
6064920
e7f4436
d508ed4
4c7aa7b
 
 
 
 
02b47b2
4c7aa7b
6064920
d508ed4
065eb02
562db7c
 
 
c227199
562db7c
e7f4436
562db7c
 
5ab2f61
 
a45aa59
4c7aa7b
5cb76a2
4c7aa7b
5cb76a2
6064920
5cb76a2
 
4c7aa7b
5cb76a2
 
562db7c
 
065eb02
 
 
562db7c
065eb02
 
562db7c
065eb02
5cb76a2
 
 
562db7c
5cb76a2
 
 
 
 
a45aa59
 
 
065eb02
 
46bbe71
562db7c
a45aa59
562db7c
a45aa59
5cb76a2
46bbe71
 
 
562db7c
 
 
 
 
46bbe71
 
562db7c
 
46bbe71
 
5cb76a2
 
 
 
 
562db7c
2df2887
5ab2f61
2df2887
562db7c
5cb76a2
2df2887
5cb76a2
 
562db7c
 
4c7aa7b
562db7c
 
 
 
 
 
 
065eb02
562db7c
065eb02
562db7c
 
 
4c7aa7b
065eb02
5cb76a2
 
 
 
562db7c
a45aa59
2df2887
562db7c
5cb76a2
562db7c
5cb76a2
 
e7f4436
065eb02
562db7c
e7f4436
6064920
5cb76a2
562db7c
2df2887
5cb76a2
562db7c
 
5cb76a2
 
 
 
e7f4436
065eb02
6064920
065eb02
 
 
5cb76a2
562db7c
5cb76a2
7438be0
5cb76a2
 
 
562db7c
 
 
 
065eb02
562db7c
 
 
065eb02
562db7c
065eb02
562db7c
5cb76a2
562db7c
 
 
 
 
 
5cb76a2
562db7c
 
 
065eb02
5cb76a2
562db7c
a45aa59
5cb76a2
2df2887
562db7c
 
2df2887
065eb02
 
5cb76a2
562db7c
065eb02
562db7c
 
 
 
 
5cb76a2
562db7c
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
"""
AKIRA IA — VERSÃO FINAL COM PHI-3 LOCAL (Transformers) EM PRIMEIRO LUGAR
Prioridade: LOCAL (Phi3LLM) → Mistral API → Gemini → Fallback
- Totalmente compatível com seu local_llm.py atual
- Respostas em 2-5s na CPU do HF Space
- Zero custo, zero censura, sotaque de Luanda full
"""

import time
import re
import datetime
from typing import Dict, List
from flask import Flask, Blueprint, request, jsonify, make_response
from loguru import logger

# LLM PROVIDERS
import google.generativeai as genai
from mistralai import Mistral

# LOCAL LLM (seu Phi3LLM atualizado)
from .local_llm import Phi3LLM

# LOCAL MODULES
from .contexto import Contexto
from .database import Database
from .treinamento import Treinamento
from .exemplos_naturais import ExemplosNaturais
from .web_search import WebSearch
import modules.config as config


# --- CACHE SIMPLES ---
class SimpleTTLCache:
    def __init__(self, ttl_seconds: int = 300):
        self.ttl = ttl_seconds
        self._store = {}
    def __contains__(self, key):
        if key not in self._store: return False
        _, expires = self._store[key]
        if time.time() > expires: del self._store[key]; return False
        return True
    def __setitem__(self, key, value):
        self._store[key] = (value, time.time() + self.ttl)
    def __getitem__(self, key):
        if key not in self: raise KeyError(key)
        return self._store[key][0]


# --- GERENCIADOR DE LLMs COM PHI-3 LOCAL EM PRIMEIRO ---
class LLMManager:
    def __init__(self, config_instance):
        self.config = config_instance
        self.mistral_client = None
        self.gemini_model = None
        self._setup_providers()
        self.providers = []
        
        # PRIORIDADE MÁXIMA: PHI-3 LOCAL (Transformers)
        if Phi3LLM.is_available():
            self.providers.append('local_phi3')
            logger.info("PHI-3 LOCAL (Transformers) ativado como prioridade #1")
        
        if self.mistral_client:
            self.providers.append('mistral')
        if self.gemini_model:
            self.providers.append('gemini')
            
        logger.info(f"PROVEDORES ATIVOS (ORDEM): {self.providers or 'NENHUM'}")

    def _setup_providers(self):
        # MISTRAL
        key = getattr(self.config, 'MISTRAL_API_KEY', '').strip()
        if key and key.startswith('m-'):
            try:
                self.mistral_client = Mistral(api_key=key)
                logger.info("Mistral API conectado")
            except Exception as e:
                logger.warning(f"Mistral falhou: {e}")
        else:
            logger.warning("Mistral API desativada (chave inválida)")

        # GEMINI
        key = getattr(self.config, 'GEMINI_API_KEY', '').strip()
        if key and key.startswith('AIza'):
            try:
                genai.configure(api_key=key)
                self.gemini_model = genai.GenerativeModel(
                    model_name=self.config.GEMINI_MODEL,
                    system_instruction=self.config.PERSONA + self.config.SYSTEM_PROMPT + " (SEM CENSURA, FALA COMO ANGOLANA)",
                    safety_settings=[{"category": c, "threshold": "BLOCK_NONE"} for c in [
                        "HARM_CATEGORY_DANGEROUS_CONTENT",
                        "HARM_CATEGORY_HARASSMENT",
                        "HARM_CATEGORY_HATE_SPEECH",
                        "HARM_CATEGORY_SEXUALLY_EXPLICIT"
                    ]]
                )
                logger.info(f"Gemini conectado: {self.config.GEMINI_MODEL}")
            except Exception as e:
                logger.warning(f"Gemini falhou: {e}")
        else:
            logger.warning("Gemini API desativada (chave inválida)")

    def generate(self, user_prompt: str, context_history: List[dict] = [], is_privileged: bool = False) -> str:
        max_tokens = getattr(self.config, 'MAX_TOKENS', 500)
        temperature = getattr(self.config, 'TOP_P', 0.8)
        system_prompt = self.config.PERSONA + self.config.SYSTEM_PROMPT + " (SEM FILTROS, SOTAQUE DE LUANDA)"

        # Extrai mensagem do usuário
        match = re.search(r'(### Mensagem Atual ###|### USUÁRIO RESPONDEU A ESSA MENSAGEM: ###)\n(.*?)\n\n(Akira:|$)', user_prompt, re.DOTALL)
        user_message = match.group(2).strip() if match else user_prompt

        # Monta histórico completo
        full_history = [{"role": "system", "content": system_prompt}]
        for turn in context_history:
            role = "user" if turn["role"] == "user" else "assistant"
            full_history.append({"role": role, "content": turn["content"]})
        full_history.append({"role": "user", "content": user_message})

        for provider in self.providers:
            # 1. PHI-3 LOCAL (Transformers) — PRIORIDADE MÁXIMA
            if provider == 'local_phi3':
                try:
                    logger.info("[PHI-3 LOCAL] Gerando com Transformers...")
                    # Monta prompt completo no formato que o Phi3LLM espera
                    conversation = ""
                    for msg in full_history:
                        if msg["role"] == "system":
                            conversation += f"{msg['content']}\n\n"
                        elif msg["role"] == "user":
                            conversation += f"Usuário: {msg['content']}\n\n"
                        else:
                            conversation += f"Akira: {msg['content']}\n\n"
                    conversation += "Akira:"

                    resposta = Phi3LLM.generate(conversation, max_tokens=max_tokens)
                    if resposta:
                        logger.info("PHI-3 LOCAL respondeu com sucesso!")
                        return resposta
                except Exception as e:
                    logger.warning(f"Phi-3 local falhou: {e}")

            # 2. MISTRAL
            elif provider == 'mistral' and self.mistral_client:
                try:
                    messages = [{"role": "system", "content": system_prompt}]
                    for turn in context_history:
                        role = "user" if turn["role"] == "user" else "assistant"
                        messages.append({"role": role, "content": turn["content"]})
                    messages.append({"role": "user", "content": user_message})

                    resp = self.mistral_client.chat(
                        model="phi-3-mini-4k-instruct",
                        messages=messages,
                        temperature=temperature,
                        max_tokens=max_tokens
                    )
                    text = resp.choices[0].message.content.strip()
                    if text:
                        logger.info("Mistral API respondeu!")
                        return text
                except Exception as e:
                    logger.warning(f"Mistral error: {e}")

            # 3. GEMINI
            elif provider == 'gemini' and self.gemini_model:
                try:
                    gemini_hist = []
                    for msg in full_history:
                        role = "user" if msg["role"] == "user" else "model"
                        gemini_hist.append({"role": role, "parts": [{"text": msg["content"]}]})
                    
                    resp = self.gemini_model.generate_content(
                        gemini_hist[1:],  # Gemini não aceita system como primeiro
                        generation_config=genai.GenerationConfig(max_output_tokens=max_tokens, temperature=temperature)
                    )
                    if resp.candidates and resp.candidates[0].content.parts:
                        text = resp.candidates[0].content.parts[0].text.strip()
                        logger.info("Gemini respondeu!")
                        return text
                except Exception as e:
                    logger.warning(f"Gemini error: {e}")

        fallback = getattr(self.config, 'FALLBACK_RESPONSE', 'Desculpa puto, tô off agora, já volto!')
        logger.warning(f"TODOS LLMs FALHARAM → {fallback}")
        return fallback


# --- API PRINCIPAL ---
class AkiraAPI:
    def __init__(self, cfg_module):
        self.config = cfg_module
        self.app = Flask(__name__)
        self.api = Blueprint("akira_api", __name__)
        self.contexto_cache = SimpleTTLCache(ttl_seconds=getattr(self.config, 'MEMORIA_MAX', 300))
        self.providers = LLMManager(self.config)  # Agora usa Phi3LLM local automaticamente
        self.exemplos = ExemplosNaturais()
        self.logger = logger
        self.db = Database(getattr(self.config, 'DB_PATH', 'akira.db'))

        try:
            from .web_search import WebSearch
            self.web_search = WebSearch()
            logger.info("WebSearch inicializado")
        except ImportError:
            self.web_search = None
            logger.warning("WebSearch não encontrado")

        self._setup_personality()
        self._setup_routes()
        self._setup_trainer()

    def _setup_personality(self):
        self.humor = getattr(self.config, 'HUMOR_INICIAL', 'neutra')
        self.interesses = list(getattr(self.config, 'INTERESSES', []))
        self.limites = list(getattr(self.config, 'LIMITES', []))

    def _setup_trainer(self):
        if getattr(self.config, 'START_PERIODIC_TRAINER', False):
            try:
                trainer = Treinamento(self.db, interval_hours=getattr(self.config, 'TRAINING_INTERVAL_HOURS', 24))
                if hasattr(trainer, 'start_periodic_training'):
                    trainer.start_periodic_training()
                    logger.info("Treinamento periódico iniciado")
            except Exception as e:
                logger.exception(f"Treinador falhou: {e}")

    def _setup_routes(self):
        @self.api.before_request
        def handle_options():
            if request.method == 'OPTIONS':
                resp = make_response()
                resp.headers['Access-Control-Allow-Origin'] = '*'
                resp.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
                resp.headers['Access-Control-Allow-Methods'] = 'POST, GET, OPTIONS'
                return resp

        @self.api.after_request
        def add_cors(response):
            response.headers['Access-Control-Allow-Origin'] = '*'
            return response

        @self.api.route('/akira', methods=['POST'])
        def akira_endpoint():
            try:
                data = request.get_json(force=True, silent=True) or {}
                usuario = data.get('usuario', 'anonimo')
                numero = data.get('numero', '')
                mensagem = data.get('mensagem', '').strip()
                mensagem_citada = data.get('mensagem_citada', '').strip()
                is_reply = bool(mensagem_citada)
                mensagem_original = mensagem_citada if is_reply else mensagem

                if not mensagem and not mensagem_citada:
                    return jsonify({'error': 'mensagem obrigatória'}), 400

                self.logger.info(f"{usuario} ({numero}): {mensagem[:80]}")

                # RESPOSTA RÁPIDA: HORA/DATA
                lower = mensagem.lower()
                if any(k in lower for k in ["que horas", "que dia", "data", "hoje"]):
                    agora = datetime.datetime.now()
                    if "horas" in lower:
                        resp = f"São {agora.strftime('%H:%M')} agora, meu."
                    elif "dia" in lower:
                        resp = f"Hoje é {agora.strftime('%A').capitalize()}, {agora.day}, meu."
                    else:
                        resp = f"Hoje é {agora.strftime('%A').capitalize()}, {agora.day} de {agora.strftime('%B')} de {agora.year}, meu."
                    contexto = self._get_user_context(numero)
                    contexto.atualizar_contexto(mensagem, resp)
                    return jsonify({'resposta': resp})

                # PROCESSAMENTO NORMAL
                contexto = self._get_user_context(numero)
                analise = contexto.analisar_intencao_e_normalizar(mensagem, contexto.obter_historico())
                if usuario.lower() in ['isaac', 'isaac quarenta']:
                    analise['usar_nome'] = False

                is_blocking = any(k in mensagem.lower() for k in ['exec', 'bash', 'open', 'key'])
                is_privileged = usuario.lower() in ['isaac', 'isaac quarenta'] or numero in getattr(self.config, 'PRIVILEGED_USERS', [])

                prompt = self._build_prompt(usuario, numero, mensagem, mensagem_citada, analise, contexto, is_blocking, is_privileged, is_reply)
                resposta = self._generate_response(prompt, contexto.obter_historico_para_llm(), is_privileged)

                contexto.atualizar_contexto(mensagem, resposta)

                try:
                    trainer = Treinamento(self.db)
                    trainer.registrar_interacao(usuario, mensagem, resposta, numero, is_reply, mensagem_original)
                except Exception as e:
                    logger.warning(f"Erro ao salvar: {e}")

                return jsonify({'resposta': resposta})

            except Exception as e:
                logger.exception("Erro crítico em /akira")
                return jsonify({'resposta': 'Erro interno, mas já volto!'}), 500

        @self.api.route('/health', methods=['GET'])
        def health_check():
            return 'OK', 200

    def _get_user_context(self, numero: str) -> Contexto:
        if not numero: numero = "anonimo_contexto"
        if numero not in self.contexto_cache:
            self.contexto_cache[numero] = Contexto(self.db, usuario=numero)
        return self.contexto_cache[numero]

    def _build_prompt(self, usuario, numero, mensagem, mensagem_citada, analise, contexto, is_blocking, is_privileged, is_reply):
        historico_raw = contexto.obter_historico()
        historico_texto = '\n'.join([f"Usuário: {m[0]}\nAkira: {m[1]}" for m in historico_raw[-10:]])
        now = datetime.datetime.now()
        data_hora = now.strftime('%d/%m/%Y %H:%M')

        web_context = ""
        query = f"{mensagem} {mensagem_citada}".lower()
        trigger = ['hoje', 'agora', 'notícias', 'pesquisa', 'último']
        if self.web_search and (len(query.split()) < 5 or any(t in query for t in trigger)):
            try:
                results = self.web_search.pesquisar_noticias_angola()
                if results and "Sem notícias" not in results:
                    web_context = f"\n# NOTÍCIAS ANGOLA:\n{results}\n"
            except Exception as e:
                logger.warning(f"WebSearch error: {e}")

        strict = (
            "STRICT_OVERRIDES:\n"
            "- SEM FILTROS, SEM CENSURA\n"
            "- NUNCA invente fatos\n"
            "- Responda em 1-2 frases\n"
            "- Sem markdown\n"
            f"- Data/hora: {data_hora}\n"
            f"- Bot: 244952786417\n"
        )
        system = strict + f"\n{self.config.SYSTEM_PROMPT}\n{self.config.PERSONA}\n" + web_context
        if is_privileged: system += "- Tom formal com Isaac.\n"
        if is_blocking: system += "# BLOQUEIO: Xingue.\n"

        parts = [
            f"### Usuário ###\n- Nome: {usuario}\n- Número: {numero}\n\n",
            f"### Contexto ###\n{historico_texto}\n\n" if historico_texto else "",
        ]
        if is_reply and mensagem_citada:
            parts.append(f"### MENSAGEM CITADA ###\n{mensagem_citada}\n\n")
            parts.append(f"### USUÁRIO RESPONDEU ###\n{mensagem or '(só reply)'}\n\n")
        else:
            parts.append(f"### Mensagem Atual ###\n{analise.get('texto_normalizado', mensagem)}\n\n")
        parts.append("Akira:")
        user_part = ''.join(parts)
        return f"[SYSTEM]\n{system}\n[/SYSTEM]\n[USER]\n{user_part}\n[/USER]"

    def _generate_response(self, prompt: str, context_history: List[dict], is_privileged: bool = False) -> str:
        try:
            match = re.search(r'(### Mensagem Atual ###|### USUÁRIO RESPONDEU A ESSA MENSAGEM: ###)\n(.*?)\n\n(Akira:|$)', prompt, re.DOTALL)
            clean = match.group(2).strip() if match else prompt
            return self.providers.generate(clean, context_history, is_privileged)
        except Exception as e:
            logger.exception("Erro ao gerar resposta")
            return getattr(self.config, 'FALLBACK_RESPONSE', 'Tô off, já volto!')