# type: ignore """ Aprendizado contínuo simples para AKIRA V21 - Registra todas as mensagens (PV/Grupo), replies e respostas geradas - Persiste em JSONL em data/continuous_learning.jsonl - Fornece contexto global resumido para alimentar o LLM quando solicitado - Sugere melhor API baseada em heurísticas leves """ import os import json import time import threading from pathlib import Path from typing import Optional, Dict, Any, List try: import modules.config as config except ImportError: # fallback relativo import config DATA_DIR: Path = getattr(config, 'DATA_DIR', Path('./data')) DATA_DIR.mkdir(parents=True, exist_ok=True) JSONL_PATH: Path = DATA_DIR / 'continuous_learning.jsonl' LOCK = threading.Lock() class AprendizadoContinuo: def __init__(self, jsonl_path: Path): self.path = jsonl_path self.path.parent.mkdir(parents=True, exist_ok=True) # índice leve em memória (opcional) self._buffer: List[Dict[str, Any]] = [] self._buffer_limit = 2000 def _append_jsonl(self, row: Dict[str, Any]) -> None: with LOCK: with self.path.open('a', encoding='utf-8') as f: f.write(json.dumps(row, ensure_ascii=False) + '\n') self._buffer.append(row) if len(self._buffer) > self._buffer_limit: self._buffer = self._buffer[-self._buffer_limit:] def _now_ts(self) -> float: return time.time() def processar_mensagem( self, mensagem: str, usuario: str, numero: str, nome_usuario: Optional[str] = None, tipo_conversa: str = 'pv', # 'pv' ou 'grupo' resposta_do_bot: bool = False, resposta_gerada: Optional[str] = None, is_reply: bool = False, reply_to_bot: bool = False, contexto_grupo: Optional[str] = None, modelo_usado: Optional[str] = None, ) -> Dict[str, Any]: """Registra evento para aprendizado contínuo e retorna análise leve.""" mensagem_norm = (mensagem or '').strip() if not mensagem_norm: return {'status': 'ignored', 'motivo': 'mensagem_vazia'} row = { 'ts': self._now_ts(), 'usuario': usuario, 'numero': numero, 'nome_usuario': nome_usuario or usuario, 'tipo_conversa': tipo_conversa, 'mensagem': mensagem_norm[:4000], 'resposta_do_bot': bool(resposta_do_bot), 'resposta_gerada': (resposta_gerada or '')[:4000] if resposta_do_bot else None, 'is_reply': bool(is_reply), 'reply_to_bot': bool(reply_to_bot), 'contexto_grupo': contexto_grupo or '', 'modelo_usado': modelo_usado or 'desconhecido', } self._append_jsonl(row) analise = { 'comprimento': len(mensagem_norm.split()), 'tem_link': ('http://' in mensagem_norm) or ('https://' in mensagem_norm), 'tem_interrogacao': '?' in mensagem_norm, } aprendizado = {'armazenado_em': str(self.path)} return {'ok': True, 'analise': analise, 'aprendizado': aprendizado} def obter_contexto_para_llm(self, topico: Optional[str] = None, limite: int = 10) -> List[str]: """Retorna últimas N mensagens (opcionalmente filtradas por tópico simples).""" linhas: List[str] = [] # Lê somente o necessário (últimas ~2000 linhas, se arquivo grande) try: if self.path.exists(): with self.path.open('r', encoding='utf-8') as f: for line in f: linhas.append(line) # Limita memória linhas = linhas[-2000:] except Exception: pass registros: List[Dict[str, Any]] = [] for line in linhas[-500:]: # parse apenas últimas 500 try: registros.append(json.loads(line)) except Exception: continue # filtra if topico: t = topico.lower().strip() registros = [r for r in registros if t in (r.get('mensagem', '').lower())] # monta blocos curtos para contexto blocos: List[str] = [] for r in registros[-limite:]: autor = r.get('nome_usuario') or r.get('usuario') msg = r.get('mensagem', '') tipo = r.get('tipo_conversa', 'pv') blocos.append(f"[{tipo}] {autor}: {msg}") return blocos def get_best_api_for_context( self, complexidade: float = 0.5, emocao: str = 'neutral', intencao: str = 'afirmacao', tipo_conversa: str = 'pv', ) -> str: """ HEURÍSTICA DELEGADA AO LOCAL_LLM / MOE ROUTER. Mantido para compatibilidade, mas agora apenas sugere o padrão. """ return 'moe_router' _singleton: Optional[AprendizadoContinuo] = None def get_aprendizado_continuo() -> AprendizadoContinuo: global _singleton if _singleton is None: _singleton = AprendizadoContinuo(JSONL_PATH) return _singleton # ============================================================ # COMPATIBILIDADE — aliases para imports legados # ============================================================ def processar_conversa_global( mensagem: str, usuario: str, numero: str, nome_usuario: Optional[str] = None, tipo_conversa: str = 'pv', resposta_do_bot: bool = False, resposta_gerada: Optional[str] = None, is_reply: bool = False, reply_to_bot: bool = False, contexto_grupo: Optional[str] = None, modelo_usado: Optional[str] = None, ) -> Dict[str, Any]: """Wrapper legado — delega para o singleton.""" ac = get_aprendizado_continuo() return ac.processar_mensagem( mensagem=mensagem, usuario=usuario, numero=numero, nome_usuario=nome_usuario, tipo_conversa=tipo_conversa, resposta_do_bot=resposta_do_bot, resposta_gerada=resposta_gerada, is_reply=is_reply, reply_to_bot=reply_to_bot, contexto_grupo=contexto_grupo, modelo_usado=modelo_usado, ) # Aliases de classe para compatibilidade ConversaGlobal = AprendizadoContinuo APIContextScore = type('APIContextScore', (), { 'score': 0.5, 'api': 'gemini', '__init__': lambda self, **kw: self.__dict__.update(kw), })