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>
25 KiB
CXInsights - Estructura del Proyecto
Árbol de Carpetas Completo
cxinsights/
│
├── 📁 data/ # Datos (ignorado en git excepto .gitkeep)
│ ├── raw/ # Input original
│ │ ├── audio/ # Archivos de audio (.mp3, .wav)
│ │ │ └── batch_2024_01/
│ │ │ ├── call_001.mp3
│ │ │ └── ...
│ │ └── metadata/ # CSV con metadatos opcionales
│ │ └── calls_metadata.csv
│ │
│ ├── transcripts/ # Output de STT
│ │ └── batch_2024_01/
│ │ ├── raw/ # Transcripciones originales del STT
│ │ │ └── call_001.json
│ │ └── compressed/ # Transcripciones reducidas para LLM
│ │ └── call_001.json
│ │
│ ├── features/ # Output de extracción de features (OBSERVED)
│ │ └── batch_2024_01/
│ │ └── call_001_features.json
│ │
│ ├── processed/ # Output de LLM (Labels con INFERRED)
│ │ └── batch_2024_01/
│ │ └── call_001_labels.json
│ │
│ ├── outputs/ # Output final
│ │ └── batch_2024_01/
│ │ ├── aggregated_stats.json
│ │ ├── call_matrix.csv
│ │ ├── rca_lost_sales.json
│ │ ├── rca_poor_cx.json
│ │ ├── emergent_drivers_review.json
│ │ ├── executive_summary.pdf
│ │ ├── full_analysis.xlsx
│ │ └── figures/
│ │ ├── rca_tree_lost_sales.png
│ │ └── rca_tree_poor_cx.png
│ │
│ ├── .checkpoints/ # Estado del pipeline para resume
│ │ ├── transcription_state.json
│ │ ├── features_state.json
│ │ ├── inference_state.json
│ │ └── pipeline_state.json
│ │
│ └── logs/ # Logs de ejecución
│ └── pipeline_2024_01_15.log
│
├── 📁 src/ # Código fuente
│ ├── __init__.py
│ │
│ ├── 📁 transcription/ # Module 1: STT (SOLO transcripción)
│ │ ├── __init__.py
│ │ ├── base.py # Interface abstracta Transcriber
│ │ ├── assemblyai_client.py # Implementación AssemblyAI
│ │ ├── whisper_client.py # Implementación Whisper (futuro)
│ │ ├── batch_processor.py # Procesamiento paralelo
│ │ ├── compressor.py # SOLO reducción de texto para LLM
│ │ └── models.py # Pydantic models: TranscriptContract
│ │
│ ├── 📁 features/ # Module 2: Extracción OBSERVED
│ │ ├── __init__.py
│ │ ├── turn_metrics.py # talk ratio, interruptions, silence duration
│ │ ├── event_detector.py # HOLD, TRANSFER, SILENCE events
│ │ └── models.py # Pydantic models: ObservedFeatures, Event
│ │
│ ├── 📁 inference/ # Module 3: LLM Analysis (INFERRED)
│ │ ├── __init__.py
│ │ ├── client.py # OpenAI/Anthropic client wrapper
│ │ ├── prompt_manager.py # Carga y renderiza prompts versionados
│ │ ├── analyzer.py # Análisis por llamada → CallLabels
│ │ ├── batch_analyzer.py # Procesamiento en lote con rate limiting
│ │ ├── rca_synthesizer.py # (opcional) Síntesis narrativa del RCA vía LLM
│ │ └── models.py # CallLabels, InferredData, EvidenceSpan
│ │
│ ├── 📁 validation/ # Module 4: Quality Gate
│ │ ├── __init__.py
│ │ ├── validator.py # Validación de evidence_spans, taxonomy, etc.
│ │ ├── schema_checker.py # Verificación de schema_version
│ │ └── models.py # ValidationResult, ValidationError
│ │
│ ├── 📁 aggregation/ # Module 5-6: Stats + RCA (DETERMINÍSTICO)
│ │ ├── __init__.py
│ │ ├── stats_engine.py # Cálculos estadísticos (pandas + DuckDB)
│ │ ├── rca_builder.py # Construcción DETERMINÍSTICA del árbol RCA
│ │ ├── emergent_collector.py # Recolección de OTHER_EMERGENT para revisión
│ │ ├── correlations.py # Análisis de correlaciones observed↔inferred
│ │ └── models.py # AggregatedStats, RCATree, RCANode
│ │
│ ├── 📁 visualization/ # Module 7: Reports (SOLO presentación)
│ │ ├── __init__.py
│ │ ├── dashboard.py # Streamlit app
│ │ ├── charts.py # Generación de gráficos (plotly/matplotlib)
│ │ ├── tree_renderer.py # Visualización de árboles RCA como PNG/SVG
│ │ ├── pdf_report.py # Generación PDF ejecutivo
│ │ └── excel_export.py # Export a Excel con drill-down
│ │
│ ├── 📁 pipeline/ # Orquestación
│ │ ├── __init__.py
│ │ ├── orchestrator.py # Pipeline principal
│ │ ├── stages.py # Definición de stages
│ │ ├── checkpoint.py # Gestión de checkpoints
│ │ └── cli.py # Interfaz de línea de comandos
│ │
│ └── 📁 utils/ # Utilidades compartidas
│ ├── __init__.py
│ ├── file_io.py # Lectura/escritura de archivos
│ ├── logging_config.py # Setup de logging
│ └── validators.py # Validación de archivos de audio
│
├── 📁 config/ # Configuración
│ ├── rca_taxonomy.yaml # Taxonomía cerrada de drivers (versionada)
│ ├── settings.yaml # Config general (no secrets)
│ │
│ └── 📁 prompts/ # Templates de prompts LLM (versionados)
│ ├── versions.yaml # Registry de versiones activas
│ ├── call_analysis/
│ │ └── v1.2/
│ │ ├── system.txt
│ │ ├── user.txt
│ │ └── schema.json
│ └── rca_synthesis/
│ └── v1.0/
│ ├── system.txt
│ └── user.txt
│
├── 📁 tests/ # Tests
│ ├── __init__.py
│ ├── conftest.py # Fixtures compartidas
│ │
│ ├── 📁 fixtures/ # Datos de prueba
│ │ ├── sample_audio/
│ │ │ └── test_call.mp3
│ │ ├── sample_transcripts/
│ │ │ ├── raw/
│ │ │ └── compressed/
│ │ ├── sample_features/
│ │ └── expected_outputs/
│ │
│ ├── 📁 unit/ # Tests unitarios
│ │ ├── test_transcription.py
│ │ ├── test_features.py
│ │ ├── test_inference.py
│ │ ├── test_validation.py
│ │ ├── test_aggregation.py
│ │ └── test_visualization.py
│ │
│ └── 📁 integration/ # Tests de integración
│ └── test_pipeline.py
│
├── 📁 notebooks/ # Jupyter notebooks para EDA
│ ├── 01_eda_transcripts.ipynb
│ ├── 02_feature_exploration.ipynb
│ ├── 03_prompt_testing.ipynb
│ ├── 04_aggregation_validation.ipynb
│ └── 05_visualization_prototypes.ipynb
│
├── 📁 scripts/ # Scripts auxiliares
│ ├── estimate_costs.py # Estimador de costes antes de ejecutar
│ ├── validate_audio.py # Validar archivos de audio
│ └── sample_calls.py # Extraer muestra para testing
│
├── 📁 docs/ # Documentación
│ ├── ARCHITECTURE.md
│ ├── TECH_STACK.md
│ ├── PROJECT_STRUCTURE.md # Este documento
│ ├── DEPLOYMENT.md
│ └── PROMPTS.md # Documentación de prompts
│
├── .env.example # Template de variables de entorno
├── .gitignore
├── pyproject.toml # Dependencias y metadata
├── Makefile # Comandos útiles
└── README.md # Documentación principal
Responsabilidades por Módulo
📁 src/transcription/
Propósito: Convertir audio a texto con diarización. SOLO STT, sin analítica.
| Archivo | Responsabilidad |
|---|---|
base.py |
Interface abstracta Transcriber. Define contrato de salida. |
assemblyai_client.py |
Implementación AssemblyAI. Maneja auth, upload, polling. |
whisper_client.py |
Implementación Whisper local (futuro). |
batch_processor.py |
Procesa N archivos en paralelo. Gestiona concurrencia. |
compressor.py |
SOLO reducción de texto: quita muletillas, normaliza, acorta para LLM. NO extrae features. |
models.py |
TranscriptContract, Utterance, Speaker - schemas Pydantic. |
Interfaces principales:
class Transcriber(ABC):
"""Interface abstracta - permite cambiar proveedor STT sin refactor."""
async def transcribe(self, audio_path: Path) -> TranscriptContract
async def transcribe_batch(self, paths: list[Path]) -> list[TranscriptContract]
class TranscriptCompressor:
"""SOLO reduce texto. NO calcula métricas ni detecta eventos."""
def compress(self, transcript: TranscriptContract) -> CompressedTranscript
Output:
data/transcripts/raw/{call_id}.json→ Transcripción original del STTdata/transcripts/compressed/{call_id}.json→ Texto reducido para LLM
📁 src/features/
Propósito: Extracción determinística de métricas y eventos desde transcripts. 100% OBSERVED.
| Archivo | Responsabilidad |
|---|---|
turn_metrics.py |
Calcula: talk_ratio, interruption_count, silence_total_seconds, avg_turn_duration. |
event_detector.py |
Detecta eventos observables: HOLD_START, HOLD_END, TRANSFER, SILENCE, CROSSTALK. |
models.py |
ObservedFeatures, ObservedEvent, TurnMetrics. |
Interfaces principales:
class TurnMetricsExtractor:
"""Calcula métricas de turno desde utterances."""
def extract(self, transcript: TranscriptContract) -> TurnMetrics
class EventDetector:
"""Detecta eventos observables (silencios, holds, transfers)."""
def detect(self, transcript: TranscriptContract) -> list[ObservedEvent]
Output:
data/features/{call_id}_features.json→ Métricas y eventos OBSERVED
Nota: Este módulo NO usa LLM. Todo es cálculo determinístico sobre el transcript.
📁 src/inference/
Propósito: Analizar transcripciones con LLM para extraer datos INFERRED.
| Archivo | Responsabilidad |
|---|---|
client.py |
Wrapper sobre OpenAI/Anthropic SDK. Maneja retries, rate limiting. |
prompt_manager.py |
Carga templates versionados, renderiza con variables, valida schema. |
analyzer.py |
Análisis de una llamada → CallLabels con separación observed/inferred. |
batch_analyzer.py |
Procesa N llamadas con rate limiting y checkpoints. |
rca_synthesizer.py |
(Opcional) Síntesis narrativa del RCA tree vía LLM. NO construye el árbol. |
models.py |
CallLabels, InferredData, EvidenceSpan, JourneyEvent. |
Interfaces principales:
class CallAnalyzer:
"""Genera labels INFERRED con evidence_spans obligatorias."""
async def analyze(self, transcript: CompressedTranscript, features: ObservedFeatures) -> CallLabels
class RCASynthesizer:
"""(Opcional) Genera narrativa ejecutiva sobre RCA tree ya construido."""
async def synthesize_narrative(self, rca_tree: RCATree) -> str
Output:
data/processed/{call_id}_labels.json→ Labels con observed + inferred
📁 src/validation/
Propósito: Quality gate antes de agregación. Rechaza datos inválidos.
| Archivo | Responsabilidad |
|---|---|
validator.py |
Valida: evidence_spans presente, rca_code en taxonomía, confidence > umbral. |
schema_checker.py |
Verifica que schema_version y prompt_version coinciden con esperados. |
models.py |
ValidationResult, ValidationError. |
Interfaces principales:
class CallLabelsValidator:
"""Valida CallLabels antes de agregación."""
def validate(self, labels: CallLabels) -> ValidationResult
# Reglas:
# - Driver sin evidence_spans → RECHAZADO
# - rca_code no en taxonomía → marca como OTHER_EMERGENT o ERROR
# - schema_version mismatch → ERROR
📁 src/aggregation/
Propósito: Consolidar labels validados en estadísticas y RCA trees. DETERMINÍSTICO, no usa LLM.
| Archivo | Responsabilidad |
|---|---|
stats_engine.py |
Cálculos: distribuciones, percentiles, cross-tabs. Usa pandas + DuckDB. |
rca_builder.py |
Construcción DETERMINÍSTICA del árbol RCA a partir de stats y taxonomía. NO usa LLM. |
emergent_collector.py |
Recolecta OTHER_EMERGENT para revisión manual y posible promoción a taxonomía. |
correlations.py |
Análisis de correlaciones entre observed_features e inferred_outcomes. |
models.py |
AggregatedStats, RCATree, RCANode, Correlation. |
Interfaces principales:
class StatsEngine:
"""Agrega labels validados en estadísticas."""
def aggregate(self, labels: list[CallLabels]) -> AggregatedStats
class RCABuilder:
"""Construye árbol RCA de forma DETERMINÍSTICA (conteo + jerarquía de taxonomía)."""
def build_lost_sales_tree(self, stats: AggregatedStats, taxonomy: RCATaxonomy) -> RCATree
def build_poor_cx_tree(self, stats: AggregatedStats, taxonomy: RCATaxonomy) -> RCATree
class EmergentCollector:
"""Recolecta OTHER_EMERGENT para revisión humana."""
def collect(self, labels: list[CallLabels]) -> EmergentDriversReport
Nota sobre RCA:
rca_builder.py→ Determinístico: cuenta ocurrencias, agrupa por taxonomía, calcula porcentajesinference/rca_synthesizer.py→ (Opcional) LLM: genera texto narrativo sobre el árbol ya construido
📁 src/visualization/
Propósito: Capa de salida. Genera reportes visuales. NO recalcula métricas ni inferencias.
| Archivo | Responsabilidad |
|---|---|
dashboard.py |
App Streamlit: filtros, gráficos interactivos, drill-down. |
charts.py |
Funciones para generar gráficos (plotly/matplotlib). |
tree_renderer.py |
Visualización de árboles RCA como PNG/SVG. |
pdf_report.py |
Generación de PDF ejecutivo con ReportLab. |
excel_export.py |
Export a Excel con múltiples hojas y formato. |
Restricción crítica: Este módulo SOLO presenta datos pre-calculados. No contiene lógica analítica.
Interfaces principales:
class ReportGenerator:
"""Genera reportes a partir de datos ya calculados."""
def generate_pdf(self, stats: AggregatedStats, trees: dict[str, RCATree]) -> Path
def generate_excel(self, labels: list[CallLabels], stats: AggregatedStats) -> Path
class TreeRenderer:
"""Renderiza RCATree como imagen."""
def render_png(self, tree: RCATree, output_path: Path) -> None
📁 src/pipeline/
Propósito: Orquestar el flujo completo de ejecución.
| Archivo | Responsabilidad |
|---|---|
orchestrator.py |
Ejecuta stages en orden, maneja errores, logging. |
stages.py |
Define cada stage: transcribe, extract_features, analyze, validate, aggregate, report. |
checkpoint.py |
Guarda/carga estado para resume. |
cli.py |
Interfaz CLI con argparse/typer. |
📁 src/utils/
Propósito: Funciones auxiliares compartidas.
| Archivo | Responsabilidad |
|---|---|
file_io.py |
Lectura/escritura JSON, CSV, audio. Glob patterns. |
logging_config.py |
Setup de logging estructurado (consola + archivo). |
validators.py |
Validación de archivos de audio (formato, duración). |
Modelo de Datos (Output Artifacts)
Estructura mínima obligatoria de labels.json
Todo archivo {call_id}_labels.json SIEMPRE incluye estos campos:
{
"_meta": {
"schema_version": "1.0.0", // OBLIGATORIO - versión del schema
"prompt_version": "v1.2", // OBLIGATORIO - versión del prompt usado
"model_id": "gpt-4o-mini", // OBLIGATORIO - modelo LLM usado
"processed_at": "2024-01-15T10:35:00Z"
},
"call_id": "c001", // OBLIGATORIO
"observed": { // OBLIGATORIO - datos del STT/features
"duration_seconds": 245,
"agent_talk_pct": 0.45,
"customer_talk_pct": 0.55,
"silence_total_seconds": 38,
"hold_events": [...],
"transfer_count": 0
},
"inferred": { // OBLIGATORIO - datos del LLM
"intent": { "code": "...", "confidence": 0.91, "evidence_spans": [...] },
"outcome": { "code": "...", "confidence": 0.85, "evidence_spans": [...] },
"lost_sale_driver": { ... } | null,
"poor_cx_driver": { ... } | null,
"sentiment": { ... },
"agent_quality": { ... },
"summary": "..."
},
"events": [ // OBLIGATORIO - timeline estructurado
{"type": "CALL_START", "t": "00:00", "source": "observed"},
{"type": "HOLD_START", "t": "02:14", "source": "observed"},
{"type": "PRICE_OBJECTION", "t": "03:55", "source": "inferred"},
...
]
}
Sobre events[]
events[] es una lista estructurada de eventos normalizados, NO texto libre.
Cada evento tiene:
type: Código del enum (HOLD_START,TRANSFER,ESCALATION,NEGATIVE_SENTIMENT_PEAK, etc.)t: Timestamp en formatoMM:SSoHH:MM:SSsource:"observed"(viene de STT/features) o"inferred"(viene de LLM)
Tipos de eventos válidos definidos en config/rca_taxonomy.yaml:
journey_event_types:
observed:
- CALL_START
- CALL_END
- HOLD_START
- HOLD_END
- TRANSFER
- SILENCE
- CROSSTALK
inferred:
- INTENT_STATED
- PRICE_OBJECTION
- COMPETITOR_MENTION
- NEGATIVE_SENTIMENT_PEAK
- RESOLUTION_ATTEMPT
- SOFT_DECLINE
- ESCALATION_REQUEST
Flujo de Datos entre Módulos
┌─────────────────────────────────────────────────────────────────────────────┐
│ DATA FLOW │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ data/raw/audio/*.mp3 │
│ │ │
│ ▼ │
│ ┌───────────────┐ │
│ │ transcription │ → data/transcripts/raw/*.json │
│ │ (STT only) │ → data/transcripts/compressed/*.json │
│ └───────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────┐ │
│ │ features │ → data/features/*_features.json │
│ │ (OBSERVED) │ (turn_metrics + detected_events) │
│ └───────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────┐ │
│ │ inference │ → data/processed/*_labels.json │
│ │ (INFERRED) │ (observed + inferred + events) │
│ └───────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────┐ │
│ │ validation │ → rechaza labels sin evidence_spans │
│ │ (quality gate)│ → marca low_confidence │
│ └───────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────┐ │
│ │ aggregation │ → data/outputs/aggregated_stats.json │
│ │(DETERMINISTIC)│ → data/outputs/rca_*.json │
│ └───────────────┘ → data/outputs/emergent_drivers_review.json │
│ │ │
│ ▼ │
│ ┌───────────────┐ │
│ │ visualization │ → data/outputs/executive_summary.pdf │
│ │(PRESENTATION) │ → data/outputs/full_analysis.xlsx │
│ └───────────────┘ → http://localhost:8501 (dashboard) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Separación de Responsabilidades (Resumen)
| Capa | Módulo | Tipo de Lógica | Usa LLM |
|---|---|---|---|
| STT | transcription/ |
Conversión audio→texto | No |
| Texto | transcription/compressor.py |
Reducción de texto | No |
| Features | features/ |
Extracción determinística | No |
| Análisis | inference/analyzer.py |
Clasificación + evidencia | Sí |
| Narrativa | inference/rca_synthesizer.py |
Síntesis textual (opcional) | Sí |
| Validación | validation/ |
Reglas de calidad | No |
| Agregación | aggregation/ |
Estadísticas + RCA tree | No |
| Presentación | visualization/ |
Reportes + dashboard | No |
Convenciones de Código
Naming
- Archivos:
snake_case.py - Clases:
PascalCase - Funciones/métodos:
snake_case - Constantes:
UPPER_SNAKE_CASE
Type hints
Usar type hints en todas las funciones públicas. Pydantic para validación de datos.
Ejemplo de estructura de módulo
# src/features/turn_metrics.py
"""Deterministic extraction of turn-based metrics from transcripts."""
from __future__ import annotations
import logging
from dataclasses import dataclass
from src.transcription.models import TranscriptContract
logger = logging.getLogger(__name__)
@dataclass
class TurnMetrics:
"""Observed metrics extracted from transcript turns."""
agent_talk_pct: float
customer_talk_pct: float
silence_total_seconds: float
interruption_count: int
avg_turn_duration_seconds: float
class TurnMetricsExtractor:
"""Extracts turn metrics from transcript. 100% deterministic, no LLM."""
def extract(self, transcript: TranscriptContract) -> TurnMetrics:
"""Extract turn metrics from transcript utterances."""
utterances = transcript.observed.utterances
# ... cálculos determinísticos ...
return TurnMetrics(...)