from datetime import datetime from pydantic import BaseModel, ConfigDict, Field from app.modules.posts.domain.feed import FeedCandidate, FeedRanking class FeedCandidateSchema(BaseModel): model_config = ConfigDict(extra="forbid") post_id: str = Field(min_length=1, max_length=100) author_id: str = Field(min_length=1, max_length=100) created_at: datetime social_affinity: float = Field(ge=0, le=1) engagement_score: float = Field(ge=0, le=1) author_affinity: float = Field(default=0.0, ge=0, le=1) class RankFeedRequest(BaseModel): model_config = ConfigDict(extra="forbid") user_id: str = Field(min_length=1, max_length=100) candidate_posts: list[FeedCandidateSchema] = Field(max_length=500) snapshot_at: datetime result_limit: int = Field(ge=0, le=500) class RankedFeedItemSchema(BaseModel): post_id: str score: float cluster_id: int | None = None class RankFeedResponse(BaseModel): items: list[RankedFeedItemSchema] ranking_version: str cluster_run_id: str | None = None cold_start: bool missing_embedding_count: int = 0 diversity_relaxations: int = 0 duplicate_penalized_count: int = 0 class ClusterRunResponse(BaseModel): run_id: str k: int sample_size: int inertia: float silhouette_score: float | None = None status: str class ClusterTrainingScheduledResponse(BaseModel): status: str = "scheduled" message: str = "entrenamiento enviado al worker en background" class ClusterStatusResponse(BaseModel): active_run_id: str | None = None k: int | None = None sample_size: int | None = None silhouette_score: float | None = None activated_at: str | None = None class ClusterActivationResponse(BaseModel): run_id: str status: str = "active" class ClusterRunDetailResponse(BaseModel): run_id: str status: str k: int sample_size: int inertia: float | None = None silhouette_score: float | None = None started_at: str completed_at: str | None = None activated_at: str | None = None error_message: str | None = None def to_domain(candidate: FeedCandidateSchema) -> FeedCandidate: return FeedCandidate(**candidate.model_dump()) def to_response(ranking: FeedRanking) -> RankFeedResponse: return RankFeedResponse( items=[ RankedFeedItemSchema( post_id=item.post_id, score=item.score, cluster_id=item.cluster_id, ) for item in ranking.items ], ranking_version=ranking.ranking_version, cluster_run_id=ranking.cluster_run_id, cold_start=ranking.cold_start, missing_embedding_count=ranking.missing_embedding_count, diversity_relaxations=ranking.diversity_relaxations, duplicate_penalized_count=ranking.duplicate_penalized_count, )