Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

Build a local call-review panel with Streamlit: load records from a CSV file, search and select a call, then save its review outcome and notes to SQLite. This tutorial handles records you already have; it does not connect to a phone service or collect calls.

What the app does—and where its data lives

Streamlit is an open-source Python framework for interactive data apps. A local app runs a Python server and opens its interface in a browser on the same computer. If you later share it over a network, the server and its files remain on the host computer, not on each viewer’s device. Streamlit’s architecture documentation explains this boundary: the app cannot silently browse a viewer’s local files. To supply files from another computer, a user must explicitly upload them or make them available to the host.

The example uses a CSV for call records and a local SQLite database for review decisions. The CSV is easy to inspect or replace; SQLite gives the app a durable place to store changing review fields. Streamlit’s data guide demonstrates SQLite as a local connection and describes it as semi-persistent storage. See Streamlit’s data-connection guide.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Choose the input and review storage

Option Setup and inspection Review-field updates Best fit
CSV or JSON file Minimal setup; straightforward to inspect and edit manually. Possible, but the app must write updates back carefully; keeping source records and review changes separate is usually clearer. A short demonstration or a supplied dataset that does not need concurrent editing.
SQLite database Requires a small schema and database connection; inspectable with SQLite tools. Designed for updating review status and notes as application data. Repeated reviews, persistent annotations, or filtering by review outcome.

There is no universal dataset-size threshold at which one choice becomes necessary. Start with a file when the data is static and move review decisions to SQLite when they need reliable persistence and repeated updates.

Prepare a small sample CSV

Create a file named calls.csv beside the Python script. Use synthetic or redacted content while developing; real call transcripts may contain personal or sensitive information.

call_id,caller,started_at,transcript
C-1001,Example Caller,2026-09-15 09:30,"I need help changing my appointment."
C-1002,Another Caller,2026-09-15 10:05,"Please call me back about my order."

The app expects the columns call_id, caller, started_at, and transcript. If your export uses different names, adjust the column references in the code or rename the CSV headers. This sample is only a format illustration, not a connection to a telephony system.

Create the Streamlit app

Install Python and Streamlit in your chosen Python environment, then save the following as my_app.py. The code loads call metadata from the CSV, creates a SQLite table for review outcomes, provides search and status filters, and saves a review when the form is submitted.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
import sqlite3
from pathlib import Path

import pandas as pd
import streamlit as st

CALLS_FILE = Path(__file__).with_name("calls.csv")
DB_FILE = Path(__file__).with_name("reviews.sqlite3")

st.set_page_config(page_title="Local Call Review", layout="wide")
st.title("Local call review")


def connect_db():
    connection = sqlite3.connect(DB_FILE)
    connection.execute("""
        CREATE TABLE IF NOT EXISTS reviews (
            call_id TEXT PRIMARY KEY,
            disposition TEXT NOT NULL DEFAULT '',
            notes TEXT NOT NULL DEFAULT '',
            reviewed INTEGER NOT NULL DEFAULT 0,
            updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
        )
    """)
    return connection


def get_reviews():
    with connect_db() as connection:
        return pd.read_sql_query("SELECT * FROM reviews", connection)


if not CALLS_FILE.exists():
    st.error(f"Could not find {CALLS_FILE.name}. Put it beside my_app.py.")
    st.stop()

calls = pd.read_csv(CALLS_FILE, dtype={"call_id": str}).fillna("")
required = {"call_id", "caller", "started_at", "transcript"}
missing = required - set(calls.columns)
if missing:
    st.error("CSV is missing required columns: " + ", ".join(sorted(missing)))
    st.stop()

reviews = get_reviews()
data = calls.merge(reviews, on="call_id", how="left")
data["disposition"] = data["disposition"].fillna("")
data["notes"] = data["notes"].fillna("")
data["reviewed"] = data["reviewed"].fillna(0).astype(int)

with st.sidebar:
    st.header("Find calls")
    query = st.text_input("Search caller, ID, or transcript")
    status = st.selectbox("Review status", ["All", "Not reviewed", "Reviewed"])

filtered = data.copy()
if query:
    searchable = filtered[["call_id", "caller", "transcript"]].astype(str).agg(" ".join, axis=1)
    filtered = filtered[searchable.str.contains(query, case=False, na=False)]
if status == "Not reviewed":
    filtered = filtered[filtered["reviewed"] == 0]
elif status == "Reviewed":
    filtered = filtered[filtered["reviewed"] == 1]

if filtered.empty:
    st.info("No calls match these filters.")
    st.stop()

labels = {
    row.call_id: f"{row.call_id} — {row.caller} — {row.started_at}"
    for row in filtered.itertuples()
}
selected_id = st.selectbox("Select a call", options=list(labels), format_func=labels.get)
call = filtered.loc[filtered["call_id"] == selected_id].iloc[0]

left, right = st.columns([1, 2])
with left:
    st.subheader("Call details")
    st.write("**Call ID:**", call["call_id"])
    st.write("**Caller:**", call["caller"])
    st.write("**Started:**", call["started_at"])
with right:
    st.subheader("Transcript")
    st.text_area("Call transcript", value=str(call["transcript"]), height=220, disabled=True)

with st.form("review_form"):
    st.subheader("Review outcome")
    choices = ["", "Resolved", "Follow-up needed", "Escalate", "Other"]
    current = call["disposition"] if call["disposition"] in choices else "Other"
    disposition = st.selectbox("Disposition", choices, index=choices.index(current))
    notes = st.text_area("Review notes", value=str(call["notes"]), height=120)
    reviewed = st.checkbox("Mark as reviewed", value=bool(call["reviewed"]))
    save = st.form_submit_button("Save review")

if save:
    with connect_db() as connection:
        connection.execute("""
            INSERT INTO reviews (call_id, disposition, notes, reviewed, updated_at)
            VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
            ON CONFLICT(call_id) DO UPDATE SET
                disposition = excluded.disposition,
                notes = excluded.notes,
                reviewed = excluded.reviewed,
                updated_at = CURRENT_TIMESTAMP
        """, (selected_id, disposition, notes, int(reviewed)))
    st.success("Review saved locally.")

The form saves on submission rather than on every widget change. Its SQLite call_id primary key ensures each call has one current review row; submitting another review for the same call updates that row. The database file is created next to the script the first time the app connects.

Rank #3
The Standards Real Book, C Version
  • Used Book in Good Condition

Run the panel on your computer

  1. Place my_app.py and calls.csv in the same folder.
  2. In a terminal opened in that folder, install the dependencies with python -m pip install streamlit pandas.
  3. Start the app with streamlit run my_app.py. Streamlit documents this local command and serves the app at http://localhost:8501. See the command-line installation guide.
  4. Search or filter the call list, select a record, enter a disposition and notes, and choose Save review. The saved row goes to reviews.sqlite3.

If the command is not found, confirm that Streamlit was installed in the same Python environment used by the terminal. If the app reports missing CSV columns, check the header row against the four required names. If a saved review seems absent, confirm that the app is using the expected folder and database file.

Keep application logs separate from review data

Call records, dispositions, and notes are application data; they belong in the CSV and SQLite table, not in diagnostic logs. Python’s logging system is for recording application events with severity levels and handlers, such as an error loading a file or a failed database operation. Python 3.14.8’s Logging HOWTO describes loggers, levels, and handlers. Avoid writing transcripts or sensitive review notes to routine logs.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Understand what “local” means if you share the app

On one computer, the Python server and browser are both local. If another person opens the app across a network, the server still runs on the computer where it was started, and that machine supplies the files and computation. A browser viewer cannot make the app read arbitrary files from their own computer; provide an explicit upload control if user-supplied files are part of the design. Streamlit documents file upload widgets as the explicit route for browser-provided files in its architecture guide.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Widget state is useful for temporary interface interaction, but it is not a substitute for saved records. Streamlit notes that widget state can be deleted when a widget is not rendered in a script run; saving a decision to SQLite avoids relying on that transient state. The widget behavior guide explains callbacks, keys, and state behavior.

Moving the app to a hosted service changes where data resides and who operates the host. Streamlit Community Cloud is hosted infrastructure, and Streamlit warns that local file storage there is not guaranteed to persist. Do not treat a local SQLite file on that service as durable storage. Streamlit’s data guide covers this persistence caveat. Authentication alone also does not determine which users may access particular calls; a shared deployment needs an authorization design appropriate to its data and users. Streamlit’s login reference describes OpenID Connect sign-in, but sign-in is not an access-control policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.