[IMPROVED] Mejorar estimación de tarifas en rate_update.py
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
1617385d3d
commit
a3e9ec89d2
1 changed files with 156 additions and 36 deletions
|
@ -199,23 +199,62 @@ def query_perplexity(prompt, model="o1"):
|
||||||
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}")
|
||||||
|
|
||||||
# Intentar encontrar un número con 2 decimales en la respuesta
|
# Estrategia 1: Buscar patrones específicos de dinero (con y sin símbolo de dólar)
|
||||||
match = re.search(r'\$?(\d+\.\d{2})', content)
|
# Buscar un patrón como $30.00, 30.00, $30, o 30 USD
|
||||||
if match:
|
money_patterns = [
|
||||||
value = float(match.group(1))
|
r'\$(\d+\.\d{2})', # $30.00
|
||||||
logger.info(f"Valor extraído: {value}")
|
r'(\d+\.\d{2})\s*USD', # 30.00 USD
|
||||||
return value
|
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:
|
try:
|
||||||
# Eliminar cualquier símbolo de moneda y espacios
|
# Eliminar cualquier símbolo de moneda y espacios
|
||||||
cleaned_content = re.sub(r'[^\d.]', '', content)
|
cleaned_content = re.sub(r'[^\d.]', '', content)
|
||||||
value = round(float(cleaned_content), 2)
|
if cleaned_content:
|
||||||
logger.info(f"Valor limpiado y extraído: {value}")
|
value = round(float(cleaned_content), 2)
|
||||||
return value
|
logger.info(f"Valor limpiado y extraído: {value}")
|
||||||
|
return value
|
||||||
except ValueError:
|
except ValueError:
|
||||||
logger.error(f"No se pudo extraer un valor numérico de la respuesta: {content}")
|
pass
|
||||||
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}")
|
||||||
|
@ -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}')
|
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}')
|
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
|
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():
|
def update_rate_files():
|
||||||
"""Actualiza los archivos de tarifas con datos de Perplexity."""
|
"""Actualiza los archivos de tarifas con datos de Perplexity."""
|
||||||
# Obtener modelo configurado
|
# Obtener modelo configurado
|
||||||
model = get_ai_model()
|
model = get_ai_model()
|
||||||
logger.info(f"Usando modelo de IA: {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
|
# Buscar todos los archivos .rate
|
||||||
rate_files = glob.glob(str(CONFIG_DIR / '*.rate'))
|
rate_files = glob.glob(str(CONFIG_DIR / '*.rate'))
|
||||||
logger.info(f"Encontrados {len(rate_files)} archivos de tarifas para actualizar.")
|
logger.info(f"Encontrados {len(rate_files)} archivos de tarifas para actualizar.")
|
||||||
|
@ -370,11 +478,10 @@ def update_rate_files():
|
||||||
max_consecutive_errors = 3
|
max_consecutive_errors = 3
|
||||||
|
|
||||||
for rate_file in rate_files:
|
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:
|
if consecutive_errors >= max_consecutive_errors:
|
||||||
logger.error(f"Se detectaron {consecutive_errors} errores consecutivos. Deteniendo el proceso.")
|
logger.error(f"Se detectaron {consecutive_errors} errores consecutivos. Cambiando a modo de respaldo.")
|
||||||
logger.error("Verifique su conexión a internet y la API key de Perplexity.")
|
api_available = False
|
||||||
break
|
|
||||||
|
|
||||||
programmer_type, region_code = parse_rate_filename(rate_file)
|
programmer_type, region_code = parse_rate_filename(rate_file)
|
||||||
|
|
||||||
|
@ -392,28 +499,41 @@ def update_rate_files():
|
||||||
else:
|
else:
|
||||||
prompt = generate_prompt(programmer_type, region_code)
|
prompt = generate_prompt(programmer_type, region_code)
|
||||||
|
|
||||||
logger.info(f"Consultando tarifa para {programmer_type}" +
|
# Variable para almacenar la tarifa
|
||||||
(f" en región {region_code}" if region_code else ""))
|
rate = None
|
||||||
|
|
||||||
try:
|
# Si la API está disponible, intentar consultarla
|
||||||
# Consultar a Perplexity
|
if api_available:
|
||||||
rate = query_perplexity(prompt, model)
|
logger.info(f"Consultando tarifa para {programmer_type}" +
|
||||||
|
(f" en región {region_code}" if region_code else ""))
|
||||||
|
|
||||||
if rate is not None:
|
try:
|
||||||
# Guardar el resultado en el archivo
|
# Consultar a Perplexity
|
||||||
with open(rate_file, 'w') as f:
|
rate = query_perplexity(prompt, model)
|
||||||
f.write(f"{rate:.2f}\n")
|
|
||||||
logger.info(f"Actualizado {os.path.basename(rate_file)} con valor: {rate:.2f}")
|
if rate is not None:
|
||||||
consecutive_errors = 0 # Reiniciar contador de errores tras un éxito
|
consecutive_errors = 0 # Reiniciar contador de errores tras un éxito
|
||||||
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}" +
|
||||||
(f" en región {region_code}" if region_code else ""))
|
(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
|
consecutive_errors += 1
|
||||||
except Exception as e:
|
# Pequeña pausa tras un error para evitar sobrecargar la API
|
||||||
logger.error(f"Error al actualizar {os.path.basename(rate_file)}: {e}")
|
time.sleep(2)
|
||||||
consecutive_errors += 1
|
|
||||||
# Pequeña pausa tras un error para evitar sobrecargar la API
|
# Si la API falló o no está disponible, usar valor de respaldo
|
||||||
time.sleep(2)
|
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__":
|
if __name__ == "__main__":
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue