"""관리자 감정 관측용 Jev 감정 전이 trace 계약.""" from __future__ import annotations import math from typing import Literal from pydantic import BaseModel, ConfigDict, Field, model_validator CLIENT_AFFECT_DIMENSIONS = ( "anxiety", "sadness", "anger", "shame", "guilt", "loneliness", "relief", "hope", "trust", ) ClientAffectDecision = Literal["accepted", "tentative", "held"] class ClientAffectPolicyV1(BaseModel): model_config = ConfigDict(extra="forbid", frozen=True, protected_namespaces=()) version: Literal["jev-affect-v1"] min_confidence: float = Field(ge=0.0, le=1.0) accepted_alpha: float = Field(ge=0.0, le=1.0) accepted_cap: float = Field(ge=0.0, le=1.0) tentative_alpha: float = Field(ge=0.0, le=1.0) tentative_cap: float = Field(ge=0.0, le=1.0) tentative_confidence_floor: float = Field(ge=0.0, le=1.0) adjacent_probability_threshold: float = Field(ge=0.0, le=1.0) class ClientAffectContextV1(BaseModel): model_config = ConfigDict(extra="forbid", frozen=True, protected_namespaces=()) stage: str resistance: float = Field(ge=0.0, le=1.0) effective_openness: float = Field(ge=0.0, le=1.0) rapport_credit: float = Field(ge=0.0) class ClientAffectDimensionTraceV1(BaseModel): model_config = ConfigDict(extra="forbid", frozen=True, protected_namespaces=()) key: str before: float = Field(ge=0.0, le=1.0) target: float | None = Field(default=None, ge=0.0, le=1.0) after: float = Field(ge=0.0, le=1.0) confidence: float | None = Field(default=None, ge=0.0, le=1.0) probabilities: tuple[float, float, float, float, float] | None = None decision: ClientAffectDecision @model_validator(mode="after") def require_probability_distribution(self) -> "ClientAffectDimensionTraceV1": if self.probabilities is None: return self if any( not math.isfinite(value) or value < 0.0 or value > 1.0 for value in self.probabilities ): raise ValueError("probabilities must be finite values within 0..1") return self class ClientAffectTraceV1(BaseModel): model_config = ConfigDict(extra="forbid", frozen=True, protected_namespaces=()) schema_version: Literal[1] provider: str model: str latency_ms: int = Field(ge=0) input_tokens: int = Field(ge=0) output_tokens: int = Field(ge=0) cost_usd: float | None = Field(default=None, ge=0.0) turn_seq: int = Field(ge=1) policy: ClientAffectPolicyV1 context: ClientAffectContextV1 dimensions: tuple[ClientAffectDimensionTraceV1, ...] = Field(min_length=9, max_length=9) @model_validator(mode="after") def require_fixed_dimension_order(self) -> "ClientAffectTraceV1": if tuple(dimension.key for dimension in self.dimensions) != CLIENT_AFFECT_DIMENSIONS: raise ValueError("dimensions must use the fixed client affect order") return self __all__ = [ "CLIENT_AFFECT_DIMENSIONS", "ClientAffectContextV1", "ClientAffectDecision", "ClientAffectDimensionTraceV1", "ClientAffectPolicyV1", "ClientAffectTraceV1", ]