Guard against duplicate narrative entries
This commit is contained in:
parent
0458811e02
commit
f76470acb4
@ -5,6 +5,7 @@ import random
|
|||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from difflib import SequenceMatcher
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from engine_lib.models import TurnResult
|
from engine_lib.models import TurnResult
|
||||||
@ -117,8 +118,14 @@ class GameEngine:
|
|||||||
if text:
|
if text:
|
||||||
book_log = (book_log + "\n\n" + text) if book_log else text
|
book_log = (book_log + "\n\n" + text) if book_log else text
|
||||||
elif name == "finalize_turn":
|
elif name == "finalize_turn":
|
||||||
if args.get("ambience"):
|
raw = (args.get("ambience") or "").strip().lower()
|
||||||
ambience = args["ambience"]
|
if raw:
|
||||||
|
valid = state.get_valid_ambiences()
|
||||||
|
if raw in valid:
|
||||||
|
ambience = raw
|
||||||
|
else:
|
||||||
|
state.append_llm_log(f"\n[WARN] invalid ambience '{raw}'")
|
||||||
|
ambience = None
|
||||||
if args.get("log_entry"):
|
if args.get("log_entry"):
|
||||||
log_entry = args["log_entry"]
|
log_entry = args["log_entry"]
|
||||||
elif name == "player_roll":
|
elif name == "player_roll":
|
||||||
@ -126,6 +133,25 @@ class GameEngine:
|
|||||||
elif name not in ("roll",):
|
elif name not in ("roll",):
|
||||||
state_changes.append(tc)
|
state_changes.append(tc)
|
||||||
|
|
||||||
|
# Duplicate check — reject if narrative is 80%+ similar to last book entry
|
||||||
|
if book_log:
|
||||||
|
prev = state.read_recent_book(1)
|
||||||
|
if prev and prev not in ("*No prior story.*",):
|
||||||
|
prev_text = re.sub(r"^## Turn \d+\n\n", "", prev, flags=re.MULTILINE).strip()
|
||||||
|
ratio = SequenceMatcher(None, book_log, prev_text).ratio()
|
||||||
|
if ratio >= 0.8:
|
||||||
|
state.append_llm_log(f"\n[TURN DUPLICATE] {ratio:.0%} match with previous turn")
|
||||||
|
if attempt < MAX_RETRIES:
|
||||||
|
feedback = "The narrative is nearly identical to the previous turn. Generate something new and different."
|
||||||
|
state.append_llm_log(f"\n[TURN REGENERATE] (duplicate) attempt {attempt + 2}")
|
||||||
|
continue
|
||||||
|
state.append_llm_log(f"\n[TURN DUPLICATE EXCEEDED] cannot generate unique narrative")
|
||||||
|
return TurnResult(
|
||||||
|
book_log="",
|
||||||
|
log_entry="Your action was rejected — could not generate a unique narrative.",
|
||||||
|
user_prompt=f"*Your action:\n\t\"{player_action}\"\nwas rejected:\nengine failed to produce unique narrative*",
|
||||||
|
)
|
||||||
|
|
||||||
# Validate the generated turn
|
# Validate the generated turn
|
||||||
if on_action:
|
if on_action:
|
||||||
on_action("DM is validating the response")
|
on_action("DM is validating the response")
|
||||||
@ -181,8 +207,6 @@ class GameEngine:
|
|||||||
|
|
||||||
if name == "narrative":
|
if name == "narrative":
|
||||||
pass
|
pass
|
||||||
elif name == "finalize_turn":
|
|
||||||
pass
|
|
||||||
elif name == "player_roll" and on_player_roll:
|
elif name == "player_roll" and on_player_roll:
|
||||||
dice = args.get("dice", "1d6")
|
dice = args.get("dice", "1d6")
|
||||||
reason = args.get("reason", "a check")
|
reason = args.get("reason", "a check")
|
||||||
|
|||||||
@ -112,8 +112,6 @@ def validate_update_size(name: str, new_content: str, path: Path) -> bool:
|
|||||||
|
|
||||||
def apply_state(result: TurnResult) -> None:
|
def apply_state(result: TurnResult) -> None:
|
||||||
"""Write state changes from a TurnResult to disk."""
|
"""Write state changes from a TurnResult to disk."""
|
||||||
if result.ambience:
|
|
||||||
AMBIENCE_PATH.write_text(result.ambience.strip().lower() + "\n")
|
|
||||||
if result.changes:
|
if result.changes:
|
||||||
CHANGES_PATH.write_text("\n".join(result.changes) + "\n")
|
CHANGES_PATH.write_text("\n".join(result.changes) + "\n")
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -4,8 +4,8 @@ import json
|
|||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from .paths import CHAR_PATH, WORLD_PATH
|
from .paths import AMBIENCE_PATH, CHAR_PATH, WORLD_PATH
|
||||||
from .state import read_file, validate_update_size, update_journal, append_llm_log
|
from .state import read_file, validate_update_size, update_journal, append_llm_log, get_valid_ambiences
|
||||||
|
|
||||||
|
|
||||||
TOOL_REGISTRY: dict[str, dict] = {
|
TOOL_REGISTRY: dict[str, dict] = {
|
||||||
@ -168,6 +168,20 @@ def tool_journal_update(args: dict) -> str:
|
|||||||
return "Journal updated."
|
return "Journal updated."
|
||||||
|
|
||||||
|
|
||||||
|
def tool_finalize_turn(args: dict) -> str:
|
||||||
|
"""Validate ambience and write to AMBIENCE_PATH."""
|
||||||
|
raw = (args or {}).get("ambience", "").strip().lower()
|
||||||
|
if not raw:
|
||||||
|
AMBIENCE_PATH.write_text("silence\n")
|
||||||
|
return "Ambience set to silence."
|
||||||
|
valid = get_valid_ambiences()
|
||||||
|
if raw not in valid:
|
||||||
|
append_llm_log(f"\n[WARN] invalid ambience '{raw}', allowed: {sorted(valid)}")
|
||||||
|
return f"**Error:** invalid ambience '{raw}'. Allowed: {', '.join(sorted(valid))}."
|
||||||
|
AMBIENCE_PATH.write_text(raw + "\n")
|
||||||
|
return f"Ambience set to {raw}."
|
||||||
|
|
||||||
|
|
||||||
def execute_tool(tool_name: str, args: dict) -> str:
|
def execute_tool(tool_name: str, args: dict) -> str:
|
||||||
"""Execute a tool by name. Returns result string."""
|
"""Execute a tool by name. Returns result string."""
|
||||||
fn_map = {
|
fn_map = {
|
||||||
@ -181,6 +195,7 @@ def execute_tool(tool_name: str, args: dict) -> str:
|
|||||||
"replace_note": tool_replace_note,
|
"replace_note": tool_replace_note,
|
||||||
"world_update": tool_world_update,
|
"world_update": tool_world_update,
|
||||||
"journal_update": tool_journal_update,
|
"journal_update": tool_journal_update,
|
||||||
|
"finalize_turn": tool_finalize_turn,
|
||||||
}
|
}
|
||||||
fn = fn_map.get(tool_name)
|
fn = fn_map.get(tool_name)
|
||||||
if not fn:
|
if not fn:
|
||||||
@ -227,6 +242,9 @@ def describe_change(tool_name: str, args: dict) -> str:
|
|||||||
for d in args.get("done", []):
|
for d in args.get("done", []):
|
||||||
parts.append(f"✅ {d}")
|
parts.append(f"✅ {d}")
|
||||||
return "; ".join(parts) if parts else ""
|
return "; ".join(parts) if parts else ""
|
||||||
|
elif tool_name == "finalize_turn":
|
||||||
|
a = args.get("ambience", "")
|
||||||
|
return f"♫ {a}" if a else ""
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user