AI EngineeringZero to ProductionHome·About·Contact
Data & App Building · Chapter B4

Building AI-Native Applications with Streamlit and Gradio

A FastAPI backend (B3) has no face. Streamlit and Gradio let you put a real UI on your LLM app in pure Python — no HTML, CSS, or JavaScript. This chapter builds a streaming chat app, covers the state model that trips everyone up, and shows when a quick UI is enough versus when you need the B3 backend.

⏱️ ~55 min🖥️ Hands-on🎯 Beginner→Intermediate
⚙️ To run this for realThe code here is complete and correct as written. To actually execute it you'll need:
  • an Anthropic API key (ANTHROPIC_API_KEY) + pip install anthropic
Reading and learning works without any of this — run when you're ready. IDs/ARNs in examples are placeholders; swap in your own.

Learning objectives

  • Explain what Streamlit and Gradio are for and how they differ.
  • Build a Streamlit app and understand its rerun-on-interaction model.
  • Manage state and build a streaming chat UI.
  • Build the same app in Gradio and know when each tool fits.
  • Decide UI-only vs UI + FastAPI backend, and mind the security basics.
The last mile — and it reuses everythingThese tools display the LLM logic you've built all course: the classifier (Ch 2), the RAG bot (Ch 3), the agent (Ch 4/L4), the charts (B2). They're the fastest way to demo or ship a small AI app. For production-grade backends you'll still want FastAPI (B3) — this chapter covers when each is the right call. Both frameworks evolve; learn the model, not exact widget names.

What they are — Python UIs, no frontend advanced

Streamlit and Gradio both turn a Python script into a web app. You write Python; they render widgets (text boxes, buttons, chat, charts, file uploaders) and run a local/hosted server. No HTML/CSS/JS. The difference is emphasis:

StreamlitGradio
Feels likeA reactive data-app / dashboard scriptAn interface wrapped around a function
Best atMulti-widget apps, dashboards, internal toolsQuick model demos; ML-focused components
ModelRe-run the whole script on every interactionWire inputs → a function → outputs
Sweet spot"An app with several controls and views""A demo of this one function/model"
Rough rule of thumbReach for Gradio when you want to wrap one function/model in an interface fast (a chat box over your agent, a demo to share). Reach for Streamlit when you're building a small app with several inputs, tabs, and charts (an internal analytics tool, a RAG explorer). Both can do both — this is about which grain you're going with.

The Streamlit mental model: rerun-on-interaction advanced

The one thing that confuses every Streamlit beginner: your entire script re-runs top-to-bottom on every interaction. Click a button, type in a box — the whole file executes again. Understand this and Streamlit is simple; miss it and state behaves "randomly."

interaction script re-runs line 1 … line Ntop to bottom fresh UI drawn local variables reset each run — persist with st.session_state Every click re-runs the whole script. That's why a plain variable resets each interaction — it's re-initialized every run. Anything that must survive (chat history, a loaded model, counters) goes in st.session_state. And expensive setup (loading data, building a retriever) gets a cache decorator so it doesn't repeat every rerun.
🗺️ How to read this diagram

This picture explains the one rule that makes Streamlit click: every time you touch the UI, Streamlit runs your whole Python file again from the top. There is no clever "only update this button" — it re-executes line 1 to line N, then draws a fresh screen.

  • The left box (interaction) is any user action — a click, typing in a box, moving a slider. That is the trigger.
  • The middle box (script re-runs, line 1 … line N, top to bottom) is the surprising part: your entire script executes again, start to finish, on every single interaction.
  • The right box (fresh UI drawn) is the result — Streamlit paints a brand-new screen from whatever that fresh run produced.
  • The red caption line is the catch: because the script starts over, an ordinary variable is re-created from scratch each run, so it "forgets." Anything that must survive (chat history, a counter, a loaded model) has to be stored in st.session_state, which Streamlit keeps between runs.

In short: Read it as a loop you can't see: interaction → whole script runs again → new screen. If a value seems to reset "randomly," it's just a plain variable being re-born on the next rerun — move it into st.session_state.

Lab B4.1 · A Streamlit chat app advanced

Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
Lab B4.1
shellpip install streamlit anthropic
streamlit run app.py
app.pyimport streamlit as st
from anthropic import Anthropic
client = Anthropic()

st.title("Chat with Claude")

# history must survive reruns → session_state
if "messages" not in st.session_state:
    st.session_state.messages = []

# re-draw the whole conversation every run
for m in st.session_state.messages:
    st.chat_message(m["role"]).write(m["content"])

if prompt := st.chat_input("Ask something"):
    st.session_state.messages.append({"role":"user","content": prompt})
    st.chat_message("user").write(prompt)

    with st.chat_message("assistant"):
        with client.messages.stream(model="claude-opus-4-8", max_tokens=1024,
                messages=st.session_state.messages) as stream:
            text = st.write_stream(stream.text_stream)   # streams tokens live
    st.session_state.messages.append({"role":"assistant","content": text})
▶ How this works

This is a complete streaming chat app in about 15 lines. Keep the rerun rule in mind: this whole file runs top-to-bottom every time you send a message, so the code is written to rebuild the screen each run while remembering the conversation in between.

  1. import streamlit as st and client = Anthropic() set up the two tools: Streamlit draws the UI, and the Anthropic client talks to Claude (it reads your API key from the environment). st.title(...) just puts a heading on the page.
  2. The session_state guard. if "messages" not in st.session_state: runs only the first time — it creates an empty list to hold the chat. On later reruns the list already exists, so it is kept instead of wiped. This is how the app remembers past turns despite the whole script re-running.
  3. Redraw the whole conversation. The for m in st.session_state.messages: loop re-prints every past message each run — because the screen is rebuilt from scratch every time, you must paint the full history yourself.
  4. Handle a new message. if prompt := st.chat_input(...) is true only when the user actually typed something. When they do, the app appends their message to the history, shows it, then opens client.messages.stream(...) and st.write_stream(stream.text_stream) prints Claude's reply token by token, live. Finally it appends the finished reply to the history so it survives the next rerun.

What the output means: A working chat window: your messages and Claude's answers stack up the page, and each new answer types out live instead of appearing all at once. The conversation stays put even though the script re-runs on every message.

Try this: Delete the if "messages" not in st.session_state: guard and set st.session_state.messages = [] directly — now the history wipes on every message and Claude forgets everything. That's the whole point of session_state.

Three ideas do all the workst.session_state holds the message history across reruns (the C2 stateless-API pattern, in the UI). Redraw the loop — because the script re-runs, you re-render the whole conversation each time. st.write_stream consumes the Claude token stream (C2) straight into the UI. That's a real streaming chat app in ~15 lines.
Cache expensive setup — don't rebuild it every rerunBecause the script re-runs on every interaction, anything expensive (loading a dataframe, building a RAG retriever/index from Ch 3, creating a client pool) will re-run too — slow and wasteful. Wrap it in Streamlit's cache decorator so it runs once and is reused. Forgetting this is why beginner Streamlit apps feel sluggish: they rebuild the world on every keystroke.

Lab B4.2 · The same, in Gradio expert

Gradio's model is different: you define a function and wrap a chat interface around it. Less boilerplate for a pure "wrap my model" demo.

Lab B4.2
gradio_app.pyimport gradio as gr
from anthropic import Anthropic
client = Anthropic()

def respond(message, history):     # Gradio passes history for you
    msgs = [{"role": h["role"], "content": h["content"]} for h in history]
    msgs.append({"role":"user","content": message})
    with client.messages.stream(model="claude-opus-4-8", max_tokens=1024,
            messages=msgs) as stream:
        partial = ""
        for t in stream.text_stream:
            partial += t
            yield partial          # yield to stream into the chat UI

gr.ChatInterface(respond).launch()     # a full chat UI from one function
▶ How this works

Same chat app, Gradio style. The mindset flips: instead of a script that re-runs and manages its own state, you write one function that takes a message and returns a reply, and Gradio wraps a whole chat UI around it. Notice there is no session_state here at all.

  1. def respond(message, history): is the one function you write. Gradio calls it for every user turn and hands you the past conversation in history automatically — you don't have to store it yourself the way Streamlit made you.
  2. The first two lines rebuild the message list Claude expects: turn each past turn in history into a {"role", "content"} dict, then append the new message as the latest user turn.
  3. with client.messages.stream(...) asks Claude for a streamed reply. The loop for t in stream.text_stream: adds each new chunk onto partial and then yield partial hands the growing text back. Yielding (instead of return) is what makes the answer stream live in the chat box — each yield updates the screen.
  4. gr.ChatInterface(respond).launch() is the payoff: one line turns your function into a full, shareable chat web app — input box, message bubbles, and streaming, all provided for you.

What the output means: A chat UI that behaves just like the Streamlit one, but you wrote far less: no state bookkeeping, no manual redraw. Gradio supplies the history and the interface; you only supplied the "what to say back" function.

Try this: Compare the two labs side by side. Streamlit = a re-running app where you own the state; Gradio = one function wrapped in a ready-made UI. That difference is the whole "which tool when" decision in this chapter.

Gradio manages the history for youNotice Gradio passes history into your function — you don't manage session state yourself as in Streamlit. And a generator that yields the growing string gives you streaming for free. gr.ChatInterface(fn) is the fastest path from "I have a function that talks to Claude" to "here's a shareable chat app." This is exactly how model demos on sharing platforms are built.

Beyond chat: the widgets that matter for AI apps expert

Chat is the headline, but both frameworks give you the pieces to build real AI tools — connecting straight back to earlier modules.

Widget / componentPowers…
File uploadDrop a PDF/CSV → RAG (Ch 3) or Document Intelligence (P4)
Dataframe / table displayShow a Pandas result (B1) — the Data Analyst UI (P5)
Chart display (st.pyplot / Gradio Plot)Render the Figures you returned in B2
Image / audio in/outMultimodal apps (T2)
Sliders / selectsExpose model, effort (C1), or a temperature-like knob
Feedback buttons (👍/👎)Collect the quality signal for evals/monitoring (O4, I4)
This is the display layer for the whole courseFile-upload + RAG chat is the Document Intelligence project's UI. A dataframe + chart + question box is the Data Analyst project. A thumbs-up/down on each answer feeds the O4/I4 evaluation flywheel. These frameworks are how the LLM engineering you've learned becomes something a non-engineer can actually use.

UI-only vs UI + FastAPI backend expert

Key architectural call: is a Streamlit/Gradio app enough, or do you need a B3 FastAPI backend behind it?

Simple: UI does everything Streamlit/Gradio Claude Scaled: UI → backend UI FastAPI (B3) Claude Two architectures. For a demo or internal tool, the UI can call Claude directly — simplest. For anything shared, multi-client, or that needs real auth/scaling/monitoring, put the logic in a FastAPI backend (B3) and let the UI be a thin client. The backend can then also serve mobile, other services, or an agent — the UI is just one consumer.
🗺️ How to read this diagram

This diagram lays out the one architecture decision of the chapter: should your UI talk to Claude directly, or should it go through a separate backend first? The two stacks show the same app at two stages of growth.

  • The left stack ("Simple: UI does everything") is two boxes: Streamlit/Gradio calls Claude directly. Fewest moving parts — perfect for a demo, a prototype, or an internal tool.
  • The right stack ("Scaled: UI → backend") adds a middle box: the UI calls FastAPI (B3), and that calls Claude. The UI becomes a thin "face" and all the real logic lives in the backend.
  • The arrows show who calls whom — always top-to-bottom, one layer to the next. The extra layer on the right is the whole point: it's where you'd add auth, rate limits, scaling, and monitoring.
  • Why bother with the right one? Because a backend can serve many clients at once (web, mobile, other services, an agent), not just this one UI — so the logic is reusable and testable on its own.

In short: Start on the left (UI → Claude) for anything small or private. Move to the right (UI → FastAPI → Claude) the moment the app is shared, needs real auth/limits, or has more than one kind of client.

UI-only is fine when…Add a FastAPI backend (B3) when…
Demo, prototype, internal toolMultiple clients (web + mobile + other services)
One or few trusted usersYou need real auth, rate limits, scaling (O3)
Logic is simpleHeavy/long agent runs, background jobs
Speed to ship matters mostYou want the logic reusable & independently testable

Security & deployment basics expert

The moment your app is reachable by others, the T1/O3 rules apply — and these frameworks make it dangerously easy to skip them.

ConcernDo this
API keyFrom env/secrets, never in the script or repo (T1, O3). The key lives server-side — never ship it to the browser
AuthA public Streamlit/Gradio app is open to the world — add auth, or keep it internal
Cost/abuseAn open chat app is an open tab on your bill — rate-limit, cap, monitor (O2/O4)
Prompt injectionUser input still flows to the model — the T1 threats don't disappear behind a nice UI
DeployContainer it (O3), or use the frameworks' hosting; put a backend behind it if it grows
A pretty UI doesn't change the threat modelIt's tempting to treat a Streamlit demo as "just a script," but the second it's shared it's a public LLM endpoint with all of T1's risks — plus a wide-open cost meter if there's no auth or rate limit. Keep the API key server-side, gate access, cap usage, and remember every input reaches the model. The UI is a face; the security is the same as B3/T1.

Common pitfalls expert

PitfallFix
Expecting variables to persist across reruns (Streamlit)Use st.session_state
Re-loading data/model every rerunCache expensive setup
Not streaming (long blank wait)st.write_stream / yield in Gradio
API key in the scriptEnv/secrets, server-side only (T1)
Public app with no auth/limitsAdd auth & rate limits, or keep internal (O3)
Cramming a production backend into the UI scriptMove logic to FastAPI (B3) when it scales
Assuming a UI removes T1 risksSame threat model; guard input & cost

Exercises expert

Exercise B4.1 — Streaming chat, both ways

Context: Building the same streaming chat in both frameworks over one Claude call is the fastest way to feel their different models — and to form an opinion on which you'd grow into a real app.

Your task: Build the streaming chat both ways — a Streamlit version and a Gradio version — over the same Claude call, noting where each manages history and streaming.

Requirements:

  • Implement the chat once in Streamlit and once in Gradio over the same underlying Claude call
  • Both stream the response as it's generated
  • Note where each framework manages conversation history
  • Reflect on which was faster to build and which you'd extend into a bigger app

💡 Hint: Contrast the mechanics — Streamlit persists history in st.session_state and re-runs, while Gradio hands you the history in the callback.

Exercise B4.2 — A real mini-app

Context: Uploading a CSV, showing it, asking a question, and rendering both an answer and a chart is the projects' UI pattern in miniature — a mini Data Analyst. Caching the load is what keeps it from re-parsing on every question.

Your task: Build a Streamlit app that uploads a CSV, shows it as a table, lets the user ask a question, and displays both a Claude answer and a chart — caching the CSV load.

Requirements:

  • Accept a CSV upload and display it as a table
  • Cache the CSV load so it isn't re-parsed on every interaction
  • Let the user ask a question and show a Claude-generated answer
  • Render a chart, using a function that returns a Figure with st.pyplot
  • The result is a mini Data Analyst app

💡 Hint: Caching matters most here — wrap the load so reruns reuse it — and reuse the return-a-Figure habit from the viz lesson to render with st.pyplot.

Show what to look for

The caching matters most here — without it, every question re-parses the CSV. And returning a Figure from a function (B2) is what lets you render the chart with st.pyplot. This app is the projects' UI pattern in miniature.

Exercise B4.3 — UI-only or backend?

Context: The UI-only-versus-backend call isn't one answer — it shifts with who uses the app. Reasoning it through for a prototype, an internal tool, and a public product builds the judgment the module is really teaching.

Your task: For three scenarios — a solo prototype, an internal team tool, and a public product — decide UI-only vs UI + FastAPI backend and justify each, and for the public one list the protections you'd add before launch.

Requirements:

  • Give a decision (UI-only or UI + backend) for each of the three scenarios
  • Justify each choice by who uses it and what it needs (shared logic, secrets, auth, scale)
  • For the public product, list the threat-model / deployment protections to add before launch
  • Tie the reasoning back to the same split the FastAPI lesson makes

💡 Hint: Lean UI-only for the prototype, lean toward a backend as the audience widens, and for the public case name the auth, rate-limit, and input-validation guards from the security rung.

🪜 Practice ladder beginner → industry

Six graded exercises, easy to real-world. Try each before opening its solution.

Exercise 1 · A minimal Streamlit pageBeginner

Context: Streamlit's whole model is one idea: it re-runs your script top-to-bottom on every interaction, so the UI is just a function of the current widget state. There's no callback wiring to learn first.

Your task: Write a minimal Streamlit app with a title, a text input, and output that echoes the input back, and explain the rerun-on-interaction model in one line.

Requirements:

  • Show a title and a text-input widget
  • Echo the entered value back as output
  • No callback wiring — you read the widget value and write output directly
  • State in one line that the whole script re-runs on every interaction
  • Note the run command (streamlit run app.py)

💡 Hint: Read the widget's return value and conditionally write output; Streamlit re-executes the script whenever that input changes.

Show solution

Rerun model: Streamlit re-runs your whole script top-to-bottom on every interaction, so the UI is just a function of the current widget state. Needs streamlit + a browser:

import streamlit as st

st.title("Echo app")
name = st.text_input("Your name")
if name:
    st.write(f"Hello, {name}!")

# run:  streamlit run app.py

There’s no callback wiring — you read the widget’s value and write output, and Streamlit re-executes the script whenever the input changes.

Exercise 2 · A Streamlit chat app with historyIntermediate

Context: Because the script re-runs on every interaction, ordinary variables reset each time — so a chat would forget its history on the next keystroke. st.session_state is what persists data across reruns.

Your task: Build a Streamlit chat UI that remembers the conversation across reruns, and explain why st.session_state is required given the rerun model.

Requirements:

  • Initialize a message list in st.session_state if it isn't present
  • Render the existing history on each run with st.chat_message
  • Append the user's turn and the assistant reply to session state
  • Explain that ordinary variables reset each rerun, so history must live in st.session_state
  • Use st.chat_input to collect the next message

💡 Hint: Guard the history list with an if "messages" not in st.session_state check so it survives reruns, then append each turn to it.

Show solution

Why session_state: because the script re-runs every interaction, ordinary variables reset each time. st.session_state persists data (the message history) across reruns. Needs streamlit + a browser:

import streamlit as st

st.title("Chat")
if "messages" not in st.session_state:
    st.session_state.messages = []          # survives reruns

for m in st.session_state.messages:
    st.chat_message(m["role"]).write(m["content"])

if prompt := st.chat_input("Say something"):
    st.session_state.messages.append({"role": "user", "content": prompt})
    reply = f"You said: {prompt}"           # replace with an LLM call
    st.session_state.messages.append({"role": "assistant", "content": reply})
    st.chat_message("assistant").write(reply)

Without session_state the history would vanish on the next keystroke — that’s the classic Streamlit beginner bug.

Exercise 3 · The same chat in GradioAdvanced

Context: Gradio takes the opposite approach to Streamlit: instead of re-running a script, it's callback-based — you write a (message, history) -> reply function and Gradio wires the UI to it and manages history for you.

Your task: Implement an equivalent chat with Gradio's ChatInterface and contrast Gradio's function/callback model with Streamlit's script-rerun model.

Requirements:

  • Write a respond(message, history) function that returns the reply
  • Wrap it in gr.ChatInterface(fn=respond, ...) and launch it
  • Note that Gradio manages the conversation history for you
  • Contrast the models: Streamlit re-runs the script; Gradio wires the UI to your function
  • State the trade-off — Gradio for a quick shareable demo, Streamlit for a custom-layout app

💡 Hint: The whole app is one function plus ChatInterface; let Gradio own the history argument rather than tracking it yourself.

Show solution

Contrast: Streamlit re-runs the script; Gradio is callback/function-based — you write a function (message, history) -> reply and Gradio wires the UI to it. Needs gradio + a browser:

import gradio as gr

def respond(message, history):
    # history is a list of prior (user, assistant) turns, managed by Gradio
    return f"You said: {message}"           # replace with an LLM call

demo = gr.ChatInterface(fn=respond, title="Chat")
demo.launch()

Gradio manages history for you and is quick for a shareable demo; Streamlit’s script-rerun model gives you more control over arbitrary page layout. Pick by whether you want a demo (Gradio) or an app (Streamlit).

Exercise 4 · The widgets that matter for AI appsExpert

Context: Beyond the chat box, a real AI app needs a few specific controls: a way to bring your own document, a knob for a model parameter, and a clean way to reset state. These three cover most of what turns a toy into a tool.

Your task: Show three Streamlit widgets an AI app actually needs — a file uploader, a slider for a parameter like temperature, and a stop/clear control — wired to sensible behavior.

Requirements:

  • A st.file_uploader restricted to sensible types, acknowledging the loaded file
  • A st.slider exposing a model parameter such as temperature
  • A clear/reset button that wipes the conversation from st.session_state
  • Call st.rerun() after clearing to force a fresh run
  • Each widget maps to a real use — upload for bring-your-own-doc, slider for a parameter, button to reset

💡 Hint: The uploader turns chat into a document assistant, the slider surfaces a knob, and the button plus st.rerun() is how you reset state cleanly.

Show solution

Needs streamlit + a browser. Uploader (bring-your-own-doc RAG), a parameter slider, and a clear-history button:

import streamlit as st

uploaded = st.file_uploader("Upload a document", type=["txt", "pdf"])
if uploaded:
    st.success(f"Loaded {uploaded.name} ({uploaded.size} bytes)")

temperature = st.slider("Creativity (temperature)", 0.0, 1.0, 0.2, 0.05)
st.caption(f"Using temperature = {temperature}")

if st.button("Clear conversation"):
    st.session_state.pop("messages", None)
    st.rerun()                              # force a fresh run after clearing

The uploader turns a chat into a document assistant; the slider exposes a model parameter to the user; the button + st.rerun() resets state cleanly.

Exercise 5 · UI-only vs UI + FastAPI backendProfessional

Context: A Streamlit app can call the model directly, and that's fine for a demo or internal tool. But once you need shared logic, secrets off the client, auth, or rate limiting, a FastAPI backend belongs in front — the same split the da3 lesson makes.

Your task: Decide when the UI should call the model directly versus call a FastAPI backend, and show the wiring for the backend case — a Streamlit front end making a plain HTTP call.

Requirements:

  • State the decision: UI-only is fine for a demo/internal tool; add a backend for shared logic, secret-keeping, auth, or rate limiting
  • Show the Streamlit UI collecting the prompt
  • Have it POST to the FastAPI backend with plain requests (with a timeout)
  • Check the response status and render the returned answer
  • Note that the backend holds the API key and business logic, letting other clients reuse it

💡 Hint: Keep the UI thin — collect input, requests.post to /ask, display the JSON answer — and let the backend own the key and the real logic.

Show solution

Decision: a UI-only app (Streamlit calls the model directly) is fine for a demo or an internal tool. Put a FastAPI backend in front when you need: shared logic across multiple UIs, to keep API keys off the client, real auth, rate limiting, or to reuse the same service from non-UI callers.

Needs streamlit to run the UI; the HTTP call is plain requests:

import streamlit as st
import requests

prompt = st.chat_input("Ask")
if prompt:
    # the backend holds the API key and the real logic:
    r = requests.post("http://localhost:8000/ask", json={"prompt": prompt}, timeout=30)
    r.raise_for_status()
    st.chat_message("assistant").write(r.json()["answer"])

The split keeps secrets and business logic server-side and lets a web UI, a CLI, or another service share one backend — the same reasoning as the da3 FastAPI lesson.

Exercise 6 · Security & deployment basics for a shared appIndustry scenario

Context: Before a Streamlit or Gradio app leaves your laptop, a short list of must-dos stands between you and a leaked key or an open door — and the single highest-value habit is never hardcoding a secret.

Your task: List the security and deployment must-dos before sharing a Streamlit/Gradio app beyond your machine, and show the one code habit that prevents the most common leak — reading secrets from the environment.

Requirements:

  • Cover the essentials: never hardcode secrets, add auth (these frameworks are open by default), validate and rate-limit input, hide internal errors, deploy behind HTTPS with pinned deps
  • Read the API key from os.environ, never a literal in source
  • Refuse to start (raise) when the key is missing rather than failing later
  • Never print or log the secret's value
  • Explain that reading from the environment is what keeps keys out of git history

💡 Hint: Pull the key with os.environ.get(...) and raise if it's absent; the value never appears in the source, so it can't be committed.

Show solution
  1. Never hardcode secrets — read API keys from environment/secrets, never commit them.
  2. Add auth — these frameworks are open by default; put the app behind SSO/a proxy or a password before exposing it.
  3. Validate & limit input — cap upload size and request rate; treat user text as untrusted.
  4. Don’t leak internals — show users a friendly error, log the detail server-side.
  5. Deploy behind HTTPS and pin dependencies.

The single highest-value habit — reading secrets from the environment — is plain Python and runs clean:

import os

api_key = os.environ.get("MODEL_API_KEY")
if not api_key:
    raise RuntimeError("MODEL_API_KEY is not set; refusing to start")
print("key loaded from environment:", bool(api_key))   # True, value never printed

Reading from os.environ (never a literal in the source) is what keeps keys out of your git history — the most common way demo apps leak credentials.

✓ Checkpoint — you can move on when you can…

  • Say what Streamlit and Gradio are for and how they differ.
  • Explain Streamlit's rerun model and use session_state + caching.
  • Build a streaming chat app in both frameworks.
  • Decide UI-only vs UI + FastAPI backend for a scenario.
  • Apply the API-key, auth, cost, and injection basics to a shared app.
🏗️ Toward the capstone — module completeEvery project in the gallery gets its face here: a Streamlit/Gradio UI over the agent, with file upload for docs (P4), a table+chart for data (P5), and 👍/👎 feedback feeding evals (O4/I4). The AI DevOps Engineer's operator dashboard — where a human reviews proposed changes and approves risky ones (L5) — is exactly this: a UI over the FastAPI backend (B3) that fronts the agent. You've now completed Data & App Building: wrangle it (B1), chart it (B2), serve it (B3), show it (B4). See the project gallery →

Knowledge check check yourself

✓ Knowledge check

What is Streamlit's "rerun-on-interaction" model, and how do you keep values (like chat history) from being wiped each run?

Show answer
Every user interaction re-executes the whole script top-to-bottom, so ordinary variables are re-created and "forget" their values; anything that must survive goes in st.session_state, which Streamlit preserves between reruns.
✓ Knowledge check

How does Gradio's model differ from Streamlit's for building a chat app?

Show answer
Gradio wraps a UI around a single function: you write a respond(message, history) function and Gradio passes the history and supplies the whole interface — no session_state or manual redraw — whereas Streamlit re-runs a script where you own the state.
© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in