11from __future__ import annotations
22
33import logging
4- import secrets
5- import string
64
7- import orjson
8- from django .conf import settings
9- from requests import HTTPError
10- from rest_framework import serializers , status
11- from rest_framework .exceptions import APIException , NotFound , ValidationError
5+ from rest_framework import serializers
6+ from rest_framework .exceptions import ValidationError
127from rest_framework .request import Request
138from rest_framework .response import Response
149
1813from sentry .api .base import region_silo_endpoint
1914from sentry .api .bases .organization import OrganizationEndpoint , OrganizationEventPermission
2015from sentry .constants import ObjectStatus
21- from sentry .integrations .coding_agent .integration import CodingAgentIntegration
22- from sentry .integrations .coding_agent .models import CodingAgentLaunchRequest
2316from sentry .integrations .coding_agent .utils import get_coding_agent_providers
2417from sentry .integrations .services .integration import integration_service
2518from sentry .models .organization import Organization
26- from sentry .net .http import connection_from_url
27- from sentry .seer .autofix .utils import (
28- AutofixState ,
29- AutofixTriggerSource ,
30- CodingAgentState ,
31- get_autofix_state ,
32- get_coding_agent_prompt ,
33- )
34- from sentry .seer .models import SeerApiError
35- from sentry .seer .signed_seer_api import make_signed_seer_api_request
36- from sentry .shared_integrations .exceptions import ApiError
19+ from sentry .seer .autofix .coding_agent import launch_coding_agents_for_run
20+ from sentry .seer .autofix .utils import AutofixTriggerSource
3721
3822logger = logging .getLogger (__name__ )
3923
4024
41- # Follows the GitHub branch name rules:
42- # https://docs.github.com/en/get-started/using-git/dealing-with-special-characters-in-branch-and-tag-names#naming-branches-and-tags
43- # As our coding agent integration only supports launching on GitHub right now.
44- VALID_BRANCH_NAME_CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_/"
45-
46-
47- def sanitize_branch_name (branch_name : str ) -> str :
48- """
49- Sanitize a branch name by taking the first 3 words, converting to lowercase,
50- removing/replacing special characters, and adding 6 unique random characters.
51-
52- Args:
53- branch_name: The raw branch name to sanitize
54-
55- Returns:
56- A sanitized branch name safe for use as a Git branch
57- """
58- # Take only the first 3 words
59- words = branch_name .strip ().split ()[:3 ]
60- truncated_name = " " .join (words )
61-
62- # Although underscores are allowed, we standardize to kebab case.
63- kebab_case = truncated_name .replace (" " , "-" ).replace ("_" , "-" ).lower ()
64- sanitized = "" .join (c for c in kebab_case if c in VALID_BRANCH_NAME_CHARS )
65- sanitized = sanitized .rstrip ("/" )
66-
67- # Generate 6 unique random characters (alphanumeric)
68- # This is to avoid potential branch name conflicts.
69- random_suffix = "" .join (
70- secrets .choice (string .ascii_lowercase + string .digits ) for _ in range (6 )
71- )
72-
73- # Combine sanitized name with random suffix
74- return f"{ sanitized } -{ random_suffix } " if sanitized else f"branch-{ random_suffix } "
75-
76-
77- def store_coding_agent_states_to_seer (
78- run_id : int , coding_agent_states : list [CodingAgentState ]
79- ) -> None :
80- """Store multiple coding agent states via Seer batch API."""
81- if not coding_agent_states :
82- return
83- path = "/v1/automation/autofix/coding-agent/state/set"
84- body = orjson .dumps (
85- {
86- "run_id" : run_id ,
87- "coding_agent_states" : [state .dict () for state in coding_agent_states ],
88- }
89- )
90-
91- connection_pool = connection_from_url (settings .SEER_AUTOFIX_URL )
92- response = make_signed_seer_api_request (
93- connection_pool ,
94- path ,
95- body = body ,
96- timeout = 30 ,
97- )
98-
99- if response .status >= 400 :
100- raise SeerApiError (response .data .decode ("utf-8" ), response .status )
101-
102- logger .info (
103- "coding_agent.states_stored_to_seer" ,
104- extra = {
105- "run_id" : run_id ,
106- "status_code" : response .status ,
107- "num_states" : len (coding_agent_states ),
108- },
109- )
110-
111-
11225class OrganizationCodingAgentLaunchSerializer (serializers .Serializer [dict [str , object ]]):
11326 integration_id = serializers .IntegerField (required = True )
11427 run_id = serializers .IntegerField (required = True , min_value = 1 )
@@ -157,11 +70,6 @@ def get(self, request: Request, organization: Organization) -> Response:
15770
15871 def post (self , request : Request , organization : Organization ) -> Response :
15972 """Launch a coding agent."""
160- if not features .has ("organizations:seer-coding-agent-integrations" , organization ):
161- return self .respond (
162- {"detail" : "Feature not available" }, status = status .HTTP_404_NOT_FOUND
163- )
164-
16573 serializer = OrganizationCodingAgentLaunchSerializer (data = request .data )
16674 if not serializer .is_valid ():
16775 raise ValidationError (serializer .errors )
@@ -172,238 +80,11 @@ def post(self, request: Request, organization: Organization) -> Response:
17280 integration_id = validated ["integration_id" ]
17381 trigger_source = validated ["trigger_source" ]
17482
175- integration , installation = self ._validate_and_get_integration (
176- request , organization , integration_id
177- )
178-
179- autofix_state = self ._get_autofix_state (run_id , organization )
180- if autofix_state is None :
181- return self .respond (
182- {"detail" : "Autofix state not found" }, status = status .HTTP_400_BAD_REQUEST
183- )
184-
185- logger .info (
186- "coding_agent.launch_request" ,
187- extra = {
188- "organization_id" : organization .id ,
189- "integration_id" : integration .id ,
190- "run_id" : run_id ,
191- },
192- )
193-
194- results = self ._launch_agents_for_repos (
195- installation , autofix_state , run_id , organization , trigger_source
196- )
197-
198- if not results :
199- return self .respond (
200- {"detail" : "No agents were launched" },
201- status = status .HTTP_500_INTERNAL_SERVER_ERROR ,
202- )
203-
204- logger .info (
205- "coding_agent.launch_success" ,
206- extra = {
207- "organization_id" : organization .id ,
208- "integration_id" : integration .id ,
209- "provider" : integration .provider ,
210- "run_id" : run_id ,
211- "repos_processed" : len (results ),
212- },
213- )
214-
215- return self .respond (
216- {
217- "success" : True ,
218- }
219- )
220-
221- def _validate_and_get_integration (self , request : Request , organization , integration_id : int ):
222- """Validate request and get the coding agent integration."""
223- integration_id_int = integration_id
224-
225- org_integration = integration_service .get_organization_integration (
83+ launch_coding_agents_for_run (
22684 organization_id = organization .id ,
227- integration_id = integration_id_int ,
228- )
229-
230- if not org_integration or org_integration .status != ObjectStatus .ACTIVE :
231- raise NotFound ("Integration not found" )
232-
233- integration = integration_service .get_integration (
234- organization_integration_id = org_integration .id ,
235- status = ObjectStatus .ACTIVE ,
236- )
237-
238- if not integration :
239- raise NotFound ("Integration not found" )
240-
241- # Verify it's a coding agent integration
242- if integration .provider not in get_coding_agent_providers ():
243- raise ValidationError ("Not a coding agent integration" )
244-
245- # Get the installation
246- installation = integration .get_installation (organization .id )
247- if not isinstance (installation , CodingAgentIntegration ):
248- raise ValidationError ("Invalid coding agent integration" )
249-
250- return integration , installation
251-
252- def _get_autofix_state (self , run_id : int , organization : Organization ) -> AutofixState | None :
253- """Extract and validate run_id and get autofix state."""
254- autofix_state = get_autofix_state (run_id = run_id , organization_id = organization .id )
255-
256- if not autofix_state :
257- logger .warning (
258- "coding_agent.post.autofix_state_not_found" ,
259- extra = {
260- "organization_id" : organization .id ,
261- "run_id" : run_id ,
262- },
263- )
264- return None
265-
266- return autofix_state
267-
268- def _extract_repos_from_root_cause (self , autofix_state : AutofixState ) -> list [str ]:
269- """Extract repository names from autofix state root cause."""
270- root_cause_step = next (
271- (step for step in autofix_state .steps if step ["key" ] == "root_cause_analysis" ), None
272- )
273-
274- if not root_cause_step or not root_cause_step ["causes" ]:
275- return []
276-
277- cause = root_cause_step ["causes" ][0 ]
278-
279- if "relevant_repos" not in cause :
280- return []
281-
282- return list (set (cause ["relevant_repos" ])) or []
283-
284- def _extract_repos_from_solution (self , autofix_state : AutofixState ) -> list [str ]:
285- """Extract repository names from autofix state solution."""
286- repos = set ()
287- solution_step = next (
288- (step for step in autofix_state .steps if step ["key" ] == "solution" ), None
85+ integration_id = integration_id ,
86+ run_id = run_id ,
87+ trigger_source = trigger_source ,
28988 )
29089
291- if not solution_step :
292- return []
293-
294- for solution_item in solution_step ["solution" ]:
295- if (
296- solution_item ["relevant_code_file" ]
297- and "repo_name" in solution_item ["relevant_code_file" ]
298- and solution_item ["relevant_code_file" ]["repo_name" ]
299- ):
300- repos .add (solution_item ["relevant_code_file" ]["repo_name" ])
301-
302- return list (repos )
303-
304- def _launch_agents_for_repos (
305- self ,
306- installation : CodingAgentIntegration ,
307- autofix_state : AutofixState ,
308- run_id : int ,
309- organization ,
310- trigger_source : AutofixTriggerSource ,
311- ) -> list [dict ]:
312- """Launch coding agents for all repositories in the solution."""
313-
314- repos = set (
315- self ._extract_repos_from_root_cause (autofix_state )
316- if trigger_source == AutofixTriggerSource .ROOT_CAUSE
317- else self ._extract_repos_from_solution (autofix_state )
318- )
319-
320- autofix_state_repos = {f"{ repo .owner } /{ repo .name } " for repo in autofix_state .request .repos }
321-
322- # Repos that were in the repos but not in the autofix state
323- repos_not_found = repos - autofix_state_repos
324- logger .warning (
325- "coding_agent.post.repos_not_found" ,
326- extra = {
327- "organization_id" : organization .id ,
328- "run_id" : run_id ,
329- "repos_not_found" : repos_not_found ,
330- },
331- )
332-
333- validated_repos = repos - repos_not_found
334-
335- repos_to_launch = validated_repos or autofix_state_repos
336-
337- if not repos_to_launch :
338- raise NotFound ("No repos to run agents" )
339-
340- prompt = get_coding_agent_prompt (run_id , trigger_source )
341-
342- if not prompt :
343- raise APIException ("No prompt to send to agents." )
344-
345- results = []
346- states_to_store : list [CodingAgentState ] = []
347-
348- for repo_name in repos_to_launch :
349- repo = next (
350- (
351- repo
352- for repo in autofix_state .request .repos
353- if f"{ repo .owner } /{ repo .name } " == repo_name
354- ),
355- None ,
356- )
357- if not repo :
358- logger .error (
359- "coding_agent.repo_not_found" ,
360- extra = {
361- "organization_id" : organization .id ,
362- "run_id" : run_id ,
363- "repo_name" : repo_name ,
364- },
365- )
366- # Continue with other repos instead of failing entirely
367- continue
368-
369- launch_request = CodingAgentLaunchRequest (
370- prompt = prompt ,
371- repository = repo ,
372- branch_name = sanitize_branch_name (autofix_state .request .issue ["title" ]),
373- )
374-
375- try :
376- coding_agent_state = installation .launch (launch_request )
377- except (HTTPError , ApiError ):
378- logger .exception (
379- "coding_agent.repo_launch_error" ,
380- extra = {
381- "organization_id" : organization .id ,
382- "run_id" : run_id ,
383- "repo_name" : repo_name ,
384- },
385- )
386- continue
387-
388- states_to_store .append (coding_agent_state )
389-
390- results .append (
391- {
392- "repo_name" : repo_name ,
393- "coding_agent_state" : coding_agent_state ,
394- }
395- )
396-
397- try :
398- store_coding_agent_states_to_seer (run_id = run_id , coding_agent_states = states_to_store )
399- except SeerApiError :
400- logger .exception (
401- "coding_agent.seer_storage_error" ,
402- extra = {
403- "organization_id" : organization .id ,
404- "run_id" : run_id ,
405- "repos" : list (repos_to_launch ),
406- },
407- )
408-
409- return results
90+ return self .respond ({"success" : True })
0 commit comments