Spaces:
Running
Running
Akshay Babbar commited on
Commit Β·
640ccba
1
Parent(s): f1cf4a9
interim commit-v3
Browse files- witgym/conversation.py +17 -8
- witgym/engine.py +5 -5
- witgym/extractor.py +8 -0
- witgym/generator.py +23 -13
- witgym/model.py +1 -2
- witgym/retriever.py +8 -2
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,
|
| 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
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
lines = []
|
| 32 |
if self._summary:
|
| 33 |
lines.append(f"[Earlier conversation summary]: {self._summary}")
|
| 34 |
-
for
|
| 35 |
-
lines.append(f"
|
| 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
|
| 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
|
| 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"] #
|
| 76 |
-
logger.info(f"twist_potential={metadata.twist_potential} β€ 6 β skipping
|
| 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
|
| 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 |
-
"
|
| 24 |
-
"
|
| 25 |
-
"
|
| 26 |
-
"
|
| 27 |
-
"
|
| 28 |
-
"
|
| 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 β€
|
| 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 β€
|
| 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 β€
|
| 274 |
Guards: rejects output that is < 4 words or longer than the original.
|
| 275 |
"""
|
| 276 |
-
if len(winner.split()) <=
|
| 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 |
-
|
| 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 |
-
#
|
| 47 |
-
query
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|