caarleexx commited on
Commit
a4970f8
·
verified ·
1 Parent(s): 9fc40b6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +568 -696
app.py CHANGED
@@ -1,766 +1,638 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
  from groq import Groq
10
 
11
- # ... (Metadados e outras configurações permanecem iguais) ...
12
-
13
- # =================================================================================
14
- # METADADOS DO PROJETO - HACKATHON OAB 2025
15
- # --------------------------------------------------------------------------------
16
- # Desenvolvedor: Carlos Rodrigues dos Santos
17
- # Contato: carlex22@gmaill.com
18
- # GitHub: github.com/carlex22
19
- # Licença: GPLv3
20
- # --------------------------------------------------------------------------------
21
- # FUNÇÃO DE ORQUESTRAÇÃO, CADEIA COGNITIVA E GOVERNANÇA (Transparência Causal)
22
- # --------------------------------------------------------------------------------
23
- # O orquestrador gerencia a 'Cadeia Cognitiva' através de 'Agentes Causais',
24
- # onde a saída de uma Fase (JSON de contexto) alimenta a entrada da próxima.
25
- # Isso garante 'Transparência' e 'Governanca' por meio da:
26
- #
27
- # 1. Pipeline Estruturada: Execução sequencial das missões definidas em
28
- # 'protocolo.json' (Fase 0 a 7), garantindo que cada agente cumpra um papel.
29
- # 2. Saída Auditorável (JSON): As fases retornam um JSON padronizado para
30
- # o painel 'Auditoria', permitindo rastrear o raciocínio de cada agente.
31
- # 3. Ponto de Controle (STOP): A Fase 0 (e a lógica da pipeline) pode solicitar
32
- # input do usuário (gatilho STOP), pausando a cadeia para validar fatos
33
- # críticos ou inserir contexto faltante, garantindo governança humana
34
- # em momentos de alta incerteza.
35
- # 4. Formato Envolvente (Fase 7): A fase final (Relatório) recebe tratamento
36
- # especial para apresentar a valoração de forma dinâmica e clara, conforme
37
- # o propósito de valorar a dignidade humana.
38
- # 5. Contexto Antecipatório: Opcionalmente inclui um passo inicial com dados
39
- # de 'data.txt' para estabelecer regras e 'verdades factuais' para os agentes.
40
- # =================================================================================
41
-
42
-
43
- # Dependências para PDF
44
- try:
45
- import PyPDF2
46
- PDF_SUPPORT = True
47
- except ImportError:
48
- PDF_SUPPORT = False
49
- print("⚠️ PyPDF2 não instalado. PDFs serão lidos como texto simples.")
50
-
51
  # ==================== 1. CONFIGURAÇÃO ====================
 
 
52
 
53
- try:
54
- groq_client = Groq(api_key=os.environ.get("GROQ_API_KEY"))
55
- print("✅ Cliente Groq inicializado com sucesso.")
56
- except Exception as e:
57
- groq_client = None
58
- print(f"❌ Erro ao inicializar o cliente Groq. Verifique sua API Key: {e}")
59
-
60
- GROQ_MODELS = {
61
- "flash": "llama3-8b-8192",
62
- "pro": "llama3-70b-8192"
63
- }
64
-
65
  ARQUIVO_CONFIG = "protocolo.json"
66
- ARQUIVO_CONTEXTO_ANTECIPATORIO = "data.txt"
67
- ARQUIVO_DOCUMENTACAO = "help.md"
68
- PASTA_TRANSCRICOES = "transcricoes"
69
- PAGES_PER_CHUNK = 10
70
- MAX_WORKERS = 5
71
 
72
- os.makedirs(PASTA_TRANSCRICOES, exist_ok=True)
 
73
 
74
- # ... (Seção 2. UTILIDADES e 3. PIPELINE DE IA permanecem as mesmas) ...
75
  # ==================== 2. UTILIDADES ====================
76
-
77
- # Define as constantes de autoria
78
- DEVS_NAME = "Carlos Rodrigues dos Santos"
79
- DEVS_EMAIL = "carlex22@gmaill.com"
80
- LICENSE_INFO = "GPLv3"
81
-
82
- def ler_arquivo_texto(arquivo_path):
83
- # Função auxiliar que lê um arquivo de texto pelo caminho
84
- if not os.path.exists(arquivo_path):
85
- return None
86
- try:
87
- with open(arquivo_path, "r", encoding="utf-8") as f:
88
- conteudo = f.read()
89
- return conteudo
90
- except: return None
91
 
92
  def carregar_protocolo():
93
- """ Carrega o protocolo. Se não existir, cria um com um esqueleto limpo. """
94
- return ler_arquivo_texto(ARQUIVO_CONFIG) or json.dumps(
95
- [{"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."}],
96
- indent=2,
97
- ensure_ascii=False
98
- )
99
-
100
- def carregar_documentacao():
101
- """ Carrega o arquivo de documentação. """
102
- 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."
103
-
104
-
105
- def limpar_nome_arquivo(nome):
106
- # (Implementação existente)
107
- nome_base = os.path.basename(nome)
108
- nome_limpo = "".join([c for c in nome_base if c.isalnum() or c in (' ', '.', '_', '-')]).strip()
109
- return nome_limpo + ".json"
110
-
111
- def extrair_texto_pdf(caminho_pdf):
112
- # (Implementação existente)
113
  try:
114
- with open(caminho_pdf, 'rb') as f:
115
- reader = PyPDF2.PdfReader(f)
116
- paginas = []
117
- for i, page in enumerate(reader.pages):
118
- texto = page.extract_text()
119
- paginas.append({
120
- "numero": i + 1,
121
- "texto": texto,
122
- "metadata": str(page)[:200]
123
- })
124
- return paginas, None
125
  except Exception as e:
126
- return None, str(e)
127
-
128
-
129
- def fragmentar_pdf(paginas, tamanho_chunk=PAGES_PER_CHUNK):
130
- # (Implementação existente)
131
- chunks = []
132
- for i in range(0, len(paginas), tamanho_chunk):
133
- chunk = paginas[i:i + tamanho_chunk]
134
- num_inicio = chunk[0]["numero"]
135
- num_fim = chunk[-1]["numero"]
136
-
137
- texto_consolidado = "\n---QUEBRA DE PÁGINA---\n".join(
138
- [f"[PÁGINA {p['numero']}]\n{p['texto']}" for p in chunk]
139
- )
140
-
141
- chunks.append({
142
- "id": f"chunk_{num_inicio}_{num_fim}",
143
- "paginas": f"{num_inicio}-{num_fim}",
144
- "num_paginas": len(chunk),
145
- "texto": texto_consolidado,
146
- "metadata": [p["metadata"] for p in chunk]
147
- })
148
- return chunks
149
 
150
- def processar_pdf_completo(arquivo_pdf):
151
- # (Implementação existente)
152
- if not PDF_SUPPORT:
153
- return None, "❌ PyPDF2 não disponível"
154
-
155
  try:
156
- paginas, erro = extrair_texto_pdf(arquivo_pdf.name if hasattr(arquivo_pdf, 'name') else arquivo_pdf)
157
- if erro:
158
- return None, f" Erro ao ler PDF: {erro}"
159
-
160
- chunks = fragmentar_pdf(paginas)
161
- nome_arquivo = os.path.basename(arquivo_pdf.name if hasattr(arquivo_pdf, 'name') else arquivo_pdf)
162
-
163
- return {
164
- "arquivo": nome_arquivo,
165
- "total_paginas": len(paginas),
166
- "total_chunks": len(chunks),
167
- "chunks": chunks,
168
- "tipo": "pdf"
169
- }, None
170
  except Exception as e:
171
- return None, f"❌ Erro no processamento: {str(e)}"
 
172
 
173
- # ==================== 3. PIPELINE DE IA ====================
174
-
175
- def transcrever_chunk(chunk_data, config_agentes):
176
- # ### ALTERADO ### - Função adaptada para usar a API da Groq
177
- if not groq_client:
178
- return None, "Cliente Groq não inicializado."
 
179
 
180
- modelo_groq = GROQ_MODELS["flash"] # Usa sempre o modelo mais rápido para transcrição
181
-
182
- prompt = f"""
183
- ANÁLISE DE DOCUMENTO (OCR/LEITURA):
184
- Transcreva e estruture o conteúdo das páginas {chunk_data['paginas']}.
185
- Texto extraído:
186
- {chunk_data['texto']}
 
187
 
188
- Retorne APENAS o JSON com a seguinte estrutura: {{ "transcricao": "...", "objetos": ["..."], "resumo": "..." }}
189
- """
190
  try:
191
- for tentativa in range(3):
192
- try:
193
- chat_completion = groq_client.chat.completions.create(
194
- messages=[{"role": "user", "content": prompt}],
195
- model=modelo_groq,
196
- temperature=0.1,
197
- max_tokens=4096,
198
- )
199
- texto_resp = chat_completion.choices[0].message.content
200
- texto_limpo = texto_resp.replace("```json", "").replace("```", "").strip()
201
- return json.loads(texto_limpo), None
202
- except Exception as inner_e:
203
- if "429" in str(inner_e): # Lida com Rate Limiting
204
- time.sleep(2 * (tentativa + 1))
205
- continue
206
- raise inner_e
207
  except Exception as e:
208
- return None, str(e)
209
-
210
- # ... (Seção 4. GERENCIADOR DE ARQUIVOS permanece a mesma) ...
211
-
212
- # ==================== 4. GERENCIADOR DE ARQUIVOS ====================
213
-
214
- class GerenciadorArquivos:
215
- def __init__(self):
216
- self.arquivos = {}
217
-
218
- def adicionar(self, arquivo, arquivo_id):
219
- # (Implementação existente)
220
- self.arquivos[arquivo_id] = {
221
- "arquivo": arquivo,
222
- "nome": os.path.basename(arquivo.name),
223
- "status": "adicionado",
224
- "processado": None,
225
- "transcricao": None
226
- }
227
 
228
- def gerar_prompt_com_transcricoes(self, texto_usuario):
229
- # (Implementação existente)
230
- prompt = texto_usuario + "\n\n--- CONTEXTO DOS ARQUIVOS ---\n"
231
- count = 0
232
- for _, item in self.arquivos.items():
233
- if item["status"] == "processado" and item["transcricao"]:
234
- count += 1
235
- trans = item["transcricao"]
236
- nome = item["nome"]
237
- prompt += f"\n[ARQUIVO: {nome}]\n"
238
-
239
- if isinstance(trans, dict) and "chunks_processados" in trans:
240
- for chunk in trans["chunks_processados"]:
241
- if chunk.get("status") == "OK":
242
- resumo = chunk.get('resumo', '')
243
- resumo = str(resumo) if resumo else ""
244
- prompt += f"Páginas {chunk['paginas']}: {resumo}\n"
245
-
246
- texto_full = chunk.get('transcricao', '')
247
- if texto_full:
248
- texto_seguro = str(texto_full)
249
- prompt += f"Trecho: {texto_seguro[:400]}...\n"
250
- else:
251
- prompt += "Trecho: (vazio)\n"
252
 
253
- elif isinstance(trans, dict) and "conteudo" in trans:
254
- conteudo = str(trans['conteudo'])
255
- prompt += f"Conteúdo: {conteudo[:1000]}...\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
256
 
257
- if count == 0:
258
- prompt += "(Nenhum arquivo processado ainda)"
259
- return prompt
260
-
261
- # Instância Global
262
- gerenciador = GerenciadorArquivos()
263
-
264
- # ==================== 5. FUNÇÕES DE ORQUESTRAÇÃO ====================
265
- # ... (automacao_upload_processamento permanece a mesma) ...
266
-
267
- def automacao_upload_processamento(files, history, config_json):
268
- # (Função de processamento de arquivos)
269
- if not files:
270
- return history
271
-
272
- if not groq_client:
273
- history.append([None, "⚠️ **SISTEMA:** Cliente Groq não configurado. Verifique a API Key e reinicie a aplicação."])
274
- yield history
275
- return
276
-
277
  try:
278
- config_agentes = json.loads(config_json)
279
- except:
280
- config_agentes = []
281
-
282
- if history is None:
283
- history = []
284
-
285
- history.append([None, f"📂 **SISTEMA:** Recebi {len(files)} arquivo(s). Verificando cache e processando..."])
286
- yield history
287
-
288
- ids_para_processar = []
289
-
290
- for f in files:
291
- # Usa um hash do conteúdo para ID de arquivo de texto simples para melhorar o cache
292
- if f.name.lower().endswith(('.txt', '.json', '.md')):
293
- with open(f.name, 'r', encoding='utf-8') as file:
294
- file_hash = hashlib.sha256(file.read().encode('utf-8')).hexdigest()
295
- arquivo_id = f"txt_{file_hash}"
296
- else:
297
- arquivo_id = f"arq_{int(time.time()*1000)}_{f.name}"
298
-
299
- gerenciador.adicionar(f, arquivo_id)
300
- ids_para_processar.append(arquivo_id)
 
 
 
 
301
 
302
- for arq_id in ids_para_processar:
303
- item = gerenciador.arquivos[arq_id]
304
- nome = item["nome"]
305
 
306
- # Cria um nome de cache consistente
307
- nome_cache = limpar_nome_arquivo(nome)
308
- caminho_cache = os.path.join(PASTA_TRANSCRICOES, nome_cache)
309
 
310
- # Lógica de Cache e Reprocessamento
311
- if os.path.exists(caminho_cache):
312
- try:
313
- with open(caminho_cache, "r", encoding="utf-8") as cache_file:
314
- dados_cache = json.load(cache_file)
315
- item["transcricao"] = dados_cache
316
- item["status"] = "processado"
317
- if nome.lower().endswith('.pdf') and "chunks_processados" in dados_cache:
318
- item["processado"] = {"tipo": "pdf", "chunks": []}
319
- history.append([None, f"♻️ **Cache Encontrado:** `{nome}` já foi processado. Carregando..."])
320
- yield history
321
- continue
322
- except Exception as e:
323
- history.append([None, f"⚠️ Erro cache `{nome}`: {e}. Reprocessando..."])
324
-
325
- history.append([None, f"⚙️ **Processando:** `{nome}`..."])
326
- yield history
327
-
328
- if nome.lower().endswith('.pdf'):
329
- if not PDF_SUPPORT:
330
- history.append([None, f" Erro em `{nome}`: Biblioteca PDF ausente."])
331
- yield history
332
- continue
333
-
334
- pdf_proc, erro = processar_pdf_completo(item["arquivo"])
335
- if erro:
336
- history.append([None, f" Erro em `{nome}`: {erro}"])
337
- yield history
338
- continue
339
-
340
- item["processado"] = pdf_proc
341
- chunks = pdf_proc["chunks"]
342
- total_chunks = len(chunks)
343
-
344
- chunks_ordenados = [None] * total_chunks
345
-
346
- history.append([None, f"📄 `{nome}` fragmentado em {total_chunks} partes. Iniciando IA (Paralelo: {MAX_WORKERS} threads)..."])
347
- yield history
348
-
349
- with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
350
- futures_map = {}
351
- for i, chunk in enumerate(chunks):
352
- future = executor.submit(transcrever_chunk, chunk, config_agentes)
353
- futures_map[future] = i
354
-
355
- concluidos = 0
356
- for future in as_completed(futures_map):
357
- index_original = futures_map[future]
358
- res, err = future.result()
359
-
360
- if err:
361
- chunks_ordenados[index_original] = {"status": "ERRO", "paginas": chunks[index_original]["paginas"]}
362
- else:
363
- chunks_ordenados[index_original] = {
364
- "status": "OK",
365
- "paginas": chunks[index_original]["paginas"],
366
- "transcricao": res.get("transcricao"),
367
- "resumo": res.get("resumo")
368
- }
369
-
370
- concluidos += 1
371
- if concluidos % 2 == 0 or concluidos == total_chunks:
372
- msg_base = f"📄 `{nome}`: Processando partes... ({concluidos}/{total_chunks})"
373
- history[-1][1] = msg_base
374
- yield history
375
-
376
- dados_finais = {
377
- "arquivo": nome,
378
- "data_processamento": str(datetime.now()),
379
- "chunks_processados": chunks_ordenados
380
- }
381
-
382
- item["transcricao"] = dados_finais
383
- item["status"] = "processado"
384
-
385
  try:
386
- with open(caminho_cache, "w", encoding="utf-8") as f_out:
387
- json.dump(dados_finais, f_out, indent=2, ensure_ascii=False)
388
- history.append([None, f"💾 `{nome}` processado e salvo no cache."])
389
- except Exception as e:
390
- history.append([None, f"⚠️ Erro ao salvar cache: {e}"])
391
-
392
- yield history
393
-
394
- else:
395
- res_content = ler_arquivo_texto(item["arquivo"].name)
396
- if res_content:
397
- item["processado"] = res_content
398
- dados_finais = {"conteudo": res_content, "data_processamento": str(datetime.now())}
399
- item["transcricao"] = dados_finais
400
- item["status"] = "processado"
401
 
402
- with open(caminho_cache, "w", encoding="utf-8") as f_out:
403
- json.dump(dados_finais, f_out, indent=2, ensure_ascii=False)
404
-
405
- history.append([None, f" `{nome}` (Texto) lido e salvo."])
406
- else:
407
- history.append([None, f"❌ Falha ao ler `{nome}`."])
408
- yield history
409
-
410
- history.append([None, "🏁 **Processamento de lote finalizado.** Os arquivos estão prontos para análise."])
411
- yield history
412
-
413
- # ### ALTERADO ### - Função principal do chat com a nova lógica de inicialização
414
- def chat_orquestrador(message, history, system_prompt, config_json, pipeline_state, incluir_passo_antecipatorio):
415
- if not groq_client:
416
- history.append([message, "⚠️ **SISTEMA:** Cliente Groq não configurado. Verifique a API Key."])
417
- yield history, [], pipeline_state
418
- return
419
-
420
- # ### NOVO ### - LÓGICA DE INICIALIZAÇÃO (APENAS NA PRIMEIRA MENSAGEM)
421
- if not history:
422
- try:
423
- # Pega o contexto dos arquivos já processados (sem mensagem de usuário ainda)
424
- contexto_arquivos = gerenciador.gerar_prompt_com_transcricoes("")
425
-
426
- # Monta o prompt de inicialização especial
427
- prompt_inicial = f"""
428
- Você é um assistente de IA especializado. Seu objetivo principal foi definido como: "{system_prompt}".
429
-
430
- Você acabou de ser inicializado e recebeu alguns documentos para análise prévia. O resumo do conteúdo desses documentos é:
431
- {contexto_arquivos}
432
-
433
- Sua primeira tarefa é enviar uma mensagem de boas-vindas ao usuário. Nesta mensagem, você DEVE:
434
- 1. Confirmar que entendeu seu objetivo, resumindo-o com suas próprias palavras.
435
- 2. Informar de forma muito breve o que você compreendeu a partir dos arquivos iniciais.
436
- 3. Se apresentar de forma profissional e se colocar à disposição para iniciar a análise detalhada.
437
-
438
- Responda diretamente ao usuário em um tom prestativo e competente.
439
- """
440
-
441
- # Cria a primeira entrada na timeline de auditoria
442
- timeline_execucao = [{
443
- "passo": 1,
444
- "tipo": "inicializacao_sistema",
445
- "objetivo_global": system_prompt,
446
- "prompt_enviado": prompt_inicial
447
- }]
448
-
449
- # Faz a chamada de streaming para a Groq
450
- stream = groq_client.chat.completions.create(
451
- messages=[{"role": "user", "content": prompt_inicial}],
452
- model=GROQ_MODELS["pro"], # Usa o modelo mais forte para a primeira impressão
453
- temperature=0.5,
454
- max_tokens=2048,
455
- stream=True,
456
- )
457
-
458
- # Adiciona um placeholder na "history" para a resposta da IA
459
- history.append([None, ""])
460
- resposta_completa = ""
461
-
462
- for chunk in stream:
463
- delta = chunk.choices[0].delta.content or ""
464
- if delta:
465
- resposta_completa += delta
466
- history[-1][1] = resposta_completa # Atualiza a resposta no chat
467
- yield history, timeline_execucao, pipeline_state
468
-
469
- # Adiciona a resposta final à auditoria
470
- timeline_execucao[0]["resposta_ia"] = resposta_completa
471
- yield history, timeline_execucao, pipeline_state
472
- return # Encerra a execução aqui, pois foi apenas a inicialização
473
-
474
- except Exception as e:
475
- history.append([None, f"❌ Erro durante a inicialização: {str(e)}"])
476
- yield history, [], pipeline_state
477
- return
478
-
479
- # --- FLUXO NORMAL PARA MENSAGENS SUBSEQUENTES ---
480
-
481
- if pipeline_state.get("is_paused"):
482
- # (Lógica de continuação após STOP - sem alterações)
483
- history.append([message, None])
484
 
485
- timeline_execucao = pipeline_state["timeline"]
486
- agentes_restantes = pipeline_state["remaining_agents"]
487
 
488
- timeline_execucao.append({
489
- "passo": len(timeline_execucao) + 1,
490
- "tipo": "resposta_usuario",
491
- "conteudo": message
492
- })
 
 
 
 
 
493
 
494
- pipeline_state["is_paused"] = False
 
 
 
 
 
495
 
496
- yield from executar_pipeline(history, timeline_execucao, agentes_restantes, pipeline_state)
497
- return
 
 
 
 
 
 
 
 
 
 
 
 
498
 
499
- # --- LÓGICA DE INÍCIO DE UMA NOVA ANÁLISE (após a inicialização) ---
 
 
 
 
 
 
 
 
500
 
501
- try:
502
- prompt_contexto = gerenciador.gerar_prompt_com_transcricoes(message)
503
- except Exception as e:
504
- history.append([message, f"❌ Erro ao gerar contexto: {str(e)}"])
505
- yield history, [], pipeline_state
506
  return
507
 
 
 
 
508
  try:
509
- protocolo = json.loads(config_json)
510
- except:
511
- history.append([message, "❌ Erro no JSON de Configuração do Protocolo."])
512
- yield history, [], pipeline_state
 
 
513
  return
514
-
515
- history.append([message, None])
516
 
517
- timeline_execucao = []
518
- agentes_a_executar = protocolo
519
 
520
- if incluir_passo_antecipatorio:
521
- # (Lógica do Passo Antecipatório - sem alterações)
522
- conteudo_antecipatorio = ler_arquivo_texto(ARQUIVO_CONTEXTO_ANTECIPATORIO)
523
- if conteudo_antecipatorio:
524
- timeline_execucao.append({
525
- "passo": 1,
526
- "tipo": "passo_antecipatorio_data",
527
- "agente": "GOVERNANCA_INICIAL",
528
- "conteudo": f"REGRAS E VERDADE FACTUAL INICIAL (data.txt):\n{conteudo_antecipatorio}"
529
- })
530
- history.append([None, f"ℹ️ **GOVERNANÇA INICIAL:** Contexto de `{ARQUIVO_CONTEXTO_ANTECIPATORIO}` injetado."])
531
- history[-1][1] += f"\n\nInstrução do usuário: {message}"
532
- else:
533
- history[-1][1] = f"⚠️ Instrução do usuário: {message}"
534
- history[-1][1] += f"\n\n**Aviso:** Passo Antecipatório ativado, mas arquivo `{ARQUIVO_CONTEXTO_ANTECIPATORIO}` não encontrado."
535
-
536
- passo_inicial_prompt = len(timeline_execucao) + 1
537
- else:
538
- passo_inicial_prompt = 1
539
 
540
- timeline_execucao.append({"passo": passo_inicial_prompt, "tipo": "prompt_usuario_base", "conteudo": prompt_contexto})
 
 
 
 
 
541
 
542
- yield history, timeline_execucao, pipeline_state
 
 
 
543
 
544
- yield from executar_pipeline(history, timeline_execucao, agentes_a_executar, pipeline_state)
545
-
546
- # ... (função executar_pipeline permanece a mesma da versão Groq anterior) ...
547
- def executar_pipeline(history, timeline_execucao, agentes_a_executar, pipeline_state):
548
- # ### ALTERADO ### - Lógica de execução da pipeline adaptada para Groq e streaming real
549
- passo_atual = len(timeline_execucao) + 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
550
 
551
- for i, cfg in enumerate(agentes_a_executar):
552
- nome_agente = cfg.get("nome", "Agente")
553
- # Seleciona o modelo Groq com base na configuração do agente
554
- modelo_agente = GROQ_MODELS.get(cfg.get("modelo"), GROQ_MODELS["flash"])
555
- tipo_saida = cfg.get("tipo_saida", "json")
 
 
 
 
 
 
 
556
 
557
- msg_atual = history[-1][1] or ""
558
- history[-1][1] = msg_atual + f"⏳ **{nome_agente}** está analisando (modelo: `{modelo_agente}`)...\n"
559
- yield history, timeline_execucao, pipeline_state
 
 
 
 
 
560
 
561
- prompt_agente = f"""
562
- --- HISTÓRICO DA CONVERSA ATÉ AGORA ---
563
- {json.dumps(timeline_execucao, ensure_ascii=False, indent=2)}
564
- -----------------
565
- Sua Identidade: {nome_agente}
566
- Sua Missão Específica Agora: {cfg['missao']}
567
- Se o tipo de saída exigido for 'json', sua resposta DEVE ser APENAS o JSON. Se for 'texto', responda de forma discursiva.
568
- """
569
 
570
- try:
571
- inicio = time.time()
 
 
 
 
 
 
 
 
572
 
573
- # --- LÓGICA DE DETECÇÃO DE SAÍDA NÃO-JSON (STOP ou Relatório) ---
574
- if tipo_saida == "json":
575
- chat_completion = groq_client.chat.completions.create(
576
- messages=[{"role": "user", "content": prompt_agente}],
577
- model=modelo_agente,
578
- temperature=0.1,
579
- max_tokens=8192,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
580
  )
581
- texto_resp = chat_completion.choices[0].message.content
582
- duracao = time.time() - inicio
583
-
584
- try:
585
- # Tenta interpretar a resposta como JSON
586
- texto_json_limpo = texto_resp.replace("```json", "").replace("```", "").strip()
587
- resposta_json = json.loads(texto_json_limpo)
588
-
589
- # 1. LÓGICA DE PAUSA (STOP)
590
- if resposta_json.get("PROXIMA_ACAO") == "PERGUNTAR_USUARIO" and resposta_json.get("DUVIDA_DETECTADA") == True:
591
-
592
- if "STOP:" in texto_resp:
593
- stop_message = texto_resp.split("STOP:")[1].strip() if "STOP:" in texto_resp else "Análise interrompida por dúvida crítica."
594
- history[-1][1] = stop_message
595
- yield history, timeline_execucao, pipeline_state
596
- return
597
-
598
- perguntas = resposta_json.get("TESTE_REFLEXAO", {}).get("perguntas", [])
599
-
600
- pipeline_state["is_paused"] = True
601
- pipeline_state["timeline"] = timeline_execucao
602
- pipeline_state["remaining_agents"] = agentes_a_executar[i+1:]
603
-
604
- stop_response = "STOP: preciso que você esclareça pontos antes de continuar."
605
- timeline_execucao.append({"passo": passo_atual, "tipo": "STOP_USUARIO_REQUERIDO", "agente": nome_agente, "detalhes": stop_response})
606
-
607
- msg_para_usuario = f"**{nome_agente}** precisa de mais informações. Por favor, responda aos pontos abaixo:\n\n"
608
- for idx, p in enumerate(perguntas):
609
- if idx < 3:
610
- msg_para_usuario += f"**{idx + 1})** {p}\n"
611
-
612
- msg_atual = history[-1][1].replace(f"⏳ **{nome_agente}** está analisando (modelo: `{modelo_agente}`)...\n", "")
613
- history[-1][1] = msg_atual + msg_para_usuario
614
-
615
- yield history, timeline_execucao, pipeline_state
616
- return
617
-
618
- # 2. Resposta JSON normal
619
- texto_para_auditoria = texto_json_limpo
620
- timeline_execucao.append({"passo": passo_atual, "tipo": "resposta_agente", "agente": nome_agente, "resposta": texto_para_auditoria})
621
-
622
- msg_atual = history[-1][1].replace(f"⏳ **{nome_agente}** está analisando (modelo: `{modelo_agente}`)...\n", "")
623
- novo_trecho = f"✅ **[{nome_agente}]** concluiu sua análise em ({duracao:.1f}s). (JSON para Auditoria)\n"
624
- history[-1][1] = msg_atual + novo_trecho
625
- yield history, timeline_execucao, pipeline_state
626
-
627
- except json.JSONDecodeError:
628
- 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."})
629
- msg_atual = history[-1][1].replace(f"⏳ **{nome_agente}** está analisando (modelo: `{modelo_agente}`)...\n", "")
630
- history[-1][1] = msg_atual + f"❌ **[{nome_agente}]** falhou: Resposta não era JSON válido. ({duracao:.1f}s).\n"
631
- yield history, timeline_execucao, pipeline_state
632
 
633
- elif tipo_saida == "texto":
634
- # Lógica para a Fase 7 com STREAMING REAL
635
- stream = groq_client.chat.completions.create(
636
- messages=[{"role": "user", "content": prompt_agente}],
637
- model=modelo_agente,
638
- temperature=0.7,
639
- max_tokens=8192,
640
- stream=True,
 
 
 
 
 
 
 
 
 
 
 
 
 
641
  )
642
 
643
- msg_final = f"**[RELATÓRIO FINAL DO {nome_agente}]**\n\n"
644
- history[-1][1] = msg_final
645
- relatorio_completo = ""
646
 
647
- for chunk in stream:
648
- delta = chunk.choices[0].delta.content or ""
649
- if delta:
650
- relatorio_completo += delta
651
- history[-1][1] = msg_final + relatorio_completo
652
- yield history, timeline_execucao, pipeline_state
653
 
654
- # Adiciona o passo completo à auditoria no final
655
- timeline_execucao.append({"passo": passo_atual, "tipo": "relatorio_final", "agente": nome_agente, "relatorio": relatorio_completo})
656
- history[-1][1] += "\n\n--- FIM DO RELATÓRIO DE VALORAÇÃO ---"
657
- yield history, timeline_execucao, pipeline_state
658
 
659
- else:
660
- timeline_execucao.append({"passo": passo_atual, "tipo": "erro_config", "agente": nome_agente, "erro": "Tipo de saída não reconhecido ('json' ou 'texto')."})
661
- msg_atual = history[-1][1].replace(f"⏳ **{nome_agente}** está analisando...\n", "")
662
- history[-1][1] = msg_atual + f"\n❌ Erro de Configuração em {nome_agente}: Tipo de saída inválido.\n"
663
- yield history, timeline_execucao, pipeline_state
664
-
665
- except Exception as e:
666
- timeline_execucao.append({"passo": passo_atual, "tipo": "erro_agente", "agente": nome_agente, "erro": str(e)})
667
- msg_atual = history[-1][1]
668
- history[-1][1] = msg_atual.replace(f"⏳ **{nome_agente}** está analisando (modelo: `{modelo_agente}`)...\n", "") + f"\n❌ Erro em {nome_agente}: {str(e)}\n"
669
- yield history, timeline_execucao, pipeline_state
670
-
671
- passo_atual += 1
672
-
673
- # ==================== 6. UI (Gradio) ====================
674
-
675
- def ui_v29_stop_logic():
676
- css = """
677
- footer {display: none !important;}
678
- .contain {border: none !important;}
679
- """
680
-
681
- config_inicial = carregar_protocolo()
682
- documentacao = carregar_documentacao()
683
-
684
- with gr.Blocks(title="AI Forensics Auto", css=css, theme=gr.themes.Soft()) as app:
685
-
686
- state_config = gr.State(config_inicial)
687
- pipeline_state = gr.State({"is_paused": False, "timeline": [], "remaining_agents": []})
688
-
689
- with gr.Tabs():
690
- with gr.Tab("💬 Investigação"):
691
 
692
- # ### NOVO ### - Campo para o Objetivo Global (System Prompt)
693
- with gr.Column():
694
- gr.Markdown("### 1. Defina o Objetivo Principal da Análise")
695
- txt_system_prompt = gr.Textbox(
696
- label="Objetivo do Modelo (System Prompt Global)",
697
- placeholder="Ex: 'Atuar como um perito judicial para analisar o processo e determinar o valor do dano moral com base na jurisprudência do TJSP.'",
698
- lines=2
699
- )
700
-
701
- chatbot = gr.Chatbot(
702
- elem_id="chatbot",
703
- height=400,
704
- show_label=False,
705
- show_copy_button=True,
706
- render_markdown=True,
707
- label="Chat de Investigação"
708
  )
 
 
 
 
 
 
 
 
709
 
710
  with gr.Row():
711
- txt_input = gr.Textbox(
712
- scale=8,
713
- show_label=False,
714
- placeholder="Digite sua primeira instrução para iniciar, ou continue a conversa...",
715
- lines=1
716
- )
717
- btn_enviar = gr.Button("Enviar 📨", variant="primary", scale=1)
718
 
719
- with gr.Accordion("⚙️ Opções Avançadas de Governança", open=False):
720
- chk_antecipatorio = gr.Checkbox(
721
- label=f"Incluir Contexto Antecipatório (`{ARQUIVO_CONTEXTO_ANTECIPATORIO}`)",
722
- value=True
723
- )
724
-
725
- with gr.Accordion("📂 Adicionar Arquivos para Análise", open=True):
726
- gr.Markdown("Selecione arquivos (PDF, TXT). A transcrição iniciará **automaticamente**.")
727
- file_uploader = gr.File(
728
- file_count="multiple",
729
- file_types=[".pdf", ".txt", ".json", ".md"],
730
- label="Arraste arquivos aqui ou clique para selecionar"
731
- )
732
-
733
- # ... (Abas de Auditoria, Protocolo e Documentação permanecem as mesmas) ...
734
- with gr.Tab("🕵️ Auditoria"):
735
- gr.Markdown("### Trilha de Auditoria\nExibe o histórico completo de prompts e respostas de cada agente na última execução.")
736
- json_audit = gr.JSON(label="Timeline da Execução da Última Mensagem")
737
-
738
- with gr.Tab("⚙️ Protocolo & Autoria"):
739
- gr.Markdown(f"### Protocolo Causal (Diretrizes de Agentes)\nDesenvolvedor: **{DEVS_NAME}** ({DEVS_EMAIL}) | Licença: **{LICENSE_INFO}**")
740
- gr.Markdown("Visualize as missões dos agentes de IA. O número de fases é totalmente configurável na estrutura JSON abaixo.")
741
- code_config = gr.Code(value=config_inicial, language="json", label="protocolo.json (Visualização de Diretrizes)", interactive=False)
742
 
743
- with gr.Tab("📚 Documentação"):
744
- gr.Markdown("### Documentação da Aplicação (help.md)")
745
- gr.Markdown(documentacao)
746
-
747
- # ### ALTERADO ### - Adiciona o txt_system_prompt como input
748
- btn_enviar.click(
749
- chat_orquestrador,
750
- inputs=[txt_input, chatbot, txt_system_prompt, state_config, pipeline_state, chk_antecipatorio],
751
- outputs=[chatbot, json_audit, pipeline_state]
752
- ).then(
753
- lambda: "", outputs=[txt_input]
754
- )
755
-
756
- file_uploader.upload(
757
- automacao_upload_processamento,
758
- inputs=[file_uploader, chatbot, state_config],
759
- outputs=[chatbot]
760
- )
761
-
 
 
 
 
 
 
 
 
762
  return app
763
 
764
  if __name__ == "__main__":
765
- app = ui_v29_stop_logic()
766
- app.launch()
 
 
 
1
+ # ╔════════════════════════════════════════════════════════════════════════════╗
2
+ # ║ PIPELINE v38: Documentação e Refatoração ║
3
+ # ╚════════════════════════════════════════════════════════════════════════════╝
4
+
5
+ # ==================== RESUMO TÉCNICO DA PIPELINE ====================
6
+ #
7
+ # OBJETIVO PRINCIPAL: Orquestrar chamadas sequenciais a modelos Groq,
8
+ # utilizando um protocolo de agentes (pipeline) e mantendo um contexto
9
+ # conversacional persistente (memória). O foco é no controle de fluxo.
10
+ #
11
+ # INOVAÇÕES CONSOLIDADAS (ADUC-SDR):
12
+ # 1. ARQUITETURA DE UNIÃO COMPOSITIVA (ADUC): Utiliza um contexto persistente
13
+ # de chat (`contexto_persistente.json`) e um protocolo sequencial de
14
+ # agentes (`protocolo.json`) para criar um fluxo de trabalho modular.
15
+ # 2. ESCALA DINÂMICA RESILIENTE (SDR): O contexto é limitado dinamicamente
16
+ # antes de cada chamada (`limitar_timeline`) por caracteres e número de
17
+ # mensagens para evitar erro de `context_length_exceeded`.
18
+ # 3. FIX "last role user": Garante que o último role da API seja sempre 'user',
19
+ # conforme exigido pela Groq API.
20
+ # 4. Prefixos de Rastreamento: Uso de prefixos `[USUARIO]` e `[AGENTE nome]`
21
+ # no contexto persistente para rastreabilidade e clareza do histórico.
22
+ # 5. Lógica de STOP Refinada: O agente inicial controla a execução com a
23
+ # palavra-chave `STOP_PIPELINE:`, cuja resposta é extraída e exibida
24
+ # limpa ao usuário.
25
+ #
26
+ # LIMITAÇÕES:
27
+ # - Não processa arquivos de imagem/áudio (apenas texto de anexos lidos).
28
+ # - O limite de tokens é uma estimativa por caracteres.
29
+ #
30
+ # ==================== AVISO LEGAL E LICENCIAMENTO ====================
31
+ #
32
+ # Desenvolvedor: Carlos Rodrigues dos Santos
33
+ # GitHub: github.com/carlex22/Izaak
34
+ # Email: Carlex22@gmail.com
35
+ #
36
+ # POLÍTICA DE LICENCIAMENTO: Este código-fonte é distribuído sob a Licença MIT.
37
+ #
38
+ # AVISO DE PATENTE PENDENTE: As inovações de Arquitetura de União Compositiva
39
+ # e Escala Dinâmica Resiliente (ADUC-SDR), essenciais para a orquestração
40
+ # causal, estão com processo de patente pendente no Brasil e internacionalmente.
41
+ # O uso comercial, replicação, ou incorporação dessas inovações sem autorização
42
+ # expressa está sujeito a legislação de propriedade intelectual.
43
+ #
44
+ # =====================================================================
45
+
46
  import os
47
  import json
48
+ import re
49
  import time
 
50
  from datetime import datetime
 
 
51
  import gradio as gr
52
  from groq import Groq
53
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  # ==================== 1. CONFIGURAÇÃO ====================
55
+ groq_key = os.getenv("GROQ_API_KEY", "SUA_GROQ_KEY_AQUI")
56
+ groq_client = Groq(api_key=groq_key)
57
 
58
+ # Arquivos de persistência e configuração
 
 
 
 
 
 
 
 
 
 
 
59
  ARQUIVO_CONFIG = "protocolo.json"
60
+ ARQUIVO_HELP = "help.md"
61
+ ARQUIVO_CONTEXTO = "contexto_persistente.json"
62
+ DELAY_ENTRE_AGENTES = 1 # Delay para feedback visual
63
+ STOP_KEYWORD = "STOP_PIPELINE" # Palavra-chave para interrupção do pipeline
 
64
 
65
+ print("🚀 App inicializada - GROQ v38 + CONTEXTO PERSISTENTE")
66
+ print(f" ✅ Groq: {'OK' if groq_key != 'SUA_GROQ_KEY_AQUI' else '⚠️ placeholder'}")
67
 
 
68
  # ==================== 2. UTILIDADES ====================
69
+ def estimar_tokens(texto):
70
+ """
71
+ Estimativa rápida de tokens.
72
+ Método: ~4 caracteres = 1 token (aproximação heurística).
73
+ """
74
+ return len(str(texto)) // 4
 
 
 
 
 
 
 
 
 
75
 
76
  def carregar_protocolo():
77
+ """ e retorna o conteúdo do protocolo de agentes em formato JSON."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  try:
79
+ with open(ARQUIVO_CONFIG, "r", encoding="utf-8") as f:
80
+ return f.read()
 
 
 
 
 
 
 
 
 
81
  except Exception as e:
82
+ print(f"❌ Erro carregar_protocolo: {e}")
83
+ return "[]"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
 
85
+ def salvar_protocolo(conteudo):
86
+ """Salva o conteúdo JSON do protocolo no arquivo de configuração."""
 
 
 
87
  try:
88
+ # Tenta validar o JSON antes de salvar
89
+ json.loads(conteudo)
90
+ with open(ARQUIVO_CONFIG, "w", encoding="utf-8") as f:
91
+ f.write(conteudo)
92
+ print(f"💾 Protocolo salvo: {len(json.loads(conteudo))} agentes")
93
+ return "✅ Protocolo salvo com sucesso"
 
 
 
 
 
 
 
 
94
  except Exception as e:
95
+ print(f"❌ Erro salvar_protocolo: {e}")
96
+ return f"❌ Erro JSON: {str(e)}"
97
 
98
+ def carregar_help():
99
+ """Lê e retorna o conteúdo do arquivo de ajuda."""
100
+ try:
101
+ with open(ARQUIVO_HELP, "r", encoding="utf-8") as f:
102
+ return f.read()
103
+ except:
104
+ return "# Help não encontrado\n\nCrie um arquivo help.md na raiz do projeto."
105
 
106
+ def carregar_contexto_persistente():
107
+ """Carrega o histórico de mensagens persistentes do arquivo JSON."""
108
+ try:
109
+ with open(ARQUIVO_CONTEXTO, "r", encoding="utf-8") as f:
110
+ return json.load(f)
111
+ except:
112
+ print("📝 Contexto persistente vazio, iniciando novo")
113
+ return []
114
 
115
+ def salvar_contexto_persistente(contexto):
116
+ """Salva o contexto persistente (histórico de mensagens) no arquivo JSON."""
117
  try:
118
+ with open(ARQUIVO_CONTEXTO, "w", encoding="utf-8") as f:
119
+ json.dump(contexto, f, ensure_ascii=False, indent=2)
120
+ print(f"💾 Contexto persistente salvo: {len(contexto)} mensagens")
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  except Exception as e:
122
+ print(f"❌ Erro salvar contexto: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
 
124
+ def limpar_contexto_persistente():
125
+ """Apaga o conteúdo do contexto persistente, iniciando uma nova memória."""
126
+ try:
127
+ with open(ARQUIVO_CONTEXTO, "w", encoding="utf-8") as f:
128
+ json.dump([], f)
129
+ print("🗑️ Contexto persistente limpo")
130
+ return "✅ Contexto limpo com sucesso"
131
+ except Exception as e:
132
+ return f"❌ Erro: {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
 
134
+ def limitar_timeline(timeline, max_chars=12000, max_msgs=12):
135
+ """
136
+ Função de Escala Dinâmica Resiliente (SDR).
137
+ Limita o contexto (timeline) para manter o modelo dentro do orçamento de tokens.
138
+ Prioriza as mensagens mais recentes.
139
+ """
140
+ acumulado = 0
141
+ selecionadas = []
142
+ # Itera de trás para frente (mensagens mais recentes)
143
+ for msg in reversed(timeline):
144
+ texto = str(msg.get("content", ""))
145
+ acumulado += len(texto)
146
+ # Condição de parada: limite de chars ou número máximo de mensagens
147
+ if acumulado > max_chars or len(selecionadas) >= max_msgs:
148
+ break
149
+ selecionadas.append(msg)
150
 
151
+ selecionadas.reverse() # Reverte para a ordem cronológica
152
+ tokens_est = estimar_tokens(acumulado)
153
+ print(f"✂️ Timeline limitada: {len(selecionadas)} msgs, {acumulado} chars (~{tokens_est} tokens)")
154
+ return selecionadas, acumulado, tokens_est
155
+
156
+ def ler_anexo(arquivo):
157
+ """Lê o conteúdo textual de um arquivo anexado e o formata com marcadores."""
158
+ if arquivo is None:
159
+ return ""
 
 
 
 
 
 
 
 
 
 
 
160
  try:
161
+ with open(arquivo.name, "r", encoding="utf-8") as f:
162
+ conteudo = f.read()
163
+ print(f"📎 Anexo lido: {os.path.basename(arquivo.name)} ({len(conteudo)} chars)")
164
+ # Retorna o conteúdo formatado com marcadores para clareza do LLM
165
+ return f"\n\n[ANEXO: {os.path.basename(arquivo.name)}]\n{conteudo}\n[FIM ANEXO]\n"
166
+ except Exception as e:
167
+ print(f"❌ Erro ler_anexo {arquivo.name}: {e}")
168
+ return ""
169
+
170
+ def verificar_stop(texto):
171
+ """Verifica se a palavra-chave de parada (STOP_PIPELINE:) está presente no output."""
172
+ if not texto:
173
+ return False
174
+ # Busca pela palavra-chave de parada
175
+ stop_detectado = bool(re.search(r'\b' + re.escape(STOP_KEYWORD) + r'\b', str(texto), re.IGNORECASE))
176
+ print(f"🛑 STOP detectado? {stop_detectado} em '{str(texto)[:100]}...'")
177
+ return stop_detectado
178
+
179
+ # ==================== 3. ENGINE DE EXECUÇÃO (GROQ) ====================
180
+ def executar_no(timeline, config):
181
+ """
182
+ Chama a API da Groq com o modelo e a timeline (contexto) definidos.
183
+ Gerencia o formato das mensagens, tokens e tratamento de streaming/erros.
184
+ """
185
+ print(f"\n🔥 === EXECUTANDO {config['nome']} ===")
186
+ modelo = config.get('modelo', 'meta-llama/llama-4-maverick-17b-128e-instruct')
187
+ print(f" Modelo Groq: {modelo}")
188
 
189
+ try:
190
+ inicio = time.time()
 
191
 
192
+ # 1. Limita o contexto
193
+ timeline_limited, chars_total, tokens_est = limitar_timeline(timeline, max_chars=12000, max_msgs=12)
 
194
 
195
+ # 2. Converte timeline para formato messages do Groq
196
+ messages = []
197
+
198
+ # System message com missão do agente
199
+ system_msg = f"AGENTE: {config['nome']}\nMISSÃO: {config['missao']}"
200
+ messages.append({
201
+ "role": "system",
202
+ "content": system_msg
203
+ })
204
+ tokens_system = estimar_tokens(system_msg)
205
+
206
+ # Adiciona timeline como contexto
207
+ for msg in timeline_limited:
208
+ role = msg.get('role')
209
+ if role in ['user', 'assistant']:
210
+ content = msg.get('content', '')
211
+ # Serializa JSON/List para string se necessário
212
+ if isinstance(content, (dict, list)):
213
+ content = json.dumps(content, ensure_ascii=False)
214
+ messages.append({
215
+ "role": role,
216
+ "content": str(content)
217
+ })
218
+
219
+ # 3. FIX: Garante que última mensagem seja sempre 'user'
220
+ user_prompt_final = (
221
+ f"Com base em TODO o contexto acima e na missão do agente '{config['nome']}', "
222
+ f"execute a missão agora e produza APENAS a saída esperada para este agente."
223
+ )
224
+ messages.append({
225
+ "role": "user",
226
+ "content": user_prompt_final
227
+ })
228
+ tokens_user_final = estimar_tokens(user_prompt_final)
229
+
230
+ tokens_total = tokens_est + tokens_system + tokens_user_final
231
+ print(f"📤 Groq messages: {len(messages)} mensagens (última=user, OK)")
232
+ print(f"📊 Tokens estimados: ~{tokens_total} (system: {tokens_system} + context: {tokens_est} + user_final: {tokens_user_final})")
233
+
234
+ # 4. Chama Groq API com streaming
235
+ completion = groq_client.chat.completions.create(
236
+ model=modelo,
237
+ messages=messages,
238
+ temperature=1,
239
+ max_completion_tokens=6048,
240
+ top_p=1,
241
+ stream=True,
242
+ stop=None
243
+ )
244
+
245
+ # 5. Coleta resposta em streaming
246
+ out_raw = ""
247
+ for chunk in completion:
248
+ content_chunk = chunk.choices[0].delta.content or ""
249
+ out_raw += content_chunk
250
+
251
+ tempo_exec = time.time() - inicio
252
+ tokens_resposta = estimar_tokens(out_raw)
253
+
254
+ print(f"📥 OUTPUT GROQ ({len(out_raw)} chars, ~{tokens_resposta} tokens, {tempo_exec:.2f}s):")
255
+ print(out_raw[:500])
256
+ print("..." if len(out_raw) > 500 else "")
257
+
258
+ # 6. Parse JSON se necessário
259
+ content = out_raw
260
+ if config.get('tipo_saida', 'texto').lower() in ['json', 'jshon']:
 
 
 
 
 
 
 
 
 
261
  try:
262
+ # Lógica de limpeza para extrair JSON de blocos de código
263
+ cleaned = out_raw.strip()
264
+ if cleaned.startswith("```"):
265
+ cleaned = re.sub(r'^```json\s*', '', cleaned)
266
+ cleaned = re.sub(r'^```', '', cleaned)
267
+ cleaned = re.sub(r'\s*```$', '', cleaned)
 
 
 
 
 
 
 
 
 
268
 
269
+ content = json.loads(cleaned)
270
+ print("✅ JSON parseado com sucesso")
271
+ except Exception as parse_e:
272
+ print(f"⚠️ Erro parse JSON: {parse_e}")
273
+ content = out_raw # Mantém o raw output em caso de erro de parse
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
 
275
+ print(f"⏱️ Tempo total: {tempo_exec:.2f}s")
 
276
 
277
+ # 7. Retorna resultado
278
+ return {
279
+ "role": "assistant",
280
+ "agent": config['nome'],
281
+ "content": content,
282
+ "raw": out_raw,
283
+ "tempo": tempo_exec,
284
+ "tokens_input": tokens_total,
285
+ "tokens_output": tokens_resposta
286
+ }, True
287
 
288
+ except Exception as e:
289
+ # Tratamento de Erros da API
290
+ msg = str(e)
291
+ print(f"💥 ERRO GROQ: {msg}")
292
+ import traceback
293
+ traceback.print_exc()
294
 
295
+ # Mensagens de erro amigáveis para o usuário
296
+ if "context_length_exceeded" in msg or "Please reduce the length of the messages" in msg:
297
+ erro_amistoso = (
298
+ "STOP: o contexto atual ficou grande demais para este modelo. "
299
+ "Tente limpar parte da memória ou resumir o histórico antes de continuar."
300
+ )
301
+ else:
302
+ erro_amistoso = msg
303
+
304
+ return {
305
+ "role": "system",
306
+ "error": erro_amistoso,
307
+ "agent": config['nome']
308
+ }, False
309
 
310
+ # ==================== 4. ORQUESTRADOR COM CONTEXTO PERSISTENTE (ADUC-SDR) ====================
311
+ def orquestrador(texto, anexos_list, history, json_config, contexto_objetivo):
312
+ """
313
+ Função principal que gerencia o fluxo de trabalho (pipeline),
314
+ mantém o contexto persistente e coordena a execução dos agentes Groq.
315
+ Implementa a Arquitetura de União Compositiva (ADUC).
316
+ """
317
+ print("\n" + "="*80)
318
+ print("🎬 INICIANDO ORQUESTRADOR - NOVA EXECUÇÃO")
319
 
320
+ if not texto.strip():
321
+ print("⚠️ Texto vazio, abortando")
322
+ yield history, [], carregar_contexto_persistente()
 
 
323
  return
324
 
325
+ # Adiciona input do usuário ao histórico temporário do chatbot
326
+ history = history + [{"role": "user", "content": texto}]
327
+
328
  try:
329
+ protocolo = json.loads(json_config)
330
+ print(f"🔗 Protocolo: {len(protocolo)} agentes")
331
+ except Exception as e:
332
+ print(f"💥 Erro JSON config: {e}")
333
+ history.append({"role": "assistant", "content": f"❌ Erro no JSON de Configuração: {str(e)}"})
334
+ yield history, [], carregar_contexto_persistente()
335
  return
 
 
336
 
337
+ # Placeholder de resposta (para feedback visual)
338
+ history.append({"role": "assistant", "content": ""})
339
 
340
+ # Carrega e atualiza contexto persistente (Memória de Longo Prazo)
341
+ contexto_persistente = carregar_contexto_persistente()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
342
 
343
+ # 1. Adiciona input do usuário ao contexto persistente COM PREFIXO [USUÁRIO]
344
+ contexto_persistente.append({
345
+ "role": "user",
346
+ "content": f"[USUÁRIO] {texto}",
347
+ "timestamp": datetime.now().strftime('%Y-%m-%d %H:%M:%S')
348
+ })
349
 
350
+ # 2. Monta o Contexto Inicial para a Chamada Atual (Objetivo + Anexos)
351
+ contexto_inicial = ""
352
+ if contexto_objetivo and contexto_objetivo.strip():
353
+ contexto_inicial += f"[OBJETIVO DO MODELO]\n{contexto_objetivo.strip()}\n[FIM OBJETIVO]\n\n"
354
 
355
+ if anexos_list:
356
+ for anexo in anexos_list:
357
+ anexo_conteudo = ler_anexo(anexo)
358
+ if anexo_conteudo:
359
+ contexto_inicial += anexo_conteudo
360
+
361
+ # 3. Monta a Timeline para o Agente (Contexto Completo para o LLM)
362
+ timeline = []
363
+ # Adiciona todo o histórico persistente (exceto o último input recém-adicionado)
364
+ for msg in contexto_persistente[:-1]:
365
+ timeline.append({
366
+ "role": msg["role"],
367
+ "content": msg["content"]
368
+ })
369
+
370
+ # Adiciona o input atual, que contém o 'contexto_inicial' (objetivo + anexos)
371
+ timeline.append({
372
+ "role": "user",
373
+ "content": f"{contexto_inicial}{texto}".strip()
374
+ })
375
 
376
+ print(f"🌐 Timeline montada: {len(timeline)} mensagens")
377
+
378
+ # Calcula tokens totais da timeline para exibição no UI
379
+ timeline_chars = sum(len(str(m.get("content", ""))) for m in timeline)
380
+ timeline_tokens = estimar_tokens(timeline_chars)
381
+
382
+ audit_data = [] # Para armazenar dados de auditoria
383
+
384
+ # 4. Loop de Execução dos Agentes (Pipeline)
385
+ for idx, cfg in enumerate(protocolo):
386
+ print(f"\n{'='*50}")
387
+ print(f"🚀 FASE {idx+1}/{len(protocolo)}: {cfg['nome']}")
388
 
389
+ # Feedback visual inicial no UI
390
+ history[-1]["content"] = (
391
+ f"⏳ **Agente {idx+1}/{len(protocolo)}: {cfg['nome']}**\n\n"
392
+ f"📊 Contexto (~{timeline_tokens} tokens)\n"
393
+ f"🔧 Modelo: `{cfg.get('modelo', 'default')}`\n"
394
+ f"*Aguarde...*"
395
+ )
396
+ yield history, audit_data, contexto_persistente
397
 
398
+ time.sleep(DELAY_ENTRE_AGENTES) # Delay entre chamadas de agentes
 
 
 
 
 
 
 
399
 
400
+ # Executa agente
401
+ res, sucesso = executar_no(timeline, cfg)
402
+
403
+ resposta_content = res.get('content', '')
404
+
405
+ # 5. Verificação de Condição de Parada (STOP_PIPELINE)
406
+ if verificar_stop(resposta_content):
407
+ print("🛑 STOP_PIPELINE detectado - encerrando pipeline")
408
+
409
+ texto_final = str(resposta_content)
410
 
411
+ # TRATAMENTO PARA EXTRAIR A RESPOSTA DO JSON
412
+ if isinstance(resposta_content, dict) and 'proximo_passo' in resposta_content:
413
+ texto_parcial = resposta_content['proximo_passo']
414
+ if texto_parcial.startswith(STOP_KEYWORD):
415
+ # Remove o prefixo de parada para exibir apenas a resposta limpa
416
+ texto_final = texto_parcial.replace(STOP_KEYWORD, "").strip()
417
+ else:
418
+ texto_final = texto_parcial
419
+ elif not isinstance(resposta_content, str):
420
+ # Se não for string nem JSON com a chave, exibe o JSON formatado
421
+ texto_final = json.dumps(resposta_content, ensure_ascii=False, indent=2)
422
+
423
+ # Se for string, verifica se contém o prefixo
424
+ elif texto_final.startswith(STOP_KEYWORD):
425
+ texto_final = texto_final.replace(STOP_KEYWORD, "").strip()
426
+
427
+
428
+ # Aplica o efeito typewriter e encerra
429
+ for i in range(0, len(texto_final), 10):
430
+ history[-1]["content"] = texto_final[:i+10]
431
+ yield history, audit_data, contexto_persistente
432
+ time.sleep(0.01)
433
+
434
+ history[-1]["content"] = texto_final
435
+ yield history, audit_data, contexto_persistente
436
+ return
437
+
438
+ # 6. Processamento e Persistência do Resultado (Apenas se não houve STOP)
439
+ if sucesso and resposta_content:
440
+ # Adiciona a resposta do agente ao contexto persistente COM PREFIXO [AGENTE nome]
441
+ content_to_persist = f"[{cfg['nome']}] {resposta_content if isinstance(resposta_content, str) else json.dumps(resposta_content, ensure_ascii=False)}"
442
+ contexto_persistente.append({
443
+ "role": "assistant",
444
+ "agent": cfg['nome'],
445
+ "content": content_to_persist,
446
+ "timestamp": datetime.now().strftime('%Y-%m-%d %H:%M:%S')
447
+ })
448
+
449
+ # Adiciona a resposta à timeline para o próximo agente (sem prefixo, para não poluir o prompt)
450
+ timeline.append({
451
+ "role": "assistant",
452
+ "content": resposta_content
453
+ })
454
+
455
+ # Atualiza contagem de tokens e salva o contexto
456
+ timeline_chars = sum(len(str(m.get("content", ""))) for m in timeline)
457
+ timeline_tokens = estimar_tokens(timeline_chars)
458
+ salvar_contexto_persistente(contexto_persistente)
459
+ print(f"🧠 Resposta de '{cfg['nome']}' salva e adicionada à timeline.")
460
+ else:
461
+ # Erro na execução (não é STOP_PIPELINE) - interrompe o pipeline
462
+ erro_msg = res.get("error", "Erro desconhecido na chamada ao modelo.")
463
+ history[-1]["content"] = f"❌ **Erro no agente {cfg['nome']}:**\n\n{erro_msg}"
464
+
465
+ audit_entry = {
466
+ "step": idx + 1, "agent": cfg['nome'], "model": cfg.get('modelo', 'default'),
467
+ "type": cfg.get('tipo_saida', 'texto'), "error": erro_msg, "sucesso": False,
468
+ "timestamp": datetime.now().strftime('%H:%M:%S')
469
+ }
470
+ audit_data.append(audit_entry)
471
+ yield history, audit_data, contexto_persistente
472
+ print("⛔ Pipeline interrompida devido a erro")
473
+ return
474
+
475
+ # 7. Registro de Auditoria
476
+ audit_entry = {
477
+ "step": idx + 1, "agent": cfg['nome'], "model": cfg.get('modelo', 'default'),
478
+ "type": cfg.get('tipo_saida', 'texto'), "response_preview": str(resposta_content)[:100] + "...",
479
+ "raw_len": len(res.get('raw', '')), "tokens_input": res.get('tokens_input', 0),
480
+ "tokens_output": res.get('tokens_output', 0), "tempo": round(res.get('tempo', 0), 2),
481
+ "sucesso": sucesso, "timestamp": datetime.now().strftime('%H:%M:%S')
482
+ }
483
+ audit_data.append(audit_entry)
484
+
485
+ # 8. Exibição da Resposta Final (último agente ou agente de saída tipo 'texto')
486
+ if idx == len(protocolo) - 1 or cfg.get('tipo_saida') == 'texto':
487
+ texto_final = str(resposta_content) if isinstance(resposta_content, str) else json.dumps(resposta_content, ensure_ascii=False, indent=2)
488
+
489
+ # Efeito Typewriter (para visualização no Gradio)
490
+ for i in range(0, len(texto_final), 10):
491
+ history[-1]["content"] = texto_final[:i+10]
492
+ yield history, audit_data, contexto_persistente
493
+ time.sleep(0.01)
494
+
495
+ history[-1]["content"] = texto_final
496
+ yield history, audit_data, contexto_persistente
497
+
498
+ print("🏁 Pipeline concluída com sucesso")
499
+ print("="*80)
500
+
501
+ # ==================== 5. UI (GRADIO) ====================
502
+ def ui_clean():
503
+ """Constrói e retorna a interface Gradio."""
504
+ config_init = carregar_protocolo()
505
+ help_init = carregar_help()
506
+
507
+ # CORREÇÃO DO WARNING: Parâmetro 'css' movido para o método .launch() no final do arquivo
508
+ with gr.Blocks(title="AI Forensics - Groq (v38)") as app:
509
+ # Estados para dados persistentes
510
+ anexos_state = gr.State([])
511
+
512
+ # Tabs de Navegação
513
+ with gr.Tabs():
514
+ # Tab 1: Chat Principal
515
+ with gr.Tab("💬 Chat"):
516
+ gr.Markdown("## Investigador AI (v38 - Orquestração ADUC-SDR)")
517
+ # CORREÇÃO DO ERRO: Garantir que não há parâmetros inesperados como 'show_copy_button'
518
+ chatbot = gr.Chatbot(label="Histórico Conversacional", height=500)
519
+
520
+ with gr.Row():
521
+ txt_in = gr.Textbox(show_label=False, placeholder="Digite sua mensagem...", lines=2, scale=9)
522
+ btn_send = gr.Button("📤 Enviar", variant="primary", scale=1)
523
+
524
+ # Tab 2: Configurações de Contexto
525
+ with gr.Tab("📎 Anexos & Contexto"):
526
+ gr.Markdown("""
527
+ ## Anexos e Contexto Factual
528
+
529
+ **Anexos:** Conteúdo lido e adicionado *apenas* ao input da execução atual. **NÃO PERSISTE** na memória.
530
+ **Objetivo:** Define o `System Prompt` e a orientação de todos os agentes.
531
+ """)
532
+ objetivo_text = gr.Textbox(
533
+ label="Objetivo do Modelo (System Prompt Global)",
534
+ value="Voce é um agente chamado IndenizaAI existe para transformar um dano — seja ele físico, emocional ou existencial — em um valor que faça sentido. Seu papel não é tomar partido nem alimentar vingança, mas construir um ponto de equilíbrio entre a dor sofrida e a responsabilidade de quem causou o prejuízo. Ele organiza os fatos, compreende o impacto real e traduz tudo isso em um número que reconhece a gravidade do acontecido sem exageros, sem minimizações e sem distorções. O cálculo é o coração do seu trabalho. O IndenizaAI observa cada elemento com sobriedade: o que aconteceu, quem foi afetado, como a vida mudou, o que se perdeu e o que não pode ser restaurado. A partir disso, ele converge todos os fatores — humanos, materiais e existenciais — para uma medida proporcional e defensável. Esse valor não é um prêmio nem uma punição; é a forma concreta de dizer que houve dano, que esse dano tem peso e que a reparação precisa ser justa. No fim, sua importância está em oferecer clareza onde a emoção costuma criar névoa. Ele dá ao usuário uma referência honesta, capaz de orientar decisões, acordos e caminhos jurídicos. Seu cálculo é a ponte entre a vítima e a justiça: um número que não repara o passado, mas reconhece sua dor e estabelece o que é devido. E é nessa precisão equilibrada que o IndenizaAI cumpre sua razão de existir"
535
+ , placeholder="Ex: Você é um analista forense imparcial...",
536
+ lines=5
537
  )
538
+ gr.Markdown("### Anexos (não persistentes)")
539
+ anexos_upload = gr.File(
540
+ file_count="multiple",
541
+ file_types=[".txt", ".md", ".json"]
542
+ )
543
+ anexos_display = gr.Textbox(label="Arquivos Carregados", interactive=False, lines=3)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
544
 
545
+ def atualizar_anexos(files):
546
+ if not files:
547
+ return [], "Nenhum arquivo carregado"
548
+ nomes = [os.path.basename(f.name) for f in files]
549
+ return files, f"📎 {len(files)} arquivo(s): {', '.join(nomes)}"
550
+
551
+ anexos_upload.change(atualizar_anexos, anexos_upload, [anexos_state, anexos_display])
552
+
553
+ # Tab 3: Memória Persistente
554
+ with gr.Tab("🧠 Contexto Persistente"):
555
+ gr.Markdown("""
556
+ ## Memória Persistente do Sistema (ADUC)
557
+
558
+ Armazena o histórico da conversação. É limitado a ~12k chars por chamada.
559
+ - **Input do usuário:** prefixo `[USUÁRIO]`
560
+ - **Respostas dos agentes:** prefixo `[AGENTE nome]`
561
+ """)
562
+
563
+ contexto_display = gr.JSON(
564
+ label="Contexto Persistente",
565
+ value=carregar_contexto_persistente()
566
  )
567
 
568
+ with gr.Row():
569
+ btn_reload_ctx = gr.Button("🔄 Recarregar", size="sm")
570
+ btn_limpar_ctx = gr.Button("🗑️ Limpar Contexto", size="sm", variant="stop")
571
 
572
+ status_ctx = gr.Markdown("")
 
 
 
 
 
573
 
574
+ btn_reload_ctx.click(
575
+ lambda: carregar_contexto_persistente(),
576
+ outputs=contexto_display
577
+ )
578
 
579
+ def limpar_e_recarregar():
580
+ msg = limpar_contexto_persistente()
581
+ return carregar_contexto_persistente(), msg
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
582
 
583
+ btn_limpar_ctx.click(
584
+ limpar_e_recarregar,
585
+ outputs=[contexto_display, status_ctx]
 
 
 
 
 
 
 
 
 
 
 
 
 
586
  )
587
+
588
+ # Tab 4: Configuração da Pipeline
589
+ with gr.Tab("⚙️ Protocolo"):
590
+ gr.Markdown("""
591
+ ## Edição do Protocolo de Agentes (Pipeline)
592
+
593
+ Define a sequência de agentes.
594
+ """)
595
 
596
  with gr.Row():
597
+ btn_save_proto = gr.Button("💾 Salvar", variant="primary", size="sm")
598
+ btn_reload_proto = gr.Button("🔄 Recarregar", size="sm")
599
+ proto_status = gr.Markdown("")
600
+ code_json = gr.Code(value=config_init, language="json", lines=30)
 
 
 
601
 
602
+ btn_save_proto.click(salvar_protocolo, code_json, proto_status)
603
+ btn_reload_proto.click(lambda: carregar_protocolo(), outputs=code_json)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
604
 
605
+ # Tab 5: Auditoria
606
+ with gr.Tab("🔍 Auditoria"):
607
+ gr.Markdown("""
608
+ ## Auditoria da Última Execução
609
+
610
+ Rastreamento do consumo de recursos por agente.
611
+ """)
612
+ audit_display = gr.JSON(label="Dados de Auditoria", value=[])
613
+
614
+ with gr.Tab("❓ Ajuda"):
615
+ help_content = gr.Markdown(help_init)
616
+ btn_reload_help = gr.Button("🔄 Recarregar Help")
617
+ btn_reload_help.click(lambda: carregar_help(), outputs=help_content)
618
+
619
+ # Triggers de Ação
620
+ btn_send.click(
621
+ orquestrador,
622
+ [txt_in, anexos_state, chatbot, code_json, objetivo_text],
623
+ [chatbot, audit_display, contexto_display]
624
+ ).then(lambda: "", outputs=txt_in)
625
+
626
+ txt_in.submit(
627
+ orquestrador,
628
+ [txt_in, anexos_state, chatbot, code_json, objetivo_text],
629
+ [chatbot, audit_display, contexto_display]
630
+ ).then(lambda: "", outputs=txt_in)
631
+
632
  return app
633
 
634
  if __name__ == "__main__":
635
+ print("🎉 Lançando app Groq v38...")
636
+ # Lançamento do Gradio
637
+ # CORREÇÃO DO WARNING: O parâmetro `css` foi movido para o método launch()
638
+ ui_clean().launch(css="footer{display:none!important;}")