Files
Yaojia Wang 33db5aeb10 feat: complete phase 4 -- conversation replay API + analytics dashboard
- Replay models: StepType enum, ReplayStep, ReplayPage frozen dataclasses
- Checkpoint transformer: PostgresSaver JSONB -> structured timeline steps
- Replay API: GET /api/conversations (paginated), GET /api/replay/{thread_id}
- Analytics models: AgentUsage, InterruptStats, AnalyticsResult
- Analytics event recorder: Protocol + PostgresAnalyticsRecorder + NoOp
- Analytics queries: resolution_rate, agent_usage, escalation_rate, cost, interrupts
- Analytics API: GET /api/analytics?range=Xd with envelope response
- DB migration: analytics_events table + conversations column additions
- 74 new tests, 399 total passing, 92.87% coverage
2026-03-31 13:35:45 +02:00

53 lines
1.3 KiB
Python

"""Value objects for conversation replay."""
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
class StepType(str, Enum):
"""Types of steps in a conversation replay."""
user_message = "user_message"
supervisor_routing = "supervisor_routing"
tool_call = "tool_call"
tool_result = "tool_result"
agent_response = "agent_response"
interrupt = "interrupt"
@dataclass(frozen=True)
class ReplayStep:
"""A single step in a conversation replay."""
step: int
type: StepType
timestamp: str
content: str = ""
agent: str | None = None
tool: str | None = None
params: dict | None = None
result: dict | None = None
reasoning: str | None = None
tokens: int | None = None
duration_ms: int | None = None
def __post_init__(self) -> None:
# Store params as a frozen copy to prevent mutation from the outside
if self.params is not None:
object.__setattr__(self, "params", dict(self.params))
if self.result is not None:
object.__setattr__(self, "result", dict(self.result))
@dataclass(frozen=True)
class ReplayPage:
"""A paginated page of replay steps for a conversation thread."""
thread_id: str
total_steps: int
page: int
per_page: int
steps: tuple[ReplayStep, ...]