mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-23 20:23:00 +00:00
104 lines
4.3 KiB
Python
104 lines
4.3 KiB
Python
"""Turn support conversations into numbers with Kai's answers, then fit a small regressor to labels."""
|
|
|
|
import gzip
|
|
import json
|
|
import pathlib
|
|
import tempfile
|
|
import urllib.request
|
|
|
|
import numpy as np
|
|
from hanzo_kai import Kai, Noul, Score, RetryPolicy
|
|
|
|
HARNESS = "https://raw.githubusercontent.com/hanzoai/benchmarks/039fbfe5fa5dcb1483407594d829fa822d78974a/decision/results/states.json.gz"
|
|
|
|
FEATURES = {
|
|
"upset": Noul(
|
|
instructions="The customer is upset.",
|
|
criteria={"true": "they sound angry, frustrated or anxious", "false": "they sound calm or satisfied"},
|
|
),
|
|
"leave": Noul(
|
|
instructions="The customer threatens to leave.",
|
|
criteria={
|
|
"true": "they mention cancelling, switching, a chargeback, a dispute or a bad review",
|
|
"false": "no such threat",
|
|
},
|
|
),
|
|
"again": Noul(
|
|
instructions="The problem has happened before.",
|
|
criteria={
|
|
"true": "they say it keeps happening, or was reported before and is still not fixed",
|
|
"false": "this is the first report",
|
|
},
|
|
),
|
|
"money": Noul(
|
|
instructions="The customer has lost money.",
|
|
criteria={
|
|
"true": "a wrong, duplicate or unexpected charge, or a refund that has not arrived",
|
|
"false": "no money is at stake",
|
|
},
|
|
),
|
|
"effort": Score(
|
|
instructions="How much effort has the customer spent on this problem?",
|
|
criteria=["almost none", "one message", "several messages or attempts", "repeated contacts over days"],
|
|
),
|
|
"tone": Score(
|
|
instructions="How does the customer's last message sound?",
|
|
criteria=["pleased", "neutral", "annoyed", "furious"],
|
|
),
|
|
}
|
|
|
|
|
|
def harness(suite: str) -> list:
|
|
"""The pinned harness's cases for one suite, each [state, questions, gold]."""
|
|
path = pathlib.Path(tempfile.gettempdir()) / "hanzo-kai-cookbook" / "states.json.gz"
|
|
if not path.exists():
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
urllib.request.urlretrieve(HARNESS, path)
|
|
with gzip.open(path) as file:
|
|
return json.load(file)[suite]
|
|
|
|
|
|
def value(answers, name: str) -> float:
|
|
"""A noul's P(yes) or a score's expected level."""
|
|
return answers.nouls[name].noul if name in answers.nouls else answers.scores[name].score
|
|
|
|
|
|
def ridge(x: np.ndarray, y: np.ndarray, train: np.ndarray, penalty: float = 1.0) -> tuple[np.ndarray, np.ndarray]:
|
|
"""Predictions for every row from a ridge fit on the training rows, and the standardized weights."""
|
|
mean, scale = x[train].mean(0), x[train].std(0) + 1e-9
|
|
z = (x - mean) / scale
|
|
weights = np.linalg.solve(
|
|
z[train].T @ z[train] + penalty * np.eye(x.shape[1]), z[train].T @ (y[train] - y[train].mean())
|
|
)
|
|
return y[train].mean() + z @ weights, weights
|
|
|
|
|
|
cases = [case for case in harness("typed_decisions") if case[2]["_wf"] == "customer_service"]
|
|
x, direct, y = [], [], []
|
|
with Kai(retry=RetryPolicy(max_retries=10)) as kai:
|
|
for state, questions, gold in cases:
|
|
answers = kai.decide(state=state, questions=FEATURES | {"churn": questions["churn_risk"]})
|
|
x.append([value(answers, name) for name in FEATURES])
|
|
direct.append(answers.scores["churn"].score)
|
|
y.append(gold["churn_risk"]["gold_score"]) # raters' mean level, 0 to 3
|
|
x, direct, y = np.array(x), np.array(direct)[:, None], np.array(y)
|
|
|
|
order = np.random.default_rng(7).permutation(len(y))
|
|
train, test = order[:60], order[60:]
|
|
print(f"{len(y)} conversations: {len(train)} to fit, {len(test)} to test; churn risk 0 to 3, mean {y.mean():.2f}\n")
|
|
print(f"{'predictor':34} test RMSE correlation")
|
|
fits = {
|
|
"mean of the training labels": np.full(len(y), y[train].mean()),
|
|
"Kai's churn score, as answered": direct[:, 0],
|
|
"Kai's churn score, refit": ridge(direct, y, train)[0],
|
|
"six Kai features": ridge(x, y, train)[0],
|
|
"six features and the churn score": ridge(np.hstack([x, direct]), y, train)[0],
|
|
}
|
|
for name, prediction in fits.items():
|
|
error = np.sqrt(np.mean((prediction[test] - y[test]) ** 2))
|
|
r = f"{np.corrcoef(prediction[test], y[test])[0, 1]:.2f}" if np.ptp(prediction[test]) else "-"
|
|
print(f"{name:34} {error:9.3f} {r:>11}")
|
|
|
|
print("\nweights of the six-feature fit, per standard deviation of each feature:")
|
|
print(" ".join(f"{name} {w:+.3f}" for name, w in zip(FEATURES, ridge(x, y, train)[1], strict=True)))
|