diff --git a/CHANGELOG.yaml b/CHANGELOG.yaml
index 77cf6d10..0b6d5ccb 100644
--- a/CHANGELOG.yaml
+++ b/CHANGELOG.yaml
@@ -1,5 +1,6 @@
0.39.0.dev:
features:
+ - "Character Creation Example Dialogue: AI-assisted character creation (World Editor → Characters → Create and the scene tools creative menu) gained a 'Generate example dialogue' option that generates a few example dialogue lines for the new character, with an optional guidance field to steer how the examples are written (tone, speech patterns, quirks). Off by default."
- "Scene Visual Manager: World Editor → Scene gained a Visuals tab mirroring the character visual manager, with Background Illustration and Scene Illustration sub-tabs to browse, upload, generate (new or as a variation of a reference image, single or batch), view, and delete scene images — plus per-image actions to set the scene cover image and set or unset the scene backdrop. A Prompt Finalization sub-tab edits the Visualizer agent's per-scene Prompt Finalization overrides directly from the world editor, creating the scene's agent-settings file with the default name when none is linked yet. The scene backdrop can now also be fully unset (not just toggled off), keeping the image in the scene assets."
- "Help Agent: A new Help agent provides an interactive help chat that answers questions about Talemate itself — settings, agents, clients, the world editor and more — grounded in the bundled documentation, which it can search and read before answering. Opened via the help icon in the app bar, it works with or without a loaded scene, supports multiple persistent multi-turn chats, and runs in the background so it never blocks the main Talemate loop. A per-chat Scene Aware toggle lets a conversation see (or stay unaware of) the currently loaded scene, and each question carries a small snapshot of what you are looking at in the interface so contextual questions like 'what does this setting do?' can be answered. The help chat cannot change anything — scene changes remain the director chat's job."
- "Scene Backdrop: Any scene illustration can now be set as the scene backdrop — an image that fills the whole scene view behind the messages instead of rendering inline. The backdrop belongs to the scene and is saved with it, so it survives reloads and history edits. Set it via 'Set as scene backdrop' on any illustration's image menu, the 'Set backdrop' button in the Visual Library, or enable 'Auto Backdrop' per visual type (Settings → Appearance → Message Visuals) to have newly generated Scene Backgrounds ('Visualize Scene (Background)') and/or Scene Illustrations ('Visualize Moment') promoted automatically. An 'Immersive' quick-toggle chip in the scene tools turns the backdrop on and off without forgetting the chosen image. Message text sits on translucent panels with a drop shadow for legibility — panel opacity and the text shadow are configurable — and a small marker icon shows which message's image is the current backdrop (click it for the image menu, which stays reachable via an Illustration chip on the message hover toolbar)."
diff --git a/docs/img/0.39.0/scene-tool-intro-char-1.png b/docs/img/0.39.0/scene-tool-intro-char-1.png
new file mode 100644
index 00000000..5e0a5330
Binary files /dev/null and b/docs/img/0.39.0/scene-tool-intro-char-1.png differ
diff --git a/docs/img/0.39.0/scene-tool-intro-char-example-dialogue.png b/docs/img/0.39.0/scene-tool-intro-char-example-dialogue.png
new file mode 100644
index 00000000..87ce5f7a
Binary files /dev/null and b/docs/img/0.39.0/scene-tool-intro-char-example-dialogue.png differ
diff --git a/docs/img/0.39.0/world-editor-create-character-example-dialogue.png b/docs/img/0.39.0/world-editor-create-character-example-dialogue.png
new file mode 100644
index 00000000..e7be45c6
Binary files /dev/null and b/docs/img/0.39.0/world-editor-create-character-example-dialogue.png differ
diff --git a/docs/img/0.39.0/world-editor-create-player-character-1.png b/docs/img/0.39.0/world-editor-create-player-character-1.png
new file mode 100644
index 00000000..fd04a140
Binary files /dev/null and b/docs/img/0.39.0/world-editor-create-player-character-1.png differ
diff --git a/docs/img/0.39.0/world-editor-create-player-character-2.png b/docs/img/0.39.0/world-editor-create-player-character-2.png
new file mode 100644
index 00000000..9b007cbe
Binary files /dev/null and b/docs/img/0.39.0/world-editor-create-player-character-2.png differ
diff --git a/docs/user-guide/howto/create-a-new-scene/create-player-character.md b/docs/user-guide/howto/create-a-new-scene/create-player-character.md
index de57756f..e783cfaa 100644
--- a/docs/user-guide/howto/create-a-new-scene/create-player-character.md
+++ b/docs/user-guide/howto/create-a-new-scene/create-player-character.md
@@ -12,7 +12,7 @@ Find the :material-account-plus: **Create Character** button and click on it.
A `New character` entry will appear in the list.
-
+
## Generate the player character
@@ -28,7 +28,7 @@ You can leave the `Description` field empty, as the AI will generate a descripti
Toggle on the **Controlled by Player** switch.
-
+
Finally click the **Create Character** button.
diff --git a/docs/user-guide/scenario-tools.md b/docs/user-guide/scenario-tools.md
index bb251de8..d5a2bc46 100644
--- a/docs/user-guide/scenario-tools.md
+++ b/docs/user-guide/scenario-tools.md
@@ -271,7 +271,7 @@ If the current narration and scene progress has not yet indicated the character
Allows you to quickly generate a new, interactive character to add to the scene.
-
+
##### Determine Name
@@ -298,6 +298,16 @@ Write direction on how you want the character to enter the scene.
!!! note "A character already interacting in the scene will not have their entrance narrated"
If the narrator has already established the character interacting with your characters this "entry narration" will be skipped. The check for this 100% based on an AI prompt so may not always work correctly.
+##### Generate example dialogue
+
+If selected, the AI will generate a few example dialogue lines for the character, showcasing how they speak and act.
+
+When checked, an **Example dialogue guidance** field appears where you can optionally steer how the examples are written — tone, speech patterns, quirks (e.g. "Speaks in short sentences, dry humor"). Leave it blank to let the AI infer the voice from the character's description and attributes.
+
+
+
+The generated examples can be reviewed and edited later under the character's [Actor management](/talemate/user-guide/world-editor/characters/actor) tab in the world editor.
+
##### Templates
Control attribute generation using your existing [World State Templates](/talemate/user-guide/templates).
diff --git a/docs/user-guide/world-editor/characters/create.md b/docs/user-guide/world-editor/characters/create.md
index 683c1a0b..cef46616 100644
--- a/docs/user-guide/world-editor/characters/create.md
+++ b/docs/user-guide/world-editor/characters/create.md
@@ -6,7 +6,7 @@ Then click on the :material-account-plus: **Create Character** button on the lef
A `New character` entry will appear in the list.
-
+
### Enable AI Generation
@@ -33,6 +33,16 @@ The short to medium length description of the character. This will be generated
If this is toggled on, the AI will generate some attributes for the character based on the instructions you provide.
+### Generate Example Dialogue
+
+If this is toggled on, the AI will generate a few example dialogue lines for the character, showcasing how they speak and act.
+
+When enabled, an **Example dialogue guidance** field appears where you can optionally steer how the examples are written — tone, speech patterns, quirks (e.g. "Speaks in short sentences, dry humor"). Leave it blank to let the AI infer the voice from the character's description and attributes.
+
+
+
+The generated examples can be reviewed and edited after creation under the character's [Actor management](/talemate/user-guide/world-editor/characters/actor) tab.
+
### Controlled by Player
If this is toggled on, the character will be flagged as the main player character. This is used to determine who the player is controlling in the scene.
diff --git a/src/talemate/agents/creator/character.py b/src/talemate/agents/creator/character.py
index 1a420bf7..08c3e228 100644
--- a/src/talemate/agents/creator/character.py
+++ b/src/talemate/agents/creator/character.py
@@ -166,6 +166,7 @@ class CharacterCreatorMixin:
self,
character: Character,
text: str = "",
+ instructions: str = "",
dynamic_instructions: list = None,
max_examples: int = 5,
) -> list[str]:
@@ -174,6 +175,7 @@ class CharacterCreatorMixin:
Args:
character: The character to extract dialogue examples for
text: Text containing dialogue examples and relevant character information
+ instructions: Optional user-provided guidance for the dialogue examples
dynamic_instructions: Optional dynamic instructions for context
max_examples: Maximum number of dialogue examples to generate (default: 5)
@@ -211,6 +213,7 @@ class CharacterCreatorMixin:
character=character,
scene=self.scene,
text=text,
+ instructions=instructions,
max_examples=max_examples,
existing_examples=character.example_dialogue[:3]
if character.example_dialogue
diff --git a/src/talemate/agents/director/character_management.py b/src/talemate/agents/director/character_management.py
index 7fcdf7bc..5496f460 100644
--- a/src/talemate/agents/director/character_management.py
+++ b/src/talemate/agents/director/character_management.py
@@ -29,6 +29,8 @@ __all__ = [
log = structlog.get_logger()
+PERSIST_CHARACTER_EXAMPLE_DIALOGUE_COUNT = 3
+
if TYPE_CHECKING:
from talemate import Character, Scene
from talemate.agents.tts import TTSAgent
@@ -152,6 +154,8 @@ class CharacterManagementMixin:
description: str = "",
assign_voice: bool = True,
is_player: bool = False,
+ generate_example_dialogue: bool = False,
+ example_dialogue_instructions: str = "",
) -> "Character":
world_state = instance.get_agent("world_state")
creator = instance.get_agent("creator")
@@ -298,6 +302,18 @@ class CharacterManagementMixin:
character.dialogue_instructions = dialogue_instructions
log.debug("persist_character", dialogue_instructions=dialogue_instructions)
+ # Generate example dialogue for the character if the option is selected
+ if generate_example_dialogue:
+ loading_status("Generating example dialogue")
+ example_dialogue = await creator.determine_character_dialogue_examples(
+ character,
+ text=content or "",
+ instructions=example_dialogue_instructions,
+ max_examples=PERSIST_CHARACTER_EXAMPLE_DIALOGUE_COUNT,
+ )
+ character.example_dialogue = example_dialogue
+ log.debug("persist_character", example_dialogue=example_dialogue)
+
# Narrate the character's entry if the option is selected
if active and narrate_entry:
loading_status("Narrating character entry")
diff --git a/src/talemate/agents/director/websocket_handler.py b/src/talemate/agents/director/websocket_handler.py
index a5df2e0b..5a516153 100644
--- a/src/talemate/agents/director/websocket_handler.py
+++ b/src/talemate/agents/director/websocket_handler.py
@@ -55,6 +55,9 @@ class PersistCharacterPayload(pydantic.BaseModel):
is_player: bool = False
+ generate_example_dialogue: bool = False
+ example_dialogue_instructions: str = ""
+
class AssignVoiceToCharacterPayload(pydantic.BaseModel):
character_name: str
diff --git a/src/talemate/agents/help/docs-index.yaml b/src/talemate/agents/help/docs-index.yaml
index dd3c88ec..fee79902 100644
--- a/src/talemate/agents/help/docs-index.yaml
+++ b/src/talemate/agents/help/docs-index.yaml
@@ -725,8 +725,8 @@
- path: user-guide/scenario-tools.md
title: Scene Tools
summary: The scene tools toolbar and agent activity bar. Covers actor actions, director actions, narrator actions, advancing
- time, world state actions, creative tools (e.g. taking characters in/out of the scene), the visualizer menu, and saving,
- with Ctrl/Cmd modifier click behavior.
+ time, world state actions, creative tools (e.g. taking characters in/out of the scene, introducing new characters with
+ optional example dialogue generation and guidance), the visualizer menu, and saving, with Ctrl/Cmd modifier click behavior.
- path: user-guide/scene-directory.md
title: Scene Directory Structure
summary: Layout of a scene's project directory under scenes/, save .json files, and the assets/, changelog/, nodes/, templates/,
@@ -805,7 +805,8 @@
- path: user-guide/world-editor/characters/create.md
title: Character creation
summary: Creating a new character with the Create Character button, the AI generation toggle and instructions field, name,
- description, generate-attributes option, and the Controlled by Player switch.
+ description, generate-attributes option, the generate-example-dialogue option with its optional guidance field, and the
+ Controlled by Player switch.
- path: user-guide/world-editor/characters/deactivate.md
title: Activate and Deactivate Characters
summary: Temporarily removing a character from dialogue generation with the Deactivate button and bringing them back with
diff --git a/src/talemate/prompts/templates/creator/determine-character-dialogue-examples.jinja2 b/src/talemate/prompts/templates/creator/determine-character-dialogue-examples.jinja2
index 782efb6f..0c546d07 100644
--- a/src/talemate/prompts/templates/creator/determine-character-dialogue-examples.jinja2
+++ b/src/talemate/prompts/templates/creator/determine-character-dialogue-examples.jinja2
@@ -34,6 +34,11 @@ Each dialogue example should be formatted as:
You are limited to {{ focal.max_calls }} {% if focal.max_calls == 1 %}dialogue example{% else %}dialogue examples{% endif %}, indicate that you understand this limitation by clearly stating how many examples you are allowed to add.
+{% if instructions %}
+**User-provided guidance for the dialogue examples:**
+{{ instructions }}
+{% endif -%}
+
{% if existing_examples %}
**Existing dialogue examples (for reference only):**
{% for example in existing_examples -%}
diff --git a/talemate_frontend/src/components/SceneToolsCreative.vue b/talemate_frontend/src/components/SceneToolsCreative.vue
index 63ac4c86..82a5faf0 100644
--- a/talemate_frontend/src/components/SceneToolsCreative.vue
+++ b/talemate_frontend/src/components/SceneToolsCreative.vue
@@ -108,6 +108,12 @@
+
+
+
+
+
+
@@ -198,6 +204,8 @@ export default {
determine_name: true,
narrate_entry: true,
narrate_entry_direction: "",
+ generate_example_dialogue: false,
+ example_dialogue_instructions: "",
augment_attributes_enabled: false,
augment_attributes: "Add some additional, interesting attributes that are not already present in the character sheet."
},
diff --git a/talemate_frontend/src/components/WorldStateManagerCharacterCreator.vue b/talemate_frontend/src/components/WorldStateManagerCharacterCreator.vue
index b1dd3d5a..20d0af15 100644
--- a/talemate_frontend/src/components/WorldStateManagerCharacterCreator.vue
+++ b/talemate_frontend/src/components/WorldStateManagerCharacterCreator.vue
@@ -53,6 +53,17 @@
+
+
+
@@ -118,6 +129,8 @@ export default {
enabled: true,
instructions: "",
generateAttributes: true,
+ generateExampleDialogue: false,
+ exampleDialogueInstructions: "",
},
description: "",
name: "",
@@ -187,6 +200,8 @@ export default {
enabled: true,
instructions: "",
generateAttributes: true,
+ generateExampleDialogue: false,
+ exampleDialogueInstructions: "",
},
description: "",
name: "",
@@ -227,6 +242,8 @@ export default {
determine_name: this.character.generation_context.enabled && !this.character.name,
narrate_entry: false,
generate_attributes: this.character.generation_context.generateAttributes,
+ generate_example_dialogue: this.character.generation_context.generateExampleDialogue,
+ example_dialogue_instructions: this.character.generation_context.exampleDialogueInstructions,
augment_attributes: "Add some additional, interesting attributes that are not already present in the character sheet.",
is_player: this.character.is_player,
description: this.character.description,
diff --git a/tests/prompts/test_creator_templates.py b/tests/prompts/test_creator_templates.py
index 44eb9050..0828180a 100644
--- a/tests/prompts/test_creator_templates.py
+++ b/tests/prompts/test_creator_templates.py
@@ -497,6 +497,67 @@ class TestCreatorDialogueExamplesMethod:
"creator.determine-character-dialogue-examples"
)
+ @pytest.mark.asyncio
+ async def test_determine_character_dialogue_examples_passes_instructions(
+ self, active_context, mock_scene
+ ):
+ """User-provided guidance is handed to the Focal handler as template context."""
+ creator = active_context
+ character = mock_scene.get_character("Elena")
+
+ with patch("talemate.agents.creator.character.focal.Focal") as MockFocal:
+ mock_focal_instance = Mock()
+ mock_focal_instance.request = AsyncMock()
+ mock_focal_instance.context = {}
+ MockFocal.return_value = mock_focal_instance
+
+ await creator.determine_character_dialogue_examples(
+ character=character,
+ text="Elena speaks softly.",
+ instructions="Give her a dry sense of humor.",
+ max_examples=3,
+ )
+
+ kwargs = MockFocal.call_args.kwargs
+ assert kwargs["instructions"] == "Give her a dry sense of humor."
+ assert kwargs["max_calls"] == 3
+
+ @pytest.mark.asyncio
+ async def test_determine_character_dialogue_examples_renders_instructions(
+ self, active_context, mock_scene
+ ):
+ """Guidance lands in the rendered prompt (real Focal + real template)."""
+ creator = active_context
+ character = mock_scene.get_character("Elena")
+ creator.client.send_prompt.return_value = '{"calls": []}'
+
+ await creator.determine_character_dialogue_examples(
+ character=character,
+ text="Elena speaks softly.",
+ instructions="Give her a dry sense of humor.",
+ )
+
+ prompt_text = str(creator.client.send_prompt.call_args_list[0][0][0])
+ assert "User-provided guidance for the dialogue examples" in prompt_text
+ assert "Give her a dry sense of humor." in prompt_text
+
+ @pytest.mark.asyncio
+ async def test_determine_character_dialogue_examples_no_instructions_section(
+ self, active_context, mock_scene
+ ):
+ """Without guidance the section is omitted from the rendered prompt."""
+ creator = active_context
+ character = mock_scene.get_character("Elena")
+ creator.client.send_prompt.return_value = '{"calls": []}'
+
+ await creator.determine_character_dialogue_examples(
+ character=character,
+ text="Elena speaks softly.",
+ )
+
+ prompt_text = str(creator.client.send_prompt.call_args_list[0][0][0])
+ assert "User-provided guidance for the dialogue examples" not in prompt_text
+
class TestCreatorScenarioMethods:
"""Tests for scenario creation methods."""
diff --git a/tests/test_director_character_management.py b/tests/test_director_character_management.py
index 56595689..c2353523 100644
--- a/tests/test_director_character_management.py
+++ b/tests/test_director_character_management.py
@@ -11,15 +11,18 @@ Covers:
- persist_characters_from_worldstate: skip logic (excluded names + names
already in scene). The actual persist_character call is replaced with a
fake on the agent instance to keep the test focused on iteration.
-
-The full LLM/persist_character pipeline is not exercised because it depends
-on a wide spectrum of agent collaborators — that's an integration test, not
-a unit test.
+- persist_character: the example dialogue step (opt-in flag, guidance and
+ count forwarding, skipped by default) with the creator's LLM-backed
+ methods patched, plus the PersistCharacterPayload/signature parity guard
+ and the early-error path when the name already exists. The remaining
+ LLM-backed steps (name/attributes/description generation) are bypassed
+ via flags rather than exercised.
"""
from __future__ import annotations
-from unittest.mock import patch
+import inspect
+from unittest.mock import AsyncMock, patch
import pytest
@@ -27,6 +30,11 @@ from conftest import MockScene, bootstrap_scene
import talemate.agents.tts.voice_library as voice_library_mod
import talemate.instance as instance
+from talemate.agents.director.character_management import (
+ PERSIST_CHARACTER_EXAMPLE_DIALOGUE_COUNT,
+ CharacterManagementMixin,
+)
+from talemate.agents.director.websocket_handler import PersistCharacterPayload
from talemate.agents.tts.schema import Voice # noqa: F401
from talemate.character import Character
from talemate.world_state import CharacterState
@@ -392,6 +400,85 @@ class TestPersistCharactersFromWorldstate:
assert result[0].name == "X"
+# ---------------------------------------------------------------------------
+# persist_character — example dialogue generation
+# ---------------------------------------------------------------------------
+
+
+class TestPersistCharacterExampleDialogue:
+ """Cover the example dialogue step of persist_character.
+
+ Creator collaborators are patched with AsyncMocks; the flags passed to
+ persist_character (determine_name=False, generate_attributes=False,
+ description set, narrate_entry=False) skip every other LLM-backed step.
+ """
+
+ @pytest.fixture
+ def patched_creator(self, scene):
+ creator = instance.get_agent("creator")
+ with (
+ patch.object(
+ creator,
+ "determine_character_dialogue_instructions",
+ AsyncMock(return_value="speaks softly"),
+ ),
+ patch.object(
+ creator,
+ "determine_character_dialogue_examples",
+ AsyncMock(return_value=['Nyx: "Well, that went great." rolls eyes']),
+ ) as mock_examples,
+ ):
+ yield mock_examples
+
+ @pytest.mark.asyncio
+ async def test_generates_examples_with_guidance(
+ self, scene, director, patched_creator
+ ):
+ character = await director.persist_character(
+ name="Nyx",
+ content="A mysterious stranger",
+ determine_name=False,
+ generate_attributes=False,
+ description="Already described.",
+ narrate_entry=False,
+ generate_example_dialogue=True,
+ example_dialogue_instructions="Dry humor, short sentences.",
+ )
+
+ assert character is not None
+ patched_creator.assert_awaited_once()
+ kwargs = patched_creator.await_args.kwargs
+ assert kwargs["text"] == "A mysterious stranger"
+ assert kwargs["instructions"] == "Dry humor, short sentences."
+ assert kwargs["max_examples"] == PERSIST_CHARACTER_EXAMPLE_DIALOGUE_COUNT
+ assert character.example_dialogue == [
+ 'Nyx: "Well, that went great." rolls eyes'
+ ]
+
+ @pytest.mark.asyncio
+ async def test_skipped_by_default(self, scene, director, patched_creator):
+ character = await director.persist_character(
+ name="Vex",
+ content="A quiet merchant",
+ determine_name=False,
+ generate_attributes=False,
+ description="Already described.",
+ narrate_entry=False,
+ )
+
+ assert character is not None
+ patched_creator.assert_not_awaited()
+ assert not character.example_dialogue
+
+ def test_payload_fields_match_persist_character_signature(self):
+ """Every field the websocket payload exposes must be a persist_character
+ kwarg — guards backend/frontend parity when either side changes."""
+ params = set(
+ inspect.signature(CharacterManagementMixin.persist_character).parameters
+ ) - {"self"}
+ assert set(PersistCharacterPayload.model_fields).issubset(params)
+
+
# ---------------------------------------------------------------------------
# persist_character — early-error path when name already exists
# ---------------------------------------------------------------------------