mirror of
https://github.com/vegu-ai/talemate.git
synced 2026-09-02 03:59:12 +02:00
Major Features - API key encryption at rest using Fernet (OS keyring with file fallback) - Prompt Manager: unified UI with template groups, priority ordering, override tracking, response extractors - Scene context history review panel with token budgets and best-fit mode - Multiple concurrent director chats with auto-generated titles - Granular scene state reset dialog - Time passage insert/edit/delete in scene view - Image analysis via OpenAI-compatible and Talemate Client backends - Volatile context placement after scene history for improved prompt caching Improvements - Configurable narrator generation length per narration type - AI Aware conversation mode - Summarizer: custom instructions, writing style inclusion, short line filtering - Anthropic: adaptive thinking support, updated model list (opus-4-5/4-6, haiku-4-5) - Google: gemini-3.1 support - World editor: generate from topic, quick create state reinforcement, reorganized menus - Node editor: promote scene modules to global - Frontend: version mismatch detection, hideable bracket content, required scene name - TTS: improved pause handling, audio tag support for vocal markers (ElevenLabs v3) - Writing style template for AI-generated instructions - Added Kimi.jinja2 LLM prompt template - Option to disable character names in stopping strings - Client response length enforcement options - Graduated token count sliders - Increased summarizer token threshold max Bugfixes - Fix bracket/paren/brace terminators stripped from message ends - Fix colon in conversation causing content loss - Fix "Use as reference" navigating to blank page - Fix avatar regeneration and manual regenerate - Fix conversation agent ignoring generation length - Fix duplicate length instructions with reasoning enabled - Fix trailing newline on message edits - Fix summarize dialogue sending too much context with layered history - Fix layered history inspection and construction issues - Fix empty response handling in summarization - Fix context ID dot notation with dotted character names - Fix recursive retry in focal agent - Fix leading whitespace causing duplicate prepared responses - Fix summarization not stripping ANALYSIS OF lines - Fix template group selection/removal in prompt manager - Fix multiline text in parentheses/brackets parser - Fix determine_character_name resolution - Fix character activate/deactivate desyncing creative menu - Fix character image generation missing context - Fix LMStudio client not sending token limits - Fix Recent Scene images on newer Chromium - Fix sequential reinforcement messages cut off at first linebreak - Fix reinforcement removal not clearing state - Fixes #252, #256, #258 Deprecations - Removed context investigations (replaced by AI-assisted RAG mixin) - Removed deprecated prompt templates (fix-continuity-errors, fix-exposition, etc.) - Removed conversation/edit.jinja2, auto break repetition, CLI reset layered history --------- Co-authored-by: theDTV2 <47825738+theDTV2@users.noreply.github.com>
189 lines
5.6 KiB
Python
189 lines
5.6 KiB
Python
import os
|
|
import json
|
|
import pytest
|
|
import enum
|
|
import pydantic
|
|
import talemate.game.engine.nodes.load_definitions # noqa: F401
|
|
import talemate.agents.director # noqa: F401
|
|
from talemate.context import ActiveScene
|
|
from talemate.game.engine.nodes.core import (
|
|
Graph,
|
|
GraphState,
|
|
)
|
|
import structlog
|
|
from talemate.game.engine.nodes.layout import load_graph_from_file
|
|
from talemate.game.engine.nodes.registry import import_talemate_node_definitions
|
|
|
|
from conftest import MockClientContext, MockScene, bootstrap_scene
|
|
|
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
TEST_GRAPH_DIR = os.path.join(BASE_DIR, "data", "graphs")
|
|
RESULTS_DIR = os.path.join(BASE_DIR, "data", "graphs", "results")
|
|
UPDATE_RESULTS = False
|
|
|
|
log = structlog.get_logger("talemate.test_graphs")
|
|
|
|
|
|
# This runs once for the entire test session
|
|
@pytest.fixture(scope="session", autouse=True)
|
|
def load_node_definitions():
|
|
import_talemate_node_definitions()
|
|
|
|
|
|
def load_test_graph(name) -> Graph:
|
|
path = os.path.join(TEST_GRAPH_DIR, f"{name}.json")
|
|
graph, _ = load_graph_from_file(path)
|
|
return graph
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_scene():
|
|
scene = MockScene()
|
|
bootstrap_scene(scene)
|
|
return scene
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_scene_with_assets():
|
|
scene = MockScene()
|
|
bootstrap_scene(scene)
|
|
|
|
# Load test assets from the test scene file
|
|
test_scene_path = os.path.join(
|
|
BASE_DIR, "data", "scenes", "talemate-laboratory", "talemate-lab.json"
|
|
)
|
|
with open(test_scene_path, "r") as f:
|
|
test_scene_data = json.load(f)
|
|
|
|
# Override scenes_dir to point to test data directory
|
|
test_scenes_dir = os.path.join(BASE_DIR, "data", "scenes")
|
|
scene.scenes_dir = lambda: test_scenes_dir
|
|
scene.project_name = "talemate-laboratory"
|
|
|
|
# Create library.json file with assets from the scene file
|
|
if "assets" in test_scene_data and "assets" in test_scene_data["assets"]:
|
|
assets_dict = test_scene_data["assets"]["assets"]
|
|
# Ensure assets directory exists
|
|
assets_dir = os.path.join(test_scenes_dir, "talemate-laboratory", "assets")
|
|
os.makedirs(assets_dir, exist_ok=True)
|
|
|
|
# Create library.json file
|
|
library_path = os.path.join(assets_dir, "library.json")
|
|
with open(library_path, "w") as f:
|
|
json.dump({"assets": assets_dict}, f, indent=2)
|
|
|
|
return scene
|
|
|
|
|
|
def serialize_state(obj):
|
|
"""Custom JSON serializer for Pydantic models"""
|
|
if isinstance(obj, pydantic.BaseModel):
|
|
return obj.model_dump()
|
|
raise TypeError(f"Object of type {type(obj)} is not JSON serializable")
|
|
|
|
|
|
def normalize_state(data):
|
|
"""Convert Pydantic models to dicts for comparison"""
|
|
if isinstance(data, pydantic.BaseModel):
|
|
return data.model_dump()
|
|
elif isinstance(data, enum.Enum):
|
|
return data.value
|
|
elif isinstance(data, dict):
|
|
return {k: normalize_state(v) for k, v in data.items()}
|
|
elif isinstance(data, list):
|
|
return [normalize_state(item) for item in data]
|
|
return data
|
|
|
|
|
|
def make_assert_fn(name: str, write_results: bool = False):
|
|
async def assert_fn(state: GraphState):
|
|
if write_results or not os.path.exists(
|
|
os.path.join(RESULTS_DIR, f"{name}.json")
|
|
):
|
|
with open(os.path.join(RESULTS_DIR, f"{name}.json"), "w") as f:
|
|
json.dump(state.shared, f, indent=4, default=serialize_state)
|
|
else:
|
|
with open(os.path.join(RESULTS_DIR, f"{name}.json"), "r") as f:
|
|
expected = json.load(f)
|
|
|
|
# Normalize state.shared to convert Pydantic models to dicts for comparison
|
|
normalized_shared = normalize_state(state.shared)
|
|
assert normalized_shared == expected
|
|
|
|
return assert_fn
|
|
|
|
|
|
def make_graph_test(name: str, write_results: bool = False):
|
|
async def test_graph(scene):
|
|
assert_fn = make_assert_fn(name, write_results)
|
|
|
|
def error_handler(state, error: Exception):
|
|
raise error
|
|
|
|
with ActiveScene(scene):
|
|
graph = load_test_graph(name)
|
|
assert graph is not None
|
|
graph.callbacks.append(assert_fn)
|
|
graph.error_handlers.append(error_handler)
|
|
await graph.execute()
|
|
|
|
return test_graph
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_graph_core(mock_scene):
|
|
fn = make_graph_test("test-harness-core", False)
|
|
await fn(mock_scene)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_graph_data(mock_scene):
|
|
fn = make_graph_test("test-harness-data", False)
|
|
await fn(mock_scene)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_graph_scene(mock_scene):
|
|
fn = make_graph_test("test-harness-scene", False)
|
|
await fn(mock_scene)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_graph_functions(mock_scene):
|
|
fn = make_graph_test("test-harness-functions", False)
|
|
await fn(mock_scene)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_graph_agents(mock_scene):
|
|
fn = make_graph_test("test-harness-agents", False)
|
|
await fn(mock_scene)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_graph_prompt(mock_scene):
|
|
fn = make_graph_test("test-harness-prompt", False)
|
|
|
|
async with MockClientContext() as client_reponses:
|
|
client_reponses.append("The sum of 1 and 5 is 6.")
|
|
client_reponses.append('```json\n{\n "result": 6\n}\n```')
|
|
await fn(mock_scene)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_graph_collectors(mock_scene):
|
|
fn = make_graph_test("test-harness-collectors", False)
|
|
await fn(mock_scene)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_graph_context_ids(mock_scene):
|
|
fn = make_graph_test("test-harness-context-ids", False)
|
|
await fn(mock_scene)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_graph_assets(mock_scene_with_assets):
|
|
fn = make_graph_test("test-harness-assets", False)
|
|
await fn(mock_scene_with_assets)
|