From 87a109c7ef3dfec98c13779a620ced96898bf89f Mon Sep 17 00:00:00 2001 From: vegu-ai-tools <152010387+vegu-ai-tools@users.noreply.github.com> Date: Tue, 31 Mar 2026 18:34:18 +0300 Subject: [PATCH] expand arc polish --- src/talemate/agents/director/__init__.py | 3 + src/talemate/agents/director/chat/mixin.py | 23 - .../agents/director/chat/websocket_handler.py | 16 +- .../director/modules/create-outline.json | 1063 +++++++++++------ .../director/modules/critique-outline.json | 2 +- .../director-action-direct-story-arc.json | 87 +- .../agents/director/modules/manage-plan.json | 174 +-- src/talemate/agents/director/plan/__init__.py | 3 +- src/talemate/agents/director/plan/expand.py | 211 +--- src/talemate/agents/director/plan/mixin.py | 293 +++++ src/talemate/agents/director/plan/nodes.py | 14 +- .../director/scene-plan-create-outline.jinja2 | 2 +- .../scene-plan-critique-outline.jinja2 | 2 +- .../src/components/SceneToolsDirector.vue | 30 +- ...ermine_character_dialogue_instructions.txt | 5 +- .../prompts/baselines/director/arc_expand.txt | 104 ++ .../arc_expand__with_preceding_text.txt | 104 ++ .../director/arc_expand_critique.txt | 33 + .../baselines/editor/revision_unslop.txt | 69 +- ...ermine_character_dialogue_instructions.txt | 5 +- .../editor/revision_unslop.txt | 69 +- ...ermine_character_dialogue_instructions.txt | 5 +- .../editor/revision_unslop.txt | 69 +- ...ermine_character_dialogue_instructions.txt | 5 +- .../editor/revision_unslop.txt | 69 +- ...ermine_character_dialogue_instructions.txt | 5 +- .../editor/revision_unslop.txt | 69 +- ...ermine_character_dialogue_instructions.txt | 5 +- .../baselines_xml/editor/revision_unslop.txt | 69 +- .../baselines/test_director_baselines.py | 167 ++- tests/test_plan_expand.py | 259 ++++ 31 files changed, 2054 insertions(+), 980 deletions(-) create mode 100644 src/talemate/agents/director/plan/mixin.py create mode 100644 tests/data/prompts/baselines/director/arc_expand.txt create mode 100644 tests/data/prompts/baselines/director/arc_expand__with_preceding_text.txt create mode 100644 tests/data/prompts/baselines/director/arc_expand_critique.txt create mode 100644 tests/test_plan_expand.py diff --git a/src/talemate/agents/director/__init__.py b/src/talemate/agents/director/__init__.py index a57d250a..39a6e91f 100644 --- a/src/talemate/agents/director/__init__.py +++ b/src/talemate/agents/director/__init__.py @@ -26,6 +26,7 @@ from .websocket_handler import DirectorWebsocketHandler from .chat.mixin import DirectorChatMixin from .character_management import CharacterManagementMixin from .scene_direction.mixin import SceneDirectionMixin +from .plan.mixin import PlanMixin import talemate.agents.director.nodes # noqa: F401 if TYPE_CHECKING: @@ -38,6 +39,7 @@ log = structlog.get_logger("talemate.agent.director") class DirectorAgent( DirectorChatMixin, SceneDirectionMixin, + PlanMixin, GuideSceneMixin, MemoryRAGMixin, GenerateChoicesMixin, @@ -95,6 +97,7 @@ class DirectorAgent( CharacterManagementMixin.add_actions(actions) DirectorChatMixin.add_actions(actions) SceneDirectionMixin.add_scene_direction_actions(actions) + PlanMixin.add_plan_actions(actions) return actions @classmethod diff --git a/src/talemate/agents/director/chat/mixin.py b/src/talemate/agents/director/chat/mixin.py index 6f537dee..165cc639 100644 --- a/src/talemate/agents/director/chat/mixin.py +++ b/src/talemate/agents/director/chat/mixin.py @@ -118,25 +118,6 @@ class DirectorChatMixin: step=10, min=20, max=500, - title="Arc Generation", - ), - "generate_arc_dialogue_ratio": AgentActionConfig( - type="number", - label="Dialogue beat ratio", - description="Target fraction of beats that should be dialogue during arc generation. 0.4 = 40% dialogue beats.", - value=0.4, - step=0.1, - min=0.0, - max=1.0, - ), - "generate_arc_expand_chunk_size": AgentActionConfig( - type="number", - label="Expand chunk size", - description="Number of beats per expansion chunk in expand mode. Larger chunks produce more cohesive prose but use more context.", - value=5, - step=1, - min=3, - max=12, ), }, ) @@ -163,10 +144,6 @@ class DirectorChatMixin: def chat_scene_context_ratio(self) -> float: return self.actions["chat"].config["scene_context_ratio"].value - @property - def chat_dialogue_ratio(self) -> float: - return self.actions["chat"].config["generate_arc_dialogue_ratio"].value - @property def chat_enable_analysis(self) -> bool: return self.actions["chat"].config["enable_analysis"].value diff --git a/src/talemate/agents/director/chat/websocket_handler.py b/src/talemate/agents/director/chat/websocket_handler.py index 27a0096d..a167a550 100644 --- a/src/talemate/agents/director/chat/websocket_handler.py +++ b/src/talemate/agents/director/chat/websocket_handler.py @@ -44,6 +44,8 @@ class ChatCreateGenerateArcPayload(pydantic.BaseModel): beat_count: int = 8 dialogue_ratio: float | None = None mode: Literal["generate_arc", "generate_arc_expand"] = "generate_arc" + outline_critique: bool | None = None + expand_critique: bool | None = None class ChatRegeneratePayload(pydantic.BaseModel): @@ -451,10 +453,20 @@ class DirectorChatWebsocketMixin: payload = ChatCreateGenerateArcPayload(**data) if payload.dialogue_ratio is not None: - self.director.actions["chat"].config[ - "generate_arc_dialogue_ratio" + self.director.actions["plan"].config[ + "dialogue_ratio" ].value = payload.dialogue_ratio + if payload.outline_critique is not None: + self.director.actions["plan"].config[ + "outline_critique" + ].value = payload.outline_critique + + if payload.expand_critique is not None: + self.director.actions["plan"].config[ + "expand_critique" + ].value = payload.expand_critique + chat = self.director.chat_create_generate_arc( payload.instructions, payload.beat_count, diff --git a/src/talemate/agents/director/modules/create-outline.json b/src/talemate/agents/director/modules/create-outline.json index 4754b13d..77822dfc 100644 --- a/src/talemate/agents/director/modules/create-outline.json +++ b/src/talemate/agents/director/modules/create-outline.json @@ -16,8 +16,8 @@ "properties": { "number_type": "int" }, - "x": 298, - "y": 524, + "x": 296, + "y": 258, "width": 210, "height": 58, "collapsed": false, @@ -32,8 +32,8 @@ "name": "beat_count", "scope": "local" }, - "x": 578, - "y": 524, + "x": 576, + "y": 258, "width": 210, "height": 122, "collapsed": false, @@ -48,8 +48,8 @@ "name": "instructions", "scope": "local" }, - "x": 568, - "y": 304, + "x": 566, + "y": 38, "width": 210, "height": 122, "collapsed": false, @@ -67,8 +67,8 @@ "input_group": "", "num": 0 }, - "x": 28, - "y": 74, + "x": 26, + "y": -192, "width": 210, "height": 154, "collapsed": false, @@ -84,8 +84,8 @@ "output_name": "beat_count", "num": 2 }, - "x": 1218, - "y": 554, + "x": 1216, + "y": 288, "width": 210, "height": 106, "collapsed": false, @@ -101,8 +101,8 @@ "output_name": "instructions", "num": 1 }, - "x": 1218, - "y": 304, + "x": 1216, + "y": 38, "width": 210, "height": 106, "collapsed": false, @@ -110,21 +110,6 @@ "registry": "core/Output", "base_type": "core/Node" }, - "2051509a-fc52-41ea-b674-faf9ca7a2faf": { - "title": "Stage 0", - "id": "2051509a-fc52-41ea-b674-faf9ca7a2faf", - "properties": { - "stage": 0 - }, - "x": 898, - "y": 404, - "width": 210, - "height": 118, - "collapsed": false, - "inherited": false, - "registry": "core/Stage", - "base_type": "core/Node" - }, "1e9d954a-1e56-4880-9f80-00c235bed988": { "title": "OUT state", "id": "1e9d954a-1e56-4880-9f80-00c235bed988", @@ -133,8 +118,8 @@ "output_name": "state", "num": 0 }, - "x": 1208, - "y": 84, + "x": 1206, + "y": -182, "width": 210, "height": 106, "collapsed": false, @@ -148,8 +133,8 @@ "properties": { "value": true }, - "x": 1044, - "y": 788, + "x": 1041, + "y": 802, "width": 210, "height": 58, "collapsed": false, @@ -165,8 +150,8 @@ "template_file": "scene-plan-create-outline", "template_text": "" }, - "x": 1059, - "y": 1022, + "x": 1056, + "y": 1036, "width": 210, "height": 146, "collapsed": false, @@ -180,8 +165,8 @@ "properties": { "value": true }, - "x": 98, - "y": 1171, + "x": 95, + "y": 1185, "width": 210, "height": 58, "collapsed": true, @@ -193,8 +178,8 @@ "title": "template_vars", "id": "517d4bb8-38aa-4b11-8ba9-f569a6a45338", "properties": {}, - "x": 849, - "y": 1056, + "x": 846, + "y": 1070, "width": 140, "height": 26, "collapsed": false, @@ -208,8 +193,8 @@ "properties": { "character_status": "active" }, - "x": 229, - "y": 916, + "x": 226, + "y": 930, "width": 228, "height": 58, "collapsed": false, @@ -224,8 +209,8 @@ "name": "beat_count", "scope": "local" }, - "x": 29, - "y": 1236, + "x": 26, + "y": 1250, "width": 210, "height": 122, "collapsed": true, @@ -239,8 +224,8 @@ "properties": { "beat_count": 8 }, - "x": 259, - "y": 1176, + "x": 256, + "y": 1190, "width": 304, "height": 118, "collapsed": false, @@ -248,74 +233,6 @@ "registry": "agents/director/plan/EstimateWords", "base_type": "core/Node" }, - "0bf06241-b92b-4f0f-bc67-0789fe061c18": { - "title": "Meta", - "id": "0bf06241-b92b-4f0f-bc67-0789fe061c18", - "properties": {}, - "x": 2009, - "y": 1466, - "width": 140, - "height": 81, - "collapsed": false, - "inherited": false, - "registry": "data/DictCollector", - "dynamic_inputs": [ - { - "name": "item0", - "type": "*" - } - ], - "base_type": "core/DynamicSocketNodeBase" - }, - "fcc70ad0-b17c-4f1a-9752-9f1377c3aa39": { - "title": "SET local.plan_id", - "id": "fcc70ad0-b17c-4f1a-9752-9f1377c3aa39", - "properties": { - "name": "plan_id", - "scope": "local" - }, - "x": 2955, - "y": 1084, - "width": 210, - "height": 122, - "collapsed": false, - "inherited": false, - "registry": "state/SetState", - "base_type": "core/Node" - }, - "789e16f5-239e-4bec-9906-4690a692a01b": { - "title": "SET local.summary", - "id": "789e16f5-239e-4bec-9906-4690a692a01b", - "properties": { - "name": "summary", - "scope": "local" - }, - "x": 2955, - "y": 1254, - "width": 210, - "height": 122, - "collapsed": false, - "inherited": false, - "registry": "state/SetState", - "base_type": "core/Node" - }, - "d70c7ca4-ef49-4964-9c3c-c9ec0df7fa04": { - "title": "OUT beat_count", - "id": "d70c7ca4-ef49-4964-9c3c-c9ec0df7fa04", - "properties": { - "output_type": "str", - "output_name": "plan_id", - "num": 3 - }, - "x": 1864, - "y": 94, - "width": 210, - "height": 106, - "collapsed": false, - "inherited": false, - "registry": "core/Output", - "base_type": "core/Node" - }, "efa8c129-8445-4860-83cd-8c180e9053aa": { "title": "GET local.plan_id", "id": "efa8c129-8445-4860-83cd-8c180e9053aa", @@ -324,7 +241,7 @@ "scope": "local" }, "x": 1484, - "y": 74, + "y": 334, "width": 210, "height": 122, "collapsed": false, @@ -332,31 +249,14 @@ "registry": "state/GetState", "base_type": "core/Node" }, - "8641d43d-c5d7-471e-870b-27fcaf4b4564": { - "title": "OUT summary", - "id": "8641d43d-c5d7-471e-870b-27fcaf4b4564", - "properties": { - "output_type": "str", - "output_name": "summary", - "num": 4 - }, - "x": 1864, - "y": 304, - "width": 210, - "height": 106, - "collapsed": false, - "inherited": false, - "registry": "core/Output", - "base_type": "core/Node" - }, "75689617-050f-4092-a0d0-36d9ca63262c": { "title": "Get Agent", "id": "75689617-050f-4092-a0d0-36d9ca63262c", "properties": { "agent_name": "director" }, - "x": 721, - "y": 890, + "x": 718, + "y": 904, "width": 210, "height": 58, "collapsed": false, @@ -368,8 +268,8 @@ "title": "Template Variables", "id": "34f29d9b-828c-4450-86ce-7163a9ffd2e0", "properties": {}, - "x": 891, - "y": 1220, + "x": 888, + "y": 1234, "width": 170, "height": 46, "collapsed": false, @@ -385,7 +285,7 @@ "scope": "local" }, "x": 1484, - "y": 284, + "y": 544, "width": 210, "height": 122, "collapsed": false, @@ -393,53 +293,8 @@ "registry": "state/GetState", "base_type": "core/Node" }, - "944cd2b9-8d55-4353-a7a9-e3cd98882768": { - "title": "GET obj.perspective", - "id": "944cd2b9-8d55-4353-a7a9-e3cd98882768", - "properties": { - "attribute": "perspective" - }, - "x": 1759, - "y": 1316, - "width": 210, - "height": 98, - "collapsed": false, - "inherited": false, - "registry": "data/Get", - "base_type": "core/Node" - }, - "78cbb918-22da-4e99-87b4-e7a0a3c9da67": { - "title": "GET obj.response", - "id": "78cbb918-22da-4e99-87b4-e7a0a3c9da67", - "properties": { - "attribute": "response" - }, - "x": 1761, - "y": 1170, - "width": 210, - "height": 98, - "collapsed": false, - "inherited": false, - "registry": "data/Get", - "base_type": "core/Node" - }, - "a6b6acdb-c0ad-4c79-a9fc-0cb4a7c5878a": { - "title": "Stage 1", - "id": "a6b6acdb-c0ad-4c79-a9fc-0cb4a7c5878a", - "properties": { - "stage": 1 - }, - "x": 3260, - "y": 1170, - "width": 210, - "height": 118, - "collapsed": false, - "inherited": false, - "registry": "core/Stage", - "base_type": "core/Node" - }, "18339c3e-031b-471d-a317-efcd2c14a4e0": { - "title": "Generate Response", + "title": "Generate Story Outline", "id": "18339c3e-031b-471d-a317-efcd2c14a4e0", "properties": { "data_output": false, @@ -448,8 +303,8 @@ "action_type": "scene_direction", "attempts": 2 }, - "x": 1399, - "y": 986, + "x": 1396, + "y": 1000, "width": 270, "height": 334, "collapsed": false, @@ -457,134 +312,12 @@ "registry": "prompt/GenerateResponse", "base_type": "core/Node" }, - "cb985916-16c3-4a51-bbf8-d961cabbf07a": { - "title": "GET local.instructions", - "id": "cb985916-16c3-4a51-bbf8-d961cabbf07a", - "properties": { - "name": "instructions", - "scope": "local" - }, - "x": 1770, - "y": 1100, - "width": 210, - "height": 122, - "collapsed": true, - "inherited": false, - "registry": "state/GetState", - "base_type": "core/Node" - }, - "97d95ce0-dccb-41ed-b41a-aca140af56eb": { - "title": "Create Plan", - "id": "97d95ce0-dccb-41ed-b41a-aca140af56eb", - "properties": { - "instructions": "", - "status": "ready" - }, - "x": 2620, - "y": 1170, - "width": 210, - "height": 162, - "collapsed": false, - "inherited": false, - "registry": "agents/director/plan/CreatePlan", - "base_type": "core/Node" - }, - "9c312643-3e88-43f7-a473-ade757d8c550": { - "title": "Critique Outline", - "id": "9c312643-3e88-43f7-a473-ade757d8c550", - "properties": {}, - "x": 2070, - "y": 1050, - "width": 346, - "height": 146, - "collapsed": false, - "inherited": false, - "registry": "agents/director/plan/critiqueOutline", - "base_type": "core/Graph" - }, - "c6488e1b-62c6-4bde-a967-f06996fbb14c": { - "title": "changes", - "id": "c6488e1b-62c6-4bde-a967-f06996fbb14c", - "properties": {}, - "x": 2280, - "y": 1290, - "width": 140, - "height": 26, - "collapsed": true, - "inherited": false, - "registry": "core/Watch", - "base_type": "core/Node" - }, - "66e5a663-7ac7-4b8f-8ab8-16c6f1623b0f": { - "title": "orig_outline", - "id": "66e5a663-7ac7-4b8f-8ab8-16c6f1623b0f", - "properties": {}, - "x": 2280, - "y": 1250, - "width": 140, - "height": 26, - "collapsed": true, - "inherited": false, - "registry": "core/Watch", - "base_type": "core/Node" - }, - "91972225-9a2c-4cb4-ae5f-a61d0de227c6": { - "title": "outline", - "id": "91972225-9a2c-4cb4-ae5f-a61d0de227c6", - "properties": {}, - "x": 2440, - "y": 1210, - "width": 140, - "height": 26, - "collapsed": false, - "inherited": false, - "registry": "core/Watch", - "base_type": "core/Node" - }, - "c9775f83-e7bc-4dc5-959c-bc2182a0d10b": { - "title": "IN instructions", - "id": "c9775f83-e7bc-4dc5-959c-bc2182a0d10b", - "properties": { - "input_type": "str", - "input_name": "instructions", - "input_optional": false, - "input_group": "", - "num": 1 - }, - "x": 28, - "y": 294, - "width": 210, - "height": 154, - "collapsed": false, - "inherited": false, - "registry": "core/Input", - "base_type": "core/Node" - }, - "7d565eb9-f156-4a92-9723-e27964dfe759": { - "title": "IN beat_count", - "id": "7d565eb9-f156-4a92-9723-e27964dfe759", - "properties": { - "input_type": "int", - "input_name": "beat_count", - "input_optional": false, - "input_group": "", - "num": 1 - }, - "x": 28, - "y": 524, - "width": 210, - "height": 154, - "collapsed": false, - "inherited": false, - "registry": "core/Input", - "base_type": "core/Node" - }, "6bb81c40-cff1-42e6-a471-001985e57e5c": { "title": "Dict Collector", "id": "6bb81c40-cff1-42e6-a471-001985e57e5c", "properties": {}, - "x": 640, - "y": 1150, + "x": 637, + "y": 1164, "width": 140, "height": 141, "collapsed": false, @@ -617,14 +350,540 @@ "name": "instructions", "scope": "local" }, - "x": 360, - "y": 1360, + "x": 357, + "y": 1374, "width": 210, "height": 122, "collapsed": false, "inherited": false, "registry": "state/GetState", "base_type": "core/Node" + }, + "cd6a7f30-cc94-4fcc-8d2a-a99dd985a526": { + "title": "As Bool", + "id": "cd6a7f30-cc94-4fcc-8d2a-a99dd985a526", + "properties": { + "default": false + }, + "x": 317, + "y": 519, + "width": 210, + "height": 58, + "collapsed": false, + "inherited": false, + "registry": "core/AsBool", + "base_type": "core/Node" + }, + "857a9a27-0b46-4ab7-99b3-c00d773c4902": { + "title": "SET local.critique", + "id": "857a9a27-0b46-4ab7-99b3-c00d773c4902", + "properties": { + "name": "critique", + "scope": "local" + }, + "x": 590, + "y": 520, + "width": 210, + "height": 122, + "collapsed": false, + "inherited": false, + "registry": "state/SetState", + "base_type": "core/Node" + }, + "d70c7ca4-ef49-4964-9c3c-c9ec0df7fa04": { + "title": "OUT beat_count", + "id": "d70c7ca4-ef49-4964-9c3c-c9ec0df7fa04", + "properties": { + "output_type": "str", + "output_name": "plan_id", + "num": 4 + }, + "x": 1864, + "y": 354, + "width": 210, + "height": 106, + "collapsed": false, + "inherited": false, + "registry": "core/Output", + "base_type": "core/Node" + }, + "8641d43d-c5d7-471e-870b-27fcaf4b4564": { + "title": "OUT summary", + "id": "8641d43d-c5d7-471e-870b-27fcaf4b4564", + "properties": { + "output_type": "str", + "output_name": "summary", + "num": 5 + }, + "x": 1864, + "y": 564, + "width": 210, + "height": 106, + "collapsed": false, + "inherited": false, + "registry": "core/Output", + "base_type": "core/Node" + }, + "2051509a-fc52-41ea-b674-faf9ca7a2faf": { + "title": "Stage 0", + "id": "2051509a-fc52-41ea-b674-faf9ca7a2faf", + "properties": { + "stage": 0 + }, + "x": 896, + "y": 138, + "width": 210, + "height": 118, + "collapsed": false, + "inherited": false, + "registry": "core/Stage", + "base_type": "core/Node" + }, + "7ef30f81-082b-4294-b0a9-bc5cb57fd6ed": { + "title": "OUT critique", + "id": "7ef30f81-082b-4294-b0a9-bc5cb57fd6ed", + "properties": { + "output_type": "bool", + "output_name": "critique", + "num": 3 + }, + "x": 1220, + "y": 540, + "width": 210, + "height": 106, + "collapsed": false, + "inherited": false, + "registry": "core/Output", + "base_type": "core/Node" + }, + "0bf06241-b92b-4f0f-bc67-0789fe061c18": { + "title": "Meta", + "id": "0bf06241-b92b-4f0f-bc67-0789fe061c18", + "properties": {}, + "x": 2006, + "y": 1480, + "width": 140, + "height": 81, + "collapsed": false, + "inherited": false, + "registry": "data/DictCollector", + "dynamic_inputs": [ + { + "name": "item0", + "type": "*" + } + ], + "base_type": "core/DynamicSocketNodeBase" + }, + "78cbb918-22da-4e99-87b4-e7a0a3c9da67": { + "title": "GET obj.response", + "id": "78cbb918-22da-4e99-87b4-e7a0a3c9da67", + "properties": { + "attribute": "response" + }, + "x": 1767, + "y": 1104, + "width": 210, + "height": 98, + "collapsed": false, + "inherited": false, + "registry": "data/Get", + "base_type": "core/Node" + }, + "944cd2b9-8d55-4353-a7a9-e3cd98882768": { + "title": "GET obj.perspective", + "id": "944cd2b9-8d55-4353-a7a9-e3cd98882768", + "properties": { + "attribute": "perspective" + }, + "x": 1767, + "y": 1304, + "width": 210, + "height": 98, + "collapsed": false, + "inherited": false, + "registry": "data/Get", + "base_type": "core/Node" + }, + "a0a70d5d-6fdc-4875-8f97-e74ad6a6480e": { + "title": "SET local.outline", + "id": "a0a70d5d-6fdc-4875-8f97-e74ad6a6480e", + "properties": { + "name": "outline", + "scope": "local" + }, + "x": 2200, + "y": 1010, + "width": 210, + "height": 122, + "collapsed": false, + "inherited": false, + "registry": "state/SetState", + "base_type": "core/Node" + }, + "2e6b8777-ffab-48c7-b396-fd4c38e8936f": { + "title": "SET local.plan_meta", + "id": "2e6b8777-ffab-48c7-b396-fd4c38e8936f", + "properties": { + "name": "plan_meta", + "scope": "local" + }, + "x": 2220, + "y": 1440, + "width": 210, + "height": 122, + "collapsed": false, + "inherited": false, + "registry": "state/SetState", + "base_type": "core/Node" + }, + "c78ff25c-e271-4da7-95e7-ea55026eb0ae": { + "title": "SET local.perspective", + "id": "c78ff25c-e271-4da7-95e7-ea55026eb0ae", + "properties": { + "name": "perspective", + "scope": "local" + }, + "x": 2210, + "y": 1240, + "width": 210, + "height": 122, + "collapsed": false, + "inherited": false, + "registry": "state/SetState", + "base_type": "core/Node" + }, + "a6b6acdb-c0ad-4c79-a9fc-0cb4a7c5878a": { + "title": "Stage 1", + "id": "a6b6acdb-c0ad-4c79-a9fc-0cb4a7c5878a", + "properties": { + "stage": 1 + }, + "x": 2530, + "y": 1240, + "width": 210, + "height": 118, + "collapsed": false, + "inherited": false, + "registry": "core/Stage", + "base_type": "core/Node" + }, + "297241d7-cf15-4f7a-98b7-077e9238258b": { + "title": "GET local.plan_meta", + "id": "297241d7-cf15-4f7a-98b7-077e9238258b", + "properties": { + "name": "plan_meta", + "scope": "local" + }, + "x": 538, + "y": 2807, + "width": 210, + "height": 122, + "collapsed": false, + "inherited": false, + "registry": "state/GetState", + "base_type": "core/Node" + }, + "50a12485-7ff7-4688-b73e-ee665f19ad20": { + "title": "SET local.summary", + "id": "50a12485-7ff7-4688-b73e-ee665f19ad20", + "properties": { + "name": "summary", + "scope": "local" + }, + "x": 1178, + "y": 2687, + "width": 210, + "height": 122, + "collapsed": false, + "inherited": false, + "registry": "state/SetState", + "base_type": "core/Node" + }, + "9e59f7a6-1e3c-4e39-a6b3-31fdd20473d7": { + "title": "SET local.plan_id", + "id": "9e59f7a6-1e3c-4e39-a6b3-31fdd20473d7", + "properties": { + "name": "plan_id", + "scope": "local" + }, + "x": 1178, + "y": 2487, + "width": 210, + "height": 122, + "collapsed": false, + "inherited": false, + "registry": "state/SetState", + "base_type": "core/Node" + }, + "fac41364-9daf-42d4-a6cb-ed52b5247830": { + "title": "GET local.outline", + "id": "fac41364-9daf-42d4-a6cb-ed52b5247830", + "properties": { + "name": "outline", + "scope": "local" + }, + "x": 25, + "y": 2061, + "width": 210, + "height": 122, + "collapsed": false, + "inherited": false, + "registry": "state/GetState", + "base_type": "core/Node" + }, + "1ffc7551-5a84-4445-8dd6-d4bf8fbe4c11": { + "title": "GET local.perspective", + "id": "1ffc7551-5a84-4445-8dd6-d4bf8fbe4c11", + "properties": { + "name": "perspective", + "scope": "local" + }, + "x": 25, + "y": 2261, + "width": 210, + "height": 122, + "collapsed": false, + "inherited": false, + "registry": "state/GetState", + "base_type": "core/Node" + }, + "f542f3d3-8b0c-4dd7-bc64-c77dd99ca10d": { + "title": "GET local.instructions", + "id": "f542f3d3-8b0c-4dd7-bc64-c77dd99ca10d", + "properties": { + "name": "instructions", + "scope": "local" + }, + "x": 25, + "y": 1861, + "width": 210, + "height": 122, + "collapsed": false, + "inherited": false, + "registry": "state/GetState", + "base_type": "core/Node" + }, + "9f99ab9d-9e25-487d-8007-2b57eedbccf0": { + "title": "GET local.critique", + "id": "9f99ab9d-9e25-487d-8007-2b57eedbccf0", + "properties": { + "name": "critique", + "scope": "local" + }, + "x": 25, + "y": 1671, + "width": 210, + "height": 122, + "collapsed": false, + "inherited": false, + "registry": "state/GetState", + "base_type": "core/Node" + }, + "5c3c0736-5504-4c7d-aff3-55efc38c4dfb": { + "title": "IS", + "id": "5c3c0736-5504-4c7d-aff3-55efc38c4dfb", + "properties": { + "pass_through": true + }, + "x": 295, + "y": 1731, + "width": 210, + "height": 78, + "collapsed": true, + "inherited": false, + "registry": "core/Switch", + "base_type": "core/Node" + }, + "1b840b34-d28e-428f-8a55-2b2062b1a9c1": { + "title": "Critique Outline", + "id": "1b840b34-d28e-428f-8a55-2b2062b1a9c1", + "properties": {}, + "x": 420, + "y": 1891, + "width": 346, + "height": 146, + "collapsed": false, + "inherited": false, + "registry": "agents/director/plan/critiqueOutline", + "base_type": "core/Graph" + }, + "e0f2a097-0a50-4a14-957e-31f607d09eaa": { + "title": "changes", + "id": "e0f2a097-0a50-4a14-957e-31f607d09eaa", + "properties": {}, + "x": 935, + "y": 2171, + "width": 140, + "height": 26, + "collapsed": true, + "inherited": false, + "registry": "core/Watch", + "base_type": "core/Node" + }, + "1521d05d-6894-4ea0-be22-4158b6ae502d": { + "title": "orig_outline", + "id": "1521d05d-6894-4ea0-be22-4158b6ae502d", + "properties": {}, + "x": 945, + "y": 1851, + "width": 140, + "height": 26, + "collapsed": true, + "inherited": false, + "registry": "core/Watch", + "base_type": "core/Node" + }, + "d7875776-8ae3-40dc-9b4b-3ce3aebee755": { + "title": "Stage 2", + "id": "d7875776-8ae3-40dc-9b4b-3ce3aebee755", + "properties": { + "stage": 2 + }, + "x": 1175, + "y": 1961, + "width": 210, + "height": 118, + "collapsed": false, + "inherited": false, + "registry": "core/Stage", + "base_type": "core/Node" + }, + "881ce23b-d561-4163-baa1-3d4b02d55c15": { + "title": "Create Plan", + "id": "881ce23b-d561-4163-baa1-3d4b02d55c15", + "properties": { + "instructions": "", + "status": "ready" + }, + "x": 848, + "y": 2597, + "width": 210, + "height": 162, + "collapsed": false, + "inherited": false, + "registry": "agents/director/plan/CreatePlan", + "base_type": "core/Node" + }, + "0cab9899-8d23-4f31-b878-582f493e9374": { + "title": "GET local.instructions", + "id": "0cab9899-8d23-4f31-b878-582f493e9374", + "properties": { + "name": "instructions", + "scope": "local" + }, + "x": 24, + "y": 2546, + "width": 210, + "height": 122, + "collapsed": false, + "inherited": false, + "registry": "state/GetState", + "base_type": "core/Node" + }, + "ba9fb7b5-9945-4286-8a02-b5d97780986a": { + "title": "GET local.outline", + "id": "ba9fb7b5-9945-4286-8a02-b5d97780986a", + "properties": { + "name": "outline", + "scope": "local" + }, + "x": 24, + "y": 2736, + "width": 210, + "height": 122, + "collapsed": false, + "inherited": false, + "registry": "state/GetState", + "base_type": "core/Node" + }, + "d119ebc0-f6d2-43d3-a38d-32f19cd27b2d": { + "title": "SET local.outline", + "id": "d119ebc0-f6d2-43d3-a38d-32f19cd27b2d", + "properties": { + "name": "outline", + "scope": "local" + }, + "x": 895, + "y": 1951, + "width": 210, + "height": 122, + "collapsed": false, + "inherited": false, + "registry": "state/SetState", + "base_type": "core/Node" + }, + "34897457-4117-4ea8-9671-97670cc3fe0d": { + "title": "Stage 2", + "id": "34897457-4117-4ea8-9671-97670cc3fe0d", + "properties": { + "stage": 3 + }, + "x": 1494, + "y": 2596, + "width": 210, + "height": 118, + "collapsed": false, + "inherited": false, + "registry": "core/Stage", + "base_type": "core/Node" + }, + "62afee08-6a03-40db-b2b4-220fb104b617": { + "title": "IN critique", + "id": "62afee08-6a03-40db-b2b4-220fb104b617", + "properties": { + "input_type": "bool", + "input_name": "critique", + "input_optional": true, + "input_group": "", + "num": 3 + }, + "x": 30, + "y": 500, + "width": 210, + "height": 154, + "collapsed": false, + "inherited": false, + "registry": "core/Input", + "base_type": "core/Node" + }, + "7d565eb9-f156-4a92-9723-e27964dfe759": { + "title": "IN beat_count", + "id": "7d565eb9-f156-4a92-9723-e27964dfe759", + "properties": { + "input_type": "int", + "input_name": "beat_count", + "input_optional": false, + "input_group": "", + "num": 2 + }, + "x": 26, + "y": 258, + "width": 210, + "height": 154, + "collapsed": false, + "inherited": false, + "registry": "core/Input", + "base_type": "core/Node" + }, + "c9775f83-e7bc-4dc5-959c-bc2182a0d10b": { + "title": "IN instructions", + "id": "c9775f83-e7bc-4dc5-959c-bc2182a0d10b", + "properties": { + "input_type": "str", + "input_name": "instructions", + "input_optional": false, + "input_group": "", + "num": 1 + }, + "x": 26, + "y": 28, + "width": 210, + "height": 154, + "collapsed": false, + "inherited": false, + "registry": "core/Input", + "base_type": "core/Node" } }, "edges": { @@ -640,12 +899,6 @@ "e9b150a0-0aab-4b92-b7ec-9f52f4e716fe.value": [ "1e9d954a-1e56-4880-9f80-00c235bed988.value" ], - "2051509a-fc52-41ea-b674-faf9ca7a2faf.state": [ - "501a32e7-e22a-4ffc-b7d9-6faef9e44b17.value" - ], - "2051509a-fc52-41ea-b674-faf9ca7a2faf.state_b": [ - "fb04842e-74d7-493e-aded-8e12a05263ce.value" - ], "d864bf7b-83ce-440b-8a9b-ac00f5ad496a.value": [ "18339c3e-031b-471d-a317-efcd2c14a4e0.state" ], @@ -667,15 +920,6 @@ "903af300-88b8-4a88-9200-1fc8a11f4100.estimated_words": [ "6bb81c40-cff1-42e6-a471-001985e57e5c.item2" ], - "0bf06241-b92b-4f0f-bc67-0789fe061c18.dict": [ - "97d95ce0-dccb-41ed-b41a-aca140af56eb.meta" - ], - "fcc70ad0-b17c-4f1a-9752-9f1377c3aa39.value": [ - "a6b6acdb-c0ad-4c79-a9fc-0cb4a7c5878a.state" - ], - "789e16f5-239e-4bec-9906-4690a692a01b.value": [ - "a6b6acdb-c0ad-4c79-a9fc-0cb4a7c5878a.state_b" - ], "efa8c129-8445-4860-83cd-8c180e9053aa.value": [ "d70c7ca4-ef49-4964-9c3c-c9ec0df7fa04.value" ], @@ -689,53 +933,10 @@ "f50e95f8-21ac-4018-a5cd-a351d646bebd.value": [ "8641d43d-c5d7-471e-870b-27fcaf4b4564.value" ], - "944cd2b9-8d55-4353-a7a9-e3cd98882768.value": [ - "0bf06241-b92b-4f0f-bc67-0789fe061c18.item0", - "9c312643-3e88-43f7-a473-ade757d8c550.perspective" - ], - "78cbb918-22da-4e99-87b4-e7a0a3c9da67.value": [ - "9c312643-3e88-43f7-a473-ade757d8c550.outline" - ], - "18339c3e-031b-471d-a317-efcd2c14a4e0.state": [ - "9c312643-3e88-43f7-a473-ade757d8c550.state" - ], "18339c3e-031b-471d-a317-efcd2c14a4e0.extracted": [ "944cd2b9-8d55-4353-a7a9-e3cd98882768.object", "78cbb918-22da-4e99-87b4-e7a0a3c9da67.object" ], - "cb985916-16c3-4a51-bbf8-d961cabbf07a.value": [ - "9c312643-3e88-43f7-a473-ade757d8c550.outline_instructions" - ], - "97d95ce0-dccb-41ed-b41a-aca140af56eb.plan_id": [ - "fcc70ad0-b17c-4f1a-9752-9f1377c3aa39.value" - ], - "97d95ce0-dccb-41ed-b41a-aca140af56eb.result": [ - "789e16f5-239e-4bec-9906-4690a692a01b.value" - ], - "9c312643-3e88-43f7-a473-ade757d8c550.state": [ - "97d95ce0-dccb-41ed-b41a-aca140af56eb.state" - ], - "9c312643-3e88-43f7-a473-ade757d8c550.outline_instructions": [ - "97d95ce0-dccb-41ed-b41a-aca140af56eb.instructions" - ], - "9c312643-3e88-43f7-a473-ade757d8c550.orig_outline": [ - "66e5a663-7ac7-4b8f-8ab8-16c6f1623b0f.value" - ], - "9c312643-3e88-43f7-a473-ade757d8c550.outline": [ - "91972225-9a2c-4cb4-ae5f-a61d0de227c6.value" - ], - "9c312643-3e88-43f7-a473-ade757d8c550.changes": [ - "c6488e1b-62c6-4bde-a967-f06996fbb14c.value" - ], - "91972225-9a2c-4cb4-ae5f-a61d0de227c6.value": [ - "97d95ce0-dccb-41ed-b41a-aca140af56eb.tasks" - ], - "c9775f83-e7bc-4dc5-959c-bc2182a0d10b.value": [ - "88f6f84d-c3ef-4efa-ad22-e079cc3f10e5.value" - ], - "7d565eb9-f156-4a92-9723-e27964dfe759.value": [ - "f06086ba-6253-4da2-ad37-d12b4f64443c.value" - ], "6bb81c40-cff1-42e6-a471-001985e57e5c.dict": [ "517d4bb8-38aa-4b11-8ba9-f569a6a45338.value", "0bf06241-b92b-4f0f-bc67-0789fe061c18.dict", @@ -743,25 +944,117 @@ ], "1a5053b8-6cb9-449e-a52c-acb44b79ab14.value": [ "6bb81c40-cff1-42e6-a471-001985e57e5c.item3" + ], + "cd6a7f30-cc94-4fcc-8d2a-a99dd985a526.value": [ + "857a9a27-0b46-4ab7-99b3-c00d773c4902.value" + ], + "857a9a27-0b46-4ab7-99b3-c00d773c4902.value": [ + "2051509a-fc52-41ea-b674-faf9ca7a2faf.state_c" + ], + "2051509a-fc52-41ea-b674-faf9ca7a2faf.state": [ + "501a32e7-e22a-4ffc-b7d9-6faef9e44b17.value" + ], + "2051509a-fc52-41ea-b674-faf9ca7a2faf.state_b": [ + "fb04842e-74d7-493e-aded-8e12a05263ce.value" + ], + "2051509a-fc52-41ea-b674-faf9ca7a2faf.state_c": [ + "7ef30f81-082b-4294-b0a9-bc5cb57fd6ed.value" + ], + "0bf06241-b92b-4f0f-bc67-0789fe061c18.dict": [ + "2e6b8777-ffab-48c7-b396-fd4c38e8936f.value" + ], + "78cbb918-22da-4e99-87b4-e7a0a3c9da67.value": [ + "a0a70d5d-6fdc-4875-8f97-e74ad6a6480e.value" + ], + "944cd2b9-8d55-4353-a7a9-e3cd98882768.value": [ + "0bf06241-b92b-4f0f-bc67-0789fe061c18.item0", + "c78ff25c-e271-4da7-95e7-ea55026eb0ae.value" + ], + "a0a70d5d-6fdc-4875-8f97-e74ad6a6480e.value": [ + "a6b6acdb-c0ad-4c79-a9fc-0cb4a7c5878a.state" + ], + "2e6b8777-ffab-48c7-b396-fd4c38e8936f.value": [ + "a6b6acdb-c0ad-4c79-a9fc-0cb4a7c5878a.state_c" + ], + "c78ff25c-e271-4da7-95e7-ea55026eb0ae.value": [ + "a6b6acdb-c0ad-4c79-a9fc-0cb4a7c5878a.state_b" + ], + "297241d7-cf15-4f7a-98b7-077e9238258b.value": [ + "881ce23b-d561-4163-baa1-3d4b02d55c15.meta" + ], + "50a12485-7ff7-4688-b73e-ee665f19ad20.value": [ + "34897457-4117-4ea8-9671-97670cc3fe0d.state_b" + ], + "9e59f7a6-1e3c-4e39-a6b3-31fdd20473d7.value": [ + "34897457-4117-4ea8-9671-97670cc3fe0d.state" + ], + "fac41364-9daf-42d4-a6cb-ed52b5247830.value": [ + "1b840b34-d28e-428f-8a55-2b2062b1a9c1.outline" + ], + "1ffc7551-5a84-4445-8dd6-d4bf8fbe4c11.value": [ + "1b840b34-d28e-428f-8a55-2b2062b1a9c1.perspective" + ], + "f542f3d3-8b0c-4dd7-bc64-c77dd99ca10d.value": [ + "1b840b34-d28e-428f-8a55-2b2062b1a9c1.outline_instructions" + ], + "9f99ab9d-9e25-487d-8007-2b57eedbccf0.value": [ + "5c3c0736-5504-4c7d-aff3-55efc38c4dfb.value" + ], + "5c3c0736-5504-4c7d-aff3-55efc38c4dfb.yes": [ + "1b840b34-d28e-428f-8a55-2b2062b1a9c1.state" + ], + "1b840b34-d28e-428f-8a55-2b2062b1a9c1.orig_outline": [ + "1521d05d-6894-4ea0-be22-4158b6ae502d.value" + ], + "1b840b34-d28e-428f-8a55-2b2062b1a9c1.outline": [ + "d119ebc0-f6d2-43d3-a38d-32f19cd27b2d.value" + ], + "1b840b34-d28e-428f-8a55-2b2062b1a9c1.changes": [ + "e0f2a097-0a50-4a14-957e-31f607d09eaa.value" + ], + "881ce23b-d561-4163-baa1-3d4b02d55c15.plan_id": [ + "9e59f7a6-1e3c-4e39-a6b3-31fdd20473d7.value" + ], + "881ce23b-d561-4163-baa1-3d4b02d55c15.result": [ + "50a12485-7ff7-4688-b73e-ee665f19ad20.value" + ], + "0cab9899-8d23-4f31-b878-582f493e9374.value": [ + "881ce23b-d561-4163-baa1-3d4b02d55c15.instructions" + ], + "ba9fb7b5-9945-4286-8a02-b5d97780986a.value": [ + "881ce23b-d561-4163-baa1-3d4b02d55c15.tasks", + "881ce23b-d561-4163-baa1-3d4b02d55c15.state" + ], + "d119ebc0-f6d2-43d3-a38d-32f19cd27b2d.value": [ + "d7875776-8ae3-40dc-9b4b-3ce3aebee755.state" + ], + "62afee08-6a03-40db-b2b4-220fb104b617.value": [ + "cd6a7f30-cc94-4fcc-8d2a-a99dd985a526.value" + ], + "7d565eb9-f156-4a92-9723-e27964dfe759.value": [ + "f06086ba-6253-4da2-ad37-d12b4f64443c.value" + ], + "c9775f83-e7bc-4dc5-959c-bc2182a0d10b.value": [ + "88f6f84d-c3ef-4efa-ad22-e079cc3f10e5.value" ] }, "groups": [ { "title": "Input", - "x": 3, - "y": -1, - "width": 1450, - "height": 704, + "x": 1, + "y": -267, + "width": 1451, + "height": 966, "color": "#88A", "font_size": 24, "inherited": false }, { "title": "Process", - "x": 3, - "y": 713, - "width": 3586, - "height": 897, + "x": 1, + "y": 704, + "width": 2761, + "height": 882, "color": "#3f789e", "font_size": 24, "inherited": false @@ -769,12 +1062,32 @@ { "title": "Output", "x": 1459, - "y": -1, + "y": 259, "width": 640, "height": 436, "color": "#8A8", "font_size": 24, "inherited": false + }, + { + "title": "Process", + "x": 0, + "y": 1591, + "width": 1410, + "height": 817, + "color": "#3f789e", + "font_size": 24, + "inherited": false + }, + { + "title": "Process", + "x": -1, + "y": 2412, + "width": 1730, + "height": 542, + "color": "#3f789e", + "font_size": 24, + "inherited": false } ], "comments": [], diff --git a/src/talemate/agents/director/modules/critique-outline.json b/src/talemate/agents/director/modules/critique-outline.json index ec89b5db..5d9cffac 100644 --- a/src/talemate/agents/director/modules/critique-outline.json +++ b/src/talemate/agents/director/modules/critique-outline.json @@ -161,7 +161,7 @@ "base_type": "core/DynamicSocketNodeBase" }, "6afa0a64-551b-4724-85f4-53a5bdebbac9": { - "title": "Generate Response", + "title": "Critique Outline", "id": "6afa0a64-551b-4724-85f4-53a5bdebbac9", "properties": { "data_output": false, diff --git a/src/talemate/agents/director/modules/director-action-direct-story-arc.json b/src/talemate/agents/director/modules/director-action-direct-story-arc.json index 4a4987cd..ba29d1ba 100644 --- a/src/talemate/agents/director/modules/director-action-direct-story-arc.json +++ b/src/talemate/agents/director/modules/director-action-direct-story-arc.json @@ -169,28 +169,15 @@ }, "x": 570, "y": 320, - "width": 212, - "height": 158, + "width": 220, + "height": 178, "collapsed": false, "inherited": false, "registry": "agents/director/plan/ExpandStoryArc", "base_type": "core/Node" }, - "dc9bff13-a1bb-46a7-9159-090287494766": { - "title": "Director Settings", - "id": "dc9bff13-a1bb-46a7-9159-090287494766", - "properties": {}, - "x": 10, - "y": 760, - "width": 363, - "height": 926, - "collapsed": true, - "inherited": false, - "registry": "agents/director/Settings", - "base_type": "core/Node" - }, "fe4b0231-b5ec-4dd2-9ff5-8e3b2bfad061": { - "title": "chat_generate_arc_expand_chunk_size", + "title": "plan_expand_chunk_size", "id": "fe4b0231-b5ec-4dd2-9ff5-8e3b2bfad061", "properties": {}, "x": 200, @@ -216,6 +203,47 @@ "inherited": false, "registry": "data/number/AsNumber", "base_type": "core/Node" + }, + "08c18906-862b-4450-a1b1-71937b095717": { + "title": "plan_expand_critique", + "id": "08c18906-862b-4450-a1b1-71937b095717", + "properties": {}, + "x": 230, + "y": 850, + "width": 168, + "height": 26, + "collapsed": false, + "inherited": false, + "registry": "core/Watch", + "base_type": "core/Node" + }, + "673c3bef-fe28-409e-8f3a-1be74aa33780": { + "title": "As Bool", + "id": "673c3bef-fe28-409e-8f3a-1be74aa33780", + "properties": { + "default": false + }, + "x": 450, + "y": 870, + "width": 210, + "height": 58, + "collapsed": true, + "inherited": false, + "registry": "core/AsBool", + "base_type": "core/Node" + }, + "dc9bff13-a1bb-46a7-9159-090287494766": { + "title": "Director Settings", + "id": "dc9bff13-a1bb-46a7-9159-090287494766", + "properties": {}, + "x": 10, + "y": 770, + "width": 363, + "height": 986, + "collapsed": true, + "inherited": false, + "registry": "agents/director/Settings", + "base_type": "core/Node" } }, "edges": { @@ -252,14 +280,23 @@ "a30b01b3-84da-46c0-8783-785660b85ccb.result": [ "b12ebc94-8767-4ce4-9d97-1f8adfa8ae66.value" ], - "dc9bff13-a1bb-46a7-9159-090287494766.chat_generate_arc_expand_chunk_size": [ - "fe4b0231-b5ec-4dd2-9ff5-8e3b2bfad061.value" - ], "fe4b0231-b5ec-4dd2-9ff5-8e3b2bfad061.value": [ "9b188432-1182-4fe0-82d4-28ad012a7a9a.value" ], "9b188432-1182-4fe0-82d4-28ad012a7a9a.value": [ "a30b01b3-84da-46c0-8783-785660b85ccb.chunk_size" + ], + "08c18906-862b-4450-a1b1-71937b095717.value": [ + "673c3bef-fe28-409e-8f3a-1be74aa33780.value" + ], + "673c3bef-fe28-409e-8f3a-1be74aa33780.value": [ + "a30b01b3-84da-46c0-8783-785660b85ccb.expand_critique" + ], + "dc9bff13-a1bb-46a7-9159-090287494766.plan_expand_chunk_size": [ + "fe4b0231-b5ec-4dd2-9ff5-8e3b2bfad061.value" + ], + "dc9bff13-a1bb-46a7-9159-090287494766.plan_expand_critique": [ + "08c18906-862b-4450-a1b1-71937b095717.value" ] }, "groups": [ @@ -277,8 +314,8 @@ "title": "Process", "x": -2, "y": 226, - "width": 1051, - "height": 574, + "width": 1050, + "height": 675, "color": "#3f789e", "font_size": 24, "inherited": false @@ -290,28 +327,28 @@ "inputs": [], "outputs": [ { - "id": "3d11a687-076d-4645-9bfc-43e63daf0739", + "id": "37499312-00a2-4f6f-862e-63372a020be1", "name": "fn", "optional": false, "group": null, "socket_type": "function" }, { - "id": "0901e75e-4235-484a-a5ed-3bdf1bd83e52", + "id": "deea07dc-3fde-475f-b6f1-db7a4e723862", "name": "name", "optional": false, "group": null, "socket_type": "str" }, { - "id": "728a9946-813f-4125-bffb-153eeaa3e2fb", + "id": "235121d0-ddca-48e0-828c-e4f8c0fe1602", "name": "allow_multiple_calls", "optional": false, "group": null, "socket_type": "bool" }, { - "id": "26402d15-3605-4ae7-aa5a-c909b2e13825", + "id": "bcbb8f4e-0ad8-48ae-b874-3d7615c4d3a1", "name": "ai_callback", "optional": false, "group": null, diff --git a/src/talemate/agents/director/modules/manage-plan.json b/src/talemate/agents/director/modules/manage-plan.json index 4a332596..e9000879 100644 --- a/src/talemate/agents/director/modules/manage-plan.json +++ b/src/talemate/agents/director/modules/manage-plan.json @@ -149,8 +149,8 @@ "typ": "int", "instructions": "The exact number of beats to generate. Each beat represents one narrator or conversation generation turn." }, - "x": -1367, - "y": 747, + "x": -1722, + "y": 642, "width": 210, "height": 106, "collapsed": false, @@ -166,8 +166,8 @@ "typ": "str", "instructions": "What the scene outline should cover. Include the setting, characters involved, tone, themes, and any specific events or arcs." }, - "x": -1367, - "y": 587, + "x": -1722, + "y": 482, "width": 210, "height": 106, "collapsed": false, @@ -191,8 +191,8 @@ } ] }, - "x": -560, - "y": 650, + "x": -546, + "y": 542, "width": 244, "height": 82, "collapsed": false, @@ -308,21 +308,6 @@ "registry": "agents/director/chat/DirectorChatSubAction", "base_type": "core/Node" }, - "506d845f-e842-4e95-9285-2ad407a2146e": { - "title": "DEF create_outline", - "id": "506d845f-e842-4e95-9285-2ad407a2146e", - "properties": { - "name": "create_outline" - }, - "x": -270, - "y": 650, - "width": 210, - "height": 78, - "collapsed": false, - "inherited": false, - "registry": "core/functions/DefineFunction", - "base_type": "core/Node" - }, "32c778ed-68e4-4039-9b95-03726c146323": { "title": "DEF create_plan", "id": "32c778ed-68e4-4039-9b95-03726c146323", @@ -861,46 +846,12 @@ "registry": "focal/Argument", "base_type": "core/Node" }, - "651a5f02-fd36-4da3-8e6a-80d18c415f22": { - "title": "Create Outline", - "id": "651a5f02-fd36-4da3-8e6a-80d18c415f22", - "properties": {}, - "x": -1053, - "y": 646, - "width": 212, - "height": 106, - "collapsed": false, - "inherited": false, - "registry": "agents/director/plan/CreateOutline", - "base_type": "core/Graph" - }, - "b52af225-f7f7-4466-b27e-3a96b591eb26": { - "title": "Jinja2 Format", - "id": "b52af225-f7f7-4466-b27e-3a96b591eb26", - "properties": { - "template": "Plan ID: {{ plan_id }}" - }, - "x": -970, - "y": 800, - "width": 210, - "height": 153, - "collapsed": true, - "inherited": false, - "registry": "prompt/Jinja2Format", - "dynamic_inputs": [ - { - "name": "item0", - "type": "*" - } - ], - "base_type": "core/DynamicSocketNodeBase" - }, "ce9a5ea3-57cf-4323-8c61-b32a47c310f6": { "title": "Return", "id": "ce9a5ea3-57cf-4323-8c61-b32a47c310f6", "properties": {}, - "x": -780, - "y": 680, + "x": -766, + "y": 542, "width": 140, "height": 26, "collapsed": false, @@ -944,6 +895,81 @@ } ], "base_type": "core/DynamicSocketNodeBase" + }, + "506d845f-e842-4e95-9285-2ad407a2146e": { + "title": "DEF create_outline", + "id": "506d845f-e842-4e95-9285-2ad407a2146e", + "properties": { + "name": "create_outline" + }, + "x": -256, + "y": 542, + "width": 210, + "height": 78, + "collapsed": false, + "inherited": false, + "registry": "core/functions/DefineFunction", + "base_type": "core/Node" + }, + "651a5f02-fd36-4da3-8e6a-80d18c415f22": { + "title": "Create Outline", + "id": "651a5f02-fd36-4da3-8e6a-80d18c415f22", + "properties": {}, + "x": -1206, + "y": 542, + "width": 212, + "height": 126, + "collapsed": false, + "inherited": false, + "registry": "agents/director/plan/CreateOutline", + "base_type": "core/Graph" + }, + "50a06e8b-0b1f-4794-ac11-5b76b6d0a341": { + "title": "plan_outline_critique", + "id": "50a06e8b-0b1f-4794-ac11-5b76b6d0a341", + "properties": {}, + "x": -1446, + "y": 722, + "width": 176, + "height": 26, + "collapsed": false, + "inherited": false, + "registry": "core/Watch", + "base_type": "core/Node" + }, + "0680094e-85a3-4704-a2c5-2a6b9c60085d": { + "title": "Director Settings", + "id": "0680094e-85a3-4704-a2c5-2a6b9c60085d", + "properties": {}, + "x": -1722, + "y": 832, + "width": 363, + "height": 986, + "collapsed": true, + "inherited": false, + "registry": "agents/director/Settings", + "base_type": "core/Node" + }, + "b52af225-f7f7-4466-b27e-3a96b591eb26": { + "title": "Jinja2 Format", + "id": "b52af225-f7f7-4466-b27e-3a96b591eb26", + "properties": { + "template": "Plan ID: {{ plan_id }}" + }, + "x": -933, + "y": 542, + "width": 210, + "height": 153, + "collapsed": true, + "inherited": false, + "registry": "prompt/Jinja2Format", + "dynamic_inputs": [ + { + "name": "item0", + "type": "*" + } + ], + "base_type": "core/DynamicSocketNodeBase" } }, "edges": { @@ -1094,12 +1120,6 @@ "c9309352-4080-401f-8e7a-8f82d4cb0cd1.state", "c9309352-4080-401f-8e7a-8f82d4cb0cd1.task_id" ], - "651a5f02-fd36-4da3-8e6a-80d18c415f22.plan_id": [ - "b52af225-f7f7-4466-b27e-3a96b591eb26.item0" - ], - "b52af225-f7f7-4466-b27e-3a96b591eb26.result": [ - "ce9a5ea3-57cf-4323-8c61-b32a47c310f6.value" - ], "ce9a5ea3-57cf-4323-8c61-b32a47c310f6.value": [ "1f391a2f-a696-44fd-8dbb-951f6612d627.state" ], @@ -1111,15 +1131,27 @@ ], "acfef6ab-44e3-4a82-8fef-ff6428374181.result": [ "bb0aaccf-8d2e-4b7a-9536-ac729b8e02dc.value" + ], + "651a5f02-fd36-4da3-8e6a-80d18c415f22.plan_id": [ + "b52af225-f7f7-4466-b27e-3a96b591eb26.item0" + ], + "50a06e8b-0b1f-4794-ac11-5b76b6d0a341.value": [ + "651a5f02-fd36-4da3-8e6a-80d18c415f22.critique" + ], + "0680094e-85a3-4704-a2c5-2a6b9c60085d.plan_outline_critique": [ + "50a06e8b-0b1f-4794-ac11-5b76b6d0a341.value" + ], + "b52af225-f7f7-4466-b27e-3a96b591eb26.result": [ + "ce9a5ea3-57cf-4323-8c61-b32a47c310f6.value" ] }, "groups": [ { "title": "Function", - "x": -1388, - "y": 521, - "width": 1366, - "height": 365, + "x": -1747, + "y": 402, + "width": 1726, + "height": 484, "color": "#b06634", "font_size": 24, "inherited": false @@ -1191,28 +1223,28 @@ "inputs": [], "outputs": [ { - "id": "e6602099-1ace-47f3-9f17-bbc3c7dea5f6", + "id": "161139d1-8eb6-4a7d-89d4-00cefc247065", "name": "fn", "optional": false, "group": null, "socket_type": "function" }, { - "id": "353a9c7c-4b64-4273-ba85-0461105f7d56", + "id": "03903a3d-5342-4b07-859f-70cb10384b7d", "name": "name", "optional": false, "group": null, "socket_type": "str" }, { - "id": "c5e18533-c2a3-4d0e-a927-0c7f11db3d99", + "id": "1c5b36f4-cd45-4b3b-9fdc-0602f0a76330", "name": "allow_multiple_calls", "optional": false, "group": null, "socket_type": "bool" }, { - "id": "8f932bf2-9c66-4ec9-8d91-a0ec166364f9", + "id": "b632a9f9-c4ae-4157-ae24-d0a8490de4b9", "name": "ai_callback", "optional": false, "group": null, diff --git a/src/talemate/agents/director/plan/__init__.py b/src/talemate/agents/director/plan/__init__.py index 847c39a2..8ee8e386 100644 --- a/src/talemate/agents/director/plan/__init__.py +++ b/src/talemate/agents/director/plan/__init__.py @@ -6,4 +6,5 @@ The director creates plans with tasks, then uses existing actions to execute the from .schema import Task, Beat, Plan, PlanStatus # noqa: F401 from .util import get_plan, save_plan, delete_plan, complete_task, parse_beats # noqa: F401 -from .expand import expand_beats # noqa: F401 +from .expand import compute_chunks, compute_arc_info # noqa: F401 +from .mixin import PlanMixin # noqa: F401 diff --git a/src/talemate/agents/director/plan/expand.py b/src/talemate/agents/director/plan/expand.py index ff567655..87f33f84 100644 --- a/src/talemate/agents/director/plan/expand.py +++ b/src/talemate/agents/director/plan/expand.py @@ -1,26 +1,24 @@ """ -Arc expansion — expands plan beats into prose and pushes to scene history. +Arc expansion — pure functions for chunking and arc metadata computation. + +The expand pipeline methods live on PlanMixin (mixin.py). """ import re -import structlog import pydantic -from talemate.emit import emit -import talemate.emit.async_signals -from talemate.agents.narrator import NarratorAgentEmission -from talemate.prompts import Prompt -from talemate.scene_message import NarratorMessage, CharacterMessage from .schema import Beat -from .util import complete_task, emit_plan_updated, get_plan - -log = structlog.get_logger("talemate.agents.director.plan.expand") # Matches any opening block tag leaked into content -# TODO: this can't hardcode the tag style or names -- the template defines it _LEAKED_TAG_RE = re.compile(r" bool: + """Check if any extracted block contains raw block tags in its content.""" + return any(_LEAKED_TAG_RE.search(b.get("content", "")) for b in blocks) + + # Minimum beats per chunk when splitting at tension valleys MIN_CHUNK_BEATS = 3 @@ -120,194 +118,3 @@ def compute_arc_info( ) return infos - - -async def revise_narrator_content(narrator, content: str) -> str: - """Send narrator generated signal so automatic revision can process the content.""" - emission = NarratorAgentEmission(agent=narrator, response=content) - await talemate.emit.async_signals.get("agent.narrator.generated").send(emission) - return emission.response - - -async def critique_expanded_blocks( - blocks: list[dict], narrator -) -> list[dict]: - """ - Run a post-expansion critique pass on all blocks to fix cross-beat - redundancy, intensity monotony, and repeated vocabulary. - """ - log.info("expand.critique", block_count=len(blocks)) - - response, extracted = await Prompt.request( - "narrator.arc-expand-critique", - narrator.client, - "narrate_4096", - vars={ - "blocks": blocks, - "max_tokens": narrator.client.max_token_length, - "response_length": 4096, - }, - ) - - revised = extracted.get("response", []) - if not revised: - log.warning("expand.critique.no_blocks_returned, using originals") - return blocks - - log.info("expand.critique.done", original=len(blocks), revised=len(revised)) - return revised - - -async def push_and_emit_block(scene, block: dict, narrator) -> int: - """ - Push a single extracted block to scene history and emit to frontend. - - Runs automatic revision on narrator blocks. Returns word count of the block, - or 0 if the block was empty/skipped. - """ - content = block.get("content", "").strip() - if not content: - return 0 - - if block["type"] == "narrator": - content = await revise_narrator_content(narrator, content) - msg = NarratorMessage(content) - await scene.push_history(msg) - emit("narrator", msg) - elif block["type"] == "character": - char_name = block.get("name", "Unknown") - msg = CharacterMessage(f"{char_name}: {content}") - await scene.push_history(msg) - character = scene.get_character(char_name) - emit("character", message=msg, character=character) - else: - return 0 - - return len(content.split()) - - -async def expand_beats( - scene, - narrator, - beats: list[Beat], - plan_id: str, - perspective: str, - director_notes: str = "", - chunk_size: int = 3, - chat_id: str | None = None, -) -> tuple[int, int]: - """ - Expand beats into prose in chunks and push to scene history. - - Chunks are split at tension valleys when possible (deliberate chunking), - falling back to max chunk_size. Each chunk receives arc-position metadata - to guide pacing. - - Returns (total_blocks, total_words). - """ - total_words = 0 - total_blocks = 0 - preceding_text = "" - all_blocks: list[dict] = [] - - # Compute chunks and arc metadata - chunks = compute_chunks(beats, chunk_size) - arc_infos = compute_arc_info(chunks, beats) - - # Build a flat index to find following beats across chunk boundaries - beat_offset = 0 - - for chunk_num, (chunk_beats, arc_info) in enumerate( - zip(chunks, arc_infos), start=1 - ): - # Following beats: first 2 beats from the next chunk - following_beats = [] - next_offset = beat_offset + len(chunk_beats) - if next_offset < len(beats): - following_beats = beats[next_offset : next_offset + 2] - - log.info( - "expand.chunk", - chunk=chunk_num, - beats=f"{beat_offset + 1}-{beat_offset + len(chunk_beats)}", - total=len(beats), - position=arc_info.position, - tension=f"{arc_info.tension_range[0]:.1f}-{arc_info.tension_range[1]:.1f}", - ) - - # Call the expansion template with retry on malformed output - max_attempts = 3 - blocks = [] - prompt_vars = { - "scene": scene, - "max_tokens": narrator.client.max_token_length, - "beats": chunk_beats, - "following_beats": following_beats, - "preceding_text": preceding_text[-2000:] if preceding_text else "", - "perspective": perspective, - "director_notes": director_notes, - "extra_instructions": narrator.extra_instructions, - "response_length": 4096, - "arc_info": arc_info, - } - - for attempt in range(1, max_attempts + 1): - response, extracted = await Prompt.request( - "narrator.arc-expand", - narrator.client, - "narrate_4096", - vars=prompt_vars, - ) - - blocks = extracted.get("response", []) - if not blocks: - log.warning("expand.no_blocks", chunk=chunk_num, attempt=attempt) - continue - - # Validate: check for leaked block tags in content - has_leaked_tags = any( - _LEAKED_TAG_RE.search(b.get("content", "")) for b in blocks - ) - if not has_leaked_tags: - break - - log.warning("expand.leaked_tags", chunk=chunk_num, attempt=attempt) - blocks = [] - - if not blocks: - raise RuntimeError( - f"Expansion failed for chunk {chunk_num} after {max_attempts} attempts. " - f"The model produced malformed output with leaked block tags. " - f"This may indicate the model is too weak for structured generation — " - f"consider using a more capable model." - ) - - all_blocks.extend(blocks) - - # Accumulate preceding text for next chunk - chunk_text = "\n\n".join(b.get("content", "") for b in blocks) - preceding_text += "\n\n" + chunk_text - - beat_offset += len(chunk_beats) - - # Post-expansion critique pass - if len(chunks) > 1 and all_blocks: - all_blocks = await critique_expanded_blocks(all_blocks, narrator) - - # Push all blocks to scene history and emit to frontend - for block in all_blocks: - words = await push_and_emit_block(scene, block, narrator) - if words: - total_blocks += 1 - total_words += words - - # Mark all beats as completed - for beat in beats: - complete_task(scene, beat.id, plan_id=plan_id) - - # Emit plan update - plan = get_plan(scene, plan_id) - if plan: - emit_plan_updated(plan, chat_id=chat_id) - - return total_blocks, total_words diff --git a/src/talemate/agents/director/plan/mixin.py b/src/talemate/agents/director/plan/mixin.py new file mode 100644 index 00000000..d8a252d0 --- /dev/null +++ b/src/talemate/agents/director/plan/mixin.py @@ -0,0 +1,293 @@ +""" +Plan mixin — provides arc generation configuration and expand pipeline methods +to the director agent. +""" + +import structlog + +from talemate.agents.base import AgentAction, AgentActionConfig +from talemate.emit import emit +import talemate.emit.async_signals +from talemate.agents.narrator import NarratorAgentEmission +from talemate.prompts import Prompt +from talemate.scene_message import NarratorMessage, CharacterMessage + +from .expand import ( + compute_chunks, + compute_arc_info, + has_leaked_tags, +) +from .schema import Beat +from .util import complete_task, emit_plan_updated, get_plan + +log = structlog.get_logger("talemate.agents.director.plan.mixin") + + +class PlanMixin: + """ + Agent mixin for arc generation planning and expansion. + + Provides: + - Configuration for expand chunk size, dialogue ratio, critique toggles + - The expand pipeline: chunked expansion with arc metadata + - Post-expansion critique pass + """ + + @classmethod + def add_plan_actions(cls, actions: dict[str, AgentAction]): + actions["plan"] = AgentAction( + enabled=True, + container=True, + can_be_disabled=False, + label="Arc Generation", + icon="mdi-movie-open", + description="Settings for the arc generation pipeline.", + config={ + "dialogue_ratio": AgentActionConfig( + type="number", + label="Dialogue beat ratio", + description="Target fraction of beats that should be dialogue during arc generation. 0.4 = 40% dialogue beats.", + value=0.4, + step=0.1, + min=0.0, + max=1.0, + ), + "expand_chunk_size": AgentActionConfig( + type="number", + label="Expand chunk size", + description="Maximum number of beats per expansion chunk in expand mode. Larger chunks produce more cohesive prose but use more context.", + value=5, + step=1, + min=3, + max=12, + ), + "outline_critique": AgentActionConfig( + type="bool", + label="Outline critique", + description="Run a critique pass on the generated outline to improve beat quality before expansion.", + value=True, + ), + "expand_critique": AgentActionConfig( + type="bool", + label="Expansion critique", + description="Run a post-expansion critique pass to fix cross-beat redundancy and intensity monotony. Adds ~10s per generation.", + value=True, + ), + }, + ) + + # === Config property helpers === + + @property + def plan_dialogue_ratio(self) -> float: + return float(self.actions["plan"].config["dialogue_ratio"].value) + + @property + def plan_expand_chunk_size(self) -> int: + return int(self.actions["plan"].config["expand_chunk_size"].value) + + @property + def plan_outline_critique(self) -> bool: + return bool(self.actions["plan"].config["outline_critique"].value) + + @property + def plan_expand_critique(self) -> bool: + return bool(self.actions["plan"].config["expand_critique"].value) + + # === Expand pipeline methods === + + async def _plan_revise_narrator_content(self, narrator, content: str) -> str: + """Send narrator generated signal so automatic revision can process the content.""" + emission = NarratorAgentEmission(agent=narrator, response=content) + await talemate.emit.async_signals.get("agent.narrator.generated").send( + emission + ) + return emission.response + + async def _plan_push_and_emit_block(self, scene, block: dict, narrator) -> int: + """ + Push a single extracted block to scene history and emit to frontend. + + Runs automatic revision on narrator blocks. Returns word count of the block, + or 0 if the block was empty/skipped. + """ + content = block.get("content", "").strip() + if not content: + return 0 + + if block["type"] == "narrator": + content = await self._plan_revise_narrator_content(narrator, content) + msg = NarratorMessage(content) + await scene.push_history(msg) + emit("narrator", msg) + elif block["type"] == "character": + char_name = block.get("name", "Unknown") + msg = CharacterMessage(f"{char_name}: {content}") + await scene.push_history(msg) + character = scene.get_character(char_name) + emit("character", message=msg, character=character) + else: + return 0 + + return len(content.split()) + + async def _plan_critique_expanded_blocks( + self, blocks: list[dict], narrator + ) -> list[dict]: + """ + Run a post-expansion critique pass on all blocks to fix cross-beat + redundancy, intensity monotony, and repeated vocabulary. + """ + log.info("expand.critique", block_count=len(blocks)) + + response, extracted = await Prompt.request( + "narrator.arc-expand-critique", + narrator.client, + "narrate_4096", + vars={ + "blocks": blocks, + "max_tokens": narrator.client.max_token_length, + "response_length": 4096, + }, + ) + + revised = extracted.get("response", []) + if not revised: + log.warning("expand.critique.no_blocks_returned", fallback="using originals") + return blocks + + log.info("expand.critique.done", original=len(blocks), revised=len(revised)) + return revised + + async def plan_expand_beats( + self, + scene, + narrator, + beats: list[Beat], + plan_id: str, + perspective: str, + director_notes: str = "", + chunk_size: int | None = None, + chat_id: str | None = None, + critique: bool | None = None, + ) -> tuple[int, int]: + """ + Expand beats into prose in chunks and push to scene history. + + Chunks are split at tension valleys when possible (deliberate chunking), + falling back to max chunk_size. Each chunk receives arc-position metadata + to guide pacing. + + Returns (total_blocks, total_words). + """ + if chunk_size is None: + chunk_size = self.plan_expand_chunk_size + + total_words = 0 + total_blocks = 0 + preceding_text = "" + all_blocks: list[dict] = [] + + # Compute chunks and arc metadata + chunks = compute_chunks(beats, chunk_size) + arc_infos = compute_arc_info(chunks, beats) + + # Build a flat index to find following beats across chunk boundaries + beat_offset = 0 + + for chunk_num, (chunk_beats, arc_info) in enumerate( + zip(chunks, arc_infos), start=1 + ): + # Following beats: first 2 beats from the next chunk + following_beats = [] + next_offset = beat_offset + len(chunk_beats) + if next_offset < len(beats): + following_beats = beats[next_offset : next_offset + 2] + + log.info( + "expand.chunk", + chunk=chunk_num, + beats=f"{beat_offset + 1}-{beat_offset + len(chunk_beats)}", + total=len(beats), + position=arc_info.position, + tension=f"{arc_info.tension_range[0]:.1f}-{arc_info.tension_range[1]:.1f}", + ) + + # Call the expansion template with retry on malformed output + max_attempts = 3 + blocks = [] + prompt_vars = { + "scene": scene, + "max_tokens": narrator.client.max_token_length, + "beats": chunk_beats, + "following_beats": following_beats, + "preceding_text": preceding_text[-2000:] if preceding_text else "", + "perspective": perspective, + "director_notes": director_notes, + "extra_instructions": narrator.extra_instructions, + "response_length": 4096, + "arc_info": arc_info, + } + + for attempt in range(1, max_attempts + 1): + response, extracted = await Prompt.request( + "narrator.arc-expand", + narrator.client, + "narrate_4096", + vars=prompt_vars, + ) + + blocks = extracted.get("response", []) + if not blocks: + log.warning( + "expand.no_blocks", chunk=chunk_num, attempt=attempt + ) + continue + + # Validate: check for leaked block tags in content + if not has_leaked_tags(blocks): + break + + log.warning( + "expand.leaked_tags", chunk=chunk_num, attempt=attempt + ) + blocks = [] + + if not blocks: + raise RuntimeError( + f"Expansion failed for chunk {chunk_num} after {max_attempts} attempts. " + f"The model produced malformed output with leaked block tags. " + f"This may indicate the model is too weak for structured generation — " + f"consider using a more capable model." + ) + + all_blocks.extend(blocks) + + # Accumulate preceding text for next chunk + chunk_text = "\n\n".join(b.get("content", "") for b in blocks) + preceding_text += "\n\n" + chunk_text + + beat_offset += len(chunk_beats) + + # Post-expansion critique pass + do_critique = critique if critique is not None else self.plan_expand_critique + if do_critique and len(chunks) > 1 and all_blocks: + all_blocks = await self._plan_critique_expanded_blocks(all_blocks, narrator) + + # Push all blocks to scene history and emit to frontend + for block in all_blocks: + words = await self._plan_push_and_emit_block(scene, block, narrator) + if words: + total_blocks += 1 + total_words += words + + # Mark all beats as completed + for beat in beats: + complete_task(scene, beat.id, plan_id=plan_id) + + # Emit plan update + plan = get_plan(scene, plan_id) + if plan: + emit_plan_updated(plan, chat_id=chat_id) + + return total_blocks, total_words diff --git a/src/talemate/agents/director/plan/nodes.py b/src/talemate/agents/director/plan/nodes.py index 5dfc2cae..07502273 100644 --- a/src/talemate/agents/director/plan/nodes.py +++ b/src/talemate/agents/director/plan/nodes.py @@ -33,7 +33,6 @@ from .schema import ( NARRATION_BEAT_RATIO, ) from .util import save_plan, complete_task, emit_plan_updated, get_plan -from .expand import expand_beats log = structlog.get_logger("talemate.agents.director.plan.nodes") @@ -327,8 +326,8 @@ class ExpandStoryArc(AgentNode): """ Expands a plan's beats into full prose and pushes them to scene history. - Delegates to expand_beats() which handles chunking, template calls, - revision, emission, and task completion. + Delegates to the director's PlanMixin.expand_beats() which handles + chunking, template calls, revision, emission, and task completion. """ _agent_name: ClassVar[str] = "narrator" @@ -353,6 +352,7 @@ class ExpandStoryArc(AgentNode): self.add_input("perspective", socket_type="str") self.add_input("director_notes", socket_type="str", optional=True) self.add_input("chunk_size", socket_type="int", optional=True) + self.add_input("expand_critique", socket_type="bool", optional=True) self.set_property("chunk_size", 5) @@ -369,9 +369,10 @@ class ExpandStoryArc(AgentNode): chunk_size = self.normalized_input_value("chunk_size") or int( self.get_property("chunk_size") ) + expand_critique = self.normalized_input_value("expand_critique") scene = active_scene.get() - narrator = get_agent("narrator") + director = get_agent("director") if not beats: self.set_output_values( @@ -386,15 +387,16 @@ class ExpandStoryArc(AgentNode): chat_ctx = director_chat_context.get() chat_id = chat_ctx.chat_id if chat_ctx else None - total_blocks, total_words = await expand_beats( + total_blocks, total_words = await director.plan_expand_beats( scene=scene, - narrator=narrator, + narrator=get_agent("narrator"), beats=beats, plan_id=plan_id, perspective=perspective, director_notes=director_notes, chunk_size=chunk_size, chat_id=chat_id, + critique=expand_critique, ) result = f"Generated {total_blocks} blocks, {total_words} words from {len(beats)} beats" diff --git a/src/talemate/prompts/templates/director/scene-plan-create-outline.jinja2 b/src/talemate/prompts/templates/director/scene-plan-create-outline.jinja2 index 62b2148e..6b1bdd7a 100644 --- a/src/talemate/prompts/templates/director/scene-plan-create-outline.jinja2 +++ b/src/talemate/prompts/templates/director/scene-plan-create-outline.jinja2 @@ -41,7 +41,7 @@ Guidelines: - The `tension` value must reflect genuine escalation. Early beats must have LOW tension (0.1-0.3). Save high tension (0.7+) for the latter portion. Do not start at high intensity. **Beat construction:** -{% set dialogue_pct = (agent_config("director.chat.generate_arc_dialogue_ratio") * 100) | int -%} +{% set dialogue_pct = (agent_config("director.plan.dialogue_ratio") * 100) | int -%} - Approximately {{ dialogue_pct }}% of beats should be `dialogue` type — characters need to speak, not just act and observe - Vary beat types for engaging pacing (don't stack 5 dialogue beats in a row) - Use `transition` beats for time/location changes diff --git a/src/talemate/prompts/templates/director/scene-plan-critique-outline.jinja2 b/src/talemate/prompts/templates/director/scene-plan-critique-outline.jinja2 index aba80840..c90d44f3 100644 --- a/src/talemate/prompts/templates/director/scene-plan-critique-outline.jinja2 +++ b/src/talemate/prompts/templates/director/scene-plan-critique-outline.jinja2 @@ -52,7 +52,7 @@ Every beat description must specify a concrete EVENT or ACTION — something tha **Structural completeness:** The final beat MUST involve a character making a choice, taking an action, or facing a consequence — not just witnessing a revelation or spectacle. If the last beat is purely "they see something terrifying/beautiful," rewrite it so the character DOES something in response that closes or pivots the arc. The arc needs somewhere to land, and that landing must involve character agency. -{% set dialogue_pct = (agent_config("director.chat.generate_arc_dialogue_ratio") * 100) | int -%} +{% set dialogue_pct = (agent_config("director.plan.dialogue_ratio") * 100) | int -%} **Dialogue ratio:** Approximately {{ dialogue_pct }}% of beats should be `dialogue` type. Characters need to speak, argue, and reveal information through conversation — not just act and observe. If the outline is heavily skewed toward narration, convert some beats to dialogue. **Perspective consistency:** All beat descriptions must be compatible with the stated perspective ({{ perspective }}). If the perspective is limited to one character, beats must not describe another character's internal thoughts. diff --git a/talemate_frontend/src/components/SceneToolsDirector.vue b/talemate_frontend/src/components/SceneToolsDirector.vue index 7d711c2d..18bafcce 100644 --- a/talemate_frontend/src/components/SceneToolsDirector.vue +++ b/talemate_frontend/src/components/SceneToolsDirector.vue @@ -130,6 +130,28 @@
Expand mode: beats are expanded into prose in chunks. Much faster but with less per-turn context injection.
+
+ + +
+
This may generate actions and dialogue for player controlled characters as well.
@@ -166,6 +188,8 @@ export default { scenePlanBeats: 8, scenePlanDialogueRatio: 40, scenePlanMode: 'generate_arc', + scenePlanOutlineCritique: true, + scenePlanExpandCritique: true, minRecommendedNarratorLength: 1024, } }, @@ -250,7 +274,9 @@ export default { this.scenePlanInstructions = ''; this.scenePlanBeats = 8; this.scenePlanMode = 'generate_arc'; - this.scenePlanDialogueRatio = Math.round((this.agentStatus?.director?.actions?.chat?.config?.generate_arc_dialogue_ratio?.value ?? 0.4) * 100); + this.scenePlanDialogueRatio = Math.round((this.agentStatus?.director?.actions?.plan?.config?.dialogue_ratio?.value ?? 0.4) * 100); + this.scenePlanOutlineCritique = this.agentStatus?.director?.actions?.plan?.config?.outline_critique?.value ?? true; + this.scenePlanExpandCritique = this.agentStatus?.director?.actions?.plan?.config?.expand_critique?.value ?? true; this.scenePlanDialog = true; }, @@ -263,6 +289,8 @@ export default { beat_count: this.scenePlanBeats, dialogue_ratio: this.scenePlanDialogueRatio / 100, mode: this.scenePlanMode, + outline_critique: this.scenePlanOutlineCritique, + expand_critique: this.scenePlanExpandCritique, })); this.openDirectorConsole(); }, diff --git a/tests/data/prompts/baselines/creator/determine_character_dialogue_instructions.txt b/tests/data/prompts/baselines/creator/determine_character_dialogue_instructions.txt index 057e072c..464e7cbf 100644 --- a/tests/data/prompts/baselines/creator/determine_character_dialogue_instructions.txt +++ b/tests/data/prompts/baselines/creator/determine_character_dialogue_instructions.txt @@ -23,7 +23,10 @@ The character MUST feel relatable to the audience. You must use simple language to describe the character's dialogue instructions. -Keep the format similar and stick to one paragraph. +**Structure your instructions in two short paragraphs:** + +1. **Default register** — How Elena normally speaks: vocabulary, formality, sentence structure, verbal habits, how they address others. +2. **Under pressure** — How their speech changes when stressed, scared, angry, or pushed past their limit. Do NOT write that they simply become "more of the same" (e.g., "gets even more clinical" or "becomes even quieter"). Their composure must crack in a specific, visible way — shorter sentences, different word choices, breaking their own verbal patterns, physical urgency replacing their normal mode. This variation is critical for believable dialogue in dramatic scenes. The length of your response must fit within 4 paragraphs. diff --git a/tests/data/prompts/baselines/director/arc_expand.txt b/tests/data/prompts/baselines/director/arc_expand.txt new file mode 100644 index 00000000..ebf074b9 --- /dev/null +++ b/tests/data/prompts/baselines/director/arc_expand.txt @@ -0,0 +1,104 @@ +## Context + +## Classification +Content Classification: Fantasy adventure story + +Narrative Perspective: + +Scenario Premise: +A peaceful clearing in the heart of an ancient forest. + +## Intention of this story +The overarching intention of this story. Use it to guide your decisions in the scene. + +A test story + +## Scene + +Elena: Hello there, traveler. +The sun filters through the leaves above. +Marcus: What brings you to these woods? + +## Characters + +### Hero +name: Hero + +A test character. + +#### General character guide for Hero +Speaks normally. + +### Elena +name: Elena + +A test character. + +#### General character guide for Elena +Speaks normally. + +## Director notes +Focus on building tension. + +## Writing style + +## Task +Write scene prose from the beat descriptions below. + +**Arc position:** OPENING (chunk 1 of 2, tension 0.3–0.7) + +You are writing the OPENING of the story. Establish the world and characters, build tension gradually. Do not peak yet — save the most intense moments for later. + +**Beats to write:** + +Beat 1 [narration] (pacing: slow, tension: 0.3) + Characters: Elena + The protagonist discovers the door is locked from the inside. + +Beat 2 [dialogue] (pacing: moderate, tension: 0.5) + Characters: Elena + Elena confronts Hero about what happened last night, demanding answers. + +Beat 3 [action] (pacing: fast, tension: 0.7) + Characters: Hero, Elena + A sudden noise from the basement forces both characters to investigate together. + +**OUTPUT FORMAT (mandatory):** + +Your response MUST be a sequence of `` and `` blocks. No prose outside of blocks. + +- Narration/action/transition/reveal beats → `` blocks (1-3 paragraphs of prose) +- Dialogue beats → `` blocks (action + body language + dialogue as one unit) + +Structure: + +``` + +... narration prose here (1-3 paragraphs) ... + + + +... physical action, body language, and dialogue together as one unit ... + + + +... more narration prose ... + + + +... his actions and dialogue as one unit ... + +``` + +**Writing rules:** +- `` blocks must be complete character moments: physical action, body language, AND dialogue together. Not just a quoted line. +- ONE strong image per paragraph. Develop it — let the reader sit with it before introducing the next. Do not stack multiple phenomena in rapid succession. +- Do NOT repeat sensory details or character descriptions already established in the "Story so far" section. +- Vary sentence length. Mix short and long. +- Show emotion through action, not narration. "His hands shook" not "he felt terror." +- Low-tension beats must use short sentences, plain language, breathing room. +- Do NOT start CHARACTER block content with the character's name — the tag already identifies them. +- **Dialogue variety:** Each character's dialogue must serve a DIFFERENT function across beats. Vary between: questioning, arguing, joking, commanding, confessing, deflecting, comforting, provoking. A character must NOT deliver the same type of speech twice. If a character made a philosophical declaration in one beat, their next line must be a question, a joke, or a pragmatic instruction — anything except another declaration. + +The length of your response must fit within 4 paragraphs. +<|BOT|> \ No newline at end of file diff --git a/tests/data/prompts/baselines/director/arc_expand__with_preceding_text.txt b/tests/data/prompts/baselines/director/arc_expand__with_preceding_text.txt new file mode 100644 index 00000000..0e895cc4 --- /dev/null +++ b/tests/data/prompts/baselines/director/arc_expand__with_preceding_text.txt @@ -0,0 +1,104 @@ +## Context + +## Classification +Content Classification: Fantasy adventure story + +Narrative Perspective: + +Scenario Premise: +A peaceful clearing in the heart of an ancient forest. + +## Intention of this story +The overarching intention of this story. Use it to guide your decisions in the scene. + +A test story + +## Scene + +Elena: Hello there, traveler. +The sun filters through the leaves above. +Marcus: What brings you to these woods? + +## Characters + +### Hero +name: Hero + +A test character. + +#### General character guide for Hero +Speaks normally. + +### Elena +name: Elena + +A test character. + +#### General character guide for Elena +Speaks normally. + +## Writing style + +## Task +Write scene prose from the beat descriptions below. + +**Arc position:** CLIMAX (chunk 2 of 2, tension 0.5–0.7) + +You are writing the CLIMAX — the dramatic high point. Commit fully to the peak intensity. This is where the biggest moments happen. + +**Story so far** (continue from here): +``` +The door creaked open, revealing an empty room. Elena stepped inside cautiously. +``` + +CRITICAL: Do NOT repeat, rephrase, or re-describe ANY event, action, dialogue, or image from the story so far. If a beat description asks for something that already happened above, find a NEW angle or skip to the next development. The reader has already seen it. + +**Beats to write:** + +Beat 2 [dialogue] (pacing: moderate, tension: 0.5) + Characters: Elena + Elena confronts Hero about what happened last night, demanding answers. + +Beat 3 [action] (pacing: fast, tension: 0.7) + Characters: Hero, Elena + A sudden noise from the basement forces both characters to investigate together. + +**OUTPUT FORMAT (mandatory):** + +Your response MUST be a sequence of `` and `` blocks. No prose outside of blocks. + +- Narration/action/transition/reveal beats → `` blocks (1-3 paragraphs of prose) +- Dialogue beats → `` blocks (action + body language + dialogue as one unit) + +Structure: + +``` + +... narration prose here (1-3 paragraphs) ... + + + +... physical action, body language, and dialogue together as one unit ... + + + +... more narration prose ... + + + +... his actions and dialogue as one unit ... + +``` + +**Writing rules:** +- `` blocks must be complete character moments: physical action, body language, AND dialogue together. Not just a quoted line. +- ONE strong image per paragraph. Develop it — let the reader sit with it before introducing the next. Do not stack multiple phenomena in rapid succession. +- Do NOT repeat sensory details or character descriptions already established in the "Story so far" section. +- Vary sentence length. Mix short and long. +- Show emotion through action, not narration. "His hands shook" not "he felt terror." +- Low-tension beats must use short sentences, plain language, breathing room. +- Do NOT start CHARACTER block content with the character's name — the tag already identifies them. +- **Dialogue variety:** Each character's dialogue must serve a DIFFERENT function across beats. Vary between: questioning, arguing, joking, commanding, confessing, deflecting, comforting, provoking. A character must NOT deliver the same type of speech twice. If a character made a philosophical declaration in one beat, their next line must be a question, a joke, or a pragmatic instruction — anything except another declaration. + +The length of your response must fit within 4 paragraphs. +<|BOT|> \ No newline at end of file diff --git a/tests/data/prompts/baselines/director/arc_expand_critique.txt b/tests/data/prompts/baselines/director/arc_expand_critique.txt new file mode 100644 index 00000000..09baf35e --- /dev/null +++ b/tests/data/prompts/baselines/director/arc_expand_critique.txt @@ -0,0 +1,33 @@ +## Task +You are an editor reviewing the following scene prose. Your job is to revise it to fix these specific problems: + +1. **Repeated imagery/vocabulary:** If the same image, metaphor, or sensory detail appears in multiple blocks (e.g., "pulsing" used 5 times, "copper taste" in 3 blocks, the ship "dissolving" described identically twice), rewrite the later occurrences to use different language or cut them entirely. + +2. **Intensity monotony:** If every block is at maximum intensity with no breathing room, revise 1-2 blocks to be quieter — shorter sentences, less sensory detail, a moment of stillness or thought. + +3. **Repeated dialogue structure:** If a character delivers the same type of speech multiple times (e.g., two exposition monologues with the same structure), rewrite one to be shorter, more fragmented, or serve a different function. + +**Rules:** +- Keep all plot events intact — do NOT change what happens, only how it's written. +- Keep the same number of blocks in the same order. +- Keep block types (NARRATOR/CHARACTER) and character names unchanged. +- Your revisions must be subtle — polish, not rewrite. Cut redundant sentences, vary repeated words, adjust intensity. Do not add new plot elements. + +**Original prose:** + + +The room was dark and cold. A chill ran down her spine. + + + +She stepped forward, her hands trembling. "Who's there?" she whispered. + + + +A chill ran through the room. The darkness pressed in from all sides. + + +Output the revised version using the same `` and `` block format. Every block from the original must appear in your output. + +The length of your response must fit within 4 paragraphs. +<|BOT|> \ No newline at end of file diff --git a/tests/data/prompts/baselines/editor/revision_unslop.txt b/tests/data/prompts/baselines/editor/revision_unslop.txt index dd4b137a..ceb55ed8 100644 --- a/tests/data/prompts/baselines/editor/revision_unslop.txt +++ b/tests/data/prompts/baselines/editor/revision_unslop.txt @@ -1,27 +1,5 @@ ## Examples - -The incandescent luminescence of twilight's dying breath painted the heavens in hues of amethyst and vermillion, while shadows danced their ancient waltz across the emerald tapestry of the lawn. Rebecca's heart thundered like a thousand war drums as she glimpsed his silhouette, a dark prince emerging from the veil of dusk's embrace. - - - -- Thought process: This is trying way too hard to describe a simple sunset scene. Every single phrase is overwritten with metaphors and fancy vocabulary when it could just say "sunset" and "she saw him." -- Purple prose: MAJOR ISSUES - "incandescent luminescence," "twilight's dying breath," "ancient waltz," "thousand war drums," "dark prince" -- Unnatural dialogue: N/A - no dialogue -- Over-description: YES - simple sunset/shadow scene made overly complex -- Length: TOO LONG - could be said in 1-2 sentences -- Tense: PAST - "Rebecca's heart thundered like a thousand war drums" -- Mature content: N/A -- Name overuse: N/A - no dialogue -- Talking vs Showing: N/A - no dialogue - - - -The sunset cast purple and red across the sky as shadows stretched across the lawn. Rebecca's heart raced when she saw him walking up the driveway. - - ---- - "Hello, Mother. I have returned from my educational institution," said Tim. "Excellent. Did you successfully complete your mathematical assignments?" she responded. @@ -50,28 +28,6 @@ The sunset cast purple and red across the sky as shadows stretched across the la --- - -She reaches for the door handle. Her index finger extends first, followed by her middle finger, then her ring finger, and finally her pinky. Her thumb moves to oppose them. The metal is cool to the touch, approximately 68 degrees Fahrenheit. She grips it with 3.2 pounds of pressure and rotates her wrist 47 degrees clockwise. - - - -- Thought process: While the exact measurements and finger-by-finger description is excessive, we should keep that the handle was cold - it's a sensory detail that adds atmosphere without being purple prose. -- Purple prose: NO -- Unnatural dialogue: N/A - no dialogue -- Over-description: EXTREME - every finger movement, exact temperature, precise measurements -- Length: WAY TOO LONG - should be 3-4 words -- Tense: PRESENT - "reaches for the door handle" -- Mature content: N/A -- Name overuse: N/A -- Talking vs Showing: N/A - no dialogue - - - -She gripped the cold metal handle and turned it, opening the door. - - ---- - Beneath the opalescent moon's ethereal glow, Jake's sapphire eyes met hers. "Your presence illuminates my existence more brilliantly than a thousand supernovas," he declared. @@ -210,6 +166,31 @@ She bit her lip, looking away. "It's just... I mean... um... something feels wro --- + +"As you know, the dual-phase cooling system requires constant monitoring to prevent a cascade failure in the secondary loop," Dr. Chen explained to her co-engineer, who had been maintaining the reactor for twelve years. "The isothermal regulators must be calibrated every six hours, and the backup condensers need to cycle through their purge sequence before any power increase can be authorized." +"Indeed, I am well aware of these procedures," he responded. "Perhaps we should also discuss the implications of the recent firmware update on the thermal management subsystem." + + + +- Thought process: This is exposition dumped into dialogue. These are two engineers who both know this information — they wouldn't explain it to each other. The dialogue exists purely to inform the reader, not because the characters would actually say it. It needs to be replaced with what they'd actually discuss: the immediate problem, not the textbook explanation. +- Purple prose: NO +- Unnatural dialogue: SEVERE - characters explaining things they both already know +- Over-description: YES - unnecessary technical detail +- Length: TOO LONG +- Tense: PAST - "explained" +- Mature content: N/A +- Name overuse: NO +- Talking vs Showing: SEVERE - lecturing the reader through dialogue + + + +"The cooling readings look wrong again." Dr. Chen frowned at her display, tapping one of the fluctuating values. +He leaned over her shoulder. "That started after the firmware update. Want me to roll it back?" +"Not yet. Let me check the secondary loop first." + + +--- + "Hi David, how are you today?" asked Emily. "I'm doing well, Emily. Have you seen Michael?" diff --git a/tests/data/prompts/baselines_cached/creator/determine_character_dialogue_instructions.txt b/tests/data/prompts/baselines_cached/creator/determine_character_dialogue_instructions.txt index 057e072c..464e7cbf 100644 --- a/tests/data/prompts/baselines_cached/creator/determine_character_dialogue_instructions.txt +++ b/tests/data/prompts/baselines_cached/creator/determine_character_dialogue_instructions.txt @@ -23,7 +23,10 @@ The character MUST feel relatable to the audience. You must use simple language to describe the character's dialogue instructions. -Keep the format similar and stick to one paragraph. +**Structure your instructions in two short paragraphs:** + +1. **Default register** — How Elena normally speaks: vocabulary, formality, sentence structure, verbal habits, how they address others. +2. **Under pressure** — How their speech changes when stressed, scared, angry, or pushed past their limit. Do NOT write that they simply become "more of the same" (e.g., "gets even more clinical" or "becomes even quieter"). Their composure must crack in a specific, visible way — shorter sentences, different word choices, breaking their own verbal patterns, physical urgency replacing their normal mode. This variation is critical for believable dialogue in dramatic scenes. The length of your response must fit within 4 paragraphs. diff --git a/tests/data/prompts/baselines_cached/editor/revision_unslop.txt b/tests/data/prompts/baselines_cached/editor/revision_unslop.txt index dd4b137a..ceb55ed8 100644 --- a/tests/data/prompts/baselines_cached/editor/revision_unslop.txt +++ b/tests/data/prompts/baselines_cached/editor/revision_unslop.txt @@ -1,27 +1,5 @@ ## Examples - -The incandescent luminescence of twilight's dying breath painted the heavens in hues of amethyst and vermillion, while shadows danced their ancient waltz across the emerald tapestry of the lawn. Rebecca's heart thundered like a thousand war drums as she glimpsed his silhouette, a dark prince emerging from the veil of dusk's embrace. - - - -- Thought process: This is trying way too hard to describe a simple sunset scene. Every single phrase is overwritten with metaphors and fancy vocabulary when it could just say "sunset" and "she saw him." -- Purple prose: MAJOR ISSUES - "incandescent luminescence," "twilight's dying breath," "ancient waltz," "thousand war drums," "dark prince" -- Unnatural dialogue: N/A - no dialogue -- Over-description: YES - simple sunset/shadow scene made overly complex -- Length: TOO LONG - could be said in 1-2 sentences -- Tense: PAST - "Rebecca's heart thundered like a thousand war drums" -- Mature content: N/A -- Name overuse: N/A - no dialogue -- Talking vs Showing: N/A - no dialogue - - - -The sunset cast purple and red across the sky as shadows stretched across the lawn. Rebecca's heart raced when she saw him walking up the driveway. - - ---- - "Hello, Mother. I have returned from my educational institution," said Tim. "Excellent. Did you successfully complete your mathematical assignments?" she responded. @@ -50,28 +28,6 @@ The sunset cast purple and red across the sky as shadows stretched across the la --- - -She reaches for the door handle. Her index finger extends first, followed by her middle finger, then her ring finger, and finally her pinky. Her thumb moves to oppose them. The metal is cool to the touch, approximately 68 degrees Fahrenheit. She grips it with 3.2 pounds of pressure and rotates her wrist 47 degrees clockwise. - - - -- Thought process: While the exact measurements and finger-by-finger description is excessive, we should keep that the handle was cold - it's a sensory detail that adds atmosphere without being purple prose. -- Purple prose: NO -- Unnatural dialogue: N/A - no dialogue -- Over-description: EXTREME - every finger movement, exact temperature, precise measurements -- Length: WAY TOO LONG - should be 3-4 words -- Tense: PRESENT - "reaches for the door handle" -- Mature content: N/A -- Name overuse: N/A -- Talking vs Showing: N/A - no dialogue - - - -She gripped the cold metal handle and turned it, opening the door. - - ---- - Beneath the opalescent moon's ethereal glow, Jake's sapphire eyes met hers. "Your presence illuminates my existence more brilliantly than a thousand supernovas," he declared. @@ -210,6 +166,31 @@ She bit her lip, looking away. "It's just... I mean... um... something feels wro --- + +"As you know, the dual-phase cooling system requires constant monitoring to prevent a cascade failure in the secondary loop," Dr. Chen explained to her co-engineer, who had been maintaining the reactor for twelve years. "The isothermal regulators must be calibrated every six hours, and the backup condensers need to cycle through their purge sequence before any power increase can be authorized." +"Indeed, I am well aware of these procedures," he responded. "Perhaps we should also discuss the implications of the recent firmware update on the thermal management subsystem." + + + +- Thought process: This is exposition dumped into dialogue. These are two engineers who both know this information — they wouldn't explain it to each other. The dialogue exists purely to inform the reader, not because the characters would actually say it. It needs to be replaced with what they'd actually discuss: the immediate problem, not the textbook explanation. +- Purple prose: NO +- Unnatural dialogue: SEVERE - characters explaining things they both already know +- Over-description: YES - unnecessary technical detail +- Length: TOO LONG +- Tense: PAST - "explained" +- Mature content: N/A +- Name overuse: NO +- Talking vs Showing: SEVERE - lecturing the reader through dialogue + + + +"The cooling readings look wrong again." Dr. Chen frowned at her display, tapping one of the fluctuating values. +He leaned over her shoulder. "That started after the firmware update. Want me to roll it back?" +"Not yet. Let me check the secondary loop first." + + +--- + "Hi David, how are you today?" asked Emily. "I'm doing well, Emily. Have you seen Michael?" diff --git a/tests/data/prompts/baselines_no_response_length/creator/determine_character_dialogue_instructions.txt b/tests/data/prompts/baselines_no_response_length/creator/determine_character_dialogue_instructions.txt index 9732312c..aaec47e9 100644 --- a/tests/data/prompts/baselines_no_response_length/creator/determine_character_dialogue_instructions.txt +++ b/tests/data/prompts/baselines_no_response_length/creator/determine_character_dialogue_instructions.txt @@ -23,6 +23,9 @@ The character MUST feel relatable to the audience. You must use simple language to describe the character's dialogue instructions. -Keep the format similar and stick to one paragraph. +**Structure your instructions in two short paragraphs:** + +1. **Default register** — How Elena normally speaks: vocabulary, formality, sentence structure, verbal habits, how they address others. +2. **Under pressure** — How their speech changes when stressed, scared, angry, or pushed past their limit. Do NOT write that they simply become "more of the same" (e.g., "gets even more clinical" or "becomes even quieter"). Their composure must crack in a specific, visible way — shorter sentences, different word choices, breaking their own verbal patterns, physical urgency replacing their normal mode. This variation is critical for believable dialogue in dramatic scenes. <|BOT|>Dialogue instructions: \ No newline at end of file diff --git a/tests/data/prompts/baselines_no_response_length/editor/revision_unslop.txt b/tests/data/prompts/baselines_no_response_length/editor/revision_unslop.txt index 96fb5e92..4fdb9f9d 100644 --- a/tests/data/prompts/baselines_no_response_length/editor/revision_unslop.txt +++ b/tests/data/prompts/baselines_no_response_length/editor/revision_unslop.txt @@ -1,27 +1,5 @@ ## Examples - -The incandescent luminescence of twilight's dying breath painted the heavens in hues of amethyst and vermillion, while shadows danced their ancient waltz across the emerald tapestry of the lawn. Rebecca's heart thundered like a thousand war drums as she glimpsed his silhouette, a dark prince emerging from the veil of dusk's embrace. - - - -- Thought process: This is trying way too hard to describe a simple sunset scene. Every single phrase is overwritten with metaphors and fancy vocabulary when it could just say "sunset" and "she saw him." -- Purple prose: MAJOR ISSUES - "incandescent luminescence," "twilight's dying breath," "ancient waltz," "thousand war drums," "dark prince" -- Unnatural dialogue: N/A - no dialogue -- Over-description: YES - simple sunset/shadow scene made overly complex -- Length: TOO LONG - could be said in 1-2 sentences -- Tense: PAST - "Rebecca's heart thundered like a thousand war drums" -- Mature content: N/A -- Name overuse: N/A - no dialogue -- Talking vs Showing: N/A - no dialogue - - - -The sunset cast purple and red across the sky as shadows stretched across the lawn. Rebecca's heart raced when she saw him walking up the driveway. - - ---- - "Hello, Mother. I have returned from my educational institution," said Tim. "Excellent. Did you successfully complete your mathematical assignments?" she responded. @@ -50,28 +28,6 @@ The sunset cast purple and red across the sky as shadows stretched across the la --- - -She reaches for the door handle. Her index finger extends first, followed by her middle finger, then her ring finger, and finally her pinky. Her thumb moves to oppose them. The metal is cool to the touch, approximately 68 degrees Fahrenheit. She grips it with 3.2 pounds of pressure and rotates her wrist 47 degrees clockwise. - - - -- Thought process: While the exact measurements and finger-by-finger description is excessive, we should keep that the handle was cold - it's a sensory detail that adds atmosphere without being purple prose. -- Purple prose: NO -- Unnatural dialogue: N/A - no dialogue -- Over-description: EXTREME - every finger movement, exact temperature, precise measurements -- Length: WAY TOO LONG - should be 3-4 words -- Tense: PRESENT - "reaches for the door handle" -- Mature content: N/A -- Name overuse: N/A -- Talking vs Showing: N/A - no dialogue - - - -She gripped the cold metal handle and turned it, opening the door. - - ---- - Beneath the opalescent moon's ethereal glow, Jake's sapphire eyes met hers. "Your presence illuminates my existence more brilliantly than a thousand supernovas," he declared. @@ -210,6 +166,31 @@ She bit her lip, looking away. "It's just... I mean... um... something feels wro --- + +"As you know, the dual-phase cooling system requires constant monitoring to prevent a cascade failure in the secondary loop," Dr. Chen explained to her co-engineer, who had been maintaining the reactor for twelve years. "The isothermal regulators must be calibrated every six hours, and the backup condensers need to cycle through their purge sequence before any power increase can be authorized." +"Indeed, I am well aware of these procedures," he responded. "Perhaps we should also discuss the implications of the recent firmware update on the thermal management subsystem." + + + +- Thought process: This is exposition dumped into dialogue. These are two engineers who both know this information — they wouldn't explain it to each other. The dialogue exists purely to inform the reader, not because the characters would actually say it. It needs to be replaced with what they'd actually discuss: the immediate problem, not the textbook explanation. +- Purple prose: NO +- Unnatural dialogue: SEVERE - characters explaining things they both already know +- Over-description: YES - unnecessary technical detail +- Length: TOO LONG +- Tense: PAST - "explained" +- Mature content: N/A +- Name overuse: NO +- Talking vs Showing: SEVERE - lecturing the reader through dialogue + + + +"The cooling readings look wrong again." Dr. Chen frowned at her display, tapping one of the fluctuating values. +He leaned over her shoulder. "That started after the firmware update. Want me to roll it back?" +"Not yet. Let me check the secondary loop first." + + +--- + "Hi David, how are you today?" asked Emily. "I'm doing well, Emily. Have you seen Michael?" diff --git a/tests/data/prompts/baselines_noncoercible/creator/determine_character_dialogue_instructions.txt b/tests/data/prompts/baselines_noncoercible/creator/determine_character_dialogue_instructions.txt index 9f952b30..99c706ac 100644 --- a/tests/data/prompts/baselines_noncoercible/creator/determine_character_dialogue_instructions.txt +++ b/tests/data/prompts/baselines_noncoercible/creator/determine_character_dialogue_instructions.txt @@ -23,7 +23,10 @@ The character MUST feel relatable to the audience. You must use simple language to describe the character's dialogue instructions. -Keep the format similar and stick to one paragraph. +**Structure your instructions in two short paragraphs:** + +1. **Default register** — How Elena normally speaks: vocabulary, formality, sentence structure, verbal habits, how they address others. +2. **Under pressure** — How their speech changes when stressed, scared, angry, or pushed past their limit. Do NOT write that they simply become "more of the same" (e.g., "gets even more clinical" or "becomes even quieter"). Their composure must crack in a specific, visible way — shorter sentences, different word choices, breaking their own verbal patterns, physical urgency replacing their normal mode. This variation is critical for believable dialogue in dramatic scenes. The length of your response must fit within 4 paragraphs. diff --git a/tests/data/prompts/baselines_noncoercible/editor/revision_unslop.txt b/tests/data/prompts/baselines_noncoercible/editor/revision_unslop.txt index 05d82737..564f18f6 100644 --- a/tests/data/prompts/baselines_noncoercible/editor/revision_unslop.txt +++ b/tests/data/prompts/baselines_noncoercible/editor/revision_unslop.txt @@ -1,27 +1,5 @@ ## Examples - -The incandescent luminescence of twilight's dying breath painted the heavens in hues of amethyst and vermillion, while shadows danced their ancient waltz across the emerald tapestry of the lawn. Rebecca's heart thundered like a thousand war drums as she glimpsed his silhouette, a dark prince emerging from the veil of dusk's embrace. - - - -- Thought process: This is trying way too hard to describe a simple sunset scene. Every single phrase is overwritten with metaphors and fancy vocabulary when it could just say "sunset" and "she saw him." -- Purple prose: MAJOR ISSUES - "incandescent luminescence," "twilight's dying breath," "ancient waltz," "thousand war drums," "dark prince" -- Unnatural dialogue: N/A - no dialogue -- Over-description: YES - simple sunset/shadow scene made overly complex -- Length: TOO LONG - could be said in 1-2 sentences -- Tense: PAST - "Rebecca's heart thundered like a thousand war drums" -- Mature content: N/A -- Name overuse: N/A - no dialogue -- Talking vs Showing: N/A - no dialogue - - - -The sunset cast purple and red across the sky as shadows stretched across the lawn. Rebecca's heart raced when she saw him walking up the driveway. - - ---- - "Hello, Mother. I have returned from my educational institution," said Tim. "Excellent. Did you successfully complete your mathematical assignments?" she responded. @@ -50,28 +28,6 @@ The sunset cast purple and red across the sky as shadows stretched across the la --- - -She reaches for the door handle. Her index finger extends first, followed by her middle finger, then her ring finger, and finally her pinky. Her thumb moves to oppose them. The metal is cool to the touch, approximately 68 degrees Fahrenheit. She grips it with 3.2 pounds of pressure and rotates her wrist 47 degrees clockwise. - - - -- Thought process: While the exact measurements and finger-by-finger description is excessive, we should keep that the handle was cold - it's a sensory detail that adds atmosphere without being purple prose. -- Purple prose: NO -- Unnatural dialogue: N/A - no dialogue -- Over-description: EXTREME - every finger movement, exact temperature, precise measurements -- Length: WAY TOO LONG - should be 3-4 words -- Tense: PRESENT - "reaches for the door handle" -- Mature content: N/A -- Name overuse: N/A -- Talking vs Showing: N/A - no dialogue - - - -She gripped the cold metal handle and turned it, opening the door. - - ---- - Beneath the opalescent moon's ethereal glow, Jake's sapphire eyes met hers. "Your presence illuminates my existence more brilliantly than a thousand supernovas," he declared. @@ -210,6 +166,31 @@ She bit her lip, looking away. "It's just... I mean... um... something feels wro --- + +"As you know, the dual-phase cooling system requires constant monitoring to prevent a cascade failure in the secondary loop," Dr. Chen explained to her co-engineer, who had been maintaining the reactor for twelve years. "The isothermal regulators must be calibrated every six hours, and the backup condensers need to cycle through their purge sequence before any power increase can be authorized." +"Indeed, I am well aware of these procedures," he responded. "Perhaps we should also discuss the implications of the recent firmware update on the thermal management subsystem." + + + +- Thought process: This is exposition dumped into dialogue. These are two engineers who both know this information — they wouldn't explain it to each other. The dialogue exists purely to inform the reader, not because the characters would actually say it. It needs to be replaced with what they'd actually discuss: the immediate problem, not the textbook explanation. +- Purple prose: NO +- Unnatural dialogue: SEVERE - characters explaining things they both already know +- Over-description: YES - unnecessary technical detail +- Length: TOO LONG +- Tense: PAST - "explained" +- Mature content: N/A +- Name overuse: NO +- Talking vs Showing: SEVERE - lecturing the reader through dialogue + + + +"The cooling readings look wrong again." Dr. Chen frowned at her display, tapping one of the fluctuating values. +He leaned over her shoulder. "That started after the firmware update. Want me to roll it back?" +"Not yet. Let me check the secondary loop first." + + +--- + "Hi David, how are you today?" asked Emily. "I'm doing well, Emily. Have you seen Michael?" diff --git a/tests/data/prompts/baselines_reasoning/creator/determine_character_dialogue_instructions.txt b/tests/data/prompts/baselines_reasoning/creator/determine_character_dialogue_instructions.txt index 2aa145e6..1c918ebd 100644 --- a/tests/data/prompts/baselines_reasoning/creator/determine_character_dialogue_instructions.txt +++ b/tests/data/prompts/baselines_reasoning/creator/determine_character_dialogue_instructions.txt @@ -23,7 +23,10 @@ The character MUST feel relatable to the audience. You must use simple language to describe the character's dialogue instructions. -Keep the format similar and stick to one paragraph. +**Structure your instructions in two short paragraphs:** + +1. **Default register** — How Elena normally speaks: vocabulary, formality, sentence structure, verbal habits, how they address others. +2. **Under pressure** — How their speech changes when stressed, scared, angry, or pushed past their limit. Do NOT write that they simply become "more of the same" (e.g., "gets even more clinical" or "becomes even quieter"). Their composure must crack in a specific, visible way — shorter sentences, different word choices, breaking their own verbal patterns, physical urgency replacing their normal mode. This variation is critical for believable dialogue in dramatic scenes. The length of your final answer must fit within 4 paragraphs. diff --git a/tests/data/prompts/baselines_reasoning/editor/revision_unslop.txt b/tests/data/prompts/baselines_reasoning/editor/revision_unslop.txt index e619b8e5..eb9be3b9 100644 --- a/tests/data/prompts/baselines_reasoning/editor/revision_unslop.txt +++ b/tests/data/prompts/baselines_reasoning/editor/revision_unslop.txt @@ -1,27 +1,5 @@ ## Examples - -The incandescent luminescence of twilight's dying breath painted the heavens in hues of amethyst and vermillion, while shadows danced their ancient waltz across the emerald tapestry of the lawn. Rebecca's heart thundered like a thousand war drums as she glimpsed his silhouette, a dark prince emerging from the veil of dusk's embrace. - - - -- Thought process: This is trying way too hard to describe a simple sunset scene. Every single phrase is overwritten with metaphors and fancy vocabulary when it could just say "sunset" and "she saw him." -- Purple prose: MAJOR ISSUES - "incandescent luminescence," "twilight's dying breath," "ancient waltz," "thousand war drums," "dark prince" -- Unnatural dialogue: N/A - no dialogue -- Over-description: YES - simple sunset/shadow scene made overly complex -- Length: TOO LONG - could be said in 1-2 sentences -- Tense: PAST - "Rebecca's heart thundered like a thousand war drums" -- Mature content: N/A -- Name overuse: N/A - no dialogue -- Talking vs Showing: N/A - no dialogue - - - -The sunset cast purple and red across the sky as shadows stretched across the lawn. Rebecca's heart raced when she saw him walking up the driveway. - - ---- - "Hello, Mother. I have returned from my educational institution," said Tim. "Excellent. Did you successfully complete your mathematical assignments?" she responded. @@ -50,28 +28,6 @@ The sunset cast purple and red across the sky as shadows stretched across the la --- - -She reaches for the door handle. Her index finger extends first, followed by her middle finger, then her ring finger, and finally her pinky. Her thumb moves to oppose them. The metal is cool to the touch, approximately 68 degrees Fahrenheit. She grips it with 3.2 pounds of pressure and rotates her wrist 47 degrees clockwise. - - - -- Thought process: While the exact measurements and finger-by-finger description is excessive, we should keep that the handle was cold - it's a sensory detail that adds atmosphere without being purple prose. -- Purple prose: NO -- Unnatural dialogue: N/A - no dialogue -- Over-description: EXTREME - every finger movement, exact temperature, precise measurements -- Length: WAY TOO LONG - should be 3-4 words -- Tense: PRESENT - "reaches for the door handle" -- Mature content: N/A -- Name overuse: N/A -- Talking vs Showing: N/A - no dialogue - - - -She gripped the cold metal handle and turned it, opening the door. - - ---- - Beneath the opalescent moon's ethereal glow, Jake's sapphire eyes met hers. "Your presence illuminates my existence more brilliantly than a thousand supernovas," he declared. @@ -210,6 +166,31 @@ She bit her lip, looking away. "It's just... I mean... um... something feels wro --- + +"As you know, the dual-phase cooling system requires constant monitoring to prevent a cascade failure in the secondary loop," Dr. Chen explained to her co-engineer, who had been maintaining the reactor for twelve years. "The isothermal regulators must be calibrated every six hours, and the backup condensers need to cycle through their purge sequence before any power increase can be authorized." +"Indeed, I am well aware of these procedures," he responded. "Perhaps we should also discuss the implications of the recent firmware update on the thermal management subsystem." + + + +- Thought process: This is exposition dumped into dialogue. These are two engineers who both know this information — they wouldn't explain it to each other. The dialogue exists purely to inform the reader, not because the characters would actually say it. It needs to be replaced with what they'd actually discuss: the immediate problem, not the textbook explanation. +- Purple prose: NO +- Unnatural dialogue: SEVERE - characters explaining things they both already know +- Over-description: YES - unnecessary technical detail +- Length: TOO LONG +- Tense: PAST - "explained" +- Mature content: N/A +- Name overuse: NO +- Talking vs Showing: SEVERE - lecturing the reader through dialogue + + + +"The cooling readings look wrong again." Dr. Chen frowned at her display, tapping one of the fluctuating values. +He leaned over her shoulder. "That started after the firmware update. Want me to roll it back?" +"Not yet. Let me check the secondary loop first." + + +--- + "Hi David, how are you today?" asked Emily. "I'm doing well, Emily. Have you seen Michael?" diff --git a/tests/data/prompts/baselines_xml/creator/determine_character_dialogue_instructions.txt b/tests/data/prompts/baselines_xml/creator/determine_character_dialogue_instructions.txt index fce697b7..6854c824 100644 --- a/tests/data/prompts/baselines_xml/creator/determine_character_dialogue_instructions.txt +++ b/tests/data/prompts/baselines_xml/creator/determine_character_dialogue_instructions.txt @@ -23,7 +23,10 @@ The character MUST feel relatable to the audience. You must use simple language to describe the character's dialogue instructions. -Keep the format similar and stick to one paragraph. +**Structure your instructions in two short paragraphs:** + +1. **Default register** — How Elena normally speaks: vocabulary, formality, sentence structure, verbal habits, how they address others. +2. **Under pressure** — How their speech changes when stressed, scared, angry, or pushed past their limit. Do NOT write that they simply become "more of the same" (e.g., "gets even more clinical" or "becomes even quieter"). Their composure must crack in a specific, visible way — shorter sentences, different word choices, breaking their own verbal patterns, physical urgency replacing their normal mode. This variation is critical for believable dialogue in dramatic scenes. The length of your response must fit within 4 paragraphs. diff --git a/tests/data/prompts/baselines_xml/editor/revision_unslop.txt b/tests/data/prompts/baselines_xml/editor/revision_unslop.txt index eaae1873..73545f18 100644 --- a/tests/data/prompts/baselines_xml/editor/revision_unslop.txt +++ b/tests/data/prompts/baselines_xml/editor/revision_unslop.txt @@ -1,27 +1,5 @@ ## Examples - -The incandescent luminescence of twilight's dying breath painted the heavens in hues of amethyst and vermillion, while shadows danced their ancient waltz across the emerald tapestry of the lawn. Rebecca's heart thundered like a thousand war drums as she glimpsed his silhouette, a dark prince emerging from the veil of dusk's embrace. - - - -- Thought process: This is trying way too hard to describe a simple sunset scene. Every single phrase is overwritten with metaphors and fancy vocabulary when it could just say "sunset" and "she saw him." -- Purple prose: MAJOR ISSUES - "incandescent luminescence," "twilight's dying breath," "ancient waltz," "thousand war drums," "dark prince" -- Unnatural dialogue: N/A - no dialogue -- Over-description: YES - simple sunset/shadow scene made overly complex -- Length: TOO LONG - could be said in 1-2 sentences -- Tense: PAST - "Rebecca's heart thundered like a thousand war drums" -- Mature content: N/A -- Name overuse: N/A - no dialogue -- Talking vs Showing: N/A - no dialogue - - - -The sunset cast purple and red across the sky as shadows stretched across the lawn. Rebecca's heart raced when she saw him walking up the driveway. - - ---- - "Hello, Mother. I have returned from my educational institution," said Tim. "Excellent. Did you successfully complete your mathematical assignments?" she responded. @@ -50,28 +28,6 @@ The sunset cast purple and red across the sky as shadows stretched across the la --- - -She reaches for the door handle. Her index finger extends first, followed by her middle finger, then her ring finger, and finally her pinky. Her thumb moves to oppose them. The metal is cool to the touch, approximately 68 degrees Fahrenheit. She grips it with 3.2 pounds of pressure and rotates her wrist 47 degrees clockwise. - - - -- Thought process: While the exact measurements and finger-by-finger description is excessive, we should keep that the handle was cold - it's a sensory detail that adds atmosphere without being purple prose. -- Purple prose: NO -- Unnatural dialogue: N/A - no dialogue -- Over-description: EXTREME - every finger movement, exact temperature, precise measurements -- Length: WAY TOO LONG - should be 3-4 words -- Tense: PRESENT - "reaches for the door handle" -- Mature content: N/A -- Name overuse: N/A -- Talking vs Showing: N/A - no dialogue - - - -She gripped the cold metal handle and turned it, opening the door. - - ---- - Beneath the opalescent moon's ethereal glow, Jake's sapphire eyes met hers. "Your presence illuminates my existence more brilliantly than a thousand supernovas," he declared. @@ -210,6 +166,31 @@ She bit her lip, looking away. "It's just... I mean... um... something feels wro --- + +"As you know, the dual-phase cooling system requires constant monitoring to prevent a cascade failure in the secondary loop," Dr. Chen explained to her co-engineer, who had been maintaining the reactor for twelve years. "The isothermal regulators must be calibrated every six hours, and the backup condensers need to cycle through their purge sequence before any power increase can be authorized." +"Indeed, I am well aware of these procedures," he responded. "Perhaps we should also discuss the implications of the recent firmware update on the thermal management subsystem." + + + +- Thought process: This is exposition dumped into dialogue. These are two engineers who both know this information — they wouldn't explain it to each other. The dialogue exists purely to inform the reader, not because the characters would actually say it. It needs to be replaced with what they'd actually discuss: the immediate problem, not the textbook explanation. +- Purple prose: NO +- Unnatural dialogue: SEVERE - characters explaining things they both already know +- Over-description: YES - unnecessary technical detail +- Length: TOO LONG +- Tense: PAST - "explained" +- Mature content: N/A +- Name overuse: NO +- Talking vs Showing: SEVERE - lecturing the reader through dialogue + + + +"The cooling readings look wrong again." Dr. Chen frowned at her display, tapping one of the fluctuating values. +He leaned over her shoulder. "That started after the firmware update. Want me to roll it back?" +"Not yet. Let me check the secondary loop first." + + +--- + "Hi David, how are you today?" asked Emily. "I'm doing well, Emily. Have you seen Michael?" diff --git a/tests/prompts/baselines/test_director_baselines.py b/tests/prompts/baselines/test_director_baselines.py index f17f5193..7a07e849 100644 --- a/tests/prompts/baselines/test_director_baselines.py +++ b/tests/prompts/baselines/test_director_baselines.py @@ -12,6 +12,8 @@ import talemate.emit.async_signals from talemate.agents.base import DynamicInstruction from talemate.agents.director.chat.schema import DirectorChatMessage +from talemate.agents.director.plan.schema import Beat +from talemate.agents.director.plan.expand import ChunkArcInfo from ..conftest import mock_llm_client # noqa: F401 from ..test_director_templates import ( # noqa: F401 @@ -28,7 +30,7 @@ from ..test_director_templates import ( # noqa: F401 active_context, MockCharacter, ) -from .conftest import capture_prompt +from .conftest import capture_prompt, capture_all_prompts AGENT = "director" @@ -265,3 +267,166 @@ class TestDirectorBaselines: baseline_checker( capture_prompt(director), AGENT, "detect_characters_from_texts" ) + + +def _make_test_beats() -> list[Beat]: + """Create a small set of beats for testing expand templates.""" + return [ + Beat( + description="The protagonist discovers the door is locked from the inside.", + order=1, tension=0.3, pacing="slow", type="narration", + characters=["Elena"], + ), + Beat( + description="Elena confronts Hero about what happened last night, demanding answers.", + order=2, tension=0.5, pacing="moderate", type="dialogue", + characters=["Elena"], + ), + Beat( + description="A sudden noise from the basement forces both characters to investigate together.", + order=3, tension=0.7, pacing="fast", type="action", + characters=["Hero", "Elena"], + ), + ] + + +class TestPlanExpandBaselines: + """Baseline tests for plan expand templates (arc-expand, arc-expand-critique).""" + + @pytest.mark.asyncio + async def test_arc_expand(self, active_context, baseline_checker): + """Test the arc-expand template renders correctly with beats and arc info.""" + from talemate.prompts import Prompt + from talemate.agents.base import ActiveAgent + + director = active_context + from talemate.instance import AGENTS + narrator = AGENTS.get("narrator") + narrator.agent_type = "narrator" + narrator.client = director.client + narrator.extra_instructions = "" + narrator.content_use_writing_style = False + narrator.action_response_length = Mock(return_value=4096) + + beats = _make_test_beats() + arc_info = ChunkArcInfo( + position="opening", + chunk_index=0, + total_chunks=2, + tension_range=(0.3, 0.7), + has_peak=False, + ) + + director.client.send_prompt = AsyncMock( + return_value="Test narration." + ) + + with ActiveAgent(narrator, lambda: None): + await Prompt.request( + "narrator.arc-expand", + narrator.client, + "narrate_4096", + vars={ + "scene": director.scene, + "max_tokens": 8192, + "beats": beats, + "following_beats": [], + "preceding_text": "", + "perspective": "Third person limited, past tense.", + "director_notes": "Focus on building tension.", + "extra_instructions": "", + "response_length": 4096, + "arc_info": arc_info, + }, + ) + + baseline_checker(capture_prompt(director), AGENT, "arc_expand") + + @pytest.mark.asyncio + async def test_arc_expand__with_preceding_text( + self, active_context, baseline_checker + ): + """Test arc-expand with preceding text context.""" + from talemate.prompts import Prompt + from talemate.agents.base import ActiveAgent + + director = active_context + from talemate.instance import AGENTS + narrator = AGENTS.get("narrator") + narrator.agent_type = "narrator" + narrator.client = director.client + narrator.extra_instructions = "" + narrator.content_use_writing_style = False + narrator.action_response_length = Mock(return_value=4096) + + beats = _make_test_beats()[1:] # beats 2-3 + arc_info = ChunkArcInfo( + position="climax", + chunk_index=1, + total_chunks=2, + tension_range=(0.5, 0.7), + has_peak=True, + ) + + director.client.send_prompt = AsyncMock( + return_value="More narration." + ) + + with ActiveAgent(narrator, lambda: None): + await Prompt.request( + "narrator.arc-expand", + narrator.client, + "narrate_4096", + vars={ + "scene": director.scene, + "max_tokens": 8192, + "beats": beats, + "following_beats": [], + "preceding_text": "The door creaked open, revealing an empty room. Elena stepped inside cautiously.", + "perspective": "Third person limited, past tense.", + "director_notes": "", + "extra_instructions": "", + "response_length": 4096, + "arc_info": arc_info, + }, + ) + + baseline_checker( + capture_prompt(director), AGENT, "arc_expand__with_preceding_text" + ) + + @pytest.mark.asyncio + async def test_arc_expand_critique(self, active_context, baseline_checker): + """Test the arc-expand-critique template renders correctly.""" + from talemate.prompts import Prompt + from talemate.agents.base import ActiveAgent + + director = active_context + narrator = Mock() + narrator.client = director.client + narrator.extra_instructions = "" + narrator.content_use_writing_style = False + + blocks = [ + {"type": "narrator", "content": "The room was dark and cold. A chill ran down her spine."}, + {"type": "character", "name": "Elena", "content": "She stepped forward, her hands trembling. \"Who's there?\" she whispered."}, + {"type": "narrator", "content": "A chill ran through the room. The darkness pressed in from all sides."}, + ] + + director.client.send_prompt = AsyncMock( + return_value="Revised narration." + ) + + with ActiveAgent(narrator, lambda: None): + await Prompt.request( + "narrator.arc-expand-critique", + narrator.client, + "narrate_4096", + vars={ + "blocks": blocks, + "max_tokens": 8192, + "response_length": 4096, + }, + ) + + baseline_checker(capture_prompt(director), AGENT, "arc_expand_critique") diff --git a/tests/test_plan_expand.py b/tests/test_plan_expand.py new file mode 100644 index 00000000..1ef165d6 --- /dev/null +++ b/tests/test_plan_expand.py @@ -0,0 +1,259 @@ +""" +Unit tests for the director plan expand system. + +Tests cover: +- Deliberate chunking (tension-valley splitting) +- Arc position metadata computation +- Leaked tag detection +- PlanMixin config properties +- Chat creation for generate_arc modes +""" + +import pytest +from unittest.mock import Mock, AsyncMock, patch + +from talemate.agents.director.plan.expand import ( + compute_chunks, + compute_arc_info, + has_leaked_tags, + MIN_CHUNK_BEATS, +) +from talemate.agents.director.plan.schema import Beat + + +def _make_beats(tensions: list[float]) -> list[Beat]: + """Helper to create Beat objects with given tension values.""" + return [ + Beat( + description=f"Beat {i+1}", + order=i + 1, + tension=t, + pacing="moderate", + type="narration", + ) + for i, t in enumerate(tensions) + ] + + +class TestComputeChunks: + """Tests for deliberate chunking at tension valleys.""" + + def test_single_chunk_when_under_max(self): + beats = _make_beats([0.2, 0.4, 0.6, 0.8]) + chunks = compute_chunks(beats, max_chunk_size=8) + assert len(chunks) == 1 + assert len(chunks[0]) == 4 + + def test_single_chunk_when_equal_to_max(self): + beats = _make_beats([0.2, 0.4, 0.6, 0.8, 1.0]) + chunks = compute_chunks(beats, max_chunk_size=5) + assert len(chunks) == 1 + assert len(chunks[0]) == 5 + + def test_splits_at_tension_valley(self): + # Tension rises to 0.7 then drops to 0.3 — should split at the valley + beats = _make_beats([0.2, 0.4, 0.7, 0.3, 0.5, 0.8, 1.0]) + chunks = compute_chunks(beats, max_chunk_size=6) + assert len(chunks) == 2 + # First chunk: beats 1-3 (tension rises to 0.7) + assert len(chunks[0]) == 3 + # Second chunk: beats 4-7 (starts at 0.3) + assert len(chunks[1]) == 4 + + def test_respects_min_chunk_size(self): + # Valley at beat 2, but that would leave chunk 1 with only 2 beats + beats = _make_beats([0.5, 0.3, 0.4, 0.6, 0.8, 1.0]) + chunks = compute_chunks(beats, max_chunk_size=5) + # Should not split at beat 2 because chunk would be < MIN_CHUNK_BEATS + assert all(len(c) >= MIN_CHUNK_BEATS for c in chunks) + + def test_splits_at_max_size_when_no_valley(self): + # Monotonically increasing — no valleys, must split at max + beats = _make_beats([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8]) + chunks = compute_chunks(beats, max_chunk_size=4) + assert len(chunks) == 2 + assert len(chunks[0]) == 4 + assert len(chunks[1]) == 4 + + def test_merges_small_leftover(self): + # 7 beats, max 4 — would split to [4, 3], but if valley is at 5 + # we might get [5, 2] which should merge to [7] + beats = _make_beats([0.2, 0.4, 0.6, 0.8, 0.5, 0.3, 0.2]) + chunks = compute_chunks(beats, max_chunk_size=6) + # With valley at beat 4→5, should split to [4, 3] + # But leftover of 2 is < MIN_CHUNK_BEATS, so merges back + for chunk in chunks: + assert len(chunk) >= MIN_CHUNK_BEATS or len(chunks) == 1 + + def test_multiple_valleys(self): + beats = _make_beats([0.2, 0.5, 0.8, 0.3, 0.6, 0.9, 0.4, 0.7, 1.0]) + chunks = compute_chunks(beats, max_chunk_size=8) + # Two valleys: beat 3→4 (0.8→0.3) and beat 6→7 (0.9→0.4) + assert len(chunks) == 3 + + def test_empty_beats(self): + chunks = compute_chunks([], max_chunk_size=5) + assert len(chunks) == 1 + assert len(chunks[0]) == 0 + + +class TestComputeArcInfo: + """Tests for arc position metadata computation.""" + + def test_single_chunk_is_full(self): + beats = _make_beats([0.2, 0.5, 0.8]) + chunks = [beats] + infos = compute_arc_info(chunks, beats) + assert len(infos) == 1 + assert infos[0].position == "full" + + def test_two_chunks_opening_and_climax(self): + beats = _make_beats([0.2, 0.4, 0.6, 0.8, 0.9, 1.0]) + chunks = [beats[:3], beats[3:]] + infos = compute_arc_info(chunks, beats) + assert infos[0].position == "opening" + assert infos[1].position == "climax" + + def test_three_chunks_opening_rising_climax(self): + beats = _make_beats([0.2, 0.3, 0.5, 0.6, 0.8, 0.9, 1.0]) + chunks = [beats[:2], beats[2:4], beats[4:]] + infos = compute_arc_info(chunks, beats) + assert infos[0].position == "opening" + assert infos[1].position == "rising" + assert infos[2].position == "climax" + + def test_resolution_when_peak_not_in_last_chunk(self): + beats = _make_beats([0.2, 0.5, 1.0, 0.8, 0.4, 0.3]) + chunks = [beats[:3], beats[3:]] + infos = compute_arc_info(chunks, beats) + assert infos[0].position == "opening" # has peak but is first chunk + assert infos[1].position == "resolution" + + def test_climax_and_resolution_when_winds_down(self): + beats = _make_beats([0.2, 0.5, 0.8, 1.0, 0.7, 0.5, 0.3]) + chunks = [beats[:3], beats[3:]] + infos = compute_arc_info(chunks, beats) + assert infos[0].position == "opening" + # Second chunk has peak (1.0) but winds down to 0.3 + assert infos[1].position == "climax_and_resolution" + + def test_tension_range_computed_correctly(self): + beats = _make_beats([0.2, 0.8, 0.5]) + chunks = [beats] + infos = compute_arc_info(chunks, beats) + assert infos[0].tension_range == (0.2, 0.8) + + def test_has_peak_within_tolerance(self): + beats = _make_beats([0.2, 0.5, 0.95, 1.0]) + chunks = [beats[:2], beats[2:]] + infos = compute_arc_info(chunks, beats) + # 0.95 is within 0.05 of peak 1.0 + assert infos[1].has_peak is True + + +class TestHasLeakedTags: + """Tests for leaked block tag detection.""" + + def test_clean_blocks(self): + blocks = [ + {"type": "narrator", "content": "The ship drifted silently."}, + {"type": "character", "name": "Elmer", "content": "Let's go."}, + ] + assert has_leaked_tags(blocks) is False + + def test_leaked_narrator_tag(self): + blocks = [ + {"type": "narrator", "content": "Some text more text"}, + ] + assert has_leaked_tags(blocks) is True + + def test_leaked_character_tag(self): + blocks = [ + {"type": "narrator", "content": 'Text more'}, + ] + assert has_leaked_tags(blocks) is True + + def test_leaked_closing_tag(self): + blocks = [ + {"type": "character", "content": "Text more"}, + ] + assert has_leaked_tags(blocks) is True + + def test_empty_blocks(self): + assert has_leaked_tags([]) is False + + def test_empty_content(self): + blocks = [{"type": "narrator", "content": ""}] + assert has_leaked_tags(blocks) is False + + +class TestPlanMixinConfig: + """Tests for PlanMixin configuration properties.""" + + def test_plan_action_registered(self): + from talemate.agents.director import DirectorAgent + + actions = DirectorAgent.init_actions() + assert "plan" in actions + plan = actions["plan"] + assert plan.label == "Arc Generation" + assert plan.container is True + assert plan.icon == "mdi-movie-open" + + def test_plan_config_keys(self): + from talemate.agents.director import DirectorAgent + + actions = DirectorAgent.init_actions() + config = actions["plan"].config + assert "dialogue_ratio" in config + assert "expand_chunk_size" in config + assert "outline_critique" in config + assert "expand_critique" in config + + def test_plan_config_defaults(self): + from talemate.agents.director import DirectorAgent + + actions = DirectorAgent.init_actions() + config = actions["plan"].config + assert config["dialogue_ratio"].value == 0.4 + assert config["expand_chunk_size"].value == 5 + assert config["outline_critique"].value is True + assert config["expand_critique"].value is True + + +class TestChatCreateGenerateArc: + """Tests for creating arc generation chats.""" + + @staticmethod + def _make_director(): + from talemate.agents.director import DirectorAgent + + director = DirectorAgent.__new__(DirectorAgent) + director.actions = DirectorAgent.init_actions() + director._chats = {} + director._last_active_chat_id = None + # Mock scene with agent_state for chat persistence + director.scene = Mock() + director.scene.agent_state = {"director": {}} + return director + + def test_create_generate_arc_default_mode(self): + director = self._make_director() + chat = director.chat_create_generate_arc("Test instructions", 8) + assert chat.mode == "generate_arc" + assert chat.confirm_write_actions is False + assert len(chat.messages) == 2 + + def test_create_generate_arc_expand_mode(self): + director = self._make_director() + chat = director.chat_create_generate_arc( + "Test instructions", 8, mode="generate_arc_expand" + ) + assert chat.mode == "generate_arc_expand" + + def test_create_generate_arc_instructions_in_message(self): + director = self._make_director() + chat = director.chat_create_generate_arc("Write a horror scene", 12) + user_msg = chat.messages[1] + assert "Write a horror scene" in user_msg.message + assert "12 beats" in user_msg.message