October DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsSlow PC?RecommendedPC slow today? Run a repair scan before it gets worseResolve common Windows issues and optimize system performance.Scan NowOctober DealsAmazon USDeal season is back - check today's better picksAmazon US: current deals, useful picks and tech finds.See Picks×
Skip to content
World desk7 min

How to Build a Local Call Review App with Python

Build a local Streamlit dashboard to search call records, inspect transcripts, and save dispositions and notes to SQLite.
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Build a local call-review panel with Streamlit and Python to browse call records, inspect a transcript, and save a review outcome. This tutorial uses a CSV file for the initial records and SQLite for durable notes and dispositions. It does not connect to a phone service or collect calls; it works with records you already have.

What the app will do

The panel will let one reviewer search and filter a small set of call records, open a selected record, and save a disposition, notes, and reviewed status. Streamlit is an open-source Python framework for interactive data apps, and its command-line workflow runs the app locally. See Streamlit documentation.

As an Amazon Associate I earn from qualifying purchases.

Keep the input data synthetic or redacted while building and testing. Call transcripts can contain personal or confidential information; this example does not determine whether a particular use complies with privacy or recording laws.

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

Choose where records and reviews are stored

Option Setup and inspection Review updates Best fit
CSV or JSON file Simple to create, inspect, and replace manually. Possible, but rewriting the source file for every review is awkward and can be error-prone when updates overlap. A short demo or a supplied dataset that does not need repeated edits.
SQLite database Requires a database file and a small amount of SQL, but remains local and inspectable with suitable tools. Stores review fields separately from source records and supports repeated updates and filtering. A panel that must retain decisions and notes between sessions.

There is no universal dataset-size threshold at which one option becomes necessary. This example keeps the original records in CSV and stores annotations in SQLite, which avoids modifying the source file when a review changes. Streamlit’s data guide describes a local SQLite connection as a semi-persistent storage option; persistence depends on the environment where the app runs: connecting to data.

Set up the project and sample data

Use a supported Python installation and create a project directory. In a terminal opened there, create and activate a virtual environment, then install Streamlit and pandas:

python -m venv .venv
# macOS or Linux:
source .venv/bin/activate
# Windows PowerShell:
.venvScriptsActivate.ps1
python -m pip install streamlit pandas

Create calls.csv in the same directory. Use a unique ID for each record; the app uses it to attach a review to the correct call.

call_id,started_at,agent,transcript
C-001,2026-09-12 09:15,Alex,"Customer asked about a delivery delay."
C-002,2026-09-12 10:40,Sam,"Customer requested help resetting an account password."
C-003,2026-09-12 11:05,Alex,"Customer asked about a billing adjustment."

These are fictional examples. Replace them with a CSV whose header names match the code, or adapt the column references. Avoid using a real transcript in a public repository or an unsecured shared folder.

Free tools Windows power users keep installed

One-click scans. No signup required.

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

Build the review panel

Create my_app.py. This version provides search, agent filtering, record selection, a transcript detail view, and a form that writes review decisions to reviews.db. SQLite creates that database file in the app’s working directory on first use.

import logging
import sqlite3
from pathlib import Path

import pandas as pd
import streamlit as st

DATA_FILE = Path(__file__).with_name("calls.csv")
DB_FILE = Path(__file__).with_name("reviews.db")

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


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,
            notes TEXT NOT NULL DEFAULT '',
            reviewed INTEGER NOT NULL DEFAULT 0,
            updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
        )
    """)
    return connection


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


def save_review(call_id, disposition, notes, reviewed):
    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
        """, (call_id, disposition, notes, int(reviewed)))


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

if not DATA_FILE.exists():
    st.error(f"Missing input file: {DATA_FILE.name}")
    st.stop()

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

if calls["call_id"].duplicated().any():
    st.error("Each call_id must be unique.")
    st.stop()

reviews = load_reviews()
review_map = reviews.set_index("call_id").to_dict("index") if not reviews.empty else {}
calls["reviewed"] = calls["call_id"].map(
    lambda call_id: bool(review_map.get(call_id, {}).get("reviewed", 0))
)

search = st.text_input("Search calls", placeholder="Search transcript, agent, or ID")
agents = sorted(value for value in calls["agent"].unique() if value)
selected_agents = st.multiselect("Filter by agent", agents, default=agents)
unreviewed_only = st.checkbox("Show unreviewed only")

visible = calls[calls["agent"].isin(selected_agents)]
if search.strip():
    term = search.strip()
    match = visible[["call_id", "started_at", "agent", "transcript"]].astype(str).apply(
        lambda column: column.str.contains(term, case=False, regex=False)
    ).any(axis=1)
    visible = visible[match]
if unreviewed_only:
    visible = visible[~visible["reviewed"]]

st.subheader("Matching calls")
if visible.empty:
    st.info("No calls match these filters.")
    st.stop()

visible = visible.sort_values("started_at", ascending=False)
st.dataframe(
    visible[["call_id", "started_at", "agent", "reviewed"]],
    hide_index=True,
    use_container_width=True,
)

labels = {
    row.call_id: f"{row.call_id} · {row.started_at} · {row.agent}"
    for row in visible.itertuples()
}
selected_id = st.selectbox("Open a call", list(labels), format_func=labels.get)
call = calls.loc[calls["call_id"] == selected_id].iloc[0]
existing = review_map.get(selected_id, {})

left, right = st.columns([2, 1])
with left:
    st.subheader("Call details")
    st.write(f"**Started:** {call['started_at']}")
    st.write(f"**Agent:** {call['agent']}")
    st.text_area("Transcript", value=call["transcript"], height=260, disabled=True)

with right:
    st.subheader("Review outcome")
    choices = ["Needs follow-up", "Resolved", "Escalate", "Other"]
    prior = existing.get("disposition", "")
    index = choices.index(prior) if prior in choices else 0
    with st.form(f"review-{selected_id}"):
        disposition = st.selectbox("Disposition", choices, index=index)
        notes = st.text_area("Review notes", value=existing.get("notes", ""), height=150)
        reviewed = st.checkbox("Mark as reviewed", value=bool(existing.get("reviewed", 0)))
        submitted = st.form_submit_button("Save review")
    if submitted:
        save_review(selected_id, disposition, notes, reviewed)
        logger.info("Review saved for call_id=%s", selected_id)
        st.success("Review saved.")
        st.rerun()

How the data is separated

The CSV remains the source for call metadata and transcript text. The SQLite table stores one review row per call_id, with a disposition, notes, reviewed flag, and timestamp. The primary key plus SQLite’s conflict update means saving again replaces that call’s earlier review rather than creating a duplicate. The parameterized SQL statement keeps values separate from the query text.

Why use a form and database write

Streamlit reruns a script as users interact with widgets. Widget state is useful for the current interface, but should not be treated as durable storage: a widget omitted during a run can lose its state. Here, the form submits an explicit save action, and the database retains the review across reruns and app restarts. Streamlit documents widget keys, callbacks, and state behavior in its widget behavior guide.

Run the app locally

  1. Save calls.csv and my_app.py in the project directory.

    What’s actually slowing this PC down?

    Pick the symptom - the matching free tool is one click away.

    Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  2. Activate the virtual environment if it is not already active.

  3. From that directory, run streamlit run my_app.py.

  4. Open http://localhost:8501 in a browser on the same computer. Streamlit documents this local server address and command in its getting-started guide.

  5. Select a call, enter an outcome, and click Save review. Confirm that the success message appears; the review is stored in reviews.db.

Common problems and fixes

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

Understand the local-file and sharing boundary

In this local setup, the Python server and browser client run on the same computer. If you make the app reachable over a network, the server still runs on the machine where you started it; that host reads the CSV and writes the database. A viewer’s browser does not grant the server general access to files on the viewer’s computer. To use a file from another computer, make it available to the host or add an explicit upload control. Streamlit explains this server/client boundary in its architecture documentation.

Sharing changes who can reach the app and where data resides. Do not expose a panel containing call information on an untrusted network without appropriate access controls and a secure deployment plan. Streamlit’s st.login supports OpenID Connect, but authentication alone does not define which users are authorized to see or edit particular records; see the login reference.

Community Cloud is a hosted service, not the same as running this script on your own computer. Streamlit warns that local file storage there is not guaranteed to persist, so a local SQLite file should not be treated as reliable long-term storage in that environment. See Streamlit’s data connection guidance before choosing storage for a deployed version.

Keep operational logs separate from review data

The call disposition and notes are application data, so they belong in the database. Python’s logging module is for operational events such as a save succeeding or a file failing to load; log severity levels and handlers let an application route those events appropriately. The example logs a call ID on save but deliberately does not log transcripts or review notes. See the Python Logging HOWTO.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy 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.

Leave a Reply

Your email address will not be published. Required fields are marked *

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

More from the Wire

  1. World desk4 min
    How to Spot an AI Voice Scam Before Sending MoneyDon’t rely on how a caller sounds. Pause, call back through a known number, and verify the emergency with another trusted person before sending money.
  2. Mountain View desk4 min
    Google’s SynthID Detector: How to Check AI-Generated Images, Video and AudioGoogle’s SynthID Detector looks for an embedded watermark in supported images, video and audio. Here is what its results do—and do not—show.
  3. Redmond desk20 min
    How to create a link to File or Folder in Windows 11Windows 11 gives you several ways to point to a file or folder without moving or duplicating it. You can create a desktop shortcut,…
Recommended PC Tool
Recommended PC Tool
Crashes, No Sound, or Screen Glitches?Free driver scan
PC Slower Than It Used to Be?Free scan - under a minute

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.