diff --git a/backend/beyond_flows/scorers/agentic_score.py b/backend/beyond_flows/scorers/agentic_score.py index 1963d66..1729dbd 100644 --- a/backend/beyond_flows/scorers/agentic_score.py +++ b/backend/beyond_flows/scorers/agentic_score.py @@ -1,22 +1,22 @@ """ agentic_score.py -Calcula el Agentic Readiness Score de un contact center a partir -de un JSON con KPIs agregados (misma estructura que results.json). +Calculates the Agentic Readiness Score of a contact center from +a JSON file with aggregated KPIs (same structure as results.json). -Diseñado como clase para integrarse fácilmente en pipelines. +Designed as a class to integrate easily into pipelines. -Características: -- Tolerante a datos faltantes: si una dimensión no se puede calcular - (porque faltan KPIs), se marca como `computed = False` y no se - incluye en el cálculo del score global. -- La llamada típica en un pipeline será: +Features: +- Tolerant to missing data: if a dimension cannot be calculated + (due to missing KPIs), it is marked as `computed = False` and not + included in the global score calculation. +- Typical pipeline call: from agentic_score import AgenticScorer scorer = AgenticScorer() - result = scorer.run_on_folder("/ruta/a/carpeta") + result = scorer.run_on_folder("/path/to/folder") -Esa carpeta debe contener un `results.json` de entrada. -El módulo generará un `agentic_readiness.json` en la misma carpeta. +The folder must contain a `results.json` input file. +The module will generate an `agentic_readiness.json` in the same folder. """ from __future__ import annotations @@ -35,7 +35,7 @@ Number = Union[int, float] # ========================= def _is_nan(x: Any) -> bool: - """Devuelve True si x es NaN, None o el string 'NaN'.""" + """Returns True if x is NaN, None or the string 'NaN'.""" try: if x is None: return True @@ -60,7 +60,7 @@ def _safe_mean(values: Sequence[Optional[Number]]) -> Optional[float]: def _get_nested(d: Dict[str, Any], *keys: str, default: Any = None) -> Any: - """Acceso seguro a diccionarios anidados.""" + """Safe access to nested dictionaries.""" cur: Any = d for k in keys: if not isinstance(cur, dict) or k not in cur: @@ -75,20 +75,20 @@ def _clamp(value: float, lo: float = 0.0, hi: float = 10.0) -> float: def _normalize_numeric_sequence(field: Any) -> Optional[List[Number]]: """ - Normaliza un campo que representa una secuencia numérica. + Normalizes a field representing a numeric sequence. - Soporta: - - Formato antiguo del pipeline: [10, 20, 30] - - Formato nuevo del pipeline: {"labels": [...], "values": [10, 20, 30]} + Supports: + - Old pipeline format: [10, 20, 30] + - New pipeline format: {"labels": [...], "values": [10, 20, 30]} - Devuelve: - - lista de números, si hay datos numéricos válidos - - None, si el campo no tiene una secuencia numérica interpretable + Returns: + - list of numbers, if there is valid numeric data + - None, if the field does not have an interpretable numeric sequence """ if field is None: return None - # Formato nuevo: {"labels": [...], "values": [...]} + # New format: {"labels": [...], "values": [...]} if isinstance(field, dict) and "values" in field: seq = field.get("values") else: @@ -102,7 +102,7 @@ def _normalize_numeric_sequence(field: Any) -> Optional[List[Number]]: if isinstance(v, (int, float)): out.append(v) else: - # Intentamos conversión suave por si viene como string numérico + # Try soft conversion in case it's a numeric string try: out.append(float(v)) except (TypeError, ValueError): @@ -117,21 +117,21 @@ def _normalize_numeric_sequence(field: Any) -> Optional[List[Number]]: def score_repetitividad(volume_by_skill: Optional[List[Number]]) -> Dict[str, Any]: """ - Repetitividad basada en volumen medio por skill. + Repeatability based on average volume per skill. - Regla (pensada por proceso/skill): - - 10 si volumen > 80 - - 5 si 40–80 - - 0 si < 40 + Rule (designed per process/skill): + - 10 if volume > 80 + - 5 if 40–80 + - 0 if < 40 - Si no hay datos (lista vacía o no numérica), la dimensión - se marca como no calculada (computed = False). + If there is no data (empty or non-numeric list), the dimension + is marked as not calculated (computed = False). """ if not volume_by_skill: return { "score": None, "computed": False, - "reason": "sin_datos_volumen", + "reason": "no_volume_data", "details": { "avg_volume_per_skill": None, "volume_by_skill": volume_by_skill, @@ -143,7 +143,7 @@ def score_repetitividad(volume_by_skill: Optional[List[Number]]) -> Dict[str, An return { "score": None, "computed": False, - "reason": "volumen_no_numerico", + "reason": "volume_not_numeric", "details": { "avg_volume_per_skill": None, "volume_by_skill": volume_by_skill, @@ -152,13 +152,13 @@ def score_repetitividad(volume_by_skill: Optional[List[Number]]) -> Dict[str, An if avg_volume > 80: score = 10.0 - reason = "alto_volumen" + reason = "high_volume" elif avg_volume >= 40: score = 5.0 - reason = "volumen_medio" + reason = "medium_volume" else: score = 0.0 - reason = "volumen_bajo" + reason = "low_volume" return { "score": score, @@ -178,36 +178,36 @@ def score_repetitividad(volume_by_skill: Optional[List[Number]]) -> Dict[str, An def score_predictibilidad(aht_ratio: Any, escalation_rate: Any) -> Dict[str, Any]: """ - Predictibilidad basada en: - - Variabilidad AHT: ratio P90/P50 - - Tasa de escalación (%) + Predictability based on: + - AHT variability: ratio P90/P50 + - Escalation rate (%) - Regla: - - 10 si ratio < 1.5 y escalación < 10% - - 5 si ratio 1.5–2.0 o escalación 10–20% - - 0 si ratio > 2.0 y escalación > 20% - - 3 fallback si datos parciales + Rule: + - 10 if ratio < 1.5 and escalation < 10% + - 5 if ratio 1.5–2.0 or escalation 10–20% + - 0 if ratio > 2.0 and escalation > 20% + - 3 fallback if data parciales - Si no hay ni ratio ni escalación, la dimensión no se calcula. + If there is no ratio nor escalation, the dimension is not calculated. """ if aht_ratio is None and escalation_rate is None: return { "score": None, "computed": False, - "reason": "sin_datos", + "reason": "no_data", "details": { "aht_p90_p50_ratio": None, "escalation_rate_pct": None, }, } - # Normalizamos ratio + # Normalize ratio if aht_ratio is None or _is_nan(aht_ratio): ratio: Optional[float] = None else: ratio = float(aht_ratio) - # Normalizamos escalación + # Normalize escalation if escalation_rate is None or _is_nan(escalation_rate): esc: Optional[float] = None else: @@ -217,7 +217,7 @@ def score_predictibilidad(aht_ratio: Any, return { "score": None, "computed": False, - "reason": "sin_datos", + "reason": "no_data", "details": { "aht_p90_p50_ratio": None, "escalation_rate_pct": None, @@ -230,20 +230,20 @@ def score_predictibilidad(aht_ratio: Any, if ratio is not None and esc is not None: if ratio < 1.5 and esc < 10.0: score = 10.0 - reason = "alta_predictibilidad" + reason = "high_predictability" elif (1.5 <= ratio <= 2.0) or (10.0 <= esc <= 20.0): score = 5.0 - reason = "predictibilidad_media" + reason = "medium_predictability" elif ratio > 2.0 and esc > 20.0: score = 0.0 - reason = "baja_predictibilidad" + reason = "low_predictability" else: score = 3.0 - reason = "caso_intermedio" + reason = "intermediate_case" else: - # Datos parciales: penalizamos pero no ponemos a 0 + # Partial data: penalize but do not set to 0 score = 3.0 - reason = "datos_parciales" + reason = "partial_data" return { "score": score, @@ -263,23 +263,23 @@ def score_predictibilidad(aht_ratio: Any, def score_estructuracion(channel_distribution_pct: Any) -> Dict[str, Any]: """ - Estructuración de datos usando proxy de canal. + Data structuring using channel proxy. - Asumimos que el canal con mayor % es texto (en proyectos reales se puede + We assume the channel with the highest % is text (en proyectos reales se puede parametrizar esta asignación). - Regla: - - 10 si texto > 60% + Rule: + - 10 if text > 60% - 5 si 30–60% - 0 si < 30% - Si no hay datos de canales, la dimensión no se calcula. + If there is no datas of channels, the dimension is not calculated. """ if not channel_distribution_pct: return { "score": None, "computed": False, - "reason": "sin_datos_canal", + "reason": "no_channel_data", "details": { "estimated_text_share_pct": None, "channel_distribution_pct": channel_distribution_pct, @@ -299,7 +299,7 @@ def score_estructuracion(channel_distribution_pct: Any) -> Dict[str, Any]: return { "score": None, "computed": False, - "reason": "canales_no_numericos", + "reason": "channels_not_numeric", "details": { "estimated_text_share_pct": None, "channel_distribution_pct": channel_distribution_pct, @@ -308,13 +308,13 @@ def score_estructuracion(channel_distribution_pct: Any) -> Dict[str, Any]: if max_share > 60.0: score = 10.0 - reason = "alta_proporcion_texto" + reason = "high_text_proportion" elif max_share >= 30.0: score = 5.0 - reason = "proporcion_texto_media" + reason = "medium_text_proportion" else: score = 0.0 - reason = "baja_proporcion_texto" + reason = "low_text_proportion" return { "score": score, @@ -334,9 +334,9 @@ def score_estructuracion(channel_distribution_pct: Any) -> Dict[str, Any]: def score_complejidad(aht_ratio: Any, escalation_rate: Any) -> Dict[str, Any]: """ - Complejidad inversa del proceso (0–10). + Inverse complexity of the process (0–10). - 1) Base: inversa lineal de la variabilidad AHT (ratio P90/P50): + 1) Base: linear inverse de la variabilidad AHT (ratio P90/P50): - ratio = 1.0 -> 10 - ratio = 1.5 -> ~7.5 - ratio = 2.0 -> 5 @@ -345,12 +345,12 @@ def score_complejidad(aht_ratio: Any, formula_base = (3 - ratio) / (3 - 1) * 10, acotado a [0,10] - 2) Ajuste por escalación: + 2) Escalation adjustment: - restamos (escalation_rate / 5) puntos. - Nota: más score = proceso más "simple / automatizable". + Nota: higher score = process more "simple / automatizable". - Si no hay ni ratio ni escalación, la dimensión no se calcula. + If there is no ratio nor escalation, the dimension is not calculated. """ if aht_ratio is None or _is_nan(aht_ratio): ratio: Optional[float] = None @@ -366,36 +366,36 @@ def score_complejidad(aht_ratio: Any, return { "score": None, "computed": False, - "reason": "sin_datos", + "reason": "no_data", "details": { "aht_p90_p50_ratio": None, "escalation_rate_pct": None, }, } - # Base por variabilidad + # Base for variability if ratio is None: - base = 5.0 # fallback neutro - base_reason = "sin_ratio_usamos_valor_neutro" + base = 5.0 # neutral fallback + base_reason = "no_ratio_using_neutral_value" else: base_raw = (3.0 - ratio) / (3.0 - 1.0) * 10.0 base = _clamp(base_raw) - base_reason = "calculado_desde_ratio" + base_reason = "calculated_from_ratio" - # Ajuste por escalación + # Escalation adjustment if esc is None: adj = 0.0 - adj_reason = "sin_escalacion_sin_ajuste" + adj_reason = "no_escalation_no_adjustment" else: - adj = - (esc / 5.0) # cada 5 puntos de escalación resta 1 - adj_reason = "ajuste_por_escalacion" + adj = - (esc / 5.0) # every 5 escalation points subtract 1 + adj_reason = "escalation_adjustment" final_score = _clamp(base + adj) return { "score": final_score, "computed": True, - "reason": "complejidad_inversa", + "reason": "inverse_complexity", "details": { "aht_p90_p50_ratio": ratio, "escalation_rate_pct": esc, @@ -409,21 +409,21 @@ def score_complejidad(aht_ratio: Any, def score_estabilidad(peak_offpeak_ratio: Any) -> Dict[str, Any]: """ - Estabilidad del proceso basada en relación pico/off-peak. + Process stability based on peak/off-peak ratio. - Regla: - - 10 si ratio < 3 + Rule: + - 10 if ratio < 3 - 7 si 3–5 - 3 si 5–7 - 0 si > 7 - Si no hay dato de ratio, la dimensión no se calcula. + If there is no data of ratio, the dimension is not calculated. """ if peak_offpeak_ratio is None or _is_nan(peak_offpeak_ratio): return { "score": None, "computed": False, - "reason": "sin_datos_peak_offpeak", + "reason": "no_peak_offpeak_data", "details": { "peak_offpeak_ratio": None, }, @@ -432,16 +432,16 @@ def score_estabilidad(peak_offpeak_ratio: Any) -> Dict[str, Any]: r = float(peak_offpeak_ratio) if r < 3.0: score = 10.0 - reason = "muy_estable" + reason = "very_stable" elif r < 5.0: score = 7.0 - reason = "estable_moderado" + reason = "moderately_stable" elif r < 7.0: score = 3.0 - reason = "pico_pronunciado" + reason = "pronounced_peak" else: score = 0.0 - reason = "muy_inestable" + reason = "very_unstable" return { "score": score, @@ -460,20 +460,20 @@ def score_estabilidad(peak_offpeak_ratio: Any) -> Dict[str, Any]: def score_roi(annual_savings: Any) -> Dict[str, Any]: """ - ROI potencial anual. + Annual potential ROI. - Regla: - - 10 si ahorro > 100k €/año - - 5 si 10k–100k €/año - - 0 si < 10k €/año + Rule: + - 10 if savings > 100k €/year + - 5 si 10k–100k €/year + - 0 si < 10k €/year - Si no hay dato de ahorro, la dimensión no se calcula. + If there is no data of savings, the dimension is not calculated. """ if annual_savings is None or _is_nan(annual_savings): return { "score": None, "computed": False, - "reason": "sin_datos_ahorro", + "reason": "no_savings_data", "details": { "annual_savings_eur": None, }, @@ -482,13 +482,13 @@ def score_roi(annual_savings: Any) -> Dict[str, Any]: savings = float(annual_savings) if savings > 100_000: score = 10.0 - reason = "roi_alto" + reason = "high_roi" elif savings >= 10_000: score = 5.0 - reason = "roi_medio" + reason = "medium_roi" else: score = 0.0 - reason = "roi_bajo" + reason = "low_roi" return { "score": score, @@ -506,20 +506,20 @@ def score_roi(annual_savings: Any) -> Dict[str, Any]: def classify_agentic_score(score: Optional[float]) -> Dict[str, Any]: """ - Clasificación final (alineada con frontend): - - ≥6: COPILOT 🤖 (Listo para Copilot) + Final classification (aligned with frontend): + - ≥6: COPILOT 🤖 (Ready for Copilot) - 4–5.99: OPTIMIZE 🔧 (Optimizar Primero) - <4: HUMAN 👤 (Requiere Gestión Humana) - Si score es None (ninguna dimensión disponible), devuelve NO_DATA. + If score is None (no dimension available), returns NO_DATA. """ if score is None: return { "label": "NO_DATA", "emoji": "❓", "description": ( - "No se ha podido calcular el Agentic Readiness Score porque " - "ninguna de las dimensiones tenía datos suficientes." + "Could not calculate the Agentic Readiness Score because " + "none of the dimensions had sufficient data." ), } @@ -527,22 +527,22 @@ def classify_agentic_score(score: Optional[float]) -> Dict[str, Any]: label = "COPILOT" emoji = "🤖" description = ( - "Listo para Copilot. Procesos con predictibilidad y simplicidad " - "suficientes para asistencia IA (sugerencias en tiempo real, autocompletado)." + "Ready for Copilot. Processes with sufficient predictability and simplicity " + "for AI assistance (real-time suggestions, autocomplete)." ) elif score >= 4.0: label = "OPTIMIZE" emoji = "🔧" description = ( - "Optimizar primero. Estandarizar procesos y reducir variabilidad " - "antes de implementar asistencia IA." + "Optimize first. Standardize processes and reduce variability " + "before implementing AI assistance." ) else: label = "HUMAN" emoji = "👤" description = ( - "Requiere gestión humana. Procesos complejos o variables que " - "necesitan intervención humana antes de considerar automatización." + "Requires human management. Complex or variable processes that " + "need human intervention before considering automation." ) return { @@ -604,22 +604,22 @@ class AgenticScorer: def compute_from_data(self, data: Dict[str, Any]) -> Dict[str, Any]: """ - Calcula el Agentic Readiness Score a partir de un dict de datos. + Calculates the Agentic Readiness Score from a data dict. - Tolerante a datos faltantes: renormaliza pesos usando solo - dimensiones con `computed = True`. + Tolerant to missing data: renormalizes weights using only + dimensions with `computed = True`. - Compatibilidad con pipeline: - - Soporta tanto el formato antiguo: + Pipeline compatibility: + - Supports both the old format: "volume_by_skill": [10, 20, 30] - - como el nuevo: + - and the new: "volume_by_skill": {"labels": [...], "values": [10, 20, 30]} """ volumetry = data.get("volumetry", {}) op = data.get("operational_performance", {}) econ = data.get("economy_costs", {}) - # Normalizamos aquí los posibles formatos para contentar al type checker + # Normalize here the possible formats for the type checker volume_by_skill = _normalize_numeric_sequence( volumetry.get("volume_by_skill") ) @@ -650,7 +650,7 @@ class AgenticScorer: "roi": roi, } - # --- Renormalización de pesos sólo con dimensiones disponibles --- + # --- Weight renormalization only with available dimensions --- effective_weights: Dict[str, float] = {} for name, base_w in self.base_weights.items(): dim = sub_scores.get(name, {}) @@ -665,7 +665,7 @@ class AgenticScorer: else: normalized_weights = {} - # --- Score final --- + # --- Final score --- if not normalized_weights: final_score: Optional[float] = None else: @@ -692,8 +692,8 @@ class AgenticScorer: "metadata": { "source_module": "agentic_score.py", "notes": ( - "Modelo simplificado basado en KPIs agregados. " - "Renormaliza los pesos cuando faltan dimensiones." + "Simplified model based on aggregated KPIs. " + "Renormalizes weights when dimensions are missing." ), }, } @@ -710,11 +710,11 @@ class AgenticScorer: def run_on_folder(self, folder_path: Union[str, Path]) -> Dict[str, Any]: """ - Punto de entrada típico para el pipeline: - - Lee /results.json - - Calcula Agentic Readiness - - Escribe /agentic_readiness.json - - Devuelve el dict con el resultado + Typical pipeline entry point: + - Reads /results.json + - Calculates Agentic Readiness + - Writes /agentic_readiness.json + - Returns the dict with the result """ data = self.load_results(folder_path) result = self.compute_from_data(data) diff --git a/frontend/components/tabs/AgenticReadinessTab.tsx b/frontend/components/tabs/AgenticReadinessTab.tsx index 246d85e..95871d8 100644 --- a/frontend/components/tabs/AgenticReadinessTab.tsx +++ b/frontend/components/tabs/AgenticReadinessTab.tsx @@ -25,7 +25,7 @@ import { // RED FLAGS CONFIGURATION AND DETECTION // ============================================ -// v3.5: Configuración de Red Flags +// v3.5: Red Flags Configuration interface RedFlagConfig { id: string; label: string; @@ -41,51 +41,51 @@ interface RedFlagConfig { const RED_FLAG_CONFIGS: RedFlagConfig[] = [ { id: 'cv_high', - label: 'CV AHT Crítico', + label: 'Critical AHT CV', shortLabel: 'CV', threshold: 120, operator: '>', getValue: (q) => q.cv_aht, format: (v) => `${v.toFixed(0)}%`, color: 'red', - description: 'Variabilidad extrema - procesos impredecibles' + description: 'Extreme variability - unpredictable processes' }, { id: 'transfer_high', - label: 'Transfer Excesivo', + label: 'Excessive Transfer', shortLabel: 'Transfer', threshold: 50, operator: '>', getValue: (q) => q.transfer_rate, format: (v) => `${v.toFixed(0)}%`, color: 'orange', - description: 'Alta complejidad - requiere escalado frecuente' + description: 'High complexity - requires frequent escalation' }, { id: 'volume_low', - label: 'Volumen Insuficiente', + label: 'Insufficient Volume', shortLabel: 'Vol', threshold: 50, operator: '<', getValue: (q) => q.volume, format: (v) => v.toLocaleString(), color: 'slate', - description: 'ROI negativo - volumen no justifica inversión' + description: 'Negative ROI - volume doesn\'t justify investment' }, { id: 'valid_low', - label: 'Calidad Datos Baja', + label: 'Low Data Quality', shortLabel: 'Valid', threshold: 30, operator: '<', getValue: (q) => q.volume > 0 ? (q.volumeValid / q.volume) * 100 : 0, format: (v) => `${v.toFixed(0)}%`, color: 'amber', - description: 'Datos poco fiables - métricas distorsionadas' + description: 'Unreliable data - distorted metrics' } ]; -// v3.5: Detectar red flags de una cola +// v3.5: Detect red flags for a queue interface DetectedRedFlag { config: RedFlagConfig; value: number; @@ -108,7 +108,7 @@ function detectRedFlags(queue: OriginalQueueMetrics): DetectedRedFlag[] { return flags; } -// v3.5: Componente de badge de Red Flag individual +// v3.5: Individual Red Flag badge component function RedFlagBadge({ flag, size = 'sm' }: { flag: DetectedRedFlag; size?: 'sm' | 'md' }) { const sizeClasses = size === 'md' ? 'px-2 py-1 text-xs' : 'px-1.5 py-0.5 text-[10px]'; diff --git a/frontend/locales/en.json b/frontend/locales/en.json index 63468b4..4433f42 100644 --- a/frontend/locales/en.json +++ b/frontend/locales/en.json @@ -570,12 +570,16 @@ "humanOnlyAction": "Maintain human management, evaluate periodically", "redFlags": { "cvCritical": "Critical AHT CV", + "cvCriticalShort": "CV", "cvCriticalDesc": "Extreme variability - unpredictable processes", "transferExcessive": "Excessive Transfer", + "transferExcessiveShort": "Transfer", "transferExcessiveDesc": "High complexity - requires frequent escalation", "volumeInsufficient": "Insufficient Volume", + "volumeInsufficientShort": "Vol", "volumeInsufficientDesc": "Negative ROI - volume doesn't justify investment", "dataQualityLow": "Low Data Quality", + "dataQualityLowShort": "Valid", "dataQualityLowDesc": "Unreliable data - distorted metrics", "threshold": "(threshold: {{operator}}{{value}})" }, @@ -814,6 +818,33 @@ "roiBad": "Marginal ROI, evaluate other benefits", "resolution": "Resolution", "dataQuality": "Data Quality" + }, + "subFactors": { + "repeatability": "Repeatability", + "repeatabilityDisplayName": "Repeatability", + "repeatabilityDescription": "Monthly volume: {{volume}} interactions", + "predictability": "Predictability", + "predictabilityDisplayName": "Predictability", + "predictabilityDescription": "AHT CV: {{cv}}%, Escalation: {{esc}}%", + "structuring": "Structuring", + "structuringDisplayName": "Structuring", + "structuringDescription": "{{pct}}% structured fields", + "inverseComplexity": "Inverse Complexity", + "inverseComplexityDisplayName": "Inverse Complexity", + "inverseComplexityDescription": "{{pct}}% exceptions", + "stability": "Stability", + "stabilityDisplayName": "Stability", + "stabilityDescription": "{{pct}}% off-hours", + "roiSavings": "ROI", + "roiSavingsDisplayName": "ROI", + "roiSavingsDescription": "€{{amount}}K annual potential savings", + "interpretations": { + "excellentForAutomation": "Excellent candidate for complete automation (Automate)", + "goodForAssistance": "Good candidate for agentic assistance (Assist)", + "candidateForAugmentation": "Candidate for human augmentation (Augment)", + "notRecommended": "Not recommended for automation at this time", + "bronzeAnalysis": "Bronze analysis does not include Agentic Readiness Score" + } } }, "economicModel": { diff --git a/frontend/locales/es.json b/frontend/locales/es.json index b6cf7a0..c96cfb5 100644 --- a/frontend/locales/es.json +++ b/frontend/locales/es.json @@ -570,12 +570,16 @@ "humanOnlyAction": "Mantener gestión humana, evaluar periódicamente", "redFlags": { "cvCritical": "CV AHT Crítico", + "cvCriticalShort": "CV", "cvCriticalDesc": "Variabilidad extrema - procesos impredecibles", "transferExcessive": "Transfer Excesivo", + "transferExcessiveShort": "Transfer", "transferExcessiveDesc": "Alta complejidad - requiere escalado frecuente", "volumeInsufficient": "Volumen Insuficiente", + "volumeInsufficientShort": "Vol", "volumeInsufficientDesc": "ROI negativo - volumen no justifica inversión", "dataQualityLow": "Calidad Datos Baja", + "dataQualityLowShort": "Valid", "dataQualityLowDesc": "Datos poco fiables - métricas distorsionadas", "threshold": "(umbral: {{operator}}{{value}})" }, @@ -814,6 +818,33 @@ "roiBad": "ROI marginal, evaluar otros beneficios", "resolution": "Resolutividad", "dataQuality": "Calidad Datos" + }, + "subFactors": { + "repeatability": "Repetitividad", + "repeatabilityDisplayName": "Repetitividad", + "repeatabilityDescription": "Volumen mensual: {{volume}} interacciones", + "predictability": "Predictibilidad", + "predictabilityDisplayName": "Predictibilidad", + "predictabilityDescription": "CV AHT: {{cv}}%, Escalación: {{esc}}%", + "structuring": "Estructuración", + "structuringDisplayName": "Estructuración", + "structuringDescription": "{{pct}}% de campos estructurados", + "inverseComplexity": "Complejidad Inversa", + "inverseComplexityDisplayName": "Complejidad Inversa", + "inverseComplexityDescription": "{{pct}}% de excepciones", + "stability": "Estabilidad", + "stabilityDisplayName": "Estabilidad", + "stabilityDescription": "{{pct}}% fuera de horario", + "roiSavings": "ROI", + "roiSavingsDisplayName": "ROI", + "roiSavingsDescription": "€{{amount}}K ahorro potencial anual", + "interpretations": { + "excellentForAutomation": "Excelente candidato para automatización completa (Automate)", + "goodForAssistance": "Buen candidato para asistencia agéntica (Assist)", + "candidateForAugmentation": "Candidato para augmentación humana (Augment)", + "notRecommended": "No recomendado para automatización en este momento", + "bronzeAnalysis": "Análisis Bronze no incluye Agentic Readiness Score" + } } }, "economicModel": { diff --git a/frontend/utils/agenticReadinessV2.ts b/frontend/utils/agenticReadinessV2.ts index 81c6ec5..039827d 100644 --- a/frontend/utils/agenticReadinessV2.ts +++ b/frontend/utils/agenticReadinessV2.ts @@ -1,20 +1,20 @@ /** * Agentic Readiness Score v2.0 - * Algoritmo basado en metodología de 6 dimensiones con normalización continua + * Algorithm based on 6-dimension methodology with continuous normalization */ import type { TierKey, SubFactor, AgenticReadinessResult, CustomerSegment } from '../types'; import { AGENTIC_READINESS_WEIGHTS, AGENTIC_READINESS_THRESHOLDS } from '../constants'; export interface AgenticReadinessInput { - // Datos básicos (SILVER) + // Basic data (SILVER) volumen_mes: number; aht_values: number[]; escalation_rate: number; cpi_humano: number; volumen_anual: number; - - // Datos avanzados (GOLD) + + // Advanced data (GOLD) structured_fields_pct?: number; exception_rate?: number; hourly_distribution?: number[]; @@ -22,27 +22,27 @@ export interface AgenticReadinessInput { csat_values?: number[]; motivo_contacto_entropy?: number; resolucion_entropy?: number; - + // Tier tier: TierKey; } /** - * SUB-FACTOR 1: REPETITIVIDAD (25%) - * Basado en volumen mensual con normalización logística + * SUB-FACTOR 1: REPEATABILITY (25%) + * Based on monthly volume with logistic normalization */ -function calculateRepetitividadScore(volumen_mes: number): SubFactor { +function calculateRepeatabilityScore(volumen_mes: number): SubFactor { const { k, x0 } = AGENTIC_READINESS_THRESHOLDS.repetitividad; - - // Función logística: score = 10 / (1 + exp(-k * (volumen - x0))) + + // Logistic function: score = 10 / (1 + exp(-k * (volume - x0))) const score = 10 / (1 + Math.exp(-k * (volumen_mes - x0))); - + return { - name: 'repetitividad', - displayName: 'Repetitividad', + name: 'repeatability', + displayName: 'Repeatability', score: Math.round(score * 10) / 10, weight: AGENTIC_READINESS_WEIGHTS.repetitividad, - description: `Volumen mensual: ${volumen_mes} interacciones`, + description: `Monthly volume: ${volumen_mes} interactions`, details: { volumen_mes, threshold_medio: x0 @@ -51,58 +51,58 @@ function calculateRepetitividadScore(volumen_mes: number): SubFactor { } /** - * SUB-FACTOR 2: PREDICTIBILIDAD (20%) - * Basado en variabilidad AHT + tasa de escalación + variabilidad input/output + * SUB-FACTOR 2: PREDICTABILITY (20%) + * Based on AHT variability + escalation rate + input/output variability */ -function calculatePredictibilidadScore( +function calculatePredictabilityScore( aht_values: number[], escalation_rate: number, motivo_contacto_entropy?: number, resolucion_entropy?: number ): SubFactor { const thresholds = AGENTIC_READINESS_THRESHOLDS.predictibilidad; - - // 1. VARIABILIDAD AHT (40%) + + // 1. AHT VARIABILITY (40%) const aht_mean = aht_values.reduce((a, b) => a + b, 0) / aht_values.length; const aht_variance = aht_values.reduce((sum, val) => sum + Math.pow(val - aht_mean, 2), 0) / aht_values.length; const aht_std = Math.sqrt(aht_variance); const cv_aht = aht_std / aht_mean; - - // Normalizar CV a escala 0-10 - const score_aht = Math.max(0, Math.min(10, + + // Normalize CV to 0-10 scale + const score_aht = Math.max(0, Math.min(10, 10 * (1 - (cv_aht - thresholds.cv_aht_excellent) / (thresholds.cv_aht_poor - thresholds.cv_aht_excellent)) )); - - // 2. TASA DE ESCALACIÓN (30%) - const score_escalacion = Math.max(0, Math.min(10, + + // 2. ESCALATION RATE (30%) + const score_escalacion = Math.max(0, Math.min(10, 10 * (1 - escalation_rate / thresholds.escalation_poor) )); - - // 3. VARIABILIDAD INPUT/OUTPUT (30%) + + // 3. INPUT/OUTPUT VARIABILITY (30%) let score_variabilidad: number; if (motivo_contacto_entropy !== undefined && resolucion_entropy !== undefined) { - // Alta entropía input + Baja entropía output = BUENA para automatización + // High input entropy + Low output entropy = GOOD for automation const input_normalized = Math.min(motivo_contacto_entropy / 3.0, 1.0); const output_normalized = Math.min(resolucion_entropy / 3.0, 1.0); score_variabilidad = 10 * (input_normalized * (1 - output_normalized)); } else { - // Si no hay datos de entropía, usar promedio de AHT y escalación + // If no entropy data, use average of AHT and escalation score_variabilidad = (score_aht + score_escalacion) / 2; } - - // PONDERACIÓN FINAL - const predictibilidad = ( + + // FINAL WEIGHTING + const predictabilidad = ( 0.40 * score_aht + 0.30 * score_escalacion + 0.30 * score_variabilidad ); - + return { - name: 'predictibilidad', - displayName: 'Predictibilidad', - score: Math.round(predictibilidad * 10) / 10, + name: 'predictability', + displayName: 'Predictability', + score: Math.round(predictabilidad * 10) / 10, weight: AGENTIC_READINESS_WEIGHTS.predictibilidad, - description: `CV AHT: ${(cv_aht * 100).toFixed(1)}%, Escalación: ${(escalation_rate * 100).toFixed(1)}%`, + description: `AHT CV: ${(cv_aht * 100).toFixed(1)}%, Escalation: ${(escalation_rate * 100).toFixed(1)}%`, details: { cv_aht: Math.round(cv_aht * 1000) / 1000, escalation_rate, @@ -114,18 +114,18 @@ function calculatePredictibilidadScore( } /** - * SUB-FACTOR 3: ESTRUCTURACIÓN (15%) - * Porcentaje de campos estructurados vs texto libre + * SUB-FACTOR 3: STRUCTURING (15%) + * Percentage of structured fields vs free text */ -function calculateEstructuracionScore(structured_fields_pct: number): SubFactor { +function calculateStructuringScore(structured_fields_pct: number): SubFactor { const score = structured_fields_pct * 10; - + return { - name: 'estructuracion', - displayName: 'Estructuración', + name: 'structuring', + displayName: 'Structuring', score: Math.round(score * 10) / 10, weight: AGENTIC_READINESS_WEIGHTS.estructuracion, - description: `${(structured_fields_pct * 100).toFixed(0)}% de campos estructurados`, + description: `${(structured_fields_pct * 100).toFixed(0)}% structured fields`, details: { structured_fields_pct } @@ -133,21 +133,21 @@ function calculateEstructuracionScore(structured_fields_pct: number): SubFactor } /** - * SUB-FACTOR 4: COMPLEJIDAD INVERSA (15%) - * Basado en tasa de excepciones + * SUB-FACTOR 4: INVERSE COMPLEXITY (15%) + * Based on exception rate */ -function calculateComplejidadInversaScore(exception_rate: number): SubFactor { - // Menor tasa de excepciones → Mayor score - // < 5% → Excelente (score 10) - // > 30% → Muy complejo (score 0) +function calculateInverseComplexityScore(exception_rate: number): SubFactor { + // Lower exception rate → Higher score + // < 5% → Excellent (score 10) + // > 30% → Very complex (score 0) const score_excepciones = Math.max(0, Math.min(10, 10 * (1 - exception_rate / 0.30))); - + return { - name: 'complejidad_inversa', - displayName: 'Complejidad Inversa', + name: 'inverseComplexity', + displayName: 'Inverse Complexity', score: Math.round(score_excepciones * 10) / 10, weight: AGENTIC_READINESS_WEIGHTS.complejidad_inversa, - description: `${(exception_rate * 100).toFixed(1)}% de excepciones`, + description: `${(exception_rate * 100).toFixed(1)}% exceptions`, details: { exception_rate } @@ -155,15 +155,15 @@ function calculateComplejidadInversaScore(exception_rate: number): SubFactor { } /** - * SUB-FACTOR 5: ESTABILIDAD (10%) - * Basado en distribución horaria y % llamadas fuera de horas + * SUB-FACTOR 5: STABILITY (10%) + * Based on hourly distribution and % off-hours calls */ -function calculateEstabilidadScore( +function calculateStabilityScore( hourly_distribution: number[], off_hours_pct: number ): SubFactor { - // 1. UNIFORMIDAD DISTRIBUCIÓN HORARIA (60%) - // Calcular entropía de Shannon + // 1. HOURLY DISTRIBUTION UNIFORMITY (60%) + // Calculate Shannon entropy const total = hourly_distribution.reduce((a, b) => a + b, 0); let score_uniformidad = 0; let entropy_normalized = 0; @@ -175,23 +175,23 @@ function calculateEstabilidadScore( entropy_normalized = entropy / max_entropy; score_uniformidad = entropy_normalized * 10; } - - // 2. % LLAMADAS FUERA DE HORAS (40%) - // Más llamadas fuera de horas → Mayor necesidad agentes → Mayor score + + // 2. % OFF-HOURS CALLS (40%) + // More off-hours calls → Higher agent need → Higher score const score_off_hours = Math.min(10, (off_hours_pct / 0.30) * 10); - - // PONDERACIÓN + + // WEIGHTING const estabilidad = ( 0.60 * score_uniformidad + 0.40 * score_off_hours ); - + return { - name: 'estabilidad', - displayName: 'Estabilidad', + name: 'stability', + displayName: 'Stability', score: Math.round(estabilidad * 10) / 10, weight: AGENTIC_READINESS_WEIGHTS.estabilidad, - description: `${(off_hours_pct * 100).toFixed(1)}% fuera de horario`, + description: `${(off_hours_pct * 100).toFixed(1)}% off-hours`, details: { entropy_normalized: Math.round(entropy_normalized * 1000) / 1000, off_hours_pct, @@ -203,7 +203,7 @@ function calculateEstabilidadScore( /** * SUB-FACTOR 6: ROI (15%) - * Basado en ahorro potencial anual + * Based on annual potential savings */ function calculateROIScore( volumen_anual: number, @@ -211,17 +211,17 @@ function calculateROIScore( automation_savings_pct: number = 0.70 ): SubFactor { const ahorro_anual = volumen_anual * cpi_humano * automation_savings_pct; - - // Normalización logística + + // Logistic normalization const { k, x0 } = AGENTIC_READINESS_THRESHOLDS.roi; const score = 10 / (1 + Math.exp(-k * (ahorro_anual - x0))); - + return { name: 'roi', displayName: 'ROI', score: Math.round(score * 10) / 10, weight: AGENTIC_READINESS_WEIGHTS.roi, - description: `€${(ahorro_anual / 1000).toFixed(0)}K ahorro potencial anual`, + description: `€${(ahorro_anual / 1000).toFixed(0)}K annual potential savings`, details: { ahorro_anual: Math.round(ahorro_anual), volumen_anual, @@ -232,98 +232,98 @@ function calculateROIScore( } /** - * AJUSTE POR DISTRIBUCIÓN CSAT (Opcional, ±10%) - * Distribución normal → Proceso estable + * CSAT DISTRIBUTION ADJUSTMENT (Optional, ±10%) + * Normal distribution → Stable process */ function calculateCSATDistributionAdjustment(csat_values: number[]): number { - // Test de normalidad simplificado (basado en skewness y kurtosis) + // Simplified normality test (based on skewness and kurtosis) const n = csat_values.length; const mean = csat_values.reduce((a, b) => a + b, 0) / n; const variance = csat_values.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / n; const std = Math.sqrt(variance); - + // Skewness const skewness = csat_values.reduce((sum, val) => sum + Math.pow((val - mean) / std, 3), 0) / n; - + // Kurtosis const kurtosis = csat_values.reduce((sum, val) => sum + Math.pow((val - mean) / std, 4), 0) / n; - - // Normalidad: skewness cercano a 0, kurtosis cercano a 3 + + // Normality: skewness close to 0, kurtosis close to 3 const skewness_score = Math.max(0, 1 - Math.abs(skewness)); const kurtosis_score = Math.max(0, 1 - Math.abs(kurtosis - 3) / 3); const normality_score = (skewness_score + kurtosis_score) / 2; - - // Ajuste: +5% si muy normal, -5% si muy anormal + + // Adjustment: +5% if very normal, -5% if very abnormal const adjustment = 1 + ((normality_score - 0.5) * 0.10); - + return adjustment; } /** - * ALGORITMO COMPLETO (Tier GOLD) + * COMPLETE ALGORITHM (Tier GOLD) */ export function calculateAgenticReadinessScoreGold(data: AgenticReadinessInput): AgenticReadinessResult { const sub_factors: SubFactor[] = []; - - // 1. REPETITIVIDAD - sub_factors.push(calculateRepetitividadScore(data.volumen_mes)); - - // 2. PREDICTIBILIDAD - sub_factors.push(calculatePredictibilidadScore( + + // 1. REPEATABILITY + sub_factors.push(calculateRepeatabilityScore(data.volumen_mes)); + + // 2. PREDICTABILITY + sub_factors.push(calculatePredictabilityScore( data.aht_values, data.escalation_rate, data.motivo_contacto_entropy, data.resolucion_entropy )); - - // 3. ESTRUCTURACIÓN - sub_factors.push(calculateEstructuracionScore(data.structured_fields_pct || 0.5)); - - // 4. COMPLEJIDAD INVERSA - sub_factors.push(calculateComplejidadInversaScore(data.exception_rate || 0.15)); - - // 5. ESTABILIDAD - sub_factors.push(calculateEstabilidadScore( + + // 3. STRUCTURING + sub_factors.push(calculateStructuringScore(data.structured_fields_pct || 0.5)); + + // 4. INVERSE COMPLEXITY + sub_factors.push(calculateInverseComplexityScore(data.exception_rate || 0.15)); + + // 5. STABILITY + sub_factors.push(calculateStabilityScore( data.hourly_distribution || Array(24).fill(1), data.off_hours_pct || 0.2 )); - + // 6. ROI sub_factors.push(calculateROIScore( data.volumen_anual, data.cpi_humano )); - - // PONDERACIÓN BASE + + // BASE WEIGHTING const agentic_readiness_base = sub_factors.reduce( (sum, factor) => sum + (factor.score * factor.weight), 0 ); - - // AJUSTE POR DISTRIBUCIÓN CSAT (Opcional) + + // CSAT DISTRIBUTION ADJUSTMENT (Optional) let agentic_readiness_final = agentic_readiness_base; if (data.csat_values && data.csat_values.length > 10) { const adjustment = calculateCSATDistributionAdjustment(data.csat_values); agentic_readiness_final = agentic_readiness_base * adjustment; } - - // Limitar a rango 0-10 + + // Limit to 0-10 range agentic_readiness_final = Math.max(0, Math.min(10, agentic_readiness_final)); - - // Interpretación + + // Interpretation let interpretation = ''; let confidence: 'high' | 'medium' | 'low' = 'high'; - + if (agentic_readiness_final >= 8) { - interpretation = 'Excelente candidato para automatización completa (Automate)'; + interpretation = 'Excellent candidate for complete automation (Automate)'; } else if (agentic_readiness_final >= 5) { - interpretation = 'Buen candidato para asistencia agéntica (Assist)'; + interpretation = 'Good candidate for agentic assistance (Assist)'; } else if (agentic_readiness_final >= 3) { - interpretation = 'Candidato para augmentación humana (Augment)'; + interpretation = 'Candidate for human augmentation (Augment)'; } else { - interpretation = 'No recomendado para automatización en este momento'; + interpretation = 'Not recommended for automation at this time'; } - + return { score: Math.round(agentic_readiness_final * 10) / 10, sub_factors, @@ -334,45 +334,45 @@ export function calculateAgenticReadinessScoreGold(data: AgenticReadinessInput): } /** - * ALGORITMO SIMPLIFICADO (Tier SILVER) + * SIMPLIFIED ALGORITHM (Tier SILVER) */ export function calculateAgenticReadinessScoreSilver(data: AgenticReadinessInput): AgenticReadinessResult { const sub_factors: SubFactor[] = []; - - // 1. REPETITIVIDAD (30%) - const repetitividad = calculateRepetitividadScore(data.volumen_mes); - repetitividad.weight = 0.30; - sub_factors.push(repetitividad); - - // 2. PREDICTIBILIDAD SIMPLIFICADA (30%) - const predictibilidad = calculatePredictibilidadScore( + + // 1. REPEATABILITY (30%) + const repeatability = calculateRepeatabilityScore(data.volumen_mes); + repeatability.weight = 0.30; + sub_factors.push(repeatability); + + // 2. SIMPLIFIED PREDICTABILITY (30%) + const predictability = calculatePredictabilityScore( data.aht_values, data.escalation_rate ); - predictibilidad.weight = 0.30; - sub_factors.push(predictibilidad); - + predictability.weight = 0.30; + sub_factors.push(predictability); + // 3. ROI (40%) const roi = calculateROIScore(data.volumen_anual, data.cpi_humano); roi.weight = 0.40; sub_factors.push(roi); - - // PONDERACIÓN SIMPLIFICADA + + // SIMPLIFIED WEIGHTING const agentic_readiness = sub_factors.reduce( (sum, factor) => sum + (factor.score * factor.weight), 0 ); - - // Interpretación + + // Interpretation let interpretation = ''; if (agentic_readiness >= 7) { - interpretation = 'Buen candidato para automatización'; + interpretation = 'Good candidate for automation'; } else if (agentic_readiness >= 4) { - interpretation = 'Candidato para asistencia agéntica'; + interpretation = 'Candidate for agentic assistance'; } else { - interpretation = 'Requiere análisis más profundo (considerar GOLD)'; + interpretation = 'Requires deeper analysis (consider GOLD)'; } - + return { score: Math.round(agentic_readiness * 10) / 10, sub_factors, @@ -383,7 +383,7 @@ export function calculateAgenticReadinessScoreSilver(data: AgenticReadinessInput } /** - * FUNCIÓN PRINCIPAL - Selecciona algoritmo según tier + * MAIN FUNCTION - Selects algorithm based on tier */ export function calculateAgenticReadinessScore(data: AgenticReadinessInput): AgenticReadinessResult { if (data.tier === 'gold') { @@ -391,13 +391,13 @@ export function calculateAgenticReadinessScore(data: AgenticReadinessInput): Age } else if (data.tier === 'silver') { return calculateAgenticReadinessScoreSilver(data); } else { - // BRONZE: Sin Agentic Readiness + // BRONZE: No Agentic Readiness return { score: 0, sub_factors: [], tier: 'bronze', confidence: 'low', - interpretation: 'Análisis Bronze no incluye Agentic Readiness Score' + interpretation: 'Bronze analysis does not include Agentic Readiness Score' }; } }