Spaces:
Sleeping
Sleeping
Deploy public authenticated AutoML gateway
Browse files- gateway_app.py +221 -40
- gateway_catalog.json +12 -0
- gateway_contract.py +149 -6
gateway_app.py
CHANGED
|
@@ -12,11 +12,11 @@ import tempfile
|
|
| 12 |
import time
|
| 13 |
import uuid
|
| 14 |
from collections import Counter
|
| 15 |
-
from concurrent.futures import ThreadPoolExecutor
|
| 16 |
from dataclasses import dataclass, replace
|
| 17 |
from datetime import datetime, timezone
|
| 18 |
from pathlib import Path
|
| 19 |
-
from typing import Any, Mapping
|
| 20 |
|
| 21 |
import gradio as gr
|
| 22 |
import pandas as pd
|
|
@@ -83,6 +83,7 @@ from gateway_contract import (
|
|
| 83 |
check_upload_size,
|
| 84 |
inspect_package_manifest,
|
| 85 |
load_gateway_catalog,
|
|
|
|
| 86 |
predictions_frame,
|
| 87 |
)
|
| 88 |
from gateway_jobs import ROUTED_JOB_ADMISSION, JobRejected
|
|
@@ -1016,6 +1017,36 @@ def _context_item(label: str, value: str) -> str:
|
|
| 1016 |
)
|
| 1017 |
|
| 1018 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1019 |
def run_context_html(
|
| 1020 |
context: RunContext,
|
| 1021 |
status_text: str,
|
|
@@ -1066,6 +1097,7 @@ def run_context_html(
|
|
| 1066 |
'<div class="run-alias-row"><span class="run-context-label">Run alias</span>'
|
| 1067 |
f'<code class="run-alias">{alias}</code>'
|
| 1068 |
f'<button type="button" class="run-id-copy" data-copy-value="{copy_value}">Copy</button></div>'
|
|
|
|
| 1069 |
f'<div class="run-context-list">{visible}</div>{technical}</section>'
|
| 1070 |
)
|
| 1071 |
|
|
@@ -1152,9 +1184,13 @@ def format_metrics_markdown(text: str) -> str:
|
|
| 1152 |
|
| 1153 |
|
| 1154 |
def candidate_comparison_markdown(summary: str) -> str:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1155 |
lines = str(summary or "").splitlines()
|
| 1156 |
start = next(
|
| 1157 |
-
(index for index, line in enumerate(lines) if re.match(r"^#{1,6}\s+
|
| 1158 |
None,
|
| 1159 |
)
|
| 1160 |
if start is None:
|
|
@@ -1539,6 +1575,14 @@ def render_training_result(
|
|
| 1539 |
str(envelope.get("summary") or "")
|
| 1540 |
)
|
| 1541 |
candidate_comparison = candidate_comparison_markdown(summary)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1542 |
download = None
|
| 1543 |
if package_path:
|
| 1544 |
try:
|
|
@@ -1599,6 +1643,53 @@ def _route_label(profile: str) -> str:
|
|
| 1599 |
return f"Private {str(profile or '').upper()}"
|
| 1600 |
|
| 1601 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1602 |
def run_existing_remote(
|
| 1603 |
payload: Mapping[str, Any],
|
| 1604 |
session_token: str | None,
|
|
@@ -1649,11 +1740,13 @@ def run_existing_remote(
|
|
| 1649 |
waking_status,
|
| 1650 |
context_html=run_context_html(context, waking_status),
|
| 1651 |
)
|
| 1652 |
-
execution =
|
| 1653 |
-
|
| 1654 |
-
|
| 1655 |
-
|
| 1656 |
-
|
|
|
|
|
|
|
| 1657 |
)
|
| 1658 |
result = execution.backend_result
|
| 1659 |
saved_model_record = execution.saved_model_record
|
|
@@ -1737,28 +1830,29 @@ def run_uploaded_remote(
|
|
| 1737 |
queued_status,
|
| 1738 |
context_html=run_context_html(context, queued_status),
|
| 1739 |
)
|
| 1740 |
-
|
| 1741 |
-
|
| 1742 |
-
|
| 1743 |
-
|
| 1744 |
-
|
| 1745 |
-
|
| 1746 |
-
|
| 1747 |
-
)
|
| 1748 |
-
|
| 1749 |
-
|
| 1750 |
-
|
| 1751 |
-
|
| 1752 |
-
|
| 1753 |
-
|
| 1754 |
-
|
| 1755 |
-
|
| 1756 |
-
|
| 1757 |
-
|
| 1758 |
-
|
| 1759 |
-
|
| 1760 |
-
|
| 1761 |
-
|
|
|
|
| 1762 |
except BackendUnavailable as exc:
|
| 1763 |
if decision is not None and decision.profile == "cpu":
|
| 1764 |
yield _training_error(
|
|
@@ -2629,6 +2723,38 @@ def _guided_run_button_ready():
|
|
| 2629 |
return gr.update(value="Run selected pipeline", interactive=True)
|
| 2630 |
|
| 2631 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2632 |
def _workbench_state_for_variant(variant: Any) -> WorkbenchState:
|
| 2633 |
approved = dict(variant.approved_research or {})
|
| 2634 |
return state_from_mapping(
|
|
@@ -3360,7 +3486,7 @@ def _add_results_section(label: str = "Latest run"):
|
|
| 3360 |
elem_classes=["run-status-source"],
|
| 3361 |
)
|
| 3362 |
with gr.Tabs(elem_classes=["guided-result-tabs"]):
|
| 3363 |
-
with gr.TabItem("Overview"):
|
| 3364 |
context = gr.HTML(
|
| 3365 |
RESULTS_PLACEHOLDER_HTML,
|
| 3366 |
elem_classes=["run-context-output"],
|
|
@@ -3594,7 +3720,7 @@ def create_interface() -> gr.Blocks:
|
|
| 3594 |
)
|
| 3595 |
|
| 3596 |
with gr.Tabs(visible=False, elem_id="workspace-tabs") as main_tabs:
|
| 3597 |
-
with gr.TabItem("Build"):
|
| 3598 |
with gr.Row(
|
| 3599 |
equal_height=False,
|
| 3600 |
elem_classes=["guided-instrument-bench", "studio-workbench"],
|
|
@@ -3670,6 +3796,13 @@ def create_interface() -> gr.Blocks:
|
|
| 3670 |
label="Test data (CSV, optional)",
|
| 3671 |
file_types=[".csv"],
|
| 3672 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3673 |
|
| 3674 |
with gr.Group(
|
| 3675 |
visible=False,
|
|
@@ -3687,12 +3820,14 @@ def create_interface() -> gr.Blocks:
|
|
| 3687 |
value=default_target,
|
| 3688 |
label="Outcome to predict",
|
| 3689 |
allow_custom_value=True,
|
|
|
|
| 3690 |
)
|
| 3691 |
with gr.Group(visible=False) as upload_target_group:
|
| 3692 |
upload_target = gr.Dropdown(
|
| 3693 |
[],
|
| 3694 |
label="Outcome to predict",
|
| 3695 |
allow_custom_value=True,
|
|
|
|
| 3696 |
)
|
| 3697 |
with gr.Row():
|
| 3698 |
task_type = gr.Dropdown(
|
|
@@ -3744,6 +3879,13 @@ def create_interface() -> gr.Blocks:
|
|
| 3744 |
),
|
| 3745 |
interactive=initial_image,
|
| 3746 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3747 |
|
| 3748 |
with gr.Group(
|
| 3749 |
visible=False,
|
|
@@ -3755,6 +3897,16 @@ def create_interface() -> gr.Blocks:
|
|
| 3755 |
'<p>The private research backend compares registered '
|
| 3756 |
'pipelines. Recommendations never start training.</p></div>'
|
| 3757 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3758 |
generate_plan_button = gr.Button(
|
| 3759 |
"Generate research plan",
|
| 3760 |
variant="secondary",
|
|
@@ -4190,8 +4342,11 @@ def create_interface() -> gr.Blocks:
|
|
| 4190 |
interactive=False,
|
| 4191 |
elem_classes=["guided-parallel-action"],
|
| 4192 |
)
|
|
|
|
|
|
|
|
|
|
| 4193 |
|
| 4194 |
-
with gr.TabItem("Runs"):
|
| 4195 |
gr.HTML(
|
| 4196 |
"""
|
| 4197 |
<section class="guided-workbench-heading">
|
|
@@ -4200,15 +4355,15 @@ def create_interface() -> gr.Blocks:
|
|
| 4200 |
</section>
|
| 4201 |
"""
|
| 4202 |
)
|
| 4203 |
-
with gr.Tabs():
|
| 4204 |
-
with gr.TabItem("Latest run"):
|
| 4205 |
latest_outputs = _add_results_section("Latest run")
|
| 4206 |
latest_visual_staging_state = gr.State(
|
| 4207 |
_staged_visual_updates(())
|
| 4208 |
)
|
| 4209 |
-
with gr.TabItem("Variant 1"):
|
| 4210 |
variant_one_outputs = _add_results_section("Variant 1")
|
| 4211 |
-
with gr.TabItem("Variant 2"):
|
| 4212 |
variant_two_outputs = _add_results_section("Variant 2")
|
| 4213 |
|
| 4214 |
with gr.TabItem("Models") as replay_tab:
|
|
@@ -5036,6 +5191,8 @@ def create_interface() -> gr.Blocks:
|
|
| 5036 |
guided_run_ui_handler_staged,
|
| 5037 |
guided_run_inputs,
|
| 5038 |
[*latest_outputs[:16], latest_visual_staging_state],
|
|
|
|
|
|
|
| 5039 |
api_name=False,
|
| 5040 |
)
|
| 5041 |
primary_run_event = primary_run_event.then(
|
|
@@ -5050,20 +5207,43 @@ def create_interface() -> gr.Blocks:
|
|
| 5050 |
latest_outputs[16],
|
| 5051 |
api_name=False,
|
| 5052 |
)
|
| 5053 |
-
primary_run_event.then(
|
| 5054 |
-
|
| 5055 |
inputs=[],
|
| 5056 |
outputs=[primary_run_button],
|
| 5057 |
queue=False,
|
| 5058 |
api_name=False,
|
| 5059 |
)
|
| 5060 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5061 |
guided_parallel_plan_ui_handler,
|
| 5062 |
[workbench_state, session_token],
|
| 5063 |
[*variant_one_outputs, *variant_two_outputs],
|
| 5064 |
api_name=False,
|
| 5065 |
concurrency_limit=2,
|
| 5066 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5067 |
next_experiment_outputs = [
|
| 5068 |
workbench_state,
|
| 5069 |
user_objective,
|
|
@@ -5148,3 +5328,4 @@ __all__ = [
|
|
| 5148 |
"replay_route_preview",
|
| 5149 |
"validate_gateway_security_config",
|
| 5150 |
]
|
|
|
|
|
|
| 12 |
import time
|
| 13 |
import uuid
|
| 14 |
from collections import Counter
|
| 15 |
+
from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutureTimeoutError
|
| 16 |
from dataclasses import dataclass, replace
|
| 17 |
from datetime import datetime, timezone
|
| 18 |
from pathlib import Path
|
| 19 |
+
from typing import Any, Callable, Mapping
|
| 20 |
|
| 21 |
import gradio as gr
|
| 22 |
import pandas as pd
|
|
|
|
| 83 |
check_upload_size,
|
| 84 |
inspect_package_manifest,
|
| 85 |
load_gateway_catalog,
|
| 86 |
+
model_leaderboard_markdown,
|
| 87 |
predictions_frame,
|
| 88 |
)
|
| 89 |
from gateway_jobs import ROUTED_JOB_ADMISSION, JobRejected
|
|
|
|
| 1017 |
)
|
| 1018 |
|
| 1019 |
|
| 1020 |
+
def run_progress_html(status_text: str, *, duration: str = "") -> str:
|
| 1021 |
+
"""Show honest indeterminate progress until the backend returns a result."""
|
| 1022 |
+
|
| 1023 |
+
state = _run_state(status_text)
|
| 1024 |
+
if state == "complete":
|
| 1025 |
+
progress = '<progress value="1" max="1" aria-label="Run completed" style="width:100%;height:10px"></progress>'
|
| 1026 |
+
detail = "Results are ready in the Runs tab."
|
| 1027 |
+
elif state in {"failed", "cancelled"}:
|
| 1028 |
+
progress = ""
|
| 1029 |
+
detail = "Review the run status before starting another run."
|
| 1030 |
+
else:
|
| 1031 |
+
progress = '<progress max="1" aria-label="Waiting for the training result" style="width:100%;height:10px"></progress>'
|
| 1032 |
+
detail = "Waiting for the private backend to return results."
|
| 1033 |
+
elapsed = f" Elapsed: {html.escape(duration)}." if duration else ""
|
| 1034 |
+
return (
|
| 1035 |
+
'<section class="run-progress-feedback" aria-label="Run progress">'
|
| 1036 |
+
f'<p role="status">{html.escape(str(status_text))}</p>{progress}'
|
| 1037 |
+
f'<p>{detail}{elapsed}</p></section>'
|
| 1038 |
+
)
|
| 1039 |
+
|
| 1040 |
+
|
| 1041 |
+
def _run_progress_update(context_html: str):
|
| 1042 |
+
match = re.search(
|
| 1043 |
+
r'<section class="run-progress-feedback"[^>]*>.*?</section>',
|
| 1044 |
+
str(context_html or ""),
|
| 1045 |
+
re.DOTALL,
|
| 1046 |
+
)
|
| 1047 |
+
return gr.update(value=match.group(0) if match else "", visible=bool(match))
|
| 1048 |
+
|
| 1049 |
+
|
| 1050 |
def run_context_html(
|
| 1051 |
context: RunContext,
|
| 1052 |
status_text: str,
|
|
|
|
| 1097 |
'<div class="run-alias-row"><span class="run-context-label">Run alias</span>'
|
| 1098 |
f'<code class="run-alias">{alias}</code>'
|
| 1099 |
f'<button type="button" class="run-id-copy" data-copy-value="{copy_value}">Copy</button></div>'
|
| 1100 |
+
f'{run_progress_html(status_text, duration=duration)}'
|
| 1101 |
f'<div class="run-context-list">{visible}</div>{technical}</section>'
|
| 1102 |
)
|
| 1103 |
|
|
|
|
| 1184 |
|
| 1185 |
|
| 1186 |
def candidate_comparison_markdown(summary: str) -> str:
|
| 1187 |
+
return _summary_section(summary, "Candidate comparison")
|
| 1188 |
+
|
| 1189 |
+
|
| 1190 |
+
def _summary_section(summary: str, heading: str) -> str:
|
| 1191 |
lines = str(summary or "").splitlines()
|
| 1192 |
start = next(
|
| 1193 |
+
(index for index, line in enumerate(lines) if re.match(r"^#{1,6}\s+" + re.escape(heading) + r"\s*$", line.strip(), re.I)),
|
| 1194 |
None,
|
| 1195 |
)
|
| 1196 |
if start is None:
|
|
|
|
| 1575 |
str(envelope.get("summary") or "")
|
| 1576 |
)
|
| 1577 |
candidate_comparison = candidate_comparison_markdown(summary)
|
| 1578 |
+
leaderboard = model_leaderboard_markdown(envelope.get("model_leaderboard"))
|
| 1579 |
+
leaderboard = leaderboard or _summary_section(summary, "AutoGluon model leaderboard")
|
| 1580 |
+
if leaderboard:
|
| 1581 |
+
candidate_comparison = "\n\n".join(
|
| 1582 |
+
section for section in (leaderboard, candidate_comparison) if section
|
| 1583 |
+
)
|
| 1584 |
+
if _run_state(status_text) != "complete":
|
| 1585 |
+
status_text = "Training complete. Your results are ready for review."
|
| 1586 |
download = None
|
| 1587 |
if package_path:
|
| 1588 |
try:
|
|
|
|
| 1643 |
return f"Private {str(profile or '').upper()}"
|
| 1644 |
|
| 1645 |
|
| 1646 |
+
RUN_HEARTBEAT_SECONDS = 2.0
|
| 1647 |
+
|
| 1648 |
+
|
| 1649 |
+
def _wait_for_backend(
|
| 1650 |
+
operation: Callable[[], Any],
|
| 1651 |
+
*,
|
| 1652 |
+
context: RunContext,
|
| 1653 |
+
status_text: str,
|
| 1654 |
+
started: float,
|
| 1655 |
+
):
|
| 1656 |
+
"""Submit once and stream elapsed-time feedback while a blocking call runs."""
|
| 1657 |
+
|
| 1658 |
+
delivered = False
|
| 1659 |
+
with ThreadPoolExecutor(max_workers=1) as executor:
|
| 1660 |
+
future = executor.submit(operation)
|
| 1661 |
+
try:
|
| 1662 |
+
while True:
|
| 1663 |
+
try:
|
| 1664 |
+
result = future.result(timeout=RUN_HEARTBEAT_SECONDS)
|
| 1665 |
+
except FutureTimeoutError:
|
| 1666 |
+
if future.done():
|
| 1667 |
+
# A TimeoutError raised by the operation is a failure,
|
| 1668 |
+
# rather than an instruction to submit it again.
|
| 1669 |
+
result = future.result()
|
| 1670 |
+
else:
|
| 1671 |
+
yield _training_outputs(
|
| 1672 |
+
status_text,
|
| 1673 |
+
context_html=run_context_html(
|
| 1674 |
+
context, status_text,
|
| 1675 |
+
duration=_format_duration(time.monotonic() - started),
|
| 1676 |
+
),
|
| 1677 |
+
)
|
| 1678 |
+
continue
|
| 1679 |
+
delivered = True
|
| 1680 |
+
return result
|
| 1681 |
+
finally:
|
| 1682 |
+
if not delivered:
|
| 1683 |
+
# Closing a browser's generator must not orphan downloaded files
|
| 1684 |
+
# or cause a second submission of the running backend request.
|
| 1685 |
+
try:
|
| 1686 |
+
result = future.result()
|
| 1687 |
+
backend_result = getattr(result, "backend_result", result)
|
| 1688 |
+
_cleanup_files(getattr(backend_result, "cleanup_files", ()))
|
| 1689 |
+
except Exception:
|
| 1690 |
+
pass
|
| 1691 |
+
|
| 1692 |
+
|
| 1693 |
def run_existing_remote(
|
| 1694 |
payload: Mapping[str, Any],
|
| 1695 |
session_token: str | None,
|
|
|
|
| 1740 |
waking_status,
|
| 1741 |
context_html=run_context_html(context, waking_status),
|
| 1742 |
)
|
| 1743 |
+
execution = yield from _wait_for_backend(
|
| 1744 |
+
lambda: PRODUCTION_SCHEDULER.run_existing(
|
| 1745 |
+
username, request_payload,
|
| 1746 |
+
display_name=_display_name(request_payload),
|
| 1747 |
+
idempotency_key=context.idempotency_key,
|
| 1748 |
+
),
|
| 1749 |
+
context=context, status_text=waking_status, started=started,
|
| 1750 |
)
|
| 1751 |
result = execution.backend_result
|
| 1752 |
saved_model_record = execution.saved_model_record
|
|
|
|
| 1830 |
queued_status,
|
| 1831 |
context_html=run_context_html(context, queued_status),
|
| 1832 |
)
|
| 1833 |
+
waking_status = f"Processing the private {profile_label} request (startup, training, and packaging)…"
|
| 1834 |
+
yield _training_outputs(
|
| 1835 |
+
waking_status,
|
| 1836 |
+
context_html=run_context_html(context, waking_status),
|
| 1837 |
+
)
|
| 1838 |
+
|
| 1839 |
+
def submit_uploaded():
|
| 1840 |
+
with ROUTED_JOB_ADMISSION.lease(decision.profile, username, timeout=None):
|
| 1841 |
+
client = GatewayBackendClient.from_env(decision.profile)
|
| 1842 |
+
return client.run_uploaded(request_payload, train_path, test_path or None)
|
| 1843 |
+
|
| 1844 |
+
result = yield from _wait_for_backend(
|
| 1845 |
+
submit_uploaded,
|
| 1846 |
+
context=context, status_text=waking_status, started=started,
|
| 1847 |
+
)
|
| 1848 |
+
prune_gateway_egress()
|
| 1849 |
+
yield render_training_result(
|
| 1850 |
+
username, result.envelope, result.package_path,
|
| 1851 |
+
_display_name(request_payload),
|
| 1852 |
+
context=context,
|
| 1853 |
+
duration_seconds=time.monotonic() - started,
|
| 1854 |
+
provider="local",
|
| 1855 |
+
)
|
| 1856 |
except BackendUnavailable as exc:
|
| 1857 |
if decision is not None and decision.profile == "cpu":
|
| 1858 |
yield _training_error(
|
|
|
|
| 2723 |
return gr.update(value="Run selected pipeline", interactive=True)
|
| 2724 |
|
| 2725 |
|
| 2726 |
+
def _guided_run_button_finishing():
|
| 2727 |
+
"""Keep the run control disabled until results and navigation are delivered."""
|
| 2728 |
+
|
| 2729 |
+
return gr.update(value="Preparing results…", interactive=False)
|
| 2730 |
+
|
| 2731 |
+
|
| 2732 |
+
def _guided_run_complete_ui(
|
| 2733 |
+
status_text: str, session_token: str | None, request: gr.Request | None = None
|
| 2734 |
+
):
|
| 2735 |
+
if not auth_storage.current_username(request, session_token):
|
| 2736 |
+
return gr.update(), gr.update(), gr.update(value="Run selected pipeline", interactive=False)
|
| 2737 |
+
if _run_state(status_text) == "complete":
|
| 2738 |
+
gr.Info("Training finished. Your results are ready in Runs → Latest run.", duration=8)
|
| 2739 |
+
return gr.update(selected="runs"), gr.update(selected="latest"), _guided_run_button_ready()
|
| 2740 |
+
gr.Warning("The run could not be completed. Review the visible run status.", duration=8)
|
| 2741 |
+
return gr.update(), gr.update(), _guided_run_button_ready()
|
| 2742 |
+
|
| 2743 |
+
|
| 2744 |
+
def _guided_parallel_complete_ui(
|
| 2745 |
+
first_status: str, second_status: str, session_token: str | None,
|
| 2746 |
+
request: gr.Request | None = None,
|
| 2747 |
+
):
|
| 2748 |
+
if not auth_storage.current_username(request, session_token):
|
| 2749 |
+
return gr.update(), gr.update()
|
| 2750 |
+
completed = sum(_run_state(status) == "complete" for status in (first_status, second_status))
|
| 2751 |
+
if completed == 2:
|
| 2752 |
+
gr.Info("Both variants finished. Review their results in Runs.", duration=8)
|
| 2753 |
+
else:
|
| 2754 |
+
gr.Warning(f"{completed} of 2 variants completed. Review each variant's run status.", duration=8)
|
| 2755 |
+
return gr.update(selected="runs"), gr.update(selected="variant-one")
|
| 2756 |
+
|
| 2757 |
+
|
| 2758 |
def _workbench_state_for_variant(variant: Any) -> WorkbenchState:
|
| 2759 |
approved = dict(variant.approved_research or {})
|
| 2760 |
return state_from_mapping(
|
|
|
|
| 3486 |
elem_classes=["run-status-source"],
|
| 3487 |
)
|
| 3488 |
with gr.Tabs(elem_classes=["guided-result-tabs"]):
|
| 3489 |
+
with gr.TabItem("Overview", render_children=True):
|
| 3490 |
context = gr.HTML(
|
| 3491 |
RESULTS_PLACEHOLDER_HTML,
|
| 3492 |
elem_classes=["run-context-output"],
|
|
|
|
| 3720 |
)
|
| 3721 |
|
| 3722 |
with gr.Tabs(visible=False, elem_id="workspace-tabs") as main_tabs:
|
| 3723 |
+
with gr.TabItem("Build", id="build"):
|
| 3724 |
with gr.Row(
|
| 3725 |
equal_height=False,
|
| 3726 |
elem_classes=["guided-instrument-bench", "studio-workbench"],
|
|
|
|
| 3796 |
label="Test data (CSV, optional)",
|
| 3797 |
file_types=[".csv"],
|
| 3798 |
)
|
| 3799 |
+
gr.Markdown(
|
| 3800 |
+
"**Training and testing:** Training data fits the model. "
|
| 3801 |
+
"Separate test data measures performance on unseen rows. "
|
| 3802 |
+
"For tabular data, use a separate test CSV or enable "
|
| 3803 |
+
"**Create a test split** in Objective. Avoid using the same "
|
| 3804 |
+
"rows for both training and testing."
|
| 3805 |
+
)
|
| 3806 |
|
| 3807 |
with gr.Group(
|
| 3808 |
visible=False,
|
|
|
|
| 3820 |
value=default_target,
|
| 3821 |
label="Outcome to predict",
|
| 3822 |
allow_custom_value=True,
|
| 3823 |
+
info="Select the outcome or label in your dataset, rather than an ID or an input predictor.",
|
| 3824 |
)
|
| 3825 |
with gr.Group(visible=False) as upload_target_group:
|
| 3826 |
upload_target = gr.Dropdown(
|
| 3827 |
[],
|
| 3828 |
label="Outcome to predict",
|
| 3829 |
allow_custom_value=True,
|
| 3830 |
+
info="Select the column you want the model to predict. Confirm that the labels match your research question.",
|
| 3831 |
)
|
| 3832 |
with gr.Row():
|
| 3833 |
task_type = gr.Dropdown(
|
|
|
|
| 3879 |
),
|
| 3880 |
interactive=initial_image,
|
| 3881 |
)
|
| 3882 |
+
gr.Markdown(
|
| 3883 |
+
"**Holdout:** For tabular data, a 0.20 test split reserves "
|
| 3884 |
+
"20% of the training file for testing and trains on the "
|
| 3885 |
+
"remaining 80%. Leave the split disabled when using an "
|
| 3886 |
+
"existing separate test set. Image workflows use the "
|
| 3887 |
+
"validation holdout setting shown above."
|
| 3888 |
+
)
|
| 3889 |
|
| 3890 |
with gr.Group(
|
| 3891 |
visible=False,
|
|
|
|
| 3897 |
'<p>The private research backend compares registered '
|
| 3898 |
'pipelines. Recommendations never start training.</p></div>'
|
| 3899 |
)
|
| 3900 |
+
gr.Markdown(
|
| 3901 |
+
"**Why use research assistance?** Compare feature "
|
| 3902 |
+
"selection, transformations and training engines before "
|
| 3903 |
+
"spending the training budget. Optional LLM assistance "
|
| 3904 |
+
"can interpret your objective and help explain the "
|
| 3905 |
+
"recommendation. When it is disabled, planning uses "
|
| 3906 |
+
"deterministic rules; the research record shows the "
|
| 3907 |
+
"actual planning source. Review the evidence and "
|
| 3908 |
+
"approve a compatible candidate before running it."
|
| 3909 |
+
)
|
| 3910 |
generate_plan_button = gr.Button(
|
| 3911 |
"Generate research plan",
|
| 3912 |
variant="secondary",
|
|
|
|
| 4342 |
interactive=False,
|
| 4343 |
elem_classes=["guided-parallel-action"],
|
| 4344 |
)
|
| 4345 |
+
active_run_progress = gr.HTML(
|
| 4346 |
+
"", visible=False, elem_classes=["run-progress-output"],
|
| 4347 |
+
)
|
| 4348 |
|
| 4349 |
+
with gr.TabItem("Runs", id="runs", render_children=True):
|
| 4350 |
gr.HTML(
|
| 4351 |
"""
|
| 4352 |
<section class="guided-workbench-heading">
|
|
|
|
| 4355 |
</section>
|
| 4356 |
"""
|
| 4357 |
)
|
| 4358 |
+
with gr.Tabs(selected="latest") as run_result_tabs:
|
| 4359 |
+
with gr.TabItem("Latest run", id="latest", render_children=True):
|
| 4360 |
latest_outputs = _add_results_section("Latest run")
|
| 4361 |
latest_visual_staging_state = gr.State(
|
| 4362 |
_staged_visual_updates(())
|
| 4363 |
)
|
| 4364 |
+
with gr.TabItem("Variant 1", id="variant-one"):
|
| 4365 |
variant_one_outputs = _add_results_section("Variant 1")
|
| 4366 |
+
with gr.TabItem("Variant 2", id="variant-two"):
|
| 4367 |
variant_two_outputs = _add_results_section("Variant 2")
|
| 4368 |
|
| 4369 |
with gr.TabItem("Models") as replay_tab:
|
|
|
|
| 5191 |
guided_run_ui_handler_staged,
|
| 5192 |
guided_run_inputs,
|
| 5193 |
[*latest_outputs[:16], latest_visual_staging_state],
|
| 5194 |
+
show_progress="minimal",
|
| 5195 |
+
show_progress_on=primary_run_button,
|
| 5196 |
api_name=False,
|
| 5197 |
)
|
| 5198 |
primary_run_event = primary_run_event.then(
|
|
|
|
| 5207 |
latest_outputs[16],
|
| 5208 |
api_name=False,
|
| 5209 |
)
|
| 5210 |
+
primary_run_finishing = primary_run_event.then(
|
| 5211 |
+
_guided_run_button_finishing,
|
| 5212 |
inputs=[],
|
| 5213 |
outputs=[primary_run_button],
|
| 5214 |
queue=False,
|
| 5215 |
api_name=False,
|
| 5216 |
)
|
| 5217 |
+
primary_run_finishing.then(
|
| 5218 |
+
_guided_run_complete_ui,
|
| 5219 |
+
inputs=[latest_outputs[0], session_token],
|
| 5220 |
+
outputs=[main_tabs, run_result_tabs, primary_run_button],
|
| 5221 |
+
queue=True,
|
| 5222 |
+
show_progress="hidden",
|
| 5223 |
+
api_name=False,
|
| 5224 |
+
)
|
| 5225 |
+
latest_outputs[7].change(
|
| 5226 |
+
_run_progress_update,
|
| 5227 |
+
inputs=[latest_outputs[7]],
|
| 5228 |
+
outputs=[active_run_progress],
|
| 5229 |
+
queue=False,
|
| 5230 |
+
api_name=False,
|
| 5231 |
+
)
|
| 5232 |
+
parallel_run_event = parallel_run_button.click(
|
| 5233 |
guided_parallel_plan_ui_handler,
|
| 5234 |
[workbench_state, session_token],
|
| 5235 |
[*variant_one_outputs, *variant_two_outputs],
|
| 5236 |
api_name=False,
|
| 5237 |
concurrency_limit=2,
|
| 5238 |
)
|
| 5239 |
+
parallel_run_event.then(
|
| 5240 |
+
_guided_parallel_complete_ui,
|
| 5241 |
+
inputs=[variant_one_outputs[0], variant_two_outputs[0], session_token],
|
| 5242 |
+
outputs=[main_tabs, run_result_tabs],
|
| 5243 |
+
queue=True,
|
| 5244 |
+
show_progress="hidden",
|
| 5245 |
+
api_name=False,
|
| 5246 |
+
)
|
| 5247 |
next_experiment_outputs = [
|
| 5248 |
workbench_state,
|
| 5249 |
user_objective,
|
|
|
|
| 5328 |
"replay_route_preview",
|
| 5329 |
"validate_gateway_security_config",
|
| 5330 |
]
|
| 5331 |
+
|
gateway_catalog.json
CHANGED
|
@@ -30,6 +30,18 @@
|
|
| 30 |
"col_0"
|
| 31 |
]
|
| 32 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
"titanic": {
|
| 34 |
"image": false,
|
| 35 |
"targets": [
|
|
|
|
| 30 |
"col_0"
|
| 31 |
]
|
| 32 |
},
|
| 33 |
+
"cervical_biopsy": {
|
| 34 |
+
"image": false,
|
| 35 |
+
"targets": [
|
| 36 |
+
"Age"
|
| 37 |
+
]
|
| 38 |
+
},
|
| 39 |
+
"thyroid_recurrence": {
|
| 40 |
+
"image": false,
|
| 41 |
+
"targets": [
|
| 42 |
+
"Age"
|
| 43 |
+
]
|
| 44 |
+
},
|
| 45 |
"titanic": {
|
| 46 |
"image": false,
|
| 47 |
"targets": [
|
gateway_contract.py
CHANGED
|
@@ -3,6 +3,8 @@
|
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
import json
|
|
|
|
|
|
|
| 6 |
import re
|
| 7 |
import stat
|
| 8 |
import zipfile
|
|
@@ -239,8 +241,8 @@ def _optional_visual_prediction(frame: pd.DataFrame) -> tuple[dict[str, Any], st
|
|
| 239 |
)
|
| 240 |
|
| 241 |
|
| 242 |
-
def
|
| 243 |
-
"""Read
|
| 244 |
|
| 245 |
The backend's public training result remains the historical seven-value
|
| 246 |
tuple. Runtime transparency is carried by the package manifest so old
|
|
@@ -260,13 +262,147 @@ def _optional_execution_metadata(package: Any) -> dict[str, Any]:
|
|
| 260 |
return {}
|
| 261 |
try:
|
| 262 |
manifest, _ = inspect_package_manifest(candidate)
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
return execution_metadata_from_manifest(manifest)
|
| 266 |
except (ContractError, OSError, TypeError, ValueError):
|
| 267 |
return {}
|
| 268 |
|
| 269 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 270 |
def success_envelope(outputs: Sequence[Any]) -> dict[str, Any]:
|
| 271 |
"""Normalize the existing seven training outputs into JSON-safe fields."""
|
| 272 |
|
|
@@ -289,9 +425,15 @@ def success_envelope(outputs: Sequence[Any]) -> dict[str, Any]:
|
|
| 289 |
"reasoning": str(reasoning or ""),
|
| 290 |
"package_available": bool(package),
|
| 291 |
}
|
| 292 |
-
|
|
|
|
| 293 |
if execution_metadata:
|
| 294 |
envelope["execution_metadata"] = execution_metadata
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
if visual_prediction:
|
| 296 |
envelope["visual_prediction"] = visual_prediction
|
| 297 |
if visual_warning:
|
|
@@ -487,3 +629,4 @@ __all__ = [
|
|
| 487 |
"inspect_package_manifest",
|
| 488 |
"load_gateway_catalog",
|
| 489 |
]
|
|
|
|
|
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
import json
|
| 6 |
+
import html
|
| 7 |
+
import math
|
| 8 |
import re
|
| 9 |
import stat
|
| 10 |
import zipfile
|
|
|
|
| 241 |
)
|
| 242 |
|
| 243 |
|
| 244 |
+
def _optional_package_manifest(package: Any) -> dict[str, Any]:
|
| 245 |
+
"""Read optional result metadata without changing the seven-value API.
|
| 246 |
|
| 247 |
The backend's public training result remains the historical seven-value
|
| 248 |
tuple. Runtime transparency is carried by the package manifest so old
|
|
|
|
| 262 |
return {}
|
| 263 |
try:
|
| 264 |
manifest, _ = inspect_package_manifest(candidate)
|
| 265 |
+
return manifest
|
|
|
|
|
|
|
| 266 |
except (ContractError, OSError, TypeError, ValueError):
|
| 267 |
return {}
|
| 268 |
|
| 269 |
|
| 270 |
+
def _optional_execution_metadata(
|
| 271 |
+
package: Any, *, manifest: Mapping[str, Any] | None = None
|
| 272 |
+
) -> dict[str, Any]:
|
| 273 |
+
from execution_metadata import execution_metadata_from_manifest
|
| 274 |
+
|
| 275 |
+
try:
|
| 276 |
+
return execution_metadata_from_manifest(
|
| 277 |
+
manifest if manifest is not None else _optional_package_manifest(package)
|
| 278 |
+
)
|
| 279 |
+
except (TypeError, ValueError):
|
| 280 |
+
return {}
|
| 281 |
+
|
| 282 |
+
|
| 283 |
+
MAX_LEADERBOARD_ROWS = 500
|
| 284 |
+
_LEADERBOARD_NUMBERS = (
|
| 285 |
+
"score_val", "fit_time", "pred_time_val", "stack_level",
|
| 286 |
+
)
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
def _leaderboard_text(value: Any) -> str:
|
| 290 |
+
if not isinstance(value, str) or _SENSITIVE_MESSAGE.search(value):
|
| 291 |
+
return ""
|
| 292 |
+
return " ".join(value.split())[:160]
|
| 293 |
+
|
| 294 |
+
|
| 295 |
+
def normalize_model_leaderboard(value: Any) -> dict[str, Any]:
|
| 296 |
+
"""Allowlist optional leaderboard metadata before publishing a model result."""
|
| 297 |
+
|
| 298 |
+
if not isinstance(value, Mapping):
|
| 299 |
+
return {}
|
| 300 |
+
records = value.get("records")
|
| 301 |
+
if isinstance(records, pd.DataFrame):
|
| 302 |
+
records = records.to_dict("records")
|
| 303 |
+
if not isinstance(records, list):
|
| 304 |
+
return {}
|
| 305 |
+
rows = []
|
| 306 |
+
for record in records:
|
| 307 |
+
if not isinstance(record, Mapping):
|
| 308 |
+
continue
|
| 309 |
+
model = _leaderboard_text(record.get("model"))
|
| 310 |
+
if not model:
|
| 311 |
+
continue
|
| 312 |
+
row: dict[str, Any] = {"model": model}
|
| 313 |
+
for key in _LEADERBOARD_NUMBERS:
|
| 314 |
+
candidate = record.get(key)
|
| 315 |
+
try:
|
| 316 |
+
number = float(candidate) if not isinstance(candidate, bool) else math.nan
|
| 317 |
+
except (TypeError, ValueError, OverflowError):
|
| 318 |
+
number = math.nan
|
| 319 |
+
row[key] = number if math.isfinite(number) else None
|
| 320 |
+
inferable = record.get("can_infer")
|
| 321 |
+
row["can_infer"] = inferable if isinstance(inferable, bool) else None
|
| 322 |
+
rows.append(row)
|
| 323 |
+
if not rows:
|
| 324 |
+
return {}
|
| 325 |
+
# AutoGluon normalizes scores so higher is better, including negative losses.
|
| 326 |
+
rows.sort(key=lambda row: (
|
| 327 |
+
row["score_val"] is None,
|
| 328 |
+
-(row["score_val"] or 0.0),
|
| 329 |
+
))
|
| 330 |
+
# Apply the display limit after ranking so an unsorted input cannot hide
|
| 331 |
+
# a higher-scoring model near the end of the backend's leaderboard.
|
| 332 |
+
truncated = bool(value.get("truncated")) or len(rows) > MAX_LEADERBOARD_ROWS
|
| 333 |
+
rows = rows[:MAX_LEADERBOARD_ROWS]
|
| 334 |
+
pipeline = value.get("pipeline")
|
| 335 |
+
pipeline = pipeline if isinstance(pipeline, Mapping) else {}
|
| 336 |
+
trial = value.get("selected_trial")
|
| 337 |
+
trial = trial if isinstance(trial, int) and not isinstance(trial, bool) and 1 <= trial <= 4 else None
|
| 338 |
+
return {
|
| 339 |
+
"schema_version": 1,
|
| 340 |
+
"selected_model": _leaderboard_text(value.get("selected_model")),
|
| 341 |
+
"eval_metric": _leaderboard_text(value.get("eval_metric")),
|
| 342 |
+
"selected_trial": trial,
|
| 343 |
+
"pipeline": {
|
| 344 |
+
key: _leaderboard_text(pipeline.get(key))
|
| 345 |
+
for key in ("feature_selection", "transformation", "training_engine")
|
| 346 |
+
},
|
| 347 |
+
"records": rows,
|
| 348 |
+
"truncated": truncated,
|
| 349 |
+
}
|
| 350 |
+
|
| 351 |
+
|
| 352 |
+
def model_leaderboard_markdown(value: Any) -> str:
|
| 353 |
+
"""Render the selected pipeline's real AutoGluon models as a public table."""
|
| 354 |
+
|
| 355 |
+
data = normalize_model_leaderboard(value)
|
| 356 |
+
if not data:
|
| 357 |
+
return ""
|
| 358 |
+
|
| 359 |
+
def cell(item: Any) -> str:
|
| 360 |
+
return html.escape(str(item)).replace("|", "\\|").replace("\n", " ")
|
| 361 |
+
|
| 362 |
+
def number(item: Any) -> str:
|
| 363 |
+
return f"{item:.6g}" if item is not None else "N/A"
|
| 364 |
+
|
| 365 |
+
selected = data["selected_model"]
|
| 366 |
+
lines = ["#### AutoGluon model leaderboard", ""]
|
| 367 |
+
if selected:
|
| 368 |
+
lines.extend([f"**Selected model:** {cell(selected)}", ""])
|
| 369 |
+
details = []
|
| 370 |
+
if data["selected_trial"] is not None:
|
| 371 |
+
details.append(f"Selected candidate run: {data['selected_trial']}")
|
| 372 |
+
details.extend(
|
| 373 |
+
f"{label}: {cell(data['pipeline'][key])}"
|
| 374 |
+
for key, label in (
|
| 375 |
+
("feature_selection", "Feature selection"),
|
| 376 |
+
("transformation", "Transformation"),
|
| 377 |
+
("training_engine", "Training engine"),
|
| 378 |
+
)
|
| 379 |
+
if data["pipeline"][key]
|
| 380 |
+
)
|
| 381 |
+
if details:
|
| 382 |
+
lines.extend([" · ".join(details), ""])
|
| 383 |
+
metric = cell(data["eval_metric"] or "the configured evaluation metric")
|
| 384 |
+
lines.extend([
|
| 385 |
+
f"Validation scores use **{metric}**. Higher scores are better; "
|
| 386 |
+
"AutoGluon displays loss metrics as negative scores. These validation "
|
| 387 |
+
"scores are separate from the held-out test metrics above.",
|
| 388 |
+
"",
|
| 389 |
+
"| Rank | Model | Validation score | Fit time (s) | Validation prediction time (s) | Stack level | Can predict | Selection |",
|
| 390 |
+
"| --- | --- | --- | --- | --- | --- | --- | --- |",
|
| 391 |
+
])
|
| 392 |
+
for rank, row in enumerate(data["records"], start=1):
|
| 393 |
+
values = [
|
| 394 |
+
str(rank), cell(row["model"]), number(row["score_val"]),
|
| 395 |
+
number(row["fit_time"]), number(row["pred_time_val"]),
|
| 396 |
+
number(row["stack_level"]),
|
| 397 |
+
"Yes" if row["can_infer"] is True else "No" if row["can_infer"] is False else "N/A",
|
| 398 |
+
"**Selected**" if selected and row["model"] == selected else "",
|
| 399 |
+
]
|
| 400 |
+
lines.append("| " + " | ".join(values) + " |")
|
| 401 |
+
if data["truncated"]:
|
| 402 |
+
lines.extend(["", f"Showing up to {MAX_LEADERBOARD_ROWS} models."])
|
| 403 |
+
return "\n".join(lines)
|
| 404 |
+
|
| 405 |
+
|
| 406 |
def success_envelope(outputs: Sequence[Any]) -> dict[str, Any]:
|
| 407 |
"""Normalize the existing seven training outputs into JSON-safe fields."""
|
| 408 |
|
|
|
|
| 425 |
"reasoning": str(reasoning or ""),
|
| 426 |
"package_available": bool(package),
|
| 427 |
}
|
| 428 |
+
manifest = _optional_package_manifest(package)
|
| 429 |
+
execution_metadata = _optional_execution_metadata(package, manifest=manifest)
|
| 430 |
if execution_metadata:
|
| 431 |
envelope["execution_metadata"] = execution_metadata
|
| 432 |
+
leaderboard = normalize_model_leaderboard(
|
| 433 |
+
manifest.get("model_leaderboard")
|
| 434 |
+
)
|
| 435 |
+
if leaderboard:
|
| 436 |
+
envelope["model_leaderboard"] = leaderboard
|
| 437 |
if visual_prediction:
|
| 438 |
envelope["visual_prediction"] = visual_prediction
|
| 439 |
if visual_warning:
|
|
|
|
| 629 |
"inspect_package_manifest",
|
| 630 |
"load_gateway_catalog",
|
| 631 |
]
|
| 632 |
+
|