Back to Articles

ell: The Language Model Programming Library That Treats Prompts Like Git Commits

[ View on GitHub ]

ell: The Language Model Programming Library That Treats Prompts Like Git Commits

Hook

What if every prompt you wrote to an LLM was automatically versioned, diffed, and tracked like a Git commit—without you ever touching a version control command?

Context

Prompt engineering has a dirty secret: most developers treat prompts like configuration files, stuffing them into YAML configs or string templates scattered across codebases. When a prompt breaks in production, good luck figuring out which version was working last week. The typical workflow involves copying prompts into playgrounds, tweaking them manually, and then copy-pasting the "good" version back into code. There's no diff, no history, no systematic way to understand what changed and why.

This chaos exists because the tooling around large language models has focused on building applications—frameworks for RAG pipelines, agent orchestration, and chain composition. But the fundamental unit of LLM work, the prompt itself, has been treated as a second-class citizen. ell emerges from a different philosophy: prompts are code, and they deserve the same rigor, versioning, and observability we give to any other critical code path. Created as a language model programming library, ell makes prompts into versioned, serializable functions that automatically track their own evolution.

Technical Insight

At its core, ell introduces the concept of Language Model Programs (LMPs)—prompts wrapped in Python decorators that transform ordinary functions into versioned, trackable units. The simplest entry point is the @ell.simple decorator, which converts a function into an LMP that returns a single string response:

import ell

@ell.simple(model="gpt-4")
def summarize_article(article: str) -> str:
    """You are an expert editor. Summarize the following article in 2-3 sentences."""
    return f"Article: {article}"

result = summarize_article("Long article text here...")

What makes this more than syntactic sugar is what happens behind the scenes. ell performs static analysis on the decorated function, serializing its source code, dependencies, and parameters. Every time you modify the function—change the prompt string, adjust the model parameter, or alter the logic—ell automatically detects the change, generates a semantic diff, and creates a new version with an AI-generated commit message describing what changed. These versions are stored locally in a SQLite database, creating a complete history without requiring manual commits or external services.

The real power emerges with @ell.complex, which supports multimodal inputs and structured outputs. ell's type coercion system lets you seamlessly integrate images, audio, and other content types:

from PIL import Image
import ell

@ell.complex(model="gpt-4-vision", temperature=0.7)
def analyze_chart(chart_image: Image.Image, context: str):
    """You are a data analyst. Analyze this chart and provide insights."""
    return [
        ell.system("You provide clear, actionable insights from data visualizations."),
        ell.user([chart_image, f"Context: {context}"])
    ]

insights = analyze_chart(Image.open("sales_chart.png"), "Q4 revenue trends")

This architectural choice—treating prompts as composable functions rather than string templates—enables a functional programming paradigm where LMPs can call other LMPs. You can build hierarchical prompt structures where higher-level prompts delegate to specialized sub-prompts, each versioned independently:

@ell.simple(model="gpt-4")
def extract_entities(text: str) -> str:
    """Extract named entities from the text as a JSON list."""
    return text

@ell.simple(model="gpt-4")
def categorize_entities(entities: str) -> str:
    """Categorize these entities by type (person, organization, location)."""
    return entities

@ell.simple(model="gpt-4")
def analyze_text(document: str) -> str:
    entities = extract_entities(document)
    categorized = categorize_entities(entities)
    return f"Provide a summary based on these categorized entities: {categorized}"

Each function in this pipeline is independently versioned. If you modify extract_entities, ell tracks that change without affecting the version history of categorize_entities or analyze_text. This composability makes prompts testable in isolation and enables you to optimize individual components without risking regression in the broader system.

The companion tool, Ell Studio, reads from the local SQLite store to provide a web interface for visualizing prompt evolution. You can see every version of an LMP, the diff between versions, performance metrics over time, and which versions were invoked when. It's like GitHub's commit history meets LLM observability, running entirely on your local machine. The local-first architecture means you own your data—no API keys to manage, no cloud service to depend on, and complete privacy for sensitive prompts.

Streaming support is built directly into the framework with minimal ceremony. The same decorator pattern extends to streaming responses, useful for user-facing applications where perceived latency matters:

@ell.complex(model="gpt-4", stream=True)
def interactive_chat(user_message: str):
    """You are a helpful assistant."""
    return [ell.user(user_message)]

for chunk in interactive_chat("Explain quantum computing"):
    print(chunk, end="", flush=True)

The versioning system isn't just storage—it's designed for empirical optimization. Ell Studio surfaces which prompt versions correlated with better outputs, enabling A/B testing of prompt variations across actual usage. You can tag specific versions as "production" and get alerted when new changes cause regressions compared to your baseline.

Gotcha

The decorator-heavy approach and automatic code serialization can create debugging challenges. Because ell analyzes your function source code at runtime to generate versions, dynamic code generation or complex closures might not serialize correctly. If your prompt logic depends on variables from enclosing scopes or uses lambdas extensively, you may encounter serialization errors that require restructuring your code to be more explicitly functional.

The local-first storage model, while excellent for privacy and simplicity, becomes problematic for team collaboration. Multiple developers working on the same prompts will have divergent version histories in their local databases unless you build custom synchronization infrastructure. There's no built-in mechanism for merging prompt histories or establishing a canonical "main" branch of prompt versions. For production deployments, you'll need to devise your own strategy for promoting specific prompt versions from development to staging to production environments, which somewhat undermines the automatic versioning benefits. Teams expecting collaborative features like pull request reviews for prompt changes or shared dashboards for monitoring production prompts will find ell's single-developer workflow limiting without significant additional tooling.

Verdict

Use if: You're doing serious prompt engineering where iteration speed and version tracking are critical, you value local-first tools that don't phone home, you're working solo or in small teams where coordination overhead is manageable, or you appreciate functional programming patterns and want prompts to be composable, testable units of logic. ell shines when you're empirically optimizing prompts and need to understand exactly what changed between versions and why. Skip if: You need out-of-the-box team collaboration features like shared version control and review workflows, you're building simple LLM integrations where prompt changes are infrequent, you work in non-Python environments, or your organization requires cloud-based audit trails and centralized monitoring. The framework's opinionated approach to treating prompts as code is powerful when it matches your workflow but adds complexity when you just need to call an LLM occasionally.

// ADD TO YOUR README
[![Featured on Starlog](https://starlog.is/api/badge/ai-dev-tools/madcowd-ell.svg)](https://starlog.is/api/badge-click/ai-dev-tools/madcowd-ell)