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.
The Tool Desk
Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →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.
#1 Best Overall
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.
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.
Rank #2
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
-
Save
calls.csvandmy_app.pyin 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. -
Activate the virtual environment if it is not already active.
-
From that directory, run
streamlit run my_app.py. -
Open
http://localhost:8501in a browser on the same computer. Streamlit documents this local server address and command in its getting-started guide. -
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
-
Missing input file: put
calls.csvbesidemy_app.py, or changeDATA_FILEto the intended path.Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy. -
Missing CSV columns: use the required headers
call_id,started_at,agent, andtranscript, or update the code consistently to your headers. -
Duplicate-ID warning: make each call ID unique. A review is keyed to that ID, so duplicates would make the relationship ambiguous.
-
A saved review seems gone: check that you are launching the app from the same project location and that the process can write to the directory containing
reviews.db. The database path is based on the script location in this example. -
Search text contains punctuation: the code uses literal, case-insensitive matching rather than interpreting the search as a regular expression.
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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.
Best Value
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.
Windows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallOutdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchQuick Recap
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.




