File size: 17,916 Bytes
0553ea7 152be26 0553ea7 152be26 0553ea7 ef4748f 152be26 5806fe4 152be26 5806fe4 08e4d77 152be26 5806fe4 152be26 5806fe4 152be26 ef4748f 152be26 5806fe4 152be26 08e4d77 5806fe4 ef4748f 0553ea7 ef4748f 08e4d77 5806fe4 ef4748f 0553ea7 ef4748f 08e4d77 5806fe4 ef4748f 0553ea7 ef4748f 08e4d77 5806fe4 ef4748f 0553ea7 ef4748f 08e4d77 0553ea7 08e4d77 5806fe4 08e4d77 5806fe4 152be26 ef4748f 152be26 08e4d77 152be26 ef4748f 152be26 0553ea7 08e4d77 152be26 ef4748f 5806fe4 ef4748f 152be26 08e4d77 ef4748f 08e4d77 152be26 0553ea7 152be26 08e4d77 152be26 0553ea7 152be26 5806fe4 152be26 0553ea7 152be26 08e4d77 152be26 ef4748f 152be26 08e4d77 5806fe4 08e4d77 5806fe4 152be26 5806fe4 08e4d77 5806fe4 08e4d77 152be26 08e4d77 0553ea7 08e4d77 0553ea7 08e4d77 152be26 0553ea7 152be26 0553ea7 08e4d77 5806fe4 08e4d77 ef4748f 0553ea7 152be26 08e4d77 5806fe4 0553ea7 08e4d77 152be26 08e4d77 0553ea7 08e4d77 5806fe4 08e4d77 152be26 0553ea7 5806fe4 0553ea7 ef4748f 5806fe4 152be26 08e4d77 0553ea7 5806fe4 152be26 0553ea7 5806fe4 0553ea7 152be26 08e4d77 0553ea7 5806fe4 152be26 5806fe4 152be26 0553ea7 152be26 0553ea7 152be26 0553ea7 152be26 0553ea7 152be26 5806fe4 0553ea7 152be26 0553ea7 | 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 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 | """Gradio demo for Agent 9 — Agentic Workflow System."""
from __future__ import annotations
import os
import time
from typing import Any
import gradio as gr
from src.demo_presentation import (
APP_CSS,
EMPTY_ACTIVITY_HTML,
EMPTY_AI_HTML,
EMPTY_JOURNEY_HTML,
EMPTY_OUTCOME_HTML,
EMPTY_PATH_HTML,
EMPTY_ROUTING_HTML,
HERO_HTML,
PATTERN_HTML,
activity_html,
activity_start_html,
playback_delay,
)
from src.demo_workflow import build_onboarding_registry, build_onboarding_workflow
from src.evaluation import run_evaluation_suite
from src.executor import (
WorkflowExecutionError,
WorkflowHumanDecisionError,
execute_workflow,
submit_human_decision,
)
from src.hf_provider import (
build_hf_onboarding_model_from_env,
configured_model_id,
live_ai_enabled_from_env,
)
from src.persistence import RunNotFoundError, SQLiteStateStore
from src.schemas import HumanDecision, WorkflowRun
from src.ui_presenters import (
CASE_HEADERS,
EVENT_HEADERS,
METRIC_HEADERS,
NODE_HEADERS,
REVIEW_HEADERS,
ai_insight_html,
business_journey_html,
business_outcome_html,
evaluation_case_rows,
evaluation_metric_rows,
routing_explanation_html,
run_bundle,
workflow_path_html,
)
WORKFLOW = build_onboarding_workflow()
DATABASE_PATH = os.getenv("WORKFLOW_DB_PATH", "workflow_runs.db")
STATE_STORE = SQLiteStateStore(DATABASE_PATH)
LIVE_MODEL_ID = configured_model_id()
LIVE_ONBOARDING_MODEL = build_hf_onboarding_model_from_env()
STORY_SPECS: dict[str, dict[str, Any]] = {
"Harbor Family — straightforward household": {
"household_id": "HH-HARBOR-STANDARD",
"household_type": "JOINT",
"onboarding_notes": "The fictional Harbor family is beginning a standard advisory relationship. The household has two adults, straightforward planning needs, and a complete synthetic intake.",
"documents_complete": True,
"identity_status": "VERIFIED",
"relationship_complexity": "STANDARD",
"simulation_mode": "NONE",
},
"Harbor Family — temporary onboarding-service issue": {
"household_id": "HH-HARBOR-RETRY",
"household_type": "JOINT",
"onboarding_notes": "The fictional Harbor family has a straightforward synthetic intake, but the demo will simulate one temporary package-preparation service interruption.",
"documents_complete": True,
"identity_status": "VERIFIED",
"relationship_complexity": "STANDARD",
"simulation_mode": "TRANSIENT_ONCE",
},
"Redwood Family Trust — human review required": {
"household_id": "HH-REDWOOD-TRUST",
"household_type": "TRUST",
"onboarding_notes": "The fictional Redwood Family Trust has multiple interested parties and a more complex planning relationship. The synthetic intake is complete but should demonstrate an exception-review path.",
"documents_complete": True,
"identity_status": "VERIFIED",
"relationship_complexity": "COMPLEX",
"simulation_mode": "NONE",
},
"Cedar Household — permanent dependency failure": {
"household_id": "HH-CEDAR-FAILURE",
"household_type": "JOINT",
"onboarding_notes": "The fictional Cedar household has a standard synthetic intake, but the demo will simulate a permanent downstream package-preparation failure.",
"documents_complete": True,
"identity_status": "VERIFIED",
"relationship_complexity": "STANDARD",
"simulation_mode": "PERMANENT",
},
}
RunBundle = tuple[str, str, list[list[Any]], list[list[str]], list[list[str]], dict[str, Any]]
AppBundle = tuple[
str,
str,
list[list[Any]],
list[list[str]],
list[list[str]],
dict[str, Any],
str,
str,
str,
str,
str,
]
def _ai_runtime_html() -> str:
if LIVE_ONBOARDING_MODEL is not None:
return (
'<div class="ai-runtime ai-runtime-live"><strong>● Live AI connected</strong><br>'
f'The AI Intake Organizer is using Hugging Face Inference Providers with <code>{LIVE_MODEL_ID}</code>. '
'Its output still must pass the same strict Pydantic schema before the workflow can use it.</div>'
)
if live_ai_enabled_from_env():
return (
'<div class="ai-runtime ai-runtime-waiting"><strong>◐ Live AI requested but not connected</strong><br>'
'LIVE_AI_ENABLED is on, but no usable HF_TOKEN was found. The demo is using its explicit deterministic fallback.</div>'
)
return (
'<div class="ai-runtime"><strong>○ AI boundary ready; deterministic fallback active</strong><br>'
'Live inference is deliberately opt-in. The same bounded node can use Hugging Face Inference Providers when the deployment sets <code>LIVE_AI_ENABLED=true</code> and supplies <code>HF_TOKEN</code>.</div>'
)
def _build_registry():
return build_onboarding_registry(
onboarding_model=LIVE_ONBOARDING_MODEL,
model_label=LIVE_MODEL_ID if LIVE_ONBOARDING_MODEL is not None else None,
)
def run_request_ui(
household_id: str,
household_type: str,
onboarding_notes: str,
documents_complete: bool,
identity_status: str,
relationship_complexity: str,
simulation_mode: str,
) -> AppBundle:
context = {
"household_id": household_id,
"household_type": household_type,
"onboarding_notes": onboarding_notes,
"documents_complete": documents_complete,
"identity_status": identity_status,
"relationship_complexity": relationship_complexity,
"simulation_mode": simulation_mode,
}
try:
run = execute_workflow(WORKFLOW, _build_registry(), context=context, state_store=STATE_STORE)
return _app_bundle(run)
except WorkflowExecutionError as exc:
return _bundle_with_notice(exc.run, "The onboarding workflow stopped after a controlled failure.")
except Exception as exc:
return _empty_bundle(f"Unable to start onboarding workflow: {type(exc).__name__}")
def run_story_ui(story: str) -> AppBundle:
spec = STORY_SPECS.get(story)
if spec is None:
return _empty_bundle("Choose one of the four onboarding stories before running the workflow.")
return run_request_ui(**spec)
def refresh_run_ui(run_id: str) -> AppBundle:
if not run_id or not run_id.strip():
return _empty_bundle("Enter a run ID to load a persisted workflow.")
try:
return _app_bundle(STATE_STORE.load(run_id.strip()))
except RunNotFoundError:
return _empty_bundle("No persisted workflow run was found for that ID.", run_id.strip())
except Exception as exc:
return _empty_bundle(f"Unable to load workflow: {type(exc).__name__}", run_id.strip())
def submit_human_decision_ui(run_id: str, decision: str) -> AppBundle:
if not run_id or not run_id.strip():
return _empty_bundle("Enter a run ID before submitting a human decision.")
normalized_run_id = run_id.strip()
try:
run = STATE_STORE.load(normalized_run_id)
except RunNotFoundError:
return _empty_bundle("No persisted workflow run was found for that ID.", normalized_run_id)
open_reviews = [review for review in run.human_reviews if review.decision is None]
if len(open_reviews) != 1:
return _bundle_with_notice(run, "This run does not have exactly one open human review. Reload the run and inspect the Human Review table.")
try:
updated = submit_human_decision(
WORKFLOW,
_build_registry(),
run_id=normalized_run_id,
review_id=open_reviews[0].review_id,
decision=HumanDecision(decision),
state_store=STATE_STORE,
)
return _app_bundle(updated)
except (WorkflowHumanDecisionError, WorkflowExecutionError) as exc:
return _bundle_with_notice(exc.run, str(exc))
except ValueError:
return _bundle_with_notice(run, "Unknown human decision.")
except Exception as exc:
return _bundle_with_notice(run, f"Unable to apply decision: {type(exc).__name__}")
def run_evaluation_ui() -> tuple[str, list[list[str]], list[list[str]]]:
report = run_evaluation_suite()
summary = (
"### Wealth Onboarding Workflow Evaluation\n"
f"**Cases:** {report.passed_cases}/{report.total_cases} passed \n"
f"**Pass rate:** {report.case_pass_rate:.2f} \n"
f"**Duplicate executions:** {report.metrics['duplicate_execution_count']} \n"
f"**Invalid transitions:** {report.metrics['invalid_transition_count']}"
)
return summary, evaluation_metric_rows(report), evaluation_case_rows(report)
def _app_bundle(run: WorkflowRun) -> AppBundle:
technical: RunBundle = run_bundle(run)
return (
*technical,
business_outcome_html(run),
workflow_path_html(run),
ai_insight_html(run),
routing_explanation_html(run),
business_journey_html(run),
)
def _bundle_with_notice(run: WorkflowRun, notice: str) -> AppBundle:
technical = run_bundle(run)
return (
f"> {notice}\n\n{technical[0]}",
*technical[1:],
business_outcome_html(run),
workflow_path_html(run),
ai_insight_html(run),
routing_explanation_html(run),
business_journey_html(run),
)
def _empty_bundle(message: str, run_id: str = "") -> AppBundle:
return (
f"### Workflow Status\n{message}", run_id, [], [], [], {},
EMPTY_OUTCOME_HTML, EMPTY_PATH_HTML, EMPTY_AI_HTML, EMPTY_ROUTING_HTML, EMPTY_JOURNEY_HTML,
)
def _running_bundle(run_id: str = "") -> AppBundle:
return (
"### Workflow running…\nThe workflow engine is controlling the approved next steps.",
run_id,
[], [], [], {},
EMPTY_OUTCOME_HTML, EMPTY_PATH_HTML, EMPTY_AI_HTML, EMPTY_ROUTING_HTML, EMPTY_JOURNEY_HTML,
)
def _stream_bundle(bundle: AppBundle):
run_id = bundle[1]
if not run_id:
yield ('<div class="activity-card"><div class="activity-badge">STOPPED</div><div class="activity-title">Workflow did not start</div></div>', *bundle)
return
try:
run = STATE_STORE.load(run_id)
except Exception:
yield ('<div class="activity-card"><div class="activity-badge">STOPPED</div><div class="activity-title">Workflow trace unavailable</div></div>', *bundle)
return
running = _running_bundle(run_id)
for index, event in enumerate(run.events, start=1):
yield (activity_html(run, index), *running)
time.sleep(playback_delay(event.event_type.value))
yield (activity_html(run, len(run.events), complete=True), *bundle)
def stream_story_ui(story: str):
yield (activity_start_html(story), *_running_bundle())
yield from _stream_bundle(run_story_ui(story))
def stream_request_ui(
household_id: str,
household_type: str,
onboarding_notes: str,
documents_complete: bool,
identity_status: str,
relationship_complexity: str,
simulation_mode: str,
):
yield (activity_start_html("Custom synthetic household"), *_running_bundle())
bundle = run_request_ui(
household_id, household_type, onboarding_notes, documents_complete,
identity_status, relationship_complexity, simulation_mode,
)
yield from _stream_bundle(bundle)
def build_app() -> gr.Blocks:
with gr.Blocks(title="Agent 9 — Wealth Onboarding Workflow", analytics_enabled=False) as demo:
gr.HTML(HERO_HTML)
gr.HTML(PATTERN_HTML)
gr.HTML(_ai_runtime_html())
gr.Markdown("## Try the onboarding pattern in one click")
gr.Markdown("Choose a fictional household and watch the workflow control plane move it through AI assistance, deterministic rules, retry/recovery, and human escalation.")
story = gr.Radio(choices=list(STORY_SPECS), value="Harbor Family — straightforward household", label="Choose an onboarding story")
story_button = gr.Button("Run this onboarding workflow", variant="primary")
live_activity = gr.HTML(EMPTY_ACTIVITY_HTML)
business_outcome = gr.HTML(EMPTY_OUTCOME_HTML)
workflow_path = gr.HTML(EMPTY_PATH_HTML)
ai_insight = gr.HTML(EMPTY_AI_HTML)
routing_explanation = gr.HTML(EMPTY_ROUTING_HTML)
business_journey = gr.HTML(EMPTY_JOURNEY_HTML)
with gr.Accordion("Reload a saved run", open=False):
gr.Markdown("Every run receives a random ID and is checkpointed in SQLite. Reloading a completed run shows the same state without replaying completed work.")
with gr.Row():
current_run_id = gr.Textbox(label="Run ID", placeholder="A random run ID appears here after execution.")
refresh_button = gr.Button("Reload Saved Run")
with gr.Tabs():
with gr.Tab("Customize Scenario"):
gr.Markdown("### Build your own synthetic onboarding scenario\nThese inputs are fictional and exist only to demonstrate workflow behavior.")
household_id = gr.Textbox(label="Household ID", value="HH-DEMO-001")
household_type = gr.Dropdown(choices=["INDIVIDUAL", "JOINT", "TRUST", "ENTITY"], value="JOINT", label="Household Type")
documents_complete = gr.Checkbox(label="Onboarding documents complete", value=True)
onboarding_notes = gr.Textbox(label="Synthetic Onboarding Notes", value="Fictional household seeking a standard advisory relationship. All information in this demo is synthetic.", lines=4)
identity_status = gr.Dropdown(choices=["VERIFIED", "REVIEW_REQUIRED"], value="VERIFIED", label="Identity Check Status")
relationship_complexity = gr.Dropdown(choices=["STANDARD", "COMPLEX"], value="STANDARD", label="Relationship Complexity")
simulation_mode = gr.Dropdown(choices=["NONE", "TRANSIENT_ONCE", "PERMANENT"], value="NONE", label="Failure Simulation")
gr.Markdown("**Helpful shortcuts:** TRUST/ENTITY, COMPLEX, missing documents, or REVIEW_REQUIRED demonstrates human review. TRANSIENT_ONCE demonstrates bounded retry recovery.")
run_button = gr.Button("Run custom workflow", variant="primary")
with gr.Tab("Engineering State"):
gr.Markdown("### Engineering evidence\nInspect exact workflow status, node state, attempt counts, errors, and structured outputs—including the AI Intake Organizer output and its source.")
status = gr.Markdown("### Workflow Status\nRun a synthetic household to begin.")
node_table = gr.Dataframe(headers=NODE_HEADERS, interactive=False, label="Node Runtime State")
final_output = gr.JSON(label="Final Output")
with gr.Tab("Audit Trail"):
gr.Markdown("### Why the workflow is auditable\nEvery meaningful control transition is recorded. The audit timeline contains control metadata only; onboarding context is not copied into event details.")
event_table = gr.Dataframe(headers=EVENT_HEADERS, interactive=False, label="Append-Only Workflow Events")
with gr.Tab("Human Review"):
gr.Markdown("### The human remains in control\nA fictional exception case pauses here. The workflow cannot mark the package ready until a person explicitly decides what should happen.")
review_table = gr.Dataframe(headers=REVIEW_HEADERS, interactive=False, label="Human Review History")
human_decision = gr.Dropdown(choices=["APPROVE", "REJECT", "RETRY"], value="APPROVE", label="What should the reviewer do?")
decision_button = gr.Button("Submit Human Decision", variant="primary")
gr.Markdown("**APPROVE** continues the approved exception path. **REJECT** ends the fictional process as `REJECTED`, not `FAILED`. **RETRY** opens a fresh review without replaying completed upstream work. This is illustrative, not any firm's actual regulatory procedure.")
with gr.Tab("Evaluation"):
gr.Markdown("### Reliability evidence\nThe same onboarding engine is exercised across success, retry, human review, persistence, failure, and adversarial-control scenarios.")
evaluation_summary = gr.Markdown("Run the deterministic evaluation harness to inspect system-level metrics.")
evaluation_button = gr.Button("Run 10-Case Evaluation")
metric_table = gr.Dataframe(headers=METRIC_HEADERS, interactive=False, label="Evaluation Metrics")
case_table = gr.Dataframe(headers=CASE_HEADERS, interactive=False, label="Evaluation Cases")
run_outputs = [
status, current_run_id, node_table, event_table, review_table, final_output,
business_outcome, workflow_path, ai_insight, routing_explanation, business_journey,
]
stream_outputs = [live_activity, *run_outputs]
story_button.click(fn=stream_story_ui, inputs=[story], outputs=stream_outputs, show_progress="hidden")
run_button.click(
fn=stream_request_ui,
inputs=[household_id, household_type, onboarding_notes, documents_complete, identity_status, relationship_complexity, simulation_mode],
outputs=stream_outputs,
show_progress="hidden",
)
refresh_button.click(fn=refresh_run_ui, inputs=[current_run_id], outputs=run_outputs)
decision_button.click(fn=submit_human_decision_ui, inputs=[current_run_id, human_decision], outputs=run_outputs)
evaluation_button.click(fn=run_evaluation_ui, outputs=[evaluation_summary, metric_table, case_table])
return demo
demo = build_app()
if __name__ == "__main__":
demo.queue().launch(server_name="0.0.0.0", server_port=int(os.getenv("PORT", "7860")), css=APP_CSS)
|