Spaces:
Sleeping
Sleeping
| """ | |
| ui/pipeline_visualiser.py | |
| -------------------------- | |
| Pipeline Visualiser β vertical card layout. | |
| Each agent is a full card with icon, name, model badge, role description, | |
| and an animated thinking row when active. The panel sits in the right | |
| column and spans the full height of the page beside the left panel. | |
| States are baked as CSS classes on every yield β no JS re-execution. | |
| """ | |
| from pipeline.state import PipelineStatus | |
| # ββ Backward-compat exports ββββββββββββββββββββββββββββββββββββββ # | |
| STATUS_TO_ACTIVE_NODE: dict = {} | |
| SUCCESS_ARROWS: set = set() | |
| DEBUG_ARROWS: set = set() | |
| NODE_NAME_TO_STATUS: dict[str, PipelineStatus] = { | |
| "clarify": PipelineStatus.CLARIFYING, | |
| "detect": PipelineStatus.DETECTING, | |
| "plan": PipelineStatus.PLANNING, | |
| "generate": PipelineStatus.GENERATING, | |
| "regen": PipelineStatus.REGENERATING, | |
| "execute": PipelineStatus.EXECUTING, | |
| "classify_error": PipelineStatus.CLASSIFYING, | |
| "debug_agent": PipelineStatus.DEBUGGING, | |
| "test": PipelineStatus.TESTING, | |
| "test_failing": PipelineStatus.TEST_FAILING, | |
| "test_debug_agent": PipelineStatus.TEST_DEBUGGING, | |
| "explain": PipelineStatus.EXPLAINING, | |
| "success": PipelineStatus.SUCCESS, | |
| "partial_success": PipelineStatus.PARTIAL_SUCCESS, | |
| "human_loop": PipelineStatus.AWAITING_HUMAN, | |
| } | |
| def get_status_update_js(status=None, debug_iterations: int = 0, is_error: bool = False) -> str: | |
| return "" | |
| # ββ Public API βββββββββββββββββββββββββββββββββββββββββββββββββββ # | |
| def get_pipeline_html() -> str: | |
| return _build(PipelineStatus.IDLE, 0, False, {}) | |
| def get_pipeline_html_with_update( | |
| node_name: str, | |
| iterations: int = 0, | |
| is_error: bool = False, | |
| model_assignments: dict | None = None, | |
| ) -> str: | |
| status = NODE_NAME_TO_STATUS.get(node_name, PipelineStatus.IDLE) | |
| return _build(status, iterations, is_error, model_assignments or {}) | |
| # ββ Agent definitions ββββββββββββββββββββββββββββββββββββββββββββ # | |
| AGENTS = { | |
| "clarify": {"icon": "π€", "name": "Clarity Check", "ma_key": None, "role": "Checks if task is clear enough to code"}, | |
| "detect": {"icon": "π", "name": "Language Detector", "ma_key": "detector", "role": "Detects Python vs SQL from task"}, | |
| "plan": {"icon": "π§ ", "name": "Planning Agent", "ma_key": "planner", "role": "Breaks task into step-by-step plan"}, | |
| "gen": {"icon": "β¨οΈ", "name": "Code Generator", "ma_key": "generator", "role": "Writes code from the plan"}, | |
| "exec": {"icon": "β‘", "name": "Executor", "ma_key": None, "role": "Runs code in secure sandbox"}, | |
| "classify": {"icon": "π", "name": "Error Classifier", "ma_key": "classifier", "role": "Labels error type (syntax/runtime/logic)"}, | |
| "debug": {"icon": "π", "name": "Debug Agent", "ma_key": "debugger", "role": "Reflects on error & rewrites code"}, | |
| "hitl": {"icon": "π", "name": "Human-in-Loop", "ma_key": None, "role": "Max retries reached β needs your input"}, | |
| "test": {"icon": "π§ͺ", "name": "Test Agent", "ma_key": "tester", "role": "Generates & runs unit tests"}, | |
| "explain": {"icon": "π‘", "name": "Explanation Agent", "ma_key": "explainer", "role": "Plain-English explanation + flowchart"}, | |
| "complete": {"icon": "β ", "name": "Done", "ma_key": None, "role": "Code Β· tests Β· report ready"}, | |
| } | |
| from agents.model_router import MODEL_DISPLAY # noqa: E402 | |
| def _model_label(ma: dict, ma_key: str | None) -> str: | |
| if not ma_key or not ma: | |
| return "" | |
| model_id = ma.get(ma_key, "") | |
| return MODEL_DISPLAY.get(model_id, model_id.split("/")[-1] if model_id else "") | |
| THINKING_MSGS = { | |
| "clarify": "Checking task clarity...", | |
| "detect": "Detecting language from task...", | |
| "plan": "Analyzing task structure...", | |
| "gen": "Writing code from plan...", | |
| "exec": "Running in secure sandbox...", | |
| "classify": "Diagnosing the error...", | |
| "debug": "Reflecting & rewriting...", | |
| "hitl": "Awaiting your input...", | |
| "test": "Generating & running tests...", | |
| "explain": "Building explanation + flowchart...", | |
| "complete": "All done!", | |
| # regen reuses the gen card β message is overridden in _STATE_TABLE banner | |
| } | |
| # ββ Node state table βββββββββββββββββββββββββββββββββββββββββββββ # | |
| _STATE_TABLE: dict[str, dict[str, str]] = { | |
| "idle": {}, | |
| "clarifying": {"clarify": "active"}, | |
| "needs_clarification":{"clarify": "error"}, | |
| "detecting": {"clarify":"done","detect": "active"}, | |
| "planning": {"clarify":"done","detect": "done", "plan": "active"}, | |
| "generating": {"clarify":"done","detect":"done","plan":"done","gen":"active"}, | |
| # regenerating: gen card pulses again (fresh rewrite), shows regen badge via banner | |
| "regenerating": {"clarify":"done","detect":"done","plan":"done","gen":"active","exec":"error","classify":"done","debug":"done"}, | |
| "executing": {"clarify":"done","detect":"done","plan":"done","gen":"done","exec":"active"}, | |
| "classifying": {"clarify":"done","detect":"done","plan":"done","gen":"done","exec":"error","classify":"active"}, | |
| "reflecting": {"clarify":"done","detect":"done","plan":"done","gen":"done","exec":"error","classify":"done","debug":"active"}, | |
| "debugging": {"clarify":"done","detect":"done","plan":"done","gen":"done","exec":"error","classify":"done","debug":"active"}, | |
| "testing": {"clarify":"done","detect":"done","plan":"done","gen":"done","exec":"done","test":"active"}, | |
| "test_failing": {"clarify":"done","detect":"done","plan":"done","gen":"done","exec":"done","test":"error"}, | |
| "test_debugging": {"clarify":"done","detect":"done","plan":"done","gen":"done","exec":"done","test":"error","classify":"done","debug":"active"}, | |
| "polishing": {"clarify":"done","detect":"done","plan":"done","gen":"done","exec":"done","test":"done","explain":"active"}, | |
| "explaining": {"clarify":"done","detect":"done","plan":"done","gen":"done","exec":"done","test":"done","explain":"active"}, | |
| "success": {"clarify":"done","detect":"done","plan":"done","gen":"done","exec":"done","test":"done","explain":"done","complete":"done"}, | |
| # partial_success: code runs, but not all tests passed after exhausting test-fix budget. | |
| # test card shown in error state so user sees which tests failed. No HITL triggered. | |
| "partial_success": {"clarify":"done","detect":"done","plan":"done","gen":"done","exec":"done","test":"error","explain":"done","complete":"done"}, | |
| "awaiting_human": {"clarify":"done","detect":"done","plan":"done","gen":"done","exec":"error","classify":"done","debug":"done","hitl":"active"}, | |
| "failed": {}, | |
| } | |
| _ALL_NODES = ["clarify","detect","plan","gen","exec","classify","debug","hitl","test","explain","complete"] | |
| STATUS_MSGS = { | |
| "idle": ("idle", "Ready β submit a task to begin."), | |
| "clarifying": ("running", "Checking if task is clear enough to proceed..."), | |
| "needs_clarification":("warn", "Task needs clarification β please answer the question below."), | |
| "detecting": ("running", "Detecting language..."), | |
| "planning": ("running", "Planning Agent is analyzing your task..."), | |
| "generating": ("running", "Code Generator is writing code..."), | |
| "regenerating": ("error", "β³ Debug loop exhausted β regenerating code from scratch with error context..."), | |
| "executing": ("running", "Executor is running code in the sandbox..."), | |
| "classifying": ("error", "Error Classifier is diagnosing the failure..."), | |
| "reflecting": ("error", "Debug Agent is reflecting on the error..."), | |
| "debugging": ("error", "Debug Agent is rewriting the code..."), | |
| "testing": ("running", "Test Agent is generating & running tests..."), | |
| "test_failing": ("error", "Tests failed β rewriting code to fix failing cases..."), | |
| "test_debugging": ("error", "Debug Agent rewriting code to pass all test cases..."), | |
| "polishing": ("running", "Explanation Agent is preparing output..."), | |
| "explaining": ("running", "Explanation Agent is building the explanation..."), | |
| "awaiting_human": ("warn", "β Max retries reached β your input is needed."), | |
| "success": ("done", "All agents completed β output is ready."), | |
| "partial_success":("warn", "β Code runs β but not all tests passed after all retry cycles. Review the Tests tab."), | |
| "failed": ("error", "Pipeline failed."), | |
| } | |
| def _get_ns(status: PipelineStatus) -> dict[str, str]: | |
| s = status.value if hasattr(status, "value") else str(status) | |
| ns = {n: "idle" for n in _ALL_NODES} | |
| ns.update(_STATE_TABLE.get(s, {})) | |
| return ns | |
| # ββ CSS ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ # | |
| CSS = """<style> | |
| @keyframes av-glow-blue {0%,100%{box-shadow:0 0 0 0 rgba(129,140,248,.0),0 0 12px rgba(129,140,248,.25)} 50%{box-shadow:0 0 0 3px rgba(129,140,248,.15),0 0 28px rgba(129,140,248,.5)}} | |
| @keyframes av-glow-red {0%,100%{box-shadow:0 0 0 0 rgba(248,113,113,.0),0 0 12px rgba(248,113,113,.25)} 50%{box-shadow:0 0 0 3px rgba(248,113,113,.15),0 0 28px rgba(248,113,113,.5)}} | |
| @keyframes av-glow-amber {0%,100%{box-shadow:0 0 0 0 rgba(251,191,36,.0), 0 0 12px rgba(251,191,36,.2)} 50%{box-shadow:0 0 0 3px rgba(251,191,36,.12), 0 0 24px rgba(251,191,36,.45)}} | |
| @keyframes av-spin {to{transform:rotate(360deg)}} | |
| @keyframes av-pulse {0%,100%{opacity:1}50%{opacity:.35}} | |
| @keyframes av-slide {from{opacity:0;transform:translateY(-3px)}to{opacity:1;transform:translateY(0)}} | |
| @keyframes av-dot1 {0%,20%,100%{opacity:.2}10%{opacity:1}} | |
| @keyframes av-dot2 {0%,30%,100%{opacity:.2}20%{opacity:1}} | |
| @keyframes av-dot3 {0%,40%,100%{opacity:.2}30%{opacity:1}} | |
| @keyframes av-scan {0%{background-position:0% 0%}100%{background-position:0% 100%}} | |
| /* ββ Wrapper ββ */ | |
| .av-wrap{ | |
| font-family:var(--font-sans,system-ui,-apple-system,sans-serif); | |
| background:linear-gradient(160deg,#0d1117 0%,#0f172a 100%); | |
| border:1px solid rgba(255,255,255,.07); | |
| border-radius:14px; | |
| padding:14px 14px 10px; | |
| box-sizing:border-box; | |
| color:#e2e8f0; | |
| height:100%; | |
| } | |
| /* ββ Banner ββ */ | |
| .av-banner{ | |
| display:flex;align-items:center;gap:10px; | |
| padding:10px 12px; | |
| border-radius:10px; | |
| margin-bottom:10px; | |
| border:1px solid rgba(255,255,255,.06); | |
| background:rgba(255,255,255,.03); | |
| position:relative;overflow:hidden; | |
| } | |
| .av-banner.b-running{border-color:rgba(129,140,248,.3);background:rgba(129,140,248,.07);} | |
| .av-banner.b-error {border-color:rgba(248,113,113,.3);background:rgba(248,113,113,.07);} | |
| .av-banner.b-done {border-color:rgba(74,222,128,.3); background:rgba(74,222,128,.07);} | |
| .av-banner.b-warn {border-color:rgba(251,191,36,.3); background:rgba(251,191,36,.07);} | |
| .av-banner-scan{ | |
| position:absolute;inset:0;pointer-events:none; | |
| background:linear-gradient(to bottom,transparent 0%,rgba(129,140,248,.05) 50%,transparent 100%); | |
| background-size:100% 200%; | |
| animation:av-scan 2s linear infinite; | |
| } | |
| .av-banner.b-running .av-banner-scan{display:block} | |
| .av-banner:not(.b-running) .av-banner-scan{display:none} | |
| .av-beacon{ | |
| width:10px;height:10px;border-radius:50%;flex-shrink:0; | |
| background:rgba(100,100,120,.3); | |
| } | |
| .av-beacon.bc-run {background:#818cf8;animation:av-pulse .9s ease-in-out infinite;} | |
| .av-beacon.bc-err {background:#f87171;animation:av-pulse .9s ease-in-out infinite;} | |
| .av-beacon.bc-ok {background:#4ade80;} | |
| .av-beacon.bc-warn{background:#fbbf24;animation:av-pulse .9s ease-in-out infinite;} | |
| .av-banner-text{flex:1;font-size:11.5px;color:rgba(226,232,240,.8);line-height:1.4;} | |
| .av-retry-badge{ | |
| font-size:10px;font-weight:700;letter-spacing:.05em; | |
| color:#f87171;background:rgba(248,113,113,.12); | |
| padding:2px 8px;border-radius:10px;border:1px solid rgba(248,113,113,.25); | |
| flex-shrink:0; | |
| } | |
| /* ββ Section label ββ */ | |
| .av-section{ | |
| font-size:9px;font-weight:700;letter-spacing:.12em;text-transform:uppercase; | |
| color:rgba(226,232,240,.25);margin:8px 0 5px 2px; | |
| } | |
| /* ββ Agent card ββ */ | |
| .av-card{ | |
| display:flex;align-items:flex-start;gap:10px; | |
| padding:9px 11px; | |
| border-radius:10px; | |
| border:1px solid rgba(255,255,255,.05); | |
| background:rgba(255,255,255,.02); | |
| margin-bottom:5px; | |
| transition:border-color .25s,background .25s; | |
| position:relative;overflow:hidden; | |
| } | |
| .av-card.c-active{ | |
| border-color:rgba(129,140,248,.5); | |
| background:rgba(129,140,248,.08); | |
| animation:av-glow-blue 2s ease-in-out infinite; | |
| } | |
| .av-card.c-active.c-error-path{ | |
| border-color:rgba(248,113,113,.5); | |
| background:rgba(248,113,113,.08); | |
| animation:av-glow-red 2s ease-in-out infinite; | |
| } | |
| .av-card.c-done{ | |
| border-color:rgba(74,222,128,.2); | |
| background:rgba(74,222,128,.04); | |
| } | |
| .av-card.c-error{ | |
| border-color:rgba(248,113,113,.35); | |
| background:rgba(248,113,113,.06); | |
| } | |
| .av-card.c-idle{opacity:.45;} | |
| /* left-edge stripe */ | |
| .av-card::before{ | |
| content:'';position:absolute;left:0;top:0;bottom:0;width:3px; | |
| border-radius:10px 0 0 10px;background:transparent; | |
| } | |
| .av-card.c-active::before {background:linear-gradient(180deg,#818cf8,#a78bfa);} | |
| .av-card.c-active.c-error-path::before{background:linear-gradient(180deg,#f87171,#fca5a5);} | |
| .av-card.c-done::before {background:linear-gradient(180deg,#4ade80,#34d399);} | |
| .av-card.c-error::before {background:linear-gradient(180deg,#f87171,#fca5a5);} | |
| /* avatar */ | |
| .av-avatar{ | |
| width:34px;height:34px;border-radius:9px;flex-shrink:0; | |
| display:flex;align-items:center;justify-content:center; | |
| font-size:17px; | |
| background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.07); | |
| } | |
| .av-card.c-active .av-avatar{background:rgba(129,140,248,.15);border-color:rgba(129,140,248,.3);} | |
| .av-card.c-active.c-error-path .av-avatar{background:rgba(248,113,113,.15);border-color:rgba(248,113,113,.3);} | |
| .av-card.c-done .av-avatar{background:rgba(74,222,128,.12);border-color:rgba(74,222,128,.25);} | |
| .av-card.c-error .av-avatar{background:rgba(248,113,113,.12);border-color:rgba(248,113,113,.25);} | |
| /* text block */ | |
| .av-info{flex:1;min-width:0;} | |
| .av-name-row{display:flex;align-items:center;gap:6px;margin-bottom:2px;} | |
| .av-name{font-size:12.5px;font-weight:600;color:rgba(226,232,240,.9);} | |
| .av-card.c-idle .av-name{color:rgba(226,232,240,.4);} | |
| .av-model{ | |
| font-size:9.5px;font-weight:700;letter-spacing:.06em; | |
| padding:1px 5px;border-radius:5px; | |
| background:rgba(255,255,255,.06);color:rgba(226,232,240,.4); | |
| border:1px solid rgba(255,255,255,.07); | |
| } | |
| .av-card.c-active .av-model{background:rgba(129,140,248,.18);color:rgba(165,180,252,.9);border-color:rgba(129,140,248,.3);} | |
| .av-card.c-active.c-error-path .av-model{background:rgba(248,113,113,.18);color:rgba(252,165,165,.9);border-color:rgba(248,113,113,.3);} | |
| .av-role{font-size:10.5px;color:rgba(148,163,184,.6);} | |
| .av-card.c-active .av-role{color:rgba(148,163,184,.85);} | |
| /* thinking row */ | |
| .av-thinking{ | |
| display:flex;align-items:center;gap:5px; | |
| margin-top:5px;font-size:10.5px;color:rgba(129,140,248,.85); | |
| animation:av-slide .25s ease; | |
| } | |
| .av-thinking.t-error{color:rgba(248,113,113,.85);} | |
| .av-th-dots{display:flex;gap:2px;} | |
| .av-th-d{width:4px;height:4px;border-radius:50%;background:rgba(129,140,248,.8);} | |
| .t-error .av-th-d{background:rgba(248,113,113,.8);} | |
| .av-th-d:nth-child(1){animation:av-dot1 1.2s ease-in-out infinite;} | |
| .av-th-d:nth-child(2){animation:av-dot2 1.2s ease-in-out infinite;} | |
| .av-th-d:nth-child(3){animation:av-dot3 1.2s ease-in-out infinite;} | |
| /* status icon */ | |
| .av-status-icon{ | |
| width:18px;height:18px;border-radius:50%;flex-shrink:0; | |
| display:flex;align-items:center;justify-content:center; | |
| font-size:10px;font-weight:700;align-self:center; | |
| } | |
| .av-card.c-idle .av-status-icon{background:rgba(255,255,255,.04);color:rgba(148,163,184,.25);} | |
| .av-card.c-done .av-status-icon{background:rgba(74,222,128,.18);color:#4ade80;} | |
| .av-card.c-error .av-status-icon{background:rgba(248,113,113,.18);color:#f87171;} | |
| .av-spin-ring{ | |
| width:16px;height:16px;border-radius:50%; | |
| border:2px solid rgba(129,140,248,.2);border-top-color:#818cf8; | |
| animation:av-spin .75s linear infinite; | |
| } | |
| .c-error-path .av-spin-ring{border-color:rgba(248,113,113,.2);border-top-color:#f87171;} | |
| /* ββ Connector ββ */ | |
| .av-conn{display:block;width:100%;margin:0 0 5px 0;text-align:center;} | |
| .av-conn-line{ | |
| display:inline-block; | |
| width:2px;height:14px; | |
| background:linear-gradient(180deg,rgba(255,255,255,.07),rgba(255,255,255,.04)); | |
| border-radius:1px; | |
| } | |
| .av-conn-line.ln-done {background:linear-gradient(180deg,rgba(74,222,128,.3),rgba(74,222,128,.15));} | |
| .av-conn-line.ln-error{background:linear-gradient(180deg,rgba(248,113,113,.3),rgba(248,113,113,.15));} | |
| /* ββ Debug branch box ββ */ | |
| .av-debug-box{ | |
| margin:0 0 5px 0; | |
| padding:9px 9px 7px; | |
| border-radius:10px; | |
| border:1px solid rgba(248,113,113,.12); | |
| border-left:3px solid rgba(248,113,113,.15); | |
| background:rgba(248,113,113,.03); | |
| } | |
| .av-debug-box.db-active{ | |
| border-color:rgba(248,113,113,.3);border-left-color:#f87171; | |
| background:rgba(248,113,113,.07); | |
| animation:av-glow-red 2.5s ease-in-out infinite; | |
| } | |
| .av-debug-label{ | |
| font-size:9px;font-weight:700;letter-spacing:.1em;text-transform:uppercase; | |
| color:rgba(248,113,113,.35);margin-bottom:7px; | |
| display:flex;align-items:center;gap:5px; | |
| } | |
| .av-debug-box.db-active .av-debug-label{color:rgba(248,113,113,.7);} | |
| .av-debug-label-icon{ | |
| width:14px;height:14px;border-radius:50%; | |
| background:rgba(248,113,113,.15); | |
| display:flex;align-items:center;justify-content:center;font-size:8px; | |
| } | |
| /* mini agent inside debug box */ | |
| .av-mini{ | |
| display:flex;align-items:center;gap:8px; | |
| padding:6px 8px;border-radius:8px; | |
| border:1px solid rgba(255,255,255,.04); | |
| background:rgba(255,255,255,.02);margin-bottom:4px; | |
| } | |
| .av-mini:last-child{margin-bottom:0;} | |
| .av-mini.m-active{border-color:rgba(248,113,113,.35);background:rgba(248,113,113,.07);} | |
| .av-mini.m-done {border-color:rgba(74,222,128,.2);background:rgba(74,222,128,.04);} | |
| .av-mini.m-error {border-color:rgba(248,113,113,.2);} | |
| .av-mini.m-idle {opacity:.4;} | |
| .av-mini-avatar{ | |
| font-size:14px;width:26px;height:26px;border-radius:7px;flex-shrink:0; | |
| display:flex;align-items:center;justify-content:center; | |
| background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.06); | |
| } | |
| .av-mini.m-active .av-mini-avatar{background:rgba(248,113,113,.12);border-color:rgba(248,113,113,.25);} | |
| .av-mini.m-done .av-mini-avatar{background:rgba(74,222,128,.1);border-color:rgba(74,222,128,.2);} | |
| .av-mini-info{flex:1;} | |
| .av-mini-name{font-size:11px;font-weight:500;color:rgba(226,232,240,.85);} | |
| .av-mini.m-idle .av-mini-name{color:rgba(226,232,240,.35);} | |
| .av-mini-role{font-size:9.5px;color:rgba(148,163,184,.5);} | |
| .av-mini-right{display:flex;align-items:center;gap:4px;} | |
| .av-mini-model{ | |
| font-size:9px;font-weight:600;padding:1px 5px;border-radius:4px; | |
| background:rgba(255,255,255,.05);color:rgba(148,163,184,.45); | |
| } | |
| .av-mini.m-active .av-mini-model{background:rgba(248,113,113,.15);color:rgba(252,165,165,.8);} | |
| .av-mini-spin{ | |
| width:12px;height:12px;border-radius:50%; | |
| border:1.5px solid rgba(248,113,113,.2);border-top-color:#f87171; | |
| animation:av-spin .75s linear infinite; | |
| } | |
| .av-mini-tick {font-size:10px;color:#4ade80;} | |
| .av-mini-cross{font-size:10px;color:#f87171;} | |
| /* ββ Legend ββ */ | |
| .av-legend{ | |
| display:flex;flex-wrap:wrap;gap:10px; | |
| margin-top:8px;padding-top:7px; | |
| border-top:1px solid rgba(255,255,255,.06); | |
| font-size:9.5px;color:rgba(148,163,184,.4); | |
| } | |
| </style>""" | |
| # ββ HTML builders βββββββββββββββββββββββββββββββββββββββββββββββββ # | |
| def _conn(prev_state: str) -> str: | |
| cls = "ln-done" if prev_state == "done" else ("ln-error" if prev_state == "error" else "") | |
| return f'<div class="av-conn"><div class="av-conn-line {cls}"></div></div>' | |
| def _agent_card(node_id: str, ns: dict, ma: dict, error_path: bool = False) -> str: | |
| state = ns.get(node_id, "idle") | |
| a = AGENTS[node_id] | |
| card_cls = f"c-{state}" | |
| if state == "active" and error_path: | |
| card_cls += " c-error-path" | |
| if state == "active": | |
| status_html = '<div class="av-spin-ring"></div>' | |
| elif state == "done": | |
| status_html = '<div class="av-status-icon">β</div>' | |
| elif state == "error": | |
| status_html = '<div class="av-status-icon">β</div>' | |
| else: | |
| status_html = '<div class="av-status-icon">Β·</div>' | |
| thinking = "" | |
| if state == "active": | |
| t_cls = "t-error" if error_path else "" | |
| thinking = ( | |
| f'<div class="av-thinking {t_cls}">' | |
| f'<div class="av-th-dots">' | |
| f'<div class="av-th-d"></div><div class="av-th-d"></div><div class="av-th-d"></div>' | |
| f'</div>' | |
| f'{THINKING_MSGS.get(node_id, "Working...")}' | |
| f'</div>' | |
| ) | |
| label = _model_label(ma, a.get("ma_key")) | |
| model_badge = f'<span class="av-model">{label}</span>' if label else "" | |
| return ( | |
| f'<div class="av-card {card_cls}">' | |
| f'<div class="av-avatar">{a["icon"]}</div>' | |
| f'<div class="av-info">' | |
| f'<div class="av-name-row">' | |
| f'<span class="av-name">{a["name"]}</span>' | |
| f'{model_badge}' | |
| f'</div>' | |
| f'<div class="av-role">{a["role"]}</div>' | |
| f'{thinking}' | |
| f'</div>' | |
| f'{status_html}' | |
| f'</div>' | |
| ) | |
| def _mini_card(node_id: str, ns: dict, ma: dict) -> str: | |
| state = ns.get(node_id, "idle") | |
| a = AGENTS[node_id] | |
| if state == "active": | |
| right = '<div class="av-mini-spin"></div>' | |
| elif state == "done": | |
| right = '<span class="av-mini-tick">β</span>' | |
| elif state == "error": | |
| right = '<span class="av-mini-cross">β</span>' | |
| else: | |
| right = "" | |
| label = _model_label(ma, a.get("ma_key")) | |
| model_html = f'<span class="av-mini-model">{label}</span>' if label else "" | |
| return ( | |
| f'<div class="av-mini m-{state}">' | |
| f'<div class="av-mini-avatar">{a["icon"]}</div>' | |
| f'<div class="av-mini-info">' | |
| f'<div class="av-mini-name">{a["name"]}</div>' | |
| f'<div class="av-mini-role">{a["role"]}</div>' | |
| f'</div>' | |
| f'<div class="av-mini-right">{model_html}{right}</div>' | |
| f'</div>' | |
| ) | |
| def _debug_box(ns: dict, ma: dict) -> str: | |
| is_active = any(ns.get(n, "idle") != "idle" for n in ("classify", "debug", "hitl")) | |
| db_cls = "db-active" if is_active else "" | |
| return ( | |
| f'<div class="av-debug-box {db_cls}">' | |
| f'<div class="av-debug-label">' | |
| f'<div class="av-debug-label-icon">β‘</div>' | |
| f'Debug loop β auto-retry' | |
| f'</div>' | |
| + _mini_card("classify", ns, ma) | |
| + _mini_card("debug", ns, ma) | |
| + _mini_card("hitl", ns, ma) | |
| + '</div>' | |
| ) | |
| # ββ Main builder βββββββββββββββββββββββββββββββββββββββββββββββββββ # | |
| def _build(status: PipelineStatus, iterations: int, is_error: bool, ma: dict) -> str: | |
| s = status.value if hasattr(status, "value") else str(status) | |
| ns = _get_ns(status) | |
| kind, msg = STATUS_MSGS.get(s, ("idle", s)) | |
| bc = {"running": "bc-run", "error": "bc-err", "done": "bc-ok", "warn": "bc-warn"}.get(kind, "") | |
| banner_cls = {"running": "b-running", "error": "b-error", "done": "b-done", "warn": "b-warn"}.get(kind, "") | |
| retry = ( | |
| f'<span class="av-retry-badge">retry {iterations}</span>' | |
| if iterations > 0 and s not in ("idle", "success") else "" | |
| ) | |
| profile = ma.get("profile", "") | |
| profile_labels = { | |
| "reasoning": ("π’", "#818cf8", "Reasoning mode"), | |
| "sql": ("ποΈ", "#38bdf8", "SQL mode"), | |
| "coding": ("β¨οΈ", "#a78bfa", "Coding mode"), | |
| "balanced": ("βοΈ", "#94a3b8", "Balanced"), | |
| "manual": ("ποΈ", "#fb923c", "Manual"), | |
| } | |
| profile_badge = "" | |
| if profile and profile in profile_labels: | |
| p_icon, p_color, p_label = profile_labels[profile] | |
| reason_tip = ma.get("reason", "") | |
| profile_badge = ( | |
| f'<span title="{reason_tip}" style="font-size:9px;font-weight:700;' | |
| f'padding:2px 7px;border-radius:8px;border:1px solid {p_color}44;' | |
| f'color:{p_color};background:{p_color}18;flex-shrink:0;cursor:help;">' | |
| f'{p_icon} {p_label}</span>' | |
| ) | |
| debug_active = any(ns.get(n, "idle") != "idle" for n in ("classify", "debug", "hitl")) | |
| exec_state = ns.get("exec", "idle") | |
| html = ( | |
| CSS | |
| + '<div class="av-wrap">' | |
| + f'<div class="av-banner {banner_cls}" style="gap:8px;">' | |
| + f'<div class="av-banner-scan"></div>' | |
| + f'<div class="av-beacon {bc}"></div>' | |
| + f'<div class="av-banner-text">{msg}</div>' | |
| + f'{profile_badge}' | |
| + f'{retry}' | |
| + '</div>' | |
| + '<div class="av-section">Agent pipeline</div>' | |
| + _agent_card("clarify", ns, ma) | |
| + _conn(ns.get("clarify", "idle")) | |
| + _agent_card("detect", ns, ma) | |
| + _conn(ns.get("detect", "idle")) | |
| + _agent_card("plan", ns, ma) | |
| + _conn(ns.get("plan", "idle")) | |
| + _agent_card("gen", ns, ma) | |
| + _conn(ns.get("gen", "idle")) | |
| + _agent_card("exec", ns, ma) | |
| + _conn(exec_state) | |
| ) | |
| html += _debug_box(ns, ma) | |
| html += _conn(exec_state if not debug_active else ns.get("debug", ns.get("hitl", exec_state))) | |
| html += ( | |
| _agent_card("test", ns, ma) | |
| + _conn(ns.get("test", "idle")) | |
| + _agent_card("explain", ns, ma) | |
| + _conn(ns.get("explain", "idle")) | |
| + _agent_card("complete", ns, ma) | |
| + '<div class="av-legend">' | |
| + '<span>Β· idle</span>' | |
| + '<span style="color:#818cf8">β³ active</span>' | |
| + '<span style="color:#4ade80">β done</span>' | |
| + '<span style="color:#f87171">β error</span>' | |
| + '<span style="color:rgba(248,113,113,.5)">β‘ debug loop</span>' | |
| + '</div>' | |
| + '</div>' | |
| ) | |
| return html | |