Changes block from prose llm
This commit is contained in:
parent
e74dd07699
commit
a7e6d5540f
@ -110,6 +110,20 @@ PROSE_PROMPT = Template("""You are the DM for "The Chaos". Narrate in 2nd person
|
||||
|
||||
A die is cast at the start of each turn — incorporate it into your narrative.
|
||||
|
||||
End your response with a `### Changes` block listing what changed:
|
||||
|
||||
### Changes
|
||||
- Current Health: 3
|
||||
- Cash: 45 silver
|
||||
- Added to inventory: Silver key
|
||||
- Removed from inventory: Torches (10)
|
||||
- Replaced gear: Mace (1d6+1) → Mace (1d6+2)
|
||||
- Note: Found a hidden passage
|
||||
- Journal done: Defeat the demon
|
||||
- Journal add: Investigate the mine
|
||||
|
||||
Only include lines for things that actually changed. Omit unused lines entirely.
|
||||
|
||||
## State
|
||||
|
||||
### Character
|
||||
@ -658,6 +672,9 @@ class GameEngine:
|
||||
try:
|
||||
return fn(args)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
tb = traceback.format_exc()
|
||||
self._append_llm_log(f"\n--- TOOL ERROR ({tool_name}) ---\n{tb}")
|
||||
return f"Tool error ({tool_name}): {e}"
|
||||
|
||||
@staticmethod
|
||||
@ -734,11 +751,13 @@ class GameEngine:
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
|
||||
def _call_llm(self, messages: list[dict], *, label: str = "", max_tokens: int | None = None) -> str | None:
|
||||
def _call_llm(self, messages: list[dict], *, label: str = "", max_tokens: int | None = None, on_debug: callable = None) -> str | None:
|
||||
"""Make a single LLM call. Returns content text or None on error."""
|
||||
try:
|
||||
import litellm
|
||||
except ImportError:
|
||||
if on_debug:
|
||||
on_debug("llm_error", {"label": label, "error": "litellm not installed"})
|
||||
return None
|
||||
try:
|
||||
response = litellm.completion(
|
||||
@ -753,7 +772,10 @@ class GameEngine:
|
||||
self._append_llm_log(f"\n--- {label} ---\n{text}")
|
||||
return text
|
||||
except Exception as e:
|
||||
self._append_llm_log(f"\n--- LLM ERROR ({label}) ---\n{e}")
|
||||
err_msg = f"{type(e).__name__}: {e}"
|
||||
self._append_llm_log(f"\n--- LLM ERROR ({label}) ---\n{err_msg}")
|
||||
if on_debug:
|
||||
on_debug("llm_error", {"label": label, "error": err_msg})
|
||||
return None
|
||||
|
||||
def generate_with_tools(
|
||||
@ -793,6 +815,7 @@ class GameEngine:
|
||||
on_debug("phase", {"phase": 1, "name": "prose", "status": "start", "dice": die_roll})
|
||||
|
||||
book_log = None
|
||||
changes_block = ""
|
||||
for attempt in range(3):
|
||||
system = PROSE_PROMPT.substitute(
|
||||
character=self._read_file(CHAR_PATH) or "*No character sheet.*",
|
||||
@ -809,16 +832,24 @@ class GameEngine:
|
||||
text = self._call_llm([
|
||||
{"role": "system", "content": system},
|
||||
{"role": "user", "content": user},
|
||||
], label=f"Prose attempt {attempt + 1}", max_tokens=1024)
|
||||
], label=f"Prose attempt {attempt + 1}", max_tokens=1024, on_debug=on_debug)
|
||||
|
||||
if not text or not text.strip():
|
||||
if on_debug:
|
||||
on_debug("phase", {"phase": 1, "status": "empty", "attempt": attempt + 1})
|
||||
continue
|
||||
book_log = text.strip()
|
||||
raw = text.strip()
|
||||
# Split narrative from ### Changes block
|
||||
changes_block = ""
|
||||
if "### Changes" in raw:
|
||||
parts = raw.split("### Changes", 1)
|
||||
book_log = parts[0].strip()
|
||||
changes_block = "### Changes" + parts[1]
|
||||
else:
|
||||
book_log = raw
|
||||
if on_debug:
|
||||
preview = book_log[:150].replace("\n", "\\n")
|
||||
on_debug("phase", {"phase": 1, "status": "done", "chars": len(book_log), "preview": preview})
|
||||
on_debug("phase", {"phase": 1, "status": "done", "chars": len(book_log), "changes": bool(changes_block), "preview": preview})
|
||||
break
|
||||
|
||||
if not book_log:
|
||||
@ -833,13 +864,16 @@ class GameEngine:
|
||||
log_context = self._read_recent_log()
|
||||
log_entry = None
|
||||
for attempt in range(2):
|
||||
context = book_log
|
||||
if changes_block:
|
||||
context += f"\n\n{changes_block}"
|
||||
text = self._call_llm([
|
||||
{"role": "user", "content":
|
||||
f"Given the session log so far, summarize the new story in one line. "
|
||||
f"Focus on who was involved (character and NPC names):\n\n"
|
||||
f"## Session Log\n{log_context}\n\n"
|
||||
f"## New Story\n{book_log}"}
|
||||
], label=f"Summarize attempt {attempt + 1}")
|
||||
f"## New Story\n{context}"}
|
||||
], label=f"Summarize attempt {attempt + 1}", on_debug=on_debug)
|
||||
if text and text.strip():
|
||||
log_entry = text.strip().split("\n")[0][:120]
|
||||
if on_debug:
|
||||
@ -864,13 +898,25 @@ class GameEngine:
|
||||
current_world = self._truncate_world(self._read_file(WORLD_PATH) or "") or "*No world.*"
|
||||
|
||||
for attempt in range(3):
|
||||
text = self._call_llm([
|
||||
{"role": "user", "content":
|
||||
f"Read the story and compare with current state. Output tool calls for any changes:\n\n"
|
||||
phase3_prompt = (
|
||||
f"## Current Character\n{current_char}\n\n"
|
||||
f"## Current World\n{current_world}\n\n"
|
||||
f"## Story\n{book_log}\n\n"
|
||||
)
|
||||
if changes_block.strip():
|
||||
phase3_prompt += (
|
||||
f"## Changes to apply\n{changes_block}\n\n"
|
||||
f"Convert the listed changes into tool calls:\n\n"
|
||||
)
|
||||
else:
|
||||
phase3_prompt += (
|
||||
f"Read the story and compare with current state. Output tool calls for any changes:\n\n"
|
||||
)
|
||||
phase3_prompt += (
|
||||
f"Output ```tool blocks for changes only. Examples:\n\n"
|
||||
)
|
||||
text = self._call_llm([
|
||||
{"role": "user", "content": phase3_prompt +
|
||||
f"```tool\n{{\"tool\": \"modify_vitals\", \"args\": {{\"current_hp\": 5, \"cash\": 45}}}}\n```\n"
|
||||
f"```tool\n{{\"tool\": \"modify_traits\", \"args\": {{\"dex\": 15}}}}\n```\n"
|
||||
f"```tool\n{{\"tool\": \"add_to_inventory\", \"args\": {{\"item\": \"Silver key\"}}}}\n```\n"
|
||||
@ -882,7 +928,7 @@ class GameEngine:
|
||||
f"```tool\n{{\"tool\": \"journal_update\", \"args\": {{\"add\": [\"Investigate the mine\"], \"done\": [\"Defeat the demon\"]}}}}\n```\n"
|
||||
f"```tool\n{{\"tool\": \"finalize_turn\", \"args\": {{\"user_prompt\": \"What do you do?\", \"ambience\": \"dungeon\"}}}}\n```\n\n"
|
||||
f"Only output tools for things that actually changed. Omit unchanged fields."}
|
||||
], label=f"Extract attempt {attempt + 1}")
|
||||
], label=f"Extract attempt {attempt + 1}", on_debug=on_debug)
|
||||
|
||||
if not text or not text.strip():
|
||||
if on_debug:
|
||||
@ -933,6 +979,8 @@ class GameEngine:
|
||||
if on_debug:
|
||||
on_debug("phase", {"phase": 3, "status": "errors", "errors": errors, "attempt": attempt + 1})
|
||||
|
||||
if errors and on_debug:
|
||||
on_debug("phase", {"phase": 3, "status": "exhausted", "errors": errors})
|
||||
if on_action:
|
||||
on_action("Turn complete")
|
||||
if on_debug:
|
||||
|
||||
28
tools/run.py
28
tools/run.py
@ -756,6 +756,7 @@ class ChaosTUI(App):
|
||||
|
||||
def _run_generation(self, player_action: str | None) -> None:
|
||||
"""Worker thread: calls engine.generate_with_tools() and posts result back."""
|
||||
import traceback
|
||||
last_prompt = self._last_prompt if self._last_prompt else None
|
||||
|
||||
def on_thought(thought: str) -> None:
|
||||
@ -767,6 +768,7 @@ class ChaosTUI(App):
|
||||
def on_debug(event_type: str, data: dict) -> None:
|
||||
self.call_from_thread(self._on_debug, event_type, data)
|
||||
|
||||
try:
|
||||
result = self.engine.generate_with_tools(
|
||||
player_action=player_action,
|
||||
last_prompt=last_prompt,
|
||||
@ -775,6 +777,10 @@ class ChaosTUI(App):
|
||||
on_player_roll=self._on_player_roll,
|
||||
on_debug=on_debug,
|
||||
)
|
||||
except Exception as e:
|
||||
tb = traceback.format_exc()
|
||||
self.call_from_thread(self._on_generation_error, e, tb)
|
||||
return
|
||||
|
||||
self.call_from_thread(self._on_generation_done, result, player_action)
|
||||
|
||||
@ -859,6 +865,11 @@ class ChaosTUI(App):
|
||||
for e in errs:
|
||||
self._append_debug(f" ✖ {e}")
|
||||
self._append_debug(f" ⟳ retry (attempt {data.get('attempt', '?')})")
|
||||
elif status == "exhausted":
|
||||
errs = data.get("errors", [])
|
||||
self._append_debug(f" ✖ Phase 3 exhausted all retries — state changes may be missing!")
|
||||
for e in errs:
|
||||
self._append_debug(f" {e}")
|
||||
elif event_type == "phase_done":
|
||||
self._append_debug(f" ✔ turn complete — book_log: {data.get('book_log_chars', 0)} chars")
|
||||
if data.get("log_entry"):
|
||||
@ -878,7 +889,9 @@ class ChaosTUI(App):
|
||||
elif event_type == "parse_error":
|
||||
self._append_debug(f" ⚠ bad tool block: {data.get('content', '')}")
|
||||
elif event_type == "llm_error":
|
||||
self._append_debug(f" ✖ LLM error: {data.get('error', '')}")
|
||||
label = data.get("label", "")
|
||||
err = data.get("error", "")
|
||||
self._append_debug(f" ✖ LLM error [{label}]: {err}")
|
||||
|
||||
def _on_player_roll(self, dice: str, reason: str) -> str:
|
||||
"""Called from worker thread. Shows roll popup, blocks until player responds."""
|
||||
@ -950,6 +963,19 @@ class ChaosTUI(App):
|
||||
self._last_result = result
|
||||
self._append_debug("✔ turn complete")
|
||||
|
||||
def _on_generation_error(
|
||||
self, error: Exception, traceback_str: str
|
||||
) -> None:
|
||||
"""Handle an unhandled exception from the worker thread."""
|
||||
import traceback
|
||||
self._is_processing = False
|
||||
self._hide_thinking()
|
||||
err_msg = f"{type(error).__name__}: {error}"
|
||||
self._append_debug(f"✖ UNHANDLED EXCEPTION: {err_msg}")
|
||||
for line in traceback_str.rstrip().split("\n")[-10:]:
|
||||
self._append_debug(f" {line}")
|
||||
self._show_error(err_msg, traceback_str)
|
||||
|
||||
def _display_scene(self, result: TurnResult) -> None:
|
||||
"""Update the UI with the last story entry followed by the DM prompt."""
|
||||
parts = []
|
||||
|
||||
Loading…
Reference in New Issue
Block a user