Repository navigation
feat(growth): turn suggestion backend #106432
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
e8a22d8
feat(growth): turn suggestion backend
rafaeelaudibert 20de204
fix(growth): pass ids to the scout creation check
rafaeelaudibert 21e10d2
fix(growth): harden turn suggestion turn selection, extraction and pu…
rafaeelaudibert 785c6c0
feat(growth): draft turn suggestions through the ai-gateway at postho…
rafaeelaudibert 4ecf438
feat(growth): draft turn suggestions with gpt-6-luna
rafaeelaudibert e36eb34
feat(growth): judge turns through the system one client
rafaeelaudibert 71e198f
feat(growth): opt the turn suggestion judge into the typesafe fallback
rafaeelaudibert 9d1aad0
fix(growth): type the drafter's fallback route as a gateway product
rafaeelaudibert c65c4c1
fix(growth): ask jev only through the ai-gateway
rafaeelaudibert 46f6508
fix(growth): mask insight names and keep named refs under the jev opt…
rafaeelaudibert 12f5841
fix(growth): judge only completed turns and expire stale suggestion jobs
rafaeelaudibert File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| from .mcp_tools import MCPToolsViewSet | ||
| from .turn_suggestions import TurnSuggestionsViewSet | ||
|
|
||
| __all__ = ["MCPToolsViewSet"] | ||
| __all__ = ["MCPToolsViewSet", "TurnSuggestionsViewSet"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| from drf_spectacular.utils import OpenApiResponse | ||
| from rest_framework import serializers | ||
| from rest_framework.decorators import action | ||
| from rest_framework.exceptions import NotFound | ||
| from rest_framework.response import Response | ||
| from rest_framework.viewsets import GenericViewSet | ||
|
|
||
| from posthog.api.mixins import ValidatedRequest, validated_request | ||
| from posthog.api.routing import TeamAndOrgViewSetMixin | ||
|
|
||
| from products.posthog_ai.backend.turn_suggestions.offer_ledger import TurnSuggestionResolution | ||
| from products.posthog_ai.backend.turn_suggestions.service import resolve_turn_suggestion, turn_suggestion_state | ||
| from products.tasks.backend.facade.api import task_visible | ||
|
|
||
|
|
||
| class ResolveTurnSuggestionSerializer(serializers.Serializer): | ||
| task_id = serializers.UUIDField( | ||
| help_text="ID of the PostHog AI conversation (task) the suggestion card belongs to." | ||
| ) | ||
| turn_index = serializers.IntegerField( | ||
| min_value=0, help_text="Zero-based index of the conversation turn the suggestion card was shown under." | ||
| ) | ||
| resolution = serializers.ChoiceField( | ||
| choices=TurnSuggestionResolution.choices, | ||
| help_text="What the user did with the card: `dismissed` mutes suggestions for the rest of the conversation, `accepted` means the offered scout, notebook, alert or subscription was created.", | ||
| ) | ||
|
|
||
|
|
||
| class ResolveTurnSuggestionResponseSerializer(serializers.Serializer): | ||
| recorded = serializers.BooleanField( | ||
| help_text="Whether a suggestion card existed for that turn and this call recorded its outcome. A card keeps the first outcome recorded for it." | ||
| ) | ||
|
|
||
|
|
||
| class TurnSuggestionStateQuerySerializer(serializers.Serializer): | ||
| task_id = serializers.UUIDField( | ||
| help_text="ID of the PostHog AI conversation (task) to read suggestion outcomes for." | ||
| ) | ||
|
|
||
|
|
||
| class TurnSuggestionStateSerializer(serializers.Serializer): | ||
| muted = serializers.BooleanField( | ||
| help_text="Whether the user dismissed a suggestion card in this conversation, which stops further cards." | ||
| ) | ||
| resolved_turns = serializers.ListField( | ||
| child=serializers.IntegerField(min_value=0), | ||
| help_text="Zero-based indexes of the turns whose suggestion card the user already dismissed or accepted.", | ||
| ) | ||
|
|
||
|
|
||
| class TurnSuggestionsViewSet(TeamAndOrgViewSetMixin, GenericViewSet): | ||
| scope_object = "task" | ||
|
|
||
| @validated_request( | ||
| query_serializer=TurnSuggestionStateQuerySerializer, | ||
| responses={200: OpenApiResponse(response=TurnSuggestionStateSerializer)}, | ||
| summary="Read which PostHog AI turn suggestion cards the user already resolved", | ||
| ) | ||
| @action(detail=False, methods=["get"], required_scopes=["task:read"]) | ||
| def state(self, request: ValidatedRequest, *args, **kwargs) -> Response: | ||
| task_id = str(request.validated_query_data["task_id"]) | ||
| if not task_visible(task_id, self.team_id, request.user.id): | ||
| raise NotFound() | ||
| state = turn_suggestion_state(task_id, self.team_id) | ||
| return Response( | ||
| TurnSuggestionStateSerializer({"muted": state.muted, "resolved_turns": list(state.resolved_turns)}).data | ||
| ) | ||
|
|
||
| @validated_request( | ||
| request_serializer=ResolveTurnSuggestionSerializer, | ||
| responses={200: OpenApiResponse(response=ResolveTurnSuggestionResponseSerializer)}, | ||
| summary="Record what the user did with a PostHog AI turn suggestion card", | ||
| ) | ||
| @action(detail=False, methods=["post"], required_scopes=["task:write"]) | ||
| def resolve(self, request: ValidatedRequest, *args, **kwargs) -> Response: | ||
| task_id = str(request.validated_data["task_id"]) | ||
| # A dismissal mutes the conversation for its creator, so a teammate who can only read it must not resolve cards. | ||
| if not task_visible(task_id, self.team_id, request.user.id, for_control=True): | ||
| raise NotFound() | ||
| recorded = resolve_turn_suggestion( | ||
| task_id, | ||
| self.team_id, | ||
| turn_index=request.validated_data["turn_index"], | ||
| resolution=TurnSuggestionResolution(request.validated_data["resolution"]), | ||
| ) | ||
| return Response(ResolveTurnSuggestionResponseSerializer({"recorded": recorded}).data) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import structlog | ||
| from celery import shared_task | ||
| from prometheus_client import Counter | ||
|
|
||
| from posthog.celery_queues import CeleryQueue | ||
|
|
||
| from products.posthog_ai.backend.turn_suggestions.service import generate_turn_suggestion | ||
|
|
||
| logger = structlog.get_logger(__name__) | ||
|
|
||
| TURN_SUGGESTION_OUTCOMES_TOTAL = Counter( | ||
| "posthog_ai_turn_suggestion_outcomes_total", | ||
| "End-of-turn suggestion jobs by outcome, so a gate that skips every turn in a region shows up", | ||
| labelnames=["status", "reason"], | ||
| ) | ||
|
|
||
|
|
||
| # No retries: a suggestion that misses its turn is worth less than a stale one arriving mid-conversation. | ||
| @shared_task(ignore_result=True, queue=CeleryQueue.POSTHOG_AI.value, soft_time_limit=90, time_limit=120) | ||
| def generate_turn_suggestion_task(*, run_id: str, team_id: int) -> None: | ||
| outcome = generate_turn_suggestion(run_id, team_id) | ||
| TURN_SUGGESTION_OUTCOMES_TOTAL.labels(status=outcome.status, reason=outcome.reason).inc() | ||
| logger.info( | ||
| "posthog_ai_turn_suggestion_outcome", | ||
| run_id=run_id, | ||
| team_id=team_id, | ||
| status=outcome.status, | ||
| reason=outcome.reason, | ||
| ) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.