Fade0510/CallCenterSummarizationAgent
0
1import streamlit as st2import sys3import os4import json5from pathlib import Path6from dotenv import load_dotenv7 8# Load environment variables from .env file9load_dotenv()10 11sys.path.append(os.path.dirname(os.path.dirname(__file__)))12 13PROCESSED_DIR = "data/processed_results"14 15 16def apply_material_css():17 st.markdown("""18 <style>19 /* Material Design CSS Overrides */20 21 .stApp {22 font-family: 'Roboto', 'Inter', sans-serif;23 background-color: #121212;24 color: #FFFFFF;25 }26 27 /* Material Cards for metrics and sections */28 .material-card {29 background-color: #1E1E1E;30 border-radius: 8px;31 padding: 20px;32 box-shadow: 0 4px 6px rgba(0,0,0,0.3);33 margin-bottom: 20px;34 }35 36 h1, h2, h3, h4 {37 font-weight: 500;38 }39 40 /* Subtle styling for Streamlit columns to look like cards */41 [data-testid="column"] {42 background-color: #1E1E1E;43 border-radius: 8px;44 padding: 20px;45 box-shadow: 0 4px 6px rgba(0,0,0,0.3);46 }47 48 /* Expander (Transcript) - keep dark theme even on hover/focus */49 div[data-testid="stExpander"] details,50 div[data-testid="stExpander"] summary,51 div[data-testid="stExpander"] summary:hover,52 div[data-testid="stExpander"] summary:focus,53 div[data-testid="stExpander"] summary:active,54 div[data-testid="stExpander"] summary:focus-visible {55 background-color: #1E1E1E !important;56 color: #FFFFFF !important;57 }58 59 div[data-testid="stExpander"] details {60 border: 1px solid #333333 !important;61 border-radius: 8px !important;62 }63 64 /* Transcript text area */65 div[data-testid="stExpander"] textarea,66 div[data-testid="stExpander"] textarea:hover,67 div[data-testid="stExpander"] textarea:focus,68 div[data-testid="stExpander"] textarea:active {69 background-color: #121212 !important;70 color: #FFFFFF !important;71 border-color: #333333 !important;72 }73 74 </style>75 """, unsafe_allow_html=True)76 77 78def display_results(final_state):79 st.subheader("Workflow Results")80 81 metadata = final_state.get("metadata", {})82 if isinstance(metadata, dict) and metadata.get("intake_error"):83 st.error(f"CSV validation failed: {metadata['intake_error']}")84 st.info("This CSV was not accepted for processing. Please fix the headers and re-upload.")85 st.markdown("### Metadata")86 for k, v in metadata.items():87 st.markdown(f"- **{k.replace('_', ' ').title()}**: {v}")88 return89 90 st.markdown("### Transcript")91 transcript_text = final_state.get("clean_content") or final_state.get("content") or ""92 if transcript_text:93 with st.expander("View transcript", expanded=False):94 st.text_area("Transcript", transcript_text, height=220)95 else:96 st.write("No transcript available.")97 98 col1, col2 = st.columns(2)99 with col1:100 st.markdown("### Summary")101 st.write(final_state.get("summary", "No summary generated."))102 103 st.markdown("### Key Points")104 key_points = final_state.get("key_points", "No key points generated.")105 if isinstance(key_points, list):106 for point in key_points:107 st.markdown(f"- {point}")108 else:109 st.write(key_points)110 111 st.markdown("### Action Items")112 action_items = final_state.get("action_items", "No action items generated.")113 if isinstance(action_items, list):114 if action_items:115 for item in action_items:116 st.markdown(f"- {item}")117 else:118 st.write("No action items generated.")119 else:120 st.write(action_items)121 122 st.markdown("### Tags / Highlights")123 tags = final_state.get("tags") or []124 highlights = final_state.get("highlights") or []125 if tags:126 st.write("**Tags:** " + ", ".join([str(t) for t in tags]))127 else:128 st.write("**Tags:** None")129 if highlights:130 st.write("**Highlights:**")131 for h in highlights:132 st.markdown(f"- {h}")133 else:134 st.write("**Highlights:** None")135 136 with col2:137 st.markdown("### Scoring Rubric")138 quality_scores = final_state.get("quality_scores", {})139 if isinstance(quality_scores, dict) and quality_scores:140 import plotly.graph_objects as go141 for metric in ['tone', 'professionalism', 'structured_resolution']:142 if metric in quality_scores:143 try:144 val = float(quality_scores[metric])145 fig = go.Figure(go.Indicator(146 mode="gauge+number",147 value=val,148 title={'text': metric.replace('_', ' ').title(), 'font': {'size': 16, 'color': '#FFFFFF'}},149 gauge={150 'axis': {'range': [None, 10], 'tickwidth': 1, 'tickcolor': "#BB86FC"},151 'bar': {'color': "#BB86FC"},152 'bgcolor': "#1E1E1E",153 'borderwidth': 2,154 'bordercolor': "#333333",155 'steps': [156 {'range': [0, 4], 'color': '#cf6679'},157 {'range': [4, 7], 'color': '#ffb74d'},158 {'range': [7, 10], 'color': '#81c784'}],159 }160 ))161 # Adjust colors for dark theme162 fig.update_layout(163 height=180,164 margin=dict(l=20, r=20, t=40, b=20),165 paper_bgcolor='#1E1E1E',166 plot_bgcolor='#1E1E1E',167 font={'color': '#FFFFFF'}168 )169 st.plotly_chart(fig, width="stretch")170 except (ValueError, TypeError):171 st.write(f"**{metric.replace('_', ' ').title()}**: {quality_scores[metric]}")172 173 if "notes" in quality_scores:174 st.write("**Notes:**", quality_scores["notes"])175 176 if "rubric" in quality_scores and quality_scores["rubric"]:177 with st.expander("View scoring rubric", expanded=False):178 rubric_rows = [179 {180 "Dimension": "Tone",181 "0": "Hostile/arguing",182 "3": "Curt/tense",183 "5": "Neutral",184 "7": "Friendly/empathic",185 "10": "Consistently calm, respectful, de-escalating",186 },187 {188 "Dimension": "Professionalism",189 "0": "Rude/unprofessional",190 "3": "Unclear or dismissive",191 "5": "Acceptable",192 "7": "Clear, courteous, policy-aligned",193 "10": "Excellent clarity, appropriate boundaries, ownership",194 },195 {196 "Dimension": "Structured resolution",197 "0": "No attempt",198 "3": "Vague / no next steps",199 "5": "Partial (some questions/steps)",200 "7": "Clear diagnosis + next steps + confirmation",201 "10": "Fully structured (issue, actions, timelines, confirmation, closure)",202 },203 ]204 205 st.dataframe(206 rubric_rows,207 hide_index=True,208 use_container_width=True,209 )210 st.caption(211 "Notes must cite 1–3 specific behaviors from the transcript (avoid long quotes)."212 )213 else:214 st.write("No scoring rubric results generated.", quality_scores)215 216 st.markdown("### Metadata")217 if isinstance(metadata, dict) and metadata:218 for k, v in metadata.items():219 st.markdown(f"- **{k.replace('_', ' ').title()}**: {v}")220 else:221 st.write("No metadata available.")222 223 224def main():225 st.set_page_config(page_title="Call Center Data Analysis", layout="wide")226 apply_material_css()227 228 st.title("Call Center Data Analysis Dashboard")229 230 os.makedirs(PROCESSED_DIR, exist_ok=True)231 os.makedirs("tmp", exist_ok=True)232 233 if "view_mode" not in st.session_state:234 st.session_state.view_mode = "none"235 236 def set_upload_mode():237 st.session_state.view_mode = "upload"238 239 def set_dropdown_mode():240 st.session_state.view_mode = "dropdown"241 242 st.sidebar.header("Upload New File")243 uploaded_file = st.sidebar.file_uploader(244 "Upload a file",245 type=["mp3", "wav", "csv", "json"],246 on_change=set_upload_mode247 )248 249 st.sidebar.markdown("---")250 st.sidebar.header("Processed Files")251 252 # Get list of processed files253 processed_files = [f for f in os.listdir(PROCESSED_DIR) if f.endswith(".json")]254 processed_files.sort(reverse=True) # Show newest (or reverse alphabetical) first255 256 selected_file = None257 if processed_files:258 options = ["-- Select a file --"] + processed_files259 selected_dropdown = st.sidebar.selectbox(260 "View cached results:",261 options,262 format_func=lambda x: x.replace(".json", "") if x != "-- Select a file --" else x,263 on_change=set_dropdown_mode264 )265 if selected_dropdown != "-- Select a file --":266 selected_file = selected_dropdown267 else:268 st.sidebar.info("No files processed yet.")269 270 st.sidebar.markdown("---")271 st.sidebar.header("Notes")272 st.sidebar.markdown("""273 Sample data:274 - [Customer Call Center Dataset Analysis](https://www.kaggle.com/datasets/rafaqatkhan608/customer-call-center-dataset-analysis/code/data)275 - [E-commerce Customer Support English Audio](https://huggingface.co/datasets/HumynLabs/e-commerce-customersupport-english-audio/tree/main)276 """)277 278 # Prioritize based on view_mode279 if st.session_state.view_mode == "upload" and uploaded_file is not None:280 st.success(f"File '{uploaded_file.name}' uploaded successfully!")281 282 from src.workflow import build_workflow283 284 file_path = os.path.join("tmp", uploaded_file.name)285 with open(file_path, "wb") as f:286 f.write(uploaded_file.getbuffer())287 288 file_extension = uploaded_file.name.split('.')[-1].lower()289 290 # Check if already processed to avoid reprocessing on rerun if same file is in uploader291 cached_json_path = os.path.join(PROCESSED_DIR, f"{uploaded_file.name}.json")292 293 if os.path.exists(cached_json_path):294 st.info("Loading cached results for this file...")295 with open(cached_json_path, 'r') as f:296 final_state = json.load(f)297 display_results(final_state)298 else:299 st.write("Processing file through LangGraph Workflow...")300 with st.spinner("Agents are analyzing the data..."):301 workflow = build_workflow()302 initial_state = {303 "file_path": file_path,304 "file_type": file_extension305 }306 307 thread_id = Path(file_path).name308 final_state = workflow.invoke(309 initial_state, config={"configurable": {"thread_id": thread_id}}310 )311 312 if final_state.get("metadata", {}).get("intake_error"):313 display_results(final_state)314 else:315 # Cache the results316 with open(cached_json_path, 'w') as f:317 json.dump(final_state, f)318 st.rerun()319 320 if not final_state.get("metadata", {}).get("intake_error"):321 display_results(final_state)322 323 elif (324 st.session_state.view_mode == "dropdown" or st.session_state.view_mode == "none") and selected_file is not None:325 st.info(f"Loading cached results for '{selected_file.replace('.json', '')}'")326 cached_json_path = os.path.join(PROCESSED_DIR, selected_file)327 with open(cached_json_path, 'r') as f:328 final_state = json.load(f)329 display_results(final_state)330 331 elif st.session_state.view_mode != "dropdown" and uploaded_file is not None:332 st.success(f"File '{uploaded_file.name}' uploaded successfully!")333 334 from src.workflow import build_workflow335 336 file_path = os.path.join("tmp", uploaded_file.name)337 with open(file_path, "wb") as f:338 f.write(uploaded_file.getbuffer())339 340 file_extension = uploaded_file.name.split('.')[-1].lower()341 342 cached_json_path = os.path.join(PROCESSED_DIR, f"{uploaded_file.name}.json")343 344 if os.path.exists(cached_json_path):345 st.info("Loading cached results for this file...")346 with open(cached_json_path, 'r') as f:347 final_state = json.load(f)348 display_results(final_state)349 else:350 st.write("Processing file through LangGraph Workflow...")351 with st.spinner("Agents are analyzing the data..."):352 workflow = build_workflow()353 initial_state = {354 "file_path": file_path,355 "file_type": file_extension356 }357 thread_id = Path(file_path).name358 final_state = workflow.invoke(359 initial_state, config={"configurable": {"thread_id": thread_id}}360 )361 if final_state.get("metadata", {}).get("intake_error"):362 display_results(final_state)363 else:364 with open(cached_json_path, 'w') as f:365 json.dump(final_state, f)366 st.rerun()367 if not final_state.get("metadata", {}).get("intake_error"):368 display_results(final_state)369 370 else:371 st.info("Please upload a file or select a previously processed file.")372 373 374if __name__ == "__main__":375 main()376 