Spaces:
Running
Running
| """ | |
| 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)}", "" | |