Ctrl K
ComunidadGuías…

api/core/telemetry/events.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
from __future__ import annotations

from collections.abc import Mapping
from dataclasses import dataclass
from datetime import datetime
from typing import Any, NotRequired, Protocol, TypedDict, runtime_checkable

from dify_trace_sdk.events import DraftNodeExecuted, PromptGenerated, TracePayload, json_safe

from enterprise.telemetry.contracts import SignalType, TelemetryCase

# ---------------------------------------------------------------------------
# Context shapes
# ---------------------------------------------------------------------------


class TraceContext(TypedDict):
    """Common ``context`` shape for TRACE-routed cases."""

    tenant_id: str | None
    user_id: NotRequired[str | None]
    app_id: NotRequired[str | None]


class MetricLogContext(TypedDict):
    """Common ``context`` shape for METRIC_LOG-routed cases."""

    tenant_id: str | None


# ---------------------------------------------------------------------------
# Payload shapes
# ---------------------------------------------------------------------------


class NodeExecutionPayload(TypedDict):
    """``payload`` shape for ``TelemetryCase.NODE_EXECUTION`` and ``DRAFT_NODE_EXECUTION``."""

    node_execution_data: dict[str, Any]


class AppCreatedPayload(TypedDict):
    """``payload`` shape for ``TelemetryCase.APP_CREATED``."""

    app_id: str | None
    mode: NotRequired[str | None]


class AppUpdatedPayload(TypedDict):
    """``payload`` shape for ``TelemetryCase.APP_UPDATED``."""

    app_id: str | None


class AppDeletedPayload(TypedDict):
    """``payload`` shape for ``TelemetryCase.APP_DELETED``."""

    app_id: str | None


class PromptGenerationPayload(TypedDict):
    """``payload`` shape for ``TelemetryCase.PROMPT_GENERATION``."""

    tenant_id: str
    app_id: NotRequired[str | None]
    operation_type: str
    instruction: str
    generated_output: str
    model_provider: str
    model_name: str
    prompt_tokens: int
    completion_tokens: int
    total_tokens: int
    latency: float
    total_price: float | None
    currency: str | None
    error: str | None
    started_at: datetime
    ended_at: datetime


class FeedbackCreatedPayload(TypedDict):
    """``payload`` shape for ``TelemetryCase.FEEDBACK_CREATED``."""

    message_id: str
    app_id: str | None
    conversation_id: NotRequired[str | None]
    from_end_user_id: NotRequired[str | None]
    from_account_id: NotRequired[str | None]
    rating: NotRequired[str | None]
    from_source: NotRequired[str | None]
    content: NotRequired[str | None]


# ---------------------------------------------------------------------------
# Shared context dataclass
# ---------------------------------------------------------------------------


@dataclass(frozen=True)
class TelemetryContext:
    tenant_id: str | None = None
    user_id: str | None = None
    app_id: str | None = None


# ---------------------------------------------------------------------------
# Event protocol — every concrete event must declare these routing fields
# so the gateway can dispatch without external mapping tables.
# ---------------------------------------------------------------------------


@runtime_checkable
class TelemetryEvent(Protocol):
    """Structural contract for all telemetry events.

    All attributes are declared as read-only ``@property`` so that
    frozen dataclasses satisfy the protocol (a bare attribute
    annotation would require writability).
    """

    @property
    def context(self) -> TelemetryContext: ...
    @property
    def payload(self) -> Mapping[str, Any]: ...
    @property
    def case(self) -> TelemetryCase: ...
    @property
    def signal_type(self) -> SignalType: ...
    @property
    def ce_eligible(self) -> bool: ...


@runtime_checkable
class TraceSignalEvent(TelemetryEvent, Protocol):
    """A ``SignalType.TRACE`` event, delivered through the trace pipeline."""

    def trace_payload(self) -> TracePayload: ...


# ---------------------------------------------------------------------------
# Concrete event classes
# ---------------------------------------------------------------------------


@dataclass(frozen=True)
class DraftNodeExecutionTraceEvent:
    context: TelemetryContext
    payload: NodeExecutionPayload
    case: TelemetryCase = TelemetryCase.DRAFT_NODE_EXECUTION
    signal_type: SignalType = SignalType.TRACE
    ce_eligible: bool = False

    def trace_payload(self) -> TracePayload:
        node_execution = self.payload["node_execution_data"]
        return DraftNodeExecuted(node_execution={key: json_safe(value) for key, value in node_execution.items()})


@dataclass(frozen=True)
class PromptGenerationEvent:
    context: TelemetryContext
    payload: PromptGenerationPayload
    case: TelemetryCase = TelemetryCase.PROMPT_GENERATION
    signal_type: SignalType = SignalType.TRACE
    ce_eligible: bool = False

    def trace_payload(self) -> TracePayload:
        payload = self.payload
        return PromptGenerated(
            operation_type=payload["operation_type"],
            instruction=payload["instruction"],
            generated_output=payload["generated_output"],
            model_provider=payload["model_provider"],
            model_name=payload["model_name"],
            prompt_tokens=payload["prompt_tokens"],
            completion_tokens=payload["completion_tokens"],
            total_tokens=payload["total_tokens"],
            latency=payload["latency"],
            total_price=payload["total_price"],
            currency=payload["currency"],
            error=payload["error"],
            started_at=payload["started_at"],
            ended_at=payload["ended_at"],
        )


@dataclass(frozen=True)
class AppCreatedEvent:
    context: TelemetryContext
    payload: AppCreatedPayload
    case: TelemetryCase = TelemetryCase.APP_CREATED
    signal_type: SignalType = SignalType.METRIC_LOG
    ce_eligible: bool = False


@dataclass(frozen=True)
class AppUpdatedEvent:
    context: TelemetryContext
    payload: AppUpdatedPayload
    case: TelemetryCase = TelemetryCase.APP_UPDATED
    signal_type: SignalType = SignalType.METRIC_LOG
    ce_eligible: bool = False


@dataclass(frozen=True)
class AppDeletedEvent:
    context: TelemetryContext
    payload: AppDeletedPayload
    case: TelemetryCase = TelemetryCase.APP_DELETED
    signal_type: SignalType = SignalType.METRIC_LOG
    ce_eligible: bool = False


@dataclass(frozen=True)
class FeedbackCreatedEvent:
    context: TelemetryContext
    payload: FeedbackCreatedPayload
    case: TelemetryCase = TelemetryCase.FEEDBACK_CREATED
    signal_type: SignalType = SignalType.METRIC_LOG
    ce_eligible: bool = False