strainsdb.org is a Django web application that catalogues cannabis strains, built as an early experiment in AI-powered content generation. Every strain description, terpene profile, and data point was generated using large language models and structured with Pydantic models for consistency.
What It Does
- Strain Browser: Browse 7,024 cannabis strains with detailed descriptions, effects, and growing information.
- Search: Find strains by name, effects, or characteristics.
- Terpene Profiles: Detailed terpene breakdowns for each strain.
- Structured AI Content: All data generated by GPT-3.5, validated through Pydantic models to ensure consistency.
Why I Built It
StrainsDB was a proof of concept: could you build a content-rich website almost entirely with AI-generated data, and could that data be structured and reliable enough to be useful?
The answer turned out to be yes, with the right tooling. The key insight was using the Instructor library to constrain LLM outputs to Pydantic models. Instead of hoping the AI would return well-formatted data, I defined exactly what "well-formatted" meant and let the library enforce it.A schema is an abstraction: a bet about what the data must look like, enforced at the boundary. This produced a database of 7,024 strains with consistent, structured information.
Technical Stack
- Django: Web framework and admin interface.
- GPT-3.5: Content generation for strain descriptions and profiles.
- Instructor + Pydantic: Structured output validation, ensuring every AI-generated entry matches the expected schema.
- SQLite: Lightweight storage, perfect for a proof of concept.
What It Demonstrated
This project predated the widespread adoption of LLMs for content generation. It showed that AI could produce not just plausible text, but structured, validated data suitable for a real web application, as long as you had the right constraints in place.Constraints as Liberation, applied to language models.
The pattern of "AI generates, Pydantic validates" has since become common practice. StrainsDB was one of the early demonstrations of why that pattern works.
Example
The pattern, in miniature. Define the shape you want, and let Instructor make the model fill it in:
import instructor
from openai import OpenAI
from pydantic import BaseModel
class Strain(BaseModel):
name: str
description: str
effects: list[str]
client = instructor.from_openai(OpenAI())
strain = client.chat.completions.create(
model="gpt-3.5-turbo",
response_model=Strain,
messages=[{"role": "user", "content": "Describe the strain Blue Dream."}],
)
If the model returns something that doesn't fit Strain, it doesn't make it into the database. This is an illustration of the approach, not the project's actual code.
Every strain gets its own page at a plain URL, like strainsdb.org/strains/100-og.
Today
StrainsDB is still live at strainsdb.org, and the homepage lists all 7,024 strains. It runs on my own little server. The source is currently private.