Flask tutorial
In this tutorial, you’ll build Bargain Chef, a standalone Genkit backend on Flask that exposes a recipe-generating flow over HTTP. It uses two AI patterns Genkit simplifies: streaming structured output and tool calling.
What you’ll build
Section titled “What you’ll build”For each request, your server prompts Gemini to draft a recipe, and the model calls a tool to look up mock grocery sale prices so it can prefer on-sale ingredients. The server streams the recipe back field-by-field as it’s generated, so clients see progress before the full recipe is ready.
You can find the finished code on GitHub.
Prerequisites
Section titled “Prerequisites”- Python 3.10 or later
- uv (or pip) for package management
This tutorial assumes you’re already familiar with building Flask applications.
Set up the application
Section titled “Set up the application”Create the project
Section titled “Create the project”Create a new project directory and initialize it:
mkdir my-genkit-flaskcd my-genkit-flaskuv init --no-readme --python 3.10Install packages
Section titled “Install packages”Install the Genkit CLI:
curl -sL cli.genkit.dev | bashThen install the packages you need in your project:
uv add "flask[async]" flask-cors genkit genkit-google-genai genkit-flaskThese packages include:
flask[async]: Flask 3.1+ with async view support.flask-cors: Adds CORS headers so a browser frontend can call the endpoint.genkit: Core Genkit SDK for Python.genkit-google-genai: Plugin that connects Genkit to Google’s Gemini models.genkit-flask: Helper that exposes Genkit flows as Flask routes, including server-sent events for streaming.
Configure a model API key
Section titled “Configure a model API key”This tutorial uses the Gemini API from Google AI Studio. Get a key at aistudio.google.com/apikey, then set the GEMINI_API_KEY environment variable to your key:
export GEMINI_API_KEY=<your API key>Create the backend
Section titled “Create the backend”The backend handles requests from clients. For each request, it prompts Gemini to draft a recipe, lets the model call a tool to look up today’s grocery sale prices, and streams the partial recipe back to the client as it’s generated.
The whole pipeline is a single Genkit flow. A flow is a special Genkit function with built-in observability, type safety, and tooling integration.
You’ll build the backend in four parts:
- Initialize Genkit and register Gemini as the model provider.
- Define a tool the model can call to fetch sale prices.
- Describe the recipe shape with Pydantic so Genkit can validate the final output and stream partial recipe chunks.
- Define the flow that ties everything together.
Replace the contents of main.py with the following:
from datetime import datetimefrom typing import Literal
from flask import Flaskfrom flask_cors import CORSfrom pydantic import BaseModel, Field
from genkit import FlowContext, Genkitfrom genkit_flask import genkit_flask_handlerfrom genkit_google_genai import GeminiConfig, GoogleAI
ai = Genkit( plugins=[GoogleAI()], model=GoogleAI.gemini('gemini-flash-latest'),)
app = Flask(__name__)# Allows all origins so any browser frontend can call the endpoint during# development. Before deploying, restrict it: CORS(app, origins=['https://your-app.com']).CORS(app)
class SaleItem(BaseModel): name: str price: str
class GetIngredientsInput(BaseModel): day_type: Literal['weekday', 'weekend'] = Field( description='Whether to fetch weekday or weekend sale prices.', )
@ai.tool()async def get_ingredients_on_sale(input: GetIngredientsInput) -> list[SaleItem]: """Returns the ingredients on sale at the local grocery store, with prices.
The sale set differs between weekdays and weekends. """ # Mock data: in a real app, query a pricing database. if input.day_type == 'weekend': return [ SaleItem(name='chicken breast', price='$2.99/lb'), SaleItem(name='pasta', price='$0.79'), SaleItem(name='canned tomatoes', price='$0.99'), SaleItem(name='garlic', price='$0.50 / head'), SaleItem(name='olive oil', price='$6.99'), ] return [ SaleItem(name='eggs', price='$3.49 / dozen'), SaleItem(name='spinach', price='$1.99'), SaleItem(name='parmesan', price='$4.99'), SaleItem(name='lemons', price='$0.50 each'), SaleItem(name='rice', price='$2.49'), SaleItem(name='butter', price='$3.99'), ]
class RecipeIngredient(BaseModel): name: str quantity: str on_sale: bool = Field(serialization_alias='onSale')
class Recipe(BaseModel): title: str description: str servings: int ingredients: list[RecipeIngredient] steps: list[str]
class BargainChefInput(BaseModel): craving: str = Field(description='What the user feels like eating right now.')
@app.post('/bargainChefFlow')@genkit_flask_handler(ai)@ai.flow(name='bargainChefFlow')async def bargain_chef_flow( input: BargainChefInput, ctx: FlowContext[Recipe],) -> Recipe: today = datetime.now().strftime('%A')
stream_response = ai.generate_stream( prompt=( f'Today is {today}. The user is craving: {input.craving}.\n\n' 'Call the get_ingredients_on_sale tool with the day_type that matches ' 'today. Saturday and Sunday are weekends; all other days are weekdays. ' 'Then propose ONE recipe that takes advantage of those deals. For each ' "ingredient, set on_sale=true if it appears in the tool's response, " 'false otherwise.' ), tools=[get_ingredients_on_sale], output_schema=Recipe, config=GeminiConfig(temperature=0.7, thinking_level='LOW'), )
async for chunk in stream_response.stream: if chunk.output: ctx.send_chunk(chunk.output)
response = await stream_response.response if response.output is None: raise RuntimeError(f'Recipe generation failed: {response.error}') return response.output
if __name__ == '__main__': app.run(host='127.0.0.1', port=8080)A few details are worth noting:
- Initialize Genkit:
Genkit(plugins=[GoogleAI()], model=GoogleAI.gemini('gemini-flash-latest'))initializes the SDK and sets a typed Gemini model reference as the default model. - The
get_ingredients_on_saletool: This is a function the model can call mid-generation. The PydanticGetIngredientsInputmodel usesLiteral['weekday', 'weekend']so the tool’s typed input forces the model to pick one of those values. In a real app, this would query a pricing database, inventory system, or third-party API. - Describe the recipe shape:
Recipeis a Pydantic model passed asoutput_schemaso Genkit instructs the model to emit matching JSON and parses each streamed chunk against it.serialization_alias='onSale'serializeson_saleas camelCaseonSaleover JSON and SSE while keeping snake_case in Python. - Define the flow:
bargain_chef_flowcallsai.generate_stream, forwards each partialRecipe(chunk.output) viactx.send_chunk(typed byctx: FlowContext[Recipe]), and awaitsstream_response.responsefor the validated result. Returnresponse.output(orresponse.text), never a rawModelResponse, whoserequestfield carries your system prompt, tools, and message history. - Model failures: A failed model call (such as
UNAVAILABLEwhen Gemini is overloaded) doesn’t raise. The response comes back with nooutputand the failure onresponse.error. The flow raises so the server log records the failure, and the client gets a genericInternal Error: a500, or anerrorevent when streaming. See Error types. - The three stacked decorators:
@ai.flow(name='bargainChefFlow')registers the flow,@genkit_flask_handler(ai)adapts it into a Flask view named after the flow that handles JSON requests and Server-Sent Events, and@app.post('/bargainChefFlow')mounts it on a route.
Attach request context
Section titled “Attach request context”Pass context_provider= to @genkit_flask_handler to validate credentials and populate ctx.context before the flow runs. The provider receives a RequestData (from genkit.web import RequestData) with method, lowercased headers, input, and Flask’s request on request.
Raise PublicError from the provider or the flow to send a specific status and message, such as 401 for 'UNAUTHENTICATED'; Flask’s abort(401) in the provider also passes through. Any other exception becomes a 500 with a generic body. See Errors and timeouts for the full mapping.
Update the decorators on bargain_chef_flow in main.py:
from genkit import FlowContext, PublicErrorfrom genkit.web import RequestDatafrom genkit_flask import genkit_flask_handler
# 1. Validate request headers and populate ctx.contextasync def auth_context(req: RequestData) -> dict[str, object]: auth_header = req.headers.get('authorization') or '' token = auth_header.removeprefix('Bearer ').strip() if not token: raise PublicError('UNAUTHENTICATED', 'Missing bearer token') return {'uid': token}
# 2. Read ctx.context inside the served flow@app.post('/bargainChefFlow')@genkit_flask_handler(ai, context_provider=auth_context)@ai.flow(name='bargainChefFlow')async def bargain_chef_flow( input: BargainChefInput, ctx: FlowContext[Recipe],) -> Recipe: uid = ctx.context.get('uid') ...Check the project layout
Section titled “Check the project layout”Verify that your project layout matches the structure below:
- pyproject.toml
- main.py
Optional: install the Genkit agent skills
Section titled “Optional: install the Genkit agent skills”If you’re coding with an AI assistant, install the Genkit Agent Skills so it has structured guidance on Genkit APIs, patterns, and common errors:
npx skills add genkit-ai/skillsSee Develop with AI for tool-specific installation instructions.
Run the app
Section titled “Run the app”Start the Flask server:
uv run python main.pyThe server listens on http://127.0.0.1:8080. Send a request with a craving and the flow will pick today’s sale list, propose a recipe, and stream the partial recipe back to the client field by field, title first, then description, then ingredients (with "onSale": true on the ones the model picked from the tool), then steps.
Test and inspect the app
Section titled “Test and inspect the app”You can test the endpoint directly with curl, and you can use the Developer UI to inspect both manual runs and requests sent to your Flask app.
Send a request with curl
Section titled “Send a request with curl”With the server running, use the -N flag and an Accept: text/event-stream header to consume the streamed response:
curl -N -X POST http://127.0.0.1:8080/bargainChefFlow \ -H "Content-Type: application/json" \ -H "Authorization: Bearer user-123" \ -H "Accept: text/event-stream" \ -d '{"data":{"craving":"something warm with chicken"}}'The { "data": ... } wrapper is required: Genkit’s HTTP handler reads the flow input from the request body’s data field.
The response arrives as a series of data: events. Each event contains the partial recipe accumulated so far, and the final event contains the validated result.
For a non-streaming request, drop the Accept header and you’ll get a single JSON response:
curl -X POST http://127.0.0.1:8080/bargainChefFlow \ -H "Content-Type: application/json" \ -H "Authorization: Bearer user-123" \ -d '{"data":{"craving":"something warm with chicken"}}'Use the Developer UI
Section titled “Use the Developer UI”The Developer UI is Genkit’s local console for testing flows and inspecting execution traces. It runs alongside your backend code, gives you a visual runner for any flow in your project, and records every tool call and model invocation so you can iterate on prompts and debug tool behavior.
-
Start the Developer UI from your project root:
Terminal window genkit start -- uv run python main.pyThis launches the Developer UI at
http://localhost:4000by default. -
Select
bargainChefFlowfrom the list of flows. -
Enter sample input:
{ "craving": "something warm with chicken" } -
Click Run.
You’ll see the generated recipe, with a trace that builds in real time so you can follow the flow’s progress through each tool call and model invocation.
What you built
Section titled “What you built”You now have a standalone Genkit backend on Flask that streams structured output from Gemini over HTTP, calls a tool during generation to ground the model’s response in mock sale-price data, validates input and output against schemas, and surfaces every step in a local trace UI.