trajaudit / src /scoring.py
Aamish Ahmad
Complete project overhaul
2a3a03b
Raw
History Blame Contribute Delete
8.14 kB
"""
TrajAudit Scoring Engine β€” Trajectory-aware risk accumulation for conversational monitoring.
The core insight: adversarial conversations distribute malicious intent across turns.
Single-turn classifiers miss this because each turn in isolation scores low. The trajectory
score accumulates evidence across the full conversation, detecting the phase-structured
manipulation pattern that defines real-world hybrid human-bot scam pipelines.
Phase Model:
RAPPORT β†’ Behaviorally indistinguishable from normal social interaction
EXTRACTION β†’ Curiosity, shared interests, flattery, information gathering
CAPTURE β†’ Authority signals, social proof, platform redirect, urgency
CONVERSION β†’ Payment request, wallet address, subscription, credential harvesting
"""
from typing import List, Dict, Optional, Tuple
# ── Signal Taxonomy ──────────────────────────────────────────────────────────
# Derived from 6 months of empirical observation of 4 deployed adversarial
# pipelines on Snapchat. Each category maps to observable behavioral patterns
# in the RAPPORT β†’ EXTRACTION β†’ CAPTURE β†’ CONVERSION phase structure.
SIGNAL_WEIGHTS = {
"financial": {
"weight": 0.15,
"keywords": [
"invest", "crypto", "money", "profit", "deposit", "trading", "$",
"usdt", "cashapp", "paypal", "betting", "winnings", "bet", "30%",
"70%", "payout", "earn", "win", "risk-free", "forex", "returns",
"wallet", "transfer", "200", "500", "1000", "4200", "bitcoin",
"zelle", "fee", "refundable", "paycheck", "shipping"
],
"description": "Financial terms indicating extraction/conversion intent"
},
"urgency": {
"weight": 0.10,
"keywords": [
"hurry", "closing", "now", "quick", "limited", "first time",
"last chance", "tonight", "immediately", "clockwork", "deadline",
"tomorrow", "before midnight", "spots fill", "before friday"
],
"description": "Temporal pressure signals to accelerate decision-making"
},
"authority": {
"weight": 0.08,
"keywords": [
"mentor", "team", "expert", "years", "reliable", "uncle", "analyst",
"presidents", "clubs", "uk", "tipico", "settlement", "singapore",
"algorithm", "signals", "whale", "goldman", "coach", "recruiter",
"global", "200+", "300+", "500+", "2000+"
],
"description": "Social proof and authority indicators used in CAPTURE phase"
},
"rapport": {
"weight": 0.04,
"keywords": [
"interesting", "noticed", "usually", "something told me", "honestly",
"nervous too", "totally understand", "life changing", "genuine",
"different", "impressive", "cutie", "vibe", "click"
],
"description": "Rapport-building language β€” low weight individually, contextualizes trajectory"
},
"extraction": {
"weight": 0.18,
"keywords": [
"platform", "register", "sign up", "join", "link", "code", "referral",
"stake", "upfront", "deposit", "wallet", "send me", "cashapp",
"bit.ly", "binance", "telegram", "verify", "age verification",
"card info", "protonmail"
],
"description": "Direct extraction signals β€” highest weight, marks CONVERSION phase"
}
}
def score_message(text: str) -> float:
"""Score a single message for adversarial signal density.
Returns a float [0, 1] representing how many adversarial signals
are present in this individual message.
"""
t = text.lower()
score = 0.0
for category, config in SIGNAL_WEIGHTS.items():
weight = config["weight"]
for keyword in config["keywords"]:
if keyword in t:
score += weight
return min(round(score, 3), 1.0)
def trajectory_score(messages: List[Dict]) -> float:
"""Compute cumulative trajectory risk score across all BOT messages.
This is the core algorithm: risk accumulates across turns with
diminishing returns (the 0.3 decay factor prevents saturation).
The trajectory score captures the behavioral arc β€” from innocent
RAPPORT through progressive EXTRACTION to CONVERSION.
Args:
messages: List of message dicts with 'role' and 'text' keys
Returns:
Cumulative risk score [0, 0.99]
"""
cumulative = 0.0
for msg in messages:
if msg["role"] in ("BOT", "bot"):
signal = score_message(msg["text"])
# Accumulate with decay to model trajectory arc
cumulative = min(cumulative + signal * (1 - cumulative * 0.3), 0.99)
return round(cumulative, 3)
def single_turn_baseline(messages: List[Dict]) -> List[float]:
"""Simulate a single-turn classifier (peak score across individual messages).
This represents the standard approach: evaluate each message independently
and take the maximum observed score. Demonstrates the Detection Latency
gap β€” early messages score near zero because adversarial intent is not
yet present in the text.
Args:
messages: List of message dicts with 'role' and 'text' keys
Returns:
List of peak single-turn scores at each conversation position
"""
scores = []
peak = 0.0
for msg in messages:
if msg["role"] in ("BOT", "bot"):
peak = max(peak, score_message(msg["text"]) * 1.1)
scores.append(round(min(peak, 0.99), 2))
return scores
def trajectory_scores_over_turns(messages: List[Dict]) -> List[float]:
"""Compute trajectory score at each turn position.
Returns a list where index i is the trajectory score using
messages[0:i+1], enabling visualization of the risk accumulation
curve alongside the single-turn baseline.
"""
return [trajectory_score(messages[:i+1]) for i in range(len(messages))]
def classify_risk(score: float) -> Tuple[str, str]:
"""Map a risk score to a phase label and severity.
Returns:
Tuple of (phase_label, severity) where severity is one of
'safe', 'low', 'medium', 'high'
"""
if score >= 0.6:
return "πŸ”΄ Extraction", "high"
elif score >= 0.35:
return "🟠 Trust Building", "medium"
elif score >= 0.15:
return "🟑 Rapport", "low"
else:
return "🟒 Opening", "safe"
def detection_gap(
traj_scores: List[float],
baseline_scores: List[float],
threshold: float = 0.75
) -> Dict:
"""Compute the detection latency gap between trajectory and baseline scoring.
This quantifies the paper's core finding: how many turns earlier does
trajectory-aware monitoring detect adversarial behavior compared to
single-turn classification?
Returns:
Dict with 'traj_flag_turn', 'baseline_flag_turn', 'gap_turns',
and 'description' explaining the gap
"""
traj_flag = next((i+1 for i, s in enumerate(traj_scores) if s >= threshold), None)
base_flag = next((i+1 for i, s in enumerate(baseline_scores) if s >= threshold), None)
gap = None
if traj_flag and base_flag:
gap = base_flag - traj_flag
return {
"traj_flag_turn": traj_flag,
"baseline_flag_turn": base_flag,
"gap_turns": gap,
"description": _describe_gap(traj_flag, base_flag, gap)
}
def _describe_gap(traj_flag, base_flag, gap):
if traj_flag and not base_flag:
return f"TrajAudit flagged at turn {traj_flag}. Single-turn baseline never reached threshold."
elif traj_flag and base_flag and gap and gap > 0:
return f"TrajAudit flagged {gap} turns earlier (turn {traj_flag} vs turn {base_flag})."
elif traj_flag and base_flag and gap == 0:
return "Both methods flagged at the same turn."
elif not traj_flag:
return "Conversation classified as safe by both methods."
return "Unable to compute gap."