Compare commits
	
		
			No commits in common. "0efd6cf2b8cabdacf8948fca185b3de8581ce13c" and "233fda566146a3692c1bb1bbdbbe4039161a2ab1" have entirely different histories.
		
	
	
		
			0efd6cf2b8
			...
			233fda5661
		
	
		
					 11 changed files with 71 additions and 829 deletions
				
			
		
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							|  | @ -23,7 +23,6 @@ | ||||||
| 
 | 
 | ||||||
| # Ignorar archivos específicos | # Ignorar archivos específicos | ||||||
| CLAUDE.md | CLAUDE.md | ||||||
| bin/config/rates/ |  | ||||||
| 
 | 
 | ||||||
| # Permitir archivos en carpetas específicas | # Permitir archivos en carpetas específicas | ||||||
| # !carpeta1/*.txt | # !carpeta1/*.txt | ||||||
|  |  | ||||||
							
								
								
									
										1
									
								
								bin/config/bash_la.rate
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								bin/config/bash_la.rate
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1 @@ | ||||||
|  | 20.00 | ||||||
							
								
								
									
										1
									
								
								bin/config/bash_ww.rate
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								bin/config/bash_ww.rate
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1 @@ | ||||||
|  | 30.00 | ||||||
							
								
								
									
										1
									
								
								bin/config/fullstack_la.rate
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								bin/config/fullstack_la.rate
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1 @@ | ||||||
|  | 100.00 | ||||||
							
								
								
									
										1
									
								
								bin/config/fullstack_ww.rate
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								bin/config/fullstack_ww.rate
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1 @@ | ||||||
|  | 200.00 | ||||||
							
								
								
									
										1
									
								
								bin/config/kdevs.rate
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								bin/config/kdevs.rate
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1 @@ | ||||||
|  | 3 | ||||||
							
								
								
									
										1
									
								
								bin/config/python_la.rate
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								bin/config/python_la.rate
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1 @@ | ||||||
|  | 30.00 | ||||||
							
								
								
									
										1
									
								
								bin/config/python_ww.rate
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								bin/config/python_ww.rate
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1 @@ | ||||||
|  | 100.00 | ||||||
|  | @ -1 +0,0 @@ | ||||||
| sonar |  | ||||||
|  | @ -1,2 +0,0 @@ | ||||||
| What is the average hourly rate in USD for a [developer type] developer, expressed as a numerical value with two decimal places? |  | ||||||
| Replace [developer type] with the desired specialization, such as "Python" or "JavaScript". |  | ||||||
|  | @ -1,5 +1,4 @@ | ||||||
| #!/usr/bin/env python3 | #!/usr/bin/env python3 | ||||||
| # -*- coding: utf-8 -*- |  | ||||||
| """ | """ | ||||||
| Rate Update Script | Rate Update Script | ||||||
| 
 | 
 | ||||||
|  | @ -19,22 +18,15 @@ import requests | ||||||
| import sys | import sys | ||||||
| import subprocess | import subprocess | ||||||
| import importlib.util | import importlib.util | ||||||
| import time |  | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| 
 | 
 | ||||||
| # Configuración de logging | # Configuración de logging | ||||||
| # Por defecto, establecemos nivel WARNING para la consola (solo errores y advertencias) | logging.basicConfig( | ||||||
| console_handler = logging.StreamHandler() |     level=logging.INFO, | ||||||
| console_handler.setLevel(logging.WARNING) |     format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', | ||||||
| console_handler.setFormatter(logging.Formatter('%(levelname)s: %(message)s')) |     handlers=[logging.StreamHandler()] | ||||||
| 
 | ) | ||||||
| # Configuración global del logger |  | ||||||
| logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[]) |  | ||||||
| logger = logging.getLogger('rate_update') | logger = logging.getLogger('rate_update') | ||||||
| logger.addHandler(console_handler) |  | ||||||
| 
 |  | ||||||
| # Variable para controlar si se muestra el resultado en la consola |  | ||||||
| SHOW_RESULTS = True |  | ||||||
| 
 | 
 | ||||||
| # Verificar si pycountry está instalado, si no, instalarlo | # Verificar si pycountry está instalado, si no, instalarlo | ||||||
| def check_install_pycountry(): | def check_install_pycountry(): | ||||||
|  | @ -85,96 +77,30 @@ def check_install_pycountry(): | ||||||
| # Directorio base del proyecto | # Directorio base del proyecto | ||||||
| BASE_DIR = Path(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | BASE_DIR = Path(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | ||||||
| CONFIG_DIR = BASE_DIR / 'bin' / 'config' | CONFIG_DIR = BASE_DIR / 'bin' / 'config' | ||||||
| RATES_DIR = CONFIG_DIR / 'rates'  # Nueva ubicación para archivos de tarifas |  | ||||||
| 
 | 
 | ||||||
| # Archivos de configuración para la API de Perplexity | # Archivo de configuración para el modelo de IA | ||||||
| MODEL_CONFIG_FILE = CONFIG_DIR / 'rate_model.ai' | MODEL_CONFIG_FILE = CONFIG_DIR / 'rate_model.ai' | ||||||
| TEMPERATURE_CONFIG_FILE = CONFIG_DIR / 'rate_temperature.ai' |  | ||||||
| 
 | 
 | ||||||
| def get_ai_model(): | def get_ai_model(): | ||||||
|     """Obtener el modelo de IA configurado.""" |     """Obtener el modelo de IA configurado.""" | ||||||
|     try: |     try: | ||||||
|         with open(MODEL_CONFIG_FILE, 'r', encoding='utf-8') as f: |         with open(MODEL_CONFIG_FILE, 'r') as f: | ||||||
|             model = f.read().strip() |             return f.read().strip() | ||||||
|             # Si hay un valor específico configurado, usarlo |  | ||||||
|             if model and model.strip(): |  | ||||||
|                 return model |  | ||||||
|     except FileNotFoundError: |     except FileNotFoundError: | ||||||
|         logger.warning(f"Archivo de configuración {MODEL_CONFIG_FILE} no encontrado. Usando modelo predeterminado 'sonar'.") |         logger.warning(f"Archivo de configuración {MODEL_CONFIG_FILE} no encontrado. Usando modelo predeterminado 'o1'.") | ||||||
|      |         return "o1" | ||||||
|     # Usar 'sonar' como modelo predeterminado (el más adecuado para la API de Perplexity) |  | ||||||
|     return "sonar" |  | ||||||
| 
 |  | ||||||
| def get_ai_temperature(): |  | ||||||
|     """Obtener la temperatura configurada para la API de Perplexity.""" |  | ||||||
|     try: |  | ||||||
|         with open(TEMPERATURE_CONFIG_FILE, 'r', encoding='utf-8') as f: |  | ||||||
|             temp_str = f.read().strip() |  | ||||||
|             # Si hay un valor específico configurado, intentar convertirlo a float |  | ||||||
|             if temp_str and temp_str.strip(): |  | ||||||
|                 try: |  | ||||||
|                     temp = float(temp_str) |  | ||||||
|                     # Validar que está en el rango correcto (0.0 a 1.0) |  | ||||||
|                     if 0.0 <= temp <= 1.0: |  | ||||||
|                         return temp |  | ||||||
|                     else: |  | ||||||
|                         logger.warning(f"Temperatura fuera de rango en {TEMPERATURE_CONFIG_FILE}: {temp}. Usando valor predeterminado 0.3.") |  | ||||||
|                 except ValueError: |  | ||||||
|                     logger.warning(f"Valor no numérico en {TEMPERATURE_CONFIG_FILE}: {temp_str}. Usando valor predeterminado 0.3.") |  | ||||||
|     except FileNotFoundError: |  | ||||||
|         logger.info(f"Archivo de configuración {TEMPERATURE_CONFIG_FILE} no encontrado. Usando temperatura predeterminada 0.3.") |  | ||||||
|      |  | ||||||
|     # Usar 0.3 como temperatura predeterminada |  | ||||||
|     return 0.3 |  | ||||||
| 
 | 
 | ||||||
| def get_perplexity_api_key(): | def get_perplexity_api_key(): | ||||||
|     """Obtener la clave API de Perplexity desde una variable de entorno o archivo.""" |     """Obtener la clave API de Perplexity desde una variable de entorno.""" | ||||||
|     # Intentar obtener la clave de la variable de entorno |  | ||||||
|     api_key = os.environ.get('PERPLEXITY_API_KEY') |     api_key = os.environ.get('PERPLEXITY_API_KEY') | ||||||
|      |  | ||||||
|     # Si no está en la variable de entorno, intentar cargarla desde un archivo |  | ||||||
|     if not api_key: |     if not api_key: | ||||||
|         # Rutas posibles para el archivo de API key |         logger.error("No se encontró la clave API de Perplexity. Establezca la variable de entorno PERPLEXITY_API_KEY.") | ||||||
|         possible_paths = [ |  | ||||||
|             os.path.expanduser('~/.perplexity/api_key'), |  | ||||||
|             os.path.expanduser('~/.config/perplexity/api_key'), |  | ||||||
|             os.path.join(BASE_DIR, 'bin', 'config', 'perplexity_api_key') |  | ||||||
|         ] |  | ||||||
|          |  | ||||||
|         for path in possible_paths: |  | ||||||
|             try: |  | ||||||
|                 if os.path.exists(path): |  | ||||||
|                     logger.info(f"Intentando cargar API key desde: {path}") |  | ||||||
|                     with open(path, 'r', encoding='utf-8') as f: |  | ||||||
|                         api_key = f.read().strip() |  | ||||||
|                         if api_key: |  | ||||||
|                             logger.info("API key de Perplexity cargada desde archivo.") |  | ||||||
|                             break |  | ||||||
|             except Exception as e: |  | ||||||
|                 logger.warning(f"Error al leer el archivo de API key {path}: {e}") |  | ||||||
|                 continue |  | ||||||
|      |  | ||||||
|     # Si seguimos sin tener API key, mostrar error y salir |  | ||||||
|     if not api_key: |  | ||||||
|         logger.error("No se encontró la clave API de Perplexity.") |  | ||||||
|         logger.error("Opciones para configurarla:") |  | ||||||
|         logger.error("1. Establecer la variable de entorno PERPLEXITY_API_KEY") |  | ||||||
|         logger.error("2. Crear un archivo ~/.perplexity/api_key con la clave") |  | ||||||
|         logger.error("3. Crear un archivo ~/.config/perplexity/api_key con la clave") |  | ||||||
|         logger.error("4. Crear un archivo bin/config/perplexity_api_key en el directorio del proyecto") |  | ||||||
|         sys.exit(1) |         sys.exit(1) | ||||||
|          |  | ||||||
|     # Validar formato básico de la API key |  | ||||||
|     if not api_key.startswith('pplx-'): |  | ||||||
|         logger.warning("El formato de la API key de Perplexity parece incorrecto.") |  | ||||||
|         logger.warning("Las API keys de Perplexity suelen comenzar con 'pplx-'.") |  | ||||||
|          |  | ||||||
|     return api_key |     return api_key | ||||||
| 
 | 
 | ||||||
| def query_perplexity(prompt, model="sonar"): | def query_perplexity(prompt, model="o1"): | ||||||
|     """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() | ||||||
|     temperature = get_ai_temperature()  # Obtener temperatura configurada |  | ||||||
|      |      | ||||||
|     url = "https://api.perplexity.ai/chat/completions" |     url = "https://api.perplexity.ai/chat/completions" | ||||||
|     headers = { |     headers = { | ||||||
|  | @ -182,62 +108,38 @@ def query_perplexity(prompt, model="sonar"): | ||||||
|         "Content-Type": "application/json" |         "Content-Type": "application/json" | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     # Configuración simple y directa para la API |  | ||||||
|     data = { |     data = { | ||||||
|         "model": model, |         "model": model, | ||||||
|         "messages": [ |         "messages": [ | ||||||
|             { |             { | ||||||
|                 "role": "system", |                 "role": "system", | ||||||
|                 "content": "You are a helpful assistant that provides accurate salary information for developers worldwide. Respond only with numeric values in USD with 2 decimal places. Do not include any text, currency symbols, or other characters." |                 "content": "You are a helpful assistant that provides accurate salary information for developers worldwide. Respond only with numeric values in USD with 2 decimal places." | ||||||
|             }, |             }, | ||||||
|             { |             { | ||||||
|                 "role": "user", |                 "role": "user", | ||||||
|                 "content": prompt |                 "content": prompt | ||||||
|             } |             } | ||||||
|         ], |         ], | ||||||
|         "max_tokens": 100, |         "max_tokens": 100 | ||||||
|         "temperature": temperature  # Usar temperatura configurada para respuestas |  | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     try: |     try: | ||||||
|         logger.info(f"Enviando consulta a Perplexity usando modelo: {model}") |  | ||||||
|         logger.info(f"Temperatura configurada: {temperature}") |  | ||||||
|         logger.info(f"Prompt: {prompt}") |  | ||||||
|          |  | ||||||
|         response = requests.post(url, headers=headers, json=data) |         response = requests.post(url, headers=headers, json=data) | ||||||
|          |         response.raise_for_status() | ||||||
|         # 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}") |  | ||||||
|             return None |  | ||||||
|          |  | ||||||
|         # 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}") |         # Intentar encontrar un número con 2 decimales en la respuesta | ||||||
|          |         match = re.search(r'\$?(\d+\.\d{2})', content) | ||||||
|         # Extraer solo el valor numérico, limpiando cualquier formato |  | ||||||
|         content_clean = content.replace(',', '') |  | ||||||
|         match = re.search(r'(\d+\.\d+|\d+)', content_clean) |  | ||||||
|          |  | ||||||
|         if match: |         if match: | ||||||
|             try: |             return float(match.group(1)) | ||||||
|                 value = float(match.group(1)) |  | ||||||
|                 logger.info(f"Valor extraído: {value}") |  | ||||||
|                 return round(value, 2)  # Asegurar 2 decimales |  | ||||||
|             except ValueError: |  | ||||||
|                 pass |  | ||||||
|          |          | ||||||
|         # Si no encontramos un número con el patrón regular, intenta limpiar todo |         # Si no encuentra un formato exacto, intentar convertir toda la respuesta a float | ||||||
|         try: |         try: | ||||||
|  |             # Eliminar cualquier símbolo de moneda y espacios | ||||||
|             cleaned_content = re.sub(r'[^\d.]', '', content) |             cleaned_content = re.sub(r'[^\d.]', '', content) | ||||||
|             if cleaned_content: |             return round(float(cleaned_content), 2) | ||||||
|                 value = float(cleaned_content) |  | ||||||
|                 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: |         except ValueError: | ||||||
|             logger.error(f"No se pudo extraer un valor numérico de la respuesta: {content}") |             logger.error(f"No se pudo extraer un valor numérico de la respuesta: {content}") | ||||||
|             return None |             return None | ||||||
|  | @ -245,9 +147,6 @@ def query_perplexity(prompt, model="sonar"): | ||||||
|     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}") | ||||||
|         return None |         return None | ||||||
|     except Exception as e: |  | ||||||
|         logger.error(f"Error inesperado en la consulta a Perplexity: {e}") |  | ||||||
|         return None |  | ||||||
| 
 | 
 | ||||||
| # Variable global para indicar si pycountry está disponible | # Variable global para indicar si pycountry está disponible | ||||||
| pycountry_available = False | pycountry_available = False | ||||||
|  | @ -350,10 +249,9 @@ def generate_prompt(programmer_type, region_code): | ||||||
|         'it': 'Italia' |         'it': 'Italia' | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     # Ya no tenemos archivos especiales como kdevs.rate |     # Archivos especiales como kdevs.rate se manejan de forma diferente | ||||||
|     # Este bloque solo debe ejecutarse para tipos especiales que no siguen el patrón normal |     if programmer_type == 'kdevs': | ||||||
|     # Como ya no tenemos kdevs.rate, esta sección no se ejecutará |         # Simplemente mantener el valor actual para kdevs.rate | ||||||
|     if programmer_type in ['kdevs', 'special_case']: |  | ||||||
|         return None |         return None | ||||||
|      |      | ||||||
|     programmer_map = { |     programmer_map = { | ||||||
|  | @ -377,728 +275,69 @@ 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}') | ||||||
|      |      | ||||||
|     # Usamos el año actual para la consulta principal |     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." | ||||||
|     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.  |  | ||||||
| 
 |  | ||||||
| IMPORTANTE: Si encuentras información en otros períodos de tiempo (mensual, anual, etc.), conviértela a tarifa POR HORA. |  | ||||||
| 
 |  | ||||||
| Responde solo con el valor numérico con dos decimales.""" |  | ||||||
|      |      | ||||||
|     return prompt |     return prompt | ||||||
| 
 | 
 | ||||||
| def get_default_rate(programmer_type): | def update_rate_files(): | ||||||
|     """ |     """Actualiza los archivos de tarifas con datos de Perplexity.""" | ||||||
|     Proporciona una tarifa por defecto simple para cuando la API falla. |  | ||||||
|     """ |  | ||||||
|     # Tarifas base por tipo de programador (valores estimados en USD) |  | ||||||
|     base_rates = { |  | ||||||
|         '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 |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     # Cálculo especial para bash: 0.4 veces la tarifa de devops |  | ||||||
|     if programmer_type == 'bash': |  | ||||||
|         return 0.4 * base_rates['devops'] |  | ||||||
|      |  | ||||||
|     # Valor por defecto si el tipo no está en la lista |  | ||||||
|     return base_rates.get(programmer_type, 35.00) |  | ||||||
| 
 |  | ||||||
| def show_result(message): |  | ||||||
|     """ |  | ||||||
|     Función para mostrar resultados en la consola. |  | ||||||
|     Solo muestra mensajes si SHOW_RESULTS es True. |  | ||||||
|     """ |  | ||||||
|     if SHOW_RESULTS: |  | ||||||
|         print(message) |  | ||||||
| 
 |  | ||||||
| def get_programmer_types(): |  | ||||||
|     """Retorna una lista de todos los tipos de programadores soportados.""" |  | ||||||
|     return [ |  | ||||||
|         'bash', |  | ||||||
|         'python', |  | ||||||
|         'fullstack', |  | ||||||
|         'frontend', |  | ||||||
|         'backend', |  | ||||||
|         'devops', |  | ||||||
|         'mobile', |  | ||||||
|         'java', |  | ||||||
|         'php', |  | ||||||
|         'ruby', |  | ||||||
|         'dotnet', |  | ||||||
|         'data', |  | ||||||
|         'ml', |  | ||||||
|         'cloud', |  | ||||||
|         'odoo' |  | ||||||
|     ] |  | ||||||
| 
 |  | ||||||
| def update_single_rate(programmer_type): |  | ||||||
|     """ |  | ||||||
|     Actualiza la tarifa para un tipo específico de programador. |  | ||||||
|     """ |  | ||||||
|     # Verificar que el tipo de programador sea válido |  | ||||||
|     if programmer_type not in get_programmer_types(): |  | ||||||
|         logger.error(f"Tipo de programador no válido: {programmer_type}") |  | ||||||
|         print(f"ERROR: Tipo de programador no válido: {programmer_type}") |  | ||||||
|         print(f"Tipos válidos: {', '.join(get_programmer_types())}") |  | ||||||
|         return False |  | ||||||
|      |  | ||||||
|     # 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 |     # Buscar todos los archivos .rate | ||||||
|     api_available = True |     rate_files = glob.glob(str(CONFIG_DIR / '*.rate')) | ||||||
|     try: |     logger.info(f"Encontrados {len(rate_files)} archivos de tarifas para actualizar.") | ||||||
|         # 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 predeterminados.") |  | ||||||
|      |      | ||||||
|     # Crear la carpeta rates si no existe |     for rate_file in rate_files: | ||||||
|     os.makedirs(RATES_DIR, exist_ok=True) |         programmer_type, region_code = parse_rate_filename(rate_file) | ||||||
|          |          | ||||||
|     # Diccionario para almacenar las tarifas calculadas y existentes |         if programmer_type is None: | ||||||
|     rates_dict = {} |             logger.warning(f"No se pudo analizar el nombre del archivo: {rate_file}, saltando.") | ||||||
|      |  | ||||||
|     # Si es fullstack, necesitamos primero obtener el valor más alto de todos los tipos |  | ||||||
|     if programmer_type == 'fullstack': |  | ||||||
|         # Leer las tarifas existentes |  | ||||||
|         for prog_type in get_programmer_types(): |  | ||||||
|             if prog_type != 'fullstack': |  | ||||||
|                 rate_file = RATES_DIR / f"{prog_type}.rate" |  | ||||||
|                 if rate_file.exists(): |  | ||||||
|                     try: |  | ||||||
|                         with open(rate_file, 'r', encoding='utf-8') as f: |  | ||||||
|                             rates_dict[prog_type] = float(f.read().strip()) |  | ||||||
|                             logger.info(f"Tarifa leída para {prog_type}: {rates_dict[prog_type]:.2f}") |  | ||||||
|                     except (FileNotFoundError, ValueError) as e: |  | ||||||
|                         logger.warning(f"Error al leer tarifa de {prog_type}: {e}") |  | ||||||
|      |  | ||||||
|     # Si es bash, necesitamos primero obtener o actualizar la tarifa de devops |  | ||||||
|     devops_rate = None |  | ||||||
|     if programmer_type == 'bash': |  | ||||||
|         logger.info("Actualizando tarifa de devops primero para calcular bash") |  | ||||||
|         devops_rate_file = RATES_DIR / "devops.rate" |  | ||||||
|          |  | ||||||
|         # Comprobar si existe un archivo de devops y si debemos actualizarlo |  | ||||||
|         if devops_rate_file.exists(): |  | ||||||
|             # Leer la tarifa guardada de devops |  | ||||||
|             try: |  | ||||||
|                 with open(devops_rate_file, 'r', encoding='utf-8') as f: |  | ||||||
|                     devops_rate = float(f.read().strip()) |  | ||||||
|                     rates_dict['devops'] = devops_rate |  | ||||||
|                     logger.info(f"Tarifa leída para devops: {devops_rate:.2f}") |  | ||||||
|             except (FileNotFoundError, ValueError) as e: |  | ||||||
|                 logger.error(f"Error al leer tarifa de devops: {e}") |  | ||||||
|                 # Si hay error de lectura, intentamos actualizar la tarifa |  | ||||||
|                 devops_rate = None |  | ||||||
|          |  | ||||||
|         # Si no existe o hubo error de lectura, actualizamos la tarifa de devops |  | ||||||
|         if devops_rate is None: |  | ||||||
|             prompt = generate_prompt_base('devops') |  | ||||||
|              |  | ||||||
|             # Si la API está disponible, intentar consultarla |  | ||||||
|             if api_available: |  | ||||||
|                 logger.info("Consultando tarifa para devops") |  | ||||||
|                  |  | ||||||
|                 try: |  | ||||||
|                     # Consultar a Perplexity |  | ||||||
|                     devops_rate = query_perplexity(prompt, model) |  | ||||||
|                      |  | ||||||
|                     if devops_rate is not None: |  | ||||||
|                         logger.info(f"Tarifa obtenida correctamente para devops: {devops_rate:.2f}") |  | ||||||
|                     else: |  | ||||||
|                         logger.error("No se pudo obtener la tarifa para devops") |  | ||||||
|                 except Exception as e: |  | ||||||
|                     logger.error(f"Error al consultar tarifa para devops: {e}") |  | ||||||
|                     time.sleep(2) |  | ||||||
|              |  | ||||||
|             # Si la API falló o no está disponible, usar valor predeterminado |  | ||||||
|             if devops_rate is None: |  | ||||||
|                 logger.warning("Usando valor predeterminado para devops") |  | ||||||
|                 devops_rate = get_default_rate('devops') |  | ||||||
|                 logger.info(f"Valor predeterminado para devops: {devops_rate:.2f}") |  | ||||||
|              |  | ||||||
|             # Limitar a tarifas menores de 200 USD/hora |  | ||||||
|             max_rate = 200.00 |  | ||||||
|             if devops_rate > max_rate: |  | ||||||
|                 logger.warning(f"Ajustando tarifa {devops_rate:.2f} al máximo permitido de {max_rate}") |  | ||||||
|                 devops_rate = max_rate |  | ||||||
|              |  | ||||||
|             # Guardar el resultado en el archivo |  | ||||||
|             with open(devops_rate_file, 'w', encoding='utf-8') as f: |  | ||||||
|                 f.write(f"{devops_rate:.2f}") |  | ||||||
|              |  | ||||||
|             # Añadir al diccionario |  | ||||||
|             rates_dict['devops'] = devops_rate |  | ||||||
|              |  | ||||||
|             # Mostrar el resultado en la consola |  | ||||||
|             result_message = f"Tarifa para devops: {devops_rate:.2f} USD/hora" |  | ||||||
|             logger.info(f"Actualizado archivo {devops_rate_file} con valor: {devops_rate:.2f}") |  | ||||||
|             show_result(result_message) |  | ||||||
|              |  | ||||||
|             # Pequeña pausa para no sobrecargar la API |  | ||||||
|             time.sleep(1) |  | ||||||
|      |  | ||||||
|     # Ahora procesamos el tipo de programador solicitado |  | ||||||
|     rate_file = RATES_DIR / f"{programmer_type}.rate" |  | ||||||
|      |  | ||||||
|     # Caso especial para bash: calculamos como 0.4 veces la tarifa de devops |  | ||||||
|     if programmer_type == 'bash' and devops_rate is not None: |  | ||||||
|         logger.info(f"Calculando tarifa para bash como 0.4 * {devops_rate:.2f}") |  | ||||||
|         rate = 0.4 * devops_rate |  | ||||||
|         logger.info(f"Tarifa calculada para bash: {rate:.2f}") |  | ||||||
|     # Caso especial para fullstack |  | ||||||
|     elif programmer_type == 'fullstack': |  | ||||||
|         # Primero calculamos el valor más alto entre todos los tipos |  | ||||||
|         max_rate_value = 0.0 |  | ||||||
|         for prog_type, rate_value in rates_dict.items(): |  | ||||||
|             if prog_type != 'fullstack' and rate_value > max_rate_value: |  | ||||||
|                 max_rate_value = rate_value |  | ||||||
|          |  | ||||||
|         logger.info(f"Valor más alto encontrado: {max_rate_value:.2f}") |  | ||||||
|          |  | ||||||
|         # Obtener tarifa para fullstack vía API |  | ||||||
|         api_rate = None |  | ||||||
|         if api_available: |  | ||||||
|             prompt = generate_prompt_base(programmer_type) |  | ||||||
|             logger.info(f"Consultando tarifa para {programmer_type}") |  | ||||||
|              |  | ||||||
|             try: |  | ||||||
|                 # Consultar a Perplexity |  | ||||||
|                 api_rate = query_perplexity(prompt, model) |  | ||||||
|                  |  | ||||||
|                 if api_rate is not None: |  | ||||||
|                     logger.info(f"Tarifa obtenida correctamente: {api_rate:.2f}") |  | ||||||
|                 else: |  | ||||||
|                     logger.error(f"No se pudo obtener la tarifa para {programmer_type}") |  | ||||||
|             except Exception as e: |  | ||||||
|                 logger.error(f"Error al consultar tarifa para {programmer_type}: {e}") |  | ||||||
|          |  | ||||||
|         # Si la API falló, usar el valor predeterminado |  | ||||||
|         if api_rate is None: |  | ||||||
|             logger.warning(f"Usando valor predeterminado para {programmer_type}") |  | ||||||
|             api_rate = get_default_rate(programmer_type) |  | ||||||
|             logger.info(f"Valor predeterminado: {api_rate:.2f}") |  | ||||||
|          |  | ||||||
|         # Calcular el valor de fullstack |  | ||||||
|         # Si el valor de la API es menor o igual al valor más alto, usar 1.5 veces el valor más alto |  | ||||||
|         if api_rate <= max_rate_value: |  | ||||||
|             rate = 1.5 * max_rate_value |  | ||||||
|             logger.info(f"API rate ({api_rate:.2f}) <= max rate ({max_rate_value:.2f}), ajustando fullstack a 1.5 * {max_rate_value:.2f} = {rate:.2f}") |  | ||||||
|         else: |  | ||||||
|             rate = api_rate |  | ||||||
|             logger.info(f"API rate ({api_rate:.2f}) > max rate ({max_rate_value:.2f}), manteniendo valor API") |  | ||||||
|     else: |  | ||||||
|         # Para otros tipos, procedemos como antes |  | ||||||
|         prompt = generate_prompt_base(programmer_type) |  | ||||||
|          |  | ||||||
|         # Variable para almacenar la tarifa |  | ||||||
|         rate = None |  | ||||||
|          |  | ||||||
|         # Si la API está disponible, intentar consultarla |  | ||||||
|         if api_available: |  | ||||||
|             logger.info(f"Consultando tarifa para {programmer_type}") |  | ||||||
|              |  | ||||||
|             try: |  | ||||||
|                 # Consultar a Perplexity |  | ||||||
|                 rate = query_perplexity(prompt, model) |  | ||||||
|                  |  | ||||||
|                 if rate is not None: |  | ||||||
|                     logger.info(f"Tarifa obtenida correctamente: {rate:.2f}") |  | ||||||
|                 else: |  | ||||||
|                     logger.error(f"No se pudo obtener la tarifa para {programmer_type}") |  | ||||||
|             except Exception as e: |  | ||||||
|                 logger.error(f"Error al consultar tarifa para {programmer_type}: {e}") |  | ||||||
|                 # Pequeña pausa tras un error para evitar sobrecargar la API |  | ||||||
|                 time.sleep(2) |  | ||||||
|          |  | ||||||
|         # Si la API falló o no está disponible, usar valor predeterminado |  | ||||||
|         if rate is None: |  | ||||||
|             logger.warning(f"Usando valor predeterminado para {programmer_type}") |  | ||||||
|             rate = get_default_rate(programmer_type) |  | ||||||
|             logger.info(f"Valor predeterminado: {rate:.2f}") |  | ||||||
|      |  | ||||||
|     # 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}") |  | ||||||
|      |  | ||||||
|     # Mostrar el resultado en la consola |  | ||||||
|     result_message = f"Tarifa para {programmer_type}: {rate:.2f} USD/hora" |  | ||||||
|     logger.info(f"Actualizado archivo {rate_file} con valor: {rate:.2f}") |  | ||||||
|     show_result(result_message) |  | ||||||
|      |  | ||||||
|     return True |  | ||||||
| 
 |  | ||||||
| def update_rate_files(force_update=False): |  | ||||||
|     """ |  | ||||||
|     Actualiza los archivos de tarifas con datos de Perplexity. |  | ||||||
|     Crea un archivo por tipo de programador con la tarifa por hora general. |  | ||||||
|      |  | ||||||
|     Parámetros: |  | ||||||
|     force_update (bool): Si es True, actualiza todos los tipos incluso si ya existen archivos. |  | ||||||
|     """ |  | ||||||
|     # Obtener modelo configurado |  | ||||||
|     model = get_ai_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 predeterminados.") |  | ||||||
|      |  | ||||||
|     # Crear la carpeta rates si no existe |  | ||||||
|     os.makedirs(RATES_DIR, exist_ok=True) |  | ||||||
|      |  | ||||||
|     # Obtener la lista de tipos de programadores |  | ||||||
|     programmer_types = get_programmer_types() |  | ||||||
|     logger.info(f"Procesando {len(programmer_types)} tipos de programadores.") |  | ||||||
|      |  | ||||||
|     # Diccionario para almacenar las tarifas ya calculadas |  | ||||||
|     rates_dict = {} |  | ||||||
|      |  | ||||||
|     # Leer las tarifas existentes primero |  | ||||||
|     if 'fullstack' in programmer_types: |  | ||||||
|         # Primero leer los valores existentes de tipos que no vamos a actualizar |  | ||||||
|         for prog_type in get_programmer_types(): |  | ||||||
|             if prog_type not in programmer_types: |  | ||||||
|                 rate_file = RATES_DIR / f"{prog_type}.rate" |  | ||||||
|                 if rate_file.exists(): |  | ||||||
|                     try: |  | ||||||
|                         with open(rate_file, 'r', encoding='utf-8') as f: |  | ||||||
|                             rates_dict[prog_type] = float(f.read().strip()) |  | ||||||
|                             logger.info(f"Tarifa leída para {prog_type}: {rates_dict[prog_type]:.2f}") |  | ||||||
|                     except (FileNotFoundError, ValueError) as e: |  | ||||||
|                         logger.warning(f"Error al leer tarifa de {prog_type}: {e}") |  | ||||||
|      |  | ||||||
|     # Procesamos primero devops siempre que force_update sea True o bash esté pendiente |  | ||||||
|     devops_rate = None |  | ||||||
|     need_devops_for_bash = ('bash' in programmer_types and (not (RATES_DIR / "bash.rate").exists() or force_update)) |  | ||||||
|     need_process_devops = ('devops' in programmer_types and (not (RATES_DIR / "devops.rate").exists() or force_update)) |  | ||||||
|      |  | ||||||
|     # Si necesitamos la tarifa de devops para bash, la procesamos primero |  | ||||||
|     if need_devops_for_bash and need_process_devops: |  | ||||||
|         logger.info("Procesando tarifa de devops primero para calcular bash") |  | ||||||
|          |  | ||||||
|         # Obtener tarifa para devops |  | ||||||
|         devops_rate_file = RATES_DIR / "devops.rate" |  | ||||||
|         prompt = generate_prompt_base('devops') |  | ||||||
|          |  | ||||||
|         # Variable para almacenar la tarifa |  | ||||||
|         if api_available: |  | ||||||
|             logger.info("Consultando tarifa para devops") |  | ||||||
|              |  | ||||||
|             try: |  | ||||||
|                 # Consultar a Perplexity |  | ||||||
|                 devops_rate = query_perplexity(prompt, model) |  | ||||||
|                  |  | ||||||
|                 if devops_rate is not None: |  | ||||||
|                     logger.info(f"Tarifa obtenida correctamente para devops: {devops_rate:.2f}") |  | ||||||
|                 else: |  | ||||||
|                     logger.error("No se pudo obtener la tarifa para devops") |  | ||||||
|             except Exception as e: |  | ||||||
|                 logger.error(f"Error al consultar tarifa para devops: {e}") |  | ||||||
|                 time.sleep(2) |  | ||||||
|          |  | ||||||
|         # Si la API falló o no está disponible, usar valor predeterminado |  | ||||||
|         if devops_rate is None: |  | ||||||
|             logger.warning("Usando valor predeterminado para devops") |  | ||||||
|             devops_rate = get_default_rate('devops') |  | ||||||
|             logger.info(f"Valor predeterminado para devops: {devops_rate:.2f}") |  | ||||||
|          |  | ||||||
|         # Limitar a tarifas menores de 200 USD/hora |  | ||||||
|         max_rate = 200.00 |  | ||||||
|         if devops_rate > max_rate: |  | ||||||
|             logger.warning(f"Ajustando tarifa {devops_rate:.2f} al máximo permitido de {max_rate}") |  | ||||||
|             devops_rate = max_rate |  | ||||||
|          |  | ||||||
|         # Guardar el resultado en el archivo |  | ||||||
|         with open(devops_rate_file, 'w', encoding='utf-8') as f: |  | ||||||
|             f.write(f"{devops_rate:.2f}") |  | ||||||
|          |  | ||||||
|         # Guardar en el diccionario de tarifas |  | ||||||
|         rates_dict['devops'] = devops_rate |  | ||||||
|          |  | ||||||
|         # Mostrar el resultado en la consola |  | ||||||
|         result_message = f"Tarifa para devops: {devops_rate:.2f} USD/hora" |  | ||||||
|         logger.info(f"{'Actualizado' if force_update else 'Creado'} archivo {devops_rate_file} con valor: {devops_rate:.2f}") |  | ||||||
|         show_result(result_message) |  | ||||||
|          |  | ||||||
|         # Actualizamos la lista de tipos pendientes |  | ||||||
|         if 'devops' in programmer_types: |  | ||||||
|             programmer_types.remove('devops') |  | ||||||
|          |  | ||||||
|         # Pequeña pausa para no sobrecargar la API |  | ||||||
|         time.sleep(1) |  | ||||||
|      |  | ||||||
|     # Si ya existe un archivo para devops pero necesitamos la tarifa para bash |  | ||||||
|     elif need_devops_for_bash and not need_process_devops: |  | ||||||
|         # Leer la tarifa guardada de devops |  | ||||||
|         devops_rate_file = RATES_DIR / "devops.rate" |  | ||||||
|         try: |  | ||||||
|             with open(devops_rate_file, 'r', encoding='utf-8') as f: |  | ||||||
|                 devops_rate = float(f.read().strip()) |  | ||||||
|                 rates_dict['devops'] = devops_rate |  | ||||||
|                 logger.info(f"Tarifa leída para devops: {devops_rate:.2f}") |  | ||||||
|         except (FileNotFoundError, ValueError) as e: |  | ||||||
|             logger.error(f"Error al leer tarifa de devops: {e}") |  | ||||||
|             devops_rate = get_default_rate('devops') |  | ||||||
|             rates_dict['devops'] = devops_rate |  | ||||||
|      |  | ||||||
|     # Procesar los demás tipos de programador antes de fullstack |  | ||||||
|     fullstack_type = None |  | ||||||
|     if 'fullstack' in programmer_types: |  | ||||||
|         # Guardar para procesarlo después |  | ||||||
|         fullstack_type = 'fullstack' |  | ||||||
|         # Eliminarlo temporalmente de la lista |  | ||||||
|         programmer_types.remove('fullstack') |  | ||||||
|      |  | ||||||
|     # Procesar cada tipo de programador (excepto fullstack) |  | ||||||
|     for programmer_type in programmer_types: |  | ||||||
|         # Comprobar si ya existe el archivo para este tipo de programador |  | ||||||
|         rate_file = RATES_DIR / f"{programmer_type}.rate" |  | ||||||
|          |  | ||||||
|         # Si el archivo ya existe y no estamos forzando la actualización, saltamos este tipo |  | ||||||
|         if rate_file.exists() and not force_update: |  | ||||||
|             # Leer el valor para almacenarlo en el diccionario |  | ||||||
|             try: |  | ||||||
|                 with open(rate_file, 'r', encoding='utf-8') as f: |  | ||||||
|                     rates_dict[programmer_type] = float(f.read().strip()) |  | ||||||
|             except (FileNotFoundError, ValueError) as e: |  | ||||||
|                 logger.warning(f"Error al leer archivo {rate_file}: {e}") |  | ||||||
|              |  | ||||||
|             logger.info(f"El archivo {rate_file} ya existe. Saltando.") |  | ||||||
|             continue |             continue | ||||||
|          |          | ||||||
|         # Caso especial para bash: calculamos como 0.4 veces la tarifa de devops |         # Si es un archivo especial como kdevs.rate | ||||||
|         if programmer_type == 'bash' and devops_rate is not None: |         if region_code is None: | ||||||
|             logger.info(f"Calculando tarifa para bash como 0.4 * {devops_rate:.2f}") |             prompt = generate_prompt(programmer_type, None) | ||||||
|             rate = 0.4 * devops_rate |             # Saltamos los archivos especiales que no necesitan actualización | ||||||
|             logger.info(f"Tarifa calculada para bash: {rate:.2f}") |             if prompt is None: | ||||||
|  |                 logger.info(f"Saltando archivo especial: {os.path.basename(rate_file)}") | ||||||
|  |                 continue | ||||||
|         else: |         else: | ||||||
|             # Para otros tipos, procedemos como antes |             prompt = generate_prompt(programmer_type, region_code) | ||||||
|             prompt = generate_prompt_base(programmer_type) |  | ||||||
|          |          | ||||||
|             # Variable para almacenar la tarifa |         logger.info(f"Consultando tarifa para {programmer_type}" +  | ||||||
|             rate = None |                    (f" en región {region_code}" if region_code else "")) | ||||||
|          |          | ||||||
|             # Si la API está disponible, intentar consultarla |  | ||||||
|             if api_available: |  | ||||||
|                 logger.info(f"Consultando tarifa para {programmer_type}") |  | ||||||
|                  |  | ||||||
|                 try: |  | ||||||
|         # Consultar a Perplexity |         # Consultar a Perplexity | ||||||
|         rate = query_perplexity(prompt, model) |         rate = query_perplexity(prompt, model) | ||||||
|          |          | ||||||
|         if rate is not None: |         if rate is not None: | ||||||
|                         logger.info(f"Tarifa obtenida correctamente: {rate:.2f}") |  | ||||||
|                     else: |  | ||||||
|                         logger.error(f"No se pudo obtener la tarifa para {programmer_type}") |  | ||||||
|                 except Exception as e: |  | ||||||
|                     logger.error(f"Error al consultar tarifa para {programmer_type}: {e}") |  | ||||||
|                     # Pequeña pausa tras un error para evitar sobrecargar la API |  | ||||||
|                     time.sleep(2) |  | ||||||
|              |  | ||||||
|             # Si la API falló o no está disponible, usar valor predeterminado |  | ||||||
|             if rate is None: |  | ||||||
|                 logger.warning(f"Usando valor predeterminado para {programmer_type}") |  | ||||||
|                 rate = get_default_rate(programmer_type) |  | ||||||
|                 logger.info(f"Valor predeterminado: {rate:.2f}") |  | ||||||
|          |  | ||||||
|         # 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 la tarifa en el diccionario |  | ||||||
|         rates_dict[programmer_type] = 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}") |  | ||||||
|          |  | ||||||
|         # Mostrar el resultado en la consola |  | ||||||
|         result_message = f"Tarifa para {programmer_type}: {rate:.2f} USD/hora" |  | ||||||
|         logger.info(f"{'Actualizado' if force_update and rate_file.exists() else 'Creado'} archivo {rate_file} con valor: {rate:.2f}") |  | ||||||
|         show_result(result_message) |  | ||||||
|          |  | ||||||
|         # Pequeña pausa para no sobrecargar la API |  | ||||||
|         time.sleep(1) |  | ||||||
|      |  | ||||||
|     # Procesar fullstack al final si está en la lista |  | ||||||
|     if fullstack_type: |  | ||||||
|         rate_file = RATES_DIR / f"{fullstack_type}.rate" |  | ||||||
|          |  | ||||||
|         # Si el archivo ya existe y no estamos forzando la actualización, saltamos |  | ||||||
|         if rate_file.exists() and not force_update: |  | ||||||
|             # Leer el valor para mostrarlo |  | ||||||
|             try: |  | ||||||
|                 with open(rate_file, 'r', encoding='utf-8') as f: |  | ||||||
|                     fullstack_rate = float(f.read().strip()) |  | ||||||
|                     logger.info(f"El archivo {rate_file} ya existe con valor {fullstack_rate:.2f}. Saltando.") |  | ||||||
|             except (FileNotFoundError, ValueError) as e: |  | ||||||
|                 logger.warning(f"Error al leer archivo {rate_file}: {e}") |  | ||||||
|             return |  | ||||||
|          |  | ||||||
|         # Primero calculamos el valor más alto entre todos los tipos |  | ||||||
|         max_rate_value = 0.0 |  | ||||||
|         for prog_type, rate_value in rates_dict.items(): |  | ||||||
|             if prog_type != 'fullstack' and rate_value > max_rate_value: |  | ||||||
|                 max_rate_value = rate_value |  | ||||||
|          |  | ||||||
|         logger.info(f"Valor más alto encontrado: {max_rate_value:.2f}") |  | ||||||
|          |  | ||||||
|         # Obtener tarifa para fullstack vía API |  | ||||||
|         api_rate = None |  | ||||||
|         if api_available: |  | ||||||
|             prompt = generate_prompt_base(fullstack_type) |  | ||||||
|             logger.info(f"Consultando tarifa para {fullstack_type}") |  | ||||||
|              |  | ||||||
|             try: |  | ||||||
|                 # Consultar a Perplexity |  | ||||||
|                 api_rate = query_perplexity(prompt, model) |  | ||||||
|                  |  | ||||||
|                 if api_rate is not None: |  | ||||||
|                     logger.info(f"Tarifa obtenida correctamente: {api_rate:.2f}") |  | ||||||
|                 else: |  | ||||||
|                     logger.error(f"No se pudo obtener la tarifa para {fullstack_type}") |  | ||||||
|             except Exception as e: |  | ||||||
|                 logger.error(f"Error al consultar tarifa para {fullstack_type}: {e}") |  | ||||||
|          |  | ||||||
|         # Si la API falló, usar el valor predeterminado |  | ||||||
|         if api_rate is None: |  | ||||||
|             logger.warning(f"Usando valor predeterminado para {fullstack_type}") |  | ||||||
|             api_rate = get_default_rate(fullstack_type) |  | ||||||
|             logger.info(f"Valor predeterminado: {api_rate:.2f}") |  | ||||||
|          |  | ||||||
|         # Calcular el valor de fullstack |  | ||||||
|         # Si el valor de la API es menor o igual al valor más alto, usar 1.5 veces el valor más alto |  | ||||||
|         if api_rate <= max_rate_value: |  | ||||||
|             fullstack_rate = 1.5 * max_rate_value |  | ||||||
|             logger.info(f"API rate ({api_rate:.2f}) <= max rate ({max_rate_value:.2f}), ajustando fullstack a 1.5 * {max_rate_value:.2f} = {fullstack_rate:.2f}") |  | ||||||
|         else: |  | ||||||
|             fullstack_rate = api_rate |  | ||||||
|             logger.info(f"API rate ({api_rate:.2f}) > max rate ({max_rate_value:.2f}), manteniendo valor API") |  | ||||||
|          |  | ||||||
|         # Limitar a tarifas menores de 200 USD/hora |  | ||||||
|         max_limit = 200.00 |  | ||||||
|         if fullstack_rate > max_limit: |  | ||||||
|             logger.warning(f"Ajustando tarifa {fullstack_rate:.2f} al máximo permitido de {max_limit}") |  | ||||||
|             fullstack_rate = max_limit |  | ||||||
|          |  | ||||||
|             # Guardar el resultado en el archivo |             # Guardar el resultado en el archivo | ||||||
|         with open(rate_file, 'w', encoding='utf-8') as f: |             with open(rate_file, 'w') as f: | ||||||
|             f.write(f"{fullstack_rate:.2f}") |                 f.write(f"{rate:.2f}\n") | ||||||
|          |             logger.info(f"Actualizado {os.path.basename(rate_file)} con valor: {rate:.2f}") | ||||||
|         # Mostrar el resultado en la consola |  | ||||||
|         result_message = f"Tarifa para {fullstack_type}: {fullstack_rate:.2f} USD/hora" |  | ||||||
|         logger.info(f"{'Actualizado' if force_update and rate_file.exists() else 'Creado'} archivo {rate_file} con valor: {fullstack_rate:.2f}") |  | ||||||
|         show_result(result_message) |  | ||||||
| 
 |  | ||||||
| def generate_prompt_base(programmer_type): |  | ||||||
|     """ |  | ||||||
|     Genera un prompt para consultar la tarifa base por hora para un tipo de programador. |  | ||||||
|     Utiliza el prompt definido en el archivo rates.prompt. |  | ||||||
|     """ |  | ||||||
|     # Mapa para traducir tipos de programador a términos en inglés |  | ||||||
|     programmer_map = { |  | ||||||
|         'bash': 'Bash/Shell', |  | ||||||
|         'python': 'Python', |  | ||||||
|         'fullstack': 'Full Stack', |  | ||||||
|         'frontend': 'Frontend', |  | ||||||
|         'backend': 'Backend', |  | ||||||
|         'devops': 'DevOps', |  | ||||||
|         'mobile': 'Mobile App', |  | ||||||
|         'java': 'Java', |  | ||||||
|         'php': 'PHP', |  | ||||||
|         'ruby': 'Ruby', |  | ||||||
|         'dotnet': '.NET', |  | ||||||
|         'data': 'Data Science', |  | ||||||
|         'ml': 'Machine Learning', |  | ||||||
|         'cloud': 'Cloud', |  | ||||||
|         'odoo': 'Odoo' |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     programmer_description = programmer_map.get(programmer_type, programmer_type) |  | ||||||
|      |  | ||||||
|     # Cargar el prompt desde el archivo |  | ||||||
|     prompt_template = "" |  | ||||||
|     prompt_file = CONFIG_DIR / 'rates.prompt' |  | ||||||
|      |  | ||||||
|     try: |  | ||||||
|         with open(prompt_file, 'r', encoding='utf-8') as f: |  | ||||||
|             prompt_template = f.read().strip() |  | ||||||
|     except FileNotFoundError: |  | ||||||
|         # Si no se encuentra el archivo, usar un prompt predeterminado |  | ||||||
|         prompt_template = "What is the average hourly rate in USD for a [developer type] developer, expressed as a numerical value with two decimal places?" |  | ||||||
|      |  | ||||||
|     # Reemplazar [developer type] con el tipo de programador específico |  | ||||||
|     prompt = prompt_template.replace('[developer type]', programmer_description) |  | ||||||
|      |  | ||||||
|     return prompt |  | ||||||
| 
 |  | ||||||
| def list_rate_files(): |  | ||||||
|     """ |  | ||||||
|     Lista todas las tarifas por tipo de programador. |  | ||||||
|     Si el archivo no existe, utiliza el valor por defecto. |  | ||||||
|     """ |  | ||||||
|     # Crear la carpeta rates si no existe |  | ||||||
|     os.makedirs(RATES_DIR, exist_ok=True) |  | ||||||
|      |  | ||||||
|     # Obtener la lista de tipos de programadores |  | ||||||
|     programmer_types = get_programmer_types() |  | ||||||
|     programmer_types.sort()  # Ordenar alfabéticamente |  | ||||||
|      |  | ||||||
|     print("Tarifas por tipo de programador:") |  | ||||||
|     print("--------------------------------") |  | ||||||
|      |  | ||||||
|     # Primero procesamos devops para tener su tarifa actualizada para bash |  | ||||||
|     devops_rate = None |  | ||||||
|     devops_rate_file = RATES_DIR / "devops.rate" |  | ||||||
|      |  | ||||||
|     if devops_rate_file.exists(): |  | ||||||
|         try: |  | ||||||
|             with open(devops_rate_file, 'r', encoding='utf-8') as f: |  | ||||||
|                 devops_rate = float(f.read().strip()) |  | ||||||
|         except (FileNotFoundError, ValueError) as e: |  | ||||||
|             logger.warning(f"Error al leer tarifa de devops: {e}") |  | ||||||
|             devops_rate = get_default_rate('devops') |  | ||||||
|         else: |         else: | ||||||
|         devops_rate = get_default_rate('devops') |             logger.error(f"No se pudo obtener la tarifa para {programmer_type}" +  | ||||||
|      |                         (f" en región {region_code}" if region_code else "")) | ||||||
|     # Procesar cada tipo de programador |  | ||||||
|     for programmer_type in programmer_types: |  | ||||||
|         rate_file = RATES_DIR / f"{programmer_type}.rate" |  | ||||||
|          |  | ||||||
|         # Si el archivo existe, leer el valor |  | ||||||
|         if rate_file.exists(): |  | ||||||
|             try: |  | ||||||
|                 with open(rate_file, 'r', encoding='utf-8') as f: |  | ||||||
|                     rate = float(f.read().strip()) |  | ||||||
|             except (FileNotFoundError, ValueError) as e: |  | ||||||
|                 logger.warning(f"Error al leer {rate_file}: {e}") |  | ||||||
|                 # Caso especial para bash |  | ||||||
|                 if programmer_type == 'bash' and devops_rate is not None: |  | ||||||
|                     rate = 0.4 * devops_rate |  | ||||||
|                 else: |  | ||||||
|                     rate = get_default_rate(programmer_type) |  | ||||||
|         else: |  | ||||||
|             # Si no existe, usar valor por defecto |  | ||||||
|             # Caso especial para bash |  | ||||||
|             if programmer_type == 'bash' and devops_rate is not None: |  | ||||||
|                 rate = 0.4 * devops_rate |  | ||||||
|             else: |  | ||||||
|                 rate = get_default_rate(programmer_type) |  | ||||||
|             logger.info(f"Usando valor predeterminado para {programmer_type}: {rate:.2f}") |  | ||||||
|          |  | ||||||
|         # Formatear la salida para alinear correctamente |  | ||||||
|         print(f"{programmer_type.ljust(10)}: {rate:.2f} USD/hora") |  | ||||||
| 
 | 
 | ||||||
| if __name__ == "__main__": | if __name__ == "__main__": | ||||||
|     # Parámetros de línea de comandos para controlar el comportamiento |  | ||||||
|     import argparse |  | ||||||
|      |      | ||||||
|     # Obtener la lista de tipos válidos para incluirlos en la ayuda |     logger.info("Iniciando actualización de tarifas...") | ||||||
|     valid_types = get_programmer_types() |  | ||||||
|     valid_types_str = ", ".join(valid_types) |  | ||||||
|      |  | ||||||
|     parser = argparse.ArgumentParser(description='Actualiza tarifas por hora de diferentes tipos de programadores.') |  | ||||||
|     parser.add_argument('-q', '--quiet', action='store_true', help='No mostrar resultados individuales') |  | ||||||
|     parser.add_argument('-v', '--verbose', action='store_true', help='Mostrar información detallada del proceso') |  | ||||||
|     parser.add_argument('-l', '--list', action='store_true', help='Listar todas las tarifas disponibles') |  | ||||||
|     parser.add_argument('-t', '--type', choices=valid_types,  |  | ||||||
|                       help=f'Actualizar la tarifa para un tipo específico de programador. Tipos válidos: {valid_types_str}') |  | ||||||
|     parser.add_argument('-i', '--init', action='store_true',  |  | ||||||
|                       help='Actualizar todas las tarifas, incluso si ya existen archivos') |  | ||||||
|     args = parser.parse_args() |  | ||||||
|      |  | ||||||
|     # Configurar nivel de log según parámetros |  | ||||||
|     if args.verbose: |  | ||||||
|         console_handler.setLevel(logging.INFO) |  | ||||||
|      |  | ||||||
|     # Configurar si se muestran resultados - usar variable global directamente |  | ||||||
|     # No es necesario declarar global aquí ya que estamos en el ámbito global |  | ||||||
|     SHOW_RESULTS = not args.quiet |  | ||||||
|      |  | ||||||
|     # Si se solicita listar las tarifas, solo mostramos la lista y terminamos |  | ||||||
|     if args.list: |  | ||||||
|         list_rate_files() |  | ||||||
|         sys.exit(0) |  | ||||||
|      |      | ||||||
|     # Verificar e instalar pycountry si es necesario |     # Verificar e instalar pycountry si es necesario | ||||||
|     pycountry_available = check_install_pycountry() |     pycountry_available = check_install_pycountry() | ||||||
|      |      | ||||||
|  |     # Actualizar la variable global | ||||||
|  |     globals()['pycountry_available'] = pycountry_available | ||||||
|  |      | ||||||
|     # Mostrar estado de validación de países |     # Mostrar estado de validación de países | ||||||
|     if pycountry_available: |     if pycountry_available: | ||||||
|         logger.info("Validación de códigos de país habilitada con pycountry.") |         logger.info("Validación de códigos de país habilitada con pycountry.") | ||||||
|     else: |     else: | ||||||
|         logger.info("Usando lista interna para validación básica de códigos de país.") |         logger.info("Usando lista interna para validación básica de códigos de país.") | ||||||
|      |      | ||||||
|     try: |     # Actualizar los archivos de tarifas | ||||||
|         # Si se solicita inicializar/actualizar todas las tarifas |  | ||||||
|         if args.init: |  | ||||||
|             logger.info("Iniciando actualización forzada de todas las tarifas...") |  | ||||||
|              |  | ||||||
|             if not args.quiet: |  | ||||||
|                 print("Actualizando todas las tarifas (forzado)...") |  | ||||||
|              |  | ||||||
|             update_rate_files(force_update=True) |  | ||||||
|              |  | ||||||
|             if not args.quiet: |  | ||||||
|                 print("Proceso de actualización forzada completado exitosamente.") |  | ||||||
|         # Si se especifica un tipo de programador, actualizar solo ese tipo |  | ||||||
|         elif args.type: |  | ||||||
|             logger.info(f"Actualizando tarifa para el tipo: {args.type}") |  | ||||||
|             if not args.quiet: |  | ||||||
|                 print(f"Actualizando tarifa para {args.type}...") |  | ||||||
|              |  | ||||||
|             success = update_single_rate(args.type) |  | ||||||
|              |  | ||||||
|             if success and not args.quiet: |  | ||||||
|                 print(f"Tarifa para {args.type} actualizada exitosamente.") |  | ||||||
|             elif not success: |  | ||||||
|                 sys.exit(1) |  | ||||||
|         else: |  | ||||||
|             # Actualizar archivos de tarifas faltantes |  | ||||||
|             logger.info("Iniciando actualización de tarifas faltantes...") |  | ||||||
|              |  | ||||||
|             if not args.quiet: |  | ||||||
|                 print("Actualizando tarifas faltantes...") |  | ||||||
|              |  | ||||||
|     update_rate_files() |     update_rate_files() | ||||||
|      |      | ||||||
|             if not args.quiet: |  | ||||||
|                 print("Proceso de actualización de tarifas completado exitosamente.") |  | ||||||
|                  |  | ||||||
|     logger.info("Proceso de actualización de tarifas completado.") |     logger.info("Proceso de actualización de tarifas completado.") | ||||||
|     except Exception as e: |  | ||||||
|         logger.error(f"Error durante la actualización de tarifas: {e}") |  | ||||||
|         print(f"ERROR: {e}") |  | ||||||
|         sys.exit(1) |  | ||||||
		Loading…
	
		Reference in a new issue