Files
BeyondCX_Insights/dashboard/exports.py
sujucu70 75e7b9da3d feat: Add Streamlit dashboard with Blueprint compliance (v2.1.0)
Dashboard Features:
- 8 navigation sections: Overview, Outcomes, Poor CX, FCR, Churn, Agent, Call Explorer, Export
- Beyond Brand Identity styling (colors #6D84E3, Outfit font)
- RCA Sankey diagram (Driver → Outcome → Churn Risk flow)
- Correlation heatmaps (driver co-occurrence, driver-outcome)
- Outcome Deep Dive (root causes, correlation, duration analysis)
- Export functionality (Excel, HTML, JSON)

Blueprint Compliance:
- FCR: 4 categories (Primera Llamada/Rellamada × Sin/Con Riesgo de Fuga)
- Churn: Binary view (Sin Riesgo de Fuga / En Riesgo de Fuga)
- Agent: Talento Para Replicar / Oportunidades de Mejora
- Fixed FCR rate calculation (only FIRST_CALL counts as success)

Technical:
- Streamlit + Plotly for interactive visualizations
- Light theme configuration (.streamlit/config.toml)
- Fixed Plotly colorbar titlefont deprecation

Documentation:
- Updated PROJECT_CONTEXT.md, TODO.md, CHANGELOG.md
- Added 4 new technical decisions (TD-014 to TD-017)
- Created TROUBLESHOOTING.md with 10 common issues

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 16:27:30 +01:00

467 lines
17 KiB
Python

"""
CXInsights Dashboard - Export Functions
Export insights to Excel, PDF, and other formats.
"""
import io
import json
from datetime import datetime
from pathlib import Path
import pandas as pd
import streamlit as st
from config import COLORS
def create_excel_export(summary: dict, analyses: list[dict], batch_id: str) -> io.BytesIO:
"""
Create comprehensive Excel export with multiple sheets.
Sheets:
- Executive Summary
- Call Details
- Poor CX Drivers
- FCR Analysis
- Churn Risk
- Agent Performance
"""
output = io.BytesIO()
with pd.ExcelWriter(output, engine='openpyxl') as writer:
# Sheet 1: Executive Summary
summary_data = {
"Metric": [
"Batch ID",
"Generated At",
"Total Calls Analyzed",
"Successful Analyses",
"Failed Analyses",
"Poor CX Drivers Found",
"Lost Sales Drivers Found",
],
"Value": [
batch_id,
summary.get("generated_at", "N/A"),
summary.get("summary", {}).get("total_calls", 0),
summary.get("summary", {}).get("successful_analyses", 0),
summary.get("summary", {}).get("failed_analyses", 0),
summary.get("poor_cx", {}).get("total_drivers_found", 0),
summary.get("lost_sales", {}).get("total_drivers_found", 0),
]
}
df_summary = pd.DataFrame(summary_data)
df_summary.to_excel(writer, sheet_name="Executive Summary", index=False)
# Sheet 2: Outcomes Distribution
outcomes = summary.get("outcomes", {})
if outcomes:
df_outcomes = pd.DataFrame([
{"Outcome": k, "Count": v, "Percentage": f"{v/sum(outcomes.values())*100:.1f}%"}
for k, v in sorted(outcomes.items(), key=lambda x: -x[1])
])
df_outcomes.to_excel(writer, sheet_name="Outcomes", index=False)
# Sheet 3: Call Details
call_data = []
for a in analyses:
call_data.append({
"Call ID": a.get("call_id", ""),
"Outcome": a.get("outcome", ""),
"FCR Status": a.get("fcr_status", ""),
"Churn Risk": a.get("churn_risk", ""),
"Agent Classification": a.get("agent_classification", ""),
"Poor CX Drivers": len(a.get("poor_cx_drivers", [])),
"FCR Failure Drivers": len(a.get("fcr_failure_drivers", [])),
"Churn Risk Drivers": len(a.get("churn_risk_drivers", [])),
"Duration (sec)": a.get("observed", {}).get("audio_duration_sec", ""),
"Total Turns": a.get("observed", {}).get("turn_metrics", {}).get("total_turns", ""),
})
df_calls = pd.DataFrame(call_data)
df_calls.to_excel(writer, sheet_name="Call Details", index=False)
# Sheet 4: Poor CX Drivers Detail
poor_cx_data = []
for a in analyses:
for d in a.get("poor_cx_drivers", []):
poor_cx_data.append({
"Call ID": a.get("call_id", ""),
"Driver Code": d.get("driver_code", ""),
"Confidence": f"{d.get('confidence', 0):.0%}",
"Origin": d.get("origin", ""),
"Reasoning": d.get("reasoning", ""),
"Corrective Action": d.get("corrective_action", ""),
"Evidence": "; ".join([e.get("text", "") for e in d.get("evidence_spans", [])]),
})
if poor_cx_data:
df_poor_cx = pd.DataFrame(poor_cx_data)
df_poor_cx.to_excel(writer, sheet_name="Poor CX Drivers", index=False)
# Sheet 5: FCR Failure Drivers
fcr_data = []
for a in analyses:
for d in a.get("fcr_failure_drivers", []):
fcr_data.append({
"Call ID": a.get("call_id", ""),
"Driver Code": d.get("driver_code", ""),
"Confidence": f"{d.get('confidence', 0):.0%}",
"Origin": d.get("origin", ""),
"Reasoning": d.get("reasoning", ""),
"Corrective Action": d.get("corrective_action", ""),
})
if fcr_data:
df_fcr = pd.DataFrame(fcr_data)
df_fcr.to_excel(writer, sheet_name="FCR Failures", index=False)
# Sheet 6: Churn Risk Drivers
churn_data = []
for a in analyses:
for d in a.get("churn_risk_drivers", []):
churn_data.append({
"Call ID": a.get("call_id", ""),
"Risk Level": a.get("churn_risk", ""),
"Driver Code": d.get("driver_code", ""),
"Confidence": f"{d.get('confidence', 0):.0%}",
"Reasoning": d.get("reasoning", ""),
"Corrective Action": d.get("corrective_action", ""),
})
if churn_data:
df_churn = pd.DataFrame(churn_data)
df_churn.to_excel(writer, sheet_name="Churn Risk", index=False)
# Sheet 7: Agent Performance
agent_data = []
for a in analyses:
positive = [s.get("skill_code", "") for s in a.get("agent_positive_skills", [])]
improvement = [s.get("skill_code", "") for s in a.get("agent_improvement_areas", [])]
agent_data.append({
"Call ID": a.get("call_id", ""),
"Classification": a.get("agent_classification", ""),
"Positive Skills": ", ".join(positive),
"Improvement Areas": ", ".join(improvement),
})
df_agent = pd.DataFrame(agent_data)
df_agent.to_excel(writer, sheet_name="Agent Performance", index=False)
# Sheet 8: Top Drivers Summary
top_drivers = []
for d in summary.get("poor_cx", {}).get("top_drivers", []):
top_drivers.append({
"Type": "Poor CX",
"Driver Code": d.get("driver_code", ""),
"Occurrences": d.get("occurrences", 0),
"Call Rate": f"{d.get('call_rate', 0)*100:.1f}%",
"Avg Confidence": f"{d.get('avg_confidence', 0):.0%}",
})
for d in summary.get("lost_sales", {}).get("top_drivers", []):
top_drivers.append({
"Type": "Lost Sales",
"Driver Code": d.get("driver_code", ""),
"Occurrences": d.get("occurrences", 0),
"Call Rate": f"{d.get('call_rate', 0)*100:.1f}%",
"Avg Confidence": f"{d.get('avg_confidence', 0):.0%}",
})
if top_drivers:
df_top = pd.DataFrame(top_drivers)
df_top.to_excel(writer, sheet_name="Top Drivers Summary", index=False)
output.seek(0)
return output
def create_executive_summary_html(summary: dict, analyses: list[dict], batch_id: str) -> str:
"""
Create HTML executive summary report for PDF export.
"""
total_calls = summary.get("summary", {}).get("total_calls", 0)
# Calculate metrics
poor_cx_calls = sum(1 for a in analyses if len(a.get("poor_cx_drivers", [])) > 0)
poor_cx_rate = (poor_cx_calls / total_calls * 100) if total_calls > 0 else 0
high_churn = sum(1 for a in analyses if a.get("churn_risk") in ["HIGH", "AT_RISK"])
churn_rate = (high_churn / total_calls * 100) if total_calls > 0 else 0
# FCR rate - Per blueprint: Primera Llamada = FCR success
fcr_success = sum(1 for a in analyses if a.get("fcr_status") == "FIRST_CALL")
fcr_rate = (fcr_success / total_calls * 100) if total_calls > 0 else 0
# Top drivers
top_drivers = summary.get("poor_cx", {}).get("top_drivers", [])[:5]
# Outcomes
outcomes = summary.get("outcomes", {})
html = f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>CXInsights Executive Report - {batch_id}</title>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;700&display=swap" rel="stylesheet">
<style>
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{
font-family: 'Outfit', sans-serif;
color: #000000;
background: #FFFFFF;
padding: 40px;
max-width: 900px;
margin: 0 auto;
}}
.header {{
border-bottom: 3px solid #6D84E3;
padding-bottom: 20px;
margin-bottom: 30px;
}}
.header h1 {{
font-size: 32px;
font-weight: 700;
color: #000000;
}}
.header .subtitle {{
color: #B1B1B0;
font-size: 14px;
margin-top: 8px;
}}
.brand {{
font-size: 18px;
font-weight: 700;
margin-bottom: 8px;
}}
.brand sup {{
color: #6D84E3;
font-size: 12px;
}}
.kpi-grid {{
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin-bottom: 40px;
}}
.kpi-card {{
background: #F8F8F8;
padding: 20px;
border-radius: 8px;
text-align: center;
}}
.kpi-value {{
font-size: 36px;
font-weight: 700;
color: #000000;
}}
.kpi-label {{
font-size: 12px;
color: #B1B1B0;
margin-top: 8px;
}}
.section {{ margin-bottom: 40px; }}
.section h2 {{
font-size: 21px;
font-weight: 700;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 2px solid #6D84E3;
}}
table {{
width: 100%;
border-collapse: collapse;
margin-top: 16px;
}}
th {{
background: #F8F8F8;
padding: 12px;
text-align: left;
font-weight: 700;
border-bottom: 2px solid #6D84E3;
}}
td {{
padding: 12px;
border-bottom: 1px solid #E4E4E4;
}}
tr:nth-child(even) {{ background: #FAFAFA; }}
.insight {{
background: #F8F8F8;
border-left: 4px solid #6D84E3;
padding: 16px;
margin: 16px 0;
}}
.insight strong {{ color: #6D84E3; }}
.footer {{
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid #E4E4E4;
font-size: 12px;
color: #B1B1B0;
}}
@media print {{
body {{ padding: 20px; }}
.kpi-grid {{ grid-template-columns: repeat(2, 1fr); }}
}}
</style>
</head>
<body>
<div class="header">
<div class="brand">beyond<sup>cx</sup></div>
<h1>CXInsights Executive Report</h1>
<div class="subtitle">
Batch: {batch_id} |
Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')} |
Calls Analyzed: {total_calls}
</div>
</div>
<div class="kpi-grid">
<div class="kpi-card">
<div class="kpi-value">{total_calls}</div>
<div class="kpi-label">Total Calls</div>
</div>
<div class="kpi-card">
<div class="kpi-value">{poor_cx_rate:.1f}%</div>
<div class="kpi-label">Poor CX Rate</div>
</div>
<div class="kpi-card">
<div class="kpi-value">{fcr_rate:.1f}%</div>
<div class="kpi-label">FCR Rate</div>
</div>
<div class="kpi-card">
<div class="kpi-value">{churn_rate:.1f}%</div>
<div class="kpi-label">Churn Risk</div>
</div>
</div>
<div class="section">
<h2>Key Insights</h2>
{"".join([f'<div class="insight"><strong>{d.get("driver_code", "")}</strong> detected in {d.get("occurrences", 0)} calls ({d.get("call_rate", 0)*100:.0f}% of total)</div>' for d in top_drivers[:3]]) if top_drivers else '<p>No critical drivers detected.</p>'}
</div>
<div class="section">
<h2>Outcome Distribution</h2>
<table>
<thead>
<tr>
<th>Outcome</th>
<th>Count</th>
<th>Percentage</th>
</tr>
</thead>
<tbody>
{"".join([f'<tr><td>{k}</td><td>{v}</td><td>{v/sum(outcomes.values())*100:.1f}%</td></tr>' for k, v in sorted(outcomes.items(), key=lambda x: -x[1])]) if outcomes else '<tr><td colspan="3">No data</td></tr>'}
</tbody>
</table>
</div>
<div class="section">
<h2>Top Poor CX Drivers</h2>
<table>
<thead>
<tr>
<th>Driver</th>
<th>Occurrences</th>
<th>Call Rate</th>
<th>Confidence</th>
</tr>
</thead>
<tbody>
{"".join([f'<tr><td>{d.get("driver_code", "")}</td><td>{d.get("occurrences", 0)}</td><td>{d.get("call_rate", 0)*100:.1f}%</td><td>{d.get("avg_confidence", 0):.0%}</td></tr>' for d in top_drivers]) if top_drivers else '<tr><td colspan="4">No drivers detected</td></tr>'}
</tbody>
</table>
</div>
<div class="footer">
<p>Generated by Beyond CXInsights | {datetime.now().strftime('%Y-%m-%d %H:%M')}</p>
<p>This report contains AI-generated insights. Please review with domain expertise.</p>
</div>
</body>
</html>
"""
return html
def create_json_export(summary: dict, analyses: list[dict], batch_id: str) -> str:
"""Create JSON export of all data."""
export_data = {
"batch_id": batch_id,
"exported_at": datetime.now().isoformat(),
"summary": summary,
"analyses": analyses,
}
return json.dumps(export_data, indent=2, ensure_ascii=False)
def render_export_section(summary: dict, analyses: list[dict], batch_id: str):
"""Render export options in the dashboard."""
st.markdown("### Export Options")
col1, col2, col3 = st.columns(3)
with col1:
st.markdown("#### Excel Report")
st.caption("Complete analysis with multiple sheets")
excel_data = create_excel_export(summary, analyses, batch_id)
st.download_button(
label="Download Excel",
data=excel_data,
file_name=f"cxinsights_{batch_id}_{datetime.now().strftime('%Y%m%d')}.xlsx",
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
use_container_width=True,
)
with col2:
st.markdown("#### Executive Summary")
st.caption("HTML report (print to PDF)")
html_data = create_executive_summary_html(summary, analyses, batch_id)
st.download_button(
label="Download HTML",
data=html_data,
file_name=f"cxinsights_{batch_id}_executive_{datetime.now().strftime('%Y%m%d')}.html",
mime="text/html",
use_container_width=True,
)
with col3:
st.markdown("#### Raw Data")
st.caption("JSON format for integration")
json_data = create_json_export(summary, analyses, batch_id)
st.download_button(
label="Download JSON",
data=json_data,
file_name=f"cxinsights_{batch_id}_{datetime.now().strftime('%Y%m%d')}.json",
mime="application/json",
use_container_width=True,
)
st.markdown("---")
# Quick stats
st.markdown("#### Export Preview")
col1, col2 = st.columns(2)
with col1:
st.markdown("**Excel sheets included:**")
st.markdown("""
- Executive Summary
- Outcomes Distribution
- Call Details
- Poor CX Drivers
- FCR Failures
- Churn Risk
- Agent Performance
- Top Drivers Summary
""")
with col2:
st.markdown("**Data summary:**")
st.markdown(f"""
- **Calls:** {len(analyses)}
- **Poor CX instances:** {sum(len(a.get('poor_cx_drivers', [])) for a in analyses)}
- **FCR failures:** {sum(len(a.get('fcr_failure_drivers', [])) for a in analyses)}
- **Churn risk drivers:** {sum(len(a.get('churn_risk_drivers', [])) for a in analyses)}
""")