Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/sentry/integrations/coding_agent/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ class CodingAgentLaunchRequest(BaseModel):
prompt: str
repository: SeerRepoDefinition
branch_name: str
auto_create_pr: bool = False
4 changes: 3 additions & 1 deletion src/sentry/integrations/cursor/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ def launch(self, webhook_url: str, request: CodingAgentLaunchRequest) -> CodingA
),
webhook=CursorAgentLaunchRequestWebhook(url=webhook_url, secret=self.webhook_secret),
target=CursorAgentLaunchRequestTarget(
autoCreatePr=True, branchName=request.branch_name, openAsCursorGithubApp=True
autoCreatePr=request.auto_create_pr,
branchName=request.branch_name,
openAsCursorGithubApp=True,
),
)

Expand Down
21 changes: 20 additions & 1 deletion src/sentry/seer/autofix/coding_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@
CodingAgentState,
get_autofix_state,
get_coding_agent_prompt,
get_project_seer_preferences,
)
from sentry.seer.models import SeerApiError
from sentry.seer.models import SeerApiError, SeerApiResponseValidationError
from sentry.seer.signed_seer_api import make_signed_seer_api_request
from sentry.shared_integrations.exceptions import ApiError

Expand Down Expand Up @@ -202,6 +203,23 @@ def _launch_agents_for_repos(
Dictionary with 'successes' and 'failures' lists
"""

# Fetch project preferences to get auto_create_pr setting from automation_handoff
auto_create_pr = False
try:
preference_response = get_project_seer_preferences(autofix_state.request.project_id)
if preference_response and preference_response.preference:
if preference_response.preference.automation_handoff:
auto_create_pr = preference_response.preference.automation_handoff.auto_create_pr
except (SeerApiError, SeerApiResponseValidationError):
logger.exception(
"coding_agent.get_project_seer_preferences_error",
extra={
"organization_id": organization.id,
"run_id": run_id,
"project_id": autofix_state.request.project_id,
},
)

repos = set(
_extract_repos_from_root_cause(autofix_state)
if trigger_source == AutofixTriggerSource.ROOT_CAUSE
Expand Down Expand Up @@ -269,6 +287,7 @@ def _launch_agents_for_repos(
prompt=prompt,
repository=repo,
branch_name=sanitize_branch_name(autofix_state.request.issue["title"]),
auto_create_pr=auto_create_pr,
)

try:
Expand Down
41 changes: 40 additions & 1 deletion src/sentry/seer/autofix/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
from typing import TypedDict

import orjson
import pydantic
import requests
from django.conf import settings
from pydantic import BaseModel
from urllib3 import Retry

from sentry import features, options, ratelimits
from sentry.constants import DataCategory
Expand All @@ -17,7 +19,13 @@
from sentry.models.repository import Repository
from sentry.net.http import connection_from_url
from sentry.seer.autofix.constants import AutofixAutomationTuningSettings, AutofixStatus
from sentry.seer.models import SeerApiError, SeerPermissionError, SeerRepoDefinition
from sentry.seer.models import (
PreferenceResponse,
SeerApiError,
SeerApiResponseValidationError,
SeerPermissionError,
SeerRepoDefinition,
)
from sentry.seer.signed_seer_api import make_signed_seer_api_request, sign_with_seer_secret
from sentry.utils import json
from sentry.utils.outcomes import Outcome, track_outcome
Expand Down Expand Up @@ -133,6 +141,37 @@ class CodingAgentStateUpdateRequest(BaseModel):
)


def get_project_seer_preferences(project_id: int):
"""
Fetch Seer project preferences from the Seer API.

Args:
project_id: The project ID to fetch preferences for

Returns:
PreferenceResponse object if successful, None otherwise
"""
path = "/v1/project-preference"
body = orjson.dumps({"project_id": project_id})

response = make_signed_seer_api_request(
autofix_connection_pool,
path,
body=body,
timeout=5,
retries=Retry(total=2, backoff_factor=0.5),
)

if response.status == 200:
try:
result = orjson.loads(response.data)
return PreferenceResponse.validate(result)
except (pydantic.ValidationError, orjson.JSONDecodeError, UnicodeDecodeError) as e:
raise SeerApiResponseValidationError(str(e)) from e

raise SeerApiError(response.data.decode("utf-8"), response.status)


def get_autofix_repos_from_project_code_mappings(project: Project) -> list[dict]:
if settings.SEER_AUTOFIX_FORCE_USE_REPOS:
# This is for testing purposes only, for example in s4s we want to force the use of specific repo(s)
Expand Down
29 changes: 2 additions & 27 deletions src/sentry/seer/endpoints/project_seer_preferences.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
from __future__ import annotations

import logging
from enum import StrEnum
from typing import Literal

import orjson
import requests
from django.conf import settings
from pydantic import BaseModel
from rest_framework import serializers
from rest_framework.request import Request
from rest_framework.response import Response
Expand All @@ -20,7 +17,7 @@
from sentry.models.project import Project
from sentry.ratelimits.config import RateLimitConfig
from sentry.seer.autofix.utils import get_autofix_repos_from_project_code_mappings
from sentry.seer.models import SeerRepoDefinition
from sentry.seer.models import PreferenceResponse, SeerProjectPreference
from sentry.seer.signed_seer_api import sign_with_seer_secret
from sentry.types.ratelimit import RateLimit, RateLimitCategory

Expand Down Expand Up @@ -61,6 +58,7 @@ class SeerAutomationHandoffConfigurationSerializer(CamelSnakeSerializer):
required=True,
)
integration_id = serializers.IntegerField(required=True)
auto_create_pr = serializers.BooleanField(required=False, default=False)


class ProjectSeerPreferencesSerializer(CamelSnakeSerializer):
Expand All @@ -71,29 +69,6 @@ class ProjectSeerPreferencesSerializer(CamelSnakeSerializer):
)


class AutofixHandoffPoint(StrEnum):
ROOT_CAUSE = "root_cause"


class SeerAutomationHandoffConfiguration(BaseModel):
handoff_point: AutofixHandoffPoint
target: Literal["cursor_background_agent"]
integration_id: int


class SeerProjectPreference(BaseModel):
organization_id: int
project_id: int
repositories: list[SeerRepoDefinition]
automated_run_stopping_point: str | None = None
automation_handoff: SeerAutomationHandoffConfiguration | None = None


class PreferenceResponse(BaseModel):
preference: SeerProjectPreference | None
code_mapping_repos: list[SeerRepoDefinition]


@region_silo_endpoint
class ProjectSeerPreferencesEndpoint(ProjectEndpoint):
permission_classes = (
Expand Down
35 changes: 34 additions & 1 deletion src/sentry/seer/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import TypedDict
from enum import StrEnum
from typing import Literal, TypedDict

from pydantic import BaseModel

Expand Down Expand Up @@ -65,6 +66,30 @@ class SummarizePageWebVitalsResponse(BaseModel):
suggested_investigations: list[PageWebVitalsInsight]


class AutofixHandoffPoint(StrEnum):
ROOT_CAUSE = "root_cause"


class SeerAutomationHandoffConfiguration(BaseModel):
handoff_point: AutofixHandoffPoint
target: Literal["cursor_background_agent"]
integration_id: int
auto_create_pr: bool = False


class SeerProjectPreference(BaseModel):
organization_id: int
project_id: int
repositories: list[SeerRepoDefinition]
automated_run_stopping_point: str | None = None
automation_handoff: SeerAutomationHandoffConfiguration | None = None


class PreferenceResponse(BaseModel):
preference: SeerProjectPreference | None
code_mapping_repos: list[SeerRepoDefinition]


class SeerApiError(Exception):
def __init__(self, message: str, status: int):
self.message = message
Expand All @@ -74,6 +99,14 @@ def __str__(self):
return f"Seer API error: {self.message} (status: {self.status})"


class SeerApiResponseValidationError(Exception):
def __init__(self, message: str):
self.message = message

def __str__(self):
return f"Seer API response validation error: {self.message}"


class SeerPermissionError(Exception):
def __init__(self, message: str):
self.message = message
Expand Down
Loading
Loading