django-anvil v0.1
🔍Search docs...⌘K
Core concepts

AI engine

Analyze + suggest, apply only with your approval. Describe a feature in plain English, get back a reviewable unified diff. By default nothing is ever written — you review the diff and apply it by hand. Add --apply to be asked, after seeing the diff, whether to write the files directly; it always requires a typed yes, never a silent auto-apply.

Usage

python manage.py anvil ai "add a coupon system with percent or fixed-amount \
discounts, an optional expiry date, and a usage limit" --app products --output coupon.patch

Omit --output to print the diff to the terminal instead of saving it.

--apply: approve without leaving the terminal

python manage.py anvil ai "add a coupon system..." --app products --apply
--- a/products/models.py
+++ b/products/models.py
...
Write 2 file(s) now (products/models.py, products/resources.py)? [y/N]: 

Say yes and the files are written. Say no (or anything else) and nothing is. Two things stay true either way:

How it decides what to write

It reads your project's existing models and Resources for context (via a compact indexer, so the LLM matches your project's actual conventions instead of inventing its own), then asks the configured provider for complete file contents — not a diff directly.

💡

Why whole files instead of asking the model for a diff? LLMs are far more reliable at writing a complete, correct file than at producing valid unified-diff syntax by hand — and diffing two known strings is a solved, deterministic problem Python's own difflib already does correctly. So the model's job is kept simple, and the diff itself is always syntactically valid.

Providers

Nothing is hard-locked to one vendor — every provider is one class implementing a single method:

class AIProvider(abc.ABC):
    @abc.abstractmethod
    def complete(self, system: str, prompt: str) -> str: ...

Swap providers with one setting — nothing else in the AI engine changes, and nothing extra to install: OpenAI, Anthropic, and Gemini's SDKs are all plain dependencies of django-anvil itself, so a single pip install django-anvil already has every one of them.

ANVIL_AI_PROVIDER = "django_anvil.ai.providers.AnthropicProvider"  # dotted path to any AIProvider
ProviderAPI keyDefault model
OpenAIProvider (default)OPENAI_API_KEYgpt-4o
AnthropicProviderANTHROPIC_API_KEYclaude-sonnet-5
GeminiProviderGOOGLE_API_KEYgemini-2.0-flash
StaticProvidernonen/a — returns a fixed response, for tests/CI

Every provider also accepts ANVIL_AI_API_KEY and ANVIL_AI_MODEL in settings as vendor-agnostic overrides, if you'd rather not rely on each SDK's own env var or want to pin a specific model.

âš ī¸

GeminiProvider uses the google-genai package, not the older google-generativeai — Google has end-of-lifed that one in favor of this one. If you see a deprecation warning mentioning google.generativeai, you're on the old package; switch to google-genai.

# settings.py
ANVIL_AI_PROVIDER = "django_anvil.ai.providers.GeminiProvider"
ANVIL_AI_MODEL = "gemini-2.0-flash"       # optional override
ANVIL_AI_API_KEY = env("GEMINI_KEY")         # optional override; else GOOGLE_API_KEY env var

A StaticProvider also ships, for tests/CI: returns a fixed ANVIL_AI_STATIC_RESPONSE instead of calling anything, so the rest of the pipeline (indexing, prompting, diff-building) can be exercised with no network and no API key.

Custom providers

Any vendor Anvil doesn't ship a wrapper for yet — a self-hosted model, an internal proxy, Azure OpenAI, Ollama — is the same shape. django_anvil.ai.providers.CustomProviderExample is a working template (a bare HTTP POST to a local Ollama-style endpoint):

class CustomProviderExample(AIProvider):
    def __init__(self):
        self._endpoint = getattr(settings, "ANVIL_AI_ENDPOINT", "http://localhost:11434/api/generate")

    def complete(self, system: str, prompt: str) -> str:
        body = json.dumps({"prompt": f"{system}\n\n{prompt}", "stream": False}).encode()
        request = urllib.request.Request(self._endpoint, data=body, headers={"Content-Type": "application/json"})
        with urllib.request.urlopen(request) as response:
            return json.loads(response.read())["response"]

Write your own the same way — one class, one method, point ANVIL_AI_PROVIDER at its dotted path (it doesn't need to live inside django_anvil; anywhere importable in your project works, e.g. "myapp.ai.MyProvider").

💡

All three real providers (Anthropic, OpenAI, Gemini) are covered by mock-based unit tests in the repo's own tests/test_ai_providers.py — verifying each one calls its SDK with the right request shape and correctly extracts the response text, without needing a real API key or network access to run.

Applying the diff by hand (without --apply)

git apply coupon.patch
# or, without git:
patch -p1 < coupon.patch

Either way — --apply or applying the saved patch by hand — finish the same way, once you've looked at what changed:

python manage.py makemigrations products
python manage.py anvil resource Coupon
â„šī¸

The system prompt tells the model Anvil's own conventions explicitly — plain models in models.py, a matching Resource in resources.py, and not to write serializers.py/views.py/urls.py/admin.py itself, since anvil resource generates those.