akshay4 commited on
Commit
949430d
Β·
verified Β·
1 Parent(s): 5ed6591

Upload folder using huggingface_hub

Browse files
scripts/compare_witgym_zero_shot.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """WitGym vs zero-shot baseline β€” same model, same API backend."""
2
+ import json
3
+ import sys
4
+ import time
5
+ from pathlib import Path
6
+
7
+ from dotenv import load_dotenv
8
+
9
+ load_dotenv()
10
+
11
+ # 7 diverse scenarios (5 canonical + 2 extra)
12
+ TEST_INPUTS = [
13
+ "I just got promoted to manager and I have no idea what I'm doing.",
14
+ "My coworker keeps stealing my lunch from the fridge.",
15
+ "I've been cc'd on an email chain I definitely should not be reading.",
16
+ "I'm pretending to understand cryptocurrency at dinner parties.",
17
+ "My therapist fell asleep during our session.",
18
+ "My boss called a quick sync that's been going for two hours.",
19
+ "I waved back at someone who wasn't waving at me.",
20
+ ]
21
+
22
+ ZERO_SHOT_PROMPT = """You are a witty friend. The user shares an awkward or funny situation.
23
+ Reply with ONE sharp, funny line. Maximum 2 sentences. No preamble.
24
+
25
+ User: {user_input}
26
+
27
+ Funny reply:"""
28
+
29
+
30
+ def zero_shot_reply(user_input: str, model, tokenizer) -> str:
31
+ from witgym.model import generate_text
32
+
33
+ prompt = ZERO_SHOT_PROMPT.format(user_input=user_input)
34
+ raw = generate_text(prompt, model, tokenizer, config_type="generate")
35
+ return raw.strip().strip('"')
36
+
37
+
38
+ def run_experiment(index_path: str = "data/index.npz") -> dict:
39
+ import os
40
+
41
+ os.environ.setdefault("LLM_BACKEND", "hf_api")
42
+
43
+ from witgym import config
44
+ from witgym.engine import WitGymEngine
45
+ from witgym.model import load_model
46
+
47
+ if not Path(index_path).exists():
48
+ print(f"[ERROR] Index missing: {index_path}")
49
+ sys.exit(1)
50
+
51
+ print(f"Model: {config.LLM_MODEL_ID} | Backend: {config.LLM_BACKEND}")
52
+ print(f"Providers: {config.HF_INFERENCE_PROVIDERS}\n")
53
+
54
+ # Shared embedder/index only β€” fresh engine per input (no conversation bleed)
55
+ shared = None
56
+ model, tokenizer = load_model()
57
+
58
+ rows = []
59
+ for i, user_input in enumerate(TEST_INPUTS, 1):
60
+ print(f"[{i}/{len(TEST_INPUTS)}] {user_input[:65]}...")
61
+
62
+ from witgym.engine import WitGymEngine, SharedResources
63
+
64
+ nonlocal_shared = shared
65
+ if nonlocal_shared is None:
66
+ nonlocal_shared = SharedResources(index_path=index_path)
67
+ shared = nonlocal_shared
68
+ engine = WitGymEngine(index_path=index_path, resources=nonlocal_shared)
69
+
70
+ t0 = time.time()
71
+ wg = engine.respond(user_input)
72
+ wg_elapsed = time.time() - t0
73
+
74
+ t0 = time.time()
75
+ zs = zero_shot_reply(user_input, model, tokenizer)
76
+ zs_elapsed = time.time() - t0
77
+
78
+ row = {
79
+ "input": user_input,
80
+ "witgym": {
81
+ "text": wg.selected,
82
+ "words": len(wg.selected.split()),
83
+ "latency_s": round(wg_elapsed, 1),
84
+ "archetype": wg.metadata.archetype.value,
85
+ "candidates": [c.text for c in wg.candidates],
86
+ },
87
+ "zero_shot": {
88
+ "text": zs,
89
+ "words": len(zs.split()),
90
+ "latency_s": round(zs_elapsed, 1),
91
+ },
92
+ }
93
+ rows.append(row)
94
+ print(f" WitGym ({row['witgym']['words']}w, {wg_elapsed:.1f}s): {wg.selected}")
95
+ print(f" Zero ({row['zero_shot']['words']}w, {zs_elapsed:.1f}s): {zs}\n")
96
+
97
+ out = {
98
+ "model": config.LLM_MODEL_ID,
99
+ "backend": config.LLM_BACKEND,
100
+ "zero_shot_prompt": ZERO_SHOT_PROMPT,
101
+ "results": rows,
102
+ }
103
+ out_path = Path("data/eval_witgym_vs_zero_shot.json")
104
+ out_path.write_text(json.dumps(out, indent=2), encoding="utf-8")
105
+ print(f"Saved β†’ {out_path}")
106
+ return out
107
+
108
+
109
+ if __name__ == "__main__":
110
+ run_experiment()
witgym/config.py CHANGED
@@ -23,7 +23,7 @@ EXTRACT_TEMP = 0.2
23
  EXTRACT_DO_SAMPLE = False
24
  GENERATE_TEMP = 1.0 # slightly lower to reduce arbitrary domain invention
25
  GENERATE_MIN_P = 0.06
26
- GENERATE_MAX_NEW_TOKENS = 40
27
  RANK_TEMP = 0.1
28
  EXTRACT_MAX_NEW_TOKENS = 220 # JSON never exceeds ~150 tokens; 220 = 47% safety margin
29
 
 
23
  EXTRACT_DO_SAMPLE = False
24
  GENERATE_TEMP = 1.0 # slightly lower to reduce arbitrary domain invention
25
  GENERATE_MIN_P = 0.06
26
+ GENERATE_MAX_NEW_TOKENS = 72
27
  RANK_TEMP = 0.1
28
  EXTRACT_MAX_NEW_TOKENS = 220 # JSON never exceeds ~150 tokens; 220 = 47% safety margin
29
 
witgym/engine.py CHANGED
@@ -314,8 +314,8 @@ class WitGymEngine:
314
  context_str = f"[Previous line β€” do NOT repeat]: {exclude_joke}\n" + context_str
315
  personas_to_run = ["cynic", "conviction", "absurdist"]
316
  if metadata.twist_potential <= 6:
317
- personas_to_run = ["cynic", "conviction"]
318
- logger.info(f"twist_potential={metadata.twist_potential} ≀ 6 β€” cynic + conviction (orthogonal pair)")
319
  elif metadata.twist_potential > 8:
320
  personas_to_run = ["cynic", "absurdist", "bisociate"]
321
  logger.info(f"twist_potential={metadata.twist_potential} > 8 β€” bisociate replaces conviction")
 
314
  context_str = f"[Previous line β€” do NOT repeat]: {exclude_joke}\n" + context_str
315
  personas_to_run = ["cynic", "conviction", "absurdist"]
316
  if metadata.twist_potential <= 6:
317
+ personas_to_run = ["cynic", "absurdist"]
318
+ logger.info(f"twist_potential={metadata.twist_potential} ≀ 6 β€” cynic + absurdist")
319
  elif metadata.twist_potential > 8:
320
  personas_to_run = ["cynic", "absurdist", "bisociate"]
321
  logger.info(f"twist_potential={metadata.twist_potential} > 8 β€” bisociate replaces conviction")
witgym/extractor.py CHANGED
@@ -28,14 +28,15 @@ Return ONLY a JSON object with these exact fields (no explanation, no markdown,
28
  }}
29
 
30
  Think carefully about the ARCHETYPE β€” pick the one that most accurately describes the comedy mechanism hiding in this input.
31
- Archetype selection guidance (avoid overusing self_delusion):
32
- - status_assertion: claiming authority/status/rightness as if saying it makes it true
33
- - misplaced_conf: confident competence claim immediately unsupported by reality
34
- - anxiety_escalation: small trigger spun into catastrophe / inevitable doom logic
35
- - social_fail: awkward performance, norm violation, cringe, saying the wrong thing at the wrong time
36
- - power_inversion: low-status person is the only honest/correct one, or social power overrides institutional power
37
- - self_delusion: specifically a self-image story ("I'm fine / I'm great / I'm the best") contradicted by behavior/evidence in the same moment
38
- If unsure between self_delusion vs social_fail/anxiety_escalation, prefer the more specific one (social_fail/anxiety_escalation) unless there is an explicit self-image contradiction.
 
39
  For twist_potential: score high if the input has self-delusion, status gap, or absurd logic. Score low if it is a neutral factual statement with no tension.
40
  If the input is ONLY a greeting, social opener, or typo-greeting with no described situation, set twist_potential to 1 and subtext to "greeting only β€” no situational content". Do not invent comedy tension.
41
  For connector: look for a single word or short phrase that carries an expected meaning in context AND a second meaning that reframes the situation. Most inputs will have null. Return null unless a genuine dual-reading exists (e.g. "manage" can mean control people or barely cope; "balance" can mean financial or emotional equilibrium).
 
28
  }}
29
 
30
  Think carefully about the ARCHETYPE β€” pick the one that most accurately describes the comedy mechanism hiding in this input.
31
+ Archetype selection guidance (avoid overusing social_fail β€” use it only for literal norm violations):
32
+ - status_assertion: claiming authority/status/rightness as if saying it makes it true (e.g. "I just got promoted and have no idea what I'm doing" β€” claiming the title while admitting incompetence)
33
+ - misplaced_conf: confident competence claim immediately unsupported by reality (e.g. "my boss called a quick sync that's been going for two hours" β€” the word "quick" is the mismatch)
34
+ - anxiety_escalation: small trigger spun into catastrophe / inevitable doom logic (e.g. "my coworker keeps stealing my lunch" β€” escalating a minor loss into a power/control spiral)
35
+ - social_fail: awkward performance, norm violation, cringe β€” ONLY when the person did/said something embarrassing in public (e.g. "I waved back at someone who wasn't waving at me" β€” literal public norm violation)
36
+ - power_inversion: low-status person is the only honest/correct one, or the person with formal authority is visibly incompetent (e.g. "my therapist fell asleep during our session" β€” the helper needs help)
37
+ - self_delusion: specifically a self-image story ("I'm fine / I'm great / I'm the best") contradicted by behavior/evidence (e.g. "I'm pretending to understand cryptocurrency at dinner parties" β€” actively constructing a false expert identity)
38
+ CRITICAL: Do NOT use social_fail as a default. Most awkward situations are self_delusion, anxiety_escalation, power_inversion, or misplaced_conf. social_fail requires a public embarrassing action the person actually performed.
39
+ If unsure between self_delusion vs social_fail, ask: did the person DO something publicly awkward (social_fail) or are they MAINTAINING a false self-image (self_delusion)?
40
  For twist_potential: score high if the input has self-delusion, status gap, or absurd logic. Score low if it is a neutral factual statement with no tension.
41
  If the input is ONLY a greeting, social opener, or typo-greeting with no described situation, set twist_potential to 1 and subtext to "greeting only β€” no situational content". Do not invent comedy tension.
42
  For connector: look for a single word or short phrase that carries an expected meaning in context AND a second meaning that reframes the situation. Most inputs will have null. Return null unless a genuine dual-reading exists (e.g. "manage" can mean control people or barely cope; "balance" can mean financial or emotional equilibrium).
witgym/router.py CHANGED
@@ -83,14 +83,27 @@ def _parse_route(raw: str) -> Route | None:
83
 
84
 
85
  def classify_intent(text: str, model, tokenizer) -> Route:
86
- """Route banter | quick_wit | coaching via LLM, with heuristic fallback."""
 
 
 
 
87
  from witgym.model import generate_text
88
 
89
  if not text.strip():
90
  return "banter"
91
 
 
 
 
92
  heuristic = _heuristic_route(text)
93
 
 
 
 
 
 
 
94
  raw = generate_text(
95
  ROUTER_PROMPT.format(user_input=text.strip()),
96
  model,
@@ -104,5 +117,5 @@ def classify_intent(text: str, model, tokenizer) -> Route:
104
  )
105
  route = heuristic
106
  else:
107
- logger.info(f"Router: {route!r} for input {text[:60]!r}")
108
  return route
 
83
 
84
 
85
  def classify_intent(text: str, model, tokenizer) -> Route:
86
+ """Route banter | quick_wit | coaching via LLM, with heuristic fallback.
87
+
88
+ Heuristic takes priority when there is no explicit coaching signal β€” avoids
89
+ LLM misclassifying confessional 'I just X' situations as coaching.
90
+ """
91
  from witgym.model import generate_text
92
 
93
  if not text.strip():
94
  return "banter"
95
 
96
+ normalized = text.strip().lower()
97
+ has_coaching_signal = any(sig in normalized for sig in _COACHING_SIGNALS)
98
+
99
  heuristic = _heuristic_route(text)
100
 
101
+ # Skip LLM router when heuristic is confident and no explicit coaching signal:
102
+ # this prevents the LLM from misrouting confessional situation inputs to coaching.
103
+ if heuristic == "quick_wit" and not has_coaching_signal:
104
+ logger.info(f"Router: heuristic fast-path β†’ 'quick_wit' for {text[:60]!r}")
105
+ return "quick_wit"
106
+
107
  raw = generate_text(
108
  ROUTER_PROMPT.format(user_input=text.strip()),
109
  model,
 
117
  )
118
  route = heuristic
119
  else:
120
+ logger.info(f"Router: LLM β†’ {route!r} for input {text[:60]!r}")
121
  return route