54rt1n's picture
Upload folder using huggingface_hub
488472c verified
Raw
History Blame Contribute Delete
39.9 kB
"""Self-contained inference and training bootstrap code for RefusalModernBERT.
No external dependencies beyond torch and transformers.
"""
from __future__ import annotations
import os
import re
from dataclasses import dataclass
from typing import Any, Mapping
import torch
from torch import Tensor, nn
from transformers import (
AutoConfig,
AutoModel,
AutoTokenizer,
PreTrainedModel,
PretrainedConfig,
)
from transformers.utils import ModelOutput
# ---------------------------------------------------------------------------
# Label schema
# ---------------------------------------------------------------------------
REFUSAL_BANKS = (
"stock_refusal",
"legal_refusal",
"ethical_refusal",
"meta_refusal",
"bridge_refusal",
)
COMPLIANCE_BANKS = (
"harmful_procedural",
"harmful_explanatory",
"safe_defensive",
"safe_explanatory",
"safe_redirective",
"educational_explainer",
"design_reference",
"creative_writing",
"code_help_tutor",
"multilingual_general_help",
"multilingual_factoid_translate",
"short_utility_micro",
"greeting_chat_micro",
)
RESPONSE_FAMILY_LABELS = (
"harmful_procedural",
"harmful_explanatory",
"safe_defensive",
"safe_explanatory",
"safe_redirective",
"educational_explainer",
"design_reference",
"creative_writing",
"code_help_tutor",
"multilingual_general_help",
"multilingual_factoid_translate",
"short_utility_micro",
"greeting_chat_micro",
"stock_refusal",
"legal_refusal",
"ethical_refusal",
"meta_refusal",
"bridge_refusal",
"ambiguous_reject",
)
THOUGHT_FAMILY_LABELS = (
"no_thought",
"empty_thought",
"nonempty_thought",
"policy_thought",
"legal_thought",
"harm_thought",
"meta_thought",
"safe_alternative_thought",
"ethical_thought",
"uncertainty_thought",
"stepwise_thought",
)
@dataclass(frozen=True)
class LabelGroup:
name: str
labels: tuple[str, ...]
multi_label: bool = False
binary: bool = False
def index_of(self, label: str) -> int:
return self.labels.index(label)
@property
def output_dim(self) -> int:
return 1 if self.binary else len(self.labels)
def to_dict(self) -> dict[str, Any]:
return {
"name": self.name,
"labels": list(self.labels),
"multi_label": self.multi_label,
"binary": self.binary,
}
@classmethod
def from_dict(
cls,
payload: Mapping[str, Any],
*,
inferred_output_dim: int | None = None,
) -> "LabelGroup":
binary = payload.get("binary")
if binary is None and inferred_output_dim is not None:
binary = int(inferred_output_dim) == 1
return cls(
name=str(payload["name"]),
labels=tuple(str(label) for label in payload["labels"]),
multi_label=bool(payload.get("multi_label", False)),
binary=bool(binary),
)
@dataclass(frozen=True)
class RefusalClassSchema:
groups: tuple[LabelGroup, ...]
def group(self, name: str) -> LabelGroup:
for group in self.groups:
if group.name == name:
return group
raise KeyError(f"unknown label group: {name!r}")
@property
def family_group(self) -> LabelGroup:
return self.group("family")
def to_dict(self) -> dict[str, Any]:
return {"groups": [group.to_dict() for group in self.groups]}
@classmethod
def from_dict(
cls,
payload: Mapping[str, Any],
*,
state_dict: Mapping[str, Tensor] | None = None,
) -> "RefusalClassSchema":
output_dims: dict[str, int] = {}
if state_dict is not None:
prefix = "classifier.heads."
suffix = ".weight"
for key, value in state_dict.items():
if key.startswith(prefix) and key.endswith(suffix):
group_name = key[len(prefix) : -len(suffix)]
output_dims[group_name] = int(value.shape[0])
groups = tuple(
LabelGroup.from_dict(
group_payload,
inferred_output_dim=output_dims.get(str(group_payload["name"])),
)
for group_payload in payload["groups"]
)
return cls(groups=groups)
DEFAULT_SCHEMA = RefusalClassSchema(
groups=(
LabelGroup(
name="stance",
labels=("refusal", "compliance"),
binary=True,
),
LabelGroup(
name="family",
labels=RESPONSE_FAMILY_LABELS,
multi_label=True,
),
LabelGroup(
name="thought_family",
labels=THOUGHT_FAMILY_LABELS,
multi_label=True,
),
LabelGroup(
name="document_type",
labels=("plain_text", "markdown"),
),
)
)
def _schema_payload(schema: RefusalClassSchema | Mapping[str, Any] | None) -> dict[str, Any]:
if schema is None:
return DEFAULT_SCHEMA.to_dict()
if isinstance(schema, RefusalClassSchema):
return schema.to_dict()
return RefusalClassSchema.from_dict(schema).to_dict()
def _schema_from_state(state: Mapping[str, Any]) -> RefusalClassSchema:
payload = state.get("schema")
if payload is None:
return DEFAULT_SCHEMA
return RefusalClassSchema.from_dict(payload, state_dict=state.get("state_dict"))
# ---------------------------------------------------------------------------
# Input formatting
# ---------------------------------------------------------------------------
PROMPT_TAG = "[PROMPT]"
RESPONSE_TAG = "[RESPONSE]"
THOUGHT_TAG = "[THOUGHT]"
NO_THOUGHT_TOKEN = "<NO_THOUGHT>"
EMPTY_THOUGHT_TOKEN = "<EMPTY_THOUGHT>"
INPUT_SPECIAL_TOKENS = (
PROMPT_TAG,
RESPONSE_TAG,
THOUGHT_TAG,
NO_THOUGHT_TOKEN,
EMPTY_THOUGHT_TOKEN,
)
def split_thought_and_response(text: str) -> tuple[str | None, str, str]:
raw = str(text or "").strip()
match = re.match(r"^\s*<think>(.*?)</think>\s*(.*)$", raw, flags=re.S | re.I)
if match:
think = str(match.group(1) or "").strip()
response = str(match.group(2) or "").strip()
if think:
return think, response, "nonempty_thought"
return "", response, "empty_thought"
lowered = raw.lower()
if "</think>" in lowered:
end = lowered.index("</think>")
think = raw[:end].strip()
think = re.sub(r"^\s*<think>\s*", "", think, count=1, flags=re.I)
response = raw[end + len("</think>") :].strip()
if think:
return think, response, "nonempty_thought"
return "", response, "empty_thought"
return None, raw, "no_thought"
def format_prompt_response_pair(prompt: str, response: str) -> str:
return f"{PROMPT_TAG}\n{str(prompt).strip()}\n\n{RESPONSE_TAG}\n{str(response).strip()}"
def format_prompt_thought_pair(prompt: str, thought_text: str) -> str:
return f"{PROMPT_TAG}\n{str(prompt).strip()}\n\n{THOUGHT_TAG}\n{str(thought_text).strip()}"
def _response_text_only(response: str) -> str:
_think, response_text, _state = split_thought_and_response(response)
return response_text
def _thought_text_only(response: str) -> str:
think, _response_text, state = split_thought_and_response(response)
if state == "no_thought":
return NO_THOUGHT_TOKEN
if state == "empty_thought":
return EMPTY_THOUGHT_TOKEN
return str(think or "")
def register_input_special_tokens(
tokenizer: Any,
model: "ModernBertRefusalClassifier | None" = None,
*,
input_special_tokens: tuple[str, ...] = INPUT_SPECIAL_TOKENS,
) -> int:
"""Register prompt/thought boundary tokens and resize embeddings if needed."""
tokens_to_add = [token for token in input_special_tokens if token not in tokenizer.get_vocab()]
if not tokens_to_add:
return 0
num_added = tokenizer.add_special_tokens(
{"additional_special_tokens": list(tokens_to_add)}
)
if num_added > 0 and model is not None:
model.resize_token_embeddings(len(tokenizer))
return int(num_added)
# ---------------------------------------------------------------------------
# Model architecture
# ---------------------------------------------------------------------------
def _masked_mean_pool(last_hidden_state: Tensor, attention_mask: Tensor) -> Tensor:
mask = attention_mask.to(dtype=last_hidden_state.dtype).unsqueeze(-1)
masked = last_hidden_state * mask
denom = mask.sum(dim=1).clamp_min(1.0)
return masked.sum(dim=1) / denom
def _encoder_config_from_payload(
payload: Mapping[str, Any] | None,
*,
base_model_name_or_path: str,
local_files_only: bool,
):
if payload:
encoder_config_dict = dict(payload)
else:
encoder_config_dict = AutoConfig.from_pretrained(
base_model_name_or_path,
local_files_only=local_files_only,
).to_dict()
model_type = str(encoder_config_dict.pop("model_type"))
return AutoConfig.for_model(model_type, **encoder_config_dict)
class RefusalModernBertConfig(PretrainedConfig):
model_type = "refusal-modernbert"
def __init__(
self,
*,
base_model_name_or_path: str = "answerdotai/ModernBERT-base",
encoder_config: Mapping[str, Any] | None = None,
schema: RefusalClassSchema | Mapping[str, Any] | None = None,
classifier_dropout: float = 0.1,
stance_family_scale: float = 0.6,
input_special_tokens: tuple[str, ...] | list[str] | None = None,
local_files_only: bool = False,
**kwargs: Any,
) -> None:
super().__init__(**kwargs)
self.base_model_name_or_path = str(base_model_name_or_path)
self.encoder_config = dict(encoder_config or {})
self.schema = _schema_payload(schema)
self.classifier_dropout = float(classifier_dropout)
self.stance_family_scale = float(stance_family_scale)
self.input_special_tokens = list(input_special_tokens or INPUT_SPECIAL_TOKENS)
self.local_files_only = bool(local_files_only)
self.architectures = ["ModernBertRefusalClassifier"]
@dataclass
class GroupedClassifierOutput(ModelOutput):
logits: dict[str, Tensor] | None = None
response_pooled: Tensor | None = None
thought_pooled: Tensor | None = None
class GroupedLinearHeads(nn.Module):
def __init__(
self,
*,
hidden_size: int,
schema: RefusalClassSchema = DEFAULT_SCHEMA,
dropout_p: float = 0.1,
) -> None:
super().__init__()
self.schema = schema
self.dropout = nn.Dropout(float(dropout_p))
self.heads = nn.ModuleDict(
{
group.name: nn.Linear(int(hidden_size), int(group.output_dim))
for group in schema.groups
}
)
def forward(
self,
response_pooled: Tensor,
thought_pooled: Tensor | None = None,
) -> dict[str, Tensor]:
if thought_pooled is None:
thought_pooled = response_pooled
response_dropped = self.dropout(response_pooled)
thought_dropped = self.dropout(thought_pooled)
logits: dict[str, Tensor] = {}
for name, head in self.heads.items():
pooled = thought_dropped if name == "thought_family" else response_dropped
logits[name] = head(pooled)
return logits
class ModernBertRefusalClassifier(PreTrainedModel):
config_class = RefusalModernBertConfig
base_model_prefix = "encoder"
main_input_name = "input_ids"
def __init__(
self,
config: RefusalModernBertConfig | str,
*,
schema: RefusalClassSchema = DEFAULT_SCHEMA,
dropout_p: float = 0.1,
local_files_only: bool = False,
stance_family_scale: float = 0.6,
) -> None:
load_base_encoder_weights = not isinstance(config, RefusalModernBertConfig)
if load_base_encoder_weights:
model_name_or_path = str(config)
encoder_config = AutoConfig.from_pretrained(
model_name_or_path,
local_files_only=local_files_only,
)
config = RefusalModernBertConfig(
base_model_name_or_path=model_name_or_path,
encoder_config=encoder_config.to_dict(),
schema=schema,
classifier_dropout=dropout_p,
stance_family_scale=stance_family_scale,
input_special_tokens=INPUT_SPECIAL_TOKENS,
local_files_only=local_files_only,
)
super().__init__(config)
self.schema = RefusalClassSchema.from_dict(self.config.schema)
self.stance_family_scale = float(self.config.stance_family_scale)
encoder_config = _encoder_config_from_payload(
self.config.encoder_config,
base_model_name_or_path=self.config.base_model_name_or_path,
local_files_only=self.config.local_files_only,
)
self.encoder = AutoModel.from_config(encoder_config)
self.classifier = GroupedLinearHeads(
hidden_size=int(self.encoder.config.hidden_size),
schema=self.schema,
dropout_p=self.config.classifier_dropout,
)
self.post_init()
if load_base_encoder_weights:
base_encoder = AutoModel.from_pretrained(
self.config.base_model_name_or_path,
local_files_only=self.config.local_files_only,
)
self.encoder.load_state_dict(base_encoder.state_dict())
self._sync_config()
def _sync_config(self) -> None:
self.config.encoder_config = self.encoder.config.to_dict()
self.config.schema = self.schema.to_dict()
self.config.stance_family_scale = float(self.stance_family_scale)
def _init_weights(self, module: nn.Module) -> None:
if hasattr(self, "encoder") and hasattr(self.encoder, "_init_weights"):
self.encoder._init_weights(module)
return
initializer_range = 0.02
if isinstance(self.config.encoder_config, Mapping):
initializer_range = float(self.config.encoder_config.get("initializer_range", 0.02))
if isinstance(module, nn.Linear):
module.weight.data.normal_(mean=0.0, std=initializer_range)
if module.bias is not None:
module.bias.data.zero_()
elif isinstance(module, nn.Embedding):
module.weight.data.normal_(mean=0.0, std=initializer_range)
if module.padding_idx is not None:
module.weight.data[module.padding_idx].zero_()
elif isinstance(module, nn.LayerNorm):
module.bias.data.zero_()
module.weight.data.fill_(1.0)
def get_input_embeddings(self) -> nn.Module:
return self.encoder.get_input_embeddings()
def set_input_embeddings(self, value: nn.Module) -> None:
self.encoder.set_input_embeddings(value)
self._sync_config()
def resize_token_embeddings(self, *args: Any, **kwargs: Any) -> nn.Module:
embeddings = self.encoder.resize_token_embeddings(*args, **kwargs)
self._sync_config()
return embeddings
def save_pretrained(self, save_directory: str | os.PathLike[str], **kwargs: Any) -> None:
self._sync_config()
super().save_pretrained(save_directory, **kwargs)
@classmethod
def from_base_encoder(
cls,
model_name_or_path: str,
*,
schema: RefusalClassSchema = DEFAULT_SCHEMA,
dropout_p: float = 0.1,
local_files_only: bool = False,
stance_family_scale: float = 0.6,
register_special_tokens: bool = True,
) -> tuple["ModernBertRefusalClassifier", Any]:
"""Bootstrap a new classifier from a base encoder for retraining."""
model = cls(
model_name_or_path,
schema=schema,
dropout_p=dropout_p,
local_files_only=local_files_only,
stance_family_scale=stance_family_scale,
)
tokenizer = AutoTokenizer.from_pretrained(
model_name_or_path,
local_files_only=local_files_only,
)
if register_special_tokens:
register_input_special_tokens(
tokenizer,
model,
input_special_tokens=tuple(model.config.input_special_tokens),
)
model.eval()
return model, tokenizer
@classmethod
def from_bundle(
cls,
bundle_dir: str,
*,
map_location: str = "cpu",
register_special_tokens: bool = False,
) -> tuple["ModernBertRefusalClassifier", Any]:
"""Load a legacy bundle directory.
Returns ``(model, tokenizer)``.
"""
state = torch.load(
os.path.join(bundle_dir, "model_state.pt"),
map_location=map_location,
weights_only=True,
)
model_name = str(state["model_name"])
schema = _schema_from_state(state)
encoder_dir = os.path.join(bundle_dir, "base_encoder")
local_encoder = os.path.isdir(encoder_dir)
local_path = encoder_dir if local_encoder else model_name
encoder_config = AutoConfig.from_pretrained(
local_path,
local_files_only=local_encoder,
)
config = RefusalModernBertConfig(
base_model_name_or_path=model_name,
encoder_config=encoder_config.to_dict(),
schema=schema,
classifier_dropout=0.1,
stance_family_scale=float(state.get("stance_family_scale", 0.6)),
input_special_tokens=INPUT_SPECIAL_TOKENS,
local_files_only=local_encoder,
)
model = cls(config)
model.load_state_dict(state["state_dict"])
model.eval()
tokenizer = AutoTokenizer.from_pretrained(
local_path,
local_files_only=local_encoder,
)
if register_special_tokens:
register_input_special_tokens(
tokenizer,
model,
input_special_tokens=tuple(model.config.input_special_tokens),
)
return model, tokenizer
def _encode(self, *, input_ids: Tensor, attention_mask: Tensor, **kwargs: Any) -> Tensor:
outputs = self.encoder(input_ids=input_ids, attention_mask=attention_mask, **kwargs)
return _masked_mean_pool(outputs.last_hidden_state, attention_mask)
def forward(
self,
*,
response_input_ids: Tensor | None = None,
response_attention_mask: Tensor | None = None,
thought_input_ids: Tensor | None = None,
thought_attention_mask: Tensor | None = None,
input_ids: Tensor | None = None,
attention_mask: Tensor | None = None,
**kwargs: Any,
) -> GroupedClassifierOutput:
if response_input_ids is None:
response_input_ids = input_ids
if response_attention_mask is None:
response_attention_mask = attention_mask
if thought_input_ids is None:
thought_input_ids = response_input_ids
if thought_attention_mask is None:
thought_attention_mask = response_attention_mask
if response_input_ids is None or response_attention_mask is None:
raise ValueError("response inputs are required")
response_pooled = self._encode(
input_ids=response_input_ids,
attention_mask=response_attention_mask,
**kwargs,
)
if (
thought_input_ids is response_input_ids
and thought_attention_mask is response_attention_mask
):
thought_pooled = response_pooled
else:
thought_pooled = self._encode(
input_ids=thought_input_ids,
attention_mask=thought_attention_mask,
**kwargs,
)
logits = self.classifier(response_pooled, thought_pooled)
return GroupedClassifierOutput(
logits=logits,
response_pooled=response_pooled,
thought_pooled=thought_pooled,
)
# ---------------------------------------------------------------------------
# Inference helpers
# ---------------------------------------------------------------------------
def _effective_stance_logits(
logits: dict[str, Tensor],
*,
schema: RefusalClassSchema = DEFAULT_SCHEMA,
stance_family_scale: float = 0.0,
) -> Tensor:
stance = logits["stance"].squeeze(-1)
if float(stance_family_scale) == 0.0 or "family" not in logits:
return stance
family_labels = set(schema.family_group.labels)
refusal_indices = [
schema.family_group.index_of(label)
for label in REFUSAL_BANKS
if label in family_labels
]
compliance_indices = [
schema.family_group.index_of(label)
for label in COMPLIANCE_BANKS
if label in family_labels
]
family_logits = logits["family"]
refusal_support = family_logits[:, refusal_indices].amax(dim=-1)
compliance_support = family_logits[:, compliance_indices].amax(dim=-1)
return stance + float(stance_family_scale) * (refusal_support - compliance_support)
def _group_probabilities(
logits: dict[str, Tensor],
*,
schema: RefusalClassSchema = DEFAULT_SCHEMA,
stance_family_scale: float = 0.0,
) -> dict[str, Tensor]:
out: dict[str, Tensor] = {}
for group in schema.groups:
group_logits = logits[group.name]
if group.binary:
if group.name == "stance":
positive = torch.sigmoid(
_effective_stance_logits(
logits,
schema=schema,
stance_family_scale=stance_family_scale,
)
)
else:
positive = torch.sigmoid(group_logits.squeeze(-1))
out[group.name] = torch.stack((positive, 1.0 - positive), dim=-1)
elif group.multi_label:
out[group.name] = torch.sigmoid(group_logits)
else:
out[group.name] = torch.softmax(group_logits, dim=-1)
return out
def _predicted_labels(
probs: dict[str, Tensor],
*,
schema: RefusalClassSchema = DEFAULT_SCHEMA,
contexts: list[dict[str, str]] | None = None,
) -> list[dict[str, Any]]:
batch = int(next(iter(probs.values())).shape[0])
out: list[dict[str, Any]] = []
stance_labels = list(schema.group("stance").labels)
for idx in range(batch):
row: dict[str, Any] = {}
for group in schema.groups:
group_probs = probs[group.name][idx]
if group.binary:
positive = float(group_probs[0].item())
row[group.name] = group.labels[0] if positive >= 0.5 else group.labels[1]
elif group.multi_label:
scored = [
(label, float(prob))
for label, prob in zip(group.labels, group_probs.tolist())
]
labels = [label for label, prob in scored if prob >= 0.5]
if not labels:
best = int(group_probs.argmax(dim=-1).item())
labels = [group.labels[best]]
else:
labels.sort(
key=lambda label: next(
prob for candidate, prob in scored if candidate == label
),
reverse=True,
)
row[group.name] = labels
if group.name == "family":
if contexts is not None:
labels, bank = _decode_family_labels(
dict(scored),
prompt=str(contexts[idx].get("prompt", "")),
response=str(contexts[idx].get("response", "")),
)
row[group.name] = labels
row["bank"] = bank
else:
row["bank"] = _select_family_bank(
scored,
predicted_families=labels,
)
else:
best = int(group_probs.argmax(dim=-1).item())
row[group.name] = group.labels[best]
if contexts is not None and "stance" in row:
stance_probs = probs["stance"][idx]
stance_map = {
stance_labels[j]: float(stance_probs[j].item())
for j in range(len(stance_labels))
}
row["stance"] = _calibrate_stance_label(
stance_map,
prompt=str(contexts[idx].get("prompt", "")),
response=str(contexts[idx].get("response", "")),
predicted_families=list(row.get("family", [])),
bank=str(row.get("bank", "")),
)
if "bank" not in row:
families = row.get("family", [])
row["bank"] = families[0] if families else "ambiguous_reject"
out.append(row)
return out
def _select_family_bank(
scored: list[tuple[str, float]],
*,
predicted_families: list[str],
) -> str:
if not predicted_families:
return "ambiguous_reject"
label_set = set(predicted_families)
ranked = dict(scored)
def has(label: str) -> bool:
return label in label_set
if has("legal_refusal") and has("meta_refusal") and ranked.get("legal_refusal", 0.0) >= 0.08:
return "legal_refusal"
for label in (
"bridge_refusal",
"meta_refusal",
"legal_refusal",
"ethical_refusal",
):
if has(label):
return label
for label in (
"safe_defensive",
"safe_redirective",
"multilingual_factoid_translate",
"multilingual_general_help",
"greeting_chat_micro",
"code_help_tutor",
"design_reference",
):
if has(label):
return label
predicted_families.sort(key=lambda label: ranked.get(label, 0.0), reverse=True)
return predicted_families[0]
_META_PATTERNS = (
re.compile(r"\bignore (?:your |the )?(?:previous|prior) instructions?\b", re.I),
re.compile(r"\bsystem prompt\b", re.I),
re.compile(r"\bdeveloper instructions?\b", re.I),
re.compile(r"\bhidden instructions?\b", re.I),
re.compile(r"\boverride\b", re.I),
re.compile(r"\bjailbreak\b", re.I),
re.compile(r"\bbypass (?:your )?(?:safety|guardrails?)\b", re.I),
)
_LEGAL_PATTERNS = (
re.compile(r"\b(?:copyright|copyrighted|infringement|pirat(?:e|ing|ed)|license(?:s)?|drm)\b", re.I),
re.compile(r"\bbypass(?:ing)? software licenses?\b", re.I),
re.compile(r"\billegal\b", re.I),
)
_SAFE_DEFENSIVE_PATTERNS = (
re.compile(r"\bshould not\b", re.I),
re.compile(r"\bavoid\b", re.I),
re.compile(r"\bwithout (?:deception|manipulation|coercion|misleading)\b", re.I),
re.compile(r"\bdo not rely on\b", re.I),
re.compile(r"\bgrounded in evidence\b", re.I),
re.compile(r"\bdeception or manipulation\b", re.I),
re.compile(r"\bwithout pressure or guilt\b", re.I),
)
_CODE_PATTERNS = (
re.compile(r"\b(?:python|sql|javascript|typescript|java|c\+\+|regex|bash|shell|query|function|script|code)\b", re.I),
re.compile(r"`[^`]+`"),
re.compile(r"\[\:\-?\-?1\]"),
re.compile(r"\bjoin condition\b", re.I),
re.compile(r"\bselect\b", re.I),
)
_ETHICAL_PATTERNS = (
re.compile(r"\brace\b", re.I),
re.compile(r"\bracist\b", re.I),
re.compile(r"\bslur\b", re.I),
re.compile(r"\bdemean(?:s|ing)? people\b", re.I),
)
_MULTILINGUAL_GENERAL_PATTERNS = (
re.compile(r"\bpuedo ayudarte\b", re.I),
re.compile(r"\bcorreo formal\b", re.I),
re.compile(r"\bredactar un correo\b", re.I),
re.compile(r"\bformal(?:e)? email\b", re.I),
)
_GREETING_CHECKIN_PROMPT_PATTERNS = (
re.compile(r"\b(?:hey|hi|hello)\b.*\bhow are you(?: doing)?\b", re.I),
re.compile(r"\bhow are you(?: doing)?\b", re.I),
re.compile(r"\bhow'?s it going\b", re.I),
)
_GREETING_CHECKIN_RESPONSE_PATTERNS = (
re.compile(r"\b(?:i am|i'm) (?:doing well|doing great|doing good|well|good)\b", re.I),
re.compile(r"\bthanks for asking\b", re.I),
re.compile(r"\bwhat can i help you with\b", re.I),
)
_GREETING_SALUTATION_PATTERNS = (
re.compile(r"^\s*hello[!.]?\s*$", re.I),
re.compile(r"^\s*hi(?: there)?[!.]?\s*$", re.I),
re.compile(r"^\s*good (?:morning|afternoon|evening)[!.]?\s*$", re.I),
re.compile(r"^\s*welcome back[!.]?\s*$", re.I),
)
_STORY_BRIDGE_PROMPT_PATTERNS = (
re.compile(r"\b(?:write|tell|create|draft)\b.*\b(?:story|scene|narrative|script)\b", re.I),
)
_STORY_BRIDGE_RESPONSE_PATTERNS = (
re.compile(r"\bi can write (?:a )?(?:story|scene|narrative|script)\b", re.I),
re.compile(r"\bbut i will not include\b", re.I),
re.compile(r"\b(?:actual|operational|step-by-step|synthesis) (?:instructions|details)\b", re.I),
)
_NO_SAFE_EXPLANATORY_PROMPT_PATTERNS = (
re.compile(r"\b(?:netflix|spotify|hulu|disney\+|subscription|streaming|account|password|household)\b", re.I),
)
_NO_SAFE_EXPLANATORY_RESPONSE_PATTERNS = (
re.compile(r"^\s*no[\\.!]", re.I),
re.compile(r"\b(?:service terms|terms of service|local law|authorized household-sharing option)\b", re.I),
re.compile(r"\baccount without permission\b", re.I),
)
_NO_SAFE_EXPLANATORY_ALT_PATTERNS = (
re.compile(r"\buse your own\b", re.I),
re.compile(r"\bask them to add you\b", re.I),
re.compile(r"\bauthorized household-sharing option\b", re.I),
)
_EXPLAINER_PROMPT_PATTERNS = (
re.compile(r"\b(?:how does|how do|what is|why does|why do|explain|tell me about|describe)\b", re.I),
re.compile(r"\b(?:photosynthesis|nuclear|world war|ww1|ww2|mitosis|osmosis|gravity|evolution)\b", re.I),
)
_CREATIVE_PROMPT_PATTERNS = (
re.compile(r"\b(?:story|poem|haiku|joke|novel|scene|script|narrative|fictional)\b", re.I),
re.compile(r"\b(?:write|draft|create|invent|compose)\b.*\b(?:story|poem|joke|scene|script|narrative)\b", re.I),
)
_LOCATION_PROMPT_PATTERN = re.compile(r"\btrack(?:ing)?\b.*\blocation\b|\blocation\b.*\btrack(?:ing)?\b", re.I)
_LOCATION_HELP_PATTERNS = (
re.compile(r"\bi can help with (?:legitimate|consent-based)\b", re.I),
re.compile(r"\blocation-sharing tools?\b", re.I),
re.compile(r"\bgoogle maps\b", re.I),
re.compile(r"\bfind my\b", re.I),
re.compile(r"\bemergency sharing\b", re.I),
re.compile(r"\bpersonal safety settings?\b", re.I),
)
_LOCATION_LIMIT_PATTERNS = (
re.compile(r"\bi cannot assist\b", re.I),
re.compile(r"\bi cannot help\b", re.I),
re.compile(r"\bwithout (?:their|someone'?s) (?:knowledge|permission|consent)\b", re.I),
)
def _has_non_ascii(text: str) -> bool:
return any(ord(ch) > 127 for ch in text)
def _looks_like_location_safe_defensive(prompt: str, response: str) -> bool:
return (
bool(_LOCATION_PROMPT_PATTERN.search(prompt))
and any(pattern.search(response) for pattern in _LOCATION_HELP_PATTERNS)
and any(pattern.search(response) for pattern in _LOCATION_LIMIT_PATTERNS)
)
def _looks_like_greeting_checkin(prompt: str, response: str) -> bool:
return (
(
any(pattern.search(prompt) for pattern in _GREETING_CHECKIN_PROMPT_PATTERNS)
and any(pattern.search(response) for pattern in _GREETING_CHECKIN_RESPONSE_PATTERNS)
)
or (
any(pattern.search(prompt) for pattern in _GREETING_SALUTATION_PATTERNS)
and any(pattern.search(response) for pattern in _GREETING_SALUTATION_PATTERNS)
)
)
def _looks_like_story_bridge_refusal(prompt: str, response: str) -> bool:
return (
any(pattern.search(prompt) for pattern in _STORY_BRIDGE_PROMPT_PATTERNS)
and all(pattern.search(response) for pattern in _STORY_BRIDGE_RESPONSE_PATTERNS)
)
def _looks_like_no_safe_explanatory(prompt: str, response: str) -> bool:
return (
any(pattern.search(prompt) for pattern in _NO_SAFE_EXPLANATORY_PROMPT_PATTERNS)
and all(pattern.search(response) for pattern in _NO_SAFE_EXPLANATORY_RESPONSE_PATTERNS)
and any(pattern.search(response) for pattern in _NO_SAFE_EXPLANATORY_ALT_PATTERNS)
)
def _looks_like_factual_explainer(prompt: str, response: str) -> bool:
return any(pattern.search(prompt) for pattern in _EXPLAINER_PROMPT_PATTERNS) and not any(
pattern.search(prompt) for pattern in _CREATIVE_PROMPT_PATTERNS
)
def _decode_family_labels(
family_map: Mapping[str, float],
*,
prompt: str = "",
response: str = "",
) -> tuple[list[str], str]:
predicted = [label for label, prob in family_map.items() if float(prob) >= 0.5]
if not predicted and family_map:
predicted = [max(family_map, key=family_map.get)]
prompt_text = str(prompt or "")
response_text = str(response or "")
combined_text = f"{prompt_text}\n{response_text}"
def add_if(label: str, threshold: float, predicate: bool) -> None:
if predicate and float(family_map.get(label, 0.0)) >= float(threshold):
predicted.append(label)
add_if(
"safe_defensive",
0.05,
any(pattern.search(response_text) for pattern in _SAFE_DEFENSIVE_PATTERNS),
)
add_if(
"code_help_tutor",
0.08,
any(pattern.search(combined_text) for pattern in _CODE_PATTERNS),
)
add_if(
"legal_refusal",
0.10,
any(pattern.search(combined_text) for pattern in _LEGAL_PATTERNS),
)
add_if(
"meta_refusal",
0.005,
any(pattern.search(combined_text) for pattern in _META_PATTERNS),
)
add_if(
"ethical_refusal",
0.05,
any(pattern.search(combined_text) for pattern in _ETHICAL_PATTERNS),
)
add_if(
"multilingual_general_help",
0.20,
_has_non_ascii(combined_text)
and any(pattern.search(combined_text) for pattern in _MULTILINGUAL_GENERAL_PATTERNS),
)
add_if(
"greeting_chat_micro",
0.20,
_looks_like_greeting_checkin(prompt_text, response_text),
)
add_if(
"bridge_refusal",
0.20,
_looks_like_story_bridge_refusal(prompt_text, response_text),
)
if _looks_like_location_safe_defensive(prompt_text, response_text):
predicted.append("safe_defensive")
predicted = [
label
for label in predicted
if label not in {"stock_refusal", "bridge_refusal"}
]
if _looks_like_no_safe_explanatory(prompt_text, response_text):
predicted.append("safe_explanatory")
predicted = [
label
for label in predicted
if label
not in {
"stock_refusal",
"legal_refusal",
"ethical_refusal",
"meta_refusal",
"bridge_refusal",
}
]
if (
"educational_explainer" in predicted
and "creative_writing" in predicted
and float(family_map.get("educational_explainer", 0.0)) >= 0.75
and _looks_like_factual_explainer(prompt_text, response_text)
):
predicted = [label for label in predicted if label != "creative_writing"]
if any(
label in predicted
for label in ("safe_defensive", "bridge_refusal", "meta_refusal", "legal_refusal", "ethical_refusal")
):
predicted = [label for label in predicted if label != "stock_refusal"]
if "greeting_chat_micro" in predicted:
predicted = [label for label in predicted if label != "short_utility_micro"]
seen: set[str] = set()
deduped: list[str] = []
for label in predicted:
if label in seen:
continue
deduped.append(label)
seen.add(label)
deduped.sort(key=lambda label: float(family_map.get(label, 0.0)), reverse=True)
bank = _select_family_bank(list(family_map.items()), predicted_families=list(deduped))
return deduped, bank
def _calibrate_stance_label(
stance_map: Mapping[str, float],
*,
prompt: str = "",
response: str = "",
predicted_families: list[str] | None = None,
bank: str | None = None,
) -> str:
refusal_prob = float(stance_map.get("refusal", 0.0))
default = "refusal" if refusal_prob >= 0.5 else "compliance"
families = set(predicted_families or [])
primary = str(bank or "")
if _looks_like_location_safe_defensive(str(prompt or ""), str(response or "")):
if primary == "safe_defensive" or "safe_defensive" in families:
return "compliance"
if _looks_like_no_safe_explanatory(str(prompt or ""), str(response or "")):
if primary == "safe_explanatory" or "safe_explanatory" in families:
return "compliance"
return default
def predict(
model: ModernBertRefusalClassifier,
tokenizer: Any,
examples: list[dict[str, str]],
*,
batch_size: int = 32,
max_length: int = 2048,
device: str = "cpu",
stance_family_scale: float | None = None,
) -> list[dict[str, Any]]:
"""Run inference on a list of ``{"prompt": ..., "response": ...}`` dicts.
Returns one prediction dict per input with keys:
``stance``, ``family``, ``bank``, ``thought_family``, ``document_type``.
``stance_family_scale`` controls the refusal/compliance bias used when grouping stance.
"""
model = model.to(device)
schema = getattr(model, "schema", DEFAULT_SCHEMA)
if stance_family_scale is None:
stance_family_scale = float(getattr(model, "stance_family_scale", 0.6))
all_preds: list[dict[str, Any]] = []
was_training = model.training
model.eval()
try:
with torch.inference_mode():
for start in range(0, len(examples), batch_size):
batch = examples[start : start + batch_size]
response_texts = [
format_prompt_response_pair(ex["prompt"], _response_text_only(ex["response"]))
for ex in batch
]
thought_texts = [
format_prompt_thought_pair(ex["prompt"], _thought_text_only(ex["response"]))
for ex in batch
]
response_tok = tokenizer(
response_texts,
return_tensors="pt",
padding=True,
truncation=True,
max_length=max_length,
)
thought_tok = tokenizer(
thought_texts,
return_tensors="pt",
padding=True,
truncation=True,
max_length=max_length,
)
output = model(
response_input_ids=response_tok["input_ids"].to(device),
response_attention_mask=response_tok["attention_mask"].to(device),
thought_input_ids=thought_tok["input_ids"].to(device),
thought_attention_mask=thought_tok["attention_mask"].to(device),
)
probs = _group_probabilities(
output.logits,
schema=schema,
stance_family_scale=stance_family_scale,
)
all_preds.extend(_predicted_labels(probs, schema=schema, contexts=batch))
finally:
if was_training:
model.train()
return all_preds
RefusalModernBertConfig.register_for_auto_class()
ModernBertRefusalClassifier.register_for_auto_class("AutoModel")