caarleexx commited on
Commit
5b4caf5
·
verified ·
1 Parent(s): bb02c59

Upload ai_studio_code (5).py

Browse files
Files changed (1) hide show
  1. ai_studio_code (5).py +716 -0
ai_studio_code (5).py ADDED
@@ -0,0 +1,716 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import time
4
+ import hashlib
5
+ from datetime import datetime
6
+ from concurrent.futures import ThreadPoolExecutor, as_completed
7
+
8
+ import gradio as gr
9
+ import google.generativeai as genai
10
+
11
+ # =================================================================================
12
+ # METADADOS DO PROJETO - HACKATHON OAB 2025
13
+ # --------------------------------------------------------------------------------
14
+ # Desenvolvedor: Carlos Rodrigues dos Santos
15
+ # Contato: carlex22@gmaill.com
16
+ # GitHub: github.com/carlex22
17
+ # Licença: GPLv3
18
+ # --------------------------------------------------------------------------------
19
+ # FUNÇÃO DE ORQUESTRAÇÃO, CADEIA COGNITIVA E GOVERNANÇA (Transparência Causal)
20
+ # --------------------------------------------------------------------------------
21
+ # O orquestrador gerencia a 'Cadeia Cognitiva' através de 'Agentes Causais',
22
+ # onde a saída de uma Fase (JSON de contexto) alimenta a entrada da próxima.
23
+ # Isso garante 'Transparência' e 'Governanca' por meio da:
24
+ #
25
+ # 1. Pipeline Estruturada: Execução sequencial das missões definidas em
26
+ # 'protocolo.json' (Fase 0 a 7), garantindo que cada agente cumpra um papel.
27
+ # 2. Saída Auditorável (JSON): As fases retornam um JSON padronizado para
28
+ # o painel 'Auditoria', permitindo rastrear o raciocínio de cada agente.
29
+ # 3. Ponto de Controle (STOP): A Fase 0 (e a lógica da pipeline) pode solicitar
30
+ # input do usuário (gatilho STOP), pausando a cadeia para validar fatos
31
+ # críticos ou inserir contexto faltante, garantindo governança humana
32
+ # em momentos de alta incerteza.
33
+ # 4. Formato Envolvente (Fase 7): A fase final (Relatório) recebe tratamento
34
+ # especial para apresentar a valoração de forma dinâmica e clara, conforme
35
+ # o propósito de valorar a dignidade humana.
36
+ # 5. Contexto Antecipatório: Opcionalmente inclui um passo inicial com dados
37
+ # de 'data.txt' para estabelecer regras e 'verdades factuais' para os agentes.
38
+ # =================================================================================
39
+
40
+
41
+ # Dependências para PDF
42
+ try:
43
+ import PyPDF2
44
+ PDF_SUPPORT = True
45
+ except ImportError:
46
+ PDF_SUPPORT = False
47
+ print("⚠️ PyPDF2 não instalado. PDFs serão lidos como texto simples.")
48
+
49
+ # ==================== 1. CONFIGURAÇÃO ====================
50
+
51
+ api_key = os.getenv("GOOGLE_API_KEY", "SUA_API_KEY_AQUI")
52
+ if api_key and api_key != "SUA_API_KEY_AQUI":
53
+ genai.configure(api_key=api_key)
54
+
55
+ # Modelos do Gemini
56
+ model_flash = genai.GenerativeModel("gemini-flash-latest")
57
+ model_pro = genai.GenerativeModel("gemini-pro-latest")
58
+
59
+ ARQUIVO_CONFIG = "protocolo.json"
60
+ ARQUIVO_CONTEXTO_ANTECIPATORIO = "data.txt" # Arquivo com regras/verdade factual
61
+ ARQUIVO_DOCUMENTACAO = "help.md" # Arquivo com documentação (pode ser .txt)
62
+ PASTA_TRANSCRICOES = "transcricoes"
63
+ PAGES_PER_CHUNK = 10
64
+ MAX_WORKERS = 5 # Limite de chamadas paralelas
65
+
66
+ os.makedirs(PASTA_TRANSCRICOES, exist_ok=True)
67
+
68
+ # ==================== 2. UTILIDADES ====================
69
+
70
+ # Define as constantes de autoria
71
+ DEVS_NAME = "Carlos Rodrigues dos Santos"
72
+ DEVS_EMAIL = "carlex22@gmaill.com"
73
+ LICENSE_INFO = "GPLv3"
74
+
75
+ def ler_arquivo_texto(arquivo_path):
76
+ # Função auxiliar que lê um arquivo de texto pelo caminho
77
+ if not os.path.exists(arquivo_path):
78
+ return None
79
+ try:
80
+ with open(arquivo_path, "r", encoding="utf-8") as f:
81
+ conteudo = f.read()
82
+ return conteudo
83
+ except: return None
84
+
85
+ def carregar_protocolo():
86
+ """ Carrega o protocolo. Se não existir, cria um com um esqueleto limpo. """
87
+ return ler_arquivo_texto(ARQUIVO_CONFIG) or json.dumps(
88
+ [{"fase": 0, "nome": "INICIAR_ANALISE", "modelo": "flash", "tipo_saida": "json", "missao": "Leia o input do usuário e os documentos anexados. Identifique quem é a vítima, qual foi o dano (ex: Morte, Lesão Grave) e quem é o réu. Se faltarem informações críticas para uma análise de valoração (ex: os fatos do caso), defina DUVIDA_DETECTADA como true e use TESTE_REFLEXAO para perguntar ao usuário."}],
89
+ indent=2,
90
+ ensure_ascii=False
91
+ )
92
+
93
+ def carregar_documentacao():
94
+ """ Carrega o arquivo de documentação. """
95
+ return ler_arquivo_texto(ARQUIVO_DOCUMENTACAO) or "Documentação não encontrada. Crie um arquivo 'help.md' ou 'help.txt' na pasta principal para exibir aqui."
96
+
97
+
98
+ def limpar_nome_arquivo(nome):
99
+ # (Implementação existente)
100
+ nome_base = os.path.basename(nome)
101
+ nome_limpo = "".join([c for c in nome_base if c.isalnum() or c in (' ', '.', '_', '-')]).strip()
102
+ return nome_limpo + ".json"
103
+
104
+ def extrair_texto_pdf(caminho_pdf):
105
+ # (Implementação existente)
106
+ try:
107
+ with open(caminho_pdf, 'rb') as f:
108
+ reader = PyPDF2.PdfReader(f)
109
+ paginas = []
110
+ for i, page in enumerate(reader.pages):
111
+ texto = page.extract_text()
112
+ paginas.append({
113
+ "numero": i + 1,
114
+ "texto": texto,
115
+ "metadata": str(page)[:200]
116
+ })
117
+ return paginas, None
118
+ except Exception as e:
119
+ return None, str(e)
120
+
121
+
122
+ def fragmentar_pdf(paginas, tamanho_chunk=PAGES_PER_CHUNK):
123
+ # (Implementação existente)
124
+ chunks = []
125
+ for i in range(0, len(paginas), tamanho_chunk):
126
+ chunk = paginas[i:i + tamanho_chunk]
127
+ num_inicio = chunk[0]["numero"]
128
+ num_fim = chunk[-1]["numero"]
129
+
130
+ texto_consolidado = "\n---QUEBRA DE PÁGINA---\n".join(
131
+ [f"[PÁGINA {p['numero']}]\n{p['texto']}" for p in chunk]
132
+ )
133
+
134
+ chunks.append({
135
+ "id": f"chunk_{num_inicio}_{num_fim}",
136
+ "paginas": f"{num_inicio}-{num_fim}",
137
+ "num_paginas": len(chunk),
138
+ "texto": texto_consolidado,
139
+ "metadata": [p["metadata"] for p in chunk]
140
+ })
141
+ return chunks
142
+
143
+ def processar_pdf_completo(arquivo_pdf):
144
+ # (Implementação existente)
145
+ if not PDF_SUPPORT:
146
+ return None, "❌ PyPDF2 não disponível"
147
+
148
+ try:
149
+ paginas, erro = extrair_texto_pdf(arquivo_pdf.name if hasattr(arquivo_pdf, 'name') else arquivo_pdf)
150
+ if erro:
151
+ return None, f"❌ Erro ao ler PDF: {erro}"
152
+
153
+ chunks = fragmentar_pdf(paginas)
154
+ nome_arquivo = os.path.basename(arquivo_pdf.name if hasattr(arquivo_pdf, 'name') else arquivo_pdf)
155
+
156
+ return {
157
+ "arquivo": nome_arquivo,
158
+ "total_paginas": len(paginas),
159
+ "total_chunks": len(chunks),
160
+ "chunks": chunks,
161
+ "tipo": "pdf"
162
+ }, None
163
+ except Exception as e:
164
+ return None, f"❌ Erro no processamento: {str(e)}"
165
+
166
+ # A função limpar_markdown_basico foi removida, pois reativamos o render_markdown no Chatbot.
167
+
168
+
169
+ # ==================== 3. PIPELINE DE IA ====================
170
+
171
+ def transcrever_chunk(chunk_data, config_agentes):
172
+ # (Implementação existente)
173
+ modelo = model_flash
174
+ try:
175
+ if config_agentes and isinstance(config_agentes, list):
176
+ if config_agentes[0].get("modelo") == "pro":
177
+ modelo = model_pro
178
+ except:
179
+ pass
180
+
181
+ prompt = f"""
182
+ ANÁLISE DE DOCUMENTO (OCR/LEITURA):
183
+ Transcreva e estruture o conteúdo das páginas {chunk_data['paginas']}.
184
+ Texto extraído:
185
+ {chunk_data['texto']}
186
+
187
+ Retorne JSON: {{ "transcricao": "...", "objetos": ["..."], "resumo": "..." }}
188
+ """
189
+ try:
190
+ for tentativa in range(3):
191
+ try:
192
+ resposta = modelo.generate_content(prompt)
193
+ texto_resp = resposta.text.replace("```json", "").replace("```", "")
194
+ return json.loads(texto_resp.strip()), None
195
+ except Exception as inner_e:
196
+ if "429" in str(inner_e):
197
+ time.sleep(2 * (tentativa + 1))
198
+ continue
199
+ raise inner_e
200
+ except Exception as e:
201
+ return None, str(e)
202
+
203
+ # ==================== 4. GERENCIADOR DE ARQUIVOS ====================
204
+
205
+ class GerenciadorArquivos:
206
+ def __init__(self):
207
+ self.arquivos = {}
208
+
209
+ def adicionar(self, arquivo, arquivo_id):
210
+ # (Implementação existente)
211
+ self.arquivos[arquivo_id] = {
212
+ "arquivo": arquivo,
213
+ "nome": os.path.basename(arquivo.name),
214
+ "status": "adicionado",
215
+ "processado": None,
216
+ "transcricao": None
217
+ }
218
+
219
+ def gerar_prompt_com_transcricoes(self, texto_usuario):
220
+ # (Implementação existente)
221
+ prompt = texto_usuario + "\n\n--- CONTEXTO DOS ARQUIVOS ---\n"
222
+ count = 0
223
+ for _, item in self.arquivos.items():
224
+ if item["status"] == "processado" and item["transcricao"]:
225
+ count += 1
226
+ trans = item["transcricao"]
227
+ nome = item["nome"]
228
+ prompt += f"\n[ARQUIVO: {nome}]\n"
229
+
230
+ if isinstance(trans, dict) and "chunks_processados" in trans:
231
+ for chunk in trans["chunks_processados"]:
232
+ if chunk.get("status") == "OK":
233
+ resumo = chunk.get('resumo', '')
234
+ resumo = str(resumo) if resumo else ""
235
+ prompt += f"Páginas {chunk['paginas']}: {resumo}\n"
236
+
237
+ texto_full = chunk.get('transcricao', '')
238
+ if texto_full:
239
+ texto_seguro = str(texto_full)
240
+ prompt += f"Trecho: {texto_seguro[:400]}...\n"
241
+ else:
242
+ prompt += "Trecho: (vazio)\n"
243
+
244
+ elif isinstance(trans, dict) and "conteudo" in trans:
245
+ conteudo = str(trans['conteudo'])
246
+ prompt += f"Conteúdo: {conteudo[:1000]}...\n"
247
+
248
+ if count == 0:
249
+ prompt += "(Nenhum arquivo processado ainda)"
250
+ return prompt
251
+
252
+ # Instância Global
253
+ gerenciador = GerenciadorArquivos()
254
+
255
+ # ==================== 5. FUNÇÕES DE ORQUESTRAÇÃO ====================
256
+
257
+ def automacao_upload_processamento(files, history, config_json):
258
+ # (Função de processamento de arquivos)
259
+ if not files:
260
+ return history
261
+
262
+ try:
263
+ config_agentes = json.loads(config_json)
264
+ except:
265
+ config_agentes = []
266
+
267
+ if history is None:
268
+ history = []
269
+
270
+ # CORREÇÃO: [None, mensagem] -> ["", mensagem]
271
+ history.append(["", f"📂 **SISTEMA:** Recebi {len(files)} arquivo(s). Verificando cache e processando..."])
272
+ yield history
273
+
274
+ ids_para_processar = []
275
+
276
+ for f in files:
277
+ # Usa um hash do conteúdo para ID de arquivo de texto simples para melhorar o cache
278
+ if f.name.lower().endswith(('.txt', '.json', '.md')):
279
+ with open(f.name, 'r', encoding='utf-8') as file:
280
+ file_hash = hashlib.sha256(file.read().encode('utf-8')).hexdigest()
281
+ arquivo_id = f"txt_{file_hash}"
282
+ else:
283
+ arquivo_id = f"arq_{int(time.time()*1000)}_{f.name}"
284
+
285
+ gerenciador.adicionar(f, arquivo_id)
286
+ ids_para_processar.append(arquivo_id)
287
+
288
+ for arq_id in ids_para_processar:
289
+ item = gerenciador.arquivos[arq_id]
290
+ nome = item["nome"]
291
+
292
+ # Cria um nome de cache consistente
293
+ nome_cache = limpar_nome_arquivo(nome)
294
+ caminho_cache = os.path.join(PASTA_TRANSCRICOES, nome_cache)
295
+
296
+ # Lógica de Cache e Reprocessamento
297
+ if os.path.exists(caminho_cache):
298
+ try:
299
+ with open(caminho_cache, "r", encoding="utf-8") as cache_file:
300
+ dados_cache = json.load(cache_file)
301
+ item["transcricao"] = dados_cache
302
+ item["status"] = "processado"
303
+ if nome.lower().endswith('.pdf') and "chunks_processados" in dados_cache:
304
+ item["processado"] = {"tipo": "pdf", "chunks": []}
305
+ # CORREÇÃO: [None, mensagem] -> ["", mensagem]
306
+ history.append(["", f"♻️ **Cache Encontrado:** `{nome}` já foi processado. Carregando..."])
307
+ yield history
308
+ continue
309
+ except Exception as e:
310
+ # CORREÇÃO: [None, mensagem] -> ["", mensagem]
311
+ history.append(["", f"⚠️ Erro cache `{nome}`: {e}. Reprocessando..."])
312
+
313
+ # CORREÇÃO: [None, mensagem] -> ["", mensagem]
314
+ history.append(["", f"⚙️ **Processando:** `{nome}`..."])
315
+ yield history
316
+
317
+ if nome.lower().endswith('.pdf'):
318
+ if not PDF_SUPPORT:
319
+ # CORREÇÃO: [None, mensagem] -> ["", mensagem]
320
+ history.append(["", f"❌ Erro em `{nome}`: Biblioteca PDF ausente."])
321
+ yield history
322
+ continue
323
+
324
+ pdf_proc, erro = processar_pdf_completo(item["arquivo"])
325
+ if erro:
326
+ # CORREÇÃO: [None, mensagem] -> ["", mensagem]
327
+ history.append(["", f"❌ Erro em `{nome}`: {erro}"])
328
+ yield history
329
+ continue
330
+
331
+ item["processado"] = pdf_proc
332
+ chunks = pdf_proc["chunks"]
333
+ total_chunks = len(chunks)
334
+
335
+ chunks_ordenados = [None] * total_chunks
336
+
337
+ # CORREÇÃO: [None, mensagem] -> ["", mensagem]
338
+ history.append(["", f"📄 `{nome}` fragmentado em {total_chunks} partes. Iniciando IA (Paralelo: {MAX_WORKERS} threads)..."])
339
+ yield history
340
+
341
+ with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
342
+ futures_map = {}
343
+ for i, chunk in enumerate(chunks):
344
+ future = executor.submit(transcrever_chunk, chunk, config_agentes)
345
+ futures_map[future] = i
346
+
347
+ concluidos = 0
348
+ for future in as_completed(futures_map):
349
+ index_original = futures_map[future]
350
+ res, err = future.result()
351
+
352
+ if err:
353
+ chunks_ordenados[index_original] = {"status": "ERRO", "paginas": chunks[index_original]["paginas"]}
354
+ else:
355
+ chunks_ordenados[index_original] = {
356
+ "status": "OK",
357
+ "paginas": chunks[index_original]["paginas"],
358
+ "transcricao": res.get("transcricao"),
359
+ "resumo": res.get("resumo")
360
+ }
361
+
362
+ concluidos += 1
363
+ if concluidos % 2 == 0 or concluidos == total_chunks:
364
+ msg_base = f"📄 `{nome}`: Processando partes... ({concluidos}/{total_chunks})"
365
+ history[-1][1] = msg_base
366
+ yield history
367
+
368
+ dados_finais = {
369
+ "arquivo": nome,
370
+ "data_processamento": str(datetime.now()),
371
+ "chunks_processados": chunks_ordenados
372
+ }
373
+
374
+ item["transcricao"] = dados_finais
375
+ item["status"] = "processado"
376
+
377
+ try:
378
+ with open(caminho_cache, "w", encoding="utf-8") as f_out:
379
+ json.dump(dados_finais, f_out, indent=2, ensure_ascii=False)
380
+ # CORREÇÃO: [None, mensagem] -> ["", mensagem]
381
+ history.append(["", f"💾 `{nome}` processado e salvo no cache."])
382
+ except Exception as e:
383
+ # CORREÇÃO: [None, mensagem] -> ["", mensagem]
384
+ history.append(["", f"⚠️ Erro ao salvar cache: {e}"])
385
+
386
+ yield history
387
+
388
+ else:
389
+ res_content = ler_arquivo_texto(item["arquivo"].name)
390
+ if res_content:
391
+ item["processado"] = res_content
392
+ dados_finais = {"conteudo": res_content, "data_processamento": str(datetime.now())}
393
+ item["transcricao"] = dados_finais
394
+ item["status"] = "processado"
395
+
396
+ with open(caminho_cache, "w", encoding="utf-8") as f_out:
397
+ json.dump(dados_finais, f_out, indent=2, ensure_ascii=False)
398
+
399
+ # CORREÇÃO: [None, mensagem] -> ["", mensagem]
400
+ history.append(["", f"✅ `{nome}` (Texto) lido e salvo."])
401
+ else:
402
+ # CORREÇÃO: [None, mensagem] -> ["", mensagem]
403
+ history.append(["", f"❌ Falha ao ler `{nome}`."])
404
+ yield history
405
+
406
+ # CORREÇÃO: [None, mensagem] -> ["", mensagem]
407
+ history.append(["", "🏁 **Processamento de lote finalizado.** Os arquivos estão prontos para análise."])
408
+ yield history
409
+
410
+
411
+ def chat_orquestrador(message, history, config_json, pipeline_state, incluir_passo_antecipatorio):
412
+ if pipeline_state.get("is_paused"):
413
+ # (Lógica de continuação após STOP)
414
+ history.append([message, None])
415
+
416
+ timeline_execucao = pipeline_state["timeline"]
417
+ agentes_restantes = pipeline_state["remaining_agents"]
418
+
419
+ timeline_execucao.append({
420
+ "passo": len(timeline_execucao) + 1,
421
+ "tipo": "resposta_usuario",
422
+ "conteudo": message
423
+ })
424
+
425
+ pipeline_state["is_paused"] = False
426
+
427
+ yield from executar_pipeline(history, timeline_execucao, agentes_restantes, pipeline_state)
428
+ return
429
+
430
+ # --- LÓGICA DE INÍCIO DE UMA NOVA CONVERSA ---
431
+
432
+ # 1. Monta o Prompt de Contexto dos Arquivos
433
+ try:
434
+ prompt_contexto = gerenciador.gerar_prompt_com_transcricoes(message)
435
+ except Exception as e:
436
+ history.append([message, f"❌ Erro ao gerar contexto: {str(e)}"])
437
+ yield history, [], pipeline_state
438
+ return
439
+
440
+ # 2. Carrega o Protocolo
441
+ try:
442
+ protocolo = json.loads(config_json)
443
+ except:
444
+ history.append([message, "❌ Erro no JSON de Configuração do Protocolo."])
445
+ yield history, [], pipeline_state
446
+ return
447
+
448
+ # CORREÇÃO: Usar None é aceitável para a primeira parte de uma nova conversa.
449
+ history.append([message, None])
450
+
451
+ # 3. Lógica do Passo Antecipatório
452
+ timeline_execucao = []
453
+ agentes_a_executar = protocolo
454
+
455
+ if incluir_passo_antecipatorio:
456
+ conteudo_antecipatorio = ler_arquivo_texto(ARQUIVO_CONTEXTO_ANTECIPATORIO)
457
+ if conteudo_antecipatorio:
458
+ # Injeta o contexto na timeline como o primeiro passo
459
+ timeline_execucao.append({
460
+ "passo": 1,
461
+ "tipo": "passo_antecipatorio_data",
462
+ "agente": "GOVERNANCA_INICIAL",
463
+ "conteudo": f"REGRAS E VERDADE FACTUAL INICIAL (data.txt):\n{conteudo_antecipatorio}"
464
+ })
465
+ # CORREÇÃO: [None, mensagem] -> ["", mensagem]
466
+ history.append(["", f"ℹ️ **GOVERNANÇA INICIAL:** Contexto de `{ARQUIVO_CONTEXTO_ANTECIPATORIO}` injetado na cadeia cognitiva."])
467
+ history[-1][1] += f"\n\nInstrução do usuário: {message}"
468
+ else:
469
+ # Se o arquivo não existe, apenas adiciona a instrução do usuário
470
+ history[-1][1] = f"⚠️ Instrução do usuário: {message}"
471
+ history[-1][1] += f"\n\n**Aviso:** Passo Antecipatório ativado, mas arquivo `{ARQUIVO_CONTEXTO_ANTECIPATORIO}` não encontrado."
472
+
473
+ # Ajusta o número do passo inicial na timeline
474
+ passo_inicial_prompt = len(timeline_execucao) + 1
475
+ else:
476
+ passo_inicial_prompt = 1
477
+
478
+ # Adiciona a instrução do usuário (com contexto de arquivos) à trilha de auditoria
479
+ timeline_execucao.append({"passo": passo_inicial_prompt, "tipo": "prompt_usuario_base", "conteudo": prompt_contexto})
480
+
481
+ yield history, timeline_execucao, pipeline_state
482
+
483
+ # Inicia a execução dos agentes
484
+ yield from executar_pipeline(history, timeline_execucao, agentes_a_executar, pipeline_state)
485
+
486
+
487
+ def executar_pipeline(history, timeline_execucao, agentes_a_executar, pipeline_state):
488
+ # (Implementação de execução da pipeline e simulação de digitação)
489
+ passo_atual = len(timeline_execucao) + 1
490
+
491
+ for i, cfg in enumerate(agentes_a_executar):
492
+ nome_agente = cfg.get("nome", "Agente")
493
+ modelo_agente = model_pro if cfg.get("modelo") == "pro" else model_flash
494
+ tipo_saida = cfg.get("tipo_saida", "json")
495
+
496
+ msg_atual = history[-1][1] or ""
497
+ history[-1][1] = msg_atual + f"⏳ **{nome_agente}** está analisando...\n"
498
+ yield history, timeline_execucao, pipeline_state
499
+
500
+ prompt_agente = f"""
501
+ --- HISTÓRICO DA CONVERSA ATÉ AGORA ---
502
+ {json.dumps(timeline_execucao, ensure_ascii=False, indent=2)}
503
+ -----------------
504
+ Sua Identidade: {nome_agente}
505
+ Sua Missão Específica Agora: {cfg['missao']}
506
+ Se o tipo de saída exigido for 'json', sua resposta DEVE ser APENAS o JSON.
507
+ """
508
+
509
+ try:
510
+ inicio = time.time()
511
+ resp = modelo_agente.generate_content(prompt_agente)
512
+ texto_resp = resp.text
513
+ duracao = time.time() - inicio
514
+
515
+ # --- LÓGICA DE DETECÇÃO DE SAÍDA NÃO-JSON (STOP ou Relatório) ---
516
+ if tipo_saida == "json":
517
+ try:
518
+ # Tenta interpretar a resposta como JSON
519
+ texto_json_limpo = texto_resp.replace("```json", "").replace("```", "").strip()
520
+ resposta_json = json.loads(texto_json_limpo)
521
+
522
+ # 1. LÓGICA DE PAUSA (STOP)
523
+ if resposta_json.get("PROXIMA_ACAO") == "PERGUNTAR_USUARIO" and resposta_json.get("DUVIDA_DETECTADA") == True:
524
+
525
+ # VERIFICA REGRAS DE STOP - Fora de escopo?
526
+ if "STOP:" in texto_resp:
527
+ # Caso de STOP por fora do escopo
528
+ stop_message = texto_resp.split("STOP:")[1].strip() if "STOP:" in texto_resp else "Análise interrompida por dúvida crítica ou tema fora de escopo."
529
+ history[-1][1] = stop_message
530
+ yield history, timeline_execucao, pipeline_state
531
+ return
532
+
533
+ # Caso de STOP por dúvida crítica (regra do protocolo da Fase 0)
534
+ perguntas = resposta_json.get("TESTE_REFLEXAO", {}).get("perguntas", [])
535
+
536
+ # Formata a resposta STOP
537
+ stop_response = "STOP: preciso que você esclareça até 3 pontos antes de continuar:\n\n"
538
+ for idx, p in enumerate(perguntas):
539
+ if idx < 3:
540
+ stop_response += f"{idx + 1}) {p}\n"
541
+
542
+ # Salva o estado atual da pipeline
543
+ pipeline_state["is_paused"] = True
544
+ pipeline_state["timeline"] = timeline_execucao
545
+ pipeline_state["remaining_agents"] = agentes_a_executar[i+1:]
546
+
547
+ # Adiciona o passo à auditoria e ao chat
548
+ timeline_execucao.append({"passo": passo_atual, "tipo": "STOP_USUARIO_REQUERIDO", "agente": nome_agente, "detalhes": stop_response})
549
+
550
+ # Prepara a mensagem para o usuário no chat
551
+ msg_para_usuario = f"**{nome_agente}** precisa de mais informações. Por favor, responda aos pontos abaixo para seguirmos com a análise:\n\n"
552
+ for idx, p in enumerate(perguntas):
553
+ if idx < 3:
554
+ msg_para_usuario += f"**{idx + 1})** {p}\n"
555
+
556
+ msg_atual = history[-1][1].replace(f"⏳ **{nome_agente}** está analisando...\n", "")
557
+ history[-1][1] = msg_atual + msg_para_usuario
558
+
559
+ yield history, timeline_execucao, pipeline_state
560
+ return # Sai da função para esperar o input do usuário
561
+
562
+ # 2. Resposta JSON normal
563
+ texto_para_auditoria = texto_json_limpo
564
+ timeline_execucao.append({"passo": passo_atual, "tipo": "resposta_agente", "agente": nome_agente, "resposta": texto_para_auditoria})
565
+
566
+ msg_atual = history[-1][1].replace(f"⏳ **{nome_agente}** está analisando...\n", "")
567
+ novo_trecho = f"✅ **[{nome_agente}]** concluiu sua análise em ({duracao:.1f}s). (JSON para Auditoria)\n"
568
+ history[-1][1] = msg_atual + novo_trecho
569
+ yield history, timeline_execucao, pipeline_state
570
+
571
+ except json.JSONDecodeError:
572
+ # Se deveria ser JSON e não é, trata como erro na auditoria
573
+ timeline_execucao.append({"passo": passo_atual, "tipo": "erro_resposta_json", "agente": nome_agente, "resposta_raw": texto_resp, "erro": "Esperado JSON, mas recebeu texto não-JSON."})
574
+ msg_atual = history[-1][1].replace(f"⏳ **{nome_agente}** está analisando...\n", "")
575
+ history[-1][1] = msg_atual + f"❌ **[{nome_agente}]** falhou: Resposta não era JSON válido. ({duracao:.1f}s).\n"
576
+ yield history, timeline_execucao, pipeline_state
577
+
578
+ elif tipo_saida == "texto":
579
+ # Lógica para a Fase 7 (RELATORIO_VALOR_VIDA)
580
+
581
+ # O markdown será renderizado pelo Chatbot, não precisa de limpeza manual.
582
+
583
+ # Adiciona o passo à auditoria
584
+ timeline_execucao.append({"passo": passo_atual, "tipo": "relatorio_final", "agente": nome_agente, "relatorio": texto_resp})
585
+
586
+ # Simula a digitação envolvente
587
+ msg_final = f"**[RELATÓRIO FINAL DO {nome_agente}]**\n\n"
588
+ history[-1][1] = msg_final
589
+ yield history, timeline_execucao, pipeline_state
590
+
591
+ # Divide o texto do relatório em blocos (parágrafos ou seções)
592
+ blocos = texto_resp.split('\n\n')
593
+
594
+ for bloco in blocos:
595
+ if bloco.strip():
596
+ # Simula a digitação bloco por bloco
597
+ msg_final += f"{bloco}\n\n"
598
+ history[-1][1] = msg_final
599
+ # Pequena pausa para o efeito dinâmico
600
+ time.sleep(0.5)
601
+ yield history, timeline_execucao, pipeline_state
602
+
603
+ # Finalização da digitação no chat
604
+ msg_final += "\n\n--- FIM DO RELATÓRIO DE VALORAÇÃO ---"
605
+ history[-1][1] = msg_final
606
+ yield history, timeline_execucao, pipeline_state
607
+
608
+ else:
609
+ # Caso de tipo_saida não definido (erro)
610
+ timeline_execucao.append({"passo": passo_atual, "tipo": "erro_config", "agente": nome_agente, "erro": "Tipo de saída não reconhecido ('json' ou 'texto')."})
611
+ msg_atual = history[-1][1].replace(f"⏳ **{nome_agente}** está analisando...\n", "")
612
+ history[-1][1] = msg_atual + f"\n❌ Erro de Configuração em {nome_agente}: Tipo de saída inválido. ({duracao:.1f}s).\n"
613
+ yield history, timeline_execucao, pipeline_state
614
+
615
+ except Exception as e:
616
+ timeline_execucao.append({"passo": passo_atual, "tipo": "erro_agente", "agente": nome_agente, "erro": str(e)})
617
+ msg_atual = history[-1][1]
618
+ history[-1][1] = msg_atual.replace(f"⏳ **{nome_agente}** está analisando...\n", "") + f"\n❌ Erro em {nome_agente}: {str(e)}\n"
619
+ yield history, timeline_execucao, pipeline_state
620
+
621
+ passo_atual += 1
622
+
623
+ # ==================== 6. UI (Gradio) ====================
624
+
625
+ def ui_v29_stop_logic():
626
+ css = """
627
+ footer {display: none !important;}
628
+ .contain {border: none !important;}
629
+ """
630
+
631
+ config_inicial = carregar_protocolo()
632
+ documentacao = carregar_documentacao()
633
+
634
+ # Define o objeto do tema
635
+ app_theme = gr.themes.Soft()
636
+
637
+ # CORREÇÃO: Removido 'css' e 'theme' do construtor gr.Blocks().
638
+ with gr.Blocks(title="AI Forensics Auto") as app:
639
+
640
+ # Estado da configuração dos agentes
641
+ state_config = gr.State(config_inicial)
642
+ # NOVO: Estado para controlar a pausa/continuação da pipeline
643
+ pipeline_state = gr.State({"is_paused": False, "timeline": [], "remaining_agents": []})
644
+
645
+ with gr.Tabs():
646
+ with gr.Tab("💬 Investigação"):
647
+
648
+ chatbot = gr.Chatbot(
649
+ height=400,
650
+ show_label=False,
651
+ # CORREÇÃO: show_copy_button foi removido, render_markdown reativado com o formato de dados corrigido.
652
+ render_markdown=True,
653
+ label="Chat de Investigação"
654
+ )
655
+
656
+ with gr.Row():
657
+ txt_input = gr.Textbox(
658
+ scale=8,
659
+ show_label=False,
660
+ placeholder="Digite sua instrução ou responda à pergunta do agente...",
661
+ lines=1
662
+ )
663
+ btn_enviar = gr.Button("Enviar 📨", variant="primary", scale=1)
664
+
665
+ # Caixa de Opções Avançadas
666
+ with gr.Accordion("⚙️ Opções Avançadas de Governança", open=False):
667
+ chk_antecipatorio = gr.Checkbox(
668
+ label=f"Incluir Contexto Antecipatório (`{ARQUIVO_CONTEXTO_ANTECIPATORIO}`) no início da análise (Recomendado).",
669
+ value=True, # Ativado por padrão
670
+ show_label=True
671
+ )
672
+
673
+ with gr.Accordion("📂 Adicionar Arquivos para Análise", open=False):
674
+ gr.Markdown("Selecione arquivos (PDF, TXT). A transcrição iniciará **automaticamente**.")
675
+ file_uploader = gr.File(
676
+ file_count="multiple",
677
+ file_types=[".pdf", ".txt", ".json", ".md"],
678
+ label="Arraste arquivos aqui ou clique para selecionar"
679
+ )
680
+
681
+ with gr.Tab("🕵️ Auditoria"):
682
+ gr.Markdown("### Trilha de Auditoria\nExibe o histórico completo de prompts e respostas de cada agente na última execução.")
683
+ json_audit = gr.JSON(label="Timeline da Execução da Última Mensagem")
684
+
685
+ with gr.Tab("⚙️ Protocolo & Autoria"):
686
+ gr.Markdown(f"### Protocolo Causal (Diretrizes de Agentes)\nDesenvolvedor: **{DEVS_NAME}** ({DEVS_EMAIL}) | Licença: **{LICENSE_INFO}**")
687
+ gr.Markdown("Visualize as missões dos agentes de IA. O número de fases é totalmente configurável na estrutura JSON abaixo.")
688
+ code_config = gr.Code(value=config_inicial, language="json", label="protocolo.json (Visualização de Diretrizes)", interactive=False)
689
+
690
+ with gr.Tab("📚 Documentação"):
691
+ gr.Markdown("### Documentação da Aplicação (help.md)")
692
+ gr.Markdown(documentacao)
693
+
694
+
695
+ # Ação de clique agora passa o novo checkbox para o orquestrador
696
+ btn_enviar.click(
697
+ chat_orquestrador,
698
+ inputs=[txt_input, chatbot, state_config, pipeline_state, chk_antecipatorio],
699
+ outputs=[chatbot, json_audit, pipeline_state]
700
+ ).then(
701
+ lambda: "", outputs=[txt_input]
702
+ )
703
+
704
+ file_uploader.upload(
705
+ automacao_upload_processamento,
706
+ inputs=[file_uploader, chatbot, state_config],
707
+ outputs=[chatbot]
708
+ )
709
+
710
+ # CORREÇÃO: Retorna o app, o css e o tema para serem passados para launch()
711
+ return app, css, app_theme
712
+
713
+ if __name__ == "__main__":
714
+ # CORREÇÃO: Recebe o app, o css e o tema e os passa para app.launch()
715
+ app, css_styles, app_theme = ui_v29_stop_logic()
716
+ app.launch(css=css_styles, theme=app_theme)