[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 return api_key
def query_perplexity(prompt, model="o1"): def query_perplexity(prompt, model="sonar"):
"""Realizar una consulta a la API de Perplexity.""" """Realizar una consulta a la API de Perplexity."""
api_key = get_perplexity_api_key() api_key = get_perplexity_api_key()
@ -158,10 +158,7 @@ def query_perplexity(prompt, model="o1"):
"Content-Type": "application/json" "Content-Type": "application/json"
} }
# Verificar los modelos disponibles en Perplexity - 2025 # Configuración simple y directa para la API
# 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
data = { data = {
"model": model, "model": model,
"messages": [ "messages": [
@ -182,163 +179,43 @@ def query_perplexity(prompt, model="o1"):
logger.info(f"Enviando consulta a Perplexity usando modelo: {model}") logger.info(f"Enviando consulta a Perplexity usando modelo: {model}")
logger.info(f"Prompt: {prompt}") 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) response = requests.post(url, headers=headers, json=data)
# Mostrar información de respuesta # Si la respuesta no es 200, mostrar el error
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
if response.status_code != 200: if response.status_code != 200:
logger.error(f"Error de API Perplexity ({response.status_code}): {response.text}") 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 return None
# Continuar si la respuesta es exitosa # Extraer el contenido de la respuesta
result = response.json() result = response.json()
# Extraer solo el valor numérico de la respuesta
content = result.get('choices', [{}])[0].get('message', {}).get('content', '') content = result.get('choices', [{}])[0].get('message', {}).get('content', '')
logger.info(f"Contenido de la respuesta: {content}") logger.info(f"Contenido de la respuesta: {content}")
# Primero, detectar si la respuesta menciona un período de tiempo diferente a hora # Extraer solo el valor numérico, limpiando cualquier formato
period_indicators = { content_clean = content.replace(',', '')
'hora': 1, # Factor de conversión a hora (1 hora = 1 hora) match = re.search(r'(\d+\.\d+|\d+)', content_clean)
'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
}
# Detectar el período mencionado en el texto if match:
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:
try: try:
# Limpiar comas y convertir a float value = float(match.group(1))
cleaned_numbers = [float(n.replace(',', '')) for n in all_numbers] logger.info(f"Valor extraído: {value}")
return round(value, 2) # Asegurar 2 decimales
# 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)
except ValueError: except ValueError:
pass 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: try:
# Eliminar cualquier símbolo que no sea dígito o punto
cleaned_content = re.sub(r'[^\d.]', '', content) cleaned_content = re.sub(r'[^\d.]', '', content)
if cleaned_content: if cleaned_content:
value = float(cleaned_content) value = float(cleaned_content)
logger.info(f"Valor limpiado y extraído: {value}")
# Aplicar factor de conversión de tiempo return round(value, 2)
hourly_rate = value * time_period_factor else:
logger.error(f"No se pudo extraer un valor numérico de la respuesta: {content}")
logger.info(f"Valor limpiado y extraído: {value} (período factor: {time_period_factor})") return None
logger.info(f"Convertido a tarifa por hora: {hourly_rate:.2f}")
return round(hourly_rate, 2)
except ValueError: 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: except requests.exceptions.RequestException as e:
logger.error(f"Error al conectar con la API de Perplexity: {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 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. Proporciona una tarifa por defecto simple para cuando la API falla.
Usa estimaciones basadas en datos de mercado generales.
""" """
# Tarifas base por tipo de programador (valores estimados en USD) # Tarifas base por tipo de programador (valores estimados en USD)
base_rates = { 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 # Valor por defecto si el tipo no está en la lista
base_rate = base_rates.get(programmer_type, 35.00) return 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)
def show_result(message): def show_result(message):
""" """
@ -590,16 +434,11 @@ def update_rate_files():
get_perplexity_api_key() get_perplexity_api_key()
except: except:
api_available = False 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 # Crear la carpeta rates si no existe
os.makedirs(RATES_DIR, exist_ok=True) 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 # Obtener la lista de tipos de programadores
programmer_types = get_programmer_types() programmer_types = get_programmer_types()
logger.info(f"Procesando {len(programmer_types)} tipos de programadores.") logger.info(f"Procesando {len(programmer_types)} tipos de programadores.")
@ -613,11 +452,6 @@ def update_rate_files():
if rate_file.exists(): if rate_file.exists():
logger.info(f"El archivo {rate_file} ya existe. Saltando.") logger.info(f"El archivo {rate_file} ya existe. Saltando.")
continue 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 # Generar el prompt para la consulta
prompt = generate_prompt_base(programmer_type) prompt = generate_prompt_base(programmer_type)
@ -634,32 +468,31 @@ def update_rate_files():
rate = query_perplexity(prompt, model) rate = query_perplexity(prompt, model)
if rate is not None: if rate is not None:
consecutive_errors = 0 # Reiniciar contador de errores tras un éxito logger.info(f"Tarifa obtenida correctamente: {rate:.2f}")
else: else:
logger.error(f"No se pudo obtener la tarifa para {programmer_type}") logger.error(f"No se pudo obtener la tarifa para {programmer_type}")
consecutive_errors += 1
except Exception as e: except Exception as e:
logger.error(f"Error al consultar tarifa para {programmer_type}: {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 # Pequeña pausa tras un error para evitar sobrecargar la API
time.sleep(2) 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: if rate is None:
logger.warning(f"Usando valor de respaldo para {programmer_type}") logger.warning(f"Usando valor predeterminado para {programmer_type}")
rate = get_fallback_rate(programmer_type, None) rate = get_default_rate(programmer_type)
logger.info(f"Valor de respaldo generado: {rate:.2f}") logger.info(f"Valor predeterminado: {rate:.2f}")
# Comprobar si la tarifa es superior al umbral establecido # Limitar a tarifas menores de 200 USD/hora
if rate > rate_threshold: max_rate = 200.00
logger.warning(f"Tarifa {rate:.2f} supera el umbral de {rate_threshold}. Ajustando...") if rate > max_rate:
rate = min(rate, rate_threshold) 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 # 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: with open(rate_file, 'w', encoding='utf-8') as f:
f.write(f"{rate:.2f}") 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" result_message = f"Tarifa para {programmer_type}: {rate:.2f} USD/hora"
logger.info(f"Creado archivo {rate_file} con valor: {rate:.2f}") logger.info(f"Creado archivo {rate_file} con valor: {rate:.2f}")
show_result(result_message) show_result(result_message)
@ -694,15 +527,8 @@ def generate_prompt_base(programmer_type):
# Usamos el año actual para la consulta principal # Usamos el año actual para la consulta principal
current_year = time.strftime("%Y") 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}? 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: IMPORTANTE: Responde ÚNICAMENTE con el valor numérico con dos decimales. Es ESENCIAL que sea el valor POR HORA."""
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."""
return prompt return prompt