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

> Explore the ten modular layers of the Calliope architecture including FastAPI Piccolo ORM and AI inference engines to power narrative generation.

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

---

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

- [`calliope/tables/story.py`](https://github.com/chrisimmel/calliope/blob/main/calliope/tables/story.py) – Defines the `Story` and `Frame` tables that store narrative sequences and individual story frames.
- [`calliope/tables/image.py`](https://github.com/chrisimmel/calliope/blob/main/calliope/tables/image.py) – Manages `Image` records including metadata and storage references.
- [`calliope/tables/video.py`](https://github.com/chrisimmel/calliope/blob/main/calliope/tables/video.py) – Handles `Video` assets associated with story frames.
- [`calliope/tables/config.py`](https://github.com/chrisimmel/calliope/blob/main/calliope/tables/config.py) – Stores `StrategyConfig` records that persist runtime parameters for different generation strategies.

## Pydantic Models for Type Safety

The architecture enforces strict type safety through Pydantic models defined in [`calliope/models/__init__.py`](https://github.com/chrisimmel/calliope/blob/main/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`](https://github.com/chrisimmel/calliope/blob/main/calliope/strategies/base.py).

### Base Strategy Interface

The abstract base class in [`calliope/strategies/base.py`](https://github.com/chrisimmel/calliope/blob/main/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`](https://github.com/chrisimmel/calliope/blob/main/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`](https://github.com/chrisimmel/calliope/blob/main/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:

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

```python

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

```python
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`](https://github.com/chrisimmel/calliope/blob/main/calliope/utils/file.py)), image metadata extraction ([`calliope/utils/image.py`](https://github.com/chrisimmel/calliope/blob/main/calliope/utils/image.py)), and API key authentication ([`calliope/utils/authentication.py`](https://github.com/chrisimmel/calliope/blob/main/calliope/utils/authentication.py)).

Runtime state persistence is handled by [`calliope/storage/state_manager.py`](https://github.com/chrisimmel/calliope/blob/main/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`](https://github.com/chrisimmel/calliope/blob/main/calliope/app.py) and centralizes configuration via Pydantic `Settings` in [`calliope/settings.py`](https://github.com/chrisimmel/calliope/blob/main/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`](https://github.com/chrisimmel/calliope/blob/main/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`](https://github.com/chrisimmel/calliope/blob/main/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`](https://github.com/chrisimmel/calliope/blob/main/calliope/tables/story.py), [`calliope/tables/image.py`](https://github.com/chrisimmel/calliope/blob/main/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`](https://github.com/chrisimmel/calliope/blob/main/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.