File size: 4,975 Bytes
bd351d2
 
 
 
 
 
 
849ee7b
 
 
bd351d2
849ee7b
 
6ac8ef6
bd351d2
 
 
 
849ee7b
ef0cd9b
849ee7b
bd351d2
849ee7b
 
bd351d2
 
849ee7b
bd351d2
849ee7b
bd351d2
840ab13
bd351d2
 
 
 
840ab13
bd351d2
849ee7b
bd351d2
 
 
 
 
 
 
 
849ee7b
bd351d2
849ee7b
bd351d2
 
 
 
 
 
849ee7b
bd351d2
 
849ee7b
 
 
bd351d2
 
849ee7b
 
bd351d2
 
 
849ee7b
 
bd351d2
 
 
 
 
 
ef0cd9b
fbb7c0c
ef0cd9b
 
 
 
 
 
 
 
fbb7c0c
6ac8ef6
 
accd68a
 
 
 
 
 
 
 
bd351d2
 
 
 
 
 
 
ef0cd9b
 
 
 
 
bd351d2
accd68a
 
 
ef0cd9b
 
 
bd351d2
ef0cd9b
 
 
 
 
 
 
 
 
 
bd351d2
 
849ee7b
ef0cd9b
 
 
accd68a
bd351d2
accd68a
bd351d2
ef0cd9b
849ee7b
 
 
bd351d2
 
 
 
 
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
"""Trace Field Notes — gradio.Server backend behind the designer's React frontend.

The custom frontend (``frontend/``) is served as static files; it talks to the
``analyze_trace`` endpoint below through ``@gradio/client``. The endpoint runs the
deterministic analyzer (and the optional small-model assist on ZeroGPU) and
returns the frontend-ready view model.
"""

from __future__ import annotations

import os
from pathlib import Path

import spaces
from fastapi.responses import HTMLResponse, PlainTextResponse
from fastapi.staticfiles import StaticFiles
from gradio import Server
from gradio.data_classes import FileData

from analyzer import apply_model_assist, stream_deterministic_analysis
from parser import TraceParseError
from view_model import build_view_model


HERE = Path(__file__).resolve().parent
FRONTEND = HERE / "frontend"

READABLE_AGENT = {"codex": "Codex", "claude_code": "Claude Code", "pi": "Pi Agent", "unknown": "Agent"}

AGENTS_MD = """# Trace Field Notes — agent instructions

This Space turns a coding-agent session log into a qualitative *field report*:
where the agent got stuck, where it changed route, how it recovered, and how
honestly it claimed success. It reads only the agent's visible narrative
messages and ignores raw tool telemetry.

## How to use it as a tool

1. Find the user's latest local session log:
   - Codex: `~/.codex/sessions`
   - Claude Code: `~/.claude/projects`
   - Pi Agent: `~/.pi/agent/sessions`
2. Review it and redact secrets, tokens, local paths, and private code first.
3. Upload the `.jsonl` (`.json` / `.txt` / `.log` also accepted) and call the
   `analyze_trace` API endpoint.
4. Return the field report to the user. Do not publish the raw trace.

## API

`POST` via the Gradio client, endpoint `/analyze_trace`:

- `trace_file` (file): the session log
- `include_user_context` (bool): include user prompts as framing
- `redact_secrets` (bool): redact likely secrets before analysis
- `analysis_engine` (str): `qwen` | `nemotron` | `deterministic`

Returns a JSON view model: a whole-session `verdict`, per-episode difficulty
`episodes`, and redacted export text.
"""


server = Server(title="Trace Field Notes")
server.mount("/static", StaticFiles(directory=str(FRONTEND / "static")), name="static")


@server.get("/", response_class=HTMLResponse)
def index() -> str:
    return (FRONTEND / "index.html").read_text(encoding="utf-8")


@server.get("/agents.md", response_class=PlainTextResponse)
def agents_md() -> str:
    return AGENTS_MD


@spaces.GPU(size="xlarge", duration=180)
def _model_assist_gpu(*, engine, result, narrative_text):
    """Run model assist inside a ZeroGPU allocation."""

    from model_runtime import run_model_assist

    return run_model_assist(engine=engine, result=result, narrative_text=narrative_text)


# completed-step count for the frontend's 6-item checklist
# (item 0 "uploading" is done once the request reaches us).
_STEP_COUNT = {"extract": 2, "redact": 3, "chart": 4, "classify": 5, "synthesize": 6}


def _file_fields(trace_file: object) -> tuple[str | None, str | None]:
    """The file input may arrive as a FileData model or a plain FileDataDict."""

    if isinstance(trace_file, dict):
        return trace_file.get("path"), trace_file.get("orig_name")
    return getattr(trace_file, "path", None), getattr(trace_file, "orig_name", None)


@server.api(name="analyze_trace")
def analyze_trace(
    trace_file: FileData,
    include_user_context: bool = True,
    redact_secrets: bool = True,
    analysis_engine: str = "qwen",
) -> dict:
    """Stream real progress, then the frontend view model, for one trace.

    Yields ``{"step": n}`` after each real pipeline stage (so the UI checklist
    tracks actual work), then a final ``{"step": 6, "result": <view model>}``.
    """

    path, orig_name = _file_fields(trace_file)
    if not path:
        raise ValueError("No uploaded file was received.")

    result = None
    narrative = ""
    try:
        for kind, payload in stream_deterministic_analysis(
            path,
            include_user_context=include_user_context,
            redact_secrets=redact_secrets,
            ignore_tool_calls=True,
        ):
            if kind == "step":
                yield {"step": _STEP_COUNT[payload]}
            elif kind == "result":
                result, narrative = payload
    except TraceParseError as exc:
        raise ValueError(str(exc)) from exc

    if analysis_engine != "deterministic":
        apply_model_assist(result, narrative, analysis_engine, run=_model_assist_gpu)

    if orig_name:
        agent = READABLE_AGENT.get(result.agent_type_guess, "Agent")
        result.trace_title = f"{agent} · {orig_name}"

    yield {"step": 6, "result": build_view_model(result, narrative)}


if __name__ == "__main__":
    server.launch(
        server_name="0.0.0.0",
        server_port=int(os.getenv("PORT", os.getenv("GRADIO_SERVER_PORT", "7860"))),
        show_error=True,
    )