zukhriddinai commited on
Commit
add0ff1
·
verified ·
1 Parent(s): 57d015e

Deploy public authenticated AutoML gateway

Browse files
Files changed (3) hide show
  1. gateway_app.py +221 -40
  2. gateway_catalog.json +12 -0
  3. 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+Candidate comparison\s*$", line.strip(), re.I)),
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 = PRODUCTION_SCHEDULER.run_existing(
1653
- username,
1654
- request_payload,
1655
- display_name=_display_name(request_payload),
1656
- idempotency_key=context.idempotency_key,
 
 
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
- with ROUTED_JOB_ADMISSION.lease(
1741
- decision.profile, username, timeout=None
1742
- ):
1743
- waking_status = f"Processing the private {profile_label} request (startup, training, and packaging)…"
1744
- yield _training_outputs(
1745
- waking_status,
1746
- context_html=run_context_html(context, waking_status),
1747
- )
1748
- client = GatewayBackendClient.from_env(decision.profile)
1749
- result = client.run_uploaded(
1750
- request_payload, train_path, test_path or None
1751
- )
1752
- prune_gateway_egress()
1753
- yield render_training_result(
1754
- username,
1755
- result.envelope,
1756
- result.package_path,
1757
- _display_name(request_payload),
1758
- context=context,
1759
- duration_seconds=time.monotonic() - started,
1760
- provider="local",
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
- _guided_run_button_ready,
5055
  inputs=[],
5056
  outputs=[primary_run_button],
5057
  queue=False,
5058
  api_name=False,
5059
  )
5060
- parallel_run_button.click(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 _optional_execution_metadata(package: Any) -> dict[str, Any]:
243
- """Read validated runtime metadata from one optional model-package key.
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
- from execution_metadata import execution_metadata_from_manifest
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
- execution_metadata = _optional_execution_metadata(package)
 
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
+