gepa-viz: watching a GEPA prompt-optimization run as a force-directed tree
Interactive live visualizer for gepa runs
At a glance
- What is it?
- gepa-viz is a live visualizer for GEPA prompt-optimization runs, shipped as a pip package that bundles a React SPA. It is useful if you already run GEPA through DSPy or base gepa and want to see candidates, rejections and per-example scores as they happen.
- Who is it for?
- Adopt gepa-viz if your optimization loop already runs through DSPy or base gepa and you want per-example valset scores and rejection feedback visible while the run is still going. Skip it if you need a hosted dashboard, multi-user access, or anything the stdlib ThreadingHTTPServer cannot serve, and skip it if your pipeline is not GEPA at all, since the callback is wired to GEPA's callback interface.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 139 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 11, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What gepa-viz actually visualizes, and who it is for
GEPA optimizes prompts by proposing candidate prompts, evaluating them on a minibatch, reflecting on the failures, and retrying. The interesting information is not a single loss number. It is which candidates survived, which were rejected and why, and how each surviving candidate scored on each individual validation example. gepa-viz takes that stream and renders it as a force-directed graph: accepted candidates appear as donuts whose ring segments are green or red per per-example valset score, and rejected proposals appear as small grey nodes you can hover to read the feedback that produced them.
The intended user is someone already running GEPA, either through DSPy or through base gepa. The README shows the callback being passed as gepa_kwargs={"callbacks": [cb]} in the DSPy case and as callbacks=[cb] in the base gepa case. This is not a tool for inspecting arbitrary LLM pipelines, and it is not a general prompt-debugging UI. It is bound to GEPA's callback interface, and the shape of the graph follows GEPA's own candidate tree, including the id convention where accepted candidates are integer strings like "0" and "1" and rejected ones are "<parent>.<n>" with n starting at 1 per parent.
The three modes: embedded, remote and static
The callback is a context manager, and the mode is decided by how you enter it. In the default embedded mode, entering the with block spins up a local server, opens a browser tab, and the graph extends node by node over SSE as GEPA accepts and rejects proposals. Exiting dumps run.json and keeps the viewer alive until you press Ctrl+C. The README describes this as needing no second terminal and no CLI step.
Remote mode separates the producer from the viewer. You run one long-lived server with gepa-viz live --host 0.0.0.0, and point the callback at it with endpoint="http://127.0.0.1:5151". In that configuration the callback starts no server of its own. It POSTs snapshots to <endpoint>/ingest, and the server fans them out to connected browsers over SSE. This is the mode that makes sense when the optimizer runs on a different machine from the person watching.
Static mode is the headless one. Passing live=False makes the callback write run.json at the end without streaming, which is what you would use in CI. You then reopen the file later with gepa-viz serve --file run.json. The README is explicit that the static viewer loads once and does not poll, so a file opened this way will not update if something writes to it afterwards.
The server itself is a stdlib ThreadingHTTPServer. There is no Node runtime and no extra dependency at view time, which is a real deployment advantage: the wheel ships the pre-built SPA inside gepa_viz/static/, so the end user never runs npm.
Install and a first run
The end-user path is a single pip install. The wheel bundles the compiled React SPA, so nothing else is needed on the machine doing the viewing.
pip install gepa-vizAfter that, the smallest useful integration is the embedded callback. The README gives this example for DSPy, where the callback is handed to GEPA through gepa_kwargs. Entering the context manager starts the server and opens a tab; the graph should begin extending as soon as GEPA starts proposing candidates.
from gepa_viz import GepaVizCallback
with GepaVizCallback(valset=valset, trainset=trainset) as cb:
dspy.GEPA(
metric=..., auto="light", reflection_lm=...,
gepa_kwargs={"callbacks": [cb]},
).compile(student, trainset=trainset, valset=valset)If you would rather keep the viewer in a separate process, start the live server first. The README states it serves the SPA, /events and /ingest on port 5151.
gepa-viz liveThen pass endpoint= to the callback so it posts snapshots instead of starting its own server. The default endpoint_timeout is 5.0 seconds, which is the value to raise if the viewer machine is slow to acknowledge.
with GepaVizCallback(valset=valset, trainset=trainset,
endpoint="http://127.0.0.1:5151") as cb:
dspy.GEPA(..., gepa_kwargs={"callbacks": [cb]}).compile(student, ...)For a run that should not open a browser at all, the README shows live=False together with path="run.json", followed later by gepa-viz serve --file run.json. The full option set documented for GepaVizCallback is path, trainset, live (default True), host (127.0.0.1), port (5151), open_browser (True), keep_alive (True), endpoint (None) and endpoint_timeout (5.0).
Reading a node: valset grid, prompt diff and minibatch feedback
Clicking a node opens a detail view with three parts. The valset results grid at the top shows the candidate's response to each example and whether it was right or wrong. The prompt section shows the candidate prompt, with a toggle switch that reveals the diff against its parent. The minibatch section shows the results for that candidate, and the diffs there are between the initial attempt and the post-reflection attempt on the minibatch.
The distinction between big and small nodes is the part worth understanding before you interpret a graph. Big nodes are candidates that were evaluated on the valset, so they carry per-example scores and can sit on the pareto frontier. Small grey nodes are candidates rejected at the minibatch phase: after reflection the minibatch was retried and the score did not go up, so they were never evaluated on the valset and are not on the frontier. You can still open their detail pages. Hovering an edge shows the feedback that was given to the reflection model to generate the child candidate, which is often the fastest way to understand why a branch of the tree went the way it did.
The README's own example of why the ring view matters is a pair of candidates with the same accuracy that performed well on different examples. A single aggregate score would make those two look identical. The segmented ring does not.
The run.json schema is the integration surface
The callback writes a JSON file, and the SPA reads from it. That means the schema is not just an artifact format; it is the contract between the producer and the viewer, and the README documents it well enough that you can hand-craft a run.json for offline browsing without running GEPA at all. The top level has examples and candidates. Each candidate carries prompt, parent, score, predictions and minibatch. Accepted candidates have a numeric score and a predictions array; rejected ones have score null, predictions null, and a minibatch array whose entries hold example, parent_prediction, parent_score, prediction, score and feedback.
The id convention is load-bearing. Accepted ids are integer strings matching gepa's internal index. Rejected ids are "<parent>.<n>" with n starting at 1 per parent. If you generate a run.json by hand or transform one from another tool, getting those ids wrong will produce a tree that either fails to connect or connects to the wrong parent. The README does not document a schema version field, and it does not describe how the format will change between releases. Given that v0.1.0 landed on 2026-05-24 and the last push was on 2026-05-26, there is no release history yet to infer a compatibility policy from. Treat the schema as stable enough to read and too young to depend on blindly.
Where gepa-viz is the wrong tool
The most obvious limitation is that it only knows about GEPA. If your optimization loop is a hand-rolled evolutionary search, an Optuna study, or a DSPy optimizer other than GEPA, there is no callback to attach and nothing to visualize. The schema gives you a manual escape hatch, since you can write a run.json yourself, but you would be reimplementing the producer side rather than using the project.
The second limitation is the server. It is a stdlib ThreadingHTTPServer, which is a deliberate trade-off: zero runtime dependencies at view time, at the cost of everything a production web server gives you. The README does not document authentication, TLS, or any access control on /ingest or /events. Running gepa-viz live --host 0.0.0.0 exposes those endpoints on the network, and the documentation is silent on what protects them. If the optimizer and the viewer are on machines you do not both control, that is a real consideration before you bind to a non-loopback address.
Third, the embedded mode keeps the viewer alive until Ctrl+C after the run exits. That is convenient interactively and awkward in any automated context, which is presumably why live=False exists. If you want a run to finish and the process to exit cleanly without a signal, the static path is the one to use.
Finally, the README does not document rollback, retention, or what happens if two producers post to the same live server. The repository layout shows a client/ directory with a Vite SPA and a python/ package, and the justfile shows the build pipeline, but nothing in the documentation describes concurrency semantics for multiple ingesting callbacks.
Alternatives and the difference in approach
The nearest alternative is not another visualizer; it is reading the run's output directly. GEPA already produces candidates, scores and feedback, and the run.json that gepa-viz dumps is a readable artifact. If your question is "which candidate won", a script over that file answers it without a browser. What the file does not give you cheaply is the shape of the search: which branches died at the minibatch stage, what feedback killed them, and which examples split two equally accurate candidates. That is the specific gap gepa-viz fills, and it is the reason to add a viewer rather than a better log parser.
The other comparison worth making is against generic experiment trackers. Those are built around scalar metrics over time and expect you to log numbers as your code runs. gepa-viz is built around a tree of prompts with per-example outcomes and a pareto frontier rendered as a clickable pixel grid. The data model is different, which is why the callback exists at all rather than a few logging calls. If your work is prompt evolution over a valset, the tree-shaped model is the one that matches the problem.
If you only need the end state of a run and not the process, the static viewer plus a hand-written run.json is the lightest path, and it does not require GEPA to be installed on the viewing machine.
Maintenance, licence and upgrade cost
gepa-viz is MIT licensed, and the LICENSE file sits at the repository root. MIT is permissive, so bundling the wheel into a larger product is not the kind of thing that requires a legal review of copyleft obligations. That is not legal advice, and the usual caveat applies: read the licence text yourself if the distinction matters to your organisation.
The maintenance picture is short. There is one release, v0.1.0, published on 2026-05-24, and the last push to the repository was on 2026-05-26. The repository is not archived. With a single release and no documented deprecation or migration policy, the upgrade cost is mostly about the run.json schema. If you build tooling that consumes that file, pin the gepa-viz version you generate it with and diff the schema when you move. The README also does not state which GEPA versions the callback is compatible with, so the GEPA side of the pin is equally worth recording.
On the build side, the justfile shows the full pipeline: just install runs npm install in client/ and uv sync in python/, just build runs sync-static and uv build to produce python/dist/*.whl. That matters only if you are building from source. For end users the wheel already contains the SPA under gepa_viz/static/, and the justfile's sync-static recipe explicitly removes run.json from the shipped static directory, so a dev fallback file does not end up in the package.
Editorial conclusion
Adopt gepa-viz if your optimization loop already runs through DSPy or base gepa and you want per-example valset scores and rejection feedback visible while the run is still going. Skip it if you need a hosted dashboard, multi-user access, or anything the stdlib ThreadingHTTPServer cannot serve, and skip it if your pipeline is not GEPA at all, since the callback is wired to GEPA's callback interface. Before committing, verify three things: that your GEPA version accepts a callbacks list, that the port you pass to GepaVizCallback is free on the machine that will host the viewer, and that the run.json the callback dumps at exit matches the schema your tooling expects, because the README documents the shape but not any migration guarantees between releases.
Frequently asked questions
Does gepa-viz require Node.js to view a run?
No. The end-user wheel ships the pre-built SPA inside gepa_viz/static/, and the server is a stdlib ThreadingHTTPServer, so no Node runtime is needed at view time. Node and npm are only prerequisites for developing the client.
Can I open a gepa-viz run without running GEPA again?
Yes. The callback dumps a run.json artifact, and gepa-viz serve --file run.json opens it in the static viewer, which loads once and does not poll. The README also notes you can hand-craft a run.json for offline browsing.
What is the difference between a big node and a small grey node in gepa-viz?
Big nodes are candidates that were evaluated on the valset, so their rings carry per-example green and red segments. Small grey nodes were rejected at the minibatch phase, where the retry after reflection did not improve the score, so they were never evaluated on the valset and are not on the pareto frontier.
Which port does gepa-viz use by default?
The documented default port is 5151, and the default host is 127.0.0.1. The README states that gepa-viz live serves the SPA, /events and /ingest on that port.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/modaic-ai-gepa-viz)