10Pratibh commited on
Commit
f4c4a96
·
1 Parent(s): 7fd4ee4

Robust judgment parser: recover brace-less output, map invented tactics

Browse files
Files changed (1) hide show
  1. troll_engine.py +57 -17
troll_engine.py CHANGED
@@ -173,11 +173,16 @@ Every time the traveller speaks, you do TWO things:
173
  what they actually said. Never break character. Never mention the rubric, the
174
  meter, or that you are an AI."""
175
 
176
- JSON_INSTRUCTION = """Respond with ONLY a single JSON object, nothing else, in this \
177
- exact shape:
178
  {"tactic": "...", "persuasiveness": 0, "reason": "<=10 words on why", "reply": "Gorm's words"}
 
 
179
  The "reply" value must contain ONLY Gorm's spoken words — never the tactic name, the \
180
- word "persuasiveness", or any judgment number."""
 
 
 
181
 
182
  SYSTEM_PROMPT = SYSTEM_BODY + "\n\n" + JSON_INSTRUCTION # nature-agnostic (eval/training)
183
 
@@ -244,25 +249,60 @@ def parse_judgment(raw: str) -> Judgment:
244
  text = raw.strip()
245
  if text.startswith("```"):
246
  text = re.sub(r"^```(?:json)?|```$", "", text, flags=re.MULTILINE).strip()
 
 
247
  match = _JSON_RE.search(text)
248
  if match:
249
  try:
250
  obj = json.loads(match.group(0))
251
- tactic = _coerce_tactic(obj.get("tactic"))
252
- persuasiveness = _coerce_int(obj.get("persuasiveness"), 0, 0, 5)
253
- reason = str(obj.get("reason", "")).strip()[:120]
254
- reply = _clean_reply(str(obj.get("reply", "")).strip()) or _fallback_reply()
255
- return Judgment(persuasiveness, tactic, reason, reply)
256
  except (json.JSONDecodeError, ValueError, TypeError):
257
- pass
258
- return Judgment(0, Tactic.SMALLTALK, "unparseable judgment", text or _fallback_reply())
259
-
260
-
261
- def _coerce_tactic(value) -> Tactic:
262
- try:
263
- return Tactic(str(value).strip().lower())
264
- except ValueError:
265
- return Tactic.SMALLTALK
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
 
267
 
268
  def _coerce_int(value, default: int, lo: int, hi: int) -> int:
 
173
  what they actually said. Never break character. Never mention the rubric, the
174
  meter, or that you are an AI."""
175
 
176
+ JSON_INSTRUCTION = """Respond with ONLY a single JSON object wrapped in curly braces, \
177
+ nothing before or after it, in this exact shape:
178
  {"tactic": "...", "persuasiveness": 0, "reason": "<=10 words on why", "reply": "Gorm's words"}
179
+ "tactic" MUST be exactly one of: genuine, flattery, threat, manipulation, repetition, smalltalk. \
180
+ Never invent other labels. A clichéd or unproven sob-story is "genuine" with low persuasiveness.
181
  The "reply" value must contain ONLY Gorm's spoken words — never the tactic name, the \
182
+ word "persuasiveness", or any judgment number.
183
+ Example of a correct response:
184
+ {"tactic": "genuine", "persuasiveness": 2, "reason": "real but unproven need", "reply": "A sick \
185
+ mother, you say? The wood is thick with such tales. Bring me something truer than words."}"""
186
 
187
  SYSTEM_PROMPT = SYSTEM_BODY + "\n\n" + JSON_INSTRUCTION # nature-agnostic (eval/training)
188
 
 
249
  text = raw.strip()
250
  if text.startswith("```"):
251
  text = re.sub(r"^```(?:json)?|```$", "", text, flags=re.MULTILINE).strip()
252
+
253
+ obj = None
254
  match = _JSON_RE.search(text)
255
  if match:
256
  try:
257
  obj = json.loads(match.group(0))
 
 
 
 
 
258
  except (json.JSONDecodeError, ValueError, TypeError):
259
+ obj = None
260
+
261
+ # Fine-tuned / multi-turn output sometimes drops the braces or invents a
262
+ # label. Recover the fields from loose "key: value" text instead of giving up.
263
+ if not isinstance(obj, dict):
264
+ obj = _loose_extract(text)
265
+
266
+ if isinstance(obj, dict) and obj:
267
+ persuasiveness = _coerce_int(obj.get("persuasiveness"), 0, 0, 5)
268
+ tactic = _coerce_tactic(obj.get("tactic"), persuasiveness)
269
+ reason = str(obj.get("reason", "")).strip()[:120]
270
+ reply = _clean_reply(str(obj.get("reply", "")).strip())
271
+ if reply:
272
+ return Judgment(persuasiveness, tactic, reason or "—", reply)
273
+
274
+ # Last resort: no recoverable structure — treat the whole thing as Gorm
275
+ # talking, score it as idle chatter so the meter never shows "unparseable".
276
+ return Judgment(0, Tactic.SMALLTALK, "no structured judgment",
277
+ _clean_reply(text) or _fallback_reply())
278
+
279
+
280
+ _LOOSE_TACTIC = re.compile(r'["\']?tactic["\']?\s*[:=]\s*["\']?([^"\',}\n]+)', re.I)
281
+ _LOOSE_PERSUAS = re.compile(r'["\']?persuasiveness["\']?\s*[:=]\s*["\']?(\d)', re.I)
282
+ _LOOSE_REASON = re.compile(r'["\']?reason["\']?\s*[:=]\s*["\'](.*?)["\']\s*[,}]', re.I | re.S)
283
+ _LOOSE_REPLY = re.compile(r'["\']?reply["\']?\s*[:=]\s*["\'](.*)["\']\s*\}?\s*$', re.I | re.S)
284
+
285
+
286
+ def _loose_extract(text: str):
287
+ found = {}
288
+ for key, pat in (("tactic", _LOOSE_TACTIC), ("persuasiveness", _LOOSE_PERSUAS),
289
+ ("reason", _LOOSE_REASON), ("reply", _LOOSE_REPLY)):
290
+ m = pat.search(text)
291
+ if m:
292
+ found[key] = m.group(1).strip()
293
+ return found or None
294
+
295
+
296
+ def _coerce_tactic(value, persuasiveness: int = 0) -> Tactic:
297
+ v = str(value).strip().lower()
298
+ for t in Tactic:
299
+ if t.value in v:
300
+ return t
301
+ # Unknown label (e.g. "generic cliché"): a weak earnest appeal is still a
302
+ # genuine reason; anything else is just chatter.
303
+ if "clich" in v or "genuine" in v or "honest" in v or "plea" in v or persuasiveness >= 1:
304
+ return Tactic.GENUINE
305
+ return Tactic.SMALLTALK
306
 
307
 
308
  def _coerce_int(value, default: int, lo: int, hi: int) -> int: