Skip to content

Commit d76524b

Browse files
roagaandrewshie-sentry
authored andcommitted
feat(seer explorer): add endpoint to chat with Seer Explorer (#95239)
Adds new endpoint for sending messages and reading state for Seer Explorer. Feature flagged for internal testing for now.
1 parent 99900ab commit d76524b

File tree

5 files changed

+426
-0
lines changed

5 files changed

+426
-0
lines changed

.github/CODEOWNERS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,9 @@ tests/sentry/api/endpoints/test_organization_dashboard_widget_details.py @ge
506506
*autofix*.py @getsentry/machine-learning-ai
507507
/static/app/components/events/autofix/ @getsentry/machine-learning-ai
508508
/src/sentry/seer/ @getsentry/machine-learning-ai
509+
*seer_explorer*.py @getsentry/machine-learning-ai
510+
*seerExplorer*.tsx @getsentry/machine-learning-ai
511+
*seerExplorer*.ts @getsentry/machine-learning-ai
509512
## End of ML & AI
510513

511514
## Issues
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
from __future__ import annotations
2+
3+
import orjson
4+
import requests
5+
from django.conf import settings
6+
from rest_framework import serializers
7+
from rest_framework.request import Request
8+
from rest_framework.response import Response
9+
10+
from sentry import features
11+
from sentry.api.api_owners import ApiOwner
12+
from sentry.api.api_publish_status import ApiPublishStatus
13+
from sentry.api.base import region_silo_endpoint
14+
from sentry.api.bases.organization import OrganizationEndpoint
15+
from sentry.models.organization import Organization
16+
from sentry.seer.seer_setup import get_seer_org_acknowledgement
17+
from sentry.seer.signed_seer_api import sign_with_seer_secret
18+
from sentry.types.ratelimit import RateLimit, RateLimitCategory
19+
20+
21+
class SeerExplorerChatSerializer(serializers.Serializer):
22+
query = serializers.CharField(
23+
required=True,
24+
allow_blank=False,
25+
help_text="The user's query to send to the Seer Explorer.",
26+
)
27+
insert_index = serializers.IntegerField(
28+
required=False,
29+
allow_null=True,
30+
help_text="Optional index to insert the message at.",
31+
)
32+
message_timestamp = serializers.FloatField(
33+
required=False,
34+
allow_null=True,
35+
help_text="Optional timestamp for the message.",
36+
)
37+
38+
39+
def _call_seer_explorer_chat(
40+
organization: Organization,
41+
run_id: int | None,
42+
query: str,
43+
insert_index: int | None = None,
44+
message_timestamp: float | None = None,
45+
):
46+
"""Call Seer explorer chat endpoint."""
47+
path = "/v1/automation/explorer/chat"
48+
body = orjson.dumps(
49+
{
50+
"organization_id": organization.id,
51+
"run_id": run_id,
52+
"query": query,
53+
"insert_index": insert_index,
54+
"message_timestamp": message_timestamp,
55+
},
56+
option=orjson.OPT_NON_STR_KEYS,
57+
)
58+
59+
response = requests.post(
60+
f"{settings.SEER_AUTOFIX_URL}{path}",
61+
data=body,
62+
headers={
63+
"content-type": "application/json;charset=utf-8",
64+
**sign_with_seer_secret(body),
65+
},
66+
)
67+
68+
response.raise_for_status()
69+
return response.json()
70+
71+
72+
def _call_seer_explorer_state(organization: Organization, run_id: int):
73+
"""Call Seer explorer state endpoint."""
74+
path = "/v1/automation/explorer/state"
75+
body = orjson.dumps(
76+
{
77+
"run_id": run_id,
78+
"organization_id": organization.id,
79+
},
80+
option=orjson.OPT_NON_STR_KEYS,
81+
)
82+
83+
response = requests.post(
84+
f"{settings.SEER_AUTOFIX_URL}{path}",
85+
data=body,
86+
headers={
87+
"content-type": "application/json;charset=utf-8",
88+
**sign_with_seer_secret(body),
89+
},
90+
)
91+
92+
response.raise_for_status()
93+
return response.json()
94+
95+
96+
@region_silo_endpoint
97+
class OrganizationSeerExplorerChatEndpoint(OrganizationEndpoint):
98+
publish_status = {
99+
"POST": ApiPublishStatus.EXPERIMENTAL,
100+
"GET": ApiPublishStatus.EXPERIMENTAL,
101+
}
102+
owner = ApiOwner.ML_AI
103+
enforce_rate_limit = True
104+
rate_limits = {
105+
"POST": {
106+
RateLimitCategory.IP: RateLimit(limit=25, window=60),
107+
RateLimitCategory.USER: RateLimit(limit=25, window=60),
108+
RateLimitCategory.ORGANIZATION: RateLimit(limit=100, window=60 * 60),
109+
},
110+
"GET": {
111+
RateLimitCategory.IP: RateLimit(limit=100, window=60),
112+
RateLimitCategory.USER: RateLimit(limit=100, window=60),
113+
RateLimitCategory.ORGANIZATION: RateLimit(limit=1000, window=60),
114+
},
115+
}
116+
117+
def get(
118+
self, request: Request, organization: Organization, run_id: int | None = None
119+
) -> Response:
120+
"""
121+
Get the current state of a Seer Explorer session.
122+
"""
123+
user = request.user
124+
if not features.has(
125+
"organizations:gen-ai-features", organization, actor=user
126+
) or not features.has("organizations:seer-explorer", organization, actor=user):
127+
return Response({"detail": "Feature flag not enabled"}, status=400)
128+
if organization.get_option("sentry:hide_ai_features"):
129+
return Response(
130+
{"detail": "AI features are disabled for this organization."}, status=403
131+
)
132+
if not get_seer_org_acknowledgement(organization.id):
133+
return Response(
134+
{"detail": "Seer has not been acknowledged by the organization."}, status=403
135+
)
136+
137+
if not run_id:
138+
return Response({"session": None}, status=404)
139+
140+
response_data = _call_seer_explorer_state(organization, run_id)
141+
return Response(response_data)
142+
143+
def post(
144+
self, request: Request, organization: Organization, run_id: int | None = None
145+
) -> Response:
146+
"""
147+
Start a new chat session or continue an existing one.
148+
149+
Parameters:
150+
- run_id: Optional session ID to continue an existing session (from URL or request body).
151+
- query: The user's query.
152+
- insert_index: Optional index to insert the message at.
153+
154+
Returns:
155+
- session_id: The session ID.
156+
"""
157+
user = request.user
158+
if not features.has(
159+
"organizations:gen-ai-features", organization, actor=user
160+
) or not features.has("organizations:seer-explorer", organization, actor=user):
161+
return Response({"detail": "Feature flag not enabled"}, status=400)
162+
if organization.get_option("sentry:hide_ai_features"):
163+
return Response(
164+
{"detail": "AI features are disabled for this organization."}, status=403
165+
)
166+
if not get_seer_org_acknowledgement(organization.id):
167+
return Response(
168+
{"detail": "Seer has not been acknowledged by the organization."}, status=403
169+
)
170+
171+
serializer = SeerExplorerChatSerializer(data=request.data)
172+
if not serializer.is_valid():
173+
return Response(serializer.errors, status=400)
174+
175+
validated_data = serializer.validated_data
176+
query = validated_data["query"]
177+
insert_index = validated_data.get("insert_index")
178+
message_timestamp = validated_data.get("message_timestamp")
179+
180+
response_data = _call_seer_explorer_chat(
181+
organization, run_id, query, insert_index, message_timestamp
182+
)
183+
return Response(response_data)

src/sentry/api/urls.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@
3838
from sentry.api.endpoints.organization_sampling_project_span_counts import (
3939
OrganizationSamplingProjectSpanCountsEndpoint,
4040
)
41+
from sentry.api.endpoints.organization_seer_explorer_chat import (
42+
OrganizationSeerExplorerChatEndpoint,
43+
)
4144
from sentry.api.endpoints.organization_seer_setup_check import OrganizationSeerSetupCheck
4245
from sentry.api.endpoints.organization_stats_summary import OrganizationStatsSummaryEndpoint
4346
from sentry.api.endpoints.organization_trace_item_attributes import (
@@ -2132,6 +2135,11 @@ def create_group_urls(name_prefix: str) -> list[URLPattern | URLResolver]:
21322135
TraceExplorerAIQuery.as_view(),
21332136
name="sentry-api-0-trace-explorer-ai-query",
21342137
),
2138+
re_path(
2139+
r"^(?P<organization_id_or_slug>[^/]+)/seer/explorer-chat/(?:(?P<run_id>[^/]+)/)?$",
2140+
OrganizationSeerExplorerChatEndpoint.as_view(),
2141+
name="sentry-api-0-organization-seer-explorer-chat",
2142+
),
21352143
re_path(
21362144
r"^(?P<organization_id_or_slug>[^/]+)/seer/setup-check/$",
21372145
OrganizationSeerSetupCheck.as_view(),

src/sentry/features/temporary.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,8 @@ def register_temporary_features(manager: FeatureManager):
334334
manager.add("organizations:revoke-org-auth-on-slug-rename", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
335335
# Enable detecting SDK crashes during event processing
336336
manager.add("organizations:sdk-crash-detection", OrganizationFeature, FeatureHandlerStrategy.INTERNAL, api_expose=False)
337+
# Enable Seer Explorer panel for AI-powered data exploration
338+
manager.add("organizations:seer-explorer", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
337339
# Enable search query builder raw search replacement
338340
manager.add("organizations:search-query-builder-raw-search-replacement", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
339341
# Enable new search query builder wildcard operators

0 commit comments

Comments
 (0)