Honest-failure scoring (no demo fallback) + radar/resilience fixes
Browse files- ui/app.py +32 -31
- ui/screens/processing.py +7 -0
- ui/theme.py +5 -0
ui/app.py
CHANGED
|
@@ -97,49 +97,51 @@ def run(name, parsed, rm_flag):
|
|
| 97 |
# history is thousands of model calls, so run it in a worker thread and stream a REAL progress bar
|
| 98 |
# (ticked per completed call) rather than freezing the screen. A live failure degrades to the dummy.
|
| 99 |
scorer = get_scorer()
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
|
|
|
|
|
|
|
| 108 |
box: dict = {}
|
|
|
|
| 109 |
def _work():
|
| 110 |
try:
|
| 111 |
box["result"] = scorer.score(parsed, progress=progress)
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
from .scoring import DummyScorer
|
| 115 |
-
d = DummyScorer()
|
| 116 |
-
box["result"] = d.score(parsed)
|
| 117 |
-
box["used"] = d
|
| 118 |
|
| 119 |
bar = len(lines)
|
| 120 |
-
lines.append(P.progress_bar(0, progress.total
|
| 121 |
yield _frame(lines, intake_v=HIDE, proc_v=SHOW, result_v=HIDE)
|
| 122 |
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
while th.is_alive():
|
| 127 |
-
done, total = progress.snapshot()
|
| 128 |
-
lines[bar] = P.progress_bar(done, total)
|
| 129 |
-
yield _frame(lines, intake_v=HIDE, proc_v=SHOW, result_v=HIDE)
|
| 130 |
-
time.sleep(0.4)
|
| 131 |
-
th.join()
|
| 132 |
done, total = progress.snapshot()
|
| 133 |
-
lines[bar] = P.progress_bar(
|
| 134 |
yield _frame(lines, intake_v=HIDE, proc_v=SHOW, result_v=HIDE)
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
|
| 139 |
result = box["result"]
|
| 140 |
-
used = box["used"]
|
| 141 |
card = score_to_card(result, name)
|
| 142 |
-
card.provenance = _provenance(
|
| 143 |
card.single_conversation = (getattr(parsed, "source", "") == "paste")
|
| 144 |
body_updates = [gr.update(value=render_accordion_body(a, card.provenance)) for a in card.axes]
|
| 145 |
yield _frame(lines, intake_v=HIDE, proc_v=HIDE, result_v=SHOW,
|
|
@@ -154,8 +156,7 @@ def run_paste(name, pasted, rm_flag):
|
|
| 154 |
text = pasted or ""
|
| 155 |
parsed = fetch_share(text) if is_share_url(text) else parse_paste(text)
|
| 156 |
except ParseError as e:
|
| 157 |
-
|
| 158 |
-
yield _frame([msg], intake_v=SHOW, proc_v=HIDE, result_v=HIDE)
|
| 159 |
return
|
| 160 |
yield from run(name, parsed, rm_flag)
|
| 161 |
|
|
|
|
| 97 |
# history is thousands of model calls, so run it in a worker thread and stream a REAL progress bar
|
| 98 |
# (ticked per completed call) rather than freezing the screen. A live failure degrades to the dummy.
|
| 99 |
scorer = get_scorer()
|
| 100 |
+
# HONEST FAILURE: never show fake "demo" scores. If there's no real backend, or scoring errors out,
|
| 101 |
+
# surface a clear message — not a DummyScorer card.
|
| 102 |
+
if type(scorer).__name__ != "ObservableScorer":
|
| 103 |
+
lines.append(P.error("Scoring backend not configured. Set OPENBMB_BASE_URL / OPENBMB_TOKEN "
|
| 104 |
+
"(see DEPLOY.md), then reload and try again."))
|
| 105 |
+
yield _frame(lines, intake_v=HIDE, proc_v=SHOW, result_v=HIDE)
|
| 106 |
+
return
|
| 107 |
|
| 108 |
+
from prompt_card.observable_pipeline import Progress
|
| 109 |
+
progress = Progress()
|
| 110 |
box: dict = {}
|
| 111 |
+
|
| 112 |
def _work():
|
| 113 |
try:
|
| 114 |
box["result"] = scorer.score(parsed, progress=progress)
|
| 115 |
+
except Exception as e: # captured, surfaced as a real error frame below
|
| 116 |
+
box["error"] = e
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
|
| 118 |
bar = len(lines)
|
| 119 |
+
lines.append(P.progress_bar(0, progress.total))
|
| 120 |
yield _frame(lines, intake_v=HIDE, proc_v=SHOW, result_v=HIDE)
|
| 121 |
|
| 122 |
+
th = threading.Thread(target=_work, daemon=True)
|
| 123 |
+
th.start()
|
| 124 |
+
while th.is_alive():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
done, total = progress.snapshot()
|
| 126 |
+
lines[bar] = P.progress_bar(done, total)
|
| 127 |
yield _frame(lines, intake_v=HIDE, proc_v=SHOW, result_v=HIDE)
|
| 128 |
+
time.sleep(0.4)
|
| 129 |
+
th.join()
|
| 130 |
+
|
| 131 |
+
if "error" in box:
|
| 132 |
+
e = box["error"]
|
| 133 |
+
lines[bar] = P.error(f"Scoring failed — {type(e).__name__}: {str(e)[:240]}")
|
| 134 |
+
lines.append(P.fact("No demo scores are shown. Check the model endpoint / secrets, then reload "
|
| 135 |
+
"and try again.", muted=True))
|
| 136 |
+
yield _frame(lines, intake_v=HIDE, proc_v=SHOW, result_v=HIDE)
|
| 137 |
+
return
|
| 138 |
+
|
| 139 |
+
done, total = progress.snapshot()
|
| 140 |
+
lines[bar] = P.progress_bar(total or done, total or done) # snap to 100%
|
| 141 |
|
| 142 |
result = box["result"]
|
|
|
|
| 143 |
card = score_to_card(result, name)
|
| 144 |
+
card.provenance = _provenance(scorer)
|
| 145 |
card.single_conversation = (getattr(parsed, "source", "") == "paste")
|
| 146 |
body_updates = [gr.update(value=render_accordion_body(a, card.provenance)) for a in card.axes]
|
| 147 |
yield _frame(lines, intake_v=HIDE, proc_v=HIDE, result_v=SHOW,
|
|
|
|
| 156 |
text = pasted or ""
|
| 157 |
parsed = fetch_share(text) if is_share_url(text) else parse_paste(text)
|
| 158 |
except ParseError as e:
|
| 159 |
+
yield _frame([P.error(str(e))], intake_v=HIDE, proc_v=SHOW, result_v=HIDE)
|
|
|
|
| 160 |
return
|
| 161 |
yield from run(name, parsed, rm_flag)
|
| 162 |
|
ui/screens/processing.py
CHANGED
|
@@ -23,6 +23,13 @@ def fact(text: str, muted: bool = False) -> str:
|
|
| 23 |
return f"<div class='{cls}'><span class='dot'></span><span>{text}</span></div>"
|
| 24 |
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
def fact_num(n: int, label: str) -> str:
|
| 27 |
return (f"<div class='omc-fact'><span class='dot'></span>"
|
| 28 |
f"<span><span class='num'>{n:,}</span> {label}</span></div>")
|
|
|
|
| 23 |
return f"<div class='{cls}'><span class='dot'></span><span>{text}</span></div>"
|
| 24 |
|
| 25 |
|
| 26 |
+
def error(text: str) -> str:
|
| 27 |
+
"""A prominent error row — shown instead of a fake card when scoring can't produce real results."""
|
| 28 |
+
from html import escape
|
| 29 |
+
return (f"<div class='omc-error'><span class='dot'></span>"
|
| 30 |
+
f"<span><b>⚠ Couldn't score this</b><br>{escape(text)}</span></div>")
|
| 31 |
+
|
| 32 |
+
|
| 33 |
def fact_num(n: int, label: str) -> str:
|
| 34 |
return (f"<div class='omc-fact'><span class='dot'></span>"
|
| 35 |
f"<span><span class='num'>{n:,}</span> {label}</span></div>")
|
ui/theme.py
CHANGED
|
@@ -160,6 +160,11 @@ footer {{ display: none !important; }}
|
|
| 160 |
.omc-fact .dot {{ width: 7px; height: 7px; border-radius: 50%; background: {TEAL}; flex: none; }}
|
| 161 |
.omc-fact .num {{ font-family: 'Saira', sans-serif; font-weight: 600; color: {TEAL}; }}
|
| 162 |
.omc-fact.muted {{ color: {TXT2}; }}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
.omc-langbar {{ height: 8px; border-radius: 4px; overflow: hidden; display: flex; width: 100%; margin-top: 6px; }}
|
| 164 |
.omc-langbar .en {{ background: {TEAL}; }}
|
| 165 |
.omc-langbar .other {{ background: {BORDER}; }}
|
|
|
|
| 160 |
.omc-fact .dot {{ width: 7px; height: 7px; border-radius: 50%; background: {TEAL}; flex: none; }}
|
| 161 |
.omc-fact .num {{ font-family: 'Saira', sans-serif; font-weight: 600; color: {TEAL}; }}
|
| 162 |
.omc-fact.muted {{ color: {TXT2}; }}
|
| 163 |
+
.omc-error {{ display: flex; align-items: flex-start; gap: 10px; color: #F8B4B4; font-size: 14px;
|
| 164 |
+
line-height: 1.5; padding: 12px 14px; margin: 10px 0; background: rgba(220,80,80,.10);
|
| 165 |
+
border: 1px solid rgba(220,80,80,.45); border-radius: 10px; }}
|
| 166 |
+
.omc-error .dot {{ width: 7px; height: 7px; border-radius: 50%; background: #E25858; flex: none; margin-top: 6px; }}
|
| 167 |
+
.omc-error b {{ color: #F3C0C0; font-family: 'Saira', sans-serif; }}
|
| 168 |
.omc-langbar {{ height: 8px; border-radius: 4px; overflow: hidden; display: flex; width: 100%; margin-top: 6px; }}
|
| 169 |
.omc-langbar .en {{ background: {TEAL}; }}
|
| 170 |
.omc-langbar .other {{ background: {BORDER}; }}
|