Coverage for narrative_harm_classifier/core/models.py: 100%
86 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-20 13:25 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-20 13:25 +0000
1"""
2core/models.py — Pydantic schemas for request/response and internal classification objects.
3"""
5from pydantic import BaseModel, Field
6from typing import Optional
7from enum import Enum
8from datetime import datetime
11class HarmCategory(str, Enum):
12 DEHUMANIZATION = "dehumanization"
13 INCITEMENT = "incitement"
14 NARRATIVE_DISTORTION = "narrative_distortion"
15 NONE = "none"
18class TargetType(str, Enum):
19 ETHNIC_GROUP = "ethnic_group"
20 RELIGIOUS_GROUP = "religious_group"
21 GENDER_GROUP = "gender_group"
22 NATIONAL_ORIGIN_GROUP = "national_origin_group"
23 POLITICAL_GROUP = "political_group"
24 UNKNOWN = "unknown"
27class IdentityAxis(str, Enum):
28 RACE_ETHNICITY = "race_ethnicity"
29 RELIGION = "religion"
30 GENDER = "gender"
31 NATIONAL_ORIGIN = "national_origin"
32 POLITICAL_AFFILIATION = "political_affiliation"
33 UNKNOWN = "unknown"
36# --- Request ---
38class ClassifyRequest(BaseModel):
39 text: str = Field(..., min_length=1, max_length=10000, description="Text to classify")
40 context: Optional[str] = Field(None, description="Optional surrounding context")
41 request_id: Optional[str] = Field(None, description="Client-supplied idempotency key")
42 language: str = Field(
43 "en",
44 description=(
45 "ISO 639-1 language code of the text (explicit, not auto-detected). "
46 "Falls back to English with a rationale note if the code isn't available."
47 ),
48 )
51class BatchClassifyRequest(BaseModel):
52 items: list[ClassifyRequest] = Field(..., min_length=1, max_length=100)
55# --- Internal signal ---
57class SignalMatch(BaseModel):
58 row_id: str
59 harm_mechanism: str
60 target_type: TargetType
61 identity_axis: IdentityAxis
62 signal_weight: float
63 matched_pattern: Optional[str] = None
64 azure_sentiment_score: Optional[float] = None
65 azure_entity_detected: Optional[str] = None
68# --- Response ---
70class ClassificationResult(BaseModel):
71 request_id: Optional[str] = None
72 text_excerpt: str = Field(..., description="First 200 chars of input text")
73 is_harmful: bool
74 harm_category: HarmCategory
75 confidence: float = Field(..., ge=0.0, le=1.0)
76 target_type: Optional[TargetType] = None
77 identity_axis: Optional[IdentityAxis] = None
78 harm_mechanism: Optional[str] = None
79 signals_matched: list[SignalMatch] = []
80 decision_rationale: str
81 taxonomy_version: str
82 language: str = Field("en", description="Language the text was classified as (echoes the request)")
83 language_confidence: str = Field(
84 "verified",
85 description="'verified' (well-resourced language) or 'experimental' (seed vocabulary, not native-speaker-reviewed)",
86 )
87 dogwhistle_matched: Optional[str] = Field(
88 None, description="Coded-language term that contributed a signal, if any"
89 )
90 counter_narrative_guidance: Optional[str] = Field(
91 None, description="General counter-messaging guidance for the matched harm mechanism, when harmful"
92 )
93 content_hash: str = Field(
94 ..., description="SHA-256 of (text, context, taxonomy_version) — deterministic, for provenance"
95 )
96 classified_at: datetime = Field(default_factory=datetime.utcnow)
98 class Config:
99 use_enum_values = True
102class BatchClassificationResult(BaseModel):
103 results: list[ClassificationResult]
104 total: int
105 harmful_count: int
106 taxonomy_version: str
107 processed_at: datetime = Field(default_factory=datetime.utcnow)
110# --- Validation / metrics ---
112class ValidationSample(BaseModel):
113 text: str
114 expected_is_harmful: bool
115 expected_category: Optional[HarmCategory] = None
118class ValidationReport(BaseModel):
119 category: str
120 taxonomy_version: str
121 sample_count: int
122 true_positives: int
123 false_positives: int
124 true_negatives: int
125 false_negatives: int
126 precision: float
127 recall: float
128 fpr: float # false positive rate
129 f1: float
130 meets_precision_threshold: bool
131 meets_recall_threshold: bool
132 meets_fpr_threshold: bool
133 passes: bool
134 validated_at: datetime = Field(default_factory=datetime.utcnow)