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
This commit is contained in:
52
backend/app/replay/models.py
Normal file
52
backend/app/replay/models.py
Normal file
@@ -0,0 +1,52 @@
|
||||
"""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, ...]
|
||||
Reference in New Issue
Block a user