Better validations
This commit is contained in:
@@ -53,9 +53,11 @@ Wrap each action in its own ```tool block:
|
||||
{"tool": "journal_update", "args": {"add": ["Investigate the mine"], "done": ["Defeat the demon"]}}
|
||||
```
|
||||
```tool
|
||||
{"tool": "finalize_turn", "args": {"ambience": "dungeon"}}
|
||||
{"tool": "finalize_turn", "args": {"ambience": "dungeon", "log_entry": "Dillion explored the dungeon, found a hidden passage, and was ambushed by goblins."}}
|
||||
```
|
||||
|
||||
**log_entry**: Provide a short, dense summary (1-2 sentences) of the turn's main events. This becomes the session log — be specific, factual, and concise.
|
||||
|
||||
You are the sole authority over the game state. The player's action is a **proposal**, not a fact. If their action contradicts the character sheet (e.g. using an item they don't have, spending cash they don't have, claiming stats they don't have), narrate the failure and do NOT call any state-changing tools.
|
||||
|
||||
**Inventory rule**: If the player wants to use an item, you must first verify it's on their character sheet. If it is, you MUST call `remove_from_inventory` for that item AND apply the effects (e.g. `modify_vitals` for HP potions). If it's not on the sheet, reject the action — do not let them use items they don't have.
|
||||
|
||||
@@ -20,7 +20,7 @@ TOOL_REGISTRY: dict[str, dict] = {
|
||||
"replace_note": {"description": "Replace note by exact match.", "args": {"before": "exact text", "after": "new text"}},
|
||||
"world_update": {"description": "Replace world state.", "args": {"content": "full world markdown"}},
|
||||
"journal_update": {"description": "Update TODO/DONE.", "args": {"add": "[...]", "done": "[...]"}},
|
||||
"finalize_turn": {"description": "End turn.", "args": {"ambience": "soundscape name"}},
|
||||
"finalize_turn": {"description": "End turn.", "args": {"ambience": "soundscape name", "log_entry": "one-line summary of what happened"}},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -101,4 +101,152 @@ def validate_action(
|
||||
return False, "Unrecognized"
|
||||
|
||||
|
||||
TURN_VALIDATION_PROMPT = """You are a strict RPG game master validating a generated turn. Check:
|
||||
|
||||
1. **Action Sense**: Did the player's request make sense given the character, inventory, and world state?
|
||||
2. **Story Coherence**: Is the story evolution coherent, non-contradictory, and within the game world's logic?
|
||||
3. **State Correctness**: Do the planned state changes match the narrative? Are they valid given current state?
|
||||
4. **Log Entry**: Does the log entry accurately summarise the narrative in 1-2 short, dense sentences? Should be specific, factual, and immediately readable.
|
||||
|
||||
## Character (before changes)
|
||||
{character}
|
||||
|
||||
## World
|
||||
{world}
|
||||
|
||||
## Recent Story
|
||||
{story}
|
||||
|
||||
## Session Log
|
||||
{log}
|
||||
|
||||
## Player Action
|
||||
{action}
|
||||
|
||||
## Generated Narrative
|
||||
{narrative}
|
||||
|
||||
## Proposed Log Entry
|
||||
{log_entry}
|
||||
|
||||
## Planned State Changes
|
||||
{changes}
|
||||
|
||||
## Instructions
|
||||
Check all criteria. **Completeness** is critical — scan the narrative for every event that should change state and verify it has a corresponding tool call:
|
||||
|
||||
- **Item used** → must have `remove_from_inventory`
|
||||
- **Item acquired** → must have `add_to_inventory` or `replace_gear`
|
||||
- **HP changed** → must have `modify_vitals`
|
||||
- **Cash changed** → must have `modify_vitals`
|
||||
- **World changed** → must have `world_update`
|
||||
- **NPC/location/thread changes** → must have `world_update` or `add_note`
|
||||
|
||||
Missing tool calls = regenerate. Also check that:
|
||||
- Items removed were actually in inventory
|
||||
- Items added are reasonable and don't duplicate existing items
|
||||
- HP/cash changes follow logically from the narrative
|
||||
- No impossible modifications
|
||||
|
||||
For log entry: must be a tight summary of the narrative's key events — specific entities, actions, outcomes. Vague, rambling, or mismatched log entries should be flagged for regenerate.
|
||||
|
||||
Reply with ONLY a JSON object using one of these formats:
|
||||
|
||||
Valid:
|
||||
```json
|
||||
{{"valid": true, "reason": "ok", "action": "ok"}}
|
||||
```
|
||||
|
||||
Reject (player action itself was impossible or nonsensical):
|
||||
```json
|
||||
{{"valid": false, "reason": "explain why the action is impossible", "action": "reject"}}
|
||||
```
|
||||
|
||||
Regenerate (turn had fixable issues like wrong state changes or minor inconsistencies):
|
||||
```json
|
||||
{{"valid": false, "reason": "describe what the LLM should fix", "action": "regenerate"}}
|
||||
```
|
||||
"""
|
||||
|
||||
|
||||
def _format_changes(changes: list[dict]) -> str:
|
||||
"""Format tool calls into a readable change list for the validation prompt."""
|
||||
if not changes:
|
||||
return "*No state changes planned.*"
|
||||
lines = []
|
||||
for tc in changes:
|
||||
tool = tc.get("tool", "?")
|
||||
args = {k: v for k, v in tc.get("args", {}).items() if v is not None}
|
||||
parts = ", ".join(f"{k}={v}" for k, v in args.items())
|
||||
lines.append(f"- {tool}: {parts}" if parts else f"- {tool}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def validate_turn(
|
||||
player_action: str,
|
||||
*,
|
||||
narrative: str = "",
|
||||
log_entry: str = "",
|
||||
changes: list[dict] | None = None,
|
||||
story: str = "",
|
||||
log: str = "",
|
||||
on_debug: callable = None,
|
||||
) -> tuple[bool, str, str]:
|
||||
"""Validate a complete generated turn.
|
||||
|
||||
Returns (valid, reason, action) where action is "ok", "reject", or "regenerate".
|
||||
"""
|
||||
if not player_action and not narrative:
|
||||
return True, "", "ok"
|
||||
|
||||
char = state.read_file(CHAR_PATH) or "*No character sheet.*"
|
||||
world = state.truncate_world(state.read_file(WORLD_PATH) or "") or "*No world state.*"
|
||||
recent = story.strip() or state.read_recent_book() or "*No prior story.*"
|
||||
log_entries = log.strip() or state.read_recent_log() or "*No recent events.*"
|
||||
change_summary = _format_changes(changes or [])
|
||||
|
||||
prompt = TURN_VALIDATION_PROMPT.format(
|
||||
character=char, world=world, story=recent,
|
||||
log=log_entries, action=player_action,
|
||||
narrative=narrative, log_entry=log_entry or "*No log entry provided.*",
|
||||
changes=change_summary,
|
||||
)
|
||||
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
|
||||
for attempt in range(2):
|
||||
text = call_llm(
|
||||
messages,
|
||||
max_tokens=1024,
|
||||
temperature=0.2,
|
||||
label="Turn validation",
|
||||
on_debug=on_debug,
|
||||
)
|
||||
|
||||
if not text:
|
||||
return False, "Not sure", "reject"
|
||||
|
||||
cleaned = text.strip()
|
||||
m = re.search(r"```(?:json)?\s*\n?(.*?)```", cleaned, re.DOTALL)
|
||||
if m:
|
||||
cleaned = m.group(1).strip()
|
||||
try:
|
||||
data = json.loads(cleaned)
|
||||
valid = data.get("valid", True)
|
||||
reason = data.get("reason", "")
|
||||
action = data.get("action", "ok")
|
||||
if action not in ("ok", "reject", "regenerate"):
|
||||
action = "ok" if valid else "reject"
|
||||
if on_debug:
|
||||
on_debug("turn_validation", {"valid": valid, "reason": reason, "action": action})
|
||||
return valid, reason, action
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
if on_debug:
|
||||
on_debug("turn_validation", {"valid": True, "reason": "parse_failed", "raw": text[:200]})
|
||||
if attempt == 0:
|
||||
messages.append({
|
||||
"role": "system",
|
||||
"content": "Your previous response was not valid JSON. Reply with ONLY a JSON object:\n\n```json\n{\"valid\": true, \"reason\": \"ok\", \"action\": \"ok\"}\n```\nor\n```json\n{\"valid\": false, \"reason\": \"...\", \"action\": \"reject\"}\n```\nor\n```json\n{\"valid\": false, \"reason\": \"...\", \"action\": \"regenerate\"}\n```"
|
||||
})
|
||||
|
||||
return False, "Unrecognized", "reject"
|
||||
|
||||
Reference in New Issue
Block a user