Fix missing story in turn llm
This commit is contained in:
parent
c5c40225a3
commit
0140e2e8d9
@ -24,7 +24,7 @@ class GameEngine:
|
|||||||
def generate_turn(
|
def generate_turn(
|
||||||
self,
|
self,
|
||||||
player_action: str | None = None,
|
player_action: str | None = None,
|
||||||
last_prompt: str | None = None,
|
recent_narrative: str | None = None,
|
||||||
on_thought: callable = None,
|
on_thought: callable = None,
|
||||||
on_action: callable = None,
|
on_action: callable = None,
|
||||||
on_player_roll: callable = None,
|
on_player_roll: callable = None,
|
||||||
@ -36,8 +36,8 @@ class GameEngine:
|
|||||||
state.append_llm_log(f"{'='*60}")
|
state.append_llm_log(f"{'='*60}")
|
||||||
if player_action:
|
if player_action:
|
||||||
state.append_llm_log(f"Player: {player_action}")
|
state.append_llm_log(f"Player: {player_action}")
|
||||||
elif last_prompt:
|
if recent_narrative is None:
|
||||||
state.append_llm_log(f"Resume from: {last_prompt[:120]}")
|
recent_narrative = state.read_recent_book(2)
|
||||||
|
|
||||||
die_roll = random.randint(1, 6)
|
die_roll = random.randint(1, 6)
|
||||||
state.append_llm_log(f"Dice: {die_roll} (1d6)")
|
state.append_llm_log(f"Dice: {die_roll} (1d6)")
|
||||||
@ -66,11 +66,11 @@ class GameEngine:
|
|||||||
|
|
||||||
system = build_system_prompt()
|
system = build_system_prompt()
|
||||||
parts = []
|
parts = []
|
||||||
if last_prompt:
|
if recent_narrative:
|
||||||
parts.append(f"## Situation\n{last_prompt}")
|
parts.append(f"## Recent Narrative\n{recent_narrative}")
|
||||||
if player_action:
|
if player_action:
|
||||||
parts.append(f"## Player's Request\n{player_action}")
|
parts.append(f"## Player's Request\n{player_action}")
|
||||||
if not player_action and not last_prompt:
|
if not player_action and not recent_narrative:
|
||||||
parts.append(
|
parts.append(
|
||||||
"## Instructions\n"
|
"## Instructions\n"
|
||||||
"This is a new story. Welcome the player and guide them through the game setup."
|
"This is a new story. Welcome the player and guide them through the game setup."
|
||||||
@ -166,8 +166,8 @@ class GameEngine:
|
|||||||
})
|
})
|
||||||
|
|
||||||
log_turn_details(
|
log_turn_details(
|
||||||
player_action=player_action or last_prompt or "",
|
player_action=player_action or "",
|
||||||
last_prompt=last_prompt or "",
|
last_prompt=recent_narrative or "",
|
||||||
strategy_name="tools",
|
strategy_name="tools",
|
||||||
die_roll=die_roll,
|
die_roll=die_roll,
|
||||||
model=model,
|
model=model,
|
||||||
@ -196,13 +196,11 @@ def main():
|
|||||||
|
|
||||||
parser = argparse.ArgumentParser(description="The Chaos Game Engine (CLI)")
|
parser = argparse.ArgumentParser(description="The Chaos Game Engine (CLI)")
|
||||||
parser.add_argument("--action", "-a", help="Player action text")
|
parser.add_argument("--action", "-a", help="Player action text")
|
||||||
parser.add_argument("--last", "-l", help="Last narrative text")
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
engine = GameEngine()
|
engine = GameEngine()
|
||||||
result = engine.generate_turn(
|
result = engine.generate_turn(
|
||||||
player_action=args.action,
|
player_action=args.action,
|
||||||
last_prompt=args.last,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if result.error:
|
if result.error:
|
||||||
|
|||||||
@ -64,32 +64,41 @@ def validate_action(
|
|||||||
|
|
||||||
prompt = VALIDATION_PROMPT.format(character=char, world=world, log=log_entries, story=recent, action=player_action)
|
prompt = VALIDATION_PROMPT.format(character=char, world=world, log=log_entries, story=recent, action=player_action)
|
||||||
|
|
||||||
text = call_llm(
|
messages = [{"role": "user", "content": prompt}]
|
||||||
[{"role": "user", "content": prompt}],
|
|
||||||
max_tokens=1024,
|
|
||||||
temperature=0.2,
|
|
||||||
label="Action validation",
|
|
||||||
on_debug=on_debug,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not text:
|
for attempt in range(2):
|
||||||
return False, "Not sure"
|
text = call_llm(
|
||||||
|
messages,
|
||||||
|
max_tokens=1024,
|
||||||
|
temperature=0.2,
|
||||||
|
label="Action validation",
|
||||||
|
on_debug=on_debug,
|
||||||
|
)
|
||||||
|
|
||||||
cleaned = text.strip()
|
if not text:
|
||||||
m = re.search(r"```(?:json)?\s*\n?(.*?)```", cleaned, re.DOTALL)
|
return False, "Not sure"
|
||||||
if m:
|
|
||||||
cleaned = m.group(1).strip()
|
cleaned = text.strip()
|
||||||
try:
|
m = re.search(r"```(?:json)?\s*\n?(.*?)```", cleaned, re.DOTALL)
|
||||||
data = json.loads(cleaned)
|
if m:
|
||||||
valid = data.get("valid", True)
|
cleaned = m.group(1).strip()
|
||||||
reason = data.get("reason", "")
|
try:
|
||||||
if on_debug:
|
data = json.loads(cleaned)
|
||||||
on_debug("action_validation", {"valid": valid, "reason": reason, "action": player_action})
|
valid = data.get("valid", True)
|
||||||
return valid, reason
|
reason = data.get("reason", "")
|
||||||
except (json.JSONDecodeError, ValueError):
|
if on_debug:
|
||||||
if on_debug:
|
on_debug("action_validation", {"valid": valid, "reason": reason, "action": player_action})
|
||||||
on_debug("action_validation", {"valid": True, "reason": "parse_failed", "raw": text[:200]})
|
return valid, reason
|
||||||
return False, "Unrecognized"
|
except (json.JSONDecodeError, ValueError):
|
||||||
|
if on_debug:
|
||||||
|
on_debug("action_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 in exactly this format, nothing else:\n\n```json\n{\"valid\": true, \"reason\": \"ok\"}\n```\nor\n```json\n{\"valid\": false, \"reason\": \"brief explanation\"}\n```"
|
||||||
|
})
|
||||||
|
|
||||||
|
return False, "Unrecognized"
|
||||||
|
|
||||||
|
|
||||||
def auto_prompt(book_log: str = "") -> str:
|
def auto_prompt(book_log: str = "") -> str:
|
||||||
|
|||||||
38
tools/run.py
38
tools/run.py
@ -21,7 +21,7 @@ from engine import GameEngine
|
|||||||
from engine_lib.models import TurnResult
|
from engine_lib.models import TurnResult
|
||||||
from engine_lib import state
|
from engine_lib import state
|
||||||
from run_utils import (
|
from run_utils import (
|
||||||
BOOK_PATH, CHAR_PATH, LAST_PROMPT_PATH, CHANGES_PATH, SETTINGS_PATH,
|
BOOK_PATH, CHAR_PATH, CHANGES_PATH, SETTINGS_PATH,
|
||||||
TODAY, REFRESH_SECS, clear_llm_log, ensure_log,
|
TODAY, REFRESH_SECS, clear_llm_log, ensure_log,
|
||||||
load_book_pages,
|
load_book_pages,
|
||||||
)
|
)
|
||||||
@ -105,7 +105,6 @@ class ChaosTUI(App):
|
|||||||
run_widgets.app_ambience_player = app_ambience_player
|
run_widgets.app_ambience_player = app_ambience_player
|
||||||
|
|
||||||
self.engine = GameEngine()
|
self.engine = GameEngine()
|
||||||
self._last_prompt: str = ""
|
|
||||||
self._last_result: TurnResult | None = None
|
self._last_result: TurnResult | None = None
|
||||||
self._is_processing: bool = False
|
self._is_processing: bool = False
|
||||||
self._spinner_frames = ["◴", "◷", "◶", "◵"]
|
self._spinner_frames = ["◴", "◷", "◶", "◵"]
|
||||||
@ -205,23 +204,18 @@ class ChaosTUI(App):
|
|||||||
|
|
||||||
def _begin_game(self):
|
def _begin_game(self):
|
||||||
self._last_narrative: str = ""
|
self._last_narrative: str = ""
|
||||||
if LAST_PROMPT_PATH.exists():
|
pages = load_book_pages()
|
||||||
saved = LAST_PROMPT_PATH.read_text().strip()
|
if pages and pages != ["*The story has not begun.*"]:
|
||||||
if saved:
|
parts = []
|
||||||
self._last_prompt = saved
|
parts.append(pages[-1])
|
||||||
pages = load_book_pages()
|
if CHANGES_PATH.exists():
|
||||||
parts = []
|
changes = [l.strip() for l in CHANGES_PATH.read_text().splitlines() if l.strip()]
|
||||||
if pages:
|
if changes:
|
||||||
parts.append(pages[-1])
|
changes_text = "\n".join(f"> {c}" for c in changes)
|
||||||
if CHANGES_PATH.exists():
|
parts.append(f"> **Last turn changes:**\n{changes_text}")
|
||||||
changes = [l.strip() for l in CHANGES_PATH.read_text().splitlines() if l.strip()]
|
self._set_narrative("\n\n".join(parts))
|
||||||
if changes:
|
self._enable_input()
|
||||||
changes_text = "\n".join(f"> {c}" for c in changes)
|
return
|
||||||
parts.append(f"> **Last turn changes:**\n{changes_text}")
|
|
||||||
parts.append(f"---\n\n{saved}")
|
|
||||||
self._set_narrative("\n\n".join(parts))
|
|
||||||
self._enable_input()
|
|
||||||
return
|
|
||||||
self._call_llm()
|
self._call_llm()
|
||||||
|
|
||||||
def _check_ambience(self):
|
def _check_ambience(self):
|
||||||
@ -261,8 +255,6 @@ class ChaosTUI(App):
|
|||||||
|
|
||||||
def _run_generation(self, player_action: str | None) -> None:
|
def _run_generation(self, player_action: str | None) -> None:
|
||||||
import traceback
|
import traceback
|
||||||
last_prompt = self._last_prompt or None
|
|
||||||
|
|
||||||
def on_thought(thought: str) -> None:
|
def on_thought(thought: str) -> None:
|
||||||
self.call_from_thread(self._on_thought, thought)
|
self.call_from_thread(self._on_thought, thought)
|
||||||
|
|
||||||
@ -275,7 +267,6 @@ class ChaosTUI(App):
|
|||||||
try:
|
try:
|
||||||
result = self.engine.generate_turn(
|
result = self.engine.generate_turn(
|
||||||
player_action=player_action,
|
player_action=player_action,
|
||||||
last_prompt=last_prompt,
|
|
||||||
on_thought=on_thought,
|
on_thought=on_thought,
|
||||||
on_action=on_action,
|
on_action=on_action,
|
||||||
on_player_roll=self._on_player_roll,
|
on_player_roll=self._on_player_roll,
|
||||||
@ -458,9 +449,6 @@ class ChaosTUI(App):
|
|||||||
self._display_scene(result)
|
self._display_scene(result)
|
||||||
if result.book_log:
|
if result.book_log:
|
||||||
self._last_result = result
|
self._last_result = result
|
||||||
if result.user_prompt:
|
|
||||||
LAST_PROMPT_PATH.write_text(result.user_prompt.strip())
|
|
||||||
self._last_prompt = result.user_prompt
|
|
||||||
self._append_debug("✔ turn complete")
|
self._append_debug("✔ turn complete")
|
||||||
|
|
||||||
def _on_generation_error(self, error: Exception, traceback_str: str) -> None:
|
def _on_generation_error(self, error: Exception, traceback_str: str) -> None:
|
||||||
|
|||||||
@ -8,7 +8,6 @@ JOURNAL_PATH = SESSION / 'journal.md'
|
|||||||
AMBIENCE_PATH = SESSION / 'ambience.md'
|
AMBIENCE_PATH = SESSION / 'ambience.md'
|
||||||
AMBIENCE_OPTIONS_PATH = SESSION / 'ambience_options.md'
|
AMBIENCE_OPTIONS_PATH = SESSION / 'ambience_options.md'
|
||||||
BOOK_PATH = SESSION / 'book.md'
|
BOOK_PATH = SESSION / 'book.md'
|
||||||
LAST_PROMPT_PATH = SESSION / 'last_prompt.md'
|
|
||||||
CHANGES_PATH = SESSION / 'changes.md'
|
CHANGES_PATH = SESSION / 'changes.md'
|
||||||
SETTINGS_PATH = SESSION / 'settings.json'
|
SETTINGS_PATH = SESSION / 'settings.json'
|
||||||
AUDIO_DIR = SESSION / 'audio'
|
AUDIO_DIR = SESSION / 'audio'
|
||||||
|
|||||||
@ -70,40 +70,35 @@ def section(name: str):
|
|||||||
def main():
|
def main():
|
||||||
section("First turn — no player action (story opening)")
|
section("First turn — no player action (story opening)")
|
||||||
r = engine.generate_turn()
|
r = engine.generate_turn()
|
||||||
check("Story opening", r, expect_error=False, expect_book=True, expect_prompt=True)
|
check("Story opening", r, expect_error=False, expect_book=True, expect_prompt=False)
|
||||||
|
|
||||||
section("Valid action — buy a drink")
|
section("Valid action — buy a drink")
|
||||||
r = engine.generate_turn(
|
r = engine.generate_turn(
|
||||||
player_action="I buy a mug of ale at the Splintered Tankard",
|
player_action="I buy a mug of ale at the Splintered Tankard",
|
||||||
last_prompt="What do you do?",
|
|
||||||
)
|
)
|
||||||
check("Buy ale", r, expect_error=False, expect_book=True, expect_prompt=True)
|
check("Buy ale", r, expect_error=False, expect_book=True, expect_prompt=False)
|
||||||
|
|
||||||
section("Valid action — talk to an NPC")
|
section("Valid action — talk to an NPC")
|
||||||
r = engine.generate_turn(
|
r = engine.generate_turn(
|
||||||
player_action="I ask Mistress Otta about recent rumours",
|
player_action="I ask Mistress Otta about recent rumours",
|
||||||
last_prompt="What do you do?",
|
|
||||||
)
|
)
|
||||||
check("Ask Otta", r, expect_error=False, expect_book=True, expect_prompt=True)
|
check("Ask Otta", r, expect_error=False, expect_book=True, expect_prompt=False)
|
||||||
|
|
||||||
section("Valid action — use inventory item")
|
section("Valid action — use inventory item")
|
||||||
r = engine.generate_turn(
|
r = engine.generate_turn(
|
||||||
player_action="I apply my healing salve to restore HP",
|
player_action="I apply my healing salve to restore HP",
|
||||||
last_prompt="What do you do?",
|
|
||||||
)
|
)
|
||||||
check("Use healing salve", r, expect_error=False, expect_book=True, expect_prompt=True)
|
check("Use healing salve", r, expect_error=False, expect_book=True, expect_prompt=False)
|
||||||
|
|
||||||
section("Valid action — explore")
|
section("Valid action — explore")
|
||||||
r = engine.generate_turn(
|
r = engine.generate_turn(
|
||||||
player_action="I head to the Market Square to look around",
|
player_action="I head to the Market Square to look around",
|
||||||
last_prompt="What do you do?",
|
|
||||||
)
|
)
|
||||||
check("Visit market", r, expect_error=False, expect_book=True, expect_prompt=True)
|
check("Visit market", r, expect_error=False, expect_book=True, expect_prompt=False)
|
||||||
|
|
||||||
section("Invalid action — use non-existent item")
|
section("Invalid action — use non-existent item")
|
||||||
r = engine.generate_turn(
|
r = engine.generate_turn(
|
||||||
player_action="I drink a potion of invisibility",
|
player_action="I drink a potion of invisibility",
|
||||||
last_prompt="What do you do?",
|
|
||||||
)
|
)
|
||||||
check("Potion of invisibility", r, expect_error=False, expect_book=False)
|
check("Potion of invisibility", r, expect_error=False, expect_book=False)
|
||||||
if r.log_entry:
|
if r.log_entry:
|
||||||
@ -112,7 +107,6 @@ def main():
|
|||||||
section("Invalid action — cast spell (not a weaver)")
|
section("Invalid action — cast spell (not a weaver)")
|
||||||
r = engine.generate_turn(
|
r = engine.generate_turn(
|
||||||
player_action="I cast a fireball at the tavern ceiling",
|
player_action="I cast a fireball at the tavern ceiling",
|
||||||
last_prompt="What do you do?",
|
|
||||||
)
|
)
|
||||||
check("Fireball spell", r, expect_error=False, expect_book=False)
|
check("Fireball spell", r, expect_error=False, expect_book=False)
|
||||||
if r.log_entry:
|
if r.log_entry:
|
||||||
@ -121,17 +115,14 @@ def main():
|
|||||||
section("Invalid action — nonsensical")
|
section("Invalid action — nonsensical")
|
||||||
r = engine.generate_turn(
|
r = engine.generate_turn(
|
||||||
player_action="I fly to the moon",
|
player_action="I fly to the moon",
|
||||||
last_prompt="What do you do?",
|
|
||||||
)
|
)
|
||||||
check("Fly to moon", r, expect_error=False, expect_book=False)
|
check("Fly to moon", r, expect_error=False, expect_book=False)
|
||||||
if r.log_entry:
|
if r.log_entry:
|
||||||
print(f" log: {r.log_entry}")
|
print(f" log: {r.log_entry}")
|
||||||
|
|
||||||
section("Resume from last_prompt (no player action)")
|
section("Resume scene (no player action)")
|
||||||
r = engine.generate_turn(
|
r = engine.generate_turn()
|
||||||
last_prompt="You stand in the market square, surrounded by stalls and bustle. What do you do?",
|
check("Resume scene", r, expect_error=False, expect_book=True, expect_prompt=False)
|
||||||
)
|
|
||||||
check("Resume scene", r, expect_error=False, expect_book=True, expect_prompt=True)
|
|
||||||
|
|
||||||
print(f"\n{'=' * 60}")
|
print(f"\n{'=' * 60}")
|
||||||
print(f" Results: {PASS} passed, {FAIL} failed")
|
print(f" Results: {PASS} passed, {FAIL} failed")
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user