File size: 14,139 Bytes
c405c6d
 
 
 
 
 
 
 
 
 
2100261
c405c6d
 
 
 
 
 
 
 
 
 
 
2100261
c405c6d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3ee9f95
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2100261
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c405c6d
3ee9f95
 
 
 
c405c6d
3ee9f95
 
 
 
 
c405c6d
 
 
 
 
 
3ee9f95
 
 
 
 
 
 
 
 
 
 
2100261
3ee9f95
2100261
 
 
 
c405c6d
 
 
 
3ee9f95
c405c6d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
"""
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)}", ""