pm-assistant / modules /knowledge_base.py
igorsteixeira
fix: KB regeneration in background thread + extend max age to 7 days
2100261
Raw
History Blame Contribute Delete
14.1 kB
"""
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)}", ""