laya 0.1.0

Rust inference for the Laya non-autoregressive typed-decision model (ModernBERT-large + RL decision head)
Documentation
#!/usr/bin/env python3
"""Condense a raw session into the ~200 tokens the model actually gets to read.

The 512-token budget leaves roughly 320 tokens for the state, and `build_sequence` keeps the
FIRST tokens of the state, so a raw session JSON loses its trailing `tags` — the single most
informative field. This puts the decisive material first and drops the identifiers the model
cannot use (session ids, ASN numbers, exact timestamps).

    python3 banking/summarize.py banking/sessions/*.json
"""
import json
import sys
from datetime import datetime
from pathlib import Path

# Rough great-circle distances between the demo cities, km. A production version would
# geocode properly; the point here is that a geo jump becomes one readable phrase.
CITY = {
    ("Milan", "Lagos"): 4030, ("Lagos", "Milan"): 4030,
    ("Milan", "Zurich"): 220, ("Zurich", "Milan"): 220,
}


def hhmmss(ts):
    return datetime.strptime(ts, "%H:%M:%S")


def summarize(s):
    u, dev, ips, evs = s["user"], s["device"], s["ips"], s["events"]

    # --- geography: the jump, not the addresses
    hops = []
    prev = None
    for e in evs:
        ip = next((i for i in ips if i["ip"] == e["ip"]), None)
        if ip and (prev is None or ip["city"] != prev["city"]):
            if prev is not None:
                dt = (hhmmss(e["ts"]) - hhmmss(hops[-1][1])).total_seconds() / 60
                km = CITY.get((prev["city"], ip["city"]))
                speed = f", {km/max(dt/60, 0.01):,.0f} km/h implied" if km else ""
                hops.append((f"{prev['city']} -> {ip['city']} after {dt:.0f}m{speed}", e["ts"]))
            else:
                hops.append((f"{ip['country']}/{ip['city']}", e["ts"]))
            prev = ip
    geo = "; ".join(h[0] for h in hops)
    new_ips = sum(1 for i in ips if not i.get("seen_before"))
    if new_ips:
        geo += f"; {new_ips} of {len(ips)} IPs never seen for this user"
    asns = {i["asn"].split()[0] for i in ips}
    if len(ips) > 1 and len(asns) == 1:
        geo += "; all IPs in one ASN"

    # --- what the user did, in order, with the money made explicit
    acts, out = [], 0
    for e in evs:
        p = e["path"].replace("/auth/", "").replace("/settings/", "settings ").strip("/")
        d = e.get("detail", {})
        if "amount_eur" in d:
            out += d["amount_eur"]
            bal = f" of {d['balance_before_eur']} balance" if "balance_before_eur" in d else ""
            age = d.get("beneficiary_age_days")
            who = "new beneficiary" if age == 0 else f"beneficiary saved {age}d ago" if age else d.get("beneficiary", "")
            acts.append(f"transfer {d['amount_eur']} EUR{bal} to {who}")
        elif "beneficiary" in d:
            acts.append("added new beneficiary")
        elif "inbound_last_hour_eur" in d:
            acts.append(f"received {d['inbound_last_hour_eur']} EUR from {d['distinct_senders']} senders in 1h")
        elif "balance_eur" in d:
            acts.append(f"balance now {d['balance_eur']} EUR")
        else:
            acts.append(p)

    dur = (hhmmss(evs[-1]["ts"]) - hhmmss(evs[0]["ts"])).total_seconds()
    tags = [t["tag"] if isinstance(t, dict) else t for t in s.get("tags", [])]
    operator = [t["tag"] for t in s.get("tags", []) if isinstance(t, dict) and t["source"].startswith("operator")]

    return {
        "tags": tags or ["none"],
        "operator_notes": operator or ["none"],
        "geo": geo,
        "device": ("known" if dev.get("known") else "NEW, never used by this user") + f" ({dev['os']})",
        "user": f"{u['segment']}, account {u['account_age_days']}d old, mfa "
                + ("enrolled" if u["enrolled_mfa"] else "NOT enrolled"),
        "session_minutes": round(dur / 60, 1),
        "money_out_eur": out,
        "actions_in_order": acts,
    }


if __name__ == "__main__":
    for p in map(Path, sys.argv[1:]):
        out = p.parent.parent / "compact" / p.name
        out.parent.mkdir(parents=True, exist_ok=True)
        out.write_text(json.dumps(summarize(json.loads(p.read_text())), indent=2) + "\n")
        print(f"{p.name} -> {out}")