Download src/demo_presentation.py from FlyingNunchucks/09-agentic-workflow-sys: direct link, hf CLI and curl.
- Browser
- Download file 18.1 kB
-
https://huggingface.co/spaces/FlyingNunchucks/09-agentic-workflow-sys/resolve/main/src/demo_presentation.py
- Command line
-
hf download hf://spaces/FlyingNunchucks/09-agentic-workflow-sys/src/demo_presentation.py
-
curl -L -o demo_presentation.py https://huggingface.co/spaces/FlyingNunchucks/09-agentic-workflow-sys/resolve/main/src/demo_presentation.py
18.1 kB
| """Business-first presentation helpers for the Agent 9 Gradio demo.""" | |
| from __future__ import annotations | |
| import html | |
| from src.schemas import WorkflowRun | |
| PLAYBACK_DELAY_SECONDS = 0.20 | |
| NODE_LABELS = { | |
| "validate_intake": "Check household intake", | |
| "ai_intake_organizer": "AI Intake Organizer", | |
| "document_check": "Check onboarding documents", | |
| "policy_check": "Apply onboarding rules", | |
| "create_onboarding_package": "Prepare onboarding package", | |
| "verify_onboarding_package": "Verify onboarding package", | |
| "review_gate": "Choose review path", | |
| "onboarding_ready": "Standard package ready", | |
| "human_review": "Operations / compliance review", | |
| "reviewed_onboarding": "Ready after human review", | |
| } | |
| EVENT_LABELS = { | |
| "WORKFLOW_STARTED": "Workflow started", | |
| "NODE_STARTED": "Step started", | |
| "NODE_COMPLETED": "Step completed", | |
| "NODE_FAILED": "Step stopped safely", | |
| "NODE_SKIPPED": "Step not needed", | |
| "DECISION_ROUTED": "Route selected", | |
| "RETRY_SCHEDULED": "Retry scheduled", | |
| "HUMAN_REVIEW_REQUESTED": "Human review requested", | |
| "HUMAN_APPROVED": "Human approved", | |
| "HUMAN_REJECTED": "Human rejected", | |
| "WORKFLOW_RESUMED": "Workflow resumed", | |
| "WORKFLOW_COMPLETED": "Workflow completed", | |
| "WORKFLOW_FAILED": "Workflow failed closed", | |
| } | |
| EVENT_ICONS = { | |
| "WORKFLOW_STARTED": "▶", "NODE_STARTED": "→", "NODE_COMPLETED": "✓", | |
| "NODE_FAILED": "×", "NODE_SKIPPED": "↷", "DECISION_ROUTED": "⇢", | |
| "RETRY_SCHEDULED": "↻", "HUMAN_REVIEW_REQUESTED": "👤", | |
| "HUMAN_APPROVED": "✓", "HUMAN_REJECTED": "■", "WORKFLOW_RESUMED": "▶", | |
| "WORKFLOW_COMPLETED": "✓", "WORKFLOW_FAILED": "×", | |
| } | |
| APP_CSS = """ | |
| .gradio-container { max-width:1120px !important; margin:0 auto !important; } | |
| .agent-hero { padding:1.65rem 1.75rem; border:1px solid var(--border-color-primary,rgba(127,127,127,.25)); border-radius:22px; background:linear-gradient(135deg,rgba(249,115,22,.12),rgba(99,102,241,.08)); margin-bottom:1rem; } | |
| .agent-eyebrow,.outcome-eyebrow,.panel-eyebrow { font-size:.76rem; font-weight:800; letter-spacing:.12em; opacity:.72; margin-bottom:.35rem; } | |
| .agent-hero h1 { margin:.25rem 0 .55rem; font-size:2.12rem; line-height:1.16; max-width:820px; } | |
| .agent-hero p { margin:0; max-width:820px; font-size:1.06rem; line-height:1.58; } | |
| .pattern-grid { display:grid; grid-template-columns:1fr; gap:.65rem; margin:1rem 0; } | |
| .pattern-card,.ai-result,.why-panel { padding:1rem 1.1rem; border-radius:14px; border:1px solid var(--border-color-primary,rgba(127,127,127,.25)); background:var(--block-background-fill,rgba(127,127,127,.06)); } | |
| .pattern-card strong { display:block; margin-bottom:.3rem; font-size:1.04rem; } | |
| .pattern-card span { opacity:.80; line-height:1.48; font-size:.96rem; } | |
| .ai-card { border-color:rgba(59,130,246,.55); background:linear-gradient(135deg,rgba(59,130,246,.14),rgba(99,102,241,.09)); } | |
| .ai-runtime { padding:1rem 1.1rem; border-radius:14px; border:1px solid rgba(59,130,246,.38); background:rgba(59,130,246,.08); margin:.7rem 0 1rem; line-height:1.5; } | |
| .ai-runtime-live { border-color:rgba(34,197,94,.45); background:rgba(34,197,94,.08); } | |
| .ai-runtime-waiting { border-color:rgba(245,158,11,.45); background:rgba(245,158,11,.08); } | |
| .pattern-flow { display:flex; flex-direction:column; align-items:stretch; gap:.35rem; margin:.8rem 0 1.2rem; } | |
| .flow-pill { padding:.7rem .85rem; border-radius:12px; border:1px solid var(--border-color-primary,rgba(127,127,127,.25)); background:var(--block-background-fill,rgba(127,127,127,.06)); font-weight:700; font-size:.96rem; } | |
| .ai-pill { border-color:rgba(59,130,246,.6); background:rgba(59,130,246,.12); } | |
| .flow-arrow { align-self:center; opacity:.5; font-weight:800; transform:rotate(90deg); line-height:1; } | |
| .section-note { padding:.9rem 1rem; border-radius:12px; background:rgba(99,102,241,.08); border:1px solid rgba(99,102,241,.18); margin-bottom:.85rem; line-height:1.5; } | |
| .business-outcome { border-radius:16px; padding:1.1rem 1.2rem; border:1px solid var(--border-color-primary,rgba(127,127,127,.25)); margin:.8rem 0; } | |
| .outcome-success { border-left:6px solid #22c55e; background:rgba(34,197,94,.08); } | |
| .outcome-waiting { border-left:6px solid #f59e0b; background:rgba(245,158,11,.08); } | |
| .outcome-rejected { border-left:6px solid #f97316; background:rgba(249,115,22,.08); } | |
| .outcome-failed { border-left:6px solid #ef4444; background:rgba(239,68,68,.08); } | |
| .outcome-neutral { border-left:6px solid #64748b; background:rgba(100,116,139,.08); } | |
| .outcome-title,.panel-title { font-size:1.2rem; font-weight:800; margin-bottom:.4rem; } | |
| .outcome-body,.panel-body { line-height:1.5; margin-bottom:.55rem; } | |
| .outcome-takeaway { font-weight:650; opacity:.83; } | |
| .ai-result-live { border-color:rgba(59,130,246,.6); background:rgba(59,130,246,.09); } | |
| .ai-result-fallback { border-color:rgba(100,116,139,.4); } | |
| .ai-result-failed { border-color:rgba(239,68,68,.45); background:rgba(239,68,68,.07); } | |
| .ai-mode { display:inline-block; padding:.25rem .5rem; border-radius:999px; font-size:.76rem; font-weight:800; background:rgba(59,130,246,.14); margin-bottom:.45rem; } | |
| .ai-model { font-size:.82rem; opacity:.7; margin-bottom:.5rem; } | |
| .ai-category { margin-bottom:.5rem; } | |
| .ai-summary { line-height:1.48; margin-bottom:.6rem; } | |
| .ai-used { font-weight:700; margin-bottom:.45rem; } | |
| .ai-boundary,.why-proof { font-size:.88rem; opacity:.78; line-height:1.4; } | |
| .why-standard { border-left:5px solid #22c55e; } | |
| .why-review { border-left:5px solid #f59e0b; } | |
| .why-list { margin:.35rem 0 .7rem 1.15rem; } | |
| .workflow-map { margin:1rem 0; padding:1rem 1.05rem; border-radius:16px; border:1px solid var(--border-color-primary,rgba(127,127,127,.25)); background:var(--block-background-fill,rgba(127,127,127,.04)); } | |
| .map-heading,.journey-heading { font-size:1.15rem; font-weight:800; } | |
| .map-subheading,.journey-subheading { opacity:.72; margin:.25rem 0 .8rem; line-height:1.45; } | |
| .map-main { display:flex; flex-wrap:wrap; align-items:center; gap:.42rem; } | |
| .map-split { display:grid; grid-template-columns:1fr; gap:.75rem; margin-top:.85rem; } | |
| .map-branch { padding:.75rem; border-radius:12px; border:1px dashed var(--border-color-primary,rgba(127,127,127,.28)); } | |
| .branch-label { font-size:.72rem; font-weight:800; letter-spacing:.08em; opacity:.65; margin-bottom:.45rem; } | |
| .map-node { display:inline-block; padding:.42rem .62rem; border-radius:10px; border:1px solid rgba(100,116,139,.35); background:rgba(100,116,139,.08); font-size:.84rem; font-weight:700; } | |
| .map-node-completed { border-color:rgba(34,197,94,.55); background:rgba(34,197,94,.12); } | |
| .map-node-waiting-for-human,.map-node-retry-scheduled,.map-node-running { border-color:rgba(245,158,11,.6); background:rgba(245,158,11,.12); } | |
| .map-node-failed { border-color:rgba(239,68,68,.6); background:rgba(239,68,68,.12); } | |
| .map-node-skipped,.map-node-pending,.map-node-ready { opacity:.42; } | |
| .map-node-ai { box-shadow:inset 0 0 0 2px rgba(59,130,246,.62); } | |
| .map-arrow { opacity:.5; font-weight:800; } | |
| .map-legend { margin-top:.8rem; font-size:.8rem; opacity:.65; } | |
| .journey-wrap { padding:1rem 0 .2rem; } | |
| .journey-grid { display:grid; grid-template-columns:1fr; gap:.65rem; } | |
| .journey-step { display:flex; gap:.7rem; padding:.85rem .9rem; border-radius:12px; border:1px solid var(--border-color-primary,rgba(127,127,127,.22)); background:var(--block-background-fill,rgba(127,127,127,.04)); } | |
| .journey-icon { width:30px; height:30px; min-width:30px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-weight:900; } | |
| .journey-done { background:rgba(34,197,94,.16); } .journey-waiting { background:rgba(245,158,11,.18); } .journey-rejected { background:rgba(249,115,22,.18); } .journey-failed { background:rgba(239,68,68,.18); } .journey-skipped { background:rgba(100,116,139,.16); } .journey-pending { background:rgba(100,116,139,.10); } | |
| .journey-title { font-weight:750; } .journey-state { font-size:.8rem; font-weight:700; opacity:.78; margin:.1rem 0 .2rem; } .journey-description { font-size:.9rem; opacity:.74; line-height:1.42; } | |
| .activity-card { border:1px solid rgba(59,130,246,.30); border-radius:18px; padding:17px 19px; margin:14px 0 18px; background:linear-gradient(135deg,rgba(59,130,246,.08),rgba(99,102,241,.035)); } | |
| .activity-head { display:flex; align-items:center; gap:10px; margin-bottom:8px; } | |
| .activity-spinner { width:24px; height:24px; box-sizing:border-box; border:4px solid rgba(59,130,246,.18); border-top-color:#3b82f6; border-right-color:#6366f1; border-radius:50%; animation:workflow-spin .78s linear infinite; flex:0 0 auto; } | |
| .activity-badge { display:inline-block; border:1px solid rgba(59,130,246,.42); border-radius:999px; padding:4px 8px; font-size:.72rem; font-weight:850; letter-spacing:.09em; } | |
| .activity-title { font-size:1.07rem; font-weight:800; } .activity-sub { font-size:.92rem; opacity:.79; line-height:1.4; margin-bottom:10px; } | |
| .activity-track { height:8px; background:rgba(148,163,184,.18); border-radius:999px; overflow:hidden; margin:10px 0 13px; } | |
| .activity-fill { height:100%; background:linear-gradient(90deg,#3b82f6,#6366f1); transition:width .2s ease; } | |
| .activity-track.indeterminate .activity-fill { width:34%; animation:workflow-slide 1.05s ease-in-out infinite; } | |
| .activity-feed { display:grid; gap:6px; } .activity-feed.activity-scroll { max-height:430px; overflow-y:auto; padding-right:7px; scrollbar-gutter:stable; } | |
| .activity-event { display:grid; grid-template-columns:28px minmax(0,1fr); gap:8px; padding-top:8px; border-top:1px solid rgba(148,163,184,.16); } .activity-event:first-child { border-top:0; padding-top:0; } | |
| .activity-icon { font-weight:850; line-height:1.45; text-align:center; } .activity-main { font-size:.94rem; line-height:1.4; } .activity-main strong { font-weight:800; } .activity-meta { font-size:.78rem; opacity:.66; margin-top:2px; } | |
| .activity-complete { border-color:rgba(34,197,94,.35); background:linear-gradient(135deg,rgba(34,197,94,.08),rgba(59,130,246,.035)); } .activity-scroll-note { font-size:.82rem; opacity:.72; margin-bottom:8px; } | |
| @keyframes workflow-spin { to { transform:rotate(360deg); } } @keyframes workflow-slide { 0% { transform:translateX(-110%); } 50% { transform:translateX(205%); } 100% { transform:translateX(-110%); } } | |
| @media (max-width:700px) { .gradio-container { max-width:100% !important; } .agent-hero { padding:1.3rem 1.1rem; } .agent-hero h1 { font-size:1.75rem; } } | |
| """ | |
| HERO_HTML = """ | |
| <div class="agent-hero"> | |
| <div class="agent-eyebrow">AGENT 9 • BUSINESS + ENGINEERING DEMO • SYNTHETIC DATA</div> | |
| <h1>How can AI help onboard a household without controlling the process?</h1> | |
| <p>A fictional wealth-management operations team needs to move a new household through onboarding. AI may organize the unstructured intake into useful work product, but application code controls sequencing, retries, routing, persistence, and the human-review boundary.</p> | |
| </div> | |
| """ | |
| PATTERN_HTML = """ | |
| <div class="pattern-grid"> | |
| <div class="pattern-card ai-card"><strong>🤖 AI prepares useful work</strong><span>The bounded AI node creates a profile category and concise intake summary for the simulated onboarding package.</span></div> | |
| <div class="pattern-card"><strong>✓ Software controls consequences</strong><span>Document checks, exception rules, routes, retries, checkpoints, and allowed next steps remain deterministic.</span></div> | |
| <div class="pattern-card"><strong>👤 A person owns exceptions</strong><span>Trust, entity, complex, missing-document, or identity-review cases pause until an explicit human decision is recorded.</span></div> | |
| <div class="pattern-card"><strong>↻ Operations recover safely</strong><span>Temporary service failures retry within policy; permanent failures stop before uncertain downstream work continues.</span></div> | |
| </div> | |
| <div class="section-note"><strong>The operating model:</strong> AI contributes work. Application code governs the process. Humans retain authority over consequential exceptions.</div> | |
| <div class="pattern-flow"> | |
| <span class="flow-pill">1 · Household intake</span><span class="flow-arrow">→</span> | |
| <span class="flow-pill ai-pill">2 · 🤖 AI organizes the intake</span><span class="flow-arrow">→</span> | |
| <span class="flow-pill">3 · Rules prepare and verify the package</span><span class="flow-arrow">→</span> | |
| <span class="flow-pill">4 · Standard path or explicit human review</span> | |
| </div> | |
| """ | |
| EMPTY_OUTCOME_HTML = """<div class="business-outcome outcome-neutral"><div class="outcome-eyebrow">READY TO DEMONSTRATE</div><div class="outcome-title">Choose a fictional household story and run it</div><div class="outcome-body">The result will be explained in business language first. The engineering tabs remain available underneath as evidence.</div><div class="outcome-takeaway">Start with the Harbor Family straightforward case.</div></div>""" | |
| EMPTY_PATH_HTML = """<div class="workflow-map"><div class="map-heading">Workflow path</div><div class="map-subheading">Run a household to light up the path it actually takes.</div></div>""" | |
| EMPTY_AI_HTML = """<div class="ai-result"><div class="panel-eyebrow">🤖 BOUNDED AI WORK PRODUCT</div><div class="panel-title">Waiting for a household</div><div class="panel-body">The validated profile category and intake summary will appear here.</div></div>""" | |
| EMPTY_ROUTING_HTML = """<div class="why-panel"><div class="panel-eyebrow">WHY DID IT ROUTE HERE?</div><div class="panel-title">Waiting for the review gate</div><div class="panel-body">After a run, this panel will show the exact deterministic reasons for the selected path.</div></div>""" | |
| EMPTY_JOURNEY_HTML = """<div class="journey-wrap"><div class="journey-heading">What happened, step by step</div><div class="journey-subheading">After you run a story, this section will translate the workflow engine into an understandable onboarding journey.</div></div>""" | |
| EMPTY_ACTIVITY_HTML = """<div class="activity-card"><div class="activity-title">Live workflow activity</div><div class="activity-sub">Run a household to watch the workflow's real append-only control events replay here at a readable pace.</div></div>""" | |
| def activity_start_html(label: str) -> str: | |
| return f"""<div class="activity-card"><div class="activity-head"><span class="activity-spinner"></span><span class="activity-badge">RUNNING</span><span class="activity-title">Workflow engine is running</span></div><div class="activity-sub">{html.escape(label)} · deterministic workflow control is executing. Real audit events from this exact run will appear here.</div><div class="activity-track indeterminate"><div class="activity-fill"></div></div><div class="activity-feed"><div class="activity-event"><div class="activity-icon">…</div><div class="activity-main">Preparing the workflow run and its persisted event trace.</div></div></div></div>""" | |
| def _event_text(event) -> tuple[str, str, str]: | |
| event_type = event.event_type.value | |
| label = EVENT_LABELS.get(event_type, event_type.replace("_", " ").title()) | |
| node_label = NODE_LABELS.get(event.node_id or "", (event.node_id or "Workflow").replace("_", " ").title()) | |
| return EVENT_ICONS.get(event_type, "•"), label, node_label | |
| def activity_html(run: WorkflowRun, count: int, *, complete: bool = False) -> str: | |
| total = max(len(run.events), 1) | |
| visible_count = min(max(count, 0), len(run.events)) | |
| percent = min(100.0, (visible_count / total) * 100) | |
| rows = run.events if complete else run.events[max(0, visible_count - 8):visible_count] | |
| if complete: | |
| badge = "STOPPED" if run.status.value == "FAILED" else "COMPLETE" | |
| title = "Workflow event trace complete" | |
| sub = f"{len(run.events)} real append-only workflow events retained. Scroll inside the transcript to review the full run." | |
| spinner = "" | |
| card_class = "activity-card activity-complete" | |
| note = '<div class="activity-scroll-note">Scroll inside this panel to review the complete workflow event transcript.</div>' | |
| feed_class = "activity-feed activity-scroll" | |
| else: | |
| current = run.events[visible_count - 1] if visible_count else None | |
| if current is None: | |
| sub = "Waiting for the first workflow event." | |
| else: | |
| _, event_label, node_label = _event_text(current) | |
| sub = f"Event {visible_count}/{len(run.events)} · {event_label} · {node_label}" | |
| badge, title = "RUNNING", "Workflow activity" | |
| spinner = '<span class="activity-spinner"></span>' | |
| card_class, note, feed_class = "activity-card", "", "activity-feed" | |
| start = 1 if complete else max(1, visible_count - len(rows) + 1) | |
| feed_rows = [] | |
| for index, event in enumerate(rows, start=start): | |
| icon, event_label, node_label = _event_text(event) | |
| feed_rows.append(f'<div class="activity-event"><div class="activity-icon">{html.escape(icon)}</div><div class="activity-main"><strong>{html.escape(event_label)}</strong><br>{html.escape(node_label)}<div class="activity-meta">Event {index} · {html.escape(event.event_type.value)}</div></div></div>') | |
| feed = "".join(feed_rows) or '<div class="activity-event"><div class="activity-icon">…</div><div class="activity-main">Waiting for the first workflow event.</div></div>' | |
| return f'<div class="{card_class}"><div class="activity-head">{spinner}<span class="activity-badge">{badge}</span><span class="activity-title">{html.escape(title)}</span></div><div class="activity-sub">{html.escape(sub)}</div><div class="activity-track"><div class="activity-fill" style="width:{percent:.1f}%"></div></div>{note}<div class="{feed_class}">{feed}</div></div>' | |
| def playback_delay(event_type: str, base: float = PLAYBACK_DELAY_SECONDS) -> float: | |
| if base <= 0: | |
| return 0 | |
| if event_type in {"WORKFLOW_STARTED", "WORKFLOW_RESUMED"}: | |
| return base * 1.5 | |
| if event_type in {"DECISION_ROUTED", "RETRY_SCHEDULED", "HUMAN_REVIEW_REQUESTED"}: | |
| return base * 2.0 | |
| if event_type in {"WORKFLOW_COMPLETED", "WORKFLOW_FAILED", "HUMAN_APPROVED", "HUMAN_REJECTED"}: | |
| return base * 2.4 | |
| return base | |