Penny_V2 / bias_utils.py
pythonprincess's picture
Upload bias_utils.py
d86bd2d verified
# models/bias/bias_utils.py
"""
Bias Detection Utilities for Penny
Provides zero-shot classification for detecting potential bias in text responses.
Uses a classification model to identify neutral content vs. biased language patterns.
"""
import asyncio
import os
import httpx
from typing import Dict, Any, Optional, List
import logging
# --- Logging Setup ---
logger = logging.getLogger(__name__)
# --- Hugging Face API Configuration ---
HF_API_URL = "https://api-inference.huggingface.co/models/facebook/bart-large-mnli"
HF_TOKEN = os.getenv("HF_TOKEN")
AGENT_NAME = "penny-bias-checker"
# Define the labels for Zero-Shot Classification.
CANDIDATE_LABELS = [
"neutral and objective",
"contains political bias",
"uses emotional language",
"is factually biased",
]
def _is_bias_available() -> bool:
"""
Check if bias detection service is available.
Returns:
bool: True if HF_TOKEN is configured
"""
return HF_TOKEN is not None and len(HF_TOKEN) > 0
async def check_bias(text: str) -> Dict[str, Any]:
"""
Runs zero-shot classification to check for bias in the input text.
Uses a pre-loaded classification model to analyze text for:
- Neutral and objective language
- Political bias
- Emotional language
- Factual bias
Args:
text: The string of text to analyze for bias
Returns:
Dictionary containing:
- analysis: List of labels with confidence scores, sorted by score
- available: Whether the bias detection service is operational
- message: Optional error or status message
Example:
>>> result = await check_bias("This is neutral text.")
>>> result['analysis'][0]['label']
'neutral and objective'
"""
# Input validation
if not text or not isinstance(text, str):
logger.warning("check_bias called with invalid text input")
return {
"analysis": [],
"available": False,
"message": "Invalid input: text must be a non-empty string"
}
# Strip text to avoid processing whitespace
text = text.strip()
if not text:
logger.warning("check_bias called with empty text after stripping")
return {
"analysis": [],
"available": False,
"message": "Invalid input: text is empty"
}
# Check API availability
if not _is_bias_available():
logger.warning(f"{AGENT_NAME}: API not configured (missing HF_TOKEN)")
return {
"analysis": [],
"available": False,
"message": "Bias detection service is currently unavailable"
}
try:
# Prepare API request for zero-shot classification
headers = {"Authorization": f"Bearer {HF_TOKEN}"}
payload = {
"inputs": text,
"parameters": {
"candidate_labels": CANDIDATE_LABELS,
"multi_label": True
}
}
# Call Hugging Face Inference API
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.post(HF_API_URL, json=payload, headers=headers)
if response.status_code != 200:
logger.error(f"Bias detection API returned status {response.status_code}")
return {
"analysis": [],
"available": False,
"message": f"Bias detection API error: {response.status_code}"
}
results = response.json()
# Validate results structure
if not results or not isinstance(results, dict):
logger.error(f"Bias detection returned unexpected format: {type(results)}")
return {
"analysis": [],
"available": True,
"message": "Inference returned unexpected format"
}
labels = results.get('labels', [])
scores = results.get('scores', [])
if not labels or not scores:
logger.warning("Bias detection returned empty labels or scores")
return {
"analysis": [],
"available": True,
"message": "No classification results returned"
}
# Build analysis results
analysis = [
{"label": label, "score": float(score)}
for label, score in zip(labels, scores)
]
# Sort by confidence score (descending)
analysis.sort(key=lambda x: x['score'], reverse=True)
logger.debug(f"Bias check completed successfully, top result: {analysis[0]['label']} ({analysis[0]['score']:.3f})")
return {
"analysis": analysis,
"available": True
}
except httpx.TimeoutException:
logger.error("Bias detection request timed out")
return {
"analysis": [],
"available": False,
"message": "Bias detection request timed out"
}
except asyncio.CancelledError:
logger.warning("Bias detection task was cancelled")
raise
except Exception as e:
logger.error(f"Error during bias detection inference: {e}", exc_info=True)
return {
"analysis": [],
"available": False,
"message": f"Bias detection error: {str(e)}"
}
def get_bias_pipeline_status() -> Dict[str, Any]:
"""
Returns the current status of the bias detection pipeline.
Returns:
Dictionary with pipeline availability status
"""
return {
"agent_name": AGENT_NAME,
"available": _is_bias_available(),
"api_configured": HF_TOKEN is not None
}