File size: 18,100 Bytes
fc82b09
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8de7c01
fc82b09
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
"""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