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:
- It only ever writes
models.py/resources.pyâ nevermakemigrations, neveranvil resource. Those are separate, deliberate commands you run yourself once you've looked at what changed. - In a non-interactive session (no TTY â CI, a piped command)
--applyalways declines and writes nothing, rather than guessing what you'd have said.
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
| Provider | API key | Default model |
|---|---|---|
OpenAIProvider (default) | OPENAI_API_KEY | gpt-4o |
AnthropicProvider | ANTHROPIC_API_KEY | claude-sonnet-5 |
GeminiProvider | GOOGLE_API_KEY | gemini-2.0-flash |
StaticProvider | none | n/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.