""" Squad Knowledge Base — Gera perfil completo do squad a partir de TODAS as fontes. Le todos os documentos do Drive, PM OS, inteligencia de fraude e memoria, sintetiza com Claude Opus num documento unico de conhecimento do squad, e salva no Drive para uso automatico em toda conversa. """ import gc import logging import threading import time as _time import streamlit as st from modules.config import client_anthropic, DRIVE_FOLDER_ID from modules.drive_utils import ( conectar_drive, conectar_drive_usuario, drive_upload_arquivo, drive_atualizar_arquivo, get_folders, ) ARQUIVO_KB = "squad_knowledge_base.md" KB_MAX_AGE_DAYS = 7 # Regenera automaticamente a cada 7 dias # --------------------------------------------------------------- # CARREGAR — usado em toda conversa # --------------------------------------------------------------- def _buscar_arquivo_kb(service): """Busca o arquivo KB no Drive. Retorna (file_id, conteudo, modifiedTime) ou (None, None, None).""" try: folders = get_folders() if not folders: return None, None, None parent_id = folders.get("root", DRIVE_FOLDER_ID) query = ( f"'{parent_id}' in parents and " f"name = '{ARQUIVO_KB}' and " "trashed = false" ) results = service.files().list( q=query, fields="files(id,name,modifiedTime)" ).execute() files = results.get("files", []) if files: file_id = files[0]["id"] modified = files[0].get("modifiedTime", "") conteudo = service.files().get_media(fileId=file_id).execute().decode("utf-8") return file_id, conteudo, modified except Exception as e: logging.warning(f"[Knowledge Base] Erro ao buscar: {e}") return None, None, None def _kb_esta_desatualizada(modified_time_str: str) -> bool: """Verifica se a KB precisa ser regenerada (mais velha que KB_MAX_AGE_DAYS).""" if not modified_time_str: return True try: from datetime import datetime, timezone # Parse ISO format: "2026-03-13T10:00:00.000Z" modified = datetime.fromisoformat(modified_time_str.replace("Z", "+00:00")) age = datetime.now(timezone.utc) - modified return age.days >= KB_MAX_AGE_DAYS except Exception: return True def carregar_knowledge_base() -> str: """Carrega a knowledge base do Drive. Cache de 30min na session.""" cache_key = "_kb_cache" cache_ts = "_kb_cache_ts" cached = st.session_state.get(cache_key) if cached is not None and _time.time() - st.session_state.get(cache_ts, 0) < 1800: return cached try: service = conectar_drive() if not service: return "" _, conteudo, _ = _buscar_arquivo_kb(service) result = conteudo or "" st.session_state[cache_key] = result st.session_state[cache_ts] = _time.time() return result except Exception as e: logging.warning(f"[Knowledge Base] Erro ao carregar: {e}") return "" _LOCK_FILE = "/tmp/kb_generating.lock" _LOCK_MAX_AGE = 600 # 10 min — se lock for mais velho, ignora (processo morreu) def _is_generating() -> bool: """Verifica se outra instancia ja esta gerando a KB.""" import os if not os.path.exists(_LOCK_FILE): return False try: age = _time.time() - os.path.getmtime(_LOCK_FILE) if age > _LOCK_MAX_AGE: os.remove(_LOCK_FILE) return False return True except Exception: return False def _set_lock(): with open(_LOCK_FILE, "w") as f: f.write(str(_time.time())) def _release_lock(): import os try: os.remove(_LOCK_FILE) except Exception: pass def _regenerar_kb_background(): """Regenera KB em background thread (nao bloqueia o login).""" _set_lock() try: ok, msg, _ = gerar_knowledge_base() if ok: logging.info(f"[Knowledge Base] Regenerada em background: {msg}") else: logging.warning(f"[Knowledge Base] Falha ao regenerar em background: {msg}") except Exception as e: logging.warning(f"[Knowledge Base] Erro na regeneracao background: {e}") finally: _release_lock() def verificar_e_atualizar_kb(): """Verifica se KB existe e esta atualizada. - Se KB existe mas esta velha: regenera (rapido, so reescreve) - Se KB NAO existe: apenas avisa no log e mostra toast (nao bloqueia app) Usa lock file para evitar regeneracoes duplicadas em refresh.""" try: # Se outra instancia ja esta gerando, pula if _is_generating(): logging.info("[Knowledge Base] Outra instancia ja esta gerando. Pulando.") return service = conectar_drive() if not service: return file_id, conteudo, modified = _buscar_arquivo_kb(service) if not conteudo: # KB nao existe — NAO bloquear o app gerando agora # Apenas avisar o usuario para gerar manualmente logging.info("[Knowledge Base] KB nao encontrada. Use o botao na sidebar para gerar.") st.toast( "Knowledge Base ainda nao gerada. " "Use o botao na sidebar (Knowledge Base do Squad) para gerar.", icon="📋", ) return # KB existe mas esta desatualizada — regenerar em background if _kb_esta_desatualizada(modified): logging.info(f"[Knowledge Base] KB desatualizada (modificada em {modified}). Regenerando em background...") st.toast("Knowledge Base desatualizada — atualizando em background...", icon="🧠") thread = threading.Thread(target=_regenerar_kb_background, daemon=True) thread.start() else: logging.info(f"[Knowledge Base] KB atualizada (modificada em {modified})") except Exception as e: _release_lock() logging.warning(f"[Knowledge Base] Erro na verificacao: {e}") # --------------------------------------------------------------- # GERAR — roda sob demanda (botao na UI) # --------------------------------------------------------------- # Fontes estáticas (não precisam de Jira/conexão ao vivo) _FONTES_PARA_KB = [ "planejamento_squad", "estado_cora", "roadmap", "okrs", "business_context", "metricas", "customer_inputs", "market_inputs", "decision_log", "prds_existentes", "pesquisas_competitivas", "contexto_fraude", "documentos_drive", ] def _coletar_todas_fontes(progress_callback=None) -> dict: """Coleta dados de todas as fontes disponíveis.""" from modules.context_router import _FETCHERS dados = {} total = len(_FONTES_PARA_KB) for i, fonte_id in enumerate(_FONTES_PARA_KB): if progress_callback: progress_callback(i + 1, total, f"Lendo: {fonte_id}...") fetcher = _FETCHERS.get(fonte_id) if not fetcher: continue try: conteudo = fetcher() if conteudo and conteudo.strip(): dados[fonte_id] = conteudo logging.info(f"[Knowledge Base] {fonte_id}: {len(conteudo)} chars") else: logging.info(f"[Knowledge Base] {fonte_id}: vazio") except Exception as e: logging.warning(f"[Knowledge Base] Erro em {fonte_id}: {e}") # Coletar memorias de usuarios try: from modules.memoria import carregar_lista_memoria memorias = carregar_lista_memoria() if memorias: mem_texts = [] from modules.drive_utils import drive_ler_conteudo service = conectar_drive() if service: for mem in memorias[:20]: try: txt = drive_ler_conteudo(service, mem["id"]) if txt and txt.strip(): mem_texts.append(f"[{mem['name']}]\n{txt[:2000]}") except Exception: pass if mem_texts: dados["memorias_usuarios"] = "\n\n".join(mem_texts) except Exception as e: logging.warning(f"[Knowledge Base] Erro ao coletar memorias: {e}") return dados def _sintetizar_knowledge_base(dados: dict, info_squad: str = "") -> str: """Usa Claude Opus para sintetizar todos os dados num perfil do squad.""" # Montar contexto partes = [] for fonte_id, conteudo in dados.items(): # Truncar fontes muito grandes if len(conteudo) > 20000: conteudo = conteudo[:20000] + "\n[...truncado...]" partes.append(f"=== FONTE: {fonte_id} ===\n{conteudo}") todas_fontes = "\n\n".join(partes) prompt = f"""Voce e o curador da base de conhecimento da Squad de Risco e Seguranca do Banco Cora. Abaixo estao TODOS os dados disponiveis sobre o squad, coletados de diversas fontes (Drive, PM OS, Jira, planilhas, inteligencia de fraude, memorias). {f"INFORMACOES ADICIONAIS DO PM:{chr(10)}{info_squad}{chr(10)}" if info_squad else ""} Sua tarefa: sintetizar TUDO num documento estruturado que sera usado como CONTEXTO PERMANENTE do assistente J.A.R.V.I.S. em todas as conversas. Use EXATAMENTE esta estrutura: # Squad de Risco e Seguranca — Base de Conhecimento ## Sobre o Squad [Quem somos, missao, papel na Cora, composicao do time] ## Time e Stakeholders [Nomes, papeis, quem e responsavel por que, stakeholders externos ao squad] ## Produtos e Sistemas [Sistemas que o squad e dono, fluxos protegidos, ferramentas/vendors] ## Estrategia e Apostas [Planejamento atual, apostas estrategicas, areas de foco] ## OKRs e Metricas [Objetivos atuais, Key Results com valores, metricas de acompanhamento] [PRESERVE valores exatos — nao arredonde] ## Roadmap [Iniciativas planejadas por trimestre/mes, status, prioridades] ## Contexto de Negocio [Modelo de negocio da Cora, proposta de valor, publico, diferenciais] ## Inteligencia de Fraude [Resumo da inteligencia acumulada: typologias, metricas de fraude, tendencias, controles] [PRESERVE dados e numeros — sao dados reais] ## Decisoes Importantes [Decisoes estrategicas do time, racional, tradeoffs] ## Projetos e PRDs [Projetos em andamento ou planejados, escopo resumido] ## Feedback de Clientes e Mercado [Insights de clientes, tendencias de mercado, benchmarks] ## Vocabulario do Squad [Termos e jargoes especificos que o time usa, com definicoes] REGRAS CRITICAS: - Use APENAS dados reais das fontes. NUNCA invente. - Preserve numeros, datas e valores exatos. - Se um dado nao esta nas fontes, NAO inclua — melhor incompleto que inventado. - Escreva em portugues brasileiro. - Seja conciso mas completo — este documento sera carregado em toda conversa. - Maximo ~8000 palavras (precisa caber no contexto do modelo). DADOS DAS FONTES: {todas_fontes}""" response = client_anthropic.messages.create( model="claude-opus-4-6", max_tokens=16384, messages=[{"role": "user", "content": prompt}], ) return response.content[0].text def _salvar_kb(conteudo: str) -> bool: """Salva a knowledge base no Drive.""" try: service = conectar_drive() user_service = conectar_drive_usuario() write_svc = user_service or service if not write_svc: return False folders = get_folders() parent_id = folders.get("root", DRIVE_FOLDER_ID) if folders else DRIVE_FOLDER_ID # Verificar se ja existe if service: query = ( f"'{parent_id}' in parents and " f"name = '{ARQUIVO_KB}' and " "trashed = false" ) results = service.files().list(q=query, fields="files(id)").execute() files = results.get("files", []) content_bytes = conteudo.encode("utf-8") if files: drive_atualizar_arquivo(write_svc, files[0]["id"], content_bytes, "text/markdown") else: drive_upload_arquivo(write_svc, ARQUIVO_KB, content_bytes, "text/markdown", parent_id) # Limpar cache st.session_state.pop("_kb_cache", None) st.session_state.pop("_kb_cache_ts", None) return True except Exception as e: logging.error(f"[Knowledge Base] Erro ao salvar: {e}") return False def gerar_knowledge_base(info_squad: str = "", progress_callback=None) -> tuple: """ Pipeline completo: coleta todas as fontes → sintetiza → salva no Drive. Args: info_squad: Informacoes adicionais fornecidas pelo PM (opcional) progress_callback: funcao(step, total, msg) Returns: (sucesso: bool, mensagem: str, conteudo: str) """ def _progress(step, total, msg): if progress_callback: progress_callback(step, total, msg) try: # 1. Coletar todas as fontes _progress(1, 4, "Coletando dados de todas as fontes...") dados = _coletar_todas_fontes( progress_callback=lambda i, t, m: _progress(1, 4, m) ) if not dados: return False, "Nenhuma fonte de dados disponivel.", "" fontes_ok = list(dados.keys()) logging.info(f"[Knowledge Base] {len(fontes_ok)} fontes coletadas: {fontes_ok}") gc.collect() # 2. Sintetizar com Claude Opus _progress(2, 4, f"Sintetizando {len(fontes_ok)} fontes com Claude Opus...") kb = _sintetizar_knowledge_base(dados, info_squad) del dados gc.collect() # 3. Salvar no Drive _progress(3, 4, "Salvando no Drive...") sucesso = _salvar_kb(kb) _progress(4, 4, "Concluido!") if sucesso: return True, f"Knowledge base gerada! {len(fontes_ok)} fontes processadas.", kb else: return False, "Erro ao salvar no Drive.", kb except Exception as e: logging.error(f"[Knowledge Base] Erro: {e}", exc_info=True) return False, f"Erro: {str(e)}", ""