caarleexx commited on
Commit
409b4cc
·
verified ·
1 Parent(s): e4db54e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +314 -390
app.py CHANGED
@@ -1,3 +1,7 @@
 
 
 
 
1
  import os
2
  import json
3
  import time
@@ -7,42 +11,48 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
7
 
8
  import gradio as gr
9
  import google.generativeai as genai
10
-
11
- # Dependências para PDF
12
- try:
13
- import PyPDF2
14
- PDF_SUPPORT = True
15
- except ImportError:
16
- PDF_SUPPORT = False
17
- print("⚠️ PyPDF2 não instalado. Install: pip install PyPDF2")
18
 
19
  # ==================== 1. CONFIGURAÇÃO ====================
20
 
21
  api_key = os.getenv("GOOGLE_API_KEY", "SUA_API_KEY_AQUI")
22
- if api_key and api_key != "SUA_API_KEY_AQUI":
23
  genai.configure(api_key=api_key)
24
 
25
  model_flash = genai.GenerativeModel("gemini-flash-latest")
26
- model_pro = genai.GenerativeModel("gemini-pro-latest")
27
 
28
- ARQUIVO_CONFIG = "protocolo.json"
29
- PASTA_TRANSCRICOES = "transcricoes"
30
- PAGES_PER_CHUNK = 10
31
- MAX_WORKERS = 5 # Limite de chamadas paralelas
32
 
33
- os.makedirs(PASTA_TRANSCRICOES, exist_ok=True)
34
 
35
  # ==================== 2. UTILIDADES ====================
36
 
 
 
 
 
37
  def carregar_protocolo():
38
  try:
39
  with open(ARQUIVO_CONFIG, "r", encoding="utf-8") as f:
40
  return f.read()
41
  except:
42
- return json.dumps([
43
- {"nome": "Leitor", "modelo": "flash", "missao": "Resumir o documento"},
44
- {"nome": "Investigador", "modelo": "pro", "missao": "Encontrar inconsistências"}
45
- ], indent=2)
 
 
 
 
 
 
 
 
 
 
46
 
47
  def salvar_protocolo(conteudo):
48
  try:
@@ -53,434 +63,348 @@ def salvar_protocolo(conteudo):
53
  except:
54
  return "❌ Erro JSON"
55
 
56
- def limpar_nome_arquivo(nome):
57
- nome_base = os.path.basename(nome)
58
- nome_limpo = "".join([c for c in nome_base if c.isalnum() or c in (' ', '.', '_', '-')]).strip()
59
- return nome_limpo + ".json"
60
 
61
- def extrair_texto_pdf(caminho_pdf):
62
- try:
63
- with open(caminho_pdf, 'rb') as f:
64
- reader = PyPDF2.PdfReader(f)
65
- paginas = []
66
- for i, page in enumerate(reader.pages):
67
- texto = page.extract_text()
68
- paginas.append({
69
- "numero": i + 1,
70
- "texto": texto,
71
- "metadata": str(page)[:200]
72
- })
73
- return paginas, None
74
- except Exception as e:
75
- return None, str(e)
76
-
77
- def fragmentar_pdf(paginas, tamanho_chunk=PAGES_PER_CHUNK):
78
- chunks = []
79
- for i in range(0, len(paginas), tamanho_chunk):
80
- chunk = paginas[i:i + tamanho_chunk]
81
- num_inicio = chunk[0]["numero"]
82
- num_fim = chunk[-1]["numero"]
83
-
84
- texto_consolidado = "\n---QUEBRA DE PÁGINA---\n".join(
85
- [f"[PÁGINA {p['numero']}]\n{p['texto']}" for p in chunk]
86
- )
87
-
88
- chunks.append({
89
- "id": f"chunk_{num_inicio}_{num_fim}",
90
- "paginas": f"{num_inicio}-{num_fim}",
91
- "num_paginas": len(chunk),
92
- "texto": texto_consolidado,
93
- "metadata": [p["metadata"] for p in chunk]
94
- })
95
- return chunks
96
 
97
- def processar_pdf_completo(arquivo_pdf):
98
- if not PDF_SUPPORT:
99
- return None, "❌ PyPDF2 não disponível"
100
-
101
- try:
102
- paginas, erro = extrair_texto_pdf(arquivo_pdf.name if hasattr(arquivo_pdf, 'name') else arquivo_pdf)
103
- if erro:
104
- return None, f"❌ Erro ao ler PDF: {erro}"
105
-
106
- chunks = fragmentar_pdf(paginas)
107
- nome_arquivo = os.path.basename(arquivo_pdf.name if hasattr(arquivo_pdf, 'name') else arquivo_pdf)
108
-
109
- return {
110
- "arquivo": nome_arquivo,
111
- "total_paginas": len(paginas),
112
- "total_chunks": len(chunks),
113
- "chunks": chunks,
114
- "tipo": "pdf"
115
- }, None
116
- except Exception as e:
117
- return None, f"❌ Erro no processamento: {str(e)}"
118
 
119
- def ler_arquivo_texto(arquivo):
120
- if arquivo is None: return None
121
- try:
122
- with open(arquivo.name, "r", encoding="utf-8") as f:
123
- conteudo = f.read()
124
- return {
125
- "arquivo": os.path.basename(arquivo.name),
126
- "conteudo": conteudo,
127
- "tipo": "texto"
128
- }
129
- except: return None
130
 
131
- # ==================== 3. PIPELINE DE IA ====================
 
132
 
133
- def transcrever_chunk(chunk_data, config_agentes):
134
- # Função auxiliar para ser executada na thread
135
- modelo = model_flash
136
- try:
137
- if config_agentes and isinstance(config_agentes, list):
138
- if config_agentes[0].get("modelo") == "pro":
139
- modelo = model_pro
140
- except:
141
- pass
142
 
143
- prompt = f"""
144
- ANÁLISE DE DOCUMENTO (OCR/LEITURA):
145
- Transcreva e estruture o conteúdo das páginas {chunk_data['paginas']}.
146
- Texto extraído:
147
- {chunk_data['texto']}
148
-
149
- Retorne JSON: {{ "transcricao": "...", "objetos": ["..."], "resumo": "..." }}
150
- """
 
 
 
 
 
 
 
151
  try:
152
- # Retry simples em caso de erro 429 (rate limit)
153
- for tentativa in range(3):
154
- try:
155
- resposta = modelo.generate_content(prompt)
156
- texto_resp = resposta.text.replace("```json", "").replace("```", "")
157
- return json.loads(texto_resp.strip()), None
158
- except Exception as inner_e:
159
- if "429" in str(inner_e):
160
- time.sleep(2 * (tentativa + 1))
161
- continue
162
- raise inner_e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  except Exception as e:
164
- return None, str(e)
 
165
 
166
- # ==================== 4. GERENCIADOR DE ARQUIVOS ====================
167
 
168
- class GerenciadorArquivos:
169
- def __init__(self):
170
- self.arquivos = {}
 
171
 
172
- def adicionar(self, arquivo, arquivo_id):
173
- self.arquivos[arquivo_id] = {
174
- "arquivo": arquivo,
175
- "nome": os.path.basename(arquivo.name),
176
- "status": "adicionado",
177
- "processado": None,
178
- "transcricao": None
179
- }
180
 
181
- def gerar_prompt_com_transcricoes(self, texto_usuario):
182
- prompt = texto_usuario + "\n\n--- CONTEXTO DOS ARQUIVOS ---\n"
183
- count = 0
184
- for _, item in self.arquivos.items():
185
- if item["status"] == "processado" and item["transcricao"]:
186
- count += 1
187
- trans = item["transcricao"]
188
- nome = item["nome"]
189
- prompt += f"\n[ARQUIVO: {nome}]\n"
190
-
191
- if isinstance(trans, dict) and "chunks_processados" in trans:
192
- # Como garantimos a ordem na lista chunks_processados, iteramos normalmente
193
- for chunk in trans["chunks_processados"]:
194
- if chunk.get("status") == "OK":
195
- resumo = chunk.get('resumo', '')
196
- resumo = str(resumo) if resumo else ""
197
- prompt += f"Páginas {chunk['paginas']}: {resumo}\n"
198
-
199
- texto_full = chunk.get('transcricao', '')
200
- if texto_full:
201
- texto_seguro = str(texto_full)
202
- prompt += f"Trecho: {texto_seguro[:400]}...\n"
203
- else:
204
- prompt += "Trecho: (vazio)\n"
205
-
206
- elif isinstance(trans, dict) and "conteudo" in trans:
207
- conteudo = str(trans['conteudo'])
208
- prompt += f"Conteúdo: {conteudo[:1000]}...\n"
209
-
210
- if count == 0:
211
- prompt += "(Nenhum arquivo processado ainda)"
212
- return prompt
213
 
214
- # Instância Global
215
- gerenciador = GerenciadorArquivos()
 
 
 
216
 
217
- # ==================== 5. FUNÇÕES DE ORQUESTRAÇÃO ====================
 
 
 
218
 
219
- def automacao_upload_processamento(files, history, config_json):
220
- if not files:
221
- return history
 
 
 
 
222
 
223
  try:
224
- config_agentes = json.loads(config_json)
225
- except:
226
- config_agentes = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
 
228
- if history is None:
229
- history = []
 
230
 
231
- history.append([None, f"📂 **SISTEMA:** Recebi {len(files)} arquivo(s). Verificando cache e processando..."])
232
- yield history
233
-
234
- ids_para_processar = []
 
 
 
 
235
 
236
- for f in files:
237
- arquivo_id = f"arq_{int(time.time()*1000)}_{f.name}"
238
- gerenciador.adicionar(f, arquivo_id)
239
- ids_para_processar.append(arquivo_id)
 
240
 
241
- for arq_id in ids_para_processar:
242
- item = gerenciador.arquivos[arq_id]
243
- nome = item["nome"]
 
 
 
244
 
245
- # --- VERIFICAÇÃO DE CACHE ---
246
- nome_cache = limpar_nome_arquivo(nome)
247
- caminho_cache = os.path.join(PASTA_TRANSCRICOES, nome_cache)
248
-
249
- if os.path.exists(caminho_cache):
250
- try:
251
- with open(caminho_cache, "r", encoding="utf-8") as cache_file:
252
- dados_cache = json.load(cache_file)
253
- item["transcricao"] = dados_cache
254
- item["status"] = "processado"
255
- if nome.lower().endswith('.pdf') and "chunks_processados" in dados_cache:
256
- item["processado"] = {"tipo": "pdf", "chunks": []}
257
- history.append([None, f"♻️ **Cache Encontrado:** `{nome}` já foi processado. Carregando..."])
258
- yield history
259
- continue
260
- except Exception as e:
261
- history.append([None, f"⚠️ Erro cache `{nome}`: {e}. Reprocessando..."])
262
- # ---------------------------
263
 
264
- history.append([None, f"⚙️ **Processando:** `{nome}`..."])
265
- yield history
266
 
267
- if nome.lower().endswith('.pdf'):
268
- if not PDF_SUPPORT:
269
- history.append([None, f"❌ Erro em `{nome}`: Biblioteca PDF ausente."])
270
- yield history
271
- continue
 
272
 
273
- pdf_proc, erro = processar_pdf_completo(item["arquivo"])
274
- if erro:
275
- history.append([None, f" Erro em `{nome}`: {erro}"])
276
- yield history
277
- continue
278
-
279
- item["processado"] = pdf_proc
280
- chunks = pdf_proc["chunks"]
281
- total_chunks = len(chunks)
282
-
283
- # Inicializa lista com o tamanho exato para garantir a ordem
284
- chunks_ordenados = [None] * total_chunks
285
-
286
- history.append([None, f"📄 `{nome}` fragmentado em {total_chunks} partes. Iniciando IA (Paralelo: {MAX_WORKERS} threads)..."])
287
- yield history
288
 
289
- # --- PROCESSAMENTO PARALELO ---
290
  with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
291
- # Dicionário para mapear Future -> Índice Original
292
- futures_map = {}
293
-
294
- # Submeter todas as tarefas
295
- for i, chunk in enumerate(chunks):
296
- future = executor.submit(transcrever_chunk, chunk, config_agentes)
297
- futures_map[future] = i
298
 
299
- # Coletar resultados conforme ficam prontos
300
  concluidos = 0
301
  for future in as_completed(futures_map):
302
- index_original = futures_map[future]
303
- res, err = future.result()
304
 
305
- if err:
306
- chunks_ordenados[index_original] = {"status": "ERRO", "paginas": chunks[index_original]["paginas"]}
307
  else:
308
- chunks_ordenados[index_original] = {
309
- "status": "OK",
310
- "paginas": chunks[index_original]["paginas"],
311
- "transcricao": res.get("transcricao"),
312
- "resumo": res.get("resumo")
313
- }
314
 
315
  concluidos += 1
316
- # Atualiza a UI a cada 2 chunks ou no final para não flodar
317
- if concluidos % 2 == 0 or concluidos == total_chunks:
318
- msg_base = f"📄 `{nome}`: Processando partes... ({concluidos}/{total_chunks})"
319
- history[-1][1] = msg_base
320
- yield history
321
- # ------------------------------
322
-
323
- dados_finais = {
324
- "arquivo": nome,
325
- "data_processamento": str(datetime.now()),
326
- "chunks_processados": chunks_ordenados # Agora contém a lista na ordem correta
327
- }
328
 
329
- item["transcricao"] = dados_finais
330
- item["status"] = "processado"
331
-
332
- try:
333
- with open(caminho_cache, "w", encoding="utf-8") as f_out:
334
- json.dump(dados_finais, f_out, indent=2, ensure_ascii=False)
335
- history.append([None, f"💾 `{nome}` processado e salvo no cache."])
336
- except Exception as e:
337
- history.append([None, f"⚠️ Erro ao salvar cache: {e}"])
 
 
 
 
 
 
 
 
 
 
 
 
 
338
 
339
- yield history
340
-
341
- else:
342
- # Processamento de Texto Simples (não precisa de paralelismo pois é 1 chunk)
343
- res = ler_arquivo_texto(item["arquivo"])
344
- if res:
345
- item["processado"] = res
346
- dados_finais = {"conteudo": res["conteudo"], "data_processamento": str(datetime.now())}
347
- item["transcricao"] = dados_finais
348
- item["status"] = "processado"
349
-
350
- with open(caminho_cache, "w", encoding="utf-8") as f_out:
351
- json.dump(dados_finais, f_out, indent=2, ensure_ascii=False)
352
-
353
- history.append([None, f"✅ `{nome}` (Texto) lido e salvo."])
354
  else:
355
- history.append([None, f"❌ Falha ao ler `{nome}`."])
356
- yield history
 
357
 
358
- history.append([None, "🏁 **Processamento de lote finalizado.** Os arquivos estão prontos para análise."])
359
- yield history
360
-
361
 
362
- def chat_orquestrador(message, history, config_json):
363
- try:
364
- prompt_contexto = gerenciador.gerar_prompt_com_transcricoes(message)
365
- except Exception as e:
366
- history.append([message, f"❌ Erro ao gerar contexto: {str(e)}"])
367
- yield history
368
- return
369
-
370
- try:
371
- protocolo = json.loads(config_json)
372
- except:
373
- history.append([message, "❌ Erro no JSON de Configuração."])
374
- yield history
375
- return
376
-
377
- history.append([message, None])
378
- yield history
379
-
380
- timeline_execucao = [{"role": "user", "content": prompt_contexto}]
381
 
382
- for cfg in protocolo:
383
- nome_agente = cfg.get("nome", "Agente")
384
- modelo_agente = model_pro if cfg.get("modelo") == "pro" else model_flash
385
-
386
- history[-1][1] = f"⏳ **{nome_agente}** está analisando..."
387
- yield history
388
-
389
- prompt_agente = f"""
390
- --- HISTÓRICO ---
391
- {json.dumps(timeline_execucao, ensure_ascii=False)}
392
- -----------------
393
- Você é: {nome_agente}
394
- Sua Missão: {cfg['missao']}
395
- Responda de forma concisa e direta.
396
- """
397
- try:
398
- inicio = time.time()
399
- resp = modelo_agente.generate_content(prompt_agente)
400
- texto_resp = resp.text
401
- duracao = time.time() - inicio
402
-
403
- timeline_execucao.append({"role": "model", "content": f"[{nome_agente}]: {texto_resp}"})
404
-
405
- msg_atual = history[-1][1]
406
- if "⏳" in msg_atual: msg_atual = ""
407
-
408
- novo_trecho = f"**[{nome_agente}]** ({duracao:.1f}s):\n{texto_resp}\n\n"
409
- history[-1][1] = msg_atual + novo_trecho
410
- yield history
411
-
412
- except Exception as e:
413
- msg_atual = history[-1][1]
414
- history[-1][1] = msg_atual + f"\n❌ Erro em {nome_agente}: {str(e)}\n"
415
- yield history
416
 
417
- # ==================== 6. UI (Gradio) ====================
418
 
419
- def ui_v28_corrected():
420
  css = """
421
  footer {display: none !important;}
422
  .contain {border: none !important;}
423
  """
424
-
425
- config_inicial = carregar_protocolo()
426
 
427
- with gr.Blocks(title="AI Forensics Auto", css=css, theme=gr.themes.Soft()) as app:
428
-
429
- state_config = gr.State(config_inicial)
 
430
 
431
  with gr.Tabs():
432
- with gr.Tab("💬 Investigação"):
433
-
434
  chatbot = gr.Chatbot(
435
- height=550,
436
- show_label=False,
 
437
  show_copy_button=True,
438
- render_markdown=True
439
  )
440
-
441
  with gr.Row():
442
- txt_input = gr.Textbox(
443
- scale=8,
444
- show_label=False,
445
- placeholder="Digite sua instrução ou pergunta sobre o caso...",
446
- lines=1
447
- )
448
- btn_enviar = gr.Button("Enviar 📨", variant="primary", scale=1)
449
-
450
- with gr.Accordion("📂 Adicionar Arquivos para Análise", open=False):
451
- gr.Markdown("Selecione arquivos (PDF, TXT). A transcrição iniciará **automaticamente** e os logs aparecerão no chat acima.")
452
- file_uploader = gr.File(
453
- file_count="multiple",
454
- file_types=[".pdf", ".txt", ".json", ".md"],
455
- label="Arraste arquivos aqui ou clique para selecionar"
456
- )
457
-
458
- with gr.Tab("⚙️ Contexto & Config"):
459
- gr.Markdown("### Protocolo dos Agentes")
 
 
 
 
 
 
 
 
 
 
460
  with gr.Row():
461
- btn_save_cfg = gr.Button("💾 Salvar Alterações")
462
- lbl_cfg_status = gr.Label(show_label=False)
463
-
464
- code_config = gr.Code(value=config_inicial, language="json", label="protocolo.json")
465
 
466
- btn_save_cfg.click(salvar_protocolo, inputs=[code_config], outputs=[lbl_cfg_status])
467
- btn_save_cfg.click(lambda x: x, inputs=[code_config], outputs=[state_config])
468
-
469
- btn_enviar.click(
470
- chat_orquestrador,
471
- inputs=[txt_input, chatbot, state_config],
472
- outputs=[chatbot]
473
- ).then(
474
- lambda: "", outputs=[txt_input]
475
- )
476
 
477
- file_uploader.upload(
478
- automacao_upload_processamento,
479
- inputs=[file_uploader, chatbot, state_config],
480
- outputs=[chatbot]
481
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
482
 
483
  return app
484
 
485
  if __name__ == "__main__":
486
- ui_v28_corrected().launch()
 
1
+ # ╔════════════════════════════════════════════════════════════════════════════╗
2
+ # ║ PIPELINE V44: FRAG + VISÃO PAGINADA + PARALELISMO + CACHE + AUDITORIA ║
3
+ # ╚════════════════════════════════════════════════════════════════════════════╝
4
+
5
  import os
6
  import json
7
  import time
 
11
 
12
  import gradio as gr
13
  import google.generativeai as genai
14
+ import pypdf # pip install pypdf
 
 
 
 
 
 
 
15
 
16
  # ==================== 1. CONFIGURAÇÃO ====================
17
 
18
  api_key = os.getenv("GOOGLE_API_KEY", "SUA_API_KEY_AQUI")
19
+ if api_key and api_key != "SUA_API_KEY_AQUI":
20
  genai.configure(api_key=api_key)
21
 
22
  model_flash = genai.GenerativeModel("gemini-flash-latest")
23
+ model_pro = genai.GenerativeModel("gemini-pro-latest")
24
 
25
+ ARQUIVO_CONFIG = "protocolo_fragmentacao_visao-3.json"
26
+ PASTA_CACHE = "cache_processamento"
27
+ MAX_WORKERS = 5 # Paralelismo
 
28
 
29
+ os.makedirs(PASTA_CACHE, exist_ok=True)
30
 
31
  # ==================== 2. UTILIDADES ====================
32
 
33
+ def log_point(msg, logs):
34
+ ts = datetime.now().strftime("%H:%M:%S")
35
+ return logs + f"[{ts}] {msg}\n"
36
+
37
  def carregar_protocolo():
38
  try:
39
  with open(ARQUIVO_CONFIG, "r", encoding="utf-8") as f:
40
  return f.read()
41
  except:
42
+ proto = [
43
+ {
44
+ "nome": "PAGINADOR_VISUAL",
45
+ "missao": (
46
+ "Você recebe o texto bruto de um conjunto de páginas de um PDF. "
47
+ "Separe por página e devolva uma lista JSON com objetos "
48
+ "{'pagina','transcricao_fiel','descricao_visual'}."
49
+ "Retorne APENAS essa lista JSON, sem texto extra."
50
+ ),
51
+ "tipo_saida": "json",
52
+ "modelo": "flash",
53
+ }
54
+ ]
55
+ return json.dumps(proto, ensure_ascii=False, indent=2)
56
 
57
  def salvar_protocolo(conteudo):
58
  try:
 
63
  except:
64
  return "❌ Erro JSON"
65
 
66
+ def gerar_hash_arquivo(nome_arquivo):
67
+ return hashlib.md5(nome_arquivo.encode()).hexdigest()
 
 
68
 
69
+ def salvar_cache(hash_id, dados):
70
+ caminho = os.path.join(PASTA_CACHE, f"{hash_id}.json")
71
+ with open(caminho, "w", encoding="utf-8") as f:
72
+ json.dump(dados, f, ensure_ascii=False, indent=2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
74
+ def carregar_cache(hash_id):
75
+ caminho = os.path.join(PASTA_CACHE, f"{hash_id}.json")
76
+ if os.path.exists(caminho):
77
+ with open(caminho, "r", encoding="utf-8") as f:
78
+ return json.load(f)
79
+ return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
 
81
+ # --------- DIVISÃO PDF ---------
 
 
 
 
 
 
 
 
 
 
82
 
83
+ def ler_anexo_e_fragmentar(arquivo, paginas_por_fragmento=5, logs=""):
84
+ logs = log_point("ler_anexo_e_fragmentar() chamado", logs)
85
 
86
+ if arquivo is None:
87
+ return [], "", logs
88
+
89
+ filename = getattr(arquivo, "name", arquivo)
 
 
 
 
 
90
 
91
+ if not os.path.exists(filename):
92
+ return [], f"[ERRO: Arquivo não encontrado]", logs
93
+
94
+ anexo_info = f"[PDF: {os.path.basename(filename)}]"
95
+
96
+ if not filename.lower().endswith(".pdf"):
97
+ logs = log_point("Arquivo texto simples detectado", logs)
98
+ try:
99
+ with open(filename, "r", encoding="utf-8") as f:
100
+ texto = f.read()
101
+ # Retorna como um único fragmento de texto
102
+ return [texto], f"[TXT: {os.path.basename(filename)}]", logs
103
+ except:
104
+ return [], "[ERRO LEITURA TXT]", logs
105
+
106
  try:
107
+ reader = pypdf.PdfReader(filename)
108
+ total_pages = len(reader.pages)
109
+ logs = log_point(f"PDF carregado: {total_pages} páginas", logs)
110
+
111
+ fragments = []
112
+ for i in range(0, total_pages, paginas_por_fragmento):
113
+ start = i + 1
114
+ end = min(i + paginas_por_fragmento, total_pages)
115
+
116
+ bloco_texto = ""
117
+ for p in range(i, end):
118
+ try:
119
+ t = reader.pages[p].extract_text() or ""
120
+ except Exception as e:
121
+ t = f"\n[ERRO_EXTRACT_PAG_{p+1}: {e}]\n"
122
+ bloco_texto += f"\n=== PAGINA {p+1}/{total_pages} ===\n{t}\n"
123
+
124
+ fragment = (
125
+ f"=== FRAG {i//paginas_por_fragmento + 1} "
126
+ f"(PÁGS {start}-{end}/{total_pages}) ===\n"
127
+ f"{bloco_texto.strip()}"
128
+ )
129
+ fragments.append(fragment)
130
+
131
+ logs = log_point(f"Total de fragmentos criados: {len(fragments)}", logs)
132
+ return fragments, anexo_info, logs
133
  except Exception as e:
134
+ logs = log_point(f"ERRO PDF: {e}", logs)
135
+ return [], f"[ERRO PDF: {str(e)}]", logs
136
 
137
+ # ==================== 3. ENGINE DE EXECUÇÃO ====================
138
 
139
+ def _extrair_json_possivel(out_raw: str) -> str:
140
+ cleaned = out_raw.strip()
141
+ idx_abre_col = cleaned.find("[")
142
+ idx_abre_obj = cleaned.find("{")
143
 
144
+ candidatos = [i for i in [idx_abre_col, idx_abre_obj] if i != -1]
145
+ if candidatos:
146
+ start = min(candidatos)
147
+ cleaned = cleaned[start:]
 
 
 
 
148
 
149
+ cleaned = cleaned.replace("```json", "").replace("```", "")
150
+ return cleaned
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
 
152
+ def executar_no(timeline, config, fragmento_input=None):
153
+ """
154
+ Função Worker que será chamada tanto sequencialmente quanto em paralelo.
155
+ """
156
+ modelo = model_pro if config.get("modelo") == "pro" else model_flash
157
 
158
+ if fragmento_input is not None:
159
+ input_para_prompt = fragmento_input
160
+ else:
161
+ input_para_prompt = json.dumps(timeline, ensure_ascii=False, indent=2)
162
 
163
+ prompt = (
164
+ "--- INPUT PARA O AGENTE ---\n"
165
+ f"{input_para_prompt}\n"
166
+ "----------------\n"
167
+ f"AGENTE: {config['nome']}\n"
168
+ f"MISSÃO: {config['missao']}"
169
+ )
170
 
171
  try:
172
+ # Retry simples para API
173
+ for tentativa in range(3):
174
+ try:
175
+ resp = modelo.generate_content(prompt)
176
+ out = resp.text or ""
177
+ break
178
+ except Exception as e:
179
+ if "429" in str(e):
180
+ time.sleep(2 * (tentativa + 1))
181
+ continue
182
+ raise e
183
+
184
+ content = out
185
+ if config["tipo_saida"] == "json":
186
+ cleaned = _extrair_json_possivel(out)
187
+ try:
188
+ content = json.loads(cleaned)
189
+ except:
190
+ content = [] # Fallback em caso de erro de parse
191
 
192
+ return {"role": "assistant", "agent": config["nome"], "content": content}, None
193
+ except Exception as e:
194
+ return {"role": "system", "error": str(e)}, str(e)
195
 
196
+ # ==================== 4. ORQUESTRADOR ====================
197
+
198
+ def orquestrador(texto, arquivo, history, json_config, confext_state):
199
+ logs = f"🚀 START: {datetime.now().strftime('%H:%M:%S')}\n"
200
+ logs = log_point("Orquestrador V44 iniciado", logs)
201
+
202
+ # 1. Preparação
203
+ if history is None: history = []
204
 
205
+ nome_arquivo = os.path.basename(getattr(arquivo, "name", "sem_arquivo")) if arquivo else "sem_arquivo"
206
+ hash_op = gerar_hash_arquivo(nome_arquivo + json_config) # Hash baseado no arquivo + protocolo
207
+
208
+ # 2. Verifica Cache
209
+ cache_existente = carregar_cache(hash_op) if arquivo else None
210
 
211
+ if cache_existente:
212
+ logs = log_point(f"♻️ Cache encontrado para {nome_arquivo}", logs)
213
+ confext_upload = cache_existente["confext_upload"]
214
+ timeline = cache_existente.get("timeline", [])
215
+ history.append([texto, "✅ Arquivo carregado do cache! Análise pronta."])
216
+ yield history, timeline, logs, confext_upload
217
 
218
+ # Se houver texto novo do usuário, seguimos para análise final, senão paramos
219
+ if not texto:
220
+ return
221
+ else:
222
+ # 3. Processamento Normal
223
+ fragmentos, anexo_info, logs = ler_anexo_e_fragmentar(
224
+ arquivo, paginas_por_fragmento=5, logs=logs
225
+ )
 
 
 
 
 
 
 
 
 
 
226
 
227
+ history.append([texto + (" 📎" if arquivo else ""), None])
228
+ yield history, {}, logs, confext_state
229
 
230
+ try:
231
+ protocolo = json.loads(json_config)
232
+ except Exception as e:
233
+ history[-1][1] = "❌ Erro no JSON de Configuração."
234
+ yield history, {}, logs, confext_state
235
+ return
236
 
237
+ timeline = [{"role": "user", "content": texto}]
238
+ confext_upload = {
239
+ "arquivo": nome_arquivo,
240
+ "meta": anexo_info,
241
+ "paginas": []
242
+ }
243
+
244
+ # 4. Execução Paginador (Paralela)
245
+ if protocolo and fragmentos:
246
+ cfg_visao = protocolo[0] # Assume que o primeiro é o leitor
247
+ logs = log_point(f"Iniciando Leitura Paralela ({MAX_WORKERS} workers) com {cfg_visao['nome']}", logs)
248
+ history[-1][1] = f"⏳ Fragmentando e lendo {len(fragmentos)} partes em paralelo..."
249
+ yield history, timeline, logs, confext_upload
250
+
251
+ resultados_ordenados = [None] * len(fragmentos)
252
 
 
253
  with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
254
+ futures_map = {executor.submit(executar_no, [], cfg_visao, frag): i for i, frag in enumerate(fragmentos)}
 
 
 
 
 
 
255
 
 
256
  concluidos = 0
257
  for future in as_completed(futures_map):
258
+ idx = futures_map[future]
259
+ res, erro = future.result()
260
 
261
+ if erro:
262
+ logs = log_point(f"Erro no frag {idx}: {erro}", logs)
263
  else:
264
+ resultados_ordenados[idx] = res["content"]
 
 
 
 
 
265
 
266
  concluidos += 1
267
+ history[-1][1] = f"⏳ Leitura: {concluidos}/{len(fragmentos)} partes processadas..."
268
+ yield history, timeline, logs, confext_upload
269
+
270
+ # Consolidar resultados ordenados
271
+ for pags in resultados_ordenados:
272
+ if pags:
273
+ if isinstance(pags, list):
274
+ confext_upload["paginas"].extend(pags)
275
+ elif isinstance(pags, dict):
276
+ confext_upload["paginas"].append(pags)
277
+
278
+ logs = log_point(f"Leitura concluída. Total páginas extraídas: {len(confext_upload['paginas'])}", logs)
279
 
280
+ # Salvar Cache após a leitura pesada
281
+ if arquivo:
282
+ salvar_cache(hash_op, {"confext_upload": confext_upload, "timeline": timeline})
283
+ logs = log_point("Estado salvo em Cache", logs)
284
+
285
+ # Injeta contexto no timeline
286
+ timeline.append({
287
+ "role": "system",
288
+ "agent": "CONFEXT_UPLOAD",
289
+ "content": confext_upload
290
+ })
291
+
292
+ # 5. Execução dos Agentes de Análise (Sequencial)
293
+ restante = protocolo[1:] if protocolo else []
294
+
295
+ for cfg in restante:
296
+ history[-1][1] = f"⚙️ {cfg['nome']} analisando..."
297
+ logs = log_point(f"Iniciando agente: {cfg['nome']}", logs)
298
+ yield history, timeline, logs, confext_upload
299
+
300
+ # Passa timeline atualizada
301
+ res, erro = executar_no(timeline, cfg, fragmento_input=None)
302
 
303
+ if erro:
304
+ logs = log_point(f"Erro agente {cfg['nome']}: {erro}", logs)
 
 
 
 
 
 
 
 
 
 
 
 
 
305
  else:
306
+ timeline.append(res)
307
+ if cfg.get("tipo_saida") == "texto":
308
+ history[-1][1] = res["content"]
309
 
310
+ yield history, timeline, logs, confext_upload
 
 
311
 
312
+ if not texto and arquivo:
313
+ history[-1][1] = "✅ Documento processado e indexado. Pode fazer perguntas."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
314
 
315
+ logs = log_point("Processo Finalizado", logs)
316
+ yield history, timeline, logs, confext_upload
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
 
318
+ # ==================== 5. UI ====================
319
 
320
+ def ui_clean():
321
  css = """
322
  footer {display: none !important;}
323
  .contain {border: none !important;}
324
  """
 
 
325
 
326
+ config_init = carregar_protocolo()
327
+
328
+ with gr.Blocks(title="AI Forensics Auto V44", css=css, theme=gr.themes.Soft()) as app:
329
+ confext_state = gr.State(value=None)
330
 
331
  with gr.Tabs():
332
+ with gr.Tab("💬 Investigador"):
 
333
  chatbot = gr.Chatbot(
334
+ label="",
335
+ show_label=False,
336
+ height=600,
337
  show_copy_button=True,
338
+ render_markdown=True,
339
  )
340
+
341
  with gr.Row():
342
+ with gr.Column(scale=10):
343
+ txt_in = gr.Textbox(
344
+ show_label=False,
345
+ placeholder="Descreva o caso ou faça perguntas...",
346
+ lines=1,
347
+ max_lines=5,
348
+ container=False,
349
+ )
350
+ with gr.Column(scale=1, min_width=50):
351
+ file_in = gr.UploadButton(
352
+ "📎",
353
+ file_types=[".txt", ".md", ".json", ".pdf"],
354
+ size="sm",
355
+ )
356
+ with gr.Column(scale=1, min_width=80):
357
+ btn_send = gr.Button("Enviar", variant="primary", size="sm")
358
+
359
+ file_status = gr.Markdown("", visible=True)
360
+
361
+ def _on_upload(x):
362
+ nome = os.path.basename(getattr(x, "name", x))
363
+ return f"📎 Anexo pronto para análise: {nome}"
364
+
365
+ file_in.upload(_on_upload, inputs=file_in, outputs=file_status)
366
+
367
+ # --- AQUI ESTÁ A ABA SOLICITADA ---
368
+ with gr.Tab("🕵️ Auditoria & Debug"):
369
+ gr.Markdown("### 🧠 Processo Interno de Pensamento")
370
  with gr.Row():
371
+ out_dna = gr.JSON(label="Timeline da IA (Contexto)")
372
+ out_logs = gr.Textbox(label="Logs do Sistema", lines=20)
 
 
373
 
374
+ gr.Markdown("### 📂 Dados Estruturados (Confext)")
375
+ confext_view = gr.JSON(label="Conteúdo Extraído")
 
 
 
 
 
 
 
 
376
 
377
+ with gr.Tab("⚙️ Config"):
378
+ with gr.Row():
379
+ btn_save = gr.Button("Salvar Config")
380
+ lbl_save = gr.Label(show_label=False)
381
+ code_json = gr.Code(value=config_init, language="json", label=ARQUIVO_CONFIG)
382
+ btn_save.click(salvar_protocolo, code_json, lbl_save)
383
+
384
+ def _orq_wrapper(texto, arquivo, history, json_cfg, confext_old):
385
+ for h, dna, logs, confext_new in orquestrador(
386
+ texto, arquivo, history, json_cfg, confext_old
387
+ ):
388
+ yield h, dna, logs, confext_new
389
+
390
+ triggers = [btn_send.click, txt_in.submit]
391
+
392
+ for trig in triggers:
393
+ trig(
394
+ _orq_wrapper,
395
+ inputs=[txt_in, file_in, chatbot, code_json, confext_state],
396
+ outputs=[chatbot, out_dna, out_logs, confext_state], # Atualiza aba Debug
397
+ ).then(
398
+ lambda c: (None, None, "", c)[1:],
399
+ inputs=confext_state,
400
+ outputs=[txt_in, file_in, file_status, confext_state],
401
+ ).then(
402
+ lambda c: c,
403
+ inputs=confext_state,
404
+ outputs=confext_view, # Atualiza visualizador JSON
405
+ )
406
 
407
  return app
408
 
409
  if __name__ == "__main__":
410
+ ui_clean().launch()