Skip to content

Commit 7b7895b

Browse files
authored
feat(cursor-agent): add toggle for whether to create pr (#103729)
We'll default PR creation to false because there's a lot of noise 1. We add a new field `auto_create_pr` to the seer preferences handoff configuration. 2. We get it from the project preferences when calling a coding agent 3. We pass it into the launch call Seer PR getsentry/seer#4078 Frontend PR #103730
1 parent 427e63e commit 7b7895b

File tree

10 files changed

+675
-86
lines changed

10 files changed

+675
-86
lines changed

src/sentry/integrations/coding_agent/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ class CodingAgentLaunchRequest(BaseModel):
77
prompt: str
88
repository: SeerRepoDefinition
99
branch_name: str
10+
auto_create_pr: bool = False

src/sentry/integrations/cursor/client.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@ def launch(self, webhook_url: str, request: CodingAgentLaunchRequest) -> CodingA
4242
),
4343
webhook=CursorAgentLaunchRequestWebhook(url=webhook_url, secret=self.webhook_secret),
4444
target=CursorAgentLaunchRequestTarget(
45-
autoCreatePr=True, branchName=request.branch_name, openAsCursorGithubApp=True
45+
autoCreatePr=request.auto_create_pr,
46+
branchName=request.branch_name,
47+
openAsCursorGithubApp=True,
4648
),
4749
)
4850

src/sentry/seer/autofix/coding_agent.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@
2323
CodingAgentState,
2424
get_autofix_state,
2525
get_coding_agent_prompt,
26+
get_project_seer_preferences,
2627
)
27-
from sentry.seer.models import SeerApiError
28+
from sentry.seer.models import SeerApiError, SeerApiResponseValidationError
2829
from sentry.seer.signed_seer_api import make_signed_seer_api_request
2930
from sentry.shared_integrations.exceptions import ApiError
3031

@@ -202,6 +203,23 @@ def _launch_agents_for_repos(
202203
Dictionary with 'successes' and 'failures' lists
203204
"""
204205

206+
# Fetch project preferences to get auto_create_pr setting from automation_handoff
207+
auto_create_pr = False
208+
try:
209+
preference_response = get_project_seer_preferences(autofix_state.request.project_id)
210+
if preference_response and preference_response.preference:
211+
if preference_response.preference.automation_handoff:
212+
auto_create_pr = preference_response.preference.automation_handoff.auto_create_pr
213+
except (SeerApiError, SeerApiResponseValidationError):
214+
logger.exception(
215+
"coding_agent.get_project_seer_preferences_error",
216+
extra={
217+
"organization_id": organization.id,
218+
"run_id": run_id,
219+
"project_id": autofix_state.request.project_id,
220+
},
221+
)
222+
205223
repos = set(
206224
_extract_repos_from_root_cause(autofix_state)
207225
if trigger_source == AutofixTriggerSource.ROOT_CAUSE
@@ -269,6 +287,7 @@ def _launch_agents_for_repos(
269287
prompt=prompt,
270288
repository=repo,
271289
branch_name=sanitize_branch_name(autofix_state.request.issue["title"]),
290+
auto_create_pr=auto_create_pr,
272291
)
273292

274293
try:

src/sentry/seer/autofix/utils.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
from typing import TypedDict
55

66
import orjson
7+
import pydantic
78
import requests
89
from django.conf import settings
910
from pydantic import BaseModel
11+
from urllib3 import Retry
1012

1113
from sentry import features, options, ratelimits
1214
from sentry.constants import DataCategory
@@ -17,7 +19,13 @@
1719
from sentry.models.repository import Repository
1820
from sentry.net.http import connection_from_url
1921
from sentry.seer.autofix.constants import AutofixAutomationTuningSettings, AutofixStatus
20-
from sentry.seer.models import SeerApiError, SeerPermissionError, SeerRepoDefinition
22+
from sentry.seer.models import (
23+
PreferenceResponse,
24+
SeerApiError,
25+
SeerApiResponseValidationError,
26+
SeerPermissionError,
27+
SeerRepoDefinition,
28+
)
2129
from sentry.seer.signed_seer_api import make_signed_seer_api_request, sign_with_seer_secret
2230
from sentry.utils import json
2331
from sentry.utils.outcomes import Outcome, track_outcome
@@ -133,6 +141,37 @@ class CodingAgentStateUpdateRequest(BaseModel):
133141
)
134142

135143

144+
def get_project_seer_preferences(project_id: int):
145+
"""
146+
Fetch Seer project preferences from the Seer API.
147+
148+
Args:
149+
project_id: The project ID to fetch preferences for
150+
151+
Returns:
152+
PreferenceResponse object if successful, None otherwise
153+
"""
154+
path = "/v1/project-preference"
155+
body = orjson.dumps({"project_id": project_id})
156+
157+
response = make_signed_seer_api_request(
158+
autofix_connection_pool,
159+
path,
160+
body=body,
161+
timeout=5,
162+
retries=Retry(total=2, backoff_factor=0.5),
163+
)
164+
165+
if response.status == 200:
166+
try:
167+
result = orjson.loads(response.data)
168+
return PreferenceResponse.validate(result)
169+
except (pydantic.ValidationError, orjson.JSONDecodeError, UnicodeDecodeError) as e:
170+
raise SeerApiResponseValidationError(str(e)) from e
171+
172+
raise SeerApiError(response.data.decode("utf-8"), response.status)
173+
174+
136175
def get_autofix_repos_from_project_code_mappings(project: Project) -> list[dict]:
137176
if settings.SEER_AUTOFIX_FORCE_USE_REPOS:
138177
# This is for testing purposes only, for example in s4s we want to force the use of specific repo(s)

src/sentry/seer/endpoints/project_seer_preferences.py

Lines changed: 2 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
from __future__ import annotations
22

33
import logging
4-
from enum import StrEnum
5-
from typing import Literal
64

75
import orjson
86
import requests
97
from django.conf import settings
10-
from pydantic import BaseModel
118
from rest_framework import serializers
129
from rest_framework.request import Request
1310
from rest_framework.response import Response
@@ -20,7 +17,7 @@
2017
from sentry.models.project import Project
2118
from sentry.ratelimits.config import RateLimitConfig
2219
from sentry.seer.autofix.utils import get_autofix_repos_from_project_code_mappings
23-
from sentry.seer.models import SeerRepoDefinition
20+
from sentry.seer.models import PreferenceResponse, SeerProjectPreference
2421
from sentry.seer.signed_seer_api import sign_with_seer_secret
2522
from sentry.types.ratelimit import RateLimit, RateLimitCategory
2623

@@ -61,6 +58,7 @@ class SeerAutomationHandoffConfigurationSerializer(CamelSnakeSerializer):
6158
required=True,
6259
)
6360
integration_id = serializers.IntegerField(required=True)
61+
auto_create_pr = serializers.BooleanField(required=False, default=False)
6462

6563

6664
class ProjectSeerPreferencesSerializer(CamelSnakeSerializer):
@@ -71,29 +69,6 @@ class ProjectSeerPreferencesSerializer(CamelSnakeSerializer):
7169
)
7270

7371

74-
class AutofixHandoffPoint(StrEnum):
75-
ROOT_CAUSE = "root_cause"
76-
77-
78-
class SeerAutomationHandoffConfiguration(BaseModel):
79-
handoff_point: AutofixHandoffPoint
80-
target: Literal["cursor_background_agent"]
81-
integration_id: int
82-
83-
84-
class SeerProjectPreference(BaseModel):
85-
organization_id: int
86-
project_id: int
87-
repositories: list[SeerRepoDefinition]
88-
automated_run_stopping_point: str | None = None
89-
automation_handoff: SeerAutomationHandoffConfiguration | None = None
90-
91-
92-
class PreferenceResponse(BaseModel):
93-
preference: SeerProjectPreference | None
94-
code_mapping_repos: list[SeerRepoDefinition]
95-
96-
9772
@region_silo_endpoint
9873
class ProjectSeerPreferencesEndpoint(ProjectEndpoint):
9974
permission_classes = (

src/sentry/seer/models.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from typing import TypedDict
1+
from enum import StrEnum
2+
from typing import Literal, TypedDict
23

34
from pydantic import BaseModel
45

@@ -65,6 +66,30 @@ class SummarizePageWebVitalsResponse(BaseModel):
6566
suggested_investigations: list[PageWebVitalsInsight]
6667

6768

69+
class AutofixHandoffPoint(StrEnum):
70+
ROOT_CAUSE = "root_cause"
71+
72+
73+
class SeerAutomationHandoffConfiguration(BaseModel):
74+
handoff_point: AutofixHandoffPoint
75+
target: Literal["cursor_background_agent"]
76+
integration_id: int
77+
auto_create_pr: bool = False
78+
79+
80+
class SeerProjectPreference(BaseModel):
81+
organization_id: int
82+
project_id: int
83+
repositories: list[SeerRepoDefinition]
84+
automated_run_stopping_point: str | None = None
85+
automation_handoff: SeerAutomationHandoffConfiguration | None = None
86+
87+
88+
class PreferenceResponse(BaseModel):
89+
preference: SeerProjectPreference | None
90+
code_mapping_repos: list[SeerRepoDefinition]
91+
92+
6893
class SeerApiError(Exception):
6994
def __init__(self, message: str, status: int):
7095
self.message = message
@@ -74,6 +99,14 @@ def __str__(self):
7499
return f"Seer API error: {self.message} (status: {self.status})"
75100

76101

102+
class SeerApiResponseValidationError(Exception):
103+
def __init__(self, message: str):
104+
self.message = message
105+
106+
def __str__(self):
107+
return f"Seer API response validation error: {self.message}"
108+
109+
77110
class SeerPermissionError(Exception):
78111
def __init__(self, message: str):
79112
self.message = message

0 commit comments

Comments
 (0)