Add phase 4: AI cleanup and photo/text import via OpenAI
- recipes/ai.py: Chat Completions with strict JSON schema (Structured Outputs), Pydantic re-validation, plausibility clamping, prompt treats input as data only - "Mit KI aufbereiten" on create/edit form returns an editable draft - Import page: photo/screenshot (vision, downscaled JPEG, not stored) and pasted text; drafts pre-fill the recipe form with a preview - Clear UI messages for timeout, rate limit, quota, auth, model and connection errors; without OPENAI_API_KEY AI features show a notice - Optional per-user and global daily AI limits (default unlimited) - Tests for response parsing, schema strictness, error mapping, views Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
9256e94286
commit
1bf8ef8b65
+26
-3
@@ -14,7 +14,7 @@ from django.views.decorators.http import require_GET, require_POST
|
||||
|
||||
from core import ratelimit
|
||||
|
||||
from . import images
|
||||
from . import ai, images
|
||||
from .forms import CommentForm, PhotoUploadForm, RecipeForm, SearchForm
|
||||
from .models import Comment, Favorite, Recipe, RecipeImage, Tag
|
||||
|
||||
@@ -99,25 +99,48 @@ def _render_detail(request, recipe, comment_form=None, status=200):
|
||||
}, status=status)
|
||||
|
||||
|
||||
def _render_form(request, form, recipe=None, **extra):
|
||||
context = {"form": form, "is_new": recipe is None, "recipe": recipe,
|
||||
"images": recipe.images.all() if recipe else [], **extra}
|
||||
return render(request, "recipes/form.html", context)
|
||||
|
||||
|
||||
def _ai_cleanup(request, recipe=None):
|
||||
"""'Mit KI aufbereiten' on the manual form: replace the fields with an AI draft (not saved)."""
|
||||
data = request.POST
|
||||
try:
|
||||
draft = ai.structure_text(request.user, ai.form_text_for_ai(data))
|
||||
except ai.AIError as error:
|
||||
messages.error(request, error.user_message)
|
||||
return _render_form(request, RecipeForm(initial=data.dict(), instance=recipe), recipe)
|
||||
keep = {key: data.get(key, "") for key in ("visibility", "source_url") if data.get(key)}
|
||||
form = RecipeForm(initial={**draft.to_form_initial(), **keep}, instance=recipe)
|
||||
return _render_form(request, form, recipe, ai_draft=draft)
|
||||
|
||||
|
||||
def recipe_create(request):
|
||||
if request.method == "POST" and "ai" in request.POST:
|
||||
return _ai_cleanup(request)
|
||||
form = RecipeForm(request.POST or None, request.FILES or None)
|
||||
if request.method == "POST" and form.is_valid():
|
||||
recipe = form.save(owner=request.user)
|
||||
attach_photos(request, recipe, form.cleaned_data.get("photos") or [])
|
||||
messages.success(request, "Rezept gespeichert.")
|
||||
return redirect("recipes:detail", pk=recipe.pk)
|
||||
return render(request, "recipes/form.html", {"form": form, "is_new": True})
|
||||
return _render_form(request, form)
|
||||
|
||||
|
||||
def recipe_edit(request, pk):
|
||||
recipe = get_object_or_404(Recipe.objects.editable_by(request.user), pk=pk)
|
||||
if request.method == "POST" and "ai" in request.POST:
|
||||
return _ai_cleanup(request, recipe)
|
||||
form = RecipeForm(request.POST or None, request.FILES or None, instance=recipe)
|
||||
if request.method == "POST" and form.is_valid():
|
||||
recipe = form.save()
|
||||
attach_photos(request, recipe, form.cleaned_data.get("photos") or [])
|
||||
messages.success(request, "Änderungen gespeichert.")
|
||||
return redirect("recipes:detail", pk=recipe.pk)
|
||||
return render(request, "recipes/form.html", {"form": form, "recipe": recipe, "images": recipe.images.all()})
|
||||
return _render_form(request, form, recipe)
|
||||
|
||||
|
||||
def recipe_delete(request, pk):
|
||||
|
||||
Reference in New Issue
Block a user