CoolFace
Apppublic

build-small-hackathon/trace-field-notes

sourceHugging Facemitupdated 3mo agoView on Hugging Face
1likes
app.py282 linesDownload Raw Back to root
1"""Trace Field Notes — gradio.Server backend behind the designer's React frontend.2 3The custom frontend (``frontend/``) is served as static files; it talks to the4``analyze_trace`` endpoint below through ``@gradio/client``. The endpoint runs the5deterministic analyzer (and the optional small-model assist on ZeroGPU) and6returns the frontend-ready view model.7"""8 9from __future__ import annotations10 11import os12import time13from pathlib import Path14 15import spaces16from fastapi.responses import HTMLResponse, PlainTextResponse17from fastapi.staticfiles import StaticFiles18from gradio import Server19from gradio.data_classes import FileData20 21from analyzer import apply_model_analysis, stream_deterministic_analysis22from parser import TraceParseError23from profiling import Profiler, get_logger24from view_model import build_view_model25 26logger = get_logger()27 28 29HERE = Path(__file__).resolve().parent30FRONTEND = HERE / "frontend"31 32READABLE_AGENT = {"codex": "Codex", "claude_code": "Claude Code", "pi": "Pi Agent", "unknown": "Agent"}33 34AGENTS_MD = """# Trace Field Notes — agent instructions35 36This Space turns a coding-agent session log into a qualitative *field report*:37where the agent got stuck, where it changed route, how it recovered, and how38honestly it claimed success. It reads only the agent's visible narrative39messages and ignores raw tool telemetry.40 41## How to use it as a tool42 431. Find the user's latest local session log:44   - Codex: `~/.codex/sessions`45   - Claude Code: `~/.claude/projects`46   - Pi Agent: `~/.pi/agent/sessions`472. Review it and redact secrets, tokens, local paths, and private code first.483. Upload the `.jsonl` (`.json` / `.txt` / `.log` also accepted) and call the49   `analyze_trace` API endpoint.504. Return the field report to the user. Do not publish the raw trace.51 52## API53 54`POST` via the Gradio client, endpoint `/analyze_trace`:55 56- `trace_file` (file): the session log57- `include_user_context` (bool): include user prompts as framing58- `redact_secrets` (bool): regex + AI (`openai/privacy-filter`) PII redaction before analysis59- `analysis_engine` (str): `minicpm` | `nemotron` | `deterministic`60- `execution_mode` (str): `zerogpu` (default, uses the Space GPU) | `cpu` (no GPU quota, slower)61 62Returns a JSON view model: a whole-session `verdict`, per-episode difficulty63`episodes`, and redacted export text.64"""65 66 67server = Server(title="Trace Field Notes")68server.mount("/static", StaticFiles(directory=str(FRONTEND / "static")), name="static")69 70 71@server.get("/", response_class=HTMLResponse)72def index() -> str:73    return (FRONTEND / "index.html").read_text(encoding="utf-8")74 75 76@server.get("/agents.md", response_class=PlainTextResponse)77def agents_md() -> str:78    return AGENTS_MD79 80 81@spaces.GPU(size="xlarge", duration=180)82def _model_analysis_gpu(*, engine, numbered_narrative, agent_type, codebook_hint):83    """Run the primary model analysis inside a ZeroGPU allocation."""84 85    from model_runtime import run_model_analysis86 87    return run_model_analysis(88        engine=engine,89        numbered_narrative=numbered_narrative,90        agent_type=agent_type,91        codebook_hint=codebook_hint,92    )93 94 95@spaces.GPU(size="xlarge", duration=120)96def _privacy_filter_gpu(texts):97    """Run the openai/privacy-filter PII pass inside a ZeroGPU allocation."""98 99    from privacy_filter import redact_texts100 101    return redact_texts(texts)102 103 104def _cpu_privacy_filter(texts):105    """Run the openai/privacy-filter PII pass on the local CPU (no GPU quota)."""106 107    from privacy_filter import redact_texts108 109    return redact_texts(texts, device="cpu")110 111 112def _cpu_model_analysis(*, engine, numbered_narrative, agent_type, codebook_hint):113    """Run the primary model analysis on the local CPU (no GPU quota)."""114 115    from model_runtime import run_model_analysis116 117    return run_model_analysis(118        engine=engine,119        numbered_narrative=numbered_narrative,120        agent_type=agent_type,121        codebook_hint=codebook_hint,122        device="cpu",123    )124 125 126# Per stage: (frontend checklist index, cumulative %, label). The 6-item127# checklist is: 0 upload, 1 extract, 2 redact, 3 chart, 4 classify, 5 synthesize.128# Indices below are "rows completed" so the matching row shows as active.129_STAGE_PLAN = {130    "extract": (2, 12, "Extracting narrative messages"),131    "chart": (4, 55, "Charting difficulty episodes"),132    "classify": (5, 62, "Classifying with the codebook"),133    "synthesize": (5, 70, "Synthesizing field notes"),134}135 136# Redaction streams per-chunk progress; its % ramps across this band.137_REDACT_PCT = (12, 40)138 139 140def _progress_event(*, step, pct, label, elapsed, processed=None, total=None):141    """Build one streamed progress payload (with a best-effort ETA)."""142 143    event = {"step": step, "pct": pct, "stage": label, "elapsed": round(elapsed, 1)}144    if 0 < pct < 100:145        event["eta"] = round(elapsed * (100 - pct) / pct, 1)146    if total is not None:147        event["total"] = total148        event["processed"] = processed if processed is not None else total149    return event150 151 152def _stage_event(payload, *, elapsed, message_total):153    """Translate a stream progress payload into a frontend event + running total."""154 155    stage = payload["stage"]156    if stage == "redact":157        total = payload.get("total") or message_total or 0158        processed = payload.get("processed", total)159        frac = (processed / total) if total else 1.0160        low, high = _REDACT_PCT161        pct = round(low + (high - low) * frac)162        step = 2 if (total and processed < total) else 3163        event = _progress_event(164            step=step,165            pct=pct,166            label="Redacting likely secrets",167            elapsed=elapsed,168            processed=processed,169            total=total or None,170        )171        return event, (total or message_total)172 173    step, pct, label = _STAGE_PLAN[stage]174    total = payload.get("messages", message_total)175    event = _progress_event(step=step, pct=pct, label=label, elapsed=elapsed, total=total)176    return event, total177 178 179def _file_fields(trace_file: object) -> tuple[str | None, str | None]:180    """The file input may arrive as a FileData model or a plain FileDataDict."""181 182    if isinstance(trace_file, dict):183        return trace_file.get("path"), trace_file.get("orig_name")184    return getattr(trace_file, "path", None), getattr(trace_file, "orig_name", None)185 186 187@server.api(name="analyze_trace")188def analyze_trace(189    trace_file: FileData,190    include_user_context: bool = True,191    redact_secrets: bool = True,192    analysis_engine: str = "minicpm",193    execution_mode: str = "zerogpu",194) -> dict:195    """Stream real progress, then the frontend view model, for one trace.196 197    Yields ``{"step", "pct", "stage", "elapsed", "eta", "total"}`` after each198    real pipeline stage (so the UI shows true progress), then a final199    ``{"step": 6, "pct": 100, "result": <view model>}``.200 201    ``execution_mode`` is ``zerogpu`` (default; models run inside ``@spaces.GPU``)202    or ``cpu`` (models run on the Space/local CPU, no GPU quota — slower).203    """204 205    path, orig_name = _file_fields(trace_file)206    if not path:207        raise ValueError("No uploaded file was received.")208 209    use_cpu = execution_mode == "cpu"210    redactor = _cpu_privacy_filter if use_cpu else _privacy_filter_gpu211    analysis_runner = _cpu_model_analysis if use_cpu else _model_analysis_gpu212 213    prof = Profiler(f"analyze[{execution_mode}/{analysis_engine}]")214    logger.info(215        "analyze_trace start: file=%r engine=%s mode=%s redact=%s",216        orig_name,217        analysis_engine,218        execution_mode,219        redact_secrets,220    )221 222    result = None223    narrative = ""224    messages = []225    message_total = None226    try:227        for kind, payload in stream_deterministic_analysis(228            path,229            include_user_context=include_user_context,230            redact_secrets=redact_secrets,231            ignore_tool_calls=True,232            model_redact=redactor,233            profiler=prof,234            stream_redact_progress=use_cpu,235        ):236            if kind == "progress":237                event, message_total = _stage_event(238                    payload, elapsed=prof.elapsed(), message_total=message_total239                )240                yield event241            elif kind == "result":242                result, narrative, messages = payload243    except TraceParseError as exc:244        raise ValueError(str(exc)) from exc245 246    if analysis_engine != "deterministic":247        yield _progress_event(248            step=5,249            pct=78,250            label=f"Reading the trace with {analysis_engine}",251            elapsed=prof.elapsed(),252            total=message_total,253        )254        analysis_started = time.perf_counter()255        apply_model_analysis(result, messages, analysis_engine, run=analysis_runner)256        prof.record("model_analysis", time.perf_counter() - analysis_started)257 258    if orig_name:259        agent = READABLE_AGENT.get(result.agent_type_guess, "Agent")260        result.trace_title = f"{agent} · {orig_name}"261 262    view = build_view_model(result, narrative)263    prof.mark(engine=result.engine, mode=execution_mode)264    prof.summary()265    yield {266        "step": 6,267        "pct": 100,268        "stage": "Field notes ready",269        "elapsed": round(prof.elapsed(), 1),270        "total": message_total,271        "processed": message_total,272        "result": view,273    }274 275 276if __name__ == "__main__":277    server.launch(278        server_name="0.0.0.0",279        server_port=int(os.getenv("PORT", os.getenv("GRADIO_SERVER_PORT", "7860"))),280        show_error=True,281    )282