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
2 changes: 2 additions & 0 deletions src/sentry/features/temporary.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,8 @@ def register_temporary_features(manager: FeatureManager) -> None:
manager.add("organizations:seer-explorer-context-engine-fe-override-ui-flag", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
# Enable code editing tools in Seer Explorer chat
manager.add("organizations:seer-explorer-chat-coding", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
# Enable code mode tools (sentry_api_search/execute) in Seer Explorer
manager.add("organizations:seer-explorer-code-mode-tools", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False)
# Enable structured LLM context (JSON snapshot) instead of ASCII DOM snapshot
manager.add("organizations:context-engine-structured-page-context", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
# Enable the Seer Overview sections for Seat-Based users
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,11 +172,15 @@ def post(
) and features.has(
"organizations:seer-explorer-chat-coding", organization, actor=request.user
)
enable_code_mode_tools = features.has(
"organizations:seer-explorer-code-mode-tools", organization, actor=request.user
)
client = SeerExplorerClient(
organization,
request.user,
is_interactive=True,
enable_coding=enable_coding,
enable_code_mode_tools=enable_code_mode_tools,
)
if run_id:
# Continue existing conversation
Expand All @@ -186,6 +190,7 @@ def post(
insert_index=insert_index,
on_page_context=on_page_context,
page_name=page_name,
request=request,
)
else:
# Start new conversation
Expand All @@ -194,6 +199,7 @@ def post(
on_page_context=on_page_context,
page_name=page_name,
override_ce_enable=override_ce_enable,
request=request,
)

return Response({"run_id": result_run_id})
Expand Down
29 changes: 26 additions & 3 deletions src/sentry/seer/explorer/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
ExplorerRunsRequest,
ExplorerUpdateRequest,
collect_user_org_context,
create_explorer_api_token,
fetch_run_status,
make_explorer_chat_request,
make_explorer_runs_request,
Expand Down Expand Up @@ -196,6 +197,7 @@ def __init__(
intelligence_level: Literal["low", "medium", "high"] = "medium",
is_interactive: bool = False,
enable_coding: bool = False,
enable_code_mode_tools: bool = False,
max_iterations: int | None = None,
):
self.organization = organization
Expand All @@ -207,6 +209,7 @@ def __init__(
self.category_key = category_key
self.category_value = category_value
self.is_interactive = is_interactive
self.enable_code_mode_tools = enable_code_mode_tools
self.max_iterations = max_iterations

if enable_coding and not organization.get_option("sentry:enable_seer_coding", True):
Expand Down Expand Up @@ -266,19 +269,28 @@ def start_run(
if bool(artifact_schema) != bool(artifact_key):
raise ValueError("artifact_key and artifact_schema must be provided together")

user_org_context = collect_user_org_context(self.user, self.organization, request=request)
user_auth_token = (
create_explorer_api_token(self.user, self.organization)
if self.enable_code_mode_tools
and self.user
and not isinstance(self.user, AnonymousUser)
else None
)

chat_body: ExplorerChatRequest = ExplorerChatRequest(
organization_id=self.organization.id,
query=prompt,
run_id=None,
insert_index=None,
on_page_context=on_page_context,
page_name=page_name,
user_org_context=collect_user_org_context(
self.user, self.organization, request=request
),
user_org_context=user_org_context,
intelligence_level=self.intelligence_level,
is_interactive=self.is_interactive,
enable_coding=self.enable_coding,
enable_code_mode_tools=self.enable_code_mode_tools,
user_auth_token=user_auth_token,
Comment thread
azulus marked this conversation as resolved.
)

if self.max_iterations is not None:
Expand Down Expand Up @@ -344,6 +356,7 @@ def continue_run(
page_name: str | None = None,
artifact_key: str | None = None,
artifact_schema: type[BaseModel] | None = None,
request: Request | None = None,
Comment thread
azulus marked this conversation as resolved.
) -> int:
"""
Continue an existing Seer Explorer session. This allows you to add follow-up queries to an ongoing conversation.
Expand All @@ -366,6 +379,14 @@ def continue_run(
if bool(artifact_schema) != bool(artifact_key):
raise ValueError("artifact_key and artifact_schema must be provided together")

user_auth_token = (
create_explorer_api_token(self.user, self.organization)
if self.enable_code_mode_tools
and self.user
and not isinstance(self.user, AnonymousUser)
else None
)

chat_body: ExplorerChatRequest = ExplorerChatRequest(
organization_id=self.organization.id,
query=prompt,
Expand All @@ -375,6 +396,8 @@ def continue_run(
page_name=page_name,
is_interactive=self.is_interactive,
enable_coding=self.enable_coding,
enable_code_mode_tools=self.enable_code_mode_tools,
user_auth_token=user_auth_token,
)

if prompt_metadata:
Expand Down
33 changes: 32 additions & 1 deletion src/sentry/seer/explorer/client_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@

import logging
import time
from datetime import datetime
from datetime import datetime, timedelta
from typing import Any, NotRequired, TypedDict

import orjson
from django.conf import settings
from django.contrib.auth.models import AnonymousUser
from django.utils import timezone
from rest_framework.request import Request
from urllib3 import BaseHTTPResponse, HTTPConnectionPool

Expand Down Expand Up @@ -57,6 +58,7 @@ class ExplorerChatRequest(TypedDict):
intelligence_level: NotRequired[str]
is_interactive: NotRequired[bool]
enable_coding: NotRequired[bool]
enable_code_mode_tools: NotRequired[bool]
project_id: NotRequired[int]
query_metadata: NotRequired[dict[str, str]]
artifact_key: NotRequired[str]
Expand All @@ -68,6 +70,7 @@ class ExplorerChatRequest(TypedDict):
metadata: NotRequired[dict[str, Any]]
is_context_engine_enabled: NotRequired[bool]
max_iterations: NotRequired[int]
user_auth_token: NotRequired[str | None]


class ExplorerRunsRequest(TypedDict):
Expand Down Expand Up @@ -297,6 +300,34 @@ def collect_user_org_context(
}


def create_explorer_api_token(user: SentryUser | RpcUser, organization: Organization) -> str | None:
"""Create a short-lived read-only API token for Seer to call back into Sentry."""
try:
from sentry.models.apitoken import ApiToken
from sentry.types.token import AuthTokenType
from sentry.users.models.user import User as UserModel

real_user = UserModel.objects.get(id=user.id)
token = ApiToken.objects.create(
user=real_user,
token_type=AuthTokenType.USER,
scoping_organization_id=organization.id,
scope_list=[
"org:read",
"project:read",
"event:read",
"alerts:read",
"member:read",
"team:read",
],
Comment thread
azulus marked this conversation as resolved.
expires_at=timezone.now() + timedelta(hours=1),
)
return token.plaintext_token
except Exception:
logger.exception("Failed to create short-lived API token for Seer Explorer")
Comment thread
azulus marked this conversation as resolved.
return None


def fetch_run_status(run_id: int, organization: Organization) -> SeerRunState:
"""Fetch current run status from Seer."""
body = ExplorerStateRequest(run_id=run_id, organization_id=organization.id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,18 @@ def test_post_new_conversation_calls_client(self, mock_client_class: MagicMock):
assert response.status_code == 200
assert response.data == {"run_id": 456}
mock_client_class.assert_called_once_with(
self.organization, ANY, is_interactive=True, enable_coding=False
self.organization,
ANY,
is_interactive=True,
enable_coding=False,
enable_code_mode_tools=False,
)
mock_client.start_run.assert_called_once_with(
prompt="What is this error about?",
on_page_context=None,
page_name=None,
override_ce_enable=True,
request=ANY,
)

@patch("sentry.seer.endpoints.organization_seer_explorer_chat.SeerExplorerClient")
Expand Down Expand Up @@ -104,6 +109,7 @@ def test_post_new_conversation_enable_coding(self, mock_client_class: MagicMock)
ANY,
is_interactive=True,
enable_coding=feature_enabled and option_enabled,
enable_code_mode_tools=False,
)

@patch("sentry.seer.endpoints.organization_seer_explorer_chat.SeerExplorerClient")
Expand All @@ -121,14 +127,19 @@ def test_post_continue_conversation_calls_client(self, mock_client_class: MagicM
assert response.status_code == 200
assert response.data == {"run_id": 789}
mock_client_class.assert_called_once_with(
self.organization, ANY, is_interactive=True, enable_coding=False
self.organization,
ANY,
is_interactive=True,
enable_coding=False,
enable_code_mode_tools=False,
)
mock_client.continue_run.assert_called_once_with(
run_id=789,
prompt="Follow up question",
insert_index=2,
on_page_context=None,
page_name=None,
request=ANY,
)

@patch("sentry.seer.endpoints.organization_seer_explorer_chat.SeerExplorerClient")
Expand All @@ -152,6 +163,7 @@ def test_post_continue_conversation_enable_coding(self, mock_client_class: Magic
ANY,
is_interactive=True,
enable_coding=feature_enabled and option_enabled,
enable_code_mode_tools=False,
)

@patch("sentry.seer.endpoints.organization_seer_explorer_chat.SeerExplorerClient")
Expand Down
Loading