# How Calliope Enables Artists to Build Repeatable Interaction Strategies

> Learn how Calliope helps artists build repeatable interaction strategies. Separate artistic logic from infrastructure for reusable frame generation, persistence, and styling.

- Repository: [chrisimmel/calliope](https://github.com/chrisimmel/calliope)
- Tags: how-to-guide
- Published: 2026-02-27

---

**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`](https://github.com/chrisimmel/calliope/blob/main/calliope/tasks/handlers.py) within the `add_frame_task` function:

```python
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`](https://github.com/chrisimmel/calliope/blob/main/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`](https://github.com/chrisimmel/calliope/blob/main/calliope/strategies/registry.py). The `StoryStrategyRegistry` maintains a mapping of strategy names to classes using a decorator pattern:

```python
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`](https://github.com/chrisimmel/calliope/blob/main/calliope/tables/model_config.py)). The `add_frame_task` handler loads this configuration alongside API keys and request parameters:

```python
(
    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`](https://github.com/chrisimmel/calliope/blob/main/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`](https://github.com/chrisimmel/calliope/blob/main/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:

```python
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`](https://github.com/chrisimmel/calliope/blob/main/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:

```python
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:

```python

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

```bash
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`](https://github.com/chrisimmel/calliope/blob/main/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`](https://github.com/chrisimmel/calliope/blob/main/calliope/strategies/registry.py)** – Implements `StoryStrategyRegistry` and the registration decorator that maps strategy names to concrete classes.
- **[`calliope/strategies/tamarisk.py`](https://github.com/chrisimmel/calliope/blob/main/calliope/strategies/tamarisk.py)** – Reference implementation showing complex text generation, image synthesis, language translation, and debug data handling.
- **[`calliope/tables/story.py`](https://github.com/chrisimmel/calliope/blob/main/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`](https://github.com/chrisimmel/calliope/blob/main/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`](https://github.com/chrisimmel/calliope/blob/main/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`](https://github.com/chrisimmel/calliope/blob/main/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`](https://github.com/chrisimmel/calliope/blob/main/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`](https://github.com/chrisimmel/calliope/blob/main/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`](https://github.com/chrisimmel/calliope/blob/main/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.