[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:
parent
fb00f958dc
commit
2aa0ad44d3
1 changed files with 35 additions and 209 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue