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

1""" 

2core/models.py — Pydantic schemas for request/response and internal classification objects. 

3""" 

4 

5from pydantic import BaseModel, Field 

6from typing import Optional 

7from enum import Enum 

8from datetime import datetime 

9 

10 

11class HarmCategory(str, Enum): 

12 DEHUMANIZATION = "dehumanization" 

13 INCITEMENT = "incitement" 

14 NARRATIVE_DISTORTION = "narrative_distortion" 

15 NONE = "none" 

16 

17 

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" 

25 

26 

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" 

34 

35 

36# --- Request --- 

37 

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 ) 

49 

50 

51class BatchClassifyRequest(BaseModel): 

52 items: list[ClassifyRequest] = Field(..., min_length=1, max_length=100) 

53 

54 

55# --- Internal signal --- 

56 

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 

66 

67 

68# --- Response --- 

69 

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) 

97 

98 class Config: 

99 use_enum_values = True 

100 

101 

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) 

108 

109 

110# --- Validation / metrics --- 

111 

112class ValidationSample(BaseModel): 

113 text: str 

114 expected_is_harmful: bool 

115 expected_category: Optional[HarmCategory] = None 

116 

117 

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)