Akshay Babbar commited on
Commit
640ccba
Β·
1 Parent(s): f1cf4a9

interim commit-v3

Browse files
witgym/conversation.py CHANGED
@@ -2,7 +2,7 @@
2
  from typing import List, Tuple, Set
3
  from loguru import logger
4
  from witgym import config
5
- from witgym.schemas import ComedyArchetype
6
 
7
  COMPRESS_PROMPT = """\
8
  Summarise the following conversation into a compact factual paragraph (max 100 words).
@@ -20,20 +20,29 @@ class ConversationManager:
20
  self.history: List[Tuple[str, str]] = [] # (user, assistant)
21
  self.used_archetypes: Set[ComedyArchetype] = set()
22
  self._summary: str = "" # Compressed summary of old turns
 
23
 
24
- def add_turn(self, user_input: str, response: str, archetype: ComedyArchetype):
25
  self.history.append((user_input, response))
26
- self.used_archetypes.add(archetype)
 
 
 
 
 
27
 
28
  def get_context_string(self) -> str:
29
- """Return the last N turns as formatted string, prefixed with summary if compressed."""
30
- recent = self.history[-config.KEEP_LAST_N_TURNS:]
 
 
 
 
31
  lines = []
32
  if self._summary:
33
  lines.append(f"[Earlier conversation summary]: {self._summary}")
34
- for user, assistant in recent:
35
- lines.append(f"User: {user}")
36
- lines.append(f"WitGym: {assistant}")
37
  return "\n".join(lines)
38
 
39
  def needs_compression(self, tokenizer) -> bool:
 
2
  from typing import List, Tuple, Set
3
  from loguru import logger
4
  from witgym import config
5
+ from witgym.schemas import ComedyArchetype, ComedyMetadata
6
 
7
  COMPRESS_PROMPT = """\
8
  Summarise the following conversation into a compact factual paragraph (max 100 words).
 
20
  self.history: List[Tuple[str, str]] = [] # (user, assistant)
21
  self.used_archetypes: Set[ComedyArchetype] = set()
22
  self._summary: str = "" # Compressed summary of old turns
23
+ self._mechanisms: List[Tuple[str, str, str]] = [] # (archetype, tension, distance)
24
 
25
+ def add_turn(self, user_input: str, response: str, metadata: ComedyMetadata):
26
  self.history.append((user_input, response))
27
+ self.used_archetypes.add(metadata.archetype)
28
+ self._mechanisms.append((
29
+ metadata.archetype.value,
30
+ metadata.tension_type.value,
31
+ metadata.violation_distance.value,
32
+ ))
33
 
34
  def get_context_string(self) -> str:
35
+ """Return the last N turns as mechanism-only context.
36
+
37
+ Structural goal: preserve callback potential (what kind of jokes were made),
38
+ while avoiding lexical bleed from raw user/assistant text across turns.
39
+ """
40
+ recent = self._mechanisms[-config.KEEP_LAST_N_TURNS:]
41
  lines = []
42
  if self._summary:
43
  lines.append(f"[Earlier conversation summary]: {self._summary}")
44
+ for i, (arch, tension, dist) in enumerate(recent, 1):
45
+ lines.append(f"Turn -{len(recent) - i + 1}: archetype={arch}, tension={tension}, distance={dist}")
 
46
  return "\n".join(lines)
47
 
48
  def needs_compression(self, tokenizer) -> bool:
witgym/engine.py CHANGED
@@ -50,7 +50,7 @@ class WitGymEngine:
50
  if metadata.twist_potential < 4:
51
  logger.info(f"twist_potential={metadata.twist_potential} < 4 β€” returning straight reply")
52
  selected = "Yeah, that tracks."
53
- self.conversation.add_turn(user_input, selected, metadata.archetype)
54
  return WitGymResponse(
55
  metadata=metadata,
56
  retrieved_scenes=[],
@@ -68,12 +68,12 @@ class WitGymEngine:
68
 
69
  # PASS 2 β€” Generate persona candidates.
70
  # Medium inputs (4-6): run cynic + absurdist only.
71
- # Rich inputs (> 6): run all three including frame_switcher.
72
  context_str = self.conversation.get_context_string()
73
  personas_to_run = None # None = all three
74
  if metadata.twist_potential <= 6:
75
- personas_to_run = ["cynic", "absurdist"] # Frame switcher needs rich tension
76
- logger.info(f"twist_potential={metadata.twist_potential} ≀ 6 β€” skipping frame_switcher")
77
 
78
  candidates = generate_candidates(
79
  user_input, metadata, scenes,
@@ -92,7 +92,7 @@ class WitGymEngine:
92
  selected = _cap_two_sentences(selected)
93
 
94
  # Update state
95
- self.conversation.add_turn(user_input, selected, metadata.archetype)
96
 
97
  return WitGymResponse(
98
  metadata=metadata,
 
50
  if metadata.twist_potential < 4:
51
  logger.info(f"twist_potential={metadata.twist_potential} < 4 β€” returning straight reply")
52
  selected = "Yeah, that tracks."
53
+ self.conversation.add_turn(user_input, selected, metadata)
54
  return WitGymResponse(
55
  metadata=metadata,
56
  retrieved_scenes=[],
 
68
 
69
  # PASS 2 β€” Generate persona candidates.
70
  # Medium inputs (4-6): run cynic + absurdist only.
71
+ # Rich inputs (> 6): run all three including conviction.
72
  context_str = self.conversation.get_context_string()
73
  personas_to_run = None # None = all three
74
  if metadata.twist_potential <= 6:
75
+ personas_to_run = ["cynic", "absurdist"] # Conviction needs richer tension to land cleanly
76
+ logger.info(f"twist_potential={metadata.twist_potential} ≀ 6 β€” skipping conviction")
77
 
78
  candidates = generate_candidates(
79
  user_input, metadata, scenes,
 
92
  selected = _cap_two_sentences(selected)
93
 
94
  # Update state
95
+ self.conversation.add_turn(user_input, selected, metadata)
96
 
97
  return WitGymResponse(
98
  metadata=metadata,
witgym/extractor.py CHANGED
@@ -23,6 +23,14 @@ Return ONLY a JSON object with these exact fields (no explanation, no markdown,
23
  }}
24
 
25
  Think carefully about the ARCHETYPE β€” pick the one that most accurately describes the comedy mechanism hiding in this input.
 
 
 
 
 
 
 
 
26
  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.
27
  Return ONLY the JSON. Nothing else."""
28
 
 
23
  }}
24
 
25
  Think carefully about the ARCHETYPE β€” pick the one that most accurately describes the comedy mechanism hiding in this input.
26
+ Archetype selection guidance (avoid overusing self_delusion):
27
+ - status_assertion: claiming authority/status/rightness as if saying it makes it true
28
+ - misplaced_conf: confident competence claim immediately unsupported by reality
29
+ - anxiety_escalation: small trigger spun into catastrophe / inevitable doom logic
30
+ - social_fail: awkward performance, norm violation, cringe, saying the wrong thing at the wrong time
31
+ - power_inversion: low-status person is the only honest/correct one, or social power overrides institutional power
32
+ - 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
33
+ 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.
34
  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.
35
  Return ONLY the JSON. Nothing else."""
36
 
witgym/generator.py CHANGED
@@ -20,15 +20,12 @@ PERSONA_INSTRUCTIONS = {
20
  "Open with the situation, the consequence, or the outcome as your subject β€” "
21
  "lead with something concrete and specific, not a generic observation about the person."
22
  ),
23
- "frame_switcher": (
24
- "This situation is actually being processed by a completely unrelated formal institution. "
25
- "Choose whichever institution maps most absurdly and precisely onto the specific tension in this input β€” "
26
- "a regulatory body, a scientific agency, a legal tribunal, a certification board, an athletic committee, "
27
- "or any other formal system whose rules would make this situation absurd. "
28
- "Do not list the institution's name as a setup β€” just speak in its voice directly. "
29
- "Check the CONVERSATION CONTEXT: if a prior WitGym response already used a particular institution type, "
30
- "pick a different one. "
31
- "Report with total conviction, as if this framing is obviously correct."
32
  ),
33
  "absurdist": (
34
  "You're the only person in the room who sees where this logically ends. "
@@ -256,13 +253,13 @@ def rank_candidates(
256
 
257
 
258
  _COMPRESS_PROMPT = """\
259
- The following comedy line is good but possibly too long. Compress it to ≀12 words while keeping the punchline completely intact.
260
 
261
  Original: "{winner}"
262
 
263
  Rules:
264
  - The FINAL CLAUSE of the sentence is almost always the punchline. NEVER cut it. Cut from the setup or the middle only.
265
- - If the final clause must be removed to hit ≀12 words, return the original UNCHANGED.
266
  - Do NOT change the joke structure β€” only remove filler words from the setup.
267
  - Return ONLY the compressed version or the original. No explanation. No quotes."""
268
 
@@ -270,10 +267,10 @@ Rules:
270
  def compress_winner(winner: str, model, tokenizer) -> str:
271
  """Swartzwelder compression pass: generate loose, cut ruthless.
272
 
273
- Skipped if winner is already ≀12 words (already tight).
274
  Guards: rejects output that is < 4 words or longer than the original.
275
  """
276
- if len(winner.split()) <= 12:
277
  return winner # Already tight β€” skip the LLM call
278
 
279
  prompt = _COMPRESS_PROMPT.format(winner=winner)
@@ -292,6 +289,19 @@ def compress_winner(winner: str, model, tokenizer) -> str:
292
  logger.debug(f"Compression rejected (fragment start: '{compressed[:20]}'). Keeping original.")
293
  return winner
294
 
 
 
 
 
 
 
 
 
 
 
 
 
 
295
  logger.info(f"Compressed: {len(winner.split())}w β†’ {len(compressed.split())}w | '{compressed}'")
296
  return compressed
297
 
 
20
  "Open with the situation, the consequence, or the outcome as your subject β€” "
21
  "lead with something concrete and specific, not a generic observation about the person."
22
  ),
23
+ "conviction": (
24
+ "You have a firm, specific belief about how this situation works. "
25
+ "State it as established fact with total sincerity. "
26
+ "Do not hedge. Do not qualify. Do not acknowledge any other interpretation exists. "
27
+ "The belief should be wrong in a way that exposes something true about the speaker. "
28
+ "No irony. No wink. Absolute conviction."
 
 
 
29
  ),
30
  "absurdist": (
31
  "You're the only person in the room who sees where this logically ends. "
 
253
 
254
 
255
  _COMPRESS_PROMPT = """\
256
+ The following comedy line is good but possibly too long. Compress it to ≀18 words while keeping the punchline completely intact.
257
 
258
  Original: "{winner}"
259
 
260
  Rules:
261
  - The FINAL CLAUSE of the sentence is almost always the punchline. NEVER cut it. Cut from the setup or the middle only.
262
+ - If the final clause must be removed to hit ≀18 words, return the original UNCHANGED.
263
  - Do NOT change the joke structure β€” only remove filler words from the setup.
264
  - Return ONLY the compressed version or the original. No explanation. No quotes."""
265
 
 
267
  def compress_winner(winner: str, model, tokenizer) -> str:
268
  """Swartzwelder compression pass: generate loose, cut ruthless.
269
 
270
+ Skipped if winner is already ≀18 words (already tight).
271
  Guards: rejects output that is < 4 words or longer than the original.
272
  """
273
+ if len(winner.split()) <= 18:
274
  return winner # Already tight β€” skip the LLM call
275
 
276
  prompt = _COMPRESS_PROMPT.format(winner=winner)
 
289
  logger.debug(f"Compression rejected (fragment start: '{compressed[:20]}'). Keeping original.")
290
  return winner
291
 
292
+ # Grammar-ish guard: reject "telegraphic" outputs that drop almost all function words.
293
+ # This is intentionally crude but catches the common failure mode:
294
+ # "Stared at smoking toaster until fire needs human response registered like software update."
295
+ _FUNCTION_WORDS = {
296
+ "a", "an", "the",
297
+ "to", "of", "for", "in", "on", "at", "with", "as", "by", "from", "into",
298
+ "like", "than", "then", "that", "which", "because", "until", "while", "since",
299
+ }
300
+ words = [w.lower() for w in _re.findall(r"\b\w+\b", compressed)]
301
+ if len(words) >= 8 and not any(w in _FUNCTION_WORDS for w in words):
302
+ logger.debug("Compression rejected (telegraphic/no function words). Keeping original.")
303
+ return winner
304
+
305
  logger.info(f"Compressed: {len(winner.split())}w β†’ {len(compressed.split())}w | '{compressed}'")
306
  return compressed
307
 
witgym/model.py CHANGED
@@ -136,8 +136,7 @@ def generate_text(
136
  )
137
  elif config_type == "rank":
138
  gen_kwargs = dict(
139
- temperature=config.RANK_TEMP,
140
- do_sample=True,
141
  max_new_tokens=10,
142
  )
143
  else:
 
136
  )
137
  elif config_type == "rank":
138
  gen_kwargs = dict(
139
+ do_sample=False,
 
140
  max_new_tokens=10,
141
  )
142
  else:
witgym/retriever.py CHANGED
@@ -43,8 +43,14 @@ def retrieve_scenes(
43
  Over-fetches (top_k * 3) then filters out already-used archetypes to
44
  prevent repetition across the conversation.
45
  """
46
- # Build query string from comedy metadata β€” NOT from raw user text
47
- query = f"{metadata.archetype.value} {metadata.tension_type.value} {metadata.violation_distance.value}"
 
 
 
 
 
 
48
  logger.debug(f"RAG query: '{query}'")
49
 
50
  query_emb = embed_model.encode(
 
43
  Over-fetches (top_k * 3) then filters out already-used archetypes to
44
  prevent repetition across the conversation.
45
  """
46
+ # Mirror the indexed representation: keep the structural labels, then add
47
+ # the extracted subtext so the query has semantic detail comparable to setup.
48
+ query = (
49
+ f"{metadata.archetype.value} "
50
+ f"{metadata.tension_type.value} "
51
+ f"{metadata.violation_distance.value} "
52
+ f"{metadata.subtext}"
53
+ )
54
  logger.debug(f"RAG query: '{query}'")
55
 
56
  query_emb = embed_model.encode(