What Are Story Strategies in Calliope and How Are They Implemented?
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, 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 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 implements a sophisticated workflow involving text generation, translation, and image inference.
The SimpleOneFrameStrategy in 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:
-
Client Request: A POST request to
/story/includes an optional"strategy"field in the JSON payload (e.g.,"tamarisk"). -
Handler Resolution: In
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 storedstrategy_name→ default ("tamarisk"). -
Registry Lookup: The handler calls
StoryStrategyRegistry.get_strategy_class(name)to retrieve the appropriate class (e.g.,TamariskStrategy). -
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. -
Frame Persistence: Within
get_frame_sequence, the strategy generates content and callsself._add_frame(...)(inherited fromStoryStrategy) to persist eachStoryFrameto the database. -
Response: The method returns a
StoryFrameSequenceResponseModelcontaining 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).
Step 2: Implement the Strategy Class
Subclass StoryStrategy, define a unique strategy_name, and implement async get_frame_sequence:
# 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:
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:
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:
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 |
Abstract base class defining the strategy interface and shared utilities. |
calliope/strategies/registry.py |
Global registry, decorator, and lookup helpers for strategy classes. |
calliope/strategies/tamarisk.py |
Example of a full-featured strategy (text generation, translation, image inference). |
calliope/strategies/simple_one_frame.py |
The original "single-frame" strategy used by the /story/ endpoint. |
calliope/tasks/handlers.py |
Orchestrates the selection and execution of a strategy based on request payload. |
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
StoryStrategyabstract base class incalliope/strategies/base.py, which defines theasync get_frame_sequencecontract that all implementations must fulfill. - Registration occurs through the
@StoryStrategyRegistry.register()decorator incalliope/strategies/registry.py, enabling dynamic discovery without hardcoded references. - Runtime execution flows from the API endpoint through
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, implementingget_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 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 or other core files.
What parameters does the get_frame_sequence method receive?
According to the StoryStrategy base class in 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.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →