How Calliope Enables Artists to Build Repeatable Interaction Strategies

Calliope implements a registry-based strategy pattern that separates artistic logic from infrastructure, allowing artists to define self-contained, reusable classes that govern how frames are generated, persisted, and styled across any story.

Calliope is an open-source platform for AI-assisted storytelling that empowers creators to automate narrative workflows. At its core, the framework enables repeatable interaction strategies through a decoupled architecture where artistic behavior is encapsulated in discrete, configurable strategy classes. By isolating the "how" of content creation from storage and inference concerns, Calliope allows the same creative logic to be applied consistently across multiple stories, users, and deployments.

The Strategy Pattern Architecture

Calliope’s design centers on the separation of artistic storytelling logic from surrounding infrastructure. This separation is achieved through a formal strategy pattern involving registry lookup, configuration-driven behavior, and abstract base classes.

Story-to-Strategy Resolution

Every story in Calliope can specify which artistic strategy governs its generation. The resolution process begins in calliope/tasks/handlers.py within the add_frame_task function:

story = await get_story(story_id) if story_id else None
strategy_name = (story and story.strategy_name) or "tamarisk"

If the Story model (defined in calliope/tables/story.py) contains a strategy_name field, that value determines which logic to apply. Otherwise, the system falls back to a default strategy such as "tamarisk". This lookup mechanism ensures that repeatable interaction strategies are selected dynamically at runtime based on database state rather than hardcoded logic.

The Strategy Registry

Concrete strategies are registered via a global registry defined in calliope/strategies/registry.py. The StoryStrategyRegistry maintains a mapping of strategy names to classes using a decorator pattern:

strategy_class = StoryStrategyRegistry.get_strategy_class(strategy_name)

When a developer decorates a class with @StoryStrategyRegistry.register(), the registry automatically adds that class to its internal _story_strategies_by_name dictionary using the class’s strategy_name attribute. This design enables artists to introduce new interaction patterns simply by defining and registering a subclass without modifying core platform code.

Configuration-Driven Behavior

Once resolved, the strategy receives its operational parameters from the StrategyConfig table (defined in calliope/tables/model_config.py). The add_frame_task handler loads this configuration alongside API keys and request parameters:

(
    parameters,
    keys,
    strategy_config,
) = await get_sparrow_story_parameters_and_keys(request_params)

Inside a concrete strategy, this configuration object determines model selections, prompt templates, languages, and image styles. For example, in calliope/strategies/tamarisk.py, the strategy accesses strategy_config.text_to_text_model_config to determine which LLM to invoke. Because configuration lives in the database, artists can modify model slugs, seed prompts, or stylistic parameters through administrative interfaces without touching source code, ensuring repeatable workflows remain flexible and updatable.

Frame Generation Workflow

The actual generation of narrative frames follows a standardized pipeline that guarantees consistency while allowing artistic customization.

Task Handler Orchestration

The add_frame_task function in calliope/tasks/handlers.py serves as the orchestration layer. After resolving the strategy name and class, it instantiates the strategy and invokes its primary entry point:

strategy = strategy_class()
return await strategy.get_frame_sequence(
    parameters=parameters,
    image_analysis=image_analysis,
    location_metadata=location_metadata,
    strategy_config=strategy_config,
    keys=keys,
    sparrow_state=sparrow_state,
    story=story,
    httpx_client=httpx_client,
)

This method signature is defined by the abstract base class StoryStrategy in calliope/strategies/base.py. By standardizing the interface, Calliope ensures that every strategy—whether simple or complex—receives the same contextual information: request parameters, location metadata, configuration objects, and HTTP clients for external API calls.

Base Class Persistence Helpers

The StoryStrategy base class provides protected methods that standardize frame persistence across all implementations. After generating content, strategies invoke _add_frame() to persist data:

frame = StoryFrame(
    story=story.id,
    number=frame_number,
    image=image,
    source_image=image,
    video=video,
    text=text,
    min_duration_seconds=DEFAULT_MIN_DURATION_SECONDS,
    metadata={**debug_data, "errors": errors},
)
await frame.save().run()

This helper automatically updates the parent story’s title, slug, and thumbnail when appropriate, ensuring that metadata remains synchronized without requiring boilerplate code in each strategy implementation. This consistency is crucial for maintaining repeatable interaction strategies that behave predictably across thousands of frame generations.

Building a Custom Strategy

Artists can create new repeatable workflows by subclassing StoryStrategy and implementing the get_frame_sequence() method. Below is a complete example of a "Haiku" strategy that generates three-line poems with accompanying watercolor images:


# File: calliope/strategies/haiku.py

from typing import Any, Dict, Optional

from calliope.models import FramesRequestParamsModel, FullLocationMetadata, KeysModel
from calliope.strategies.base import StoryStrategy
from calliope.strategies.registry import StoryStrategyRegistry
from calliope.tables import StrategyConfig, Story, SparrowState

@StoryStrategyRegistry.register()
class HaikuStrategy(StoryStrategy):
    """Generate a haiku (5-7-5 syllable) and an accompanying image."""
    strategy_name = "haiku"

    async def get_frame_sequence(
        self,
        parameters: FramesRequestParamsModel,
        image_analysis: Optional[Dict[str, Any]],
        location_metadata: FullLocationMetadata,
        strategy_config: StrategyConfig,
        keys: KeysModel,
        sparrow_state: SparrowState,
        story: Story,
        httpx_client,
    ):
        # Build a prompt using location context

        situation = location_metadata.description or "a quiet place"
        prompt = (
            f"Write a haiku in English about {situation}. "
            "Make the first line 5 syllables, the second 7, the third 5."
        )

        # Generate text using configured models

        haiku = await self._run_text_model(prompt, strategy_config, keys, httpx_client)

        # Generate matching image

        style = parameters.output_image_style or "watercolor on textured paper"
        image_prompt = f"{style} illustration of: {haiku}"
        image_path = await self._run_image_model(
            image_prompt, strategy_config, keys, httpx_client, parameters
        )

        # Persist using base class helper

        frame_number = await story.get_num_frames()
        frame = await self._add_frame(
            story,
            image=image_path,
            text=haiku,
            frame_number=frame_number,
            debug_data={"prompt": prompt, "image_prompt": image_prompt},
            errors=[],
        )
        return StoryFrameSequenceResponseModel(frames=[frame])

Key aspects of this implementation demonstrate the power of repeatable interaction strategies:

  • Registration: The @StoryStrategyRegistry.register() decorator automatically makes the strategy available system-wide under the name "haiku".
  • Configuration Abstraction: Model choices come from strategy_config, allowing artists to switch from GPT-4 to Claude or from DALL-E to Midjourney without code changes.
  • Statelessness: The strategy relies entirely on incoming parameters and database state, ensuring that identical inputs produce consistent outputs (modulo non-deterministic LLM sampling).

To use this strategy, create a story referencing the strategy_name field:

curl -X POST https://api.calliope.dev/story \
  -H "Content-Type: application/json" \
  -d '{"title":"Morning Breeze","strategy_name":"haiku"}'

Key Source Files for Strategy Development

Understanding these files is essential for extending Calliope with custom artistic logic:

  • calliope/strategies/base.py – Defines the StoryStrategy abstract base class and persistence helpers such as _add_frame(), _run_text_model(), and _run_image_model().
  • calliope/strategies/registry.py – Implements StoryStrategyRegistry and the registration decorator that maps strategy names to concrete classes.
  • calliope/strategies/tamarisk.py – Reference implementation showing complex text generation, image synthesis, language translation, and debug data handling.
  • calliope/tables/story.py – Contains the Story model with strategy_name, title/slug logic, and get_num_frames() helper methods.
  • calliope/tables/model_config.py – Houses StrategyConfig, the database model for persisting per-strategy model slugs, prompt templates, and stylistic parameters.
  • calliope/tasks/handlers.py – Contains add_frame_task(), the entry point that resolves strategies, loads configurations, and dispatches to get_frame_sequence().
  • calliope/storage/state_manager.py – Provides utilities for loading and saving Story and SparrowState objects within the strategy pipeline.

Summary

  • Strategy Pattern: Calliope isolates artistic logic from infrastructure through the StoryStrategy abstract base class and registry system.
  • Dynamic Resolution: The add_frame_task handler resolves strategies via the strategy_name field on the Story model, enabling per-story customization.
  • Configuration-Driven: StrategyConfig allows runtime modification of models, prompts, and languages without code deployments.
  • Automatic Persistence: Base class helpers like _add_frame() ensure consistent database updates and metadata synchronization across all strategies.
  • Registration Mechanism: The @StoryStrategyRegistry.register() decorator enables plug-and-play strategy development, making artistic logic truly repeatable across stories and deployments.

Frequently Asked Questions

What defines a repeatable interaction strategy in Calliope?

A repeatable interaction strategy is a self-contained Python class that inherits from StoryStrategy and implements the get_frame_sequence() method. Registered via StoryStrategyRegistry, these classes define how input parameters transform into narrative frames, ensuring the same artistic logic can be applied consistently across multiple stories, users, or API endpoints without code duplication.

How does the strategy registry enable code reuse?

The registry in calliope/strategies/registry.py maintains a global dictionary mapping strategy names to classes. By decorating a strategy with @StoryStrategyRegistry.register(), artists automatically make their logic available to the task handler. When add_frame_task calls StoryStrategyRegistry.get_strategy_class(strategy_name), it retrieves the appropriate class dynamically, allowing the same compiled code to service unlimited stories simply by changing the strategy_name field in the database.

Can I modify model parameters without changing strategy code?

Yes. Strategies consume a StrategyConfig object (from calliope/tables/model_config.py) that contains model slugs, prompt templates, and stylistic settings. Because this configuration is loaded at runtime from the database, artists can change which LLM or image generator a strategy uses—switching from GPT-4 to Claude or adjusting temperature settings—through administrative interfaces without modifying the strategy’s Python source code.

How does Calliope ensure frames are persisted consistently?

The StoryStrategy base class in calliope/strategies/base.py provides the _add_frame() helper method, which all concrete strategies use to save StoryFrame objects. This method standardizes the creation of frame records, handles metadata serialization, and automatically updates the parent story’s title, slug, and thumbnail when necessary. By centralizing persistence logic, Calliope guarantees that all repeatable interaction strategies maintain data integrity according to the same schema and business rules.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →