caarleexx commited on
Commit
f276067
·
verified ·
1 Parent(s): 079186a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +173 -461
app.py CHANGED
@@ -1,4 +1,3 @@
1
-
2
  import os
3
  import json
4
  import time
@@ -6,76 +5,82 @@ import hashlib
6
  from datetime import datetime
7
  from concurrent.futures import ThreadPoolExecutor, as_completed
8
 
9
- import gradio as gr
 
 
 
10
  import google.generativeai as genai
11
 
12
- # Dependências para PDF
13
  try:
14
  import PyPDF2
15
  PDF_SUPPORT = True
16
  except ImportError:
17
  PDF_SUPPORT = False
18
- print("⚠️ PyPDF2 não instalado. Instale com: pip install PyPDF2")
19
 
20
- # ==================== 1. CONFIGURAÇÃO ====================
21
 
22
  api_key = os.getenv("GOOGLE_API_KEY", "SUA_API_KEY_AQUI")
23
  if api_key and api_key != "SUA_API_KEY_AQUI":
24
  genai.configure(api_key=api_key)
25
 
26
- # Modelos do Gemini
27
  model_flash = genai.GenerativeModel("gemini-flash-latest")
28
  model_pro = genai.GenerativeModel("gemini-pro-latest")
29
 
30
  ARQUIVO_CONFIG = "protocolo.json"
31
  PASTA_TRANSCRICOES = "transcricoes"
32
  PAGES_PER_CHUNK = 10
33
- MAX_WORKERS = 5 # Limite de chamadas paralelas
34
 
35
  os.makedirs(PASTA_TRANSCRICOES, exist_ok=True)
36
 
37
- # ==================== 2. UTILIDADES ====================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
  def carregar_protocolo():
40
- """ Carrega o protocolo. Se não existir, cria um com exemplo de STOP. """
41
  try:
42
  with open(ARQUIVO_CONFIG, "r", encoding="utf-8") as f:
43
  return f.read()
44
  except FileNotFoundError:
45
- # Protocolo padrão que inclui um agente com a lógica de pergunta ao usuário
46
- protocolo_padrao = [
47
- {"nome": "Leitor Inicial", "modelo": "flash", "missao": "Leia o contexto e resuma os fatos principais em 3 a 5 pontos."},
48
  {
49
  "nome": "Advogado de Acusação",
50
  "modelo": "pro",
51
- "missao": "Com base nos fatos, formule uma pergunta crucial para o usuário para fortalecer um caso. Sua resposta DEVE ser APENAS um JSON no formato: {\"tipo\": \"pergunta_usuario\", \"pergunta\": \"Sua pergunta aqui\"}"
52
  },
53
- {"nome": "Analista Final", "modelo": "pro", "missao": "Considere a resposta do usuário e os fatos iniciais para dar um parecer final sobre o caso."}
54
- ]
55
- return json.dumps(protocolo_padrao, indent=2)
56
 
57
  def salvar_protocolo(conteudo):
58
- try:
59
- json.loads(conteudo)
60
- with open(ARQUIVO_CONFIG, "w", encoding="utf-8") as f:
61
- f.write(conteudo)
62
- return "✅ Protocolo salvo com sucesso!"
63
- except Exception as e:
64
- return f"❌ Erro ao salvar: {str(e)}"
65
 
66
- def limpar_nome_arquivo(nome):
67
- nome_base = os.path.basename(nome)
68
- nome_limpo = "".join([c for c in nome_base if c.isalnum() or c in (' ', '.', '_', '-')]).strip()
69
- return nome_limpo + ".json"
70
 
71
  def extrair_texto_pdf(caminho_pdf):
72
- # (Implementação existente, sem alterações)
73
  try:
74
- with open(caminho_pdf, 'rb') as f:
75
  reader = PyPDF2.PdfReader(f)
76
  paginas = []
77
  for i, page in enumerate(reader.pages):
78
- texto = page.extract_text()
79
  paginas.append({
80
  "numero": i + 1,
81
  "texto": texto,
@@ -87,483 +92,190 @@ def extrair_texto_pdf(caminho_pdf):
87
 
88
 
89
  def fragmentar_pdf(paginas, tamanho_chunk=PAGES_PER_CHUNK):
90
- # (Implementação existente, sem alterações)
91
  chunks = []
92
  for i in range(0, len(paginas), tamanho_chunk):
93
  chunk = paginas[i:i + tamanho_chunk]
94
  num_inicio = chunk[0]["numero"]
95
  num_fim = chunk[-1]["numero"]
96
-
97
  texto_consolidado = "\n---QUEBRA DE PÁGINA---\n".join(
98
  [f"[PÁGINA {p['numero']}]\n{p['texto']}" for p in chunk]
99
  )
100
-
101
  chunks.append({
102
  "id": f"chunk_{num_inicio}_{num_fim}",
103
  "paginas": f"{num_inicio}-{num_fim}",
104
- "num_paginas": len(chunk),
105
- "texto": texto_consolidado,
106
- "metadata": [p["metadata"] for p in chunk]
107
  })
108
  return chunks
109
 
110
- def processar_pdf_completo(arquivo_pdf):
111
- # (Implementação existente, sem alterações)
112
- if not PDF_SUPPORT:
113
- return None, "❌ PyPDF2 não disponível"
114
-
115
- try:
116
- paginas, erro = extrair_texto_pdf(arquivo_pdf.name if hasattr(arquivo_pdf, 'name') else arquivo_pdf)
117
- if erro:
118
- return None, f"❌ Erro ao ler PDF: {erro}"
119
-
120
- chunks = fragmentar_pdf(paginas)
121
- nome_arquivo = os.path.basename(arquivo_pdf.name if hasattr(arquivo_pdf, 'name') else arquivo_pdf)
122
-
123
- return {
124
- "arquivo": nome_arquivo,
125
- "total_paginas": len(paginas),
126
- "total_chunks": len(chunks),
127
- "chunks": chunks,
128
- "tipo": "pdf"
129
- }, None
130
- except Exception as e:
131
- return None, f"❌ Erro no processamento: {str(e)}"
132
-
133
- def ler_arquivo_texto(arquivo):
134
- # (Implementação existente, sem alterações)
135
- if arquivo is None: return None
136
- try:
137
- with open(arquivo.name, "r", encoding="utf-8") as f:
138
- conteudo = f.read()
139
- return {
140
- "arquivo": os.path.basename(arquivo.name),
141
- "conteudo": conteudo,
142
- "tipo": "texto"
143
- }
144
- except: return None
145
-
146
- # ==================== 3. PIPELINE DE IA ====================
147
 
148
  def transcrever_chunk(chunk_data, config_agentes):
149
- # (Implementação existente, sem alterações)
150
  modelo = model_flash
151
  try:
152
- if config_agentes and isinstance(config_agentes, list):
153
- if config_agentes[0].get("modelo") == "pro":
154
- modelo = model_pro
155
  except:
156
  pass
157
-
158
  prompt = f"""
159
- ANÁLISE DE DOCUMENTO (OCR/LEITURA):
160
- Transcreva e estruture o conteúdo das páginas {chunk_data['paginas']}.
161
- Texto extraído:
162
  {chunk_data['texto']}
163
 
164
- Retorne JSON: {{ "transcricao": "...", "objetos": ["..."], "resumo": "..." }}
165
  """
166
  try:
167
- for tentativa in range(3):
168
- try:
169
- resposta = modelo.generate_content(prompt)
170
- texto_resp = resposta.text.replace("```json", "").replace("```", "")
171
- return json.loads(texto_resp.strip()), None
172
- except Exception as inner_e:
173
- if "429" in str(inner_e):
174
- time.sleep(2 * (tentativa + 1))
175
- continue
176
- raise inner_e
177
  except Exception as e:
178
  return None, str(e)
179
 
180
- # ==================== 4. GERENCIADOR DE ARQUIVOS ====================
181
-
182
- class GerenciadorArquivos:
183
- # (Implementação existente, sem alterações)
184
- def __init__(self):
185
- self.arquivos = {}
186
-
187
- def adicionar(self, arquivo, arquivo_id):
188
- self.arquivos[arquivo_id] = {
189
- "arquivo": arquivo,
190
- "nome": os.path.basename(arquivo.name),
191
- "status": "adicionado",
192
- "processado": None,
193
- "transcricao": None
194
- }
195
-
196
- def gerar_prompt_com_transcricoes(self, texto_usuario):
197
- prompt = texto_usuario + "\n\n--- CONTEXTO DOS ARQUIVOS ---\n"
198
- count = 0
199
- for _, item in self.arquivos.items():
200
- if item["status"] == "processado" and item["transcricao"]:
201
- count += 1
202
- trans = item["transcricao"]
203
- nome = item["nome"]
204
- prompt += f"\n[ARQUIVO: {nome}]\n"
205
-
206
- if isinstance(trans, dict) and "chunks_processados" in trans:
207
- for chunk in trans["chunks_processados"]:
208
- if chunk.get("status") == "OK":
209
- resumo = chunk.get('resumo', '')
210
- resumo = str(resumo) if resumo else ""
211
- prompt += f"Páginas {chunk['paginas']}: {resumo}\n"
212
-
213
- texto_full = chunk.get('transcricao', '')
214
- if texto_full:
215
- texto_seguro = str(texto_full)
216
- prompt += f"Trecho: {texto_seguro[:400]}...\n"
217
- else:
218
- prompt += "Trecho: (vazio)\n"
219
-
220
- elif isinstance(trans, dict) and "conteudo" in trans:
221
- conteudo = str(trans['conteudo'])
222
- prompt += f"Conteúdo: {conteudo[:1000]}...\n"
223
-
224
- if count == 0:
225
- prompt += "(Nenhum arquivo processado ainda)"
226
- return prompt
227
-
228
- # Instância Global
229
- gerenciador = GerenciadorArquivos()
230
-
231
- # ==================== 5. FUNÇÕES DE ORQUESTRAÇÃO ====================
232
-
233
- def automacao_upload_processamento(files, history, config_json):
234
- # (Implementação existente, sem alterações)
235
- if not files:
236
- return history
237
 
238
- try:
239
- config_agentes = json.loads(config_json)
240
- except:
241
- config_agentes = []
242
-
243
- if history is None:
244
- history = []
245
-
246
- history.append([None, f"📂 **SISTEMA:** Recebi {len(files)} arquivo(s). Verificando cache e processando..."])
247
- yield history
248
-
249
- ids_para_processar = []
250
-
251
- for f in files:
252
- arquivo_id = f"arq_{int(time.time()*1000)}_{f.name}"
253
- gerenciador.adicionar(f, arquivo_id)
254
- ids_para_processar.append(arquivo_id)
255
-
256
- for arq_id in ids_para_processar:
257
- item = gerenciador.arquivos[arq_id]
258
- nome = item["nome"]
259
-
260
- nome_cache = limpar_nome_arquivo(nome)
261
- caminho_cache = os.path.join(PASTA_TRANSCRICOES, nome_cache)
262
-
263
- if os.path.exists(caminho_cache):
264
- try:
265
- with open(caminho_cache, "r", encoding="utf-8") as cache_file:
266
- dados_cache = json.load(cache_file)
267
- item["transcricao"] = dados_cache
268
- item["status"] = "processado"
269
- if nome.lower().endswith('.pdf') and "chunks_processados" in dados_cache:
270
- item["processado"] = {"tipo": "pdf", "chunks": []}
271
- history.append([None, f"♻️ **Cache Encontrado:** `{nome}` já foi processado. Carregando..."])
272
- yield history
273
- continue
274
- except Exception as e:
275
- history.append([None, f"⚠️ Erro cache `{nome}`: {e}. Reprocessando..."])
276
 
277
- history.append([None, f"⚙️ **Processando:** `{nome}`..."])
278
- yield history
279
 
280
- if nome.lower().endswith('.pdf'):
281
- if not PDF_SUPPORT:
282
- history.append([None, f"❌ Erro em `{nome}`: Biblioteca PDF ausente."])
283
- yield history
284
- continue
285
 
286
- pdf_proc, erro = processar_pdf_completo(item["arquivo"])
 
 
 
 
 
287
  if erro:
288
- history.append([None, f" Erro em `{nome}`: {erro}"])
289
- yield history
290
  continue
291
-
292
- item["processado"] = pdf_proc
293
- chunks = pdf_proc["chunks"]
294
- total_chunks = len(chunks)
295
-
296
- chunks_ordenados = [None] * total_chunks
297
-
298
- history.append([None, f"📄 `{nome}` fragmentado em {total_chunks} partes. Iniciando IA (Paralelo: {MAX_WORKERS} threads)..."])
299
- yield history
300
 
 
 
 
301
  with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
302
- futures_map = {}
303
- for i, chunk in enumerate(chunks):
304
- future = executor.submit(transcrever_chunk, chunk, config_agentes)
305
- futures_map[future] = i
306
-
307
- concluidos = 0
308
- for future in as_completed(futures_map):
309
- index_original = futures_map[future]
310
  res, err = future.result()
311
-
312
  if err:
313
- chunks_ordenados[index_original] = {"status": "ERRO", "paginas": chunks[index_original]["paginas"]}
314
  else:
315
- chunks_ordenados[index_original] = {
316
- "status": "OK",
317
- "paginas": chunks[index_original]["paginas"],
318
- "transcricao": res.get("transcricao"),
319
- "resumo": res.get("resumo")
320
- }
321
-
322
- concluidos += 1
323
- if concluidos % 2 == 0 or concluidos == total_chunks:
324
- msg_base = f"📄 `{nome}`: Processando partes... ({concluidos}/{total_chunks})"
325
- history[-1][1] = msg_base
326
- yield history
327
-
328
- dados_finais = {
329
  "arquivo": nome,
330
- "data_processamento": str(datetime.now()),
331
- "chunks_processados": chunks_ordenados
332
- }
333
-
334
- item["transcricao"] = dados_finais
335
- item["status"] = "processado"
336
-
337
- try:
338
- with open(caminho_cache, "w", encoding="utf-8") as f_out:
339
- json.dump(dados_finais, f_out, indent=2, ensure_ascii=False)
340
- history.append([None, f"💾 `{nome}` processado e salvo no cache."])
341
- except Exception as e:
342
- history.append([None, f"⚠️ Erro ao salvar cache: {e}"])
343
-
344
- yield history
345
-
346
- else:
347
- res = ler_arquivo_texto(item["arquivo"])
348
- if res:
349
- item["processado"] = res
350
- dados_finais = {"conteudo": res["conteudo"], "data_processamento": str(datetime.now())}
351
- item["transcricao"] = dados_finais
352
- item["status"] = "processado"
353
-
354
- with open(caminho_cache, "w", encoding="utf-8") as f_out:
355
- json.dump(dados_finais, f_out, indent=2, ensure_ascii=False)
356
-
357
- history.append([None, f"✅ `{nome}` (Texto) lido e salvo."])
358
- else:
359
- history.append([None, f"❌ Falha ao ler `{nome}`."])
360
- yield history
361
-
362
- history.append([None, "🏁 **Processamento de lote finalizado.** Os arquivos estão prontos para análise."])
363
- yield history
364
-
365
-
366
- def chat_orquestrador(message, history, config_json, pipeline_state):
367
- """
368
- Orquestra a conversa. Pode iniciar uma nova pipeline ou continuar uma que foi pausada.
369
- """
370
-
371
- # --- LÓGICA DE CONTINUAÇÃO ---
372
- if pipeline_state.get("is_paused"):
373
- history.append([message, None])
374
-
375
- # Recupera o estado
376
- timeline_execucao = pipeline_state["timeline"]
377
- agentes_restantes = pipeline_state["remaining_agents"]
378
-
379
- # Adiciona a resposta do usuário à trilha de auditoria
380
- timeline_execucao.append({
381
- "passo": len(timeline_execucao) + 1,
382
- "tipo": "resposta_usuario",
383
- "conteudo": message
384
- })
385
-
386
- # Reseta o estado para evitar loops
387
- pipeline_state["is_paused"] = False
388
-
389
- # Continua a execução do ponto onde parou
390
- yield from executar_pipeline(history, timeline_execucao, agentes_restantes, pipeline_state)
391
- return
392
 
393
- # --- LÓGICA DE INÍCIO DE UMA NOVA CONVERSA ---
394
- try:
395
- prompt_contexto = gerenciador.gerar_prompt_com_transcricoes(message)
396
- except Exception as e:
397
- history.append([message, f"❌ Erro ao gerar contexto: {str(e)}"])
398
- yield history, [], pipeline_state
399
- return
400
-
401
  try:
402
  protocolo = json.loads(config_json)
403
  except:
404
- history.append([message, " Erro no JSON de Configuração do Protocolo."])
405
- yield history, [], pipeline_state
406
- return
407
-
408
- history.append([message, None])
409
-
410
- # Inicia uma nova trilha de auditoria
411
- timeline_execucao = [{"passo": 1, "tipo": "prompt_usuario", "conteudo": prompt_contexto}]
412
- yield history, timeline_execucao, pipeline_state
413
-
414
- # Inicia a execução com todos os agentes do protocolo
415
- yield from executar_pipeline(history, timeline_execucao, protocolo, pipeline_state)
416
-
417
-
418
- def executar_pipeline(history, timeline_execucao, agentes_a_executar, pipeline_state):
419
- """
420
- Função core que executa a lista de agentes em sequência.
421
- Pode ser pausada se um agente pedir input do usuário.
422
- """
423
- passo_atual = len(timeline_execucao) + 1
424
-
425
- for i, cfg in enumerate(agentes_a_executar):
426
- nome_agente = cfg.get("nome", "Agente")
427
- modelo_agente = model_pro if cfg.get("modelo") == "pro" else model_flash
428
-
429
- msg_atual = history[-1][1] or ""
430
- history[-1][1] = msg_atual + f"⏳ **{nome_agente}** está analisando...\n"
431
- yield history, timeline_execucao, pipeline_state
432
-
433
- prompt_agente = f"""
434
- --- HISTÓRICO DA CONVERSA ATÉ AGORA ---
435
- {json.dumps(timeline_execucao, ensure_ascii=False, indent=2)}
436
- -----------------
437
- Sua Identidade: {nome_agente}
438
- Sua Missão Específica Agora: {cfg['missao']}
439
- Responda de forma concisa e direta, focando apenas na sua missão.
440
  """
 
 
 
 
 
441
  try:
442
- inicio = time.time()
443
- resp = modelo_agente.generate_content(prompt_agente)
444
- texto_resp = resp.text
445
- duracao = time.time() - inicio
446
-
447
- # --- LÓGICA DE PAUSA (STOP) ---
448
- try:
449
- # Tenta interpretar a resposta como JSON para verificar se é uma pergunta
450
- resposta_json = json.loads(texto_resp)
451
- if resposta_json.get("tipo") == "pergunta_usuario":
452
- pergunta = resposta_json.get("pergunta", "Não foi possível extrair a pergunta.")
453
-
454
- # Salva o estado atual da pipeline
455
- pipeline_state["is_paused"] = True
456
- pipeline_state["timeline"] = timeline_execucao
457
- # Salva os agentes que AINDA NÃO rodaram
458
- pipeline_state["remaining_agents"] = agentes_a_executar[i+1:]
459
-
460
- # Adiciona a pergunta à auditoria e ao chat
461
- timeline_execucao.append({"passo": passo_atual, "tipo": "pergunta_agente", "agente": nome_agente, "pergunta": pergunta})
462
- msg_atual = history[-1][1].replace(f"⏳ **{nome_agente}** está analisando...\n", "")
463
- history[-1][1] = msg_atual + f"**{nome_agente}** precisa de mais informações:\n\n> *{pergunta}*\n\nAguardando sua resposta na caixa de texto abaixo..."
464
-
465
- # Encerra a execução atual e aguarda o usuário
466
- yield history, timeline_execucao, pipeline_state
467
- return # Sai da função
468
- except (json.JSONDecodeError, TypeError):
469
- # Se não for um JSON de pergunta, é uma resposta normal
470
- pass
471
- # ---------------------------
472
-
473
- timeline_execucao.append({"passo": passo_atual, "tipo": "resposta_agente", "agente": nome_agente, "resposta": texto_resp})
474
-
475
- msg_atual = history[-1][1].replace(f"⏳ **{nome_agente}** está analisando...\n", "")
476
-
477
- # Não exibe o conteúdo da resposta do modelo no chat, apenas a confirmação
478
- novo_trecho = f"✅ **[{nome_agente}]** concluiu sua análise em ({duracao:.1f}s).\n"
479
- history[-1][1] = msg_atual + novo_trecho
480
- yield history, timeline_execucao, pipeline_state
481
-
482
- except Exception as e:
483
- timeline_execucao.append({"passo": passo_atual, "tipo": "erro_agente", "agente": nome_agente, "erro": str(e)})
484
- msg_atual = history[-1][1]
485
- history[-1][1] = msg_atual.replace(f"⏳ **{nome_agente}** está analisando...\n", "") + f"\n❌ Erro em {nome_agente}: {str(e)}\n"
486
- yield history, timeline_execucao, pipeline_state
487
-
488
- passo_atual += 1
489
-
490
- # ==================== 6. UI (Gradio) ====================
491
-
492
- def ui_v29_stop_logic():
493
- css = """
494
- footer {display: none !important;}
495
- .contain {border: none !important;}
496
- """
497
-
498
- config_inicial = carregar_protocolo()
499
-
500
- with gr.Blocks(title="AI Forensics Auto", css=css, theme=gr.themes.Soft()) as app:
501
-
502
- # Estado da configuração dos agentes
503
- state_config = gr.State(config_inicial)
504
- # NOVO: Estado para controlar a pausa/continuação da pipeline
505
- pipeline_state = gr.State({"is_paused": False, "timeline": [], "remaining_agents": []})
506
-
507
- with gr.Tabs():
508
- with gr.Tab("💬 Investigação"):
509
-
510
- chatbot = gr.Chatbot(
511
- height=400,
512
- show_label=False,
513
- show_copy_button=True,
514
- render_markdown=True,
515
- label="Chat de Investigação"
516
- )
517
-
518
- with gr.Row():
519
- txt_input = gr.Textbox(
520
- scale=8,
521
- show_label=False,
522
- placeholder="Digite sua instrução ou responda à pergunta do agente...",
523
- lines=1
524
- )
525
- btn_enviar = gr.Button("Enviar 📨", variant="primary", scale=1)
526
-
527
- with gr.Accordion("📂 Adicionar Arquivos para Análise", open=False):
528
- gr.Markdown("Selecione arquivos (PDF, TXT). A transcrição iniciará **automaticamente**.")
529
- file_uploader = gr.File(
530
- file_count="multiple",
531
- file_types=[".pdf", ".txt", ".json", ".md"],
532
- label="Arraste arquivos aqui ou clique para selecionar"
533
- )
534
-
535
- with gr.Tab("🕵️ Auditoria"):
536
- gr.Markdown("### Trilha de Auditoria\nExibe o histórico completo de prompts e respostas de cada agente na última execução.")
537
- json_audit = gr.JSON(label="Timeline da Execução da Última Mensagem")
538
-
539
- with gr.Tab("⚙️ Contexto & Config"):
540
- gr.Markdown("### Protocolo dos Agentes\nDefina a sequência e as missões dos agentes de IA. Para pausar e pedir input, use a missão de exemplo para o agente retornar um JSON específico.")
541
- with gr.Row():
542
- btn_save_cfg = gr.Button("💾 Salvar Alterações")
543
- lbl_cfg_status = gr.Label(show_label=False)
544
-
545
- code_config = gr.Code(value=config_inicial, language="json", label="protocolo.json")
546
-
547
- btn_save_cfg.click(salvar_protocolo, inputs=[code_config], outputs=[lbl_cfg_status])
548
- # Atualiza o state_config em memória após salvar
549
- btn_save_cfg.click(lambda x: x, inputs=[code_config], outputs=[state_config])
550
-
551
- # Ação de clique agora passa o pipeline_state para o orquestrador
552
- btn_enviar.click(
553
- chat_orquestrador,
554
- inputs=[txt_input, chatbot, state_config, pipeline_state],
555
- outputs=[chatbot, json_audit, pipeline_state] # Atualiza o estado da pipeline
556
- ).then(
557
- lambda: "", outputs=[txt_input]
558
- )
559
 
560
- file_uploader.upload(
561
- automacao_upload_processamento,
562
- inputs=[file_uploader, chatbot, state_config],
563
- outputs=[chatbot]
564
- )
 
 
 
 
 
 
 
 
565
 
566
- return app
 
 
567
 
568
- if __name__ == "__main__":
569
- ui_v29_stop_logic().launch()
 
 
 
 
 
 
 
 
1
  import os
2
  import json
3
  import time
 
5
  from datetime import datetime
6
  from concurrent.futures import ThreadPoolExecutor, as_completed
7
 
8
+ from fastapi import FastAPI, UploadFile, Form
9
+ from fastapi.middleware.cors import CORSMiddleware
10
+ from fastapi.responses import JSONResponse
11
+
12
  import google.generativeai as genai
13
 
14
+ # PDF
15
  try:
16
  import PyPDF2
17
  PDF_SUPPORT = True
18
  except ImportError:
19
  PDF_SUPPORT = False
 
20
 
21
+ # ==================== CONFIG ====================
22
 
23
  api_key = os.getenv("GOOGLE_API_KEY", "SUA_API_KEY_AQUI")
24
  if api_key and api_key != "SUA_API_KEY_AQUI":
25
  genai.configure(api_key=api_key)
26
 
 
27
  model_flash = genai.GenerativeModel("gemini-flash-latest")
28
  model_pro = genai.GenerativeModel("gemini-pro-latest")
29
 
30
  ARQUIVO_CONFIG = "protocolo.json"
31
  PASTA_TRANSCRICOES = "transcricoes"
32
  PAGES_PER_CHUNK = 10
33
+ MAX_WORKERS = 5
34
 
35
  os.makedirs(PASTA_TRANSCRICOES, exist_ok=True)
36
 
37
+ # ==================== APP FASTAPI ====================
38
+
39
+ app = FastAPI(title="AI Forensics API", version="1.0")
40
+
41
+ app.add_middleware(
42
+ CORSMiddleware,
43
+ allow_origins=["*"],
44
+ allow_methods=["*"],
45
+ allow_headers=["*"],
46
+ )
47
+
48
+ # Estado global
49
+ pipeline_state = {"is_paused": False, "timeline": [], "remaining_agents": []}
50
+
51
+ # ----------------- UTILIDADES -----------------
52
 
53
  def carregar_protocolo():
 
54
  try:
55
  with open(ARQUIVO_CONFIG, "r", encoding="utf-8") as f:
56
  return f.read()
57
  except FileNotFoundError:
58
+ return json.dumps([
59
+ {"nome": "Leitor Inicial", "modelo": "flash", "missao": "Leia o contexto e resuma os fatos."},
 
60
  {
61
  "nome": "Advogado de Acusação",
62
  "modelo": "pro",
63
+ "missao": "Faça uma pergunta ao usuário apenas no formato JSON: {\"tipo\":\"pergunta_usuario\", \"pergunta\":\"...\"}"
64
  },
65
+ {"nome": "Analista Final", "modelo": "pro", "missao": " o parecer final com base na resposta."}
66
+ ], indent=2)
67
+
68
 
69
  def salvar_protocolo(conteudo):
70
+ json.loads(conteudo)
71
+ with open(ARQUIVO_CONFIG, "w", encoding="utf-8") as f:
72
+ f.write(conteudo)
73
+
 
 
 
74
 
75
+ # ----------------- PDF -----------------
 
 
 
76
 
77
  def extrair_texto_pdf(caminho_pdf):
 
78
  try:
79
+ with open(caminho_pdf, "rb") as f:
80
  reader = PyPDF2.PdfReader(f)
81
  paginas = []
82
  for i, page in enumerate(reader.pages):
83
+ texto = page.extract_text() or ""
84
  paginas.append({
85
  "numero": i + 1,
86
  "texto": texto,
 
92
 
93
 
94
  def fragmentar_pdf(paginas, tamanho_chunk=PAGES_PER_CHUNK):
 
95
  chunks = []
96
  for i in range(0, len(paginas), tamanho_chunk):
97
  chunk = paginas[i:i + tamanho_chunk]
98
  num_inicio = chunk[0]["numero"]
99
  num_fim = chunk[-1]["numero"]
 
100
  texto_consolidado = "\n---QUEBRA DE PÁGINA---\n".join(
101
  [f"[PÁGINA {p['numero']}]\n{p['texto']}" for p in chunk]
102
  )
103
+
104
  chunks.append({
105
  "id": f"chunk_{num_inicio}_{num_fim}",
106
  "paginas": f"{num_inicio}-{num_fim}",
107
+ "texto": texto_consolidado
 
 
108
  })
109
  return chunks
110
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
112
  def transcrever_chunk(chunk_data, config_agentes):
 
113
  modelo = model_flash
114
  try:
115
+ if config_agentes[0].get("modelo") == "pro":
116
+ modelo = model_pro
 
117
  except:
118
  pass
119
+
120
  prompt = f"""
121
+ Transcreva e estruture o conteúdo das páginas {chunk_data['paginas']}:
122
+
 
123
  {chunk_data['texto']}
124
 
125
+ Retorne JSON: {{"transcricao":"...", "resumo":"..."}}
126
  """
127
  try:
128
+ resposta = modelo.generate_content(prompt)
129
+ texto_resp = resposta.text.replace("```json", "").replace("```", "")
130
+ return json.loads(texto_resp), None
 
 
 
 
 
 
 
131
  except Exception as e:
132
  return None, str(e)
133
 
134
+ # ==================== UPLOAD ====================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
 
136
+ @app.post("/upload")
137
+ async def upload_arquivos(files: list[UploadFile]):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
 
139
+ resultados = []
 
140
 
141
+ for file in files:
142
+ nome = file.filename
143
+ caminho_temp = f"/tmp/{nome}"
 
 
144
 
145
+ with open(caminho_temp, "wb") as f:
146
+ f.write(await file.read())
147
+
148
+ # PDF
149
+ if nome.lower().endswith(".pdf"):
150
+ paginas, erro = extrair_texto_pdf(caminho_temp)
151
  if erro:
152
+ resultados.append({"arquivo": nome, "erro": erro})
 
153
  continue
 
 
 
 
 
 
 
 
 
154
 
155
+ chunks = fragmentar_pdf(paginas)
156
+
157
+ resultados_chunks = []
158
  with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
159
+ futures = {
160
+ executor.submit(transcrever_chunk, c, []): c
161
+ for c in chunks
162
+ }
163
+ for future in as_completed(futures):
 
 
 
164
  res, err = future.result()
 
165
  if err:
166
+ resultados_chunks.append({"status": "ERRO", "erro": err})
167
  else:
168
+ resultados_chunks.append({"status": "OK", **res})
169
+
170
+ resultados.append({
 
 
 
 
 
 
 
 
 
 
 
171
  "arquivo": nome,
172
+ "tipo": "pdf",
173
+ "chunks": resultados_chunks
174
+ })
175
+
176
+ # Texto
177
+ elif nome.lower().endswith(".txt"):
178
+ with open(caminho_temp, "r", encoding="utf-8") as f:
179
+ conteudo = f.read()
180
+
181
+ resultados.append({
182
+ "arquivo": nome,
183
+ "tipo": "texto",
184
+ "conteudo": conteudo
185
+ })
186
+
187
+ return JSONResponse(resultados)
188
+
189
+
190
+ # ==================== CHAT AGENTES ====================
191
+
192
+ @app.post("/chat")
193
+ async def chat_pipeline(message: str = Form(...), config_json: str = Form(...)):
194
+
195
+ global pipeline_state
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
 
 
 
 
 
 
 
 
 
197
  try:
198
  protocolo = json.loads(config_json)
199
  except:
200
+ return {"erro": "Configuração JSON inválida."}
201
+
202
+ # pipeline pausado → usuário respondeu
203
+ if pipeline_state["is_paused"]:
204
+ timeline = pipeline_state["timeline"]
205
+ remanescentes = pipeline_state["remaining_agents"]
206
+ pipeline_state["is_paused"] = False
207
+
208
+ timeline.append({"tipo": "resposta_usuario", "conteudo": message})
209
+ return await executar_pipeline(timeline, remanescentes)
210
+
211
+ # nova thread
212
+ timeline = [{"tipo": "prompt_usuario", "conteudo": message}]
213
+ return await executar_pipeline(timeline, protocolo)
214
+
215
+
216
+ async def executar_pipeline(timeline_execucao, agentes):
217
+
218
+ global pipeline_state
219
+
220
+ for i, cfg in enumerate(agentes):
221
+
222
+ nome = cfg["nome"]
223
+ modelo = model_pro if cfg.get("modelo") == "pro" else model_flash
224
+
225
+ prompt = f"""
226
+ --- HISTÓRICO ---
227
+ {json.dumps(timeline_execucao, indent=2, ensure_ascii=False)}
228
+
229
+ Sua identidade: {nome}
230
+ Sua missão: {cfg['missao']}
 
 
 
 
 
231
  """
232
+
233
+ resp = modelo.generate_content(prompt)
234
+ texto = resp.text
235
+
236
+ # STOP?
237
  try:
238
+ parsed = json.loads(texto)
239
+ if parsed.get("tipo") == "pergunta_usuario":
240
+ pergunta = parsed.get("pergunta")
241
+
242
+ pipeline_state = {
243
+ "is_paused": True,
244
+ "timeline": timeline_execucao,
245
+ "remaining_agents": agentes[i+1:]
246
+ }
247
+
248
+ return {
249
+ "status": "aguardando_usuario",
250
+ "agente": nome,
251
+ "pergunta": pergunta
252
+ }
253
+ except:
254
+ pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
 
256
+ timeline_execucao.append({
257
+ "tipo": "resposta_agente",
258
+ "agente": nome,
259
+ "resposta": texto
260
+ })
261
+
262
+ return {
263
+ "status": "concluido",
264
+ "timeline": timeline_execucao
265
+ }
266
+
267
+
268
+ # ==================== PROTOCOLO ====================
269
 
270
+ @app.get("/protocolo")
271
+ def get_protocolo():
272
+ return JSONResponse(json.loads(carregar_protocolo()))
273
 
274
+
275
+ @app.post("/protocolo")
276
+ def post_protocolo(conteudo: str = Form(...)):
277
+ try:
278
+ salvar_protocolo(conteudo)
279
+ return {"status": "ok", "mensagem": "Protocolo salvo."}
280
+ except Exception as e:
281
+ return {"erro": str(e)}