Spaces:
Runtime error
Runtime error
File size: 8,369 Bytes
cbb1b1a b861cd9 cbb1b1a b861cd9 cbb1b1a b861cd9 cbb1b1a b861cd9 cbb1b1a b861cd9 cbb1b1a b861cd9 cbb1b1a b861cd9 cbb1b1a b861cd9 cbb1b1a b861cd9 cbb1b1a b861cd9 1f54b5c b861cd9 1f54b5c b861cd9 1f54b5c b861cd9 1f54b5c b861cd9 cbb1b1a 1f54b5c b861cd9 1f54b5c b861cd9 cbb1b1a b861cd9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 | """
NextActionPredictor model definition.
Architecture: 2-hidden-layer MLP (12-dim β 64 β 64 β 6)
Input layout (12 dims, indices):
[0-5] domain one-hot β ecommerce | telecom | banking | cibil | insurance | general
[6-10] entity flags β has_ORG | has_AMOUNT | has_DATE | has_REF_ID | has_ACCOUNT
[11] prior_contact β 1.0 if user has already contacted the company, else 0.0
Output: all 6 EscalationActions sorted by confidence descending.
Fallback: if no checkpoint exists, DOMAIN_ACTION_PRIORS rule-based mapping is
used so the pipeline never crashes.
"""
from __future__ import annotations
import logging
import os
from dataclasses import dataclass
from typing import Optional
import torch
import torch.nn as nn
from src.next_action.priors import ACTION_METADATA, DOMAIN_ACTION_PRIORS
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# Label / feature constants β single source of truth for all modules
# ---------------------------------------------------------------------------
ACTION_LABELS: list[str] = [
"company_support", "nch", "trai", "rbi_ombudsman", "irdai", "legal"
]
ACTION2ID: dict[str, int] = {a: i for i, a in enumerate(ACTION_LABELS)}
# Domain ordering must match the one-hot encoding used during training
DOMAIN_LABELS_ORDERED: list[str] = [
"ecommerce", "telecom", "banking", "cibil", "insurance", "general"
]
# The 5 NER entity types used as routing signals (PERSON excluded β role reference)
ENTITY_NAMES: list[str] = ["ORG", "AMOUNT", "DATE", "REF_ID", "ACCOUNT"]
FEATURE_DIM: int = 12 # 6 + 5 + 1
NUM_ACTIONS: int = 6
# ---------------------------------------------------------------------------
# Feature vector builder β used by train.py and predict.py
# ---------------------------------------------------------------------------
def build_feature_vector(
domain: str,
entity_flags: list[float],
prior_contact: float,
) -> list[float]:
"""
Construct the 12-dim feature vector.
Args:
domain: one of DOMAIN_LABELS_ORDERED
entity_flags: 5 floats (0.0/1.0) in ENTITY_NAMES order
prior_contact: 1.0 if user already contacted the company, else 0.0
Returns: list[float] of length FEATURE_DIM (12)
"""
domain_oh = [1.0 if d == domain else 0.0 for d in DOMAIN_LABELS_ORDERED]
return domain_oh + list(entity_flags) + [float(prior_contact)]
def entities_dict_to_flags(entities: dict) -> list[float]:
"""
Convert an EvidenceNER entities dict {entity_type: value} β 5-dim flag vector.
Example: {"ORG": "Flipkart", "AMOUNT": "βΉ4,299"} β [1.0, 1.0, 0.0, 0.0, 0.0]
"""
return [1.0 if name in entities and entities[name] else 0.0
for name in ENTITY_NAMES]
# ---------------------------------------------------------------------------
# Public output type
# ---------------------------------------------------------------------------
@dataclass
class EscalationAction:
"""A single recommended escalation step."""
action: str
authority: str
url: str
confidence: float
# ---------------------------------------------------------------------------
# Raw PyTorch module
# ---------------------------------------------------------------------------
class GUIDE_MLP(nn.Module):
"""
2-hidden-layer MLP: 12 β 64 β ReLU β 64 β ReLU β 6.
Returns raw logits; softmax is applied at inference time.
"""
def __init__(
self,
input_dim: int = FEATURE_DIM,
hidden_dim: int = 64,
num_classes: int = NUM_ACTIONS,
) -> None:
super().__init__()
self.net = nn.Sequential(
nn.Linear(input_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, num_classes),
)
def forward(self, x: torch.Tensor) -> torch.Tensor: # (B, 12) β (B, 6)
return self.net(x)
# ---------------------------------------------------------------------------
# Rule-based fallback
# ---------------------------------------------------------------------------
def _rule_based_predict(feature_vector: list[float]) -> list[EscalationAction]:
"""
Return a ranked EscalationAction list from DOMAIN_ACTION_PRIORS.
When prior_contact=1, company_support is deprioritised β the user already
tried that path.
"""
domain_oh = feature_vector[:6]
prior_contact = feature_vector[11]
domain_idx = int(max(range(6), key=lambda i: domain_oh[i]))
domain = DOMAIN_LABELS_ORDERED[domain_idx]
ordered: list[str] = list(DOMAIN_ACTION_PRIORS[domain]) # e.g. ["company_support","nch","legal"]
if prior_contact > 0.5 and ordered[0] == "company_support":
# Already tried company β rotate company_support to last position
ordered = ordered[1:] + ordered[:1]
# Rank-based exponential score: position 0 β highest
decay = 0.55
score_map: dict[str, float] = {}
s = 1.0
for action in ordered:
score_map[action] = s
s *= decay
for action in ACTION_LABELS:
if action not in score_map:
score_map[action] = decay ** len(ordered) # small residual
total = sum(score_map.values())
return sorted(
[
EscalationAction(
action=action,
authority=ACTION_METADATA[action]["authority"],
url=ACTION_METADATA[action]["url"],
confidence=round(score_map[action] / total, 4),
)
for action in ACTION_LABELS
],
key=lambda e: -e.confidence,
)
# ---------------------------------------------------------------------------
# NextActionPredictor β public wrapper
# ---------------------------------------------------------------------------
class NextActionPredictor:
"""
MLP escalation router with rule-based fallback.
Pass model_path=None (or a path that does not exist) to use the rule-based
fallback only. The pipeline never crashes: if the checkpoint is missing
a warning is logged and DOMAIN_ACTION_PRIORS is used instead.
"""
def __init__(self, model_path: Optional[str] = None) -> None:
self._mlp: Optional[GUIDE_MLP] = None
self._device = torch.device(
"cuda" if torch.cuda.is_available()
else "mps" if torch.backends.mps.is_available()
else "cpu"
)
if model_path and os.path.isfile(model_path):
try:
ckpt = torch.load(model_path, map_location=self._device, weights_only=True)
self._mlp = GUIDE_MLP()
self._mlp.load_state_dict(ckpt["state_dict"])
self._mlp.to(self._device)
self._mlp.eval()
logger.info("NextActionPredictor loaded from %s on %s", model_path, self._device)
except Exception:
logger.warning(
"Failed to load NextActionPredictor from %s β using rule-based fallback.",
model_path, exc_info=True,
)
else:
logger.info(
"No NextActionPredictor checkpoint at '%s' β using rule-based fallback.",
model_path,
)
@property
def uses_fallback(self) -> bool:
return self._mlp is None
def predict(self, feature_vector: list[float]) -> list[EscalationAction]:
"""Return all 6 EscalationActions ranked by confidence (highest first)."""
if self._mlp is None:
return _rule_based_predict(feature_vector)
x = torch.tensor(feature_vector, dtype=torch.float32).unsqueeze(0).to(self._device)
with torch.no_grad():
logits = self._mlp(x)[0].cpu() # (6,)
probs: list[float] = torch.softmax(logits, dim=-1).tolist()
return sorted(
[
EscalationAction(
action=ACTION_LABELS[i],
authority=ACTION_METADATA[ACTION_LABELS[i]]["authority"],
url=ACTION_METADATA[ACTION_LABELS[i]]["url"],
confidence=round(probs[i], 4),
)
for i in range(NUM_ACTIONS)
],
key=lambda e: -e.confidence,
)
|