Journal for generation LLM
This commit is contained in:
parent
b6f56f22fd
commit
52deb1db6a
@ -168,7 +168,8 @@ class GameEngine:
|
||||
user_prompt=f"*Your action:\n\t\"{player_action}\"\nwas rejected:\n\t{reason}*",
|
||||
)
|
||||
elif action == "regenerate" and attempt < MAX_RETRIES:
|
||||
feedback = f"The generated turn has issues: {reason}\n\nPlease regenerate the turn addressing this feedback. Keep the same player action but fix the problems described above."
|
||||
validator_tool = json.dumps({"tool": "validate", "args": {"valid": False, "reason": reason, "action": "regenerate"}})
|
||||
feedback = f"The validation tool returned:\n```tool\n{validator_tool}\n```\n\nPlease regenerate the turn addressing the issues above. Keep the same player action but fix the problems described."
|
||||
state.append_llm_log(f"\n[TURN REGENERATE] attempt {attempt + 2}: {reason}")
|
||||
continue
|
||||
else:
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .paths import CHAR_PATH, WORLD_PATH
|
||||
from .paths import CHAR_PATH, WORLD_PATH, JOURNAL_PATH
|
||||
from .prompts import SYSTEM_PROMPT
|
||||
from . import state
|
||||
|
||||
@ -10,7 +10,8 @@ def build_system_prompt(recent_narrative: str | None = None, recent_log: str | N
|
||||
char = state.read_file(CHAR_PATH) or "*No character sheet.*"
|
||||
world = state.truncate_world(state.read_file(WORLD_PATH) or "") or "*No world state.*"
|
||||
log = recent_log if recent_log is not None else state.read_recent_log()
|
||||
journal = state.read_file(JOURNAL_PATH) or "*No journal entries.*"
|
||||
story = recent_narrative if recent_narrative is not None else state.read_recent_book(2)
|
||||
return SYSTEM_PROMPT.substitute(
|
||||
character=char, world=world, log=log, story=story,
|
||||
character=char, world=world, log=log, journal=journal, story=story,
|
||||
)
|
||||
|
||||
@ -73,5 +73,10 @@ $world
|
||||
### Log
|
||||
$log
|
||||
|
||||
### Journal (TODO / DONE)
|
||||
$journal
|
||||
|
||||
**journal_update rule**: When calling `journal_update`, you MUST use the EXACT wording of the TODO items from the Journal above. Do not rephrase, paraphrase, or invent alternate descriptions — match the TODO text character-for-character. Mark items as `done` exactly as they appear in TODO. Add new items with exact wording matching their entry in the list.
|
||||
|
||||
### Story
|
||||
$story""")
|
||||
|
||||
@ -4,7 +4,7 @@ import json
|
||||
import re
|
||||
|
||||
from .llm import call_llm
|
||||
from .paths import CHAR_PATH, WORLD_PATH
|
||||
from .paths import CHAR_PATH, WORLD_PATH, JOURNAL_PATH
|
||||
from . import state
|
||||
|
||||
|
||||
@ -108,6 +108,7 @@ TURN_VALIDATION_PROMPT = """You are a strict RPG game master validating a genera
|
||||
3. **State Correctness**: Do the planned state changes match the narrative? Are they valid given current state?
|
||||
4. **Self-Contained Narrative**: The narrative must read clearly on its own — explicitly describe what the character did in response to the action. Do not skip the character's action and jump straight to consequences. Each turn must make sense without referencing the player action line.
|
||||
5. **Log Entry**: Does the log entry accurately summarise the narrative in 1-2 short, dense sentences? Should be specific, factual, and immediately readable.
|
||||
6. **Journal Progress**: Are TODO items being addressed? If the narrative resolves an open TODO, the turn must call `journal_update` to mark it done. Unchecked items left stale too long may need prompting.
|
||||
|
||||
## Character (before changes)
|
||||
{character}
|
||||
@ -121,6 +122,9 @@ TURN_VALIDATION_PROMPT = """You are a strict RPG game master validating a genera
|
||||
## Session Log
|
||||
{log}
|
||||
|
||||
## Journal
|
||||
{journal}
|
||||
|
||||
## Player Action
|
||||
{action}
|
||||
|
||||
@ -142,6 +146,7 @@ Check all criteria. **Completeness** is critical — scan the narrative for ever
|
||||
- **Cash changed** → must have `modify_vitals`
|
||||
- **World changed** → must have `world_update`
|
||||
- **NPC/location/thread changes** → must have `world_update` or `add_note`
|
||||
- **TODO resolved** → must have `journal_update` with `done`
|
||||
|
||||
Missing tool calls = regenerate. Also check that:
|
||||
- The narrative explicitly describes the character acting — not just the world reacting
|
||||
@ -153,21 +158,21 @@ Missing tool calls = regenerate. Also check that:
|
||||
|
||||
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:
|
||||
Reply with ONLY a ```tool block. Examples:
|
||||
|
||||
Valid:
|
||||
```json
|
||||
{{"valid": true, "reason": "ok", "action": "ok"}}
|
||||
```tool
|
||||
{{"tool": "validate", "args": {{"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"}}
|
||||
```tool
|
||||
{{"tool": "validate", "args": {{"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"}}
|
||||
```tool
|
||||
{{"tool": "validate", "args": {{"valid": false, "reason": "describe what the LLM should fix", "action": "regenerate"}}}}
|
||||
```
|
||||
"""
|
||||
|
||||
@ -206,11 +211,12 @@ def validate_turn(
|
||||
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.*"
|
||||
journal = state.read_file(JOURNAL_PATH) or "*No journal entries.*"
|
||||
change_summary = _format_changes(changes or [])
|
||||
|
||||
prompt = TURN_VALIDATION_PROMPT.format(
|
||||
character=char, world=world, story=recent,
|
||||
log=log_entries, action=player_action,
|
||||
log=log_entries, journal=journal, action=player_action,
|
||||
narrative=narrative, log_entry=log_entry or "*No log entry provided.*",
|
||||
changes=change_summary,
|
||||
)
|
||||
@ -229,15 +235,19 @@ def validate_turn(
|
||||
if not text:
|
||||
return False, "Not sure", "reject"
|
||||
|
||||
cleaned = text.strip()
|
||||
m = re.search(r"```(?:json)?\s*\n?(.*?)```", cleaned, re.DOTALL)
|
||||
m = re.search(r"```tool\s*\n?(.*?)```", text, re.DOTALL)
|
||||
if m:
|
||||
cleaned = m.group(1).strip()
|
||||
else:
|
||||
cleaned = text.strip()
|
||||
try:
|
||||
data = json.loads(cleaned)
|
||||
valid = data.get("valid", True)
|
||||
reason = data.get("reason", "")
|
||||
action = data.get("action", "ok")
|
||||
if data.get("tool") != "validate":
|
||||
return False, "Unrecognized", "reject"
|
||||
args = data.get("args", {})
|
||||
valid = args.get("valid", True)
|
||||
reason = args.get("reason", "")
|
||||
action = args.get("action", "ok")
|
||||
if action not in ("ok", "reject", "regenerate"):
|
||||
action = "ok" if valid else "reject"
|
||||
if on_debug:
|
||||
@ -249,7 +259,7 @@ def validate_turn(
|
||||
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```"
|
||||
"content": "Your previous response was not valid. Reply with ONLY a ```tool block:\n\n```tool\n{\"tool\": \"validate\", \"args\": {\"valid\": true, \"reason\": \"ok\", \"action\": \"ok\"}}\n```\nor\n```tool\n{\"tool\": \"validate\", \"args\": {\"valid\": false, \"reason\": \"...\", \"action\": \"reject\"}}\n```\nor\n```tool\n{\"tool\": \"validate\", \"args\": {\"valid\": false, \"reason\": \"...\", \"action\": \"regenerate\"}}\n```"
|
||||
})
|
||||
|
||||
return False, "Unrecognized", "reject"
|
||||
|
||||
13
tools/run.py
13
tools/run.py
@ -209,10 +209,9 @@ class ChaosTUI(App):
|
||||
parts = []
|
||||
parts.append(pages[-1])
|
||||
if CHANGES_PATH.exists():
|
||||
changes = [l.strip() for l in CHANGES_PATH.read_text().splitlines() if l.strip()]
|
||||
if changes:
|
||||
changes_text = "\n".join(f"> {c}" for c in changes)
|
||||
parts.append(f"> **Last turn changes:**\n{changes_text}")
|
||||
saved = [l.strip() for l in CHANGES_PATH.read_text().splitlines() if l.strip()]
|
||||
if saved:
|
||||
parts.append(self._render_changes(saved))
|
||||
self._set_narrative("\n\n".join(parts))
|
||||
self._enable_input()
|
||||
return
|
||||
@ -460,12 +459,16 @@ class ChaosTUI(App):
|
||||
self._append_debug(f" {line}")
|
||||
self._show_error(err_msg, traceback_str)
|
||||
|
||||
@staticmethod
|
||||
def _render_changes(changes: list[str]) -> str:
|
||||
return "**Changes:**\n" + "\n".join(f"- {c}" for c in changes)
|
||||
|
||||
def _display_scene(self, result: TurnResult) -> None:
|
||||
parts = []
|
||||
if result.book_log:
|
||||
parts.append(result.book_log)
|
||||
if result.changes:
|
||||
parts.append(f"> **Changes:**\n" + "\n".join(f"> {c}" for c in result.changes))
|
||||
parts.append(self._render_changes(result.changes))
|
||||
if result.user_prompt:
|
||||
parts.append(f"---\n\n{result.user_prompt}")
|
||||
self._set_narrative("\n\n".join(parts) if parts else "")
|
||||
|
||||
@ -140,15 +140,47 @@ def test_turn_empty_inputs():
|
||||
print("✓ empty inputs returns (True, '', 'ok')")
|
||||
|
||||
|
||||
def _mock_read(p: str) -> str:
|
||||
"""Helper for mock_read_file side_effect handling char/world/journal."""
|
||||
low = str(p).lower()
|
||||
if "character" in low:
|
||||
return "HP: 10\nGold: 5\nInventory:\n- Healing Salve"
|
||||
if "journal" in low:
|
||||
return "# Journal\n\n## TODO\n- Find the relic\n\n## DONE\n"
|
||||
return "## Location\nTavern"
|
||||
|
||||
|
||||
def _mock_read_no_gold(p: str) -> str:
|
||||
low = str(p).lower()
|
||||
if "character" in low:
|
||||
return "HP: 10\nGold: 0"
|
||||
if "journal" in low:
|
||||
return "# Journal\n\n## TODO\n\n## DONE\n"
|
||||
return "## Location\nTavern"
|
||||
|
||||
|
||||
def _mock_read_basic(p: str) -> str:
|
||||
low = str(p).lower()
|
||||
if "character" in low:
|
||||
return "HP: 10"
|
||||
if "journal" in low:
|
||||
return "# Journal\n\n## TODO\n\n## DONE\n"
|
||||
return "## Location\nTavern"
|
||||
|
||||
|
||||
def _tool_response(valid: bool, reason: str, action: str) -> str:
|
||||
return f'```tool\n{{"tool": "validate", "args": {{"valid": {str(valid).lower()}, "reason": "{reason}", "action": "{action}"}}}}\n```'
|
||||
|
||||
|
||||
@patch("engine_lib.validation.state.read_file")
|
||||
@patch("engine_lib.validation.state.truncate_world")
|
||||
@patch("engine_lib.validation.call_llm")
|
||||
def test_turn_valid(mock_call_llm, mock_truncate_world, mock_read_file):
|
||||
from engine_lib.validation import validate_turn
|
||||
|
||||
mock_read_file.side_effect = lambda p: "HP: 10\nGold: 5\nInventory:\n- Healing Salve" if "character" in str(p).lower() else "## Location\nTavern"
|
||||
mock_read_file.side_effect = _mock_read
|
||||
mock_truncate_world.return_value = "## Location\nTavern"
|
||||
mock_call_llm.return_value = '{"valid": true, "reason": "ok", "action": "ok"}'
|
||||
mock_call_llm.return_value = _tool_response(True, "ok", "ok")
|
||||
|
||||
valid, reason, action = validate_turn(
|
||||
"I use my healing salve",
|
||||
@ -172,9 +204,9 @@ def test_turn_valid(mock_call_llm, mock_truncate_world, mock_read_file):
|
||||
def test_turn_reject(mock_call_llm, mock_truncate_world, mock_read_file):
|
||||
from engine_lib.validation import validate_turn
|
||||
|
||||
mock_read_file.side_effect = lambda p: "HP: 10\nGold: 0" if "character" in str(p).lower() else "## Location\nTavern"
|
||||
mock_read_file.side_effect = _mock_read_no_gold
|
||||
mock_truncate_world.return_value = "## Location\nTavern"
|
||||
mock_call_llm.return_value = '{"valid": false, "reason": "Player has no gold", "action": "reject"}'
|
||||
mock_call_llm.return_value = _tool_response(False, "Player has no gold", "reject")
|
||||
|
||||
valid, reason, action = validate_turn(
|
||||
"I buy a round for the house",
|
||||
@ -197,9 +229,9 @@ def test_turn_reject(mock_call_llm, mock_truncate_world, mock_read_file):
|
||||
def test_turn_regenerate(mock_call_llm, mock_truncate_world, mock_read_file):
|
||||
from engine_lib.validation import validate_turn
|
||||
|
||||
mock_read_file.side_effect = lambda p: "HP: 10\nInventory:\n- Healing Salve" if "character" in str(p).lower() else "## Location\nTavern"
|
||||
mock_read_file.side_effect = _mock_read
|
||||
mock_truncate_world.return_value = "## Location\nTavern"
|
||||
mock_call_llm.return_value = '{"valid": false, "reason": "Narrative says salve used but no remove_from_inventory", "action": "regenerate"}'
|
||||
mock_call_llm.return_value = _tool_response(False, "Narrative says salve used but no remove_from_inventory", "regenerate")
|
||||
|
||||
valid, reason, action = validate_turn(
|
||||
"I use my healing salve",
|
||||
@ -222,7 +254,7 @@ def test_turn_regenerate(mock_call_llm, mock_truncate_world, mock_read_file):
|
||||
def test_turn_bad_json(mock_call_llm, mock_truncate_world, mock_read_file):
|
||||
from engine_lib.validation import validate_turn
|
||||
|
||||
mock_read_file.side_effect = lambda p: "HP: 10" if "character" in str(p).lower() else "## Location\nTavern"
|
||||
mock_read_file.side_effect = _mock_read_basic
|
||||
mock_truncate_world.return_value = "## Location\nTavern"
|
||||
mock_call_llm.return_value = "not valid json"
|
||||
|
||||
@ -247,9 +279,9 @@ def test_turn_bad_json(mock_call_llm, mock_truncate_world, mock_read_file):
|
||||
def test_turn_on_debug(mock_call_llm, mock_truncate_world, mock_read_file):
|
||||
from engine_lib.validation import validate_turn
|
||||
|
||||
mock_read_file.side_effect = lambda p: "HP: 10" if "character" in str(p).lower() else "## Location\nTavern"
|
||||
mock_read_file.side_effect = _mock_read_basic
|
||||
mock_truncate_world.return_value = "## Location\nTavern"
|
||||
mock_call_llm.return_value = '{"valid": true, "reason": "ok", "action": "ok"}'
|
||||
mock_call_llm.return_value = _tool_response(True, "ok", "ok")
|
||||
|
||||
events = []
|
||||
def debug_cb(key, data):
|
||||
|
||||
Loading…
Reference in New Issue
Block a user