-
-
Notifications
You must be signed in to change notification settings - Fork 4.9k
feat(seer): Curate autofix issue labels #124834
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5c2548a
711b69e
24275b0
ccf7212
982224b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ | |
| from uuid import UUID | ||
|
|
||
| import sentry_sdk | ||
| from django.db import router, transaction | ||
|
|
||
| from sentry import features | ||
| from sentry.api.serializers import EventSerializer, serialize | ||
|
|
@@ -37,6 +38,7 @@ | |
| SeerWorkflowStrategy, | ||
| ) | ||
| from sentry.seer.night_shift.models import TriageResponse, TriageVerdict | ||
| from sentry.tasks.seer.autofix_issue_data import schedule_judging_for_org | ||
| from sentry.tasks.seer.night_shift.models import TriageAction | ||
| from sentry.tasks.seer.night_shift.skip_cache import mark_skipped | ||
| from sentry.types.activity import ActivityType | ||
|
|
@@ -131,6 +133,44 @@ def _capture_autofix_issue_data( | |
| return event_ids | ||
|
|
||
|
|
||
| def _schedule_judging_after_delivery( | ||
| shard: SeerWorkflowRunExecution, log_extra: Mapping[str, object] | ||
| ) -> None: | ||
| judging_enabled = features.has( | ||
| "organizations:seer-fixability-training-data", shard.run.organization | ||
| ) | ||
| using = router.db_for_write(SeerWorkflowRun) | ||
| with transaction.atomic(using=using): | ||
| locked_run = SeerWorkflowRun.objects.select_for_update().get(id=shard.run_id) | ||
| locked_shard = SeerWorkflowRunExecution.objects.select_for_update().get(id=shard.id) | ||
| shard_extras = { | ||
| **(locked_shard.extras or {}), | ||
| "autofix_issue_data_delivery_completed": True, | ||
| } | ||
| locked_shard.update(extras=shard_extras) | ||
|
|
||
| run_extras = dict(locked_run.extras or {}) | ||
| completed_deliveries = SeerWorkflowRunExecution.objects.filter( | ||
| run=locked_run, extras__autofix_issue_data_delivery_completed=True | ||
| ).count() | ||
| if ( | ||
| not judging_enabled | ||
| or run_extras.get("autofix_issue_data_judging_scheduled") | ||
| or completed_deliveries != locked_run.executions.count() | ||
| ): | ||
| return | ||
|
|
||
| run_extras["autofix_issue_data_judging_scheduled"] = True | ||
| locked_run.update(extras=run_extras) | ||
|
Comment on lines
+161
to
+164
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: A transient task dispatch failure will permanently prevent judging because the Suggested FixTo ensure atomicity, move the task dispatch call Prompt for AI AgentDid we get this right? 👍 / 👎 to inform future reviews. |
||
|
|
||
| try: | ||
| schedule_judging_for_org.apply_async( | ||
| args=[shard.run.organization_id], headers={"sentry-propagate-traces": False} | ||
| ) | ||
| except Exception: | ||
| logger.exception("night_shift.autofix_issue_data.judge_dispatch_failed", extra=log_extra) | ||
|
|
||
|
|
||
| def deliver_night_shift_result( | ||
| organization_id: int, | ||
| run_uuid: UUID, | ||
|
|
@@ -183,6 +223,7 @@ def deliver_night_shift_result( | |
| attributes={"error_type": "delivery_error" if status == "error" else "no_artifact"}, | ||
| ) | ||
| logger.warning("night_shift.delivery.no_result", extra={**log_extra, "status": status}) | ||
| _schedule_judging_after_delivery(shard, log_extra) | ||
| return | ||
|
|
||
| try: | ||
|
|
@@ -192,6 +233,7 @@ def deliver_night_shift_result( | |
| "night_shift.triage_error", 1, attributes={"error_type": "invalid_artifact"} | ||
| ) | ||
| logger.exception("night_shift.delivery.invalid_result", extra=log_extra) | ||
| _schedule_judging_after_delivery(shard, log_extra) | ||
| return | ||
|
|
||
| options = (run.extras or {}).get("options") or {} | ||
|
|
@@ -212,6 +254,7 @@ def deliver_night_shift_result( | |
| prompt_version=prompt_version, | ||
| log_extra=log_extra, | ||
| ) | ||
| _schedule_judging_after_delivery(shard, log_extra) | ||
|
|
||
|
|
||
| def _process_verdicts( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,173 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from typing import Literal | ||
|
|
||
| from django.db.models import F, Window | ||
| from django.db.models.functions import PercentRank | ||
| from django.utils import timezone | ||
| from pydantic import BaseModel, Field | ||
| from taskbroker_client.retry import Retry | ||
|
|
||
| from sentry import features | ||
| from sentry.models.organization import Organization | ||
| from sentry.ratelimits import backend as ratelimiter | ||
| from sentry.seer.models import SeerApiError | ||
| from sentry.seer.models.autofix_issue_data import SeerAutofixIssueData | ||
| from sentry.seer.signed_seer_api import ( | ||
| LlmGenerateRequest, | ||
| SeerViewerContext, | ||
| make_llm_generate_request, | ||
| ) | ||
| from sentry.tasks.base import instrumented_task | ||
| from sentry.taskworker.namespaces import seer_tasks | ||
| from sentry.utils import json, metrics | ||
|
|
||
| FEATURE_FLAG = "organizations:seer-fixability-training-data" | ||
| MAX_REVIEWS_PER_ORG_PER_DAY = 20 | ||
|
|
||
| SYSTEM_PROMPT = """Night Shift reviews software issues and may trigger Autofix to investigate | ||
| and open a pull request. Your job is to identify issues where opening a pull request would be | ||
| wasteful because the issue cannot be fixed in the relevant codebase. | ||
|
|
||
| An issue is fixable when it can reasonably be resolved with one or two pull requests to the | ||
| relevant codebase. It is not_fixable when it cannot. Choose uncertain only when the supplied | ||
| evidence is insufficient to decide. Use only the supplied issue and event evidence. Do not | ||
| assume an attempted fix, pull request, or prior automated decision. | ||
|
|
||
| Return only a JSON object matching this shape, with a concise one-to-two-sentence reason: | ||
| {"verdict":"fixable|not_fixable|uncertain","confidence":"high|medium|low","reason":"..."} | ||
| """ | ||
|
|
||
|
|
||
| class JudgeResponse(BaseModel): | ||
| verdict: Literal["fixable", "not_fixable", "uncertain"] | ||
| confidence: Literal["high", "medium", "low"] | ||
| reason: str = Field(min_length=1, max_length=2048) | ||
|
|
||
|
|
||
| def _select_candidates(organization_id: int) -> list[SeerAutofixIssueData]: | ||
| # Randomly sample 20 of the issues from bottom 40% of the fixability score | ||
| rows = ( | ||
| SeerAutofixIssueData.objects.filter( | ||
| organization_id=organization_id, | ||
| judge_review__isnull=True, | ||
| group__seer_fixability_score__isnull=False, | ||
| ) | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| .exclude(raw_issue_data__status="pr_merged") | ||
| .select_related("group") | ||
| .annotate( | ||
| score_percentile=Window( | ||
| expression=PercentRank(), | ||
| order_by=F("group__seer_fixability_score").asc(), | ||
| ) | ||
| ) | ||
| ) | ||
| return list(rows.filter(score_percentile__lte=0.4).order_by("?")[:MAX_REVIEWS_PER_ORG_PER_DAY]) | ||
|
|
||
|
|
||
| @instrumented_task( | ||
| name="sentry.tasks.seer.autofix_issue_data.schedule_judging_for_org", | ||
| namespace=seer_tasks, | ||
| processing_deadline_duration=5 * 60, | ||
| ) | ||
| def schedule_judging_for_org(organization_id: int) -> None: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this could schedule overlapping jobs that race against each other since jobs get sharded into multiple runs. It might be best to limit the judge to the issues scoped in the delivery result, rather than all unscored issues for the org.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The bugbot pointed that out here: #124834 (comment)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed the approach a bit to launch this task after all shards are done to address the bugbots comment above too. Lmk what you think |
||
| organization = Organization.objects.filter(id=organization_id).first() | ||
| if organization is None or not features.has(FEATURE_FLAG, organization): | ||
| return | ||
|
|
||
| for issue_data in _select_candidates(organization.id): | ||
| event_id = issue_data.raw_issue_data.get("event_id") | ||
| if not isinstance(event_id, str): | ||
| continue | ||
| if ratelimiter.is_limited( | ||
| f"autofix_issue_data_judge:org:{organization.id}", | ||
| limit=MAX_REVIEWS_PER_ORG_PER_DAY, | ||
| window=24 * 60 * 60, | ||
| ): | ||
| break | ||
| judge_issue_data.apply_async( | ||
| args=[issue_data.id, event_id], | ||
| headers={"sentry-propagate-traces": False}, | ||
| ) | ||
|
Mihir-Mavalankar marked this conversation as resolved.
|
||
|
|
||
|
|
||
| def _parse_response(content: str) -> JudgeResponse: | ||
| value = content.strip() | ||
| if value.startswith("```json") and value.endswith("```"): | ||
| value = value[7:-3].strip() | ||
| return JudgeResponse.parse_obj(json.loads(value)) | ||
|
|
||
|
|
||
| @instrumented_task( | ||
| name="sentry.tasks.seer.autofix_issue_data.judge", | ||
| namespace=seer_tasks, | ||
| processing_deadline_duration=60, | ||
| retry=Retry(times=2, delay=30, on=(Exception,)), | ||
| ) | ||
| def judge_issue_data(issue_data_id: int, event_id: str) -> None: | ||
| issue_data = ( | ||
| SeerAutofixIssueData.objects.select_related("organization").filter(id=issue_data_id).first() | ||
| ) | ||
| if issue_data is None or not features.has(FEATURE_FLAG, issue_data.organization): | ||
| return | ||
| if issue_data.judge_review is not None: | ||
| return | ||
| if issue_data.raw_issue_data.get("event_id") != event_id: | ||
| metrics.incr("autofix_issue_data.judge.skipped", tags={"reason": "stale_event"}) | ||
| return | ||
|
|
||
| body = LlmGenerateRequest( | ||
| provider="anthropic", | ||
| model="opus", | ||
| referrer="sentry.autofix_issue_data.judge", | ||
| prompt=json.dumps( | ||
| { | ||
| key: value | ||
| for key, value in issue_data.raw_issue_data.items() | ||
| if key not in {"status", "reason"} | ||
| } | ||
| ), | ||
| system_prompt=SYSTEM_PROMPT, | ||
| temperature=0.0, | ||
| max_tokens=1000, | ||
| timeout=25, | ||
| reasoning="high", | ||
| conversation_id=None, | ||
| ) | ||
| response = make_llm_generate_request( | ||
| body, | ||
| timeout=30, | ||
| viewer_context=SeerViewerContext(organization_id=issue_data.organization_id), | ||
| ) | ||
| if response.status >= 400: | ||
| raise SeerApiError("Seer autofix issue data judge request failed", response.status) | ||
|
|
||
| response_data = response.json() | ||
| content = response_data.get("content") | ||
| model = response_data.get("model") | ||
| if not isinstance(content, str) or not isinstance(model, str): | ||
| raise ValueError("Seer autofix issue data judge returned an invalid response") | ||
| result = _parse_response(content) | ||
|
Mihir-Mavalankar marked this conversation as resolved.
|
||
| reviewed_at = timezone.now() | ||
|
|
||
| updated = SeerAutofixIssueData.objects.filter( | ||
| id=issue_data.id, | ||
| judge_review__isnull=True, | ||
| raw_issue_data__event_id=event_id, | ||
| ).update( | ||
| judge_review={ | ||
| "reviewer": "llm_judge", | ||
| "verdict": result.verdict, | ||
| "confidence": result.confidence, | ||
| "reason": result.reason, | ||
| "model": model, | ||
| "prompt_version": "1", | ||
| "reviewed_at": reviewed_at.isoformat(), | ||
| "reviewed_event_id": event_id, | ||
| }, | ||
| date_updated=reviewed_at, | ||
| ) | ||
| metrics.incr( | ||
| "autofix_issue_data.judge.completed" if updated else "autofix_issue_data.judge.skipped", | ||
| tags={} if updated else {"reason": "stale_event"}, | ||
| ) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing shard delivery blocks judging
Medium Severity
Judging is dispatched only when every execution row has
autofix_issue_data_delivery_completed. A shard that never callbacks, or a success path that fails inside_process_verdictsbefore_schedule_judging_after_delivery, leaves the count short forever, so captured issues from the other shards are never reviewed.Additional Locations (1)
src/sentry/seer/night_shift/delivery.py#L248-L257Reviewed by Cursor Bugbot for commit 982224b. Configure here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is acceptable if it happens rarely.