coredipper commited on
Commit
1ebd476
·
verified ·
1 Parent(s): 3ad54b3

Initial deploy: LangGraph Visualizer

Browse files
Files changed (4) hide show
  1. README.md +32 -6
  2. __pycache__/app.cpython-311.pyc +0 -0
  3. app.py +332 -0
  4. requirements.txt +3 -0
README.md CHANGED
@@ -1,12 +1,38 @@
1
  ---
2
- title: Operon Langgraph Visualizer
3
- emoji: 👀
4
- colorFrom: red
5
- colorTo: purple
6
  sdk: gradio
7
- sdk_version: 6.12.0
8
  app_file: app.py
9
  pinned: false
 
 
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Operon LangGraph Visualizer
3
+ emoji: "\U0001F4CA"
4
+ colorFrom: blue
5
+ colorTo: green
6
  sdk: gradio
7
+ sdk_version: "6.5.1"
8
  app_file: app.py
9
  pinned: false
10
+ license: mit
11
+ short_description: Visualize per-stage LangGraph compilation
12
  ---
13
 
14
+ # Operon LangGraph Visualizer
15
+
16
+ Compile an organism to a **per-stage LangGraph** and visualize the graph topology. Each organism stage becomes a LangGraph node with conditional edges that route based on continue/halt decisions.
17
+
18
+ ## What to Try
19
+
20
+ 1. Click **Visualize & Run** with defaults to see a 3-stage graph with execution results.
21
+ 2. Try the "4-stage incident" preset for a longer pipeline.
22
+ 3. Try the "5-stage deep" preset to see how deep-mode stages appear in the graph.
23
+ 4. Edit stages directly (name, role, mode -- one per line) to build custom topologies.
24
+ 5. Uncheck "Execute after compiling" to see the topology without running.
25
+
26
+ ## How It Works
27
+
28
+ `organism_to_langgraph()` creates one LangGraph node per `SkillStage`. Each node calls `organism.run_single_stage()`, so all structural guarantees (certificates, watcher interventions, halt-on-block) are handled by the organism. LangGraph provides the execution host, graph topology, observability, and checkpointing.
29
+
30
+ ## Graph Topology
31
+
32
+ - **START** -> stage_1 -> stage_2 -> ... -> stage_N -> **END**
33
+ - Each stage has a conditional edge: `continue` -> next stage, `halt`/`blocked` -> END
34
+ - Stage colors indicate mode: blue = fixed, amber = fuzzy, purple = deep
35
+
36
+ ## Learn More
37
+
38
+ [GitHub](https://github.com/coredipper/operon) | [PyPI](https://pypi.org/project/operon-ai/) | [Paper](https://github.com/coredipper/operon/tree/main/article)
__pycache__/app.cpython-311.pyc ADDED
Binary file (16.8 kB). View file
 
app.py ADDED
@@ -0,0 +1,332 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Operon LangGraph Visualizer -- Per-Stage Graph Topology
3
+ ========================================================
4
+
5
+ Build a multi-stage organism, compile it to a per-stage LangGraph,
6
+ visualize the graph topology, and run it to see which stages execute.
7
+
8
+ Run locally: pip install gradio && python space-langgraph-visualizer/app.py
9
+ """
10
+
11
+ import html as html_mod
12
+ import sys
13
+ from pathlib import Path
14
+
15
+ import gradio as gr
16
+
17
+ _repo_root = Path(__file__).resolve().parents[2]
18
+ if str(_repo_root) not in sys.path:
19
+ sys.path.insert(0, str(_repo_root))
20
+
21
+ from operon_ai import ATP_Store, MockProvider, Nucleus, SkillStage, skill_organism
22
+ from operon_ai.convergence.langgraph_compiler import (
23
+ organism_to_langgraph,
24
+ run_organism_langgraph,
25
+ )
26
+
27
+ # ---------------------------------------------------------------------------
28
+ # Presets
29
+ # ---------------------------------------------------------------------------
30
+
31
+ PRESETS = {
32
+ "3-stage pipeline": {
33
+ "stages": [
34
+ ("intake", "Normalizer", "fixed"),
35
+ ("router", "Classifier", "fixed"),
36
+ ("executor", "Engineer", "fuzzy"),
37
+ ],
38
+ "task": "Fix the login crash after session timeout",
39
+ },
40
+ "4-stage incident": {
41
+ "stages": [
42
+ ("triage", "Triager", "fixed"),
43
+ ("classify", "Classifier", "fixed"),
44
+ ("investigate", "Investigator", "fixed"),
45
+ ("fix", "Engineer", "fuzzy"),
46
+ ],
47
+ "task": "Production auth failures after JWT migration",
48
+ },
49
+ "2-stage simple": {
50
+ "stages": [
51
+ ("analyze", "Analyst", "fixed"),
52
+ ("respond", "Responder", "fuzzy"),
53
+ ],
54
+ "task": "Summarize the quarterly report",
55
+ },
56
+ "5-stage deep": {
57
+ "stages": [
58
+ ("intake", "Normalizer", "fixed"),
59
+ ("triage", "Triager", "fixed"),
60
+ ("plan", "Planner", "fuzzy"),
61
+ ("execute", "Engineer", "deep"),
62
+ ("review", "Reviewer", "fixed"),
63
+ ],
64
+ "task": "Refactor the authentication middleware for SOC2 compliance",
65
+ },
66
+ }
67
+
68
+ # ---------------------------------------------------------------------------
69
+ # Visualization
70
+ # ---------------------------------------------------------------------------
71
+
72
+ def _node_html(name, mode, index, total, executed=False, is_start=False, is_end=False):
73
+ """Render a single graph node."""
74
+ if is_start:
75
+ return (
76
+ '<div style="display:flex;flex-direction:column;align-items:center;">'
77
+ '<div style="width:40px;height:40px;border-radius:50%;background:#10b981;'
78
+ 'display:flex;align-items:center;justify-content:center;color:white;'
79
+ 'font-weight:700;font-size:0.8em;">START</div></div>')
80
+ if is_end:
81
+ return (
82
+ '<div style="display:flex;flex-direction:column;align-items:center;">'
83
+ '<div style="width:40px;height:40px;border-radius:50%;background:#ef4444;'
84
+ 'display:flex;align-items:center;justify-content:center;color:white;'
85
+ 'font-weight:700;font-size:0.8em;">END</div></div>')
86
+
87
+ mode_colors = {"fixed": "#3b82f6", "fuzzy": "#f59e0b", "deep": "#8b5cf6"}
88
+ border_color = mode_colors.get(mode, "#6b7280")
89
+ bg = f"{border_color}15"
90
+ check = ' <span style="color:#22c55e;">&#10003;</span>' if executed else ""
91
+
92
+ return (
93
+ f'<div style="display:flex;flex-direction:column;align-items:center;">'
94
+ f'<div style="border:3px solid {border_color};border-radius:10px;'
95
+ f'padding:12px 20px;background:{bg};min-width:120px;text-align:center;'
96
+ f'{"box-shadow:0 0 12px " + border_color + "40;" if executed else ""}">'
97
+ f'<div style="font-weight:700;font-size:1.05em;">{html_mod.escape(name)}{check}</div>'
98
+ f'<div style="font-size:0.85em;color:#6b7280;">mode: {html_mod.escape(mode)}</div>'
99
+ f'</div></div>')
100
+
101
+
102
+ def _arrow_html(label="continue"):
103
+ color = "#22c55e" if label == "continue" else "#ef4444"
104
+ return (
105
+ f'<div style="display:flex;flex-direction:column;align-items:center;'
106
+ f'padding:0 8px;">'
107
+ f'<div style="font-size:1.5em;color:{color};">&#8594;</div>'
108
+ f'<div style="font-size:0.7em;color:#9ca3af;">{label}</div></div>')
109
+
110
+
111
+ def _halt_arrow_html():
112
+ return (
113
+ '<div style="display:flex;flex-direction:column;align-items:center;'
114
+ 'padding:0 4px;opacity:0.5;">'
115
+ '<div style="font-size:1.2em;color:#ef4444;">&#8600;</div>'
116
+ '<div style="font-size:0.65em;color:#ef4444;">halt</div></div>')
117
+
118
+
119
+ def build_graph_html(stages_info, executed_stages=None):
120
+ """Build an HTML visualization of the per-stage graph."""
121
+ executed = set(executed_stages or [])
122
+ n = len(stages_info)
123
+
124
+ # Main flow: START → stages → END
125
+ nodes = []
126
+ nodes.append(_node_html("", "", 0, n, is_start=True))
127
+ nodes.append(_arrow_html(""))
128
+
129
+ for i, (name, _, mode) in enumerate(stages_info):
130
+ nodes.append(_node_html(name, mode, i, n, executed=name in executed))
131
+ if i < n - 1:
132
+ nodes.append(_arrow_html("continue"))
133
+ else:
134
+ nodes.append(_arrow_html(""))
135
+
136
+ nodes.append(_node_html("", "", 0, n, is_end=True))
137
+
138
+ main_flow = (
139
+ '<div style="display:flex;align-items:center;justify-content:center;'
140
+ 'flex-wrap:wrap;gap:4px;padding:20px 0;">'
141
+ + "".join(nodes) + '</div>')
142
+
143
+ # Halt edges legend
144
+ halt_legend = (
145
+ '<div style="text-align:center;padding:8px;color:#9ca3af;font-size:0.85em;">'
146
+ 'Each stage has a conditional edge: '
147
+ '<span style="color:#22c55e;font-weight:600;">continue</span> &rarr; next stage, '
148
+ '<span style="color:#ef4444;font-weight:600;">halt/blocked</span> &rarr; END'
149
+ '</div>')
150
+
151
+ return main_flow + halt_legend
152
+
153
+
154
+ # ---------------------------------------------------------------------------
155
+ # Core logic
156
+ # ---------------------------------------------------------------------------
157
+
158
+ def _parse_stages(stages_text):
159
+ """Parse stages from text format: name, role, mode (one per line)."""
160
+ stages = []
161
+ for line in stages_text.strip().split("\n"):
162
+ line = line.strip()
163
+ if not line:
164
+ continue
165
+ parts = [p.strip() for p in line.split(",")]
166
+ if len(parts) >= 3:
167
+ stages.append((parts[0], parts[1], parts[2]))
168
+ elif len(parts) == 2:
169
+ stages.append((parts[0], parts[1], "fixed"))
170
+ elif len(parts) == 1:
171
+ stages.append((parts[0], parts[0].title(), "fixed"))
172
+ return stages
173
+
174
+
175
+ def visualize_and_run(stages_text, task, do_run):
176
+ if not stages_text.strip():
177
+ return "<p>Enter at least one stage.</p>", ""
178
+
179
+ stages_info = _parse_stages(stages_text)
180
+ if not stages_info:
181
+ return "<p>Could not parse stages. Use format: name, role, mode</p>", ""
182
+
183
+ # Build organism with deterministic handlers (avoids MockProvider
184
+ # substring-matching collisions across stages)
185
+ def _make_handler(stage_name, stage_role):
186
+ def handler(task, state, outputs, stage):
187
+ return f"[{stage_role}] Processed: task complete."
188
+ return handler
189
+
190
+ fast = Nucleus(provider=MockProvider(responses={}))
191
+ deep = Nucleus(provider=MockProvider(responses={}))
192
+
193
+ org = skill_organism(
194
+ stages=[
195
+ SkillStage(name=name, role=role,
196
+ handler=_make_handler(name, role),
197
+ mode=mode)
198
+ for name, role, mode in stages_info
199
+ ],
200
+ fast_nucleus=fast,
201
+ deep_nucleus=deep,
202
+ budget=ATP_Store(budget=2000, silent=True),
203
+ )
204
+
205
+ # Compile to LangGraph
206
+ graph = organism_to_langgraph(org)
207
+ all_nodes = list(graph.nodes.keys())
208
+ stage_nodes = [n for n in all_nodes if not n.startswith("__")]
209
+
210
+ # Graph stats
211
+ stats_html = (
212
+ f'<div style="padding:12px;background:#f8fafc;border-radius:8px;'
213
+ f'margin-bottom:12px;">'
214
+ f'<span style="font-weight:600;">Graph Stats:</span> '
215
+ f'{len(stage_nodes)} stage nodes, '
216
+ f'{len(stage_nodes)} conditional edges (continue/halt), '
217
+ f'1 START edge, 1 terminal edge'
218
+ f'</div>')
219
+
220
+ # Visualize
221
+ executed_stages = []
222
+ run_html = ""
223
+
224
+ if do_run and task.strip():
225
+ result = run_organism_langgraph(org, task=task.strip())
226
+ executed_stages = result.metadata.get("stages_completed", [])
227
+
228
+ # Build run results
229
+ rows = ""
230
+ for sr in result.stage_outputs.items():
231
+ name, output = sr
232
+ preview = str(output)[:60]
233
+ rows += (
234
+ f'<tr style="border-bottom:1px solid #f3f4f6;">'
235
+ f'<td style="padding:6px 8px;font-weight:600;">{html_mod.escape(name)}</td>'
236
+ f'<td style="padding:6px 8px;font-family:monospace;'
237
+ f'font-size:0.9em;">{html_mod.escape(preview)}</td></tr>')
238
+
239
+ cert_rows = ""
240
+ for cv in result.certificates_verified:
241
+ status = "HOLDS" if cv["holds"] else "FAILS"
242
+ color = "#22c55e" if cv["holds"] else "#ef4444"
243
+ cert_rows += (
244
+ f'<span style="background:{color};color:white;padding:2px 8px;'
245
+ f'border-radius:4px;font-size:0.85em;margin-right:6px;">'
246
+ f'{cv["theorem"]}: {status}</span>')
247
+
248
+ run_html = (
249
+ f'<div style="border:2px solid #3b82f6;border-radius:8px;'
250
+ f'margin-top:12px;overflow:hidden;">'
251
+ f'<div style="padding:8px 14px;background:#3b82f610;'
252
+ f'border-bottom:1px solid #3b82f6;">'
253
+ f'<span style="font-weight:700;">Execution Results</span> '
254
+ f'<span style="color:#6b7280;font-size:0.9em;">'
255
+ f'({result.timing_ms:.1f} ms)</span></div>'
256
+ f'<div style="padding:12px 14px;">'
257
+ f'<table style="width:100%;border-collapse:collapse;">'
258
+ f'<tr style="border-bottom:2px solid #e5e7eb;color:#6b7280;">'
259
+ f'<th style="text-align:left;padding:6px 8px;">Stage</th>'
260
+ f'<th style="text-align:left;padding:6px 8px;">Output</th></tr>'
261
+ f'{rows}</table>'
262
+ f'<div style="margin-top:10px;">{cert_rows or "No certificates"}</div>'
263
+ f'<div style="margin-top:8px;color:#6b7280;font-size:0.9em;">'
264
+ f'Halted: {result.metadata.get("halted", False)}</div>'
265
+ f'</div></div>')
266
+
267
+ graph_html = stats_html + build_graph_html(stages_info, executed_stages)
268
+ return graph_html, run_html
269
+
270
+
271
+ def load_preset(name):
272
+ p = PRESETS.get(name)
273
+ if not p:
274
+ return "", ""
275
+ lines = [f"{n}, {r}, {m}" for n, r, m in p["stages"]]
276
+ return "\n".join(lines), p["task"]
277
+
278
+
279
+ # ---------------------------------------------------------------------------
280
+ # Gradio UI
281
+ # ---------------------------------------------------------------------------
282
+
283
+ def build_app() -> gr.Blocks:
284
+ with gr.Blocks(title="Operon LangGraph Visualizer") as app:
285
+ gr.Markdown(
286
+ "# Operon LangGraph Visualizer\n"
287
+ "Compile an organism to a **per-stage LangGraph** and visualize the "
288
+ "graph topology. Each stage becomes a LangGraph node; conditional "
289
+ "edges route based on continue/halt decisions.\n\n"
290
+ "[GitHub](https://github.com/coredipper/operon) | "
291
+ "[Paper](https://github.com/coredipper/operon/tree/main/article)")
292
+
293
+ with gr.Row():
294
+ preset_dd = gr.Dropdown(
295
+ choices=list(PRESETS.keys()),
296
+ value="3-stage pipeline",
297
+ label="Load Preset", scale=2)
298
+ run_btn = gr.Button("Visualize & Run", variant="primary", scale=1)
299
+
300
+ with gr.Row():
301
+ with gr.Column(scale=1):
302
+ stages_input = gr.Textbox(
303
+ value="intake, Normalizer, fixed\nrouter, Classifier, fixed\nexecutor, Engineer, fuzzy",
304
+ label="Stages (name, role, mode -- one per line)",
305
+ lines=6)
306
+ with gr.Column(scale=1):
307
+ task_input = gr.Textbox(
308
+ value="Fix the login crash after session timeout",
309
+ label="Task (for execution)", lines=2)
310
+ do_run = gr.Checkbox(value=True, label="Execute after compiling")
311
+
312
+ gr.Markdown("### Graph Topology")
313
+ graph_output = gr.HTML()
314
+
315
+ gr.Markdown("### Execution Results")
316
+ run_output = gr.HTML()
317
+
318
+ run_btn.click(
319
+ fn=visualize_and_run,
320
+ inputs=[stages_input, task_input, do_run],
321
+ outputs=[graph_output, run_output])
322
+ preset_dd.change(
323
+ fn=load_preset,
324
+ inputs=[preset_dd],
325
+ outputs=[stages_input, task_input])
326
+
327
+ return app
328
+
329
+
330
+ if __name__ == "__main__":
331
+ app = build_app()
332
+ app.launch(theme=gr.themes.Soft())
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ gradio>=4.0
2
+ operon-ai[langgraph]>=0.33.0
3
+ pydantic>=2.0