feat: add SessionRating model and analytics schemas
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,7 @@ from .refresh_token import RefreshToken
|
||||
from .audit_log import AuditLog
|
||||
from .password_reset_token import PasswordResetToken
|
||||
from .session_share import SessionShare, SessionShareView
|
||||
from .session_rating import SessionRating
|
||||
from .account_limit_override import AccountLimitOverride
|
||||
from .feature_flag import FeatureFlag, PlanFeatureDefault, AccountFeatureOverride
|
||||
from .platform_setting import PlatformSetting
|
||||
@@ -47,6 +48,7 @@ __all__ = [
|
||||
"PasswordResetToken",
|
||||
"SessionShare",
|
||||
"SessionShareView",
|
||||
"SessionRating",
|
||||
"AccountLimitOverride",
|
||||
"FeatureFlag",
|
||||
"PlanFeatureDefault",
|
||||
|
||||
46
backend/app/models/session_rating.py
Normal file
46
backend/app/models/session_rating.py
Normal file
@@ -0,0 +1,46 @@
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from sqlalchemy import String, DateTime, Integer, CheckConstraint, UniqueConstraint, ForeignKey
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from app.core.database import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.models.user import User
|
||||
from app.models.session import Session
|
||||
from app.models.tree import Tree
|
||||
|
||||
|
||||
class SessionRating(Base):
|
||||
__tablename__ = "session_ratings"
|
||||
__table_args__ = (
|
||||
CheckConstraint("rating >= 1 AND rating <= 5", name="ck_session_ratings_rating_range"),
|
||||
UniqueConstraint("session_id", name="uq_session_ratings_session_id"),
|
||||
)
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||
)
|
||||
session_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("sessions.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
user_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
tree_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("trees.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
account_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("accounts.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
rating: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
comment: Mapped[Optional[str]] = mapped_column(String(500), nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False
|
||||
)
|
||||
|
||||
# Relationships
|
||||
session: Mapped["Session"] = relationship("Session", foreign_keys=[session_id])
|
||||
user: Mapped["User"] = relationship("User", foreign_keys=[user_id])
|
||||
tree: Mapped["Tree"] = relationship("Tree", foreign_keys=[tree_id])
|
||||
111
backend/app/schemas/analytics.py
Normal file
111
backend/app/schemas/analytics.py
Normal file
@@ -0,0 +1,111 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
# --- Session Rating Schemas ---
|
||||
|
||||
class SessionRatingCreate(BaseModel):
|
||||
rating: int = Field(..., ge=1, le=5)
|
||||
comment: Optional[str] = Field(None, max_length=500)
|
||||
|
||||
|
||||
class SessionRatingResponse(BaseModel):
|
||||
id: str
|
||||
session_id: str
|
||||
rating: int
|
||||
comment: Optional[str]
|
||||
created_at: datetime
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class FlowRatingItem(BaseModel):
|
||||
"""Anonymous feedback item — no user_name for privacy."""
|
||||
rating: int
|
||||
comment: Optional[str]
|
||||
created_at: datetime
|
||||
|
||||
|
||||
# --- Step Feedback Schema ---
|
||||
|
||||
class StepFeedbackCreate(BaseModel):
|
||||
session_id: str
|
||||
was_helpful: bool
|
||||
|
||||
|
||||
# --- Analytics Response Schemas ---
|
||||
|
||||
class OutcomeBreakdown(BaseModel):
|
||||
resolved: int = 0
|
||||
escalated: int = 0
|
||||
workaround: int = 0
|
||||
unresolved: int = 0
|
||||
|
||||
|
||||
class AnalyticsSummary(BaseModel):
|
||||
total_sessions: int
|
||||
completed_sessions: int
|
||||
completion_rate: float
|
||||
median_duration_minutes: float
|
||||
active_engineers: int = 0
|
||||
outcome_breakdown: OutcomeBreakdown
|
||||
|
||||
|
||||
class TimeSeriesPoint(BaseModel):
|
||||
date: str
|
||||
sessions: int = 0
|
||||
resolved: int = 0
|
||||
escalated: int = 0
|
||||
workaround: int = 0
|
||||
unresolved: int = 0
|
||||
|
||||
|
||||
class TopFlow(BaseModel):
|
||||
tree_id: str
|
||||
name: str
|
||||
sessions: int
|
||||
completion_rate: float
|
||||
median_duration_minutes: float
|
||||
avg_csat: Optional[float] = None
|
||||
|
||||
|
||||
class TopEngineer(BaseModel):
|
||||
user_id: str
|
||||
name: str
|
||||
sessions: int
|
||||
completion_rate: float
|
||||
median_duration_minutes: float
|
||||
|
||||
|
||||
class TeamAnalyticsResponse(BaseModel):
|
||||
summary: AnalyticsSummary
|
||||
time_series: list[TimeSeriesPoint]
|
||||
top_flows: list[TopFlow]
|
||||
top_engineers: list[TopEngineer]
|
||||
|
||||
|
||||
class PersonalAnalyticsResponse(BaseModel):
|
||||
summary: AnalyticsSummary
|
||||
time_series: list[TimeSeriesPoint]
|
||||
top_flows: list[TopFlow]
|
||||
|
||||
|
||||
class StepFeedbackSummary(BaseModel):
|
||||
node_id: str
|
||||
node_title: str
|
||||
helpful_yes: int
|
||||
helpful_no: int
|
||||
helpful_rate: float
|
||||
visit_count: int = 0
|
||||
dropoff_count: int = 0
|
||||
dropoff_rate: float = 0.0
|
||||
|
||||
|
||||
class FlowAnalyticsResponse(BaseModel):
|
||||
summary: AnalyticsSummary
|
||||
avg_csat: Optional[float]
|
||||
total_ratings: int
|
||||
time_series: list[TimeSeriesPoint]
|
||||
step_feedback: list[StepFeedbackSummary]
|
||||
recent_comments: list[FlowRatingItem]
|
||||
Reference in New Issue
Block a user