feat(narrative): orchestrate round translation with state updates
translate_round() ties together: - reading the round's actions from actions.jsonl - generating prose via the LLM - updating per-character emotional state - persisting the beat, characters, and file offset Full test suite now at 19/19 passing across action mapper, character engine, story store, and translator. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9db7b399cc
commit
caec8b5c77
|
|
@ -183,3 +183,57 @@ def generate_prose(actions: list, characters: list, tone: str, previous_beats: l
|
|||
previous=prev_prose or "(this is the first scene)",
|
||||
)
|
||||
return call_llm(prompt)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Round orchestration — the entry point most callers use
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def translate_round(sim_dir: str, platform: str, target_round: int, tone: str = "neutral") -> dict:
|
||||
"""Translate a single simulation round into a story beat end-to-end.
|
||||
|
||||
Steps:
|
||||
1. Read the round's actions from `{sim_dir}/{platform}/actions.jsonl`
|
||||
(starting from the saved file offset).
|
||||
2. Generate prose via the LLM using current character state.
|
||||
3. Apply emotional-state deltas for every action.
|
||||
4. Persist the new beat, updated character state, and new file offset.
|
||||
|
||||
Returns the newly created story beat dict.
|
||||
"""
|
||||
# Lazy imports keep module import lightweight for unit tests of helpers
|
||||
from app.services.narrative.story_store import StoryStore
|
||||
from app.services.narrative.character_engine import apply_action_emotional_delta
|
||||
|
||||
store = StoryStore(sim_dir)
|
||||
actions_path = os.path.join(sim_dir, platform, "actions.jsonl")
|
||||
start_offset = store.get_file_offset(platform)
|
||||
|
||||
actions, new_offset = read_actions_for_round(actions_path, start_offset, target_round)
|
||||
|
||||
characters = store.load_characters()
|
||||
previous_beats = store.get_all_beats()
|
||||
|
||||
prose = generate_prose(actions, characters, tone, previous_beats)
|
||||
|
||||
# De-duplicated list of character names that acted this round
|
||||
involved = sorted({a.get("agent_name") for a in actions if a.get("agent_name")})
|
||||
|
||||
# Mutate character emotional state in-place, then persist
|
||||
char_by_name = {c["name"]: c for c in characters}
|
||||
for action in actions:
|
||||
char = char_by_name.get(action.get("agent_name"))
|
||||
if char:
|
||||
apply_action_emotional_delta(char, action.get("action_type", ""))
|
||||
store.save_characters(list(char_by_name.values()))
|
||||
|
||||
beat = {
|
||||
"round": target_round,
|
||||
"prose": prose,
|
||||
"characters": involved,
|
||||
"action_count": len(actions),
|
||||
"platform": platform,
|
||||
}
|
||||
store.append_beat(beat)
|
||||
store.set_file_offset(platform, new_offset)
|
||||
return beat
|
||||
|
|
|
|||
|
|
@ -79,3 +79,42 @@ def test_generate_prose_calls_llm_with_context():
|
|||
def test_generate_prose_empty_actions_returns_placeholder():
|
||||
result = generate_prose([], [], tone="any", previous_beats=[])
|
||||
assert "quiet" in result.lower() or "pause" in result.lower()
|
||||
|
||||
|
||||
# ---- Round orchestration tests ----
|
||||
from app.services.narrative.narrative_translator import translate_round
|
||||
from app.services.narrative.story_store import StoryStore
|
||||
|
||||
|
||||
def test_translate_round_produces_beat(tmp_path):
|
||||
sim_dir = str(tmp_path / "sim_test")
|
||||
os.makedirs(sim_dir)
|
||||
platform_dir = os.path.join(sim_dir, "twitter")
|
||||
os.makedirs(platform_dir)
|
||||
actions_path = os.path.join(platform_dir, "actions.jsonl")
|
||||
|
||||
lines = [
|
||||
{"round": 1, "agent_id": 1, "agent_name": "Alice", "action_type": "CREATE_POST",
|
||||
"action_args": {"content": "Hi"}, "success": True, "timestamp": "t"},
|
||||
{"event_type": "round_end", "round": 1, "timestamp": "t"},
|
||||
]
|
||||
with open(actions_path, "w") as f:
|
||||
for line in lines:
|
||||
f.write(json.dumps(line) + "\n")
|
||||
|
||||
store = StoryStore(sim_dir)
|
||||
store.save_characters([{
|
||||
"id": "1", "name": "Alice",
|
||||
"emotional_state": {"current": {k: 0.0 for k in ["anger","fear","joy","sadness","trust","surprise"]}},
|
||||
}])
|
||||
|
||||
with patch("app.services.narrative.narrative_translator.call_llm") as mock_llm:
|
||||
mock_llm.return_value = "Alice spoke into the void."
|
||||
beat = translate_round(sim_dir, platform="twitter", target_round=1, tone="neutral")
|
||||
|
||||
assert beat["round"] == 1
|
||||
assert beat["prose"] == "Alice spoke into the void."
|
||||
assert "Alice" in beat.get("characters", [])
|
||||
|
||||
stored_beats = store.get_all_beats()
|
||||
assert len(stored_beats) == 1
|
||||
|
|
|
|||
Loading…
Reference in New Issue