11from __future__ import annotations
22
3+ from typing import Literal , get_args
4+
5+ from django .contrib .postgres .fields .array import ArrayField
36from django .db import models
47
58from sentry .backup .scopes import RelocationScope
6- from sentry .db .models import FlexibleForeignKey , cell_silo_model , sane_repr
9+ from sentry .db .models import (
10+ BoundedBigIntegerField ,
11+ BoundedPositiveIntegerField ,
12+ FlexibleForeignKey ,
13+ cell_silo_model ,
14+ sane_repr ,
15+ )
716from sentry .db .models .base import DefaultFieldsModel
817
918
1019class SeerWorkflowStrategy (models .TextChoices ):
1120 AGENTIC_TRIAGE = "agentic_triage"
1221 DUPLICATE_MONITORS = "duplicate_monitors"
22+ AGENTIC_TRIAGE_V2 = "agentic_triage_v2"
23+
24+
25+ # Strategies with one config per organization, found or created by get_or_create_for_strategy.
26+ SeerLegacyWorkflowStrategy = Literal [
27+ SeerWorkflowStrategy .AGENTIC_TRIAGE , SeerWorkflowStrategy .DUPLICATE_MONITORS
28+ ]
29+ # Strategies with any number of configs per organization. Each config is looked up by its id.
30+ SeerConfigurableWorkflowStrategy = Literal [SeerWorkflowStrategy .AGENTIC_TRIAGE_V2 ]
31+ SEER_LEGACY_WORKFLOW_STRATEGIES : frozenset [SeerWorkflowStrategy ] = frozenset (
32+ get_args (SeerLegacyWorkflowStrategy )
33+ )
34+ SEER_CONFIGURABLE_WORKFLOW_STRATEGIES : frozenset [SeerWorkflowStrategy ] = frozenset (
35+ get_args (SeerConfigurableWorkflowStrategy )
36+ )
1337
1438
1539class SeerWorkflowSchedule (models .TextChoices ):
@@ -18,6 +42,12 @@ class SeerWorkflowSchedule(models.TextChoices):
1842
1943@cell_silo_model
2044class SeerWorkflowConfig (DefaultFieldsModel ):
45+ """An automation's saved configuration.
46+
47+ Repositories are not configured here. Execution uses the resolved projects' existing Seer
48+ project-repository associations.
49+ """
50+
2151 __relocation_scope__ = RelocationScope .Excluded
2252
2353 organization = FlexibleForeignKey ("sentry.Organization" , on_delete = models .CASCADE )
@@ -26,30 +56,78 @@ class SeerWorkflowConfig(DefaultFieldsModel):
2656 schedule = models .CharField (
2757 max_length = 256 , choices = SeerWorkflowSchedule .choices , default = SeerWorkflowSchedule .DAILY
2858 )
59+ # Legacy free-form field.
2960 extras = models .JSONField (db_default = {}, default = dict )
61+ strategy_version = BoundedPositiveIntegerField (default = 1 , db_default = 1 )
62+ version = BoundedPositiveIntegerField (default = 1 , db_default = 1 )
63+ # Each strategy defines and validates the shape of the JSON fields below.
64+ # How the automation does its work, such as {"intelligence_level": "high"}.
65+ strategy_options = models .JSONField (default = dict , db_default = {})
66+ # Which work it picks beyond projects and environments, such as {"min_event_count": 10}.
67+ # These are work filters, not access limits. The service account controls access.
68+ filters = models .JSONField (default = dict , db_default = {})
69+ # Where results go and how they are delivered: destinations, notifications and review.
70+ output_policy = models .JSONField (default = dict , db_default = {})
71+ # How a run executes: batch size, concurrency, overlap with a previous run and limits.
72+ execution_policy = models .JSONField (default = dict , db_default = {})
73+ # False with no SeerWorkflowConfigProject rows means no selected projects remain, so a config
74+ # saved without a project selection never widens to all projects.
75+ is_all_projects = models .BooleanField (default = False , db_default = False )
76+ # An empty list means all environments.
77+ environments = ArrayField (models .TextField (), default = list , db_default = [])
78+ # Id from the service-account management API. Becomes a HybridCloudForeignKey once the
79+ # control-silo model lands.
80+ service_account_id = BoundedBigIntegerField (null = True )
3081
3182 class Meta :
3283 app_label = "seer"
3384 db_table = "seer_workflowconfig"
3485 constraints = [
3586 models .UniqueConstraint (
36- fields = ["organization" , "strategy" ], name = "seer_workflowconfig_org_strategy_uniq"
87+ fields = ["organization" , "strategy" ],
88+ condition = models .Q (
89+ strategy__in = sorted (s .value for s in SEER_LEGACY_WORKFLOW_STRATEGIES )
90+ ),
91+ name = "seer_legacy_org_strategy_unique" ,
3792 ),
3893 ]
3994
4095 __repr__ = sane_repr ("organization_id" , "strategy" , "enabled" )
4196
4297 @classmethod
4398 def get_or_create_for_strategy (
44- cls , organization_id : int , strategy : SeerWorkflowStrategy
99+ cls , organization_id : int , strategy : SeerLegacyWorkflowStrategy
45100 ) -> SeerWorkflowConfig :
101+ if strategy not in SEER_LEGACY_WORKFLOW_STRATEGIES :
102+ raise ValueError (f"{ strategy } configs are addressed by id, not by strategy" )
46103 config , _ = cls .objects .get_or_create (
47104 organization_id = organization_id ,
48105 strategy = strategy .value ,
49106 )
50107 return config
51108
52109
110+ @cell_silo_model
111+ class SeerWorkflowConfigProject (DefaultFieldsModel ):
112+ """One selected project of a config whose `is_all_projects` is False."""
113+
114+ __relocation_scope__ = RelocationScope .Excluded
115+
116+ config = FlexibleForeignKey ("seer.SeerWorkflowConfig" , on_delete = models .CASCADE )
117+ project = FlexibleForeignKey ("sentry.Project" , on_delete = models .CASCADE )
118+
119+ class Meta :
120+ app_label = "seer"
121+ db_table = "seer_workflowconfigproject"
122+ constraints = [
123+ models .UniqueConstraint (
124+ fields = ["config" , "project" ], name = "seer_workflowconfigproject_unique"
125+ ),
126+ ]
127+
128+ __repr__ = sane_repr ("config_id" , "project_id" )
129+
130+
53131@cell_silo_model
54132class SeerWorkflowRun (DefaultFieldsModel ):
55133 """Records each workflow invocation for an organization.
0 commit comments