Rigging: The LLM Framework That Treats Prompts Like Functions
Hook
What if your LLM prompts were just Python functions with type hints, and the framework automatically enforced that the AI returns exactly the data structure you expect?
Context
The explosion of LLM frameworks has created a paradox: tools meant to simplify AI integration often introduce more complexity than the raw API calls they replace. LangChain, the 800-pound gorilla of the space, offers incredible breadth but forces developers to navigate thousands of abstractions, chain types, and callback handlers just to ask GPT-4 a question and get structured JSON back.
Rigging emerged from Dreadnode's internal tooling with a different philosophy: what if an LLM framework felt like writing normal Python instead of learning a proprietary DSL? By leaning heavily on Pydantic for validation, Python decorators for prompt templates, and database-style connection strings for model configuration, Rigging treats language models as typed, awaitable functions rather than mystical black boxes requiring specialized orchestration layers.
Technical Insight
The framework's core insight is modeling LLM interactions as pipelines with three distinct phases: generator initialization, pipeline construction, and execution. Unlike LangChain's object-heavy approach, Rigging uses a fluent API that feels closer to SQLAlchemy or Pandas.
Here's how structured output extraction works in practice:
import rigging as rg
from pydantic import BaseModel, Field
class ProductAnalysis(BaseModel):
sentiment: str = Field(description="positive, negative, or neutral")
key_features: list[str] = Field(description="Main features mentioned")
price_sensitivity: float = Field(ge=0, le=1, description="0=price insensitive, 1=very price sensitive")
# Connection string abstracts provider details
generator = rg.get_generator("gpt-4o-mini")
review = "This laptop is expensive but the battery life is incredible. Worth every penny for remote work."
# Pipeline construction with Pydantic parsing
pipeline = generator.chat(
{"role": "user", "content": f"Analyze this review: {review}"}
).with_model(ProductAnalysis)
# Execute and get typed result
result = await pipeline.run()
analysis: ProductAnalysis = result.parsed
print(f"Sentiment: {analysis.sentiment}")
print(f"Price sensitivity: {analysis.price_sensitivity}")
The with_model() method is where Rigging's magic happens. Under the hood, it injects JSON schema derived from your Pydantic model into the system prompt, constrains the generation format, and validates the response before returning. If the LLM returns invalid JSON or missing required fields, Rigging raises a validation error with Pydantic's detailed messages rather than silently failing or returning unparsed text.
The decorator-based prompt system eliminates the string concatenation hell common in other frameworks:
@rg.prompt
def analyze_code(code: str, language: str) -> ProductAnalysis:
"""
You are an expert code reviewer.
Analyze this {{language}} code and identify:
- Security vulnerabilities
- Performance issues
- Best practice violations
Code:
{{code}}
"""
# The function signature becomes the contract
result = await analyze_code(
code="SELECT * FROM users WHERE id = " + user_input,
language="SQL"
)
Notice how the docstring serves as the prompt template with Jinja2-style variable interpolation, while the return type annotation (ProductAnalysis) automatically configures structured output parsing. This keeps prompt engineering close to the code that uses it, unlike externalized YAML files or template strings scattered across modules.
Rigging's tool calling implementation works even for models without native function calling support. When you register tools, the framework compiles them into text descriptions and performs in-band execution:
import rigging as rg
def get_weather(location: str) -> str:
"""Fetch current weather for a location."""
# Actual API call here
return f"Sunny, 72°F in {location}"
generator = rg.get_generator("claude-3-sonnet").with_tools([get_weather])
chat = generator.chat([
{"role": "user", "content": "What's the weather in Portland?"}
])
result = await chat.run()
# Rigging detects tool call in response, executes get_weather(),
# and includes result in continued conversation
For models like Claude that lack OpenAI-style function calling, Rigging transforms tool definitions into natural language descriptions, parses the model's intent from its response text, executes the function, and feeds the result back—all transparently.
The connection string pattern deserves special attention because it solves configuration hell. Instead of environment-specific client initialization code, you get portability:
# Local development with vLLM
gen = rg.get_generator("vllm:localhost:8000/mistral-7b")
# Production with OpenAI
gen = rg.get_generator("openai:gpt-4o")
# Staging with Anthropic
gen = rg.get_generator("anthropic:claude-3-opus")
The same pipeline code runs unchanged—just swap the connection string via environment variable. This mirrors how modern applications handle database connections, and it's shockingly rare in LLM frameworks where switching providers often requires rewriting large chunks of code.
Gotcha
Rigging's minimalism is both its strength and Achilles' heel. The framework intentionally avoids building high-level abstractions like LangChain's ConversationalRetrievalChain or pre-built agent loops. If you need complex multi-agent orchestration with memory management, tool routing logic, and conditional workflows, you're implementing that logic yourself. The 410 GitHub stars translate to a small community—expect to read source code rather than find Stack Overflow answers when debugging.
The LiteLLM dependency creates an abstraction tax. When OpenAI releases a new feature like structured outputs with strict schema adherence, there's a lag before LiteLLM supports it, then another lag before Rigging exposes it. For teams that need day-one access to cutting-edge model capabilities, this double-hop creates friction. Additionally, LiteLLM's error messages sometimes obscure the underlying provider error, making debugging authentication issues or rate limits unnecessarily painful. The Pydantic-based validation also assumes you can define your output schema upfront—if your use case involves truly dynamic schemas determined at runtime, you'll fight the type system.
Verdict
Use Rigging if you're building production LLM applications where type safety and code clarity matter more than ecosystem breadth. It's perfect for teams that already use Pydantic heavily (FastAPI shops, data engineering teams) and want LLM interactions to feel like the rest of their typed Python codebase. The async-first design and lightweight footprint make it ideal for high-throughput services where LangChain's overhead becomes noticeable. Skip it if you need extensive pre-built chains for RAG, document processing, or conversational agents—LangChain and Haystack offer far more batteries-included solutions. Also skip if you're doing research requiring instant access to brand-new model features, since the LiteLLM layer introduces lag. Rigging thrives in greenfield projects where you control the architecture and value simplicity over comprehensive tooling.