Core Components of the Calliope Architecture: A Modular FastAPI Backend for AI Story Generation

The Calliope architecture comprises ten modular layers—including a FastAPI web layer, Piccolo ORM data layer, pluggable story-generation strategies, and inference engine wrappers—that collectively orchestrate AI-powered narrative generation.

Calliope is an open-source story generation framework built by chrisimmel as a modular FastAPI backend. Understanding the core components of the Calliope architecture is essential for extending its capabilities, whether you're adding new AI providers or implementing custom narrative strategies.

FastAPI Application and Configuration Layer

The entry point for the entire system is calliope/app.py, which bootstraps the FastAPI service, configures structured logging, mounts the admin UI and static assets, and registers all API routers. This file handles the application lifecycle including startup and shutdown hooks for database connections.

Configuration is centralized in calliope/settings.py through a single Pydantic-based Settings object. This class pulls values from environment variables and supplies sensible defaults for database connections, API keys, storage backends, and inference model endpoints. By centralizing configuration, the architecture ensures that all components reference the same source of truth for runtime parameters.

Data Persistence with Piccolo ORM

The data layer relies on Piccolo ORM tables that map directly to PostgreSQL tables. These models provide asynchronous CRUD helpers that the rest of the system uses to persist stories, frames, and media assets.

Key table definitions include:

Pydantic Models for Type Safety

The architecture enforces strict type safety through Pydantic models defined in calliope/models/__init__.py and related modules. These models define request and response schemas for API endpoints, validation rules for incoming data, and type-safe data structures exchanged between internal components.

By separating Pydantic models from ORM tables, Calliope maintains a clean boundary between database representation and API contracts, allowing the data layer to evolve independently from the web interface.

Pluggable Story Generation Strategies

The business logic layer implements a Strategy pattern that makes the system extensible. Each strategy is a Python class that implements the StoryStrategy interface defined in calliope/strategies/base.py.

Base Strategy Interface

The abstract base class in calliope/strategies/base.py defines the core contract that all strategies must fulfill. It specifies the get_frame_sequence method signature and provides helper methods like _add_frame for constructing story frames. The base class also handles debug data collection and error propagation.

Strategy Registry

The decorator-based registry in calliope/strategies/registry.py automatically discovers and registers any StoryStrategy subclass. When a developer applies the @StoryStrategyRegistry.register() decorator to a new strategy, it becomes immediately available for selection by name through the API.

Example Strategy Implementation

The calliope/strategies/simple_one_frame.py module demonstrates the simplest possible implementation, generating a single frame per request. This serves as a reference implementation for developers building custom strategies.

Register a New Story Strategy

To extend Calliope with custom generation logic, create a class that inherits from StoryStrategy and register it with the decorator:

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

@StoryStrategyRegistry.register()
class MyCustomStrategy(StoryStrategy):
    """Generate a story frame that always uses a fixed image."""
    strategy_name = "my-custom"

    async def get_frame_sequence(
        self,
        parameters: FramesRequestParamsModel,
        image_analysis,
        location_metadata: FullLocationMetadata,
        strategy_config: StrategyConfig,
        keys: KeysModel,
        sparrow_state: SparrowState,
        story: Story,
        httpx_client,
    ):
        # Very simple: always return the same image + supplied text.

        frame_number = await story.get_num_frames()
        image = await Image.get_or_create(url="https://example.com/static.jpg")
        text = parameters.input_text or "A mysterious tale unfolds."
        frame = await self._add_frame(
            story, image, text, frame_number, {}, []
        )
        return StoryFrameSequenceResponseModel(frames=[frame], debug_data={}, errors=[])

Inference Engine Abstraction

Calliope abstracts external AI services through inference engines located in calliope/inference/engines/. These thin wrappers expose a uniform async API—such as text_to_text, text_to_image, and text_to_video—that strategies can call without worrying about provider-specific details.

For example, calliope/inference/engines/openai_text.py implements the OpenAI GPT integration, handling authentication, request formatting, and response parsing. This pattern allows developers to swap between OpenAI, Stability, Replicate, or Azure Vision by changing configuration rather than code.

Add a New Inference Engine

To integrate a custom AI provider, implement an async function that matches the expected signature:


# calliope/inference/engines/echo_text.py

from calliope.models import KeysModel
import httpx

async def echo_text(
    httpx_client: httpx.AsyncClient,
    prompt: str,
    model_config,
    keys: KeysModel,
) -> str:
    # Simply returns the prompt unchanged – useful for testing.

    return prompt

Register this engine in calliope/inference/__init__.py and reference it from a strategy via the text_to_text_model_config field.

API Routing and Endpoints

The web interface exposes functionality through FastAPI routers organized under versioned prefixes. The primary entry point for story generation is calliope/routes/v1/story.py, which defines the /v1/story endpoint.

This route validates incoming requests against Pydantic models, looks up the requested strategy from the registry, invokes the appropriate generation logic, and returns JSON responses containing the generated frames. Additional routers handle configuration (/v1/config) and media assets (/v1/media).

Call the Story API

Clients interact with the generation pipeline via standard HTTP requests:

import httpx
import asyncio

async def generate_one_frame():
    async with httpx.AsyncClient(base_url="http://localhost:1234") as client:
        payload = {
            "client_id": "demo",
            "input_text": "A dragon soaring over a mountain.",
            "strategy_name": "simple-one-frame"
        }
        resp = await client.post("/v1/story", json=payload)
        resp.raise_for_status()
        data = resp.json()
        print("Created frame:", data["frames"][0]["text"])

asyncio.run(generate_one_frame())

Utilities and State Management

Supporting infrastructure resides in calliope/utils/ and calliope/storage/. The utility modules provide cross-cutting concerns such as sequential file naming (calliope/utils/file.py), image metadata extraction (calliope/utils/image.py), and API key authentication (calliope/utils/authentication.py).

Runtime state persistence is handled by calliope/storage/state_manager.py, which maintains client-specific data such as Sparrow state and optionally synchronizes with Firebase for real-time updates. This separation ensures that stateful logic remains isolated from the stateless API layer.

Summary

  • FastAPI Application Layer: Boots the service in calliope/app.py and centralizes configuration via Pydantic Settings in calliope/settings.py.
  • Data Layer: Piccolo ORM tables in calliope/tables/ provide async PostgreSQL access for stories, frames, images, and configuration.
  • Type Safety: Pydantic models in calliope/models/ enforce API contracts and internal data validation.
  • Business Logic: Pluggable StoryStrategy classes in calliope/strategies/ implement the Strategy pattern with automatic registry discovery.
  • AI Abstraction: Inference engines in calliope/inference/engines/ unify access to OpenAI, Stability, and other providers behind consistent async interfaces.
  • Web Interface: FastAPI routers in calliope/routes/v1/ expose versioned endpoints for story generation and media management.
  • Infrastructure: Utility modules and state managers handle authentication, file naming, image processing, and runtime state persistence.

Frequently Asked Questions

What is the role of the Strategy pattern in Calliope?

The Strategy pattern decouples story generation algorithms from the API layer. By implementing the StoryStrategy interface defined in calliope/strategies/base.py, developers can create custom narrative generators—such as single-frame producers or multi-chapter engines—that the system discovers automatically via the decorator-based registry in calliope/strategies/registry.py.

How does Calliope handle database operations?

Calliope uses the Piccolo ORM to map Python classes to PostgreSQL tables. The table definitions in calliope/tables/story.py, calliope/tables/image.py, and related files expose asynchronous CRUD methods. This design allows story generation strategies to persist frames and media assets without blocking the event loop, maintaining the high concurrency expected of FastAPI applications.

Can I add custom inference providers to Calliope?

Yes. You can extend the system by creating a new module in calliope/inference/engines/ that implements the required async interface—such as text_to_text or text_to_image—and then registering it in calliope/inference/__init__.py. Strategies reference these engines through configuration objects, allowing you to swap between OpenAI, Stability, Replicate, or custom providers without modifying business logic.

What makes Calliope's architecture modular?

The architecture achieves modularity through strict separation of concerns: the web layer (FastAPI routers) is isolated from the data layer (Piccolo tables) and the business logic layer (pluggable strategies). Dependency injection via Pydantic settings and the strategy registry pattern allow components to be added, removed, or swapped without cascading changes across the codebase. This design supports multiple inference backends, diverse narrative strategies, and various storage configurations within a single unified framework.

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 →