feat(subscriptions): scheduled AI delivery via Temporal (email + Slack)#59631
feat(subscriptions): scheduled AI delivery via Temporal (email + Slack)#59631vdekrijger wants to merge 2 commits into
Conversation
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
|
🎭 Playwright didn't run on this PR — your changes touch code that could affect E2E behavior, but Playwright is opt-in via label now to keep CI cost down. Add the Most PRs don't need this. Real regressions still get caught on master and fix-forward. |
🔍 Migration Risk AnalysisWe've analyzed your migrations for potential risks. Summary: 0 Safe | 0 Needs Review | 1 Blocked ❌ BlockedCauses locks or breaks compatibility 📚 How to Deploy These Changes SafelyAddField: This operation acquires a brief lock but doesn't rewrite the table. Deployment uses lock timeouts with automatic retries, so lock contention will cause retries rather than connection pile-up. RunPython: Use batching for large data migrations:
See the migration safety guide Last updated: 2026-05-22 14:31 UTC (a6394b0) |
| "reason": reason, | ||
| "reason": reason.description, | ||
| # The re-enable guidance doubles as the "what to do next" line in the email — | ||
| # `{target_type}` is interpolated for the channel-specific reasons. |
There was a problem hiding this comment.
I dont think we need to keep this 2 liner comment here, it's clear from reading the code 😃
|
|
||
| ctx = email_cls.call_args.kwargs["template_context"] | ||
| assert ctx["reason"] == AI_PROMPT_INVALID_DISABLE_REASON.description | ||
| assert "Edit the subscription with a valid prompt" in ctx["action_message"] |
There was a problem hiding this comment.
This assertion feels prone to become outdated, can we do better here?
There was a problem hiding this comment.
Now asserts against AI_PROMPT_INVALID_DISABLE_REASON.user_message.format(target_type=...) instead of a hardcoded substring.
|
|
||
| @parameterized.expand( | ||
| [ | ||
| # "Injection-shaped" phrasings used to be regex-rejected. We now accept them: |
There was a problem hiding this comment.
Honestly I think we should remove this test, it doesn't add any value and is solely there for legacy / historical reasons.
There was a problem hiding this comment.
Removed; folded the valid injection-shaped cases into the parameterized test_accepts_valid (which now also asserts the salient content survives sanitization, not just non-empty).
| @patch("ee.hogai.ai_reports.MaxChatOpenAI") | ||
| @patch("ee.hogai.ai_reports.AssistantQueryExecutor") | ||
| @patch("ee.hogai.ai_reports.build_enriched_prompt") | ||
| def test_orchestrates_plan_query_synthesis(self, mock_build, mock_executor_cls, mock_llm_cls): |
There was a problem hiding this comment.
It feels like we can parameterize all htese tests! Please do so.
There was a problem hiding this comment.
Parameterized into test_pipeline_returns_synthesized_markdown and test_query_fix_retry_loop.
| fix_llm.invoke.assert_called_once() | ||
|
|
||
|
|
||
| class TestEmailHtmlSanitization(APIBaseTest): |
There was a problem hiding this comment.
Are we testing a library here? I don't think there is that much value into that.
There was a problem hiding this comment.
Removed — that was effectively testing nh3.
| ) | ||
| ) | ||
| last_error = exc | ||
| # If every recipient failed (typical AI sub has a single recipient), the |
There was a problem hiding this comment.
This makes an assumption that we have no clue about as we have 0 prod data.
There was a problem hiding this comment.
Removed the assumption-laden comment.
| @@ -1,173 +0,0 @@ | |||
| import pytest | |||
|
|
|||
There was a problem hiding this comment.
It feels odd that we added this in the last PR only to fully remove it from here; can we just completely remove it from the previously stacked PR and not have to deal with cleanup here? (incl the test)
There was a problem hiding this comment.
Agreed the clean fix is to never-add it downstack. That's a multi-branch Graphite restack, so I've flagged it rather than doing it unilaterally (see top-level comment) — left the functional removal here for now. Happy to do the downstack cleanup on your go-ahead.
| # (snapshot) and go straight to `deliver_subscription`, instead of the | ||
| # default empty-assets SKIPPED short-circuit. | ||
| is_ai_prompt: bool = False | ||
| # Deprecated (TODO slug: subscriptions-patched-cleanup) — kept only so |
There was a problem hiding this comment.
This has since been cleaned up and can be rmeoved here? Probably a merge artifact
| target_type: str = "" | ||
| # Set by `create_export_assets` for AI subscriptions, which have no insights | ||
| # to export. The workflow uses this to skip Phase 2 (export) + Phase 2.5 | ||
| # (snapshot) and go straight to `deliver_subscription`, instead of the |
There was a problem hiding this comment.
As mentioned, would suggest a different flow in these cases.
There was a problem hiding this comment.
delivery_id kept but reframed as the generate→deliver report reference — delivery reads the report back from the row rather than receiving it on the wire.
|
|
||
| # AI prompt subscriptions skip the per-insight export + snapshot phases; | ||
| # the LLM output is the report. | ||
| if prepare_result.is_ai_prompt: |
There was a problem hiding this comment.
As mentioned, this feels very iffy, I would suggest to split it in 2 separate workflows but reuse activities and the same scheduler mechanism.
There was a problem hiding this comment.
Done — added ProcessAISubscriptionWorkflow, sharing the scheduler and the underlying activities.
Address review feedback on the AI delivery PR: - Route AI-prompt subscriptions to a dedicated ProcessAISubscriptionWorkflow (scheduler fan-out + value-change), reusing the shared activities and the same scheduler rather than threading AI flags through the insight/dashboard pipeline. - Move LLM work out of delivery into a generate_ai_subscription_report activity: consent gated up front (before any LLM cost), idempotent on retry, the report persisted to the SubscriptionDelivery row by reference (same content_snapshot / 2 MiB-boundary pattern insights use). deliver_subscription now just ships the already-generated report. - Drop the is_ai_prompt flag, the redundant delivery-time target check, and the workflow_run_id try/except fallback; fail loud (non-retryable) on missing report. - Hoist shared retry-policy constants + a recipient-dict helper used by both workflows. - Reorganize tests: sync unit tests in test_ai_subscriptions.py; async activity + end-to-end workflow tests in test_subscriptions_workflows.py (parameterized).
|
Pushed Architecture (the big one)AI-prompt subscriptions now run a dedicated
TestsReorganized to test at the right level: sync unit tests stay in One thing I did not do unilaterallyThe Verification
Reviewed via a local multi-persona review swarm; iterated until the remaining findings were all low-severity. |

Problem
Changes
How did you test this code?
👉 Stay up-to-date with PostHog coding conventions for a smoother review.
Publish to changelog?
Docs update
🤖 Agent context