vignette/apps/api/app/contracts/client_affect.py

100 lines
3.1 KiB
Python

"""관리자 감정 관측용 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",
]