Skip to content

Commit ed40f5a

Browse files
committed
feat: propagate advisor verdict through revert flow and render in HUD
1. AutorevertPattern carries optional advisor_verdict field. When a revert is advisor-accelerated, the revert comment includes "Note: This revert was accelerated by the AI advisor" with verdict and confidence. 2. State JSON includes advisor data in two places (both forward/backward compatible — absent in older states, gracefully ignored): - outcomes: advisor_verdict on AutorevertPattern data - columns: advisor_results map per (commit_sha → verdict/confidence) 3. HUD renderer shows: - "AI:revert" / "AI:not_related" / etc badges in table cells - "[AI: revert @95%]" in outcome notes for advisor-accelerated reverts
1 parent 83e7f8a commit ed40f5a

4 files changed

Lines changed: 91 additions & 9 deletions

File tree

aws/lambda/pytorch-auto-revert/pytorch_auto_revert/hud_renderer.py

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import re
44
from datetime import datetime
5-
from typing import Any, Dict, List, Mapping, Optional, Sequence, Union
5+
from typing import Any, Dict, List, Mapping, Optional, Sequence, Tuple, Union
66

77
from .signal import SignalStatus
88
from .utils import build_pytorch_hud_url
@@ -53,6 +53,13 @@
5353
td.cell.hl-baseline { background: #e6f7ff; }
5454
td.cell.hl-newer-fail { background: #fdecea; }
5555
td.cell.hl-restart { outline: 2px dashed #888; outline-offset: -2px; }
56+
.advisor-cell { font-size: 10px; display: block; margin-top: 2px; }
57+
.advisor-cell.adv-revert { color: #a40000; }
58+
.advisor-cell.adv-not_related { color: #1a73e8; }
59+
.advisor-cell.adv-garbage { color: #7a5a00; }
60+
.advisor-cell.adv-unsure { color: #555; }
61+
.advisor-dispatch { font-size: 10px; display: block; margin-top: 2px;
62+
color: #1a73e8; font-style: italic; }
5663
"""
5764

5865
HUD_JS = (
@@ -285,10 +292,15 @@ def _note_from_outcome(outcome: Optional[Mapping[str, Any]]) -> str:
285292
newer = data.get("newer_failing_commits", []) or []
286293
suspected = data.get("suspected_commit") or "?"
287294
baseline = data.get("older_successful_commit") or "?"
288-
return (
295+
note = (
289296
f"Pattern: newer fail {len(newer)}; suspect {suspected[:7]}"
290297
f" vs baseline {baseline[:7]}"
291298
)
299+
# Forward-compatible: advisor_verdict may not exist in older states
300+
adv = data.get("advisor_verdict")
301+
if adv:
302+
note += f" [AI: {adv.get('verdict', '?')} @{adv.get('confidence', 0):.0%}]"
303+
return note
292304
if outcome_type == "RestartCommits":
293305
commits = data.get("commit_shas", []) or []
294306
if commits:
@@ -316,12 +328,18 @@ def render_html_from_state(
316328
advisor_dispatches: Sequence[Mapping[str, Any]] = (
317329
state.get("advisor_dispatches", []) or []
318330
)
319-
# Build lookup: signal_key -> advisor dispatch info
331+
# Build lookups for advisor dispatches
332+
# signal_key -> dispatch info (for outcome badges)
320333
advisor_by_signal: Dict[str, Mapping[str, Any]] = {}
334+
# (signal_key, commit_sha) -> dispatch info (for in-cell rendering)
335+
advisor_by_cell: Dict[Tuple[str, str], Mapping[str, Any]] = {}
321336
for ad in advisor_dispatches:
322337
sk = ad.get("signal_key", "")
338+
sha = ad.get("commit_sha", "")
323339
if sk:
324340
advisor_by_signal[sk] = ad
341+
if sk and sha:
342+
advisor_by_cell[(sk, sha)] = ad
325343

326344
raw_outcomes = (
327345
state.get("outcomes") if isinstance(state.get("outcomes"), dict) else None
@@ -446,12 +464,37 @@ def render_html_from_state(
446464
for col in columns:
447465
cells_map = col.get("cells", {}) or {}
448466
events = cells_map.get(sha, []) or []
467+
# Forward-compatible: advisor_results may not exist in older states
468+
advisor_results = col.get("advisor_results", {}) or {}
449469
workflow = str(col.get("workflow", ""))
450470
key = str(col.get("key", ""))
451471
sig_key = f"{workflow}:{key}" if key else workflow
452472
highlights_map = highlight_lookup.get(sig_key, {})
453473
cell_classes = " ".join(sorted(highlights_map.get(sha, [])))
454-
if not events:
474+
475+
# Render advisor verdict badge for this cell (if available)
476+
advisor_badge = ""
477+
adv = advisor_results.get(sha)
478+
if adv:
479+
adv_verdict = adv.get("verdict", "")
480+
adv_conf = adv.get("confidence", 0)
481+
adv_class = f"adv-{adv_verdict}" if adv_verdict else ""
482+
advisor_badge = (
483+
f'<span class="advisor-cell {adv_class}" '
484+
f'title="AI advisor: {adv_verdict} ({adv_conf:.0%})">'
485+
f"AI:{adv_verdict}</span>"
486+
)
487+
488+
# Render advisor dispatch indicator (from advisor_dispatches)
489+
dispatch = advisor_by_cell.get((sig_key, sha))
490+
if dispatch and not advisor_badge:
491+
# Show dispatch indicator only if no verdict badge already shown
492+
advisor_badge = (
493+
'<span class="advisor-dispatch" '
494+
'title="AI advisor dispatched">AI:pending</span>'
495+
)
496+
497+
if not events and not advisor_badge:
455498
html_parts.append(f'<td class="cell {cell_classes}"></td>')
456499
continue
457500

@@ -484,7 +527,8 @@ def render_html_from_state(
484527
f'<span class="ev" title="{title_attr}">{icon}</span>'
485528
)
486529
html_parts.append(
487-
f'<td class="cell {cell_classes}">{"".join(cell_parts)}</td>'
530+
f'<td class="cell {cell_classes}">'
531+
f'{"".join(cell_parts)}{advisor_badge}</td>'
488532
)
489533
html_parts.append("</tr>")
490534
html_parts.append("</tbody>")

aws/lambda/pytorch-auto-revert/pytorch_auto_revert/run_state_logger.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ def _build_state_json(
7878
data["wf_run_id"] = outcome.wf_run_id
7979
if outcome.job_id is not None:
8080
data["job_id"] = outcome.job_id
81+
if outcome.advisor_verdict is not None:
82+
data["advisor_verdict"] = {
83+
"verdict": outcome.advisor_verdict.verdict.value,
84+
"confidence": outcome.advisor_verdict.confidence,
85+
}
8186
serialized = {
8287
"type": "AutorevertPattern",
8388
"data": data,
@@ -105,8 +110,9 @@ def _build_state_json(
105110
},
106111
}
107112

108-
# Per-commit events for this signal
113+
# Per-commit events and advisor results for this signal
109114
cells: Dict[str, List[Dict]] = {}
115+
advisor_results_map: Dict[str, Dict] = {}
110116
for c in sig.commits:
111117
evs = []
112118
for e in c.events:
@@ -124,6 +130,13 @@ def _build_state_json(
124130
evs.append(ev)
125131
if evs:
126132
cells[c.head_sha] = evs
133+
# Capture advisor result if present (forward-compatible: absent in old states)
134+
if c.advisor_result is not None:
135+
advisor_results_map[c.head_sha] = {
136+
"verdict": c.advisor_result.verdict.value,
137+
"confidence": c.advisor_result.confidence,
138+
"signal_key": c.advisor_result.signal_key,
139+
}
127140

128141
col = {
129142
"workflow": sig.workflow_name,
@@ -135,6 +148,9 @@ def _build_state_json(
135148
col["job_base_name"] = sig.job_base_name
136149
if ineligible is not None:
137150
col["ineligible"] = ineligible
151+
# Optional: per-commit advisor results (forward-compatible)
152+
if advisor_results_map:
153+
col["advisor_results"] = advisor_results_map
138154
cols.append(col)
139155

140156
sig_key = f"{sig.workflow_name}:{sig.key}"

aws/lambda/pytorch-auto-revert/pytorch_auto_revert/signal.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,9 @@ class AutorevertPattern:
5050
- suspected_commit: the oldest commit that first started to fail.
5151
- older_successful_commit: the most recent successful commit before
5252
failures started (direct parent of the suspected commit for this signal).
53-
- job_base_name: optional job base name for the signal
5453
- wf_run_id: optional workflow run ID from a failing event on suspected commit
5554
- job_id: optional job ID from a failing event on suspected commit
55+
- advisor_verdict: optional AI advisor verdict that accelerated this decision
5656
"""
5757

5858
workflow_name: str
@@ -61,6 +61,7 @@ class AutorevertPattern:
6161
older_successful_commit: str
6262
wf_run_id: Optional[int] = None
6363
job_id: Optional[int] = None
64+
advisor_verdict: Optional["AIAdvisorResult"] = None
6465

6566

6667
@dataclass
@@ -448,7 +449,9 @@ def partition_by_autorevert_pattern(self) -> Optional[PartitionedCommits]:
448449
ADVISOR_CONFIDENCE_THRESHOLD = 0.9
449450

450451
def _build_autorevert_pattern(
451-
self, partition: "PartitionedCommits"
452+
self,
453+
partition: "PartitionedCommits",
454+
advisor_result: Optional[AIAdvisorResult] = None,
452455
) -> AutorevertPattern:
453456
"""Build an AutorevertPattern from a validated partition."""
454457
suspected = partition.failed[-1]
@@ -464,6 +467,7 @@ def _build_autorevert_pattern(
464467
older_successful_commit=partition.successful[0].head_sha,
465468
wf_run_id=failure_event.wf_run_id if failure_event else None,
466469
job_id=failure_event.job_id if failure_event else None,
470+
advisor_verdict=advisor_result,
467471
)
468472

469473
def _check_advisor_verdict(
@@ -490,7 +494,7 @@ def _check_advisor_verdict(
490494
return None
491495

492496
if result.verdict == AdvisorVerdict.REVERT:
493-
return self._build_autorevert_pattern(partition)
497+
return self._build_autorevert_pattern(partition, advisor_result=result)
494498

495499
if result.verdict == AdvisorVerdict.NOT_RELATED:
496500
return Ineligible(

aws/lambda/pytorch-auto-revert/pytorch_auto_revert/signal_actions.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ class SignalMetadata:
5050
test_module: Optional[str] = None
5151
wf_run_id: Optional[int] = None
5252
job_id: Optional[int] = None
53+
advisor_summary: Optional[str] = None # short AI advisor verdict summary
5354

5455

5556
def _derive_job_filter(job_base_name: Optional[str]) -> Optional[str]:
@@ -293,9 +294,16 @@ def group_actions(
293294
# Extract fields for job/HUD links from AutorevertPattern
294295
wf_run_id = None
295296
job_id = None
297+
advisor_summary = None
296298
if isinstance(outcome, AutorevertPattern):
297299
wf_run_id = outcome.wf_run_id
298300
job_id = outcome.job_id
301+
if outcome.advisor_verdict is not None:
302+
av = outcome.advisor_verdict
303+
advisor_summary = (
304+
f"AI advisor: {av.verdict.value} "
305+
f"(confidence={av.confidence:.2f})"
306+
)
299307

300308
meta = SignalMetadata(
301309
workflow_name=sig.workflow_name,
@@ -304,6 +312,7 @@ def group_actions(
304312
test_module=sig.test_module,
305313
wf_run_id=wf_run_id,
306314
job_id=job_id,
315+
advisor_summary=advisor_summary,
307316
)
308317
if isinstance(outcome, AutorevertPattern):
309318
sha = outcome.suspected_commit
@@ -1064,6 +1073,15 @@ def _comment_issue_pr_revert(
10641073
all_signals = ", ".join(all_signals_urls)
10651074
breaking_notification_msg += f"- {workflow_name}: {all_signals}\n"
10661075

1076+
# Add AI advisor info if any signal was advisor-accelerated
1077+
advisor_summaries = [s.advisor_summary for s in sources if s.advisor_summary]
1078+
if advisor_summaries:
1079+
breaking_notification_msg += (
1080+
"\n**Note:** This revert was accelerated by the AI advisor: "
1081+
+ "; ".join(advisor_summaries)
1082+
+ "\n"
1083+
)
1084+
10671085
try:
10681086
if should_do_revert_on_pr:
10691087
for attempt in RetryWithBackoff():

0 commit comments

Comments
 (0)