Skip to content
7 changes: 6 additions & 1 deletion posthog/temporal/ai/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
from posthog.temporal.ai.anomaly_investigation import AnomalyInvestigationWorkflow, investigate_anomaly_activity
from posthog.temporal.ai.anomaly_investigation import (
AnomalyInvestigationWorkflow,
investigate_anomaly_activity,
record_investigation_deliveries_started_activity,
)
from posthog.temporal.ai.chat_agent import (
AssistantConversationRunnerWorkflow,
ChatAgentWorkflow,
Expand Down Expand Up @@ -45,6 +49,7 @@
process_research_agent_activity,
summarize_llm_traces_activity,
investigate_anomaly_activity,
record_investigation_deliveries_started_activity,
*CHECKPOINT_COMPACTION_ACTIVITIES,
]

Expand Down
2 changes: 2 additions & 0 deletions posthog/temporal/ai/anomaly_investigation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
AnomalyInvestigationWorkflow,
AnomalyInvestigationWorkflowInputs,
investigate_anomaly_activity,
record_investigation_deliveries_started_activity,
)

__all__ = [
"AnomalyInvestigationWorkflow",
"AnomalyInvestigationWorkflowInputs",
"investigate_anomaly_activity",
"record_investigation_deliveries_started_activity",
]
48 changes: 48 additions & 0 deletions posthog/temporal/ai/anomaly_investigation/evaluated.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""What the investigated check measured, read from whichever stack recorded it.

The insight stack keeps it on an `AlertCheck`. The alerts platform never writes one, so an
investigation of a platform evaluation carries it on its `InsightAlertInvestigation` row.
"""

from __future__ import annotations

from typing import Any

from posthog.dataclasses import frozen

from products.alerts.backend.models.alert import AlertCheck
from products.alerts.backend.models.investigation import InsightAlertInvestigation


@frozen
class EvaluatedCheck:
calculated_value: float | None
interval: str | None
triggered_dates: list[str]
triggered_points: list[int]
triggered_metadata: dict[str, Any]
reference: str

@classmethod
def of_alert_check(cls, alert_check: AlertCheck) -> EvaluatedCheck:
return cls(
calculated_value=alert_check.calculated_value,
interval=alert_check.interval,
triggered_dates=list(alert_check.triggered_dates or []),
triggered_points=list(alert_check.triggered_points or []),
triggered_metadata=dict(alert_check.triggered_metadata or {}),
reference=f"Alert check id {alert_check.id}",
)

@classmethod
def of_investigation(cls, investigation: InsightAlertInvestigation) -> EvaluatedCheck:
evaluated = investigation.evaluated or {}
value = evaluated.get("value")
return cls(
calculated_value=float(value) if isinstance(value, (int, float)) and not isinstance(value, bool) else None,
interval=evaluated.get("interval"),
triggered_dates=list(evaluated.get("triggered_dates") or []),
triggered_points=list(evaluated.get("triggered_points") or []),
triggered_metadata=dict(evaluated.get("triggered_metadata") or {}),
reference=f"Investigation id {investigation.id}",
)
15 changes: 8 additions & 7 deletions posthog/temporal/ai/anomaly_investigation/notebook.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import json
import uuid
from dataclasses import dataclass

from markdown_it import MarkdownIt
from markdown_it.token import Token

from posthog.dataclasses import frozen
from posthog.temporal.ai.anomaly_investigation.evaluated import EvaluatedCheck
from posthog.temporal.ai.anomaly_investigation.report import InvestigationReport

from products.alerts.backend.models.alert import AlertCheck, AlertConfiguration
from products.alerts.backend.models.alert import AlertConfiguration
from products.notebooks.backend.facade.content import (
TipTapContent,
TipTapNode,
Expand All @@ -27,10 +28,10 @@
}


@dataclass
@frozen
class NotebookRenderContext:
alert: AlertConfiguration
alert_check: AlertCheck
evaluated: EvaluatedCheck
insight: Insight
report: InvestigationReport

Expand Down Expand Up @@ -122,13 +123,13 @@ def _plain_text(children: list[Token]) -> str:

def _summary_line(ctx: NotebookRenderContext) -> str:
detector_type = (ctx.alert.detector_config or {}).get("type") or "threshold"
triggered_dates = ctx.alert_check.triggered_dates or []
triggered_dates = ctx.evaluated.triggered_dates
window = ""
if triggered_dates:
window = f" at {triggered_dates[0]}"
if len(triggered_dates) > 1:
window = f" from {triggered_dates[0]} to {triggered_dates[-1]}"
value = ctx.alert_check.calculated_value
value = ctx.evaluated.calculated_value
value_str = f"{value:.4f}" if isinstance(value, (int, float)) else "n/a"
return (
f'Detector {detector_type} flagged an anomaly on insight "{ctx.insight.name or ctx.insight.short_id}"'
Expand All @@ -138,7 +139,7 @@ def _summary_line(ctx: NotebookRenderContext) -> str:

def _footer_line(ctx: NotebookRenderContext) -> str:
return (
f"Alert check id {ctx.alert_check.id}. "
f"{ctx.evaluated.reference}. "
f"Tool calls used: {ctx.report.tool_calls_used}. "
f"Generated automatically — review before acting."
)
Expand Down
Loading
Loading