Wire the Anthropic SDK into Flask — type a shot, click Generate, receive a production-ready prompt
Day 32 of 80
A new section appears at the top of the Prompt Vault: a form with a shot description field and a platform selector. You hit "Generate with Claude." Python receives the form data, calls the Anthropic API, and the response comes back as a new variable in your template. The generated prompt appears on the page with a "Save This" button. Clicking Save adds it to prompts.json with one more POST.
You're combining everything from the last six weeks: Flask routes (Week 6), the Anthropic SDK (Week 4), and JSON storage via helpers.py (Week 5).
Add these lines near the top of app.py, after your existing imports:
from flask import Flask, render_template, request, redirect
from helpers import load_prompts, save_prompts, validate_platform
import anthropic # NEW
from dotenv import load_dotenv # NEW
load_dotenv() # loads ANTHROPIC_API_KEY from .env
app = Flask(__name__)
claude = anthropic.Anthropic() # one client instance, reused across requests
load_dotenv() — Reads your .env file and puts its contents into environment variables. Must be called before anthropic.Anthropic(), which reads ANTHROPIC_API_KEY from the environment.
claude once at module level, not inside the route function. This is more efficient: creating an API client involves some initialization work. Reusing the same object across requests is standard practice.
Add this route to app.py after your existing routes:
@app.route("/generate", methods=["POST"])
def generate():
shot = request.form.get("shot", "").strip()
platform = request.form.get("platform", "Kling")
if not shot:
return redirect("/")
message = claude.messages.create(
model="claude-sonnet-4-6",
max_tokens=300,
system="You are an expert AI video prompt engineer. You write precise, evocative prompts for AI video generation tools. Always include camera movement, lighting, and mood.",
messages=[{
"role": "user",
"content": f"Write a production-ready {platform} AI video prompt for: {shot}. Include camera movement, lighting, and mood. Under 100 words. Return only the prompt text, no preamble."
}]
)
generated = message.content[0].text
prompts = load_prompts()
counts = {}
for p in prompts:
counts[p["platform"]] = counts.get(p["platform"], 0) + 1
return render_template(
"index.html",
prompts=prompts,
generated=generated,
gen_shot=shot,
gen_platform=platform,
counts=counts,
all_count=len(prompts),
active_filter="all"
)
model="claude-sonnet-4-6" — Use the same model you've been using since Week 4. For prompt generation, Sonnet is fast and high quality. If you want to reduce API cost, claude-haiku-4-5 is cheaper and still excellent for this task.
message.content[0].text — The Anthropic API returns a list of content blocks. For standard text responses, content[0] is the text block. This is identical to how you called Claude in Week 4's CLI scripts.
gen_shot and gen_platform — The Save form (coming next) needs these to POST a complete record back. Without them, the template can't build the Save form.
Add this block to templates/index.html — place it before the prompt grid but after the filter bar:
{# ── Generate with Claude ── #}
<div class="generate-section">
<h2>Generate with Claude</h2>
<form method="POST" action="/generate">
<div class="gen-row">
<input type="text" name="shot"
placeholder="Describe the shot (e.g. aerial over misty golf course at sunrise)..."
required>
<select name="platform">
<option value="Kling">Kling</option>
<option value="Runway">Runway</option>
<option value="Veo">Veo</option>
</select>
<button type="submit" class="btn-generate">Generate →</button>
</div>
</form>
{# Show the generated result if we have one #}
{% if generated %}
<div class="generated-result">
<div class="result-header">
<span class="platform-tag platform-{{ gen_platform | lower }}">{{ gen_platform }}</span>
<span class="result-label">Generated for: <em>{{ gen_shot }}</em></span>
</div>
<p class="generated-text">{{ generated }}</p>
{# Save This form — hidden fields carry all three values #}
<form method="POST" action="/save-generated">
<input type="hidden" name="platform" value="{{ gen_platform }}">
<input type="hidden" name="shot" value="{{ gen_shot }}">
<input type="hidden" name="prompt" value="{{ generated }}">
<button type="submit" class="btn-save">Save This Prompt</button>
</form>
</div>
{% endif %}
</div>
{% if generated %} — On the home page, generated is never passed to the template, so this block is invisible. After a /generate POST, generated holds the prompt text and the result section appears.
<input type="hidden"> is how you pass data through a form that the user doesn't need to see or edit. The browser includes hidden inputs in the POST body just like visible ones. This is the standard way to carry context through a form chain.
method="POST".
@app.route("/save-generated", methods=["POST"])
def save_generated():
prompts = load_prompts()
prompts.append({
"platform": request.form["platform"],
"shot": request.form["shot"],
"prompt": request.form["prompt"]
})
save_prompts(prompts)
return redirect("/")
add_prompt() but without the validation step (the prompt came from Claude, not user-typed input, so it's already in the right shape). Load, append, save, redirect. The pattern never changes.
$ python app.py
* Running on http://127.0.0.1:5000
* Debug mode: on
# Open http://localhost:5000
# Type: "aerial drone shot over misty Scottish golf course at sunrise"
# Select: Kling
# Click: Generate →
# Wait ~2 seconds for the Claude API response
# The generated prompt appears below the form
# Click "Save This Prompt" — it appears in your vault cards
Look at what just happened. The generate() route calls load_prompts() and save_prompts() from helpers.py — the same functions you wrote in Week 5. It calls the Anthropic SDK the same way you did in your Week 4 CLI scripts. It uses Flask patterns from Week 6. Nothing was wasted. Every piece you built is still in play. This is how good software accumulates.
API key error — Make sure .env is in your prompt-vault/ folder (same directory as app.py) and contains ANTHROPIC_API_KEY=sk-ant-.... The load_dotenv() call at the top of app.py must happen before anthropic.Anthropic().
Generated result disappears on refresh — Correct behavior. Refreshing re-runs the GET / route, which doesn't pass generated. The result only shows immediately after generation. Save it if you want to keep it.
Template variable not found — If Jinja2 throws a UndefinedError for generated, it means the home route doesn't pass it. Jinja2's {% if generated %} handles this: if generated isn't passed, treat it as falsy. This works in Flask's default Jinja2 configuration.
import anthropic and load_dotenv() to app.py/generate route calls the Claude API and passes the result to the templateindex.html and posts to /generate{% if generated %})/save-generated route saves the generated prompt to prompts.jsonTomorrow you add a search bar. Type "golf" and see only golf-related prompts. Type "kling" and see only Kling prompts. It's a GET route, a list comprehension, and about 20 new lines of HTML. One of the faster build days in the course.