Example dialogue guidance during character creation (#71)

* feat: example dialogue guidance during character creation (#69)

* docs: document example dialogue generation option; update stale test docstring

* docs: refresh character creation screenshots for example dialogue option
This commit is contained in:
veguAI
2026-07-06 01:24:11 +03:00
committed by GitHub
parent b2686b81f8
commit 44e09d5881
18 changed files with 234 additions and 12 deletions

View File

@@ -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)."

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -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.
![world-editor-create-player-character-1](/talemate/img/0.26.0/world-editor-create-player-character-1.png)
![world-editor-create-player-character-1](/talemate/img/0.39.0/world-editor-create-player-character-1.png)
## 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.
![world-editor-create-player-character-2](/talemate/img/0.26.0/world-editor-create-player-character-2.png)
![world-editor-create-player-character-2](/talemate/img/0.39.0/world-editor-create-player-character-2.png)
Finally click the **Create Character** button.

View File

@@ -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.
![Introduce new character 1](/talemate/img/0.30.0/scene-tool-intro-char-1.png)
![Introduce new character 1](/talemate/img/0.39.0/scene-tool-intro-char-1.png)
##### 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.
![Introduce new character example dialogue](/talemate/img/0.39.0/scene-tool-intro-char-example-dialogue.png)
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).

View File

@@ -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.
![world-editor-create-player-character-1](/talemate/img/0.26.0/world-editor-create-player-character-1.png)
![world-editor-create-player-character-1](/talemate/img/0.39.0/world-editor-create-player-character-1.png)
### 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.
![world-editor-create-character-example-dialogue](/talemate/img/0.39.0/world-editor-create-character-example-dialogue.png)
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.

View File

@@ -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

View File

@@ -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")

View File

@@ -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

View File

@@ -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

View File

@@ -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 -%}

View File

@@ -108,6 +108,12 @@
<v-textarea v-model="newIntroduction.narrate_entry_direction" :label="`Narration direction for ${newIntroduction.name || 'the character'}\'s entry into the scene`" rows="4" auto-grow hide-details></v-textarea>
</v-col>
</v-row>
<v-row>
<v-col cols="12">
<v-checkbox v-model="newIntroduction.generate_example_dialogue" label="Generate example dialogue" color="primary" messages="Generate a few examples of how the character speaks."></v-checkbox>
<v-textarea v-if="newIntroduction.generate_example_dialogue" v-model="newIntroduction.example_dialogue_instructions" label="Example dialogue guidance" class="mt-2" rows="2" auto-grow placeholder="Speaks in short sentences, dry humor ..." hint="Optional guidance for how the example dialogue should be generated."></v-textarea>
</v-col>
</v-row>
<v-row>
<v-col cols="12">
<v-checkbox v-if="newIntroduction.templates.length > 0" v-model="newIntroduction.augment_attributes_enabled" label="Augment attributes" color="primary" messages="If your template selection includes character attributes, then this option will augment the character sheet with some additional attributes that are not already present."></v-checkbox>
@@ -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."
},

View File

@@ -53,6 +53,17 @@
<div v-if="character.generation_context.enabled">
<v-checkbox density="compact" :disabled="busy" v-model="character.generation_context.generateAttributes" label="Generate attributes" messages="Generate a few attributes based on the instructions and the description."></v-checkbox>
<v-checkbox density="compact" :disabled="busy" v-model="character.generation_context.generateExampleDialogue" label="Generate example dialogue" messages="Generate a few examples of how the character speaks."></v-checkbox>
<v-textarea
v-if="character.generation_context.generateExampleDialogue"
:disabled="busy"
v-model="character.generation_context.exampleDialogueInstructions"
label="Example dialogue guidance"
class="mt-2"
auto-grow rows="2"
placeholder="Speaks in short sentences, dry humor ..."
hint="Optional guidance for how the example dialogue should be generated.">
</v-textarea>
</div>
<v-checkbox density="compact" :disabled="!canBePlayer || busy" v-model="character.is_player" label="Controlled by the player" hide-details></v-checkbox>
@@ -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,

View File

@@ -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."""

View File

@@ -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
# ---------------------------------------------------------------------------