# What Are Story Strategies in Calliope and How Are They Implemented?

> Discover Story Strategies in Calliope. Learn how these pluggable algorithms implement frame sequence creation for text, image, and video generation using a registry architecture.

- Repository: [chrisimmel/calliope](https://github.com/chrisimmel/calliope)
- Tags: deep-dive
- Published: 2026-02-27

---

**Story strategies in Calliope are pluggable algorithms that drive the creation of a story's frame sequence, implemented via a registry-based architecture where each strategy inherits from an abstract base class and implements an async `get_frame_sequence` method to generate text, images, or video frames.**

Story strategies form the core engine of the Calliope open-source storytelling platform. When clients interact with the `/story/` endpoint in the chrisimmel/calliope repository, they can specify which algorithm should generate their narrative sequence. Understanding how these story strategies in Calliope are structured and registered enables developers to extend the system with custom storytelling logic without modifying core request handling code.

## Understanding Story Strategies in Calliope

A story strategy is essentially a Python class that encapsulates a specific approach to narrative generation. When a client calls the `/story/` endpoint and passes a `strategy` parameter (e.g., `"tamarisk"`, `"simple-one-frame"`), Calliope looks up that name in a global registry, instantiates the corresponding class, and delegates frame generation to that instance.

The strategy is responsible for assembling the sequence of **frames**—discrete units containing text, images, or video—that constitute a story. Each frame persists to the database via the `_add_frame` helper method provided by the abstract base class.

## Core Architecture and Implementation

The implementation of story strategies in Calliope relies on three primary components: an abstract base class that defines the interface, a global registry that manages strategy discovery, and concrete implementations that provide specific narrative logic.

### The StoryStrategy Abstract Base Class

Located in [`calliope/strategies/base.py`](https://github.com/chrisimmel/calliope/blob/main/calliope/strategies/base.py), the `StoryStrategy` class defines the contract every strategy must fulfill. It declares the abstract async method `get_frame_sequence` and provides shared utilities for frame persistence and debug data generation.

Key elements from the source:

- **`async get_frame_sequence(...)`**: The core method that implementations must override. It receives parameters, image analysis, location metadata, configuration, API keys, story state, and an HTTP client.
- **`_add_frame(...)`**: Helper method to persist generated frames to the database.
- **`_get_default_debug_data(...)`**: Utility for standardizing debug output across strategies.

### The Global Strategy Registry

The `StoryStrategyRegistry` in [`calliope/strategies/registry.py`](https://github.com/chrisimmel/calliope/blob/main/calliope/strategies/registry.py) functions as a global singleton that maps `strategy_name` strings to strategy classes. It provides:

- **`@StoryStrategyRegistry.register()`**: A class decorator that automatically registers a strategy at import time.
- **`get_strategy_class(name)`**: Retrieves the class associated with a strategy name.
- **`get_all_strategy_names()`**: Returns a tuple of all available strategy identifiers.

This registry pattern allows Calliope to discover strategies dynamically without hardcoding class references in the request handlers.

### Concrete Strategy Implementation

Concrete strategies inherit from `StoryStrategy`, set a unique `strategy_name` attribute, and implement the `get_frame_sequence` method. For example, the `TamariskStrategy` in [`calliope/strategies/tamarisk.py`](https://github.com/chrisimmel/calliope/blob/main/calliope/strategies/tamarisk.py) implements a sophisticated workflow involving text generation, translation, and image inference.

The `SimpleOneFrameStrategy` in [`calliope/strategies/simple_one_frame.py`](https://github.com/chrisimmel/calliope/blob/main/calliope/strategies/simple_one_frame.py) provides a minimal example that generates a single static frame, useful for testing and simple use cases.

## How Story Strategies Execute at Runtime

The execution flow for story strategies in Calliope follows a clear path from HTTP request to frame generation:

1. **Client Request**: A POST request to `/story/` includes an optional `"strategy"` field in the JSON payload (e.g., `"tamarisk"`).

2. **Handler Resolution**: In [`calliope/tasks/handlers.py`](https://github.com/chrisimmel/calliope/blob/main/calliope/tasks/handlers.py), the task handler examines the request payload. It resolves the strategy name using a fallback chain: explicit request parameter → story's stored `strategy_name` → default (`"tamarisk"`).

3. **Registry Lookup**: The handler calls `StoryStrategyRegistry.get_strategy_class(name)` to retrieve the appropriate class (e.g., `TamariskStrategy`).

4. **Instantiation and Execution**: The handler instantiates the strategy class and awaits `instance.get_frame_sequence(...)`, passing all necessary context including parameters, image analysis, and HTTP clients.

5. **Frame Persistence**: Within `get_frame_sequence`, the strategy generates content and calls `self._add_frame(...)` (inherited from `StoryStrategy`) to persist each `StoryFrame` to the database.

6. **Response**: The method returns a `StoryFrameSequenceResponseModel` containing the generated frames, which the handler returns to the client.

## Creating Custom Story Strategies in Calliope

Extending Calliope with a new story strategy involves four steps: creating a Python module, subclassing the base strategy, implementing the frame generation logic, and registering the class with the decorator.

### Step 1: Create the Strategy File

Create a new Python file in the `calliope/strategies/` directory (e.g., [`my_strategy.py`](https://github.com/chrisimmel/calliope/blob/main/my_strategy.py)).

### Step 2: Implement the Strategy Class

Subclass `StoryStrategy`, define a unique `strategy_name`, and implement `async get_frame_sequence`:

```python

# calliope/strategies/my_strategy.py

from calliope.strategies.base import StoryStrategy
from calliope.strategies.registry import StoryStrategyRegistry

@StoryStrategyRegistry.register()
class MyStrategy(StoryStrategy):
    """A very simple strategy that returns a single static frame."""
    strategy_name = "my-strategy"

    async def get_frame_sequence(
        self,
        parameters,
        image_analysis,
        location_metadata,
        strategy_config,
        keys,
        sparrow_state,
        story,
        httpx_client,
    ):
        # Produce static text and a placeholder image.

        text = "A quiet meadow at sunrise."
        image = None  # Could load a pre-generated image here.

        frame = await self._add_frame(
            story=story,
            image=image,
            text=text,
            frame_number=await story.get_num_frames(),
            debug_data=self._get_default_debug_data(parameters, strategy_config, "static"),
            errors=[],
        )
        return StoryFrameSequenceResponseModel(frames=[frame])

```

### Step 3: Register the Strategy

The `@StoryStrategyRegistry.register()` decorator automatically adds the class to the global map when the module is imported. Ensure your module is imported during application startup.

### Step 4: Configure Defaults (Optional)

Add the strategy name to your `StrategyConfig` database rows or configuration files so it can be selected as a default without requiring an explicit request parameter.

## Working with Story Strategies: Code Examples

### Calling the API with a Specific Strategy

Clients can specify which story strategy in Calliope to use by including the `strategy` field in the JSON payload:

```bash
curl -X POST https://api.calliope.dev/story/ \
  -H "Content-Type: application/json" \
  -d '{
        "client_id": "demo123",
        "strategy": "tamarisk",
        "output_image_style": "A pastel watercolor."
      }'

```

The request is routed to the Tamarisk strategy as described in the architecture section.

### Registering a Custom Strategy at Runtime

For dynamic extension without modifying source files, you can define and register a strategy at runtime:

```python
from calliope.strategies.registry import StoryStrategyRegistry
from calliope.strategies.base import StoryStrategy

# Dynamically define a strategy

class EchoStrategy(StoryStrategy):
    strategy_name = "echo"

    async def get_frame_sequence(self, *args, **kwargs):
        story = kwargs["story"]
        text = "Echo: " + (kwargs["parameters"].input_text or "nothing")
        return await self._add_frame(
            story=story,
            image=None,
            text=text,
            frame_number=await story.get_num_frames(),
            debug_data=self._get_default_debug_data(kwargs["parameters"], kwargs["strategy_config"], "echo"),
            errors=[],
        )

# Register it without a static file

StoryStrategyRegistry.register()(EchoStrategy)

# Now the API can be called with `?strategy=echo`

```

### Listing Available Strategies

To discover which story strategies in Calliope are currently registered:

```python
from calliope.strategies.registry import StoryStrategyRegistry

print("Registered strategies:", StoryStrategyRegistry.get_all_strategy_names())

# → ('tamarisk', 'simple-one-frame', 'show-this-frame', ... )

```

## Key Files and Locations

Understanding the file structure helps navigate the implementation of story strategies in Calliope:

| File | Purpose |
|---|---|
| [`calliope/strategies/base.py`](https://github.com/chrisimmel/calliope/blob/main/calliope/strategies/base.py) | Abstract base class defining the strategy interface and shared utilities. |
| [`calliope/strategies/registry.py`](https://github.com/chrisimmel/calliope/blob/main/calliope/strategies/registry.py) | Global registry, decorator, and lookup helpers for strategy classes. |
| [`calliope/strategies/tamarisk.py`](https://github.com/chrisimmel/calliope/blob/main/calliope/strategies/tamarisk.py) | Example of a full-featured strategy (text generation, translation, image inference). |
| [`calliope/strategies/simple_one_frame.py`](https://github.com/chrisimmel/calliope/blob/main/calliope/strategies/simple_one_frame.py) | The original "single-frame" strategy used by the `/story/` endpoint. |
| [`calliope/tasks/handlers.py`](https://github.com/chrisimmel/calliope/blob/main/calliope/tasks/handlers.py) | Orchestrates the selection and execution of a strategy based on request payload. |
| [`docs/api.md`](https://github.com/chrisimmel/calliope/blob/main/docs/api.md) | Documents the `strategy` query parameter for the public API. |

These components together give Calliope a **plug-in architecture** for story generation, making it easy to add, replace, or experiment with new storytelling algorithms without touching the core request handling code.

## Summary

- **Story strategies in Calliope** are pluggable algorithms that generate sequences of story frames (text, images, or video) via a standardized interface.
- The architecture centers on the `StoryStrategy` abstract base class in [`calliope/strategies/base.py`](https://github.com/chrisimmel/calliope/blob/main/calliope/strategies/base.py), which defines the `async get_frame_sequence` contract that all implementations must fulfill.
- Registration occurs through the `@StoryStrategyRegistry.register()` decorator in [`calliope/strategies/registry.py`](https://github.com/chrisimmel/calliope/blob/main/calliope/strategies/registry.py), enabling dynamic discovery without hardcoded references.
- Runtime execution flows from the API endpoint through [`calliope/tasks/handlers.py`](https://github.com/chrisimmel/calliope/blob/main/calliope/tasks/handlers.py), which resolves the strategy name, instantiates the appropriate class, and awaits frame generation.
- Developers can extend the system by subclassing `StoryStrategy`, implementing `get_frame_sequence`, and decorating the class for automatic registration.

## Frequently Asked Questions

### What is the default story strategy in Calliope if none is specified?

If a client request does not specify a strategy parameter, the task handler in [`calliope/tasks/handlers.py`](https://github.com/chrisimmel/calliope/blob/main/calliope/tasks/handlers.py) falls back to the story's stored `strategy_name` attribute, and ultimately defaults to `"tamarisk"` if no value is found. This ensures that every story generation request has a valid strategy algorithm assigned.

### How do I register a new story strategy without modifying the core Calliope source code?

You can create a new Python module in the `calliope/strategies/` directory (or any importable location) that subclasses `StoryStrategy`, sets a unique `strategy_name`, implements `async get_frame_sequence`, and applies the `@StoryStrategyRegistry.register()` decorator. When your module is imported during application startup, the decorator automatically adds the class to the global registry, making it available to the API without touching [`calliope/tasks/handlers.py`](https://github.com/chrisimmel/calliope/blob/main/calliope/tasks/handlers.py) or other core files.

### What parameters does the `get_frame_sequence` method receive?

According to the `StoryStrategy` base class in [`calliope/strategies/base.py`](https://github.com/chrisimmel/calliope/blob/main/calliope/strategies/base.py), the `get_frame_sequence` method receives the following parameters: `parameters` (request configuration), `image_analysis` (analyzed input images), `location_metadata` (geographic/temporal context), `strategy_config` (strategy-specific settings), `keys` (API credentials), `sparrow_state` (internal state), `story` (the story object being built), and `httpx_client` (async HTTP client for external API calls).

### Can I use multiple story strategies in a single story?

While a single story generation request invokes exactly one strategy (determined by the `strategy` parameter or default), you can change strategies between requests to the same story ID. The story object persists frames generated by different strategies over time, effectively allowing a story to contain frames from multiple algorithms, though each individual frame sequence generation uses one specific strategy implementation.