File size: 14,688 Bytes
75b0ef8 b6ed6f7 f41ca4f b6ed6f7 f41ca4f b6ed6f7 f41ca4f b6ed6f7 f41ca4f b6ed6f7 f41ca4f b6ed6f7 f41ca4f 0aa8e87 b6ed6f7 f41ca4f b6ed6f7 f41ca4f 0aa8e87 b6ed6f7 f41ca4f b6ed6f7 f41ca4f 18ddfa2 0aa8e87 b6ed6f7 18ddfa2 b6ed6f7 f41ca4f b6ed6f7 0aa8e87 b6ed6f7 f41ca4f b6ed6f7 18ddfa2 b6ed6f7 18ddfa2 f41ca4f b6ed6f7 f41ca4f b6ed6f7 f41ca4f b6ed6f7 f41ca4f b6ed6f7 f41ca4f b6ed6f7 f41ca4f 18ddfa2 b6ed6f7 f41ca4f b6ed6f7 f41ca4f b6ed6f7 f41ca4f b6ed6f7 f41ca4f b6ed6f7 2a1f84b b6ed6f7 f41ca4f b6ed6f7 f41ca4f b6ed6f7 f41ca4f b6ed6f7 f41ca4f b6ed6f7 f41ca4f b6ed6f7 f41ca4f 0aa8e87 f41ca4f 0aa8e87 f41ca4f 0aa8e87 f41ca4f b6ed6f7 f41ca4f b6ed6f7 f41ca4f b6ed6f7 f41ca4f b6ed6f7 f41ca4f b6ed6f7 f41ca4f b6ed6f7 f41ca4f b6ed6f7 0aa8e87 f41ca4f b6ed6f7 f41ca4f b6ed6f7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 | 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()
|