Skip to content

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.

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.

  • Python 3.10 or later
  • uv (or pip) for package management

This tutorial assumes you’re already familiar with building Flask applications.

Create a new project directory and initialize it:

Terminal window
mkdir my-genkit-flask
cd my-genkit-flask
uv init --no-readme --python 3.10

Install the Genkit CLI:

Terminal window
curl -sL cli.genkit.dev | bash

Then install the packages you need in your project:

Terminal window
uv add "flask[async]" flask-cors genkit genkit-plugin-google-genai genkit-plugin-flask

These packages include:

  • flask: The Flask web framework.
  • genkit: Core Genkit SDK for Python.
  • genkit-plugin-google-genai: Plugin that connects Genkit to Google’s Gemini models.
  • genkit-plugin-flask: Helper that exposes Genkit flows as Flask routes, including server-sent events for streaming.

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:

Terminal window
export GEMINI_API_KEY=<your API key>

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:

  1. Initialize Genkit and register Gemini as the model provider.
  2. Define a tool the model can call to fetch sale prices.
  3. Describe the recipe shape with Pydantic so Genkit can validate the final output and stream partial recipe chunks.
  4. Define the flow that ties everything together.

Create main.py:

main.py
from datetime import datetime
from typing import Literal
from flask import Flask
from flask_cors import CORS
from pydantic import BaseModel, Field
from genkit import ActionRunContext, Genkit
from genkit.plugins.flask import genkit_flask_handler
from genkit.plugins.google_genai import GoogleAI
ai = Genkit(
plugins=[GoogleAI()],
model='googleai/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(
name='get_ingredients_on_sale',
description=(
'Returns the ingredients on sale at the local grocery store, with prices. '
'The sale set differs between weekdays and weekends.'
),
)
async def get_ingredients_on_sale(input: GetIngredientsInput) -> list[SaleItem]:
# 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
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', chunk_type=Recipe)
async def bargain_chef_flow(input: BargainChefInput, ctx: ActionRunContext) -> 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={'temperature': 0.7, 'thinkingConfig': {'thinkingLevel': 'MINIMAL'}},
)
async for chunk in stream_response.stream:
if chunk.output:
ctx.send_chunk(chunk.output)
response = await stream_response.response
if not response.output:
raise ValueError('Failed to generate recipe')
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-flash-latest') sets up the SDK and registers Gemini as the default model provider.
  • The get_ingredients_on_sale tool: This is a function the model can call mid-generation. Tools let the model reach outside its training data and into your code. Here, the tool fetches live sale prices before the model finalizes the recipe. The Pydantic GetIngredientsInput model uses Literal['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: Recipe is a Pydantic model that describes the structure of the final response. The flow passes it as output_schema so Genkit instructs the model to emit JSON matching that shape, and parses each streamed chunk against it.
  • Define the flow: bargain_chef_flow ties everything together. It calls ai.generate_stream, which yields chunks as the model produces them; chunk.output is the partial Recipe object parsed from everything generated so far, and ctx.send_chunk forwards it to the client so the UI can fill in field by field. After the stream completes, the flow awaits stream_response.response so the HTTP request still resolves with a validated recipe.
  • The three stacked decorators: These wire everything up. @ai.flow(name='bargainChefFlow', chunk_type=Recipe) registers the function as a Genkit flow, @genkit_flask_handler(ai) adapts it into a Flask view that handles JSON requests and server-sent events for streaming, and @app.post('/bargainChefFlow') mounts it on a route.

Verify that your project layout matches the structure below:

  • pyproject.toml
  • main.py

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:

Terminal window
npx skills add genkit-ai/skills

See Develop with AI for tool-specific installation instructions.

Start the Flask server:

Terminal window
uv run python main.py

The 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 on_sale=true on the ones the model picked from the tool), then steps.

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.

With the server running, use the -N flag and an Accept: text/event-stream header to consume the streamed response:

Terminal window
curl -N -X POST http://127.0.0.1:8080/bargainChefFlow \
-H "Content-Type: application/json" \
-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:

Terminal window
curl -X POST http://127.0.0.1:8080/bargainChefFlow \
-H "Content-Type: application/json" \
-d '{"data":{"craving":"something warm with chicken"}}'

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.

  1. Start the Developer UI from your project root:

    Terminal window
    genkit start -- uv run python main.py

    This launches the Developer UI at http://localhost:4000 by default.

  2. Select bargainChefFlow from the list of flows.

  3. Enter sample input:

    { "craving": "something warm with chicken" }
  4. 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.

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.