from typing import Callable, NamedTuple class _Rule(NamedTuple): name: str decision: str matches: Callable[[dict, dict], bool] confidence: Callable[[dict, dict], float] # Floor ESCALATE at 0.5 — we'd rather over-escalate than miss something urgent. # Other rules scale proportionally with sentiment confidence. RULES: list[_Rule] = [ _Rule( name="negative_high_urgency", decision="ESCALATE", matches=lambda s, sig: s["label"] == "NEGATIVE" and sig["urgency"] == "HIGH", confidence=lambda s, sig: round(min(1.0, 0.5 + s["confidence"] * 0.5), 3), ), _Rule( name="negative_complaint", decision="FOLLOW_UP", matches=lambda s, sig: s["label"] == "NEGATIVE" and sig["intent"] == "COMPLAINT", confidence=lambda s, sig: round(min(1.0, 0.4 + s["confidence"] * 0.4), 3), ), _Rule( name="positive_praise", decision="LOG_FEEDBACK", matches=lambda s, sig: s["label"] == "POSITIVE" and sig["intent"] == "PRAISE", confidence=lambda s, sig: round(min(1.0, 0.3 + s["confidence"] * 0.6), 3), ), _Rule( name="question_any_sentiment", decision="AUTO_RESOLVE", matches=lambda s, sig: sig["intent"] == "QUESTION", confidence=lambda s, sig: 0.75, ), ] _FALLBACK_DECISION = "FOLLOW_UP" _FALLBACK_RULE = "fallback" def _fallback_confidence(sentiment: dict) -> float: # Low ceiling (0.4) signals that no strong rule fired. return round(min(0.4, 0.2 + sentiment["confidence"] * 0.2), 3) def decide(sentiment: dict, signals: dict) -> dict: for rule in RULES: if rule.matches(sentiment, signals): return { "decision": rule.decision, "confidence": rule.confidence(sentiment, signals), "signals_used": { "label": sentiment["label"], "sentiment_confidence": sentiment["confidence"], "urgency": signals["urgency"], "intent": signals["intent"], "rule_matched": rule.name, }, } return { "decision": _FALLBACK_DECISION, "confidence": _fallback_confidence(sentiment), "signals_used": { "label": sentiment["label"], "sentiment_confidence": sentiment["confidence"], "urgency": signals["urgency"], "intent": signals["intent"], "rule_matched": _FALLBACK_RULE, }, }