CallCenterSummarizationAgent / src /streamlit_app.py
Fade0510's picture
Add Scoring Rubric
18ddfa2
Raw
History Blame Contribute Delete
14.7 kB
import streamlit as st
import sys
import os
import json
from pathlib import Path
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
PROCESSED_DIR = "data/processed_results"
def apply_material_css():
st.markdown("""
<style>
/* Material Design CSS Overrides */
.stApp {
font-family: 'Roboto', 'Inter', sans-serif;
background-color: #121212;
color: #FFFFFF;
}
/* Material Cards for metrics and sections */
.material-card {
background-color: #1E1E1E;
border-radius: 8px;
padding: 20px;
box-shadow: 0 4px 6px rgba(0,0,0,0.3);
margin-bottom: 20px;
}
h1, h2, h3, h4 {
font-weight: 500;
}
/* Subtle styling for Streamlit columns to look like cards */
[data-testid="column"] {
background-color: #1E1E1E;
border-radius: 8px;
padding: 20px;
box-shadow: 0 4px 6px rgba(0,0,0,0.3);
}
/* Expander (Transcript) - keep dark theme even on hover/focus */
div[data-testid="stExpander"] details,
div[data-testid="stExpander"] summary,
div[data-testid="stExpander"] summary:hover,
div[data-testid="stExpander"] summary:focus,
div[data-testid="stExpander"] summary:active,
div[data-testid="stExpander"] summary:focus-visible {
background-color: #1E1E1E !important;
color: #FFFFFF !important;
}
div[data-testid="stExpander"] details {
border: 1px solid #333333 !important;
border-radius: 8px !important;
}
/* Transcript text area */
div[data-testid="stExpander"] textarea,
div[data-testid="stExpander"] textarea:hover,
div[data-testid="stExpander"] textarea:focus,
div[data-testid="stExpander"] textarea:active {
background-color: #121212 !important;
color: #FFFFFF !important;
border-color: #333333 !important;
}
</style>
""", unsafe_allow_html=True)
def display_results(final_state):
st.subheader("Workflow Results")
metadata = final_state.get("metadata", {})
if isinstance(metadata, dict) and metadata.get("intake_error"):
st.error(f"CSV validation failed: {metadata['intake_error']}")
st.info("This CSV was not accepted for processing. Please fix the headers and re-upload.")
st.markdown("### Metadata")
for k, v in metadata.items():
st.markdown(f"- **{k.replace('_', ' ').title()}**: {v}")
return
st.markdown("### Transcript")
transcript_text = final_state.get("clean_content") or final_state.get("content") or ""
if transcript_text:
with st.expander("View transcript", expanded=False):
st.text_area("Transcript", transcript_text, height=220)
else:
st.write("No transcript available.")
col1, col2 = st.columns(2)
with col1:
st.markdown("### Summary")
st.write(final_state.get("summary", "No summary generated."))
st.markdown("### Key Points")
key_points = final_state.get("key_points", "No key points generated.")
if isinstance(key_points, list):
for point in key_points:
st.markdown(f"- {point}")
else:
st.write(key_points)
st.markdown("### Action Items")
action_items = final_state.get("action_items", "No action items generated.")
if isinstance(action_items, list):
if action_items:
for item in action_items:
st.markdown(f"- {item}")
else:
st.write("No action items generated.")
else:
st.write(action_items)
st.markdown("### Tags / Highlights")
tags = final_state.get("tags") or []
highlights = final_state.get("highlights") or []
if tags:
st.write("**Tags:** " + ", ".join([str(t) for t in tags]))
else:
st.write("**Tags:** None")
if highlights:
st.write("**Highlights:**")
for h in highlights:
st.markdown(f"- {h}")
else:
st.write("**Highlights:** None")
with col2:
st.markdown("### Scoring Rubric")
quality_scores = final_state.get("quality_scores", {})
if isinstance(quality_scores, dict) and quality_scores:
import plotly.graph_objects as go
for metric in ['tone', 'professionalism', 'structured_resolution']:
if metric in quality_scores:
try:
val = float(quality_scores[metric])
fig = go.Figure(go.Indicator(
mode="gauge+number",
value=val,
title={'text': metric.replace('_', ' ').title(), 'font': {'size': 16, 'color': '#FFFFFF'}},
gauge={
'axis': {'range': [None, 10], 'tickwidth': 1, 'tickcolor': "#BB86FC"},
'bar': {'color': "#BB86FC"},
'bgcolor': "#1E1E1E",
'borderwidth': 2,
'bordercolor': "#333333",
'steps': [
{'range': [0, 4], 'color': '#cf6679'},
{'range': [4, 7], 'color': '#ffb74d'},
{'range': [7, 10], 'color': '#81c784'}],
}
))
# Adjust colors for dark theme
fig.update_layout(
height=180,
margin=dict(l=20, r=20, t=40, b=20),
paper_bgcolor='#1E1E1E',
plot_bgcolor='#1E1E1E',
font={'color': '#FFFFFF'}
)
st.plotly_chart(fig, width="stretch")
except (ValueError, TypeError):
st.write(f"**{metric.replace('_', ' ').title()}**: {quality_scores[metric]}")
if "notes" in quality_scores:
st.write("**Notes:**", quality_scores["notes"])
if "rubric" in quality_scores and quality_scores["rubric"]:
with st.expander("View scoring rubric", expanded=False):
rubric_rows = [
{
"Dimension": "Tone",
"0": "Hostile/arguing",
"3": "Curt/tense",
"5": "Neutral",
"7": "Friendly/empathic",
"10": "Consistently calm, respectful, de-escalating",
},
{
"Dimension": "Professionalism",
"0": "Rude/unprofessional",
"3": "Unclear or dismissive",
"5": "Acceptable",
"7": "Clear, courteous, policy-aligned",
"10": "Excellent clarity, appropriate boundaries, ownership",
},
{
"Dimension": "Structured resolution",
"0": "No attempt",
"3": "Vague / no next steps",
"5": "Partial (some questions/steps)",
"7": "Clear diagnosis + next steps + confirmation",
"10": "Fully structured (issue, actions, timelines, confirmation, closure)",
},
]
st.dataframe(
rubric_rows,
hide_index=True,
use_container_width=True,
)
st.caption(
"Notes must cite 1–3 specific behaviors from the transcript (avoid long quotes)."
)
else:
st.write("No scoring rubric results generated.", quality_scores)
st.markdown("### Metadata")
if isinstance(metadata, dict) and metadata:
for k, v in metadata.items():
st.markdown(f"- **{k.replace('_', ' ').title()}**: {v}")
else:
st.write("No metadata available.")
def main():
st.set_page_config(page_title="Call Center Data Analysis", layout="wide")
apply_material_css()
st.title("Call Center Data Analysis Dashboard")
os.makedirs(PROCESSED_DIR, exist_ok=True)
os.makedirs("tmp", exist_ok=True)
if "view_mode" not in st.session_state:
st.session_state.view_mode = "none"
def set_upload_mode():
st.session_state.view_mode = "upload"
def set_dropdown_mode():
st.session_state.view_mode = "dropdown"
st.sidebar.header("Upload New File")
uploaded_file = st.sidebar.file_uploader(
"Upload a file",
type=["mp3", "wav", "csv", "json"],
on_change=set_upload_mode
)
st.sidebar.markdown("---")
st.sidebar.header("Processed Files")
# Get list of processed files
processed_files = [f for f in os.listdir(PROCESSED_DIR) if f.endswith(".json")]
processed_files.sort(reverse=True) # Show newest (or reverse alphabetical) first
selected_file = None
if processed_files:
options = ["-- Select a file --"] + processed_files
selected_dropdown = st.sidebar.selectbox(
"View cached results:",
options,
format_func=lambda x: x.replace(".json", "") if x != "-- Select a file --" else x,
on_change=set_dropdown_mode
)
if selected_dropdown != "-- Select a file --":
selected_file = selected_dropdown
else:
st.sidebar.info("No files processed yet.")
st.sidebar.markdown("---")
st.sidebar.header("Notes")
st.sidebar.markdown("""
Sample data:
- [Customer Call Center Dataset Analysis](https://www.kaggle.com/datasets/rafaqatkhan608/customer-call-center-dataset-analysis/code/data)
- [E-commerce Customer Support English Audio](https://huggingface.co/datasets/HumynLabs/e-commerce-customersupport-english-audio/tree/main)
""")
# Prioritize based on view_mode
if st.session_state.view_mode == "upload" and uploaded_file is not None:
st.success(f"File '{uploaded_file.name}' uploaded successfully!")
from src.workflow import build_workflow
file_path = os.path.join("tmp", uploaded_file.name)
with open(file_path, "wb") as f:
f.write(uploaded_file.getbuffer())
file_extension = uploaded_file.name.split('.')[-1].lower()
# Check if already processed to avoid reprocessing on rerun if same file is in uploader
cached_json_path = os.path.join(PROCESSED_DIR, f"{uploaded_file.name}.json")
if os.path.exists(cached_json_path):
st.info("Loading cached results for this file...")
with open(cached_json_path, 'r') as f:
final_state = json.load(f)
display_results(final_state)
else:
st.write("Processing file through LangGraph Workflow...")
with st.spinner("Agents are analyzing the data..."):
workflow = build_workflow()
initial_state = {
"file_path": file_path,
"file_type": file_extension
}
thread_id = Path(file_path).name
final_state = workflow.invoke(
initial_state, config={"configurable": {"thread_id": thread_id}}
)
if final_state.get("metadata", {}).get("intake_error"):
display_results(final_state)
else:
# Cache the results
with open(cached_json_path, 'w') as f:
json.dump(final_state, f)
st.rerun()
if not final_state.get("metadata", {}).get("intake_error"):
display_results(final_state)
elif (
st.session_state.view_mode == "dropdown" or st.session_state.view_mode == "none") and selected_file is not None:
st.info(f"Loading cached results for '{selected_file.replace('.json', '')}'")
cached_json_path = os.path.join(PROCESSED_DIR, selected_file)
with open(cached_json_path, 'r') as f:
final_state = json.load(f)
display_results(final_state)
elif st.session_state.view_mode != "dropdown" and uploaded_file is not None:
st.success(f"File '{uploaded_file.name}' uploaded successfully!")
from src.workflow import build_workflow
file_path = os.path.join("tmp", uploaded_file.name)
with open(file_path, "wb") as f:
f.write(uploaded_file.getbuffer())
file_extension = uploaded_file.name.split('.')[-1].lower()
cached_json_path = os.path.join(PROCESSED_DIR, f"{uploaded_file.name}.json")
if os.path.exists(cached_json_path):
st.info("Loading cached results for this file...")
with open(cached_json_path, 'r') as f:
final_state = json.load(f)
display_results(final_state)
else:
st.write("Processing file through LangGraph Workflow...")
with st.spinner("Agents are analyzing the data..."):
workflow = build_workflow()
initial_state = {
"file_path": file_path,
"file_type": file_extension
}
thread_id = Path(file_path).name
final_state = workflow.invoke(
initial_state, config={"configurable": {"thread_id": thread_id}}
)
if final_state.get("metadata", {}).get("intake_error"):
display_results(final_state)
else:
with open(cached_json_path, 'w') as f:
json.dump(final_state, f)
st.rerun()
if not final_state.get("metadata", {}).get("intake_error"):
display_results(final_state)
else:
st.info("Please upload a file or select a previously processed file.")
if __name__ == "__main__":
main()