Spaces:
Running
Running
Upload 22 files
Browse files- modules/api.py +8 -12
- modules/config.py +0 -0
- modules/local_llm.py +67 -20
- modules/persona_tracker.py +9 -3
- modules/unified_context.py +2 -1
modules/api.py
CHANGED
|
@@ -287,7 +287,7 @@ class LLMManager:
|
|
| 287 |
logger.warning(f"Together AI falhou: {e}")
|
| 288 |
self.together_client = None
|
| 289 |
|
| 290 |
-
def generate(self, user_prompt: str, context_history: List[dict] = [], is_privileged: bool = False) -> str:
|
| 291 |
"""
|
| 292 |
Gera resposta usando provedores LLM com fallback em loop.
|
| 293 |
|
|
@@ -727,19 +727,15 @@ class AkiraAPI:
|
|
| 727 |
# Captura robusta de JSON
|
| 728 |
raw_data = request.data
|
| 729 |
try:
|
| 730 |
-
#
|
| 731 |
data = request.get_json(force=True, silent=True)
|
| 732 |
if data is None:
|
| 733 |
-
|
|
|
|
|
|
|
| 734 |
except Exception as e:
|
| 735 |
-
self.logger.
|
| 736 |
-
|
| 737 |
-
# Tenta UTF-8 ignorando erros ou Latin-1 como fallback comum de terminais Windows
|
| 738 |
-
decoded = raw_data.decode('utf-8', errors='ignore')
|
| 739 |
-
data = json.loads(decoded)
|
| 740 |
-
except Exception as e2:
|
| 741 |
-
self.logger.error(f"[API] Falha crítica ao decodificar JSON: {e2} | Bruto: {raw_data[:200]}")
|
| 742 |
-
data = {}
|
| 743 |
|
| 744 |
if not data:
|
| 745 |
raw_str = request.data.decode('latin-1', errors='replace') if request.data else "Vazio"
|
|
@@ -1362,7 +1358,7 @@ class AkiraAPI:
|
|
| 1362 |
context_hint: str = "",
|
| 1363 |
tipo_conversa: str = "pv",
|
| 1364 |
tem_imagem: bool = False,
|
| 1365 |
-
analise_visao: Dict[str, Any] = None,
|
| 1366 |
analise_doc: str = "",
|
| 1367 |
unified_context = None
|
| 1368 |
) -> str:
|
|
|
|
| 287 |
logger.warning(f"Together AI falhou: {e}")
|
| 288 |
self.together_client = None
|
| 289 |
|
| 290 |
+
def generate(self, user_prompt: str, context_history: List[dict] = [], is_privileged: bool = False) -> Tuple[str, str]:
|
| 291 |
"""
|
| 292 |
Gera resposta usando provedores LLM com fallback em loop.
|
| 293 |
|
|
|
|
| 727 |
# Captura robusta de JSON
|
| 728 |
raw_data = request.data
|
| 729 |
try:
|
| 730 |
+
# Tenta extrair o JSON perfeitamente
|
| 731 |
data = request.get_json(force=True, silent=True)
|
| 732 |
if data is None:
|
| 733 |
+
# Se falhou, tenta decodificar manualmente o bruto
|
| 734 |
+
decoded = raw_data.decode('utf-8', errors='ignore').strip()
|
| 735 |
+
data = json.loads(decoded) if decoded else {}
|
| 736 |
except Exception as e:
|
| 737 |
+
self.logger.error(f"[API] Falha crítica ao decodificar JSON: {e} | Bruto: {raw_data[:200]}")
|
| 738 |
+
data = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 739 |
|
| 740 |
if not data:
|
| 741 |
raw_str = request.data.decode('latin-1', errors='replace') if request.data else "Vazio"
|
|
|
|
| 1358 |
context_hint: str = "",
|
| 1359 |
tipo_conversa: str = "pv",
|
| 1360 |
tem_imagem: bool = False,
|
| 1361 |
+
analise_visao: Optional[Dict[str, Any]] = None,
|
| 1362 |
analise_doc: str = "",
|
| 1363 |
unified_context = None
|
| 1364 |
) -> str:
|
modules/config.py
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
modules/local_llm.py
CHANGED
|
@@ -137,7 +137,8 @@ class LocalLLMFallback:
|
|
| 137 |
if cls._instance is None:
|
| 138 |
cls._instance = super().__new__(cls)
|
| 139 |
cls._instance._initialized = False
|
| 140 |
-
|
|
|
|
| 141 |
return cls._instance
|
| 142 |
|
| 143 |
def __init__(self):
|
|
@@ -147,7 +148,10 @@ class LocalLLMFallback:
|
|
| 147 |
|
| 148 |
# Componentes do modelo
|
| 149 |
self._model = None # type: ignore
|
| 150 |
-
self._model_path = None
|
|
|
|
|
|
|
|
|
|
| 151 |
self._is_loaded = False
|
| 152 |
self._tokenizer = None # type: ignore
|
| 153 |
self._pipeline = None # type: ignore
|
|
@@ -165,7 +169,7 @@ class LocalLLMFallback:
|
|
| 165 |
self._hf_client = None
|
| 166 |
|
| 167 |
# Estatísticas
|
| 168 |
-
self._stats = {
|
| 169 |
"total_calls": 0,
|
| 170 |
"successful_calls": 0,
|
| 171 |
"failed_calls": 0,
|
|
@@ -180,7 +184,13 @@ class LocalLLMFallback:
|
|
| 180 |
"""Configura o fallback via Cloud API (Hugging Face Inference)."""
|
| 181 |
logger.info("Local LLM: Configurando fallback exclusivo via HuggingFace Cloud API.")
|
| 182 |
|
| 183 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
|
| 185 |
if hf_token:
|
| 186 |
self._is_hf_inference_mode = True
|
|
@@ -248,7 +258,13 @@ class LocalLLMFallback:
|
|
| 248 |
formatted = f"<|system|>\n{sys_prompt}</s>\n<|user|>\n{prompt}</s>\n<|assistant|>\n"
|
| 249 |
|
| 250 |
if getattr(self, '_is_hf_inference_mode', False):
|
| 251 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
if not hf_token:
|
| 253 |
logger.error("❌ Token HF não encontrado para a requisição de inferência HF")
|
| 254 |
# Tentar prosseguir sem token se for modelo free (geralmente Llama 3 precisa)
|
|
@@ -284,22 +300,46 @@ class LocalLLMFallback:
|
|
| 284 |
candidate_models = []
|
| 285 |
|
| 286 |
long_prompt = prompt.count('\n') >= 4 or len(prompt) > 800
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 291 |
]
|
| 292 |
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
if prefer_heavy or long_prompt or any(x in prompt.lower() for x in trigger_keywords):
|
| 296 |
-
candidate_models.extend([self._heavy_model, self._portuguese_model, self._multilingual_beast])
|
| 297 |
|
| 298 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 299 |
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
candidate_models.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
|
| 304 |
for current_model in candidate_models:
|
| 305 |
for provider in providers:
|
|
@@ -308,16 +348,23 @@ class LocalLLMFallback:
|
|
| 308 |
current_messages = messages.copy()
|
| 309 |
|
| 310 |
# Se for modelo Luana ou Mistral, aplicamos o template [INST] conforme a documentação
|
| 311 |
-
|
|
|
|
| 312 |
# Para Mistral via Chat API, geralmente o provedor já cuida da conversão,
|
| 313 |
# mas podemos reforçar na primeira mensagem se necessário.
|
| 314 |
# No caso da Luana específica, ela gosta do formato "Abaixo está uma instrução..."
|
| 315 |
-
if "luana" in
|
| 316 |
instruction = f"Abaixo está uma instrução que descreve uma tarefa, juntamente com uma entrada que fornece mais contexto.\nEscreva uma resposta que complete adequadamente o pedido.\n### instrução: {sys_prompt}\n### entrada: {prompt}"
|
| 317 |
current_messages = [{"role": "user", "content": instruction}]
|
| 318 |
|
| 319 |
# Extrair parâmetros específicos do modelo injetando agressividade e coerência
|
| 320 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 321 |
|
| 322 |
payload = {
|
| 323 |
"model": model_with_provider,
|
|
|
|
| 137 |
if cls._instance is None:
|
| 138 |
cls._instance = super().__new__(cls)
|
| 139 |
cls._instance._initialized = False
|
| 140 |
+
import threading as _threading
|
| 141 |
+
cls._instance._model_lock = _threading.Lock()
|
| 142 |
return cls._instance
|
| 143 |
|
| 144 |
def __init__(self):
|
|
|
|
| 148 |
|
| 149 |
# Componentes do modelo
|
| 150 |
self._model = None # type: ignore
|
| 151 |
+
self._model_path: Optional[str] = None
|
| 152 |
+
self._heavy_model: Optional[str] = None
|
| 153 |
+
self._portuguese_model: Optional[str] = None
|
| 154 |
+
self._multilingual_beast: Optional[str] = None
|
| 155 |
self._is_loaded = False
|
| 156 |
self._tokenizer = None # type: ignore
|
| 157 |
self._pipeline = None # type: ignore
|
|
|
|
| 169 |
self._hf_client = None
|
| 170 |
|
| 171 |
# Estatísticas
|
| 172 |
+
self._stats: Dict[str, Any] = {
|
| 173 |
"total_calls": 0,
|
| 174 |
"successful_calls": 0,
|
| 175 |
"failed_calls": 0,
|
|
|
|
| 184 |
"""Configura o fallback via Cloud API (Hugging Face Inference)."""
|
| 185 |
logger.info("Local LLM: Configurando fallback exclusivo via HuggingFace Cloud API.")
|
| 186 |
|
| 187 |
+
try:
|
| 188 |
+
import importlib as _iloc
|
| 189 |
+
_cfgloc = _iloc.import_module('modules.config')
|
| 190 |
+
_hf_fallback = getattr(_cfgloc, 'HF_TOKEN', None)
|
| 191 |
+
except Exception:
|
| 192 |
+
_hf_fallback = None
|
| 193 |
+
hf_token: Optional[str] = os.getenv("HF_TOKEN") or _hf_fallback
|
| 194 |
|
| 195 |
if hf_token:
|
| 196 |
self._is_hf_inference_mode = True
|
|
|
|
| 258 |
formatted = f"<|system|>\n{sys_prompt}</s>\n<|user|>\n{prompt}</s>\n<|assistant|>\n"
|
| 259 |
|
| 260 |
if getattr(self, '_is_hf_inference_mode', False):
|
| 261 |
+
try:
|
| 262 |
+
import importlib as _il2
|
| 263 |
+
_cfg2 = _il2.import_module('modules.config')
|
| 264 |
+
_hf2 = getattr(_cfg2, 'HF_TOKEN', None)
|
| 265 |
+
except Exception:
|
| 266 |
+
_hf2 = None
|
| 267 |
+
hf_token = os.getenv("HF_TOKEN") or _hf2
|
| 268 |
if not hf_token:
|
| 269 |
logger.error("❌ Token HF não encontrado para a requisição de inferência HF")
|
| 270 |
# Tentar prosseguir sem token se for modelo free (geralmente Llama 3 precisa)
|
|
|
|
| 300 |
candidate_models = []
|
| 301 |
|
| 302 |
long_prompt = prompt.count('\n') >= 4 or len(prompt) > 800
|
| 303 |
+
prompt_lower = prompt.lower()
|
| 304 |
+
|
| 305 |
+
# Palavras-chave exclusivas para o modelo Pesado (Qwen Abliterated: debates, polêmicas, ciência, etc)
|
| 306 |
+
heavy_keywords = [
|
| 307 |
+
"debate", "discuti", "ideia", "explica", "acadêmica", "religião", "científica", "ciência", "filosófico", "filosofia",
|
| 308 |
+
"ética", "etica", "moral", "radical", "polêmica", "polemica", "país", "analise", "fale sobre", "codigo", "programar",
|
| 309 |
+
"política", "politi", "guerra", "mundo"
|
| 310 |
+
]
|
| 311 |
+
# Palavras-chave exclusivas para Luna (Girias, cultura PT/AO/BR, informalidade)
|
| 312 |
+
luna_keywords = [
|
| 313 |
+
"kkk", "mn", "informal", "foda", "lá ele", "puto", "mano", "porra", "fxe", "cassule"
|
| 314 |
]
|
| 315 |
|
| 316 |
+
needs_heavy = long_prompt or any(x in prompt_lower for x in heavy_keywords)
|
| 317 |
+
needs_luna = any(x in prompt_lower for x in luna_keywords)
|
|
|
|
|
|
|
| 318 |
|
| 319 |
+
# Verifica a flag global de preferência por modelos pesados
|
| 320 |
+
try:
|
| 321 |
+
import importlib as _il3
|
| 322 |
+
_cfg3 = _il3.import_module('modules.config')
|
| 323 |
+
prefer_heavy: bool = bool(getattr(_cfg3, 'PREFER_HEAVY_MODEL', False))
|
| 324 |
+
except Exception:
|
| 325 |
+
prefer_heavy = False
|
| 326 |
|
| 327 |
+
if needs_heavy:
|
| 328 |
+
# MENSAGEM COMPLEXA/SEM CENSURA/DEBATE: Qwen -> Llama 70B -> Luna -> Lexi
|
| 329 |
+
candidate_models.extend([self._heavy_model, "meta-llama/Llama-3.3-70B-Instruct", self._portuguese_model, base_model])
|
| 330 |
+
elif needs_luna:
|
| 331 |
+
# MENSAGEM CULTURAL/GÍRIA EM PT: Luna -> Lexi -> Qwen
|
| 332 |
+
candidate_models.extend([self._portuguese_model, base_model, self._heavy_model])
|
| 333 |
+
else:
|
| 334 |
+
# MENSAGEM SIMPLES (TROCA ADAPTATIVA):
|
| 335 |
+
if prefer_heavy:
|
| 336 |
+
candidate_models.extend([base_model, self._heavy_model, self._portuguese_model])
|
| 337 |
+
else:
|
| 338 |
+
candidate_models.extend([base_model, self._portuguese_model, self._heavy_model])
|
| 339 |
+
|
| 340 |
+
# Garantir apenas modelos únicos mantendo a ordem
|
| 341 |
+
seen = set()
|
| 342 |
+
candidate_models = [x for x in candidate_models if not (x in seen or seen.add(x))]
|
| 343 |
|
| 344 |
for current_model in candidate_models:
|
| 345 |
for provider in providers:
|
|
|
|
| 348 |
current_messages = messages.copy()
|
| 349 |
|
| 350 |
# Se for modelo Luana ou Mistral, aplicamos o template [INST] conforme a documentação
|
| 351 |
+
_cm = str(current_model) if current_model else ""
|
| 352 |
+
if "mistral" in _cm.lower() or "luana" in _cm.lower():
|
| 353 |
# Para Mistral via Chat API, geralmente o provedor já cuida da conversão,
|
| 354 |
# mas podemos reforçar na primeira mensagem se necessário.
|
| 355 |
# No caso da Luana específica, ela gosta do formato "Abaixo está uma instrução..."
|
| 356 |
+
if "luana" in _cm.lower():
|
| 357 |
instruction = f"Abaixo está uma instrução que descreve uma tarefa, juntamente com uma entrada que fornece mais contexto.\nEscreva uma resposta que complete adequadamente o pedido.\n### instrução: {sys_prompt}\n### entrada: {prompt}"
|
| 358 |
current_messages = [{"role": "user", "content": instruction}]
|
| 359 |
|
| 360 |
# Extrair parâmetros específicos do modelo injetando agressividade e coerência
|
| 361 |
+
try:
|
| 362 |
+
import importlib as _il
|
| 363 |
+
_cfg = _il.import_module('modules.config')
|
| 364 |
+
_all_params: dict = getattr(_cfg, 'MODEL_PARAMETERS', {})
|
| 365 |
+
except Exception:
|
| 366 |
+
_all_params = {}
|
| 367 |
+
model_params: Dict[str, Any] = dict(_all_params.get(current_model, {}))
|
| 368 |
|
| 369 |
payload = {
|
| 370 |
"model": model_with_provider,
|
modules/persona_tracker.py
CHANGED
|
@@ -86,9 +86,15 @@ Retorne APENAS um JSON válido estruturado assim (e NADA de texto fora das chave
|
|
| 86 |
"""
|
| 87 |
|
| 88 |
# Chama o LLM (garante formato json)
|
| 89 |
-
#
|
| 90 |
-
|
| 91 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
|
| 93 |
# Extrai o JSON (Robusto contra texto extra, markdown e quebras parciais)
|
| 94 |
response_clean = response_json_str.strip()
|
|
|
|
| 86 |
"""
|
| 87 |
|
| 88 |
# Chama o LLM (garante formato json)
|
| 89 |
+
# Agora retorna (resposta, modelo_usado) ou apenas resposta
|
| 90 |
+
response_raw = self.llm_client.generate(prompt, [])
|
| 91 |
+
if isinstance(response_raw, tuple):
|
| 92 |
+
response_json_str = response_raw[0]
|
| 93 |
+
else:
|
| 94 |
+
response_json_str = response_raw
|
| 95 |
+
|
| 96 |
+
if not response_json_str:
|
| 97 |
+
return
|
| 98 |
|
| 99 |
# Extrai o JSON (Robusto contra texto extra, markdown e quebras parciais)
|
| 100 |
response_clean = response_json_str.strip()
|
modules/unified_context.py
CHANGED
|
@@ -185,6 +185,7 @@ class UnifiedMessageContext:
|
|
| 185 |
# Mensagem atual
|
| 186 |
current_message: str = ""
|
| 187 |
current_emotion: str = "neutral"
|
|
|
|
| 188 |
|
| 189 |
def to_dict(self) -> Dict[str, Any]:
|
| 190 |
"""Serializa para dicionário."""
|
|
@@ -434,7 +435,7 @@ class ShortTermMemoryManager:
|
|
| 434 |
content: str,
|
| 435 |
emocao: str = "neutral",
|
| 436 |
reply_info: Optional[Dict] = None,
|
| 437 |
-
importancia: float = None
|
| 438 |
) -> MessageWithContext:
|
| 439 |
"""
|
| 440 |
Adiciona mensagem à STM de uma conversa.
|
|
|
|
| 185 |
# Mensagem atual
|
| 186 |
current_message: str = ""
|
| 187 |
current_emotion: str = "neutral"
|
| 188 |
+
system_override: str = ""
|
| 189 |
|
| 190 |
def to_dict(self) -> Dict[str, Any]:
|
| 191 |
"""Serializa para dicionário."""
|
|
|
|
| 435 |
content: str,
|
| 436 |
emocao: str = "neutral",
|
| 437 |
reply_info: Optional[Dict] = None,
|
| 438 |
+
importancia: Optional[float] = None
|
| 439 |
) -> MessageWithContext:
|
| 440 |
"""
|
| 441 |
Adiciona mensagem à STM de uma conversa.
|