Cristobal299 commited on
Commit
7cf0047
·
verified ·
1 Parent(s): eaf7f9c

Upload teste_app.py

Browse files
Files changed (1) hide show
  1. teste_app.py +245 -0
teste_app.py ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ import os
3
+ import json
4
+ import base64
5
+ import tempfile
6
+ from concurrent.futures import ThreadPoolExecutor
7
+
8
+ import gradio as gr
9
+ from openai import OpenAI
10
+ from playwright.sync_api import sync_playwright
11
+
12
+ GROQ_KEY = os.environ.get("GROQ_KEY", "")
13
+
14
+ # Modelo de vision de Groq (si cambia el nombre, ajustalo aqui).
15
+ VISION_MODEL = "meta-llama/llama-4-scout-17b-16e-instruct"
16
+ # Respaldo solo-texto si la vision falla.
17
+ TEXT_MODEL = "llama-3.3-70b-versatile"
18
+
19
+ _groq = OpenAI(api_key=GROQ_KEY, base_url="https://api.groq.com/openai/v1") if GROQ_KEY else None
20
+
21
+ # Playwright (API sync) debe correr en un hilo SIN loop asyncio. Usamos un
22
+ # executor propio para garantizarlo.
23
+ _executor = ThreadPoolExecutor(max_workers=1)
24
+
25
+
26
+ def _interactuar(pagina):
27
+ """Best-effort: rellena inputs/selects visibles y pulsa un boton de accion
28
+ real, para que el Tester vea la app DESPUES de usarla, no solo al cargar.
29
+ Devuelve el texto del primer error de Gradio detectado en el DOM tras el
30
+ clic, o "" si no se detecto ninguno."""
31
+ try:
32
+ inputs = pagina.locator("input[type=text], input[type=number], input:not([type]), textarea").all()
33
+ for inp in inputs[:6]:
34
+ try:
35
+ if not inp.is_visible():
36
+ continue
37
+ tipo = inp.get_attribute("type") or "text"
38
+ inp.fill("5" if tipo == "number" else "prueba")
39
+ except Exception:
40
+ continue
41
+ except Exception:
42
+ pass
43
+ try:
44
+ selects = pagina.locator("select").all()
45
+ for sel in selects[:4]:
46
+ try:
47
+ if not sel.is_visible():
48
+ continue
49
+ opciones = sel.locator("option").all()
50
+ if len(opciones) > 1:
51
+ valor = opciones[1].get_attribute("value")
52
+ if valor is not None:
53
+ sel.select_option(valor)
54
+ except Exception:
55
+ continue
56
+ except Exception:
57
+ pass
58
+
59
+ error_detectado = ""
60
+ try:
61
+ for btn in pagina.locator("button").all():
62
+ try:
63
+ if not btn.is_visible():
64
+ continue
65
+ texto_btn = (btn.inner_text() or "").strip().lower()
66
+ if any(p in texto_btn for p in ["clear", "limpiar", "borrar", "flag"]):
67
+ continue
68
+ btn.click(timeout=3000)
69
+ pagina.wait_for_timeout(3000)
70
+ try:
71
+ pagina.wait_for_load_state("networkidle", timeout=8000)
72
+ except Exception:
73
+ pass
74
+ error_detectado = _detectar_error_gradio(pagina)
75
+ break
76
+ except Exception:
77
+ continue
78
+ except Exception:
79
+ pass
80
+ return error_detectado
81
+
82
+
83
+ def _detectar_error_gradio(pagina):
84
+ """Busca en el DOM senales explicitas de error que Gradio muestra tras
85
+ fallar un event handler: el toast rojo de error, o un componente Label
86
+ cuyo contenido literal es 'Error' / 'error'. No depende del LLM."""
87
+ try:
88
+ # Toast de error de Gradio (aparece en la esquina al fallar un evento).
89
+ toasts = pagina.locator(".toast-body.error, [class*='toast'][class*='error']").all()
90
+ for t in toasts:
91
+ try:
92
+ if t.is_visible():
93
+ txt = (t.inner_text() or "").strip()
94
+ return txt or "Gradio mostro un toast de error tras la accion."
95
+ except Exception:
96
+ continue
97
+ except Exception:
98
+ pass
99
+ try:
100
+ # Componente Label/salida cuyo unico contenido es literalmente "Error".
101
+ candidatos = pagina.locator("text=/^Error$/i").all()
102
+ for c in candidatos:
103
+ try:
104
+ if c.is_visible():
105
+ return "Un componente de salida muestra literalmente 'Error' tras la accion."
106
+ except Exception:
107
+ continue
108
+ except Exception:
109
+ pass
110
+ return ""
111
+
112
+
113
+ def _abrir_y_capturar(url):
114
+ """Abre la URL, interactua con la app (rellena+clic), y devuelve
115
+ (texto_visible, ruta_captura, error_dom) DESPUES de esa interaccion.
116
+ error_dom es "" si no se detecto ningun error explicito de Gradio, o el
117
+ texto del error si si se detecto."""
118
+ with sync_playwright() as p:
119
+ navegador = p.chromium.launch(args=["--no-sandbox", "--disable-dev-shm-usage"])
120
+ pagina = navegador.new_page(viewport={"width": 1280, "height": 1600})
121
+ pagina.goto(url, wait_until="domcontentloaded", timeout=45000)
122
+ try:
123
+ pagina.wait_for_selector(".gradio-container, gradio-app, #root", timeout=15000)
124
+ except Exception:
125
+ pass
126
+ pagina.wait_for_timeout(2500)
127
+ error_dom = _interactuar(pagina)
128
+ pagina.wait_for_timeout(1000)
129
+ texto = pagina.inner_text("body")[:6000]
130
+ ruta = os.path.join(tempfile.gettempdir(), "captura.png")
131
+ pagina.screenshot(path=ruta, full_page=True)
132
+ navegador.close()
133
+ return texto, ruta, error_dom
134
+
135
+
136
+ def _abrir_seguro(url):
137
+ return _executor.submit(_abrir_y_capturar, url).result()
138
+
139
+
140
+ def _img_b64(ruta):
141
+ with open(ruta, "rb") as f:
142
+ return base64.b64encode(f.read()).decode("utf-8")
143
+
144
+
145
+ SYSTEM = (
146
+ "Eres un tester de control de calidad de webs y apps, estricto y desconfiado. "
147
+ "Te dan el OBJETIVO de una pagina, el texto visible y una captura tomada "
148
+ "DESPUES de rellenar campos y pulsar un boton de accion real (no es la "
149
+ "primera carga). Marca ok=false si: la pagina esta en blanco; el texto o "
150
+ "la captura muestran la palabra 'Error', un traceback, un mensaje en rojo, "
151
+ "o un recuadro/toast de error; falta alguna seccion pedida en el objetivo; "
152
+ "o la accion principal (el boton) no parece haber producido ningun "
153
+ "resultado visible. Ante la duda entre 'parece que funciona' y 'no estoy "
154
+ "seguro', responde ok=false: es preferible un falso negativo a aprobar "
155
+ "una app rota. "
156
+ 'Responde SOLO JSON: {"ok": true/false, "problemas": ["..."], "resumen": "..."}'
157
+ )
158
+
159
+
160
+ def _juzgar(objetivo, texto, ruta_img):
161
+ if _groq is None:
162
+ return '{"ok": false, "problemas": ["Falta GROQ_KEY en los secrets"], "resumen": "Sin clave"}'
163
+ base = f"OBJETIVO:\n{objetivo}\n\nTEXTO VISIBLE EN LA PAGINA:\n{texto}"
164
+ # 1) Intento con vision (incluye la captura)
165
+ try:
166
+ b64 = _img_b64(ruta_img)
167
+ resp = _groq.chat.completions.create(
168
+ model=VISION_MODEL,
169
+ messages=[
170
+ {"role": "system", "content": SYSTEM},
171
+ {"role": "user", "content": [
172
+ {"type": "text", "text": base},
173
+ {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64}"}},
174
+ ]},
175
+ ],
176
+ temperature=0,
177
+ )
178
+ return resp.choices[0].message.content
179
+ except Exception:
180
+ pass
181
+ # 2) Respaldo solo-texto
182
+ try:
183
+ resp = _groq.chat.completions.create(
184
+ model=TEXT_MODEL,
185
+ messages=[
186
+ {"role": "system", "content": SYSTEM},
187
+ {"role": "user", "content": base},
188
+ ],
189
+ temperature=0,
190
+ )
191
+ return resp.choices[0].message.content
192
+ except Exception as e:
193
+ return '{"ok": false, "problemas": ["Error del modelo: ' + str(e)[:120] + '"], "resumen": "Fallo al juzgar"}'
194
+
195
+
196
+ def probar(url, objetivo):
197
+ url = (url or "").strip()
198
+ if not url:
199
+ return "Pon una URL.", None
200
+ if not url.startswith("http"):
201
+ url = "https://" + url
202
+ try:
203
+ texto, ruta, error_dom = _abrir_seguro(url)
204
+ except Exception as e:
205
+ return f"No se pudo abrir la pagina: {e}", None
206
+
207
+ # Veto programatico: si el DOM muestra un error explicito de Gradio tras
208
+ # la interaccion, NO PASA sin importar lo que opine el LLM de vision.
209
+ if error_dom:
210
+ salida = (
211
+ f"### NO PASA\n\n"
212
+ f"La pagina mostro un error real al usarla (no solo al cargar).\n\n"
213
+ f"- {error_dom}"
214
+ )
215
+ return salida, ruta
216
+
217
+ veredicto = _juzgar(objetivo or "La pagina debe cargar sin errores y verse completa.", texto, ruta)
218
+ try:
219
+ limpio = veredicto.replace("```json", "").replace("```", "").strip()
220
+ d = json.loads(limpio)
221
+ estado = "PASA" if d.get("ok") else "NO PASA"
222
+ problemas = "\n".join(f"- {x}" for x in d.get("problemas", []))
223
+ salida = f"### {estado}\n\n{d.get('resumen','')}\n\n{problemas}".strip()
224
+ except Exception:
225
+ # No se pudo interpretar el veredicto del LLM como JSON valido.
226
+ # Por seguridad, NO se asume PASA: se marca como fallo explicito.
227
+ salida = (
228
+ f"### NO PASA\n\n"
229
+ f"El Tester no pudo interpretar su propio veredicto como JSON valido "
230
+ f"(respuesta cruda no concluyente).\n\n"
231
+ f"- Respuesta del modelo: {str(veredicto)[:300]}"
232
+ )
233
+ return salida, ruta
234
+
235
+
236
+ with gr.Blocks(title="Forja Tester") as demo:
237
+ gr.Markdown("# Forja Tester\nAbre una app en un navegador real, la mira y dice si funciona.")
238
+ url_in = gr.Textbox(label="URL de la app a probar")
239
+ obj_in = gr.Textbox(label="Que deberia hacer / contener", lines=3)
240
+ btn = gr.Button("Probar", variant="primary")
241
+ out = gr.Markdown()
242
+ img = gr.Image(label="Lo que vio el navegador")
243
+ btn.click(probar, [url_in, obj_in], [out, img], api_name="probar")
244
+
245
+ demo.launch(server_name="0.0.0.0", server_port=7860)