Coverage for narrative_harm_classifier/classifier/rules/azure_nlp.py: 41%
66 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"""
2classifier/rules/azure_nlp.py — Azure Text Analytics connector.
4Wraps Azure Cognitive Services Text Analytics for:
5- Sentiment analysis (used as negative signal amplifier)
6- Named Entity Recognition (detects group/identity mentions)
7- Key phrase extraction (surface-level harm signal detection)
9Falls back gracefully when Azure credentials are not configured (dev mode).
10"""
12import logging
13from dataclasses import dataclass
14from typing import Optional
16logger = logging.getLogger(__name__)
19@dataclass
20class AzureNLPResult:
21 sentiment: str # positive | negative | neutral | mixed
22 sentiment_score_negative: float # 0.0–1.0
23 entities: list[dict] # [{text, category, confidence}]
24 key_phrases: list[str]
25 language: str
26 is_fallback: bool = False # True when Azure not configured
29class AzureNLPClient:
30 """
31 Azure Text Analytics client with graceful dev-mode fallback.
33 In production: set AZURE_TEXT_ANALYTICS_ENDPOINT and AZURE_TEXT_ANALYTICS_KEY.
34 In dev/test: client operates in fallback mode with neutral scores.
35 """
37 def __init__(self, endpoint: str = "", key: str = ""):
38 self.endpoint = endpoint
39 self.key = key
40 self._client = None
42 if endpoint and key:
43 try:
44 from azure.ai.textanalytics import TextAnalyticsClient
45 from azure.core.credentials import AzureKeyCredential
46 self._client = TextAnalyticsClient(
47 endpoint=endpoint,
48 credential=AzureKeyCredential(key)
49 )
50 logger.info("Azure Text Analytics client initialized")
51 except ImportError:
52 logger.warning("azure-ai-textanalytics not installed — running in fallback mode")
53 except Exception as e:
54 logger.warning(f"Azure Text Analytics init failed: {e} — running in fallback mode")
55 else:
56 logger.info("Azure credentials not configured — running in fallback/dev mode")
58 @property
59 def is_configured(self) -> bool:
60 return self._client is not None
62 def analyze(self, text: str, language: str = "en") -> AzureNLPResult:
63 """Run sentiment + NER + key phrases in a single batched call."""
64 if not self.is_configured:
65 return self._fallback_result(text)
67 try:
68 from azure.ai.textanalytics import (
69 RecognizeEntitiesAction,
70 AnalyzeSentimentAction,
71 ExtractKeyPhrasesAction,
72 )
74 poller = self._client.begin_analyze_actions(
75 documents=[{"id": "1", "text": text, "language": language}],
76 actions=[
77 AnalyzeSentimentAction(),
78 RecognizeEntitiesAction(),
79 ExtractKeyPhrasesAction(),
80 ],
81 )
82 results = list(poller.result())
84 sentiment_result = None
85 entity_result = None
86 keyphrase_result = None
88 for action_result in results[0]:
89 if action_result.kind == "SentimentAnalysis" and not action_result.is_error:
90 sentiment_result = action_result
91 elif action_result.kind == "EntityRecognition" and not action_result.is_error:
92 entity_result = action_result
93 elif action_result.kind == "KeyPhraseExtraction" and not action_result.is_error:
94 keyphrase_result = action_result
96 sentiment = "neutral"
97 neg_score = 0.0
98 if sentiment_result:
99 sentiment = sentiment_result.sentiment
100 neg_score = sentiment_result.confidence_scores.negative
102 entities = []
103 if entity_result:
104 for ent in entity_result.entities:
105 entities.append({
106 "text": ent.text,
107 "category": ent.category,
108 "confidence": ent.confidence_score,
109 })
111 key_phrases = []
112 if keyphrase_result:
113 key_phrases = list(keyphrase_result.key_phrases)
115 return AzureNLPResult(
116 sentiment=sentiment,
117 sentiment_score_negative=neg_score,
118 entities=entities,
119 key_phrases=key_phrases,
120 language=language,
121 is_fallback=False,
122 )
124 except Exception as e:
125 logger.error(f"Azure NLP analysis failed: {e}")
126 return self._fallback_result(text)
128 def _fallback_result(self, text: str) -> AzureNLPResult:
129 """Dev-mode fallback: neutral scores, no entities."""
130 return AzureNLPResult(
131 sentiment="neutral",
132 sentiment_score_negative=0.0,
133 entities=[],
134 key_phrases=[],
135 language="en",
136 is_fallback=True,
137 )