""" Operon LangGraph Visualizer -- Per-Stage Graph Topology ======================================================== Build a multi-stage organism, compile it to a per-stage LangGraph, visualize the graph topology, and run it to see which stages execute. Run locally: pip install gradio && python space-langgraph-visualizer/app.py """ import html as html_mod import sys from pathlib import Path import gradio as gr try: _repo_root = Path(__file__).resolve().parents[2] if str(_repo_root) not in sys.path: sys.path.insert(0, str(_repo_root)) except IndexError: pass # Running on HF — operon-ai installed via requirements.txt from operon_ai import ATP_Store, MockProvider, Nucleus, SkillStage, skill_organism from operon_ai.convergence.langgraph_compiler import ( organism_to_langgraph, run_organism_langgraph, ) # --------------------------------------------------------------------------- # Presets # --------------------------------------------------------------------------- PRESETS = { "3-stage pipeline": { "stages": [ ("intake", "Normalizer", "fixed"), ("router", "Classifier", "fixed"), ("executor", "Engineer", "fuzzy"), ], "task": "Fix the login crash after session timeout", }, "4-stage incident": { "stages": [ ("triage", "Triager", "fixed"), ("classify", "Classifier", "fixed"), ("investigate", "Investigator", "fixed"), ("fix", "Engineer", "fuzzy"), ], "task": "Production auth failures after JWT migration", }, "2-stage simple": { "stages": [ ("analyze", "Analyst", "fixed"), ("respond", "Responder", "fuzzy"), ], "task": "Summarize the quarterly report", }, "5-stage deep": { "stages": [ ("intake", "Normalizer", "fixed"), ("triage", "Triager", "fixed"), ("plan", "Planner", "fuzzy"), ("execute", "Engineer", "deep"), ("review", "Reviewer", "fixed"), ], "task": "Refactor the authentication middleware for SOC2 compliance", }, } # --------------------------------------------------------------------------- # Visualization # --------------------------------------------------------------------------- def _node_html(name, mode, index, total, executed=False, is_start=False, is_end=False): """Render a single graph node.""" if is_start: return ( '
' '
START
') if is_end: return ( '
' '
END
') mode_colors = {"fixed": "#3b82f6", "fuzzy": "#f59e0b", "deep": "#8b5cf6"} border_color = mode_colors.get(mode, "#6b7280") bg = f"{border_color}15" check = ' ' if executed else "" return ( f'
' f'
' f'
{html_mod.escape(name)}{check}
' f'
mode: {html_mod.escape(mode)}
' f'
') def _arrow_html(label="continue"): color = "#22c55e" if label == "continue" else "#ef4444" return ( f'
' f'
' f'
{label}
') def _halt_arrow_html(): return ( '
' '
' '
halt
') def build_graph_html(stages_info, executed_stages=None): """Build an HTML visualization of the per-stage graph.""" executed = set(executed_stages or []) n = len(stages_info) # Main flow: START → stages → END nodes = [] nodes.append(_node_html("", "", 0, n, is_start=True)) nodes.append(_arrow_html("")) for i, (name, _, mode) in enumerate(stages_info): nodes.append(_node_html(name, mode, i, n, executed=name in executed)) if i < n - 1: nodes.append(_arrow_html("continue")) else: nodes.append(_arrow_html("")) nodes.append(_node_html("", "", 0, n, is_end=True)) main_flow = ( '
' + "".join(nodes) + '
') # Halt edges legend halt_legend = ( '
' 'Each stage has a conditional edge: ' 'continue → next stage, ' 'halt/blocked → END' '
') return main_flow + halt_legend # --------------------------------------------------------------------------- # Core logic # --------------------------------------------------------------------------- def _parse_stages(stages_text): """Parse stages from text format: name, role, mode (one per line).""" stages = [] for line in stages_text.strip().split("\n"): line = line.strip() if not line: continue parts = [p.strip() for p in line.split(",")] if len(parts) >= 3: stages.append((parts[0], parts[1], parts[2])) elif len(parts) == 2: stages.append((parts[0], parts[1], "fixed")) elif len(parts) == 1: stages.append((parts[0], parts[0].title(), "fixed")) return stages def visualize_and_run(stages_text, task, do_run): if not stages_text.strip(): return "

Enter at least one stage.

", "" stages_info = _parse_stages(stages_text) if not stages_info: return "

Could not parse stages. Use format: name, role, mode

", "" # Build organism with deterministic handlers (avoids MockProvider # substring-matching collisions across stages) def _make_handler(stage_name, stage_role): def handler(task, state, outputs, stage): return f"[{stage_role}] Processed: task complete." return handler fast = Nucleus(provider=MockProvider(responses={})) deep = Nucleus(provider=MockProvider(responses={})) org = skill_organism( stages=[ SkillStage(name=name, role=role, handler=_make_handler(name, role), mode=mode) for name, role, mode in stages_info ], fast_nucleus=fast, deep_nucleus=deep, budget=ATP_Store(budget=2000, silent=True), ) # Compile to LangGraph graph = organism_to_langgraph(org) all_nodes = list(graph.nodes.keys()) stage_nodes = [n for n in all_nodes if not n.startswith("__")] # Graph stats stats_html = ( f'
' f'Graph Stats: ' f'{len(stage_nodes)} stage nodes, ' f'{len(stage_nodes)} conditional edges (continue/halt), ' f'1 START edge, 1 terminal edge' f'
') # Visualize executed_stages = [] run_html = "" if do_run and task.strip(): result = run_organism_langgraph(org, task=task.strip()) executed_stages = result.metadata.get("stages_completed", []) # Build run results rows = "" for sr in result.stage_outputs.items(): name, output = sr preview = str(output)[:60] rows += ( f'' f'{html_mod.escape(name)}' f'{html_mod.escape(preview)}') cert_rows = "" for cv in result.certificates_verified: status = "HOLDS" if cv["holds"] else "FAILS" color = "#22c55e" if cv["holds"] else "#ef4444" cert_rows += ( f'' f'{cv["theorem"]}: {status}') run_html = ( f'
' f'
' f'Execution Results ' f'' f'({result.timing_ms:.1f} ms)
' f'
' f'' f'' f'' f'' f'{rows}
StageOutput
' f'
{cert_rows or "No certificates"}
' f'
' f'Halted: {result.metadata.get("halted", False)}
' f'
') graph_html = stats_html + build_graph_html(stages_info, executed_stages) return graph_html, run_html def load_preset(name): p = PRESETS.get(name) if not p: return "", "" lines = [f"{n}, {r}, {m}" for n, r, m in p["stages"]] return "\n".join(lines), p["task"] # --------------------------------------------------------------------------- # Gradio UI # --------------------------------------------------------------------------- def build_app() -> gr.Blocks: with gr.Blocks(title="Operon LangGraph Visualizer") as app: gr.Markdown( "# Operon LangGraph Visualizer\n" "Compile an organism to a **per-stage LangGraph** and visualize the " "graph topology. Each stage becomes a LangGraph node; conditional " "edges route based on continue/halt decisions.\n\n" "[GitHub](https://github.com/coredipper/operon) | " "[Paper](https://github.com/coredipper/operon/tree/main/article)") with gr.Row(): preset_dd = gr.Dropdown( choices=list(PRESETS.keys()), value="3-stage pipeline", label="Load Preset", scale=2) run_btn = gr.Button("Visualize & Run", variant="primary", scale=1) with gr.Row(): with gr.Column(scale=1): stages_input = gr.Textbox( value="intake, Normalizer, fixed\nrouter, Classifier, fixed\nexecutor, Engineer, fuzzy", label="Stages (name, role, mode -- one per line)", lines=6) with gr.Column(scale=1): task_input = gr.Textbox( value="Fix the login crash after session timeout", label="Task (for execution)", lines=2) do_run = gr.Checkbox(value=True, label="Execute after compiling") gr.Markdown("### Graph Topology") graph_output = gr.HTML() gr.Markdown("### Execution Results") run_output = gr.HTML() run_btn.click( fn=visualize_and_run, inputs=[stages_input, task_input, do_run], outputs=[graph_output, run_output]) preset_dd.change( fn=load_preset, inputs=[preset_dd], outputs=[stages_input, task_input]) return app if __name__ == "__main__": app = build_app() app.launch(theme=gr.themes.Soft())