ITheEqualizer's picture
Release Banking77 Intent Error Predictor v1.0.0
74b8664 verified
Raw
History Blame
4.76 kB
from __future__ import annotations
import hashlib
import json
import math
import unicodedata
from pathlib import Path
from typing import Any, cast
import numpy as np
import skops.io as sio
def sha256_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for chunk in iter(lambda: stream.read(1 << 20), b""):
digest.update(chunk)
return digest.hexdigest()
def normalize_text(value: str, maximum_characters: int) -> str:
if not isinstance(value, str):
raise TypeError("input must be a string")
if len(value) > maximum_characters:
raise ValueError(f"input must not exceed {maximum_characters} characters")
normalized = " ".join(unicodedata.normalize("NFKC", value).casefold().split())
if not normalized:
raise ValueError("input must not be empty")
return normalized
def risk_features(probabilities: np.ndarray, texts: list[str]) -> np.ndarray:
clipped = np.clip(probabilities, 1e-12, 1.0)
ordered = np.sort(clipped, axis=1)
top1 = ordered[:, -1]
top2 = ordered[:, -2]
entropy = -(clipped * np.log(clipped)).sum(axis=1) / math.log(clipped.shape[1])
shapes = np.asarray(
[
[
min(len(text), 512) / 512,
min(len(text.split()), 100) / 100,
min(sum(ch.isdigit() for ch in text), 20) / 20,
float("?" in text),
float(
any(
token in text.split()
for token in ("not", "no", "never", "wrong")
)
),
]
for text in texts
],
dtype=np.float64,
)
predicted_one_hot = np.zeros_like(clipped)
predicted_one_hot[np.arange(len(clipped)), np.argmax(clipped, axis=1)] = 1.0
return np.column_stack(
[clipped, predicted_one_hot, top1, top2, top1 - top2, entropy, shapes]
)
def _load_checked(path: Path, expected_hash: str) -> dict[str, Any]:
if sha256_file(path) != expected_hash:
raise RuntimeError(f"artifact checksum mismatch: {path.name}")
untrusted = sio.get_untrusted_types(file=path)
if untrusted:
raise RuntimeError(f"artifact requires untrusted skops types: {untrusted}")
return cast(dict[str, Any], sio.load(path, trusted=[]))
def predict_text(
text: str,
*,
primary_path: Path = Path("primary_baseline.skops"),
risk_path: Path = Path("model.skops"),
config_path: Path = Path("config.json"),
) -> dict[str, Any]:
config = cast(dict[str, Any], json.loads(config_path.read_text(encoding="utf-8")))
if config.get("schema_version") != "1.0":
raise RuntimeError("unsupported configuration schema")
normalized = normalize_text(text, int(config["input"]["maximum_characters"]))
primary = _load_checked(primary_path, str(config["primary_artifact_sha256"]))
candidate = _load_checked(risk_path, str(config["artifact_sha256"]))
labels = cast(list[str], config["labels"])
if labels != primary["labels"] or labels != candidate["labels"]:
raise RuntimeError("label mapping mismatch")
probabilities = np.asarray(
primary["classifier"].predict_proba(
primary["features"].transform([normalized])
),
dtype=np.float64,
)
if probabilities.shape != (1, len(labels)) or not np.isfinite(probabilities).all():
raise RuntimeError("primary model returned invalid probabilities")
predicted = int(probabilities[0].argmax())
error_risk = float(
candidate["classifier"].predict_proba(
risk_features(probabilities, [normalized])
)[0, 1]
)
threshold = float(config["review_at_or_above_error_risk"])
return {
"intent": labels[predicted],
"intent_score": float(probabilities[0, predicted]),
"error_risk": error_risk,
"review": error_risk >= threshold,
"review_threshold": threshold,
"advisory_only": True,
"score_semantics": config["score_semantics"],
}
def main() -> None:
import argparse
parser = argparse.ArgumentParser(description="Route one banking-support query")
parser.add_argument("text")
parser.add_argument("--primary", type=Path, default=Path("primary_baseline.skops"))
parser.add_argument("--risk-model", type=Path, default=Path("model.skops"))
parser.add_argument("--config", type=Path, default=Path("config.json"))
args = parser.parse_args()
result = predict_text(
args.text,
primary_path=args.primary,
risk_path=args.risk_model,
config_path=args.config,
)
print(json.dumps(result, indent=2, sort_keys=True))
if __name__ == "__main__":
main()