File size: 11,052 Bytes
5d5b899 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 | """Demo Gradio (docs/PLAN_RAG.md Fase 6, tarea 2): pestaña "buscar" (hits con citas,
firma Lean, docs_url, recorte y descripción de figura) y pestaña "resolver" (el sistema
actual: demostrador local en la GPU como etapa 0, verificación con el kernel de Lean).
Local: bash scripts/demo_up.sh (levanta el demostrador en la GPU y esta demo; Lean y el
embedder se precalientan en segundo plano al arrancar, ~5 min)
Space: el índice se descarga con `snapshot_download` si RAGSOLVER_HF_DATASET está definido.
"""
from __future__ import annotations
import json
import os
import re
import shutil
import tempfile
import threading
from functools import lru_cache
from pathlib import Path
import gradio as gr
DB_PATH = os.environ.get("RAGSOLVER_DB", "data/index/lancedb")
_LOOPBACK_HOSTS = {"127.0.0.1", "localhost", "::1"}
_STATEMENT_RE = re.compile(r"^\s*(?:theorem|lemma)\s+[A-Za-z_][\w'.]*.*:=\s*by\s+sorry\s*$", re.S)
MINIF2F_HEADER = "open BigOperators Real Nat Topology Rat"
# Problemas que el sistema resolvió en eval-v3: sus muestras del demostrador están en la caché de disco,
# así que en una presentación salen en segundos (la verificación de Lean sí corre en vivo).
EXAMPLES = ["mathd_algebra_67", "imo_1964_p1_1", "imo_1965_p1", "amc12b_2003_p17"]
_LOCK = threading.Lock()
def _examples() -> list[list[str]]:
path = Path("data/eval/problems/eval_v1.jsonl")
if not path.exists():
return []
rows = {d["problem_id"]: d for d in (json.loads(l) for l in path.read_text(encoding="utf-8").splitlines() if l.strip())}
return [[rows[i]["informal_statement"], rows[i]["formal_statement"], rows[i].get("header", "")] for i in EXAMPLES if i in rows]
def _ensure_index() -> str:
repo = os.environ.get("RAGSOLVER_HF_DATASET")
if repo and not Path(DB_PATH).exists():
from huggingface_hub import snapshot_download
local = snapshot_download(repo_id=repo, repo_type="dataset", allow_patterns=["index/**"])
return str(Path(local) / "index" / "lancedb")
return DB_PATH
@lru_cache(maxsize=1)
def _retriever():
from ragsolver.rag.retrieve import Retriever
r = Retriever(db_path=_ensure_index())
r.trace_dir = tempfile.mkdtemp(prefix="ragsolver-demo-")
return r
@lru_cache(maxsize=1)
def _base() -> dict:
from ragsolver.agents.agents import load_agents_config
return load_agents_config()
@lru_cache(maxsize=1)
def _new_checker():
from ragsolver.lean.verify import LeanChecker
return LeanChecker(imports=_base()["lean"]["imports"], timeout_s=_base()["lean"]["timeout_s"])
def _checker():
"""Un solo REPL de Lean para toda la sesión: `import Mathlib` tarda ~5 min y no se repite por problema
(el lock evita que el precalentamiento y un primer clic creen dos)."""
with _LOCK:
return _new_checker()
def _prover():
"""El demostrador local si su servidor responde (se vuelve a preguntar en cada problema: puede arrancar después)."""
from ragsolver.prover import local_prover
return local_prover(_base().get("prover") or {})
def _warmup() -> None:
"""Lean (`import Mathlib`) y el embedder en segundo plano, para que el primer problema no espere."""
try:
_checker().check("theorem _warmup : True := by sorry", "trivial")
print("[demo] Lean listo", flush=True)
r = _retriever()
if getattr(r, "versions", None):
from ragsolver.rag.embed import embed_queries
embed_queries(["warmup"], model_name=r.versions["embedding"]["model"])
print("[demo] embedder listo", flush=True)
except Exception as e: # sin Lean o sin índice la pestaña Buscar/Resolver lo dirá al usarse
print(f"[demo] precalentamiento omitido: {type(e).__name__}: {e}", flush=True)
def search(query: str, corpus: str, mode: str, k: int, rerank: bool):
from ragsolver.rag.schema import RetrievalRequest
if not query.strip():
return [], [], "Escribí una consulta."
try:
res = _retriever().search(RetrievalRequest(queries=[query], mode=mode, corpora=[corpus], k=int(k), rerank=rerank, context={"problem_id": "demo", "agent": "demo"}))
except (FileNotFoundError, ValueError) as e: # p.ej. el corpus informal no se publica (licencia) y no existe en el Space
return [], [], f"Corpus `{corpus}` no disponible aquí: {str(e)[:200]}"
rows, images = [], []
for h in res.hits:
c = h.citation
link = c.locator.get("docs_url") or ""
sig = (h.metadata.get("signature") or "")[:200] if corpus == "mathlib" else (h.metadata.get("context_summary") or "")[:200]
rows.append([c.citation_id, c.label, sig, round(c.scores.fused or 0, 4), round(c.scores.rerank, 3) if c.scores.rerank is not None else "", link, c.snippet[:200]])
for f in h.figures:
if f.get("image_path") and Path(f["image_path"]).exists():
images.append((f["image_path"], f"{c.citation_id} · {f['figure_id']}: {(f.get('description') or '')[:150]}"))
return rows, images, f"{len(res.hits)} hits · {res.trace.latency_ms:.0f} ms · 0 llamadas LLM"
def solve(informal: str, formal: str, header: str, config: str, k_max: int):
from ragsolver.agents.agents import Agents
from ragsolver.agents.schemas import Problem
from ragsolver.llm import LLM
from ragsolver.orchestrator.graph import Orchestrator
from ragsolver.orchestrator.run import drafter_agents, effective_config, run_baseline, run_prover_only
if not (shutil.which("lake") or (Path.home() / ".elan" / "bin" / "lake").exists()):
return "El modo **Resolver** necesita Lean 4 + Mathlib y una API key del LLM: sólo está disponible en una instalación local (`docs/install.md`).", "", [], "{}"
host = os.environ.get("GRADIO_SERVER_NAME", "127.0.0.1")
if host not in _LOOPBACK_HOSTS: # SEC-1: Lean ejecuta la salida del LLM sin sandbox (docs/architecture.md "Modelo de amenazas del verificador")
return f"El modo **Resolver** ejecuta código no confiable (Lean sin sandbox) y sólo corre si el servidor escucha en loopback; `GRADIO_SERVER_NAME={host}` lo expone a la red. Cambiá la variable para usar este modo.", "", [], "{}"
formal_statement = formal.strip() or "theorem demo : True := by sorry"
if not _STATEMENT_RE.match(formal_statement): # el usuario escribe el enunciado a mano en la demo: mismo formato que exige Problem
return "El enunciado formal debe empezar con `theorem <nombre>` o `lemma <nombre>` y terminar en `:= by sorry` (docs/user_guide.md §2).", "", [], "{}"
base = _base()
cfg = effective_config(base, config)
cfg["k_max"] = int(k_max) if config not in ("no_critic",) else 1
problem = Problem(problem_id="demo", informal_statement=informal, formal_statement=formal_statement, header=header.strip())
trace_dir = tempfile.mkdtemp(prefix="ragsolver-demo-")
agents = Agents(LLM(section="llm"), base, trace_dir=trace_dir)
checker, prover = _checker(), _prover()
if cfg["mode"] == "baseline_prover":
rep = run_prover_only(problem, cfg, checker, prover)
elif cfg["mode"].startswith("baseline"):
rep = run_baseline(cfg["mode"], problem, cfg, agents, "demo", checker)
else:
rep = Orchestrator(agents, _retriever() if cfg["rag"] else None, checker, prover, drafter_agents(base, trace_dir)).solve(problem, cfg, "demo")
cites = [[c.citation_id, c.label, "✔ prueba" if c.used_in_proof else ("boceto" if c.used_in_sketch else "consultada"), c.locator.get("docs_url") or ""] for c in rep.citations]
who = {"prover_root": "demostrador local (etapa 0, 0 API)", "prover_draft": "demostrador local con el borrador de la API (DRAFT)", "agents": "agentes por API"}.get(rep.solved_by, "")
head = f"**{rep.status.upper()}**" + (f" · resuelto por: {who}" if who else "") + f" · {rep.prover_samples} muestras del demostrador · {rep.llm_calls} llamadas LLM · {rep.wall_seconds}s · lemas verificados {rep.n_used_in_proof}/{rep.n_retrieved}"
if prover is None and (cfg.get("prover") or {}).get("enabled"):
head += "\n\n⚠ El demostrador local no responde (`bash scripts/demo_up.sh`): se usó el sistema sin él."
if rep.verdict and not rep.verdict.ok:
head += "\n\nErrores de Lean:\n```\n" + "\n".join(e["message"][:300] for e in rep.verdict.errors[:3]) + "\n```"
return head, rep.lean_code or "(sin código)", cites, json.dumps(rep.model_dump(exclude={"citations", "lean_code", "verdict"}), indent=2, ensure_ascii=False)
with gr.Blocks(title="RagSolver") as app:
gr.Markdown("# RagSolver — recuperación y resolución verificada (Lean 4 + Mathlib v4.28.0-rc1)")
with gr.Tab("Buscar"):
with gr.Row():
q = gr.Textbox(label="Consulta", placeholder="prove that 2^2001 + 3^2001 is divisible by 7", scale=4)
corpus = gr.Dropdown(["mathlib", "informal"], value="mathlib", label="Corpus")
mode = gr.Dropdown(["nl", "formal", "error", "exemplar"], value="nl", label="Modo")
k = gr.Slider(1, 20, value=8, step=1, label="k")
rerank = gr.Checkbox(value=False, label="rerank (cross-encoder, +2.4 GB RAM)")
btn = gr.Button("Buscar")
status = gr.Markdown()
table = gr.Dataframe(headers=["id", "cita", "firma / resumen", "fused", "rerank", "docs_url", "snippet"], wrap=True)
gallery = gr.Gallery(label="Figuras adjuntas", columns=4, height=260)
btn.click(search, [q, corpus, mode, k, rerank], [table, gallery, status])
with gr.Tab("Resolver"):
informal = gr.Textbox(label="Enunciado (lenguaje natural)", lines=3, value="For every natural number n, n + 0 = n.")
formal = gr.Textbox(label="Enunciado formal (Lean 4, termina en := by sorry)", lines=3, value="theorem demo (n : ℕ) : n + 0 = n := by sorry")
header = gr.Textbox(label="Cabecera (open …)", value=MINIF2F_HEADER)
with gr.Row():
config = gr.Dropdown(["full", "dsp", "baseline_prover", "no_rag", "baseline_a"], value="full", label="Configuración")
kmax = gr.Slider(1, 5, value=3, step=1, label="k_max (reintentos de los agentes por API)")
if _examples():
gr.Examples(_examples(), [informal, formal, header], label="Problemas de eval-v3 (muestras en caché: salen en segundos)")
run_btn = gr.Button("Resolver (demostrador local + kernel de Lean)")
out_head = gr.Markdown()
out_code = gr.Code(language=None, label="Lean 4 (aceptado por el kernel)")
out_cites = gr.Dataframe(headers=["id", "cita", "uso", "docs_url"], wrap=True)
out_json = gr.Code(language="json", label="SolutionReport")
run_btn.click(solve, [informal, formal, header, config, kmax], [out_head, out_code, out_cites, out_json])
if __name__ == "__main__":
if shutil.which("lake") or (Path.home() / ".elan" / "bin" / "lake").exists():
threading.Thread(target=_warmup, daemon=True).start()
app.launch(server_name=os.environ.get("GRADIO_SERVER_NAME", "127.0.0.1"), share=False)
|