diff --git a/bin/rate_update.py b/bin/rate_update.py index d8cc18e..72386e1 100755 --- a/bin/rate_update.py +++ b/bin/rate_update.py @@ -148,7 +148,7 @@ def get_perplexity_api_key(): return api_key -def query_perplexity(prompt, model="o1"): +def query_perplexity(prompt, model="sonar"): """Realizar una consulta a la API de Perplexity.""" api_key = get_perplexity_api_key() @@ -158,10 +158,7 @@ def query_perplexity(prompt, model="o1"): "Content-Type": "application/json" } - # Verificar los modelos disponibles en Perplexity - 2025 - # Modelos válidos: sonar, mistral-7b, llama-3-sonar-small, llama-3-sonar-medium, llama-3-70b, mixtral-8x7b, codellama-70b - # Usamos el modelo 'sonar' de Perplexity para mejor compatibilidad y resultados - + # Configuración simple y directa para la API data = { "model": model, "messages": [ @@ -182,163 +179,43 @@ def query_perplexity(prompt, model="o1"): logger.info(f"Enviando consulta a Perplexity usando modelo: {model}") logger.info(f"Prompt: {prompt}") - # Convertir el diccionario a JSON para obtener la representación exacta que se enviará - request_body = json.dumps(data, indent=2) - logger.info(f"Cuerpo de la solicitud: {request_body}") - response = requests.post(url, headers=headers, json=data) - # Mostrar información de respuesta - logger.info(f"Código de estado HTTP: {response.status_code}") - logger.info(f"Respuesta (raw): {response.text[:200]}...") # Mostrar los primeros 200 caracteres - - # Si la respuesta no es 200, mostrar el error completo + # Si la respuesta no es 200, mostrar el error if response.status_code != 200: logger.error(f"Error de API Perplexity ({response.status_code}): {response.text}") - # Intentar analizar el error para obtener más información - try: - error_data = response.json() - error_message = error_data.get("error", {}).get("message", "Mensaje de error no disponible") - logger.error(f"Mensaje de error: {error_message}") - except: - pass return None - # Continuar si la respuesta es exitosa + # Extraer el contenido de la respuesta result = response.json() - - # Extraer solo el valor numérico de la respuesta content = result.get('choices', [{}])[0].get('message', {}).get('content', '') logger.info(f"Contenido de la respuesta: {content}") - # Primero, detectar si la respuesta menciona un período de tiempo diferente a hora - period_indicators = { - 'hora': 1, # Factor de conversión a hora (1 hora = 1 hora) - 'día': 1/8, # Asumiendo jornada de 8 horas - 'semana': 1/40, # Asumiendo 40 horas semanales - 'mes': 1/160, # Asumiendo ~160 horas mensuales (40 h/sem * 4 sem) - 'año': 1/2000, # Asumiendo ~2000 horas anuales - 'anual': 1/2000, - 'mensual': 1/160, - 'semanal': 1/40, - 'diario': 1/8, - 'por hora': 1, - 'por día': 1/8, - 'por semana': 1/40, - 'por mes': 1/160, - 'por año': 1/2000, - 'por jornada': 1/8 - } + # Extraer solo el valor numérico, limpiando cualquier formato + content_clean = content.replace(',', '') + match = re.search(r'(\d+\.\d+|\d+)', content_clean) - # Detectar el período mencionado en el texto - time_period_factor = 1 # Por defecto asumimos que el valor ya está por hora - for period, factor in period_indicators.items(): - if period in content.lower(): - time_period_factor = factor - logger.info(f"Detectado período de tiempo: {period} (factor: {factor})") - break - - # 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'\$\s*(\d{1,3}(?:,\d{3})*(?:\.\d{2}))', # $30,000.00 o $30.00 - r'(\d{1,3}(?:,\d{3})*(?:\.\d{2}))\s*USD', # 30,000.00 USD o 30.00 USD - r'\$\s*(\d{1,3}(?:,\d{3})*)', # $30,000 o $30 - r'(\d{1,3}(?:,\d{3})*)\s*USD', # 30,000 USD o 30 USD - r'\$\s*(\d+\.\d{2})', # $30.00 - r'(\d+\.\d{2})\s*USD', # 30.00 USD - r'\$\s*(\d+)', # $30 - r'(\d+)\s*USD', # 30 USD - r'(\d+(?:\.\d+)?)' # Cualquier número con o sin decimales - ] - - for pattern in money_patterns: - match = re.search(pattern, content) - if match: - try: - # Limpiar comas para poder convertir a float - value_str = match.group(1).replace(',', '') - value = float(value_str) - - # Convertir a tarifa por hora según el período detectado - hourly_rate = value * time_period_factor - - logger.info(f"Valor extraído: {value} (periodo factor: {time_period_factor})") - logger.info(f"Convertido a tarifa por hora: {hourly_rate:.2f}") - - return round(hourly_rate, 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{1,3}(?:,\d{3})*(?:\.\d+)?|\d+(?:\.\d+)?', content) - if all_numbers: + if match: try: - # Limpiar comas y convertir a float - cleaned_numbers = [float(n.replace(',', '')) for n in all_numbers] - - # Si hay varios números, elegir el más probable - if len(cleaned_numbers) > 1: - logger.info(f"Múltiples números encontrados: {cleaned_numbers}") - # Eliminamos valores extremadamente altos o bajos según el período de tiempo - - # Definir rangos válidos según el factor de tiempo - min_valid = 5 - max_valid = 500 - - # Ajustar rangos según el período detectado - if time_period_factor == 1/8: # diario - min_valid = 40 - max_valid = 4000 - elif time_period_factor == 1/40: # semanal - min_valid = 200 - max_valid = 20000 - elif time_period_factor == 1/160: # mensual - min_valid = 800 - max_valid = 80000 - elif time_period_factor == 1/2000: # anual - min_valid = 10000 - max_valid = 1000000 - - filtered_numbers = [n for n in cleaned_numbers if min_valid <= n <= max_valid] - - if filtered_numbers: - value = sum(filtered_numbers) / len(filtered_numbers) - else: - # Si no hay valores en el rango, tomar el primer número - value = cleaned_numbers[0] - else: - value = cleaned_numbers[0] - - # Aplicar factor de conversión de tiempo - hourly_rate = value * time_period_factor - - logger.info(f"Valor extraído: {value} (período factor: {time_period_factor})") - logger.info(f"Convertido a tarifa por hora: {hourly_rate:.2f}") - - return round(hourly_rate, 2) + value = float(match.group(1)) + logger.info(f"Valor extraído: {value}") + return round(value, 2) # Asegurar 2 decimales except ValueError: pass - # Estrategia 3: Último recurso, intentar limpiar el texto y extraer un número + # Si no encontramos un número con el patrón regular, intenta limpiar todo try: - # Eliminar cualquier símbolo que no sea dígito o punto cleaned_content = re.sub(r'[^\d.]', '', content) if cleaned_content: value = float(cleaned_content) - - # Aplicar factor de conversión de tiempo - hourly_rate = value * time_period_factor - - logger.info(f"Valor limpiado y extraído: {value} (período factor: {time_period_factor})") - logger.info(f"Convertido a tarifa por hora: {hourly_rate:.2f}") - - return round(hourly_rate, 2) + logger.info(f"Valor limpiado y extraído: {value}") + return round(value, 2) + else: + logger.error(f"No se pudo extraer un valor numérico de la respuesta: {content}") + return None except ValueError: - pass - - logger.error(f"No se pudo extraer un valor numérico de la respuesta: {content}") - return None + 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}") @@ -487,10 +364,9 @@ Responde solo con el valor numérico con dos decimales.""" return prompt -def get_fallback_rate(programmer_type, region_code=None): +def get_default_rate(programmer_type): """ - Proporciona una tarifa de respaldo cuando la API falla o no está disponible. - Usa estimaciones basadas en datos de mercado generales. + Proporciona una tarifa por defecto simple para cuando la API falla. """ # Tarifas base por tipo de programador (valores estimados en USD) base_rates = { @@ -512,39 +388,7 @@ def get_fallback_rate(programmer_type, region_code=None): } # Valor por defecto si el tipo no está en la lista - base_rate = base_rates.get(programmer_type, 35.00) - - # Si no se proporciona región o es None, devolver la tarifa base global (promedio mundial) - if region_code is None: - return base_rate - - # Si se proporciona región, aplicar factor regional - # 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 - } - - # Aplicar factor regional - factor = region_factors.get(region_code, 1.0) - adjusted_rate = base_rate * factor - - return round(adjusted_rate, 2) + return base_rates.get(programmer_type, 35.00) def show_result(message): """ @@ -590,16 +434,11 @@ def update_rate_files(): get_perplexity_api_key() except: api_available = False - logger.warning("API de Perplexity no disponible. Se usarán valores de respaldo.") + logger.warning("API de Perplexity no disponible. Se usarán valores predeterminados.") # Crear la carpeta rates si no existe os.makedirs(RATES_DIR, exist_ok=True) - # Control de errores para limitar los intentos de API - consecutive_errors = 0 - max_consecutive_errors = 3 - rate_threshold = 200.00 # Umbral para considerar una tarifa como excesiva - # Obtener la lista de tipos de programadores programmer_types = get_programmer_types() logger.info(f"Procesando {len(programmer_types)} tipos de programadores.") @@ -613,11 +452,6 @@ def update_rate_files(): if rate_file.exists(): logger.info(f"El archivo {rate_file} ya existe. Saltando.") continue - - # 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. Cambiando a modo de respaldo.") - api_available = False # Generar el prompt para la consulta prompt = generate_prompt_base(programmer_type) @@ -634,32 +468,31 @@ def update_rate_files(): rate = query_perplexity(prompt, model) if rate is not None: - consecutive_errors = 0 # Reiniciar contador de errores tras un éxito + logger.info(f"Tarifa obtenida correctamente: {rate:.2f}") else: logger.error(f"No se pudo obtener la tarifa para {programmer_type}") - consecutive_errors += 1 except Exception as e: logger.error(f"Error al consultar tarifa para {programmer_type}: {e}") - consecutive_errors += 1 # 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 + # Si la API falló o no está disponible, usar valor predeterminado if rate is None: - logger.warning(f"Usando valor de respaldo para {programmer_type}") - rate = get_fallback_rate(programmer_type, None) - logger.info(f"Valor de respaldo generado: {rate:.2f}") + logger.warning(f"Usando valor predeterminado para {programmer_type}") + rate = get_default_rate(programmer_type) + logger.info(f"Valor predeterminado: {rate:.2f}") - # Comprobar si la tarifa es superior al umbral establecido - if rate > rate_threshold: - logger.warning(f"Tarifa {rate:.2f} supera el umbral de {rate_threshold}. Ajustando...") - rate = min(rate, rate_threshold) + # Limitar a tarifas menores de 200 USD/hora + max_rate = 200.00 + if rate > max_rate: + logger.warning(f"Ajustando tarifa {rate:.2f} al máximo permitido de {max_rate}") + rate = max_rate # Guardar el resultado en el archivo - solo el valor numérico con dos decimales, sin salto de línea with open(rate_file, 'w', encoding='utf-8') as f: f.write(f"{rate:.2f}") - # Registrar en el log y mostrar el resultado en la consola + # Mostrar el resultado en la consola result_message = f"Tarifa para {programmer_type}: {rate:.2f} USD/hora" logger.info(f"Creado archivo {rate_file} con valor: {rate:.2f}") show_result(result_message) @@ -694,15 +527,8 @@ def generate_prompt_base(programmer_type): # Usamos el año actual para la consulta principal current_year = time.strftime("%Y") prompt = f"""¿Cuál es la tarifa POR HORA promedio mundial en dólares estadounidenses (USD) para un {programmer_description} en {current_year}? - -Si no tienes datos del {current_year}, usa la información más reciente y haz una estimación aproximada. -IMPORTANTE: -1. Necesito ESPECÍFICAMENTE el valor POR HORA, no mensual ni anual. -2. Si encuentras información en otros períodos de tiempo (mensual, anual, etc.), conviértela a tarifa POR HORA. -3. Busca un promedio mundial, con conocimiento de valores actuales de mercado. - -Responde ÚNICAMENTE con el valor numérico con dos decimales.""" +IMPORTANTE: Responde ÚNICAMENTE con el valor numérico con dos decimales. Es ESENCIAL que sea el valor POR HORA.""" return prompt