From a3e9ec89d27ba9e7155ee273722c76c00dd30732 Mon Sep 17 00:00:00 2001 From: "Mauro Rosero P." Date: Wed, 12 Mar 2025 10:41:40 -0500 Subject: [PATCH] =?UTF-8?q?[IMPROVED]=20Mejorar=20estimaci=C3=B3n=20de=20t?= =?UTF-8?q?arifas=20en=20rate=5Fupdate.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Añadir sistema de respaldo para cuando la API de Perplexity no está disponible - Crear función get_fallback_rate para generar estimaciones basadas en datos de mercado - Mejorar la extracción de valores numéricos con múltiples estrategias de parsing - Permitir estimaciones aproximadas cuando no hay datos del año actual - Implementar factores regionales para ajustar tarifas según la ubicación - Continuar con valores de respaldo cuando hay errores de API consecutivos 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- bin/rate_update.py | 192 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 156 insertions(+), 36 deletions(-) diff --git a/bin/rate_update.py b/bin/rate_update.py index 99be45e..eb6307e 100755 --- a/bin/rate_update.py +++ b/bin/rate_update.py @@ -199,23 +199,62 @@ def query_perplexity(prompt, model="o1"): content = result.get('choices', [{}])[0].get('message', {}).get('content', '') logger.info(f"Contenido de la respuesta: {content}") - # Intentar encontrar un número con 2 decimales en la respuesta - match = re.search(r'\$?(\d+\.\d{2})', content) - if match: - value = float(match.group(1)) - logger.info(f"Valor extraído: {value}") - return value + # Estrategia 1: Buscar patrones específicos de dinero (con y sin símbolo de dólar) + # Buscar un patrón como $30.00, 30.00, $30, o 30 USD + money_patterns = [ + r'\$(\d+\.\d{2})', # $30.00 + r'(\d+\.\d{2})\s*USD', # 30.00 USD + r'(\d+\.\d{2})', # 30.00 + r'\$(\d+)', # $30 + r'(\d+)\s*USD', # 30 USD + r'(\d+(?:\.\d+)?)' # Cualquier número con o sin decimales + ] - # Si no encuentra un formato exacto, intentar convertir toda la respuesta a float + for pattern in money_patterns: + match = re.search(pattern, content) + if match: + try: + value = float(match.group(1)) + logger.info(f"Valor extraído con patrón {pattern}: {value}") + return round(value, 2) # Asegurar 2 decimales + except (ValueError, IndexError): + continue + + # Estrategia 2: Extraer todos los números y elegir el más probable + all_numbers = re.findall(r'\d+(?:\.\d+)?', content) + if all_numbers: + try: + # Si hay varios números, elegir el más probable + # (En este caso, podríamos tomar el promedio o el primero) + if len(all_numbers) > 1: + logger.info(f"Múltiples números encontrados: {all_numbers}") + # Eliminamos valores extremadamente altos o bajos + filtered_numbers = [float(n) for n in all_numbers if 5 <= float(n) <= 500] + if filtered_numbers: + value = sum(filtered_numbers) / len(filtered_numbers) + else: + value = float(all_numbers[0]) + else: + value = float(all_numbers[0]) + + logger.info(f"Valor final extraído: {value}") + return round(value, 2) + except ValueError: + pass + + # Estrategia 3: Último recurso, intentar limpiar el texto y extraer un número try: # Eliminar cualquier símbolo de moneda y espacios cleaned_content = re.sub(r'[^\d.]', '', content) - value = round(float(cleaned_content), 2) - logger.info(f"Valor limpiado y extraído: {value}") - return value + if cleaned_content: + value = round(float(cleaned_content), 2) + logger.info(f"Valor limpiado y extraído: {value}") + return value except ValueError: - logger.error(f"No se pudo extraer un valor numérico de la respuesta: {content}") - return None + pass + + logger.error(f"No se pudo extraer un valor numérico de la respuesta: {content}") + return None except requests.exceptions.RequestException as e: logger.error(f"Error al conectar con la API de Perplexity: {e}") @@ -351,16 +390,85 @@ def generate_prompt(programmer_type, region_code): region_name = region_map.get(region_code, f'región con código {region_code}') programmer_description = programmer_map.get(programmer_type, f'desarrollador {programmer_type}') - prompt = f"¿Cuál es la tarifa por hora promedio en dólares estadounidenses (USD) para un {programmer_description} en {region_name} en 2025? Responde solo con el valor numérico con dos decimales." + # Usamos el año actual para la consulta principal + current_year = time.strftime("%Y") + prompt = f"¿Cuál es la tarifa por hora promedio en dólares estadounidenses (USD) para un {programmer_description} en {region_name} en {current_year}? Si no tienes datos del {current_year}, usa la información más reciente y haz una estimación aproximada. Responde solo con el valor numérico con dos decimales." return prompt +def get_fallback_rate(programmer_type, region_code): + """ + Proporciona una tarifa de respaldo cuando la API falla o no está disponible. + Usa estimaciones basadas en datos de mercado generales. + """ + # Tarifas base por tipo de programador (valores estimados en USD) + base_rates = { + 'bash': 25.00, + 'python': 35.00, + 'fullstack': 45.00, + 'frontend': 40.00, + 'backend': 42.00, + 'devops': 50.00, + 'mobile': 45.00, + 'java': 40.00, + 'php': 30.00, + 'ruby': 40.00, + 'dotnet': 45.00, + 'data': 55.00, + 'ml': 65.00, + 'cloud': 60.00, + 'odoo': 40.00 + } + + # Valor por defecto si el tipo no está en la lista + base_rate = base_rates.get(programmer_type, 35.00) + + # Factores de ajuste por región + region_factors = { + 'ww': 1.0, # Promedio mundial + 'la': 0.7, # Latinoamérica + 'pa': 0.75, # Panamá + 'co': 0.65, # Colombia + 'mx': 0.7, # México + 'ar': 0.6, # Argentina + 'br': 0.7, # Brasil + 'cl': 0.75, # Chile + 'pe': 0.6, # Perú + 'ec': 0.6, # Ecuador + 'us': 1.8, # Estados Unidos + 'ca': 1.6, # Canadá + 'uk': 1.5, # Reino Unido + 'de': 1.6, # Alemania + 'fr': 1.5, # Francia + 'es': 1.3, # España + 'it': 1.3 # Italia + } + + # Si no hay región, devolver la tarifa base + if region_code is None: + return base_rate + + # Aplicar factor regional + factor = region_factors.get(region_code, 1.0) + adjusted_rate = base_rate * factor + + return round(adjusted_rate, 2) + def update_rate_files(): """Actualiza los archivos de tarifas con datos de Perplexity.""" # Obtener modelo configurado model = get_ai_model() logger.info(f"Usando modelo de IA: {model}") + # Verificar disponibilidad de la API + api_available = True + try: + # Intentar obtener la clave API (si falla, saltará una excepción) + get_perplexity_api_key() + except: + api_available = False + logger.warning("API de Perplexity no disponible. Se usarán valores de respaldo.") + # Buscar todos los archivos .rate rate_files = glob.glob(str(CONFIG_DIR / '*.rate')) logger.info(f"Encontrados {len(rate_files)} archivos de tarifas para actualizar.") @@ -370,11 +478,10 @@ def update_rate_files(): max_consecutive_errors = 3 for rate_file in rate_files: - # Si hemos tenido demasiados errores consecutivos, detener el proceso + # Si hemos tenido demasiados errores consecutivos, cambiar a modo de respaldo if consecutive_errors >= max_consecutive_errors: - logger.error(f"Se detectaron {consecutive_errors} errores consecutivos. Deteniendo el proceso.") - logger.error("Verifique su conexión a internet y la API key de Perplexity.") - break + logger.error(f"Se detectaron {consecutive_errors} errores consecutivos. Cambiando a modo de respaldo.") + api_available = False programmer_type, region_code = parse_rate_filename(rate_file) @@ -392,28 +499,41 @@ def update_rate_files(): else: prompt = generate_prompt(programmer_type, region_code) - logger.info(f"Consultando tarifa para {programmer_type}" + - (f" en región {region_code}" if region_code else "")) + # Variable para almacenar la tarifa + rate = None - try: - # Consultar a Perplexity - rate = query_perplexity(prompt, model) + # Si la API está disponible, intentar consultarla + if api_available: + logger.info(f"Consultando tarifa para {programmer_type}" + + (f" en región {region_code}" if region_code else "")) - if rate is not None: - # Guardar el resultado en el archivo - with open(rate_file, 'w') as f: - f.write(f"{rate:.2f}\n") - logger.info(f"Actualizado {os.path.basename(rate_file)} con valor: {rate:.2f}") - consecutive_errors = 0 # Reiniciar contador de errores tras un éxito - else: - logger.error(f"No se pudo obtener la tarifa para {programmer_type}" + - (f" en región {region_code}" if region_code else "")) + try: + # Consultar a Perplexity + rate = query_perplexity(prompt, model) + + if rate is not None: + consecutive_errors = 0 # Reiniciar contador de errores tras un éxito + else: + logger.error(f"No se pudo obtener la tarifa para {programmer_type}" + + (f" en región {region_code}" if region_code else "")) + consecutive_errors += 1 + except Exception as e: + logger.error(f"Error al actualizar {os.path.basename(rate_file)}: {e}") consecutive_errors += 1 - except Exception as e: - logger.error(f"Error al actualizar {os.path.basename(rate_file)}: {e}") - consecutive_errors += 1 - # Pequeña pausa tras un error para evitar sobrecargar la API - time.sleep(2) + # Pequeña pausa tras un error para evitar sobrecargar la API + time.sleep(2) + + # Si la API falló o no está disponible, usar valor de respaldo + if rate is None: + logger.warning(f"Usando valor de respaldo para {programmer_type}" + + (f" en región {region_code}" if region_code else "")) + rate = get_fallback_rate(programmer_type, region_code) + logger.info(f"Valor de respaldo generado: {rate:.2f}") + + # Guardar el resultado en el archivo + with open(rate_file, 'w') as f: + f.write(f"{rate:.2f}\n") + logger.info(f"Actualizado {os.path.basename(rate_file)} con valor: {rate:.2f}") if __name__ == "__main__":