[REFACTORED] Simplificar lógica de cálculo de tarifas en rate_update.py

- Eliminar toda la lógica compleja relacionada con factores de conversión
- Simplificar el proceso de extracción numérica de respuestas de Perplexity
- Usar prompt más directo enfocado específicamente en la tarifa por hora
- Reemplazar función get_fallback_rate por get_default_rate más simple
- Eliminar código innecesario para análisis de períodos de tiempo
- Mantener la lógica central de consulta a la API y extracción de valores

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Mauro Rosero P. 2025-03-12 12:01:40 -05:00
parent fb00f958dc
commit 2aa0ad44d3
Signed by: mrosero
GPG key ID: 83BD2A5F674B7E26

View file

@ -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.")
@ -614,11 +453,6 @@ def update_rate_files():
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)
@ -695,14 +528,7 @@ def generate_prompt_base(programmer_type):
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