caarleexx commited on
Commit
8a6fb22
·
verified ·
1 Parent(s): 63c5aa9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +225 -131
app.py CHANGED
@@ -1,6 +1,6 @@
1
  # ╔════════════════════════════════════════════════════════════════════════════╗
2
- # ║ PIPELINE v33: DIVISÃO BURRA + VISÃO PURA + CONCATENAÇÃO FIEL
3
- # ║ SEM OCR/EXTRAÇÃO - APENAS METADADOS + OLHOS DO GEMINI
4
  # ╚════════════════════════════════════════════════════════════════════════════╝
5
 
6
  import os
@@ -8,95 +8,129 @@ import json
8
  import re
9
  import time
10
  from datetime import datetime
 
11
  import gradio as gr
12
  import google.generativeai as genai
13
  import pypdf # pip install pypdf
14
 
15
  # ==================== 1. CONFIGURAÇÃO ====================
 
16
  api_key = os.getenv("GOOGLE_API_KEY", "SUA_API_KEY_AQUI")
17
- if api_key: genai.configure(api_key=api_key)
 
18
 
19
  model_flash = genai.GenerativeModel("gemini-flash-latest")
20
- model_pro = genai.GenerativeModel("gemini-pro-latest")
21
 
22
- ARQUIVO_CONFIG = "protocolo_fragmentacao_visao.json"
23
 
24
  # ==================== 2. UTILIDADES ====================
 
25
  def carregar_protocolo():
 
 
 
 
26
  try:
27
- with open(ARQUIVO_CONFIG, "r", encoding="utf-8") as f:
28
  return f.read()
29
- except:
30
- return """[
31
- {
32
- "nome": "VISÃO_FRAGMENTO (PASSO 0)",
33
- "missao": "Você é OLHO MECÂNICO. Descreva APENAS o que vê nestas páginas: imagens, texto visível, layout, objetos, cores, diagramas. SEM interpretações, julgamentos, omissões ou criações. Liste tudo fielmente.",
34
- "tipo_saida": "texto",
35
- "modelo": "flash"
36
- },
37
- {
38
- "nome": "CATALOGADOR",
39
- "missao": "Analise todas as descrições de visão acumuladas. Crie índices temáticos e estrutura sem alterar fatos visuais.",
40
- "tipo_saida": "json",
41
- "modelo": "pro"
42
- },
43
- {
44
- "nome": "CONCATENADOR_FINAL",
45
- "missao": "UNA TODAS descrições de visão em TEXTO ÚNICO CONTÍNUO. Mantenha ordem original. Sem resumos, só o retrato fiel completo do documento.",
46
- "tipo_saida": "texto",
47
- "modelo": "pro"
48
- }
49
- ]"""
50
 
51
  def salvar_protocolo(conteudo):
 
 
 
52
  try:
53
  json.loads(conteudo)
54
- with open("protocolo.json", "w", encoding="utf-8") as f:
55
  f.write(conteudo)
56
  return "✅ Salvo"
57
- except:
58
  return "❌ Erro JSON"
59
 
60
- # DIVISÃO BURRA: SEM TEXTO/OCR - APENAS METADADOS
61
- def ler_anexo_e_fragmentar(arquivo, paginas_por_fragmento=3):
62
- """DIVISÃO BURRA: APENAS METADADOS. LLM faz visão nativa."""
63
- if arquivo is None: return [], ""
64
-
65
- filename = getattr(arquivo, 'name', arquivo)
66
- if not os.path.exists(filename):
 
 
 
 
 
 
 
67
  return [], f"[ERRO: Arquivo não encontrado: {filename}]"
68
-
69
  anexo_info = f"[PDF: {os.path.basename(filename)}]"
70
- if not filename.lower().endswith('.pdf'):
 
 
71
  return [f"[ARQUIVO_TEXTO: {os.path.basename(filename)}]"], anexo_info
72
-
73
  try:
74
  reader = pypdf.PdfReader(filename)
75
  total_pages = len(reader.pages)
76
  fragments = []
 
77
  for i in range(0, total_pages, paginas_por_fragmento):
78
  start = i + 1
79
  end = min(i + paginas_por_fragmento, total_pages)
80
- fragment = f"=== FRAG {i//paginas_por_fragmento+1} (PÁGS {start}-{end}/{total_pages}) ===\n[VISÃO: Descreva IMAGENS/TEXTO_VISUAL/LAYOUT. SEM julgar/omitir/criar.]"
 
 
 
 
 
 
 
81
  fragments.append(fragment)
 
82
  return fragments, anexo_info
83
  except Exception as e:
84
  return [f"[ERRO PDF: {str(e)}]"], anexo_info
85
 
86
  # ==================== 3. ENGINE DE EXECUÇÃO ====================
 
87
  def executar_no(timeline, config, fragmento_input=None):
 
 
 
 
 
88
  modelo = model_pro if config.get("modelo") == "pro" else model_flash
89
-
90
  if fragmento_input is not None:
91
  input_para_prompt = fragmento_input
92
  else:
93
  input_para_prompt = json.dumps(timeline, ensure_ascii=False, indent=2)
94
-
95
- prompt = f"""--- INPUT PARA O AGENTE ---
96
- {input_para_prompt}
97
- ----------------
98
- AGENTE: {config['nome']}
99
- MISSÃO: {config['missao']}"""
 
 
100
 
101
  log = f"\n🔸 {config['nome']}..."
102
  try:
@@ -104,156 +138,216 @@ MISSÃO: {config['missao']}"""
104
  resp = modelo.generate_content(prompt)
105
  out = resp.text
106
  tempo = time.time() - inicio
107
-
108
- content = (json.loads(out.strip().replace('``````',''))
109
- if config['tipo_saida']=='json' else out)
110
-
 
 
 
 
111
  log += f" (OK - {tempo:.2f}s)"
112
- return {"role": "assistant", "agent": config['nome'], "content": content}, log, out
113
  except Exception as e:
114
  return {"role": "system", "error": str(e)}, f" (ERRO: {e})", str(e)
115
 
116
  # ==================== 4. ORQUESTRADOR ====================
117
- def orquestrador(texto, arquivo, history, json_config):
118
- # 1. Leitura e Fragmentação BURRA
 
 
 
 
 
 
 
 
119
  fragmentos, anexo_info = ler_anexo_e_fragmentar(arquivo)
120
-
121
  if not texto and not fragmentos:
122
- yield history, {}, "⚠️ Sem input ou erro ao ler arquivo."
123
  return
124
-
125
- # 2. Setup Inicial
126
  history = history + [[texto + (" 📎" if arquivo else ""), None]]
127
- try:
 
128
  protocolo = json.loads(json_config)
129
  except:
130
  history[-1][1] = "❌ Erro no JSON de Configuração."
131
- yield history, {}, "Erro JSON"
132
  return
133
-
134
  timeline = [{"role": "user", "content": texto}]
135
  logs = f"🚀 START: {datetime.now().strftime('%H:%M:%S')}\n"
136
- if fragmentos:
137
- logs += f"📎 PDF dividido: {len(fragmentos)} fragments burros.\n"
138
-
139
- history[-1][1] = " Dividindo + Visão..."
140
- yield history, timeline, logs
141
-
142
- # --- PASSO 0: LOOP DE VISÃO ---
143
- concatenated_vision = anexo_info + "\n\n"
144
- if protocolo and protocolo[0]['nome'] == 'VISÃO_FRAGMENTO (PASSO 0)' and len(fragmentos) > 0:
145
- cfg_visao = protocolo.pop(0) # Remove PASSO 0
146
-
 
 
 
 
 
 
 
 
147
  for i, fragmento in enumerate(fragmentos):
148
- history[-1][1] = f"👁️ {cfg_visao['nome']} frag {i+1}/{len(fragmentos)}..."
149
- yield history, timeline, logs
150
-
 
 
151
  res, log_add, raw = executar_no(timeline, cfg_visao, fragmento_input=fragmento)
152
  logs += log_add + "\n"
153
-
154
- if 'error' in res:
155
  logs += f"❌ Erro frag {i+1}: {res['error']}\n"
156
-
157
- concatenated_vision += f"\n--- VISÃO FRAG {i+1} ---\n{res['content']}\n"
158
-
159
- logs += "\n✅ Visões completas.\n"
160
- timeline.append({
161
- "role": "system",
162
- "agent": "DOCUMENTO_VISAO_COMPLETA",
163
- "content": concatenated_vision
164
- })
165
- elif len(fragmentos) > 0:
166
- timeline.append({
167
- "role": "system",
168
- "agent": "FRAGMENTS_RAW",
169
- "content": concatenated_vision + "\n".join(fragmentos)
170
- })
171
-
172
- # --- PASSOS 1+: ANÁLISE SEQUENCIAL ---
 
 
 
 
 
173
  final_response = ""
174
- for cfg in protocolo:
 
 
 
175
  history[-1][1] = f"⚙️ {cfg['nome']}..."
176
- yield history, timeline, logs
177
-
178
  res, log_add, raw = executar_no(timeline, cfg)
179
  timeline.append(res)
180
  logs += log_add + "\n"
181
-
182
- if cfg['tipo_saida'] == 'texto':
183
- final_response = res['content']
184
  history[-1][1] = final_response
185
- yield history, timeline, logs
186
-
187
- logs += "✅ FIM."
188
- yield history, timeline, logs
189
 
190
- # ==================== 5. UI LIMPA (v33) ====================
 
 
 
 
 
 
 
 
 
 
 
191
  def ui_clean():
192
  css = """
193
  footer {display: none !important;}
194
  .contain {border: none !important;}
195
  """
196
-
197
  config_init = carregar_protocolo()
198
-
199
- with gr.Blocks(title="👁️ AI Visão Pura", css=css, theme=gr.themes.Soft()) as app:
 
 
200
  with gr.Tabs():
201
- # === ABA 1: CHAT ===
202
  with gr.Tab("💬 Investigador"):
203
  chatbot = gr.Chatbot(
204
- label="", show_label=False, height=600,
205
- show_copy_button=True, render_markdown=True
 
 
 
206
  )
207
-
208
  with gr.Row():
209
  with gr.Column(scale=10):
210
  txt_in = gr.Textbox(
211
  show_label=False,
212
- placeholder="Instruções opcionais...",
213
- lines=1, max_lines=5, container=False
 
 
214
  )
215
  with gr.Column(scale=1, min_width=50):
216
  file_in = gr.UploadButton(
217
- "📎", file_types=[".txt", ".md", ".csv", ".json", ".pdf"], size="sm"
 
 
218
  )
219
  with gr.Column(scale=1, min_width=80):
220
  btn_send = gr.Button("Enviar", variant="primary", size="sm")
221
-
222
  file_status = gr.Markdown("", visible=True)
 
223
  file_in.upload(
224
- lambda x: f"📎 PDF pronto: {os.path.basename(getattr(x, 'name', x))}",
225
- file_in, file_status
 
226
  )
227
-
228
- # === ABA 2: LOGS ===
229
  with gr.Tab("🕵️ Depuração"):
230
  with gr.Row():
231
  out_dna = gr.JSON(label="DNA (Timeline)")
232
  out_logs = gr.Textbox(label="Logs do Sistema", lines=20)
233
-
 
234
  # === ABA 3: CONFIG ===
235
  with gr.Tab("⚙️ Config"):
236
  with gr.Row():
237
  btn_save = gr.Button("Salvar Config")
238
  lbl_save = gr.Label(show_label=False)
239
- code_json = gr.Code(
240
- value=config_init, language="json", label="protocolo.json"
241
- )
242
  btn_save.click(salvar_protocolo, code_json, lbl_save)
243
-
244
  # === TRIGGERS ===
 
 
 
 
 
245
  triggers = [btn_send.click, txt_in.submit]
 
246
  for trig in triggers:
247
  trig(
248
- orquestrador,
249
- inputs=[txt_in, file_in, chatbot, code_json],
250
- outputs=[chatbot, out_dna, out_logs]
 
 
 
 
251
  ).then(
252
- lambda: (None, None, ""),
253
- outputs=[txt_in, file_in, file_status]
 
254
  )
255
-
256
  return app
257
 
 
258
  if __name__ == "__main__":
259
  ui_clean().launch()
 
1
  # ╔════════════════════════════════════════════════════════════════════════════╗
2
+ # ║ PIPELINE v40: FRAGMENTAÇÃO + VISÃO PAGINADA + ORQUESTRAÇÃO EM ABAS
3
+ # ║ Upload de PDF fragmentos PAGINADOR_VISUAL confext_upload JSON
4
  # ╚════════════════════════════════════════════════════════════════════════════╝
5
 
6
  import os
 
8
  import re
9
  import time
10
  from datetime import datetime
11
+
12
  import gradio as gr
13
  import google.generativeai as genai
14
  import pypdf # pip install pypdf
15
 
16
  # ==================== 1. CONFIGURAÇÃO ====================
17
+
18
  api_key = os.getenv("GOOGLE_API_KEY", "SUA_API_KEY_AQUI")
19
+ if api_key:
20
+ genai.configure(api_key=api_key)
21
 
22
  model_flash = genai.GenerativeModel("gemini-flash-latest")
23
+ model_pro = genai.GenerativeModel("gemini-pro-latest")
24
 
25
+ ARQUIVO_CONFIG = "protocolo_fragmentacao_visao-3.json"
26
 
27
  # ==================== 2. UTILIDADES ====================
28
+
29
  def carregar_protocolo():
30
+ """
31
+ Carrega o protocolo JSON de agentes.
32
+ Se não existir, usa o protocolo básico de PAGINADOR_VISUAL.
33
+ """
34
  try:
35
+ with open(ARQUIVO_CONFIG, "r", encoding="utf-8") as f:
36
  return f.read()
37
+ except:
38
+ # fallback: protocolo mínimo de visão paginada
39
+ return json.dumps([
40
+ {
41
+ "nome": "PAGINADOR_VISUAL",
42
+ "missao": (
43
+ "Você recebe o texto bruto de um conjunto de páginas de um PDF. "
44
+ "Separe o conteúdo por PÁGINA, na ordem original. "
45
+ "Para cada página, produza um objeto com: 'pagina', "
46
+ "'transcricao_fiel' (texto integral, sem resumo) e "
47
+ "'descricao_visual' (imagens, tabelas, diagramas, layout, sem julgamentos). "
48
+ "Se o fragmento tiver 5 páginas, devolva uma lista JSON com EXATAMENTE 5 objetos, "
49
+ "um por página."
50
+ ),
51
+ "tipo_saida": "json",
52
+ "modelo": "flash"
53
+ }
54
+ ], ensure_ascii=False, indent=2)
 
 
 
55
 
56
  def salvar_protocolo(conteudo):
57
+ """
58
+ Salva o JSON de protocolo após validar sintaxe.
59
+ """
60
  try:
61
  json.loads(conteudo)
62
+ with open(ARQUIVO_CONFIG, "w", encoding="utf-8") as f:
63
  f.write(conteudo)
64
  return "✅ Salvo"
65
+ except:
66
  return "❌ Erro JSON"
67
 
68
+ # --------- DIVISÃO BURRA: PDF EM BLOCOS DE 5 PÁGINAS ---------
69
+
70
+ def ler_anexo_e_fragmentar(arquivo, paginas_por_fragmento=5):
71
+ """
72
+ DIVISÃO BURRA:
73
+ - Se PDF: cria fragmentos de metadados indicando blocos de páginas.
74
+ - Se texto: devolve um único fragmento marcador.
75
+ """
76
+ if arquivo is None:
77
+ return [], ""
78
+
79
+ filename = getattr(arquivo, "name", arquivo)
80
+
81
+ if not os.path.exists(filename):
82
  return [], f"[ERRO: Arquivo não encontrado: {filename}]"
83
+
84
  anexo_info = f"[PDF: {os.path.basename(filename)}]"
85
+
86
+ # Se não for PDF, tratamos como arquivo texto simples
87
+ if not filename.lower().endswith(".pdf"):
88
  return [f"[ARQUIVO_TEXTO: {os.path.basename(filename)}]"], anexo_info
89
+
90
  try:
91
  reader = pypdf.PdfReader(filename)
92
  total_pages = len(reader.pages)
93
  fragments = []
94
+
95
  for i in range(0, total_pages, paginas_por_fragmento):
96
  start = i + 1
97
  end = min(i + paginas_por_fragmento, total_pages)
98
+
99
+ # Aqui o fragmento é apenas um cabeçalho + marcador
100
+ fragment = (
101
+ f"=== FRAG {i//paginas_por_fragmento + 1} "
102
+ f"(PÁGS {start}-{end}/{total_pages}) ===\n"
103
+ f"[PAGINADOR: você está vendo páginas {start}-{end}. "
104
+ f"Extraia JSON pagina/transcricao_fiel/descricao_visual.]"
105
+ )
106
  fragments.append(fragment)
107
+
108
  return fragments, anexo_info
109
  except Exception as e:
110
  return [f"[ERRO PDF: {str(e)}]"], anexo_info
111
 
112
  # ==================== 3. ENGINE DE EXECUÇÃO ====================
113
+
114
  def executar_no(timeline, config, fragmento_input=None):
115
+ """
116
+ Executa um nó (agente) do protocolo.
117
+ - Se fragmento_input for fornecido, ele é o INPUT PARA O AGENTE.
118
+ - Caso contrário, o input é o JSON da timeline inteira.
119
+ """
120
  modelo = model_pro if config.get("modelo") == "pro" else model_flash
121
+
122
  if fragmento_input is not None:
123
  input_para_prompt = fragmento_input
124
  else:
125
  input_para_prompt = json.dumps(timeline, ensure_ascii=False, indent=2)
126
+
127
+ prompt = (
128
+ "--- INPUT PARA O AGENTE ---\n"
129
+ f"{input_para_prompt}\n"
130
+ "----------------\n"
131
+ f"AGENTE: {config['nome']}\n"
132
+ f"MISSÃO: {config['missao']}"
133
+ )
134
 
135
  log = f"\n🔸 {config['nome']}..."
136
  try:
 
138
  resp = modelo.generate_content(prompt)
139
  out = resp.text
140
  tempo = time.time() - inicio
141
+
142
+ if config["tipo_saida"] == "json":
143
+ # limpeza mínima de cercas de código
144
+ cleaned = out.strip().replace("``````", "")
145
+ content = json.loads(cleaned)
146
+ else:
147
+ content = out
148
+
149
  log += f" (OK - {tempo:.2f}s)"
150
+ return {"role": "assistant", "agent": config["nome"], "content": content}, log, out
151
  except Exception as e:
152
  return {"role": "system", "error": str(e)}, f" (ERRO: {e})", str(e)
153
 
154
  # ==================== 4. ORQUESTRADOR ====================
155
+
156
+ def orquestrador(texto, arquivo, history, json_config, confext_state):
157
+ """
158
+ Pipeline:
159
+ 1) Se houver arquivo, faz divisão burra + visão PAGINADOR_VISUAL por fragmento.
160
+ - Resultado consolidado em confext_upload (state) por página.
161
+ 2) Timeline recebe um nó de sistema com esse confext_upload.
162
+ 3) Protocolos adicionais (se existirem além do PAGINADOR_VISUAL) usam só esse contexto.
163
+ """
164
+ # 1. Fragmentação
165
  fragmentos, anexo_info = ler_anexo_e_fragmentar(arquivo)
166
+
167
  if not texto and not fragmentos:
168
+ yield history, {}, "⚠️ Sem input ou erro ao ler arquivo.", confext_state
169
  return
170
+
171
+ # 2. Setup inicial
172
  history = history + [[texto + (" 📎" if arquivo else ""), None]]
173
+
174
+ try:
175
  protocolo = json.loads(json_config)
176
  except:
177
  history[-1][1] = "❌ Erro no JSON de Configuração."
178
+ yield history, {}, "Erro JSON", confext_state
179
  return
180
+
181
  timeline = [{"role": "user", "content": texto}]
182
  logs = f"🚀 START: {datetime.now().strftime('%H:%M:%S')}\n"
183
+
184
+ # Estrutura interna de contexto de anexos
185
+ confext_upload = {
186
+ "arquivo": os.path.basename(getattr(arquivo, "name", "sem_arquivo"))
187
+ if arquivo else None,
188
+ "meta": anexo_info,
189
+ "paginas": [] # cada item: {pagina, transcricao_fiel, descricao_visual}
190
+ }
191
+
192
+ if fragmentos:
193
+ logs += f"📎 PDF dividido: {len(fragmentos)} fragmentos burros.\n"
194
+ history[-1][1] = "⏳ Fragmentando + visão paginada..."
195
+ yield history, timeline, logs, confext_upload
196
+
197
+ # 3. PASSO DE VISÃO PAGINADA (PAGINADOR_VISUAL)
198
+ # Espera-se que seja o primeiro (único) agente, mas tratamos genericamente.
199
+ if protocolo and fragmentos:
200
+ cfg_visao = protocolo[0] # usamos apenas o primeiro como PAGINADOR_VISUAL
201
+
202
  for i, fragmento in enumerate(fragmentos):
203
+ history[-1][1] = (
204
+ f"👁️ {cfg_visao['nome']} frag {i+1}/{len(fragmentos)}..."
205
+ )
206
+ yield history, timeline, logs, confext_upload
207
+
208
  res, log_add, raw = executar_no(timeline, cfg_visao, fragmento_input=fragmento)
209
  logs += log_add + "\n"
210
+
211
+ if "error" in res:
212
  logs += f"❌ Erro frag {i+1}: {res['error']}\n"
213
+ continue
214
+
215
+ # res['content'] deve ser lista JSON de páginas [{pagina, transcricao_fiel, descricao_visual}, ...]
216
+ try:
217
+ paginas_res = res["content"]
218
+ if isinstance(paginas_res, dict):
219
+ paginas_res = [paginas_res]
220
+ for p in paginas_res:
221
+ confext_upload["paginas"].append(p)
222
+ except Exception as e:
223
+ logs += f"❌ Erro ao incorporar páginas do frag {i+1}: {e}\n"
224
+
225
+ logs += " Visões paginadas completas.\n"
226
+
227
+ # 4. Injeta confext_upload na timeline como nó de sistema
228
+ timeline.append({
229
+ "role": "system",
230
+ "agent": "CONFEXT_UPLOAD",
231
+ "content": confext_upload
232
+ })
233
+
234
+ # 5. Se existirem passos adicionais além do PAGINADOR_VISUAL, executa-os sequencialmente
235
  final_response = ""
236
+ # pula o primeiro passo se ele é o PAGINADOR_VISUAL
237
+ restante = protocolo[1:] if protocolo else []
238
+
239
+ for cfg in restante:
240
  history[-1][1] = f"⚙️ {cfg['nome']}..."
241
+ yield history, timeline, logs, confext_upload
242
+
243
  res, log_add, raw = executar_no(timeline, cfg)
244
  timeline.append(res)
245
  logs += log_add + "\n"
246
+
247
+ if cfg["tipo_saida"] == "texto":
248
+ final_response = res["content"]
249
  history[-1][1] = final_response
 
 
 
 
250
 
251
+ yield history, timeline, logs, confext_upload
252
+
253
+ if not restante and not texto:
254
+ # se não há mais agentes e não houve prompt, só confirma pré-processamento
255
+ history[-1][1] = "✅ PDF processado. Pronto para perguntas usando contexto interno."
256
+ final_response = history[-1][1]
257
+
258
+ logs += "✅ FIM.\n"
259
+ yield history, timeline, logs, confext_upload
260
+
261
+ # ==================== 5. UI LIMPA ====================
262
+
263
  def ui_clean():
264
  css = """
265
  footer {display: none !important;}
266
  .contain {border: none !important;}
267
  """
268
+
269
  config_init = carregar_protocolo()
270
+
271
+ with gr.Blocks(title="AI Forensics – Visão Paginada", css=css, theme=gr.themes.Soft()) as app:
272
+ confext_state = gr.State(value=None)
273
+
274
  with gr.Tabs():
275
+ # === ABA 1: CHAT / INVESTIGADOR ===
276
  with gr.Tab("💬 Investigador"):
277
  chatbot = gr.Chatbot(
278
+ label="",
279
+ show_label=False,
280
+ height=600,
281
+ show_copy_button=True,
282
+ render_markdown=True,
283
  )
284
+
285
  with gr.Row():
286
  with gr.Column(scale=10):
287
  txt_in = gr.Textbox(
288
  show_label=False,
289
+ placeholder="Descreva o caso ou faça perguntas (opcional após upload)...",
290
+ lines=1,
291
+ max_lines=5,
292
+ container=False,
293
  )
294
  with gr.Column(scale=1, min_width=50):
295
  file_in = gr.UploadButton(
296
+ "📎",
297
+ file_types=[".txt", ".md", ".csv", ".json", ".pdf"],
298
+ size="sm",
299
  )
300
  with gr.Column(scale=1, min_width=80):
301
  btn_send = gr.Button("Enviar", variant="primary", size="sm")
302
+
303
  file_status = gr.Markdown("", visible=True)
304
+ # Mostra nome do arquivo ao subir
305
  file_in.upload(
306
+ lambda x: f"📎 Anexo recebido: {os.path.basename(getattr(x, 'name', x))}",
307
+ inputs=file_in,
308
+ outputs=file_status,
309
  )
310
+
311
+ # === ABA 2: DEPURAÇÃO ===
312
  with gr.Tab("🕵️ Depuração"):
313
  with gr.Row():
314
  out_dna = gr.JSON(label="DNA (Timeline)")
315
  out_logs = gr.Textbox(label="Logs do Sistema", lines=20)
316
+ confext_view = gr.JSON(label="confext_upload")
317
+
318
  # === ABA 3: CONFIG ===
319
  with gr.Tab("⚙️ Config"):
320
  with gr.Row():
321
  btn_save = gr.Button("Salvar Config")
322
  lbl_save = gr.Label(show_label=False)
323
+ code_json = gr.Code(value=config_init, language="json", label=ARQUIVO_CONFIG)
 
 
324
  btn_save.click(salvar_protocolo, code_json, lbl_save)
325
+
326
  # === TRIGGERS ===
327
+ def _orq_wrapper(texto, arquivo, history, json_cfg, confext_old):
328
+ # delega para o orquestrador, passando state antigo
329
+ for h, dna, logs, confext_new in orquestrador(texto, arquivo, history, json_cfg, confext_old):
330
+ yield h, dna, logs, confext_new
331
+
332
  triggers = [btn_send.click, txt_in.submit]
333
+
334
  for trig in triggers:
335
  trig(
336
+ _orq_wrapper,
337
+ inputs=[txt_in, file_in, chatbot, code_json, confext_state],
338
+ outputs=[chatbot, out_dna, out_logs, confext_state],
339
+ ).then(
340
+ lambda c: (None, None, "", c),
341
+ inputs=confext_state,
342
+ outputs=[txt_in, file_in, file_status, confext_state],
343
  ).then(
344
+ lambda c: c,
345
+ inputs=confext_state,
346
+ outputs=confext_view,
347
  )
348
+
349
  return app
350
 
351
+
352
  if __name__ == "__main__":
353
  ui_clean().launch()