| | """ |
| | Career Assistant - AI-powered career advisor using Groq + Llama 3.3 with specialized modes. |
| | """ |
| | from typing import List, Dict, Generator |
| | from langchain_groq import ChatGroq |
| | from langchain_core.messages import HumanMessage, SystemMessage, AIMessage |
| |
|
| |
|
| | class CareerAssistant: |
| | """AI Career Assistant with specialized modes for job matching, cover letters, and skills analysis.""" |
| |
|
| | SYSTEM_BASE = """Eres CareerAI, un Asistente de Carrera Profesional de élite. Eres experto en: |
| | - Análisis de CVs y perfiles profesionales |
| | - Matching de candidatos con ofertas de trabajo |
| | - Redacción de cover letters y cartas de presentación |
| | - Análisis de brechas de habilidades (skills gap) |
| | - Estrategia de carrera y desarrollo profesional |
| | |
| | REGLAS FUNDAMENTALES: |
| | 1. SIEMPRE basa tus respuestas en los documentos REALES del usuario que se te proporcionan |
| | 2. Si no tienes información suficiente en los documentos, indícalo claramente |
| | 3. NO inventes datos, experiencias o habilidades que no estén en los documentos |
| | 4. Sé específico, accionable y práctico en tus recomendaciones |
| | 5. Responde en el MISMO IDIOMA que usa el usuario |
| | 6. Usa formato Markdown rico (headers, bullets, emojis, tablas) para estructurar |
| | 7. Sé honesto pero motivador - señala fortalezas Y áreas de mejora |
| | 8. Cuando des porcentajes o métricas, explica tu razonamiento""" |
| |
|
| | PROMPTS = { |
| | "general": """Eres CareerAI. Responde la pregunta del usuario sobre su carrera profesional. |
| | |
| | DOCUMENTOS DEL USUARIO: |
| | {context} |
| | |
| | Instrucciones: |
| | - Basa tu respuesta en los documentos proporcionados |
| | - Sé práctico y accionable |
| | - Si el usuario no ha subido documentos relevantes, sugiérele qué subir |
| | - Formato: Usa markdown con headers, bullets y emojis |
| | |
| | Pregunta del usuario: {query}""", |
| |
|
| | "job_match": """Eres CareerAI en modo ANÁLISIS DE COMPATIBILIDAD LABORAL. |
| | |
| | DOCUMENTOS DEL USUARIO (CV, perfil, ofertas de trabajo): |
| | {context} |
| | |
| | INSTRUCCIONES - Analiza la compatibilidad y genera un reporte detallado: |
| | |
| | ## 1. 🎯 Score de Compatibilidad |
| | - Calcula un porcentaje REALISTA (0-100%) basado en: |
| | • Skills técnicos que coinciden vs. requeridos |
| | • Años de experiencia relevante |
| | • Nivel de seniority (Junior/Mid/Senior/Lead) |
| | • Requisitos específicos (idiomas, certificaciones, ubicación) |
| | • Soft skills mencionados |
| | |
| | ## 2. ✅ Lo que SÍ tiene el candidato |
| | - Lista cada skill/requisito que el candidato cumple |
| | - Referencia dónde aparece en su CV |
| | |
| | ## 3. ❌ Lo que le FALTA |
| | - Lista cada gap identificado |
| | - Clasifica por importancia (Crítico / Importante / Nice-to-have) |
| | |
| | ## 4. 💡 Recomendaciones |
| | - Cómo cubrir cada gap en orden de prioridad |
| | - Recursos gratuitos específicos para aprender |
| | - Timeframe estimado |
| | |
| | ## 5. 📊 Resumen Ejecutivo |
| | - Veredicto: ¿Debería aplicar? ¿Con qué estrategia? |
| | |
| | Pregunta del usuario: {query}""", |
| |
|
| | "cover_letter": """Eres CareerAI en modo GENERADOR DE COVER LETTERS. |
| | |
| | DOCUMENTOS DEL USUARIO (CV, perfil, oferta de trabajo): |
| | {context} |
| | |
| | INSTRUCCIONES - Genera una cover letter profesional y personalizada: |
| | |
| | 1. **Usa datos REALES** del CV/perfil del usuario (nombre, experiencia, logros) |
| | 2. **Adapta** específicamente a la oferta de trabajo (empresa, rol, requisitos) |
| | 3. **Estructura**: |
| | - Apertura impactante (hook + por qué esta empresa) |
| | - Párrafo de experiencia relevante (con logros cuantificables) |
| | - Párrafo de skills matching (conecta tu perfil con requisitos) |
| | - Cierre fuerte (call to action) |
| | 4. **Tono**: Profesional pero auténtico, no genérico |
| | 5. **Longitud**: 3-4 párrafos (250-400 palabras) |
| | 6. **Idioma**: Genera en el idioma de la oferta de trabajo |
| | 7. **Formato**: Cover letter lista para copiar y pegar |
| | |
| | Después de la carta, incluye: |
| | - 💡 Tips para personalizar aún más |
| | - 📧 Sugerencia de subject line para email |
| | - ⚠️ Cosas a verificar antes de enviar |
| | |
| | Solicitud del usuario: {query}""", |
| |
|
| | "skills_gap": """Eres CareerAI en modo ANÁLISIS DE BRECHA DE HABILIDADES. |
| | |
| | DOCUMENTOS DEL USUARIO: |
| | {context} |
| | |
| | INSTRUCCIONES - Realiza un análisis profundo de skills: |
| | |
| | ## 1. 📋 Inventario de Skills Actuales |
| | Extrae TODAS las habilidades del usuario de sus documentos: |
| | - 💻 Hard Skills / Técnicos |
| | - 🧠 Soft Skills |
| | - 🛠️ Herramientas y Tecnologías |
| | - 🌍 Idiomas |
| | - 🎓 Certificaciones |
| | |
| | ## 2. 📍 Nivel Actual Estimado |
| | - Junior / Mid-Level / Senior / Lead / Principal |
| | - Justifica tu evaluación con evidencia de los documentos |
| | |
| | ## 3. 🚀 Roadmap al Siguiente Nivel |
| | Para cada categoría, indica: |
| | |
| | | Skill Necesario | Prioridad | Recurso Gratuito Recomendado | Tiempo Estimado | |
| | |----------------|-----------|------------------------------|-----------------| |
| | |
| | ## 4. 📈 Plan de Acción (90 días) |
| | - Semana 1-2: Quick wins |
| | - Semana 3-6: Skills prioritarios |
| | - Semana 7-12: Profundización y proyectos |
| | |
| | ## 5. 🎯 Skills más Demandados en el Mercado |
| | - Basado en el perfil del usuario, qué skills tienen más demanda |
| | |
| | Pregunta del usuario: {query}""", |
| |
|
| | "interview": """Eres CareerAI en modo SIMULADOR DE ENTREVISTAS LABORALES. |
| | |
| | DOCUMENTOS DEL USUARIO (CV, perfil, ofertas de trabajo): |
| | {context} |
| | |
| | INSTRUCCIONES - Actúa como un entrevistador profesional experto: |
| | |
| | ## Tu rol: |
| | Eres un entrevistador senior que está evaluando al candidato para el puesto. |
| | Tus preguntas deben ser ESPECÍFICAS basadas en el CV real y la oferta de trabajo (si hay). |
| | |
| | ## Cómo funciona la simulación: |
| | |
| | ### Si el usuario dice "empezar entrevista" o "simular entrevista": |
| | Genera una sesión de entrevista estructurada con: |
| | |
| | ## 🎤 Simulación de Entrevista |
| | |
| | ### 👋 Introducción |
| | - Preséntate como entrevistador (inventa un nombre y empresa basado en la oferta) |
| | - Rompe el hielo con una pregunta ligera |
| | |
| | ### 📋 Fase 1: Preguntas de Comportamiento (STAR Method) |
| | Genera 3-4 preguntas basadas en la experiencia del CV: |
| | - Usa el método STAR (Situación, Tarea, Acción, Resultado) |
| | - Referencia experiencias específicas del CV |
| | - Ejemplos: "Cuéntame sobre un proyecto donde tuviste que..." |
| | |
| | ### 💻 Fase 2: Preguntas Técnicas |
| | Genera 3-4 preguntas técnicas relevantes: |
| | - Basadas en los skills del CV y requisitos de la oferta |
| | - Variedad: conceptuales, de diseño, y prácticas |
| | - Adaptadas al nivel del candidato (junior/mid/senior) |
| | |
| | ### 🧠 Fase 3: Preguntas Situacionales |
| | Genera 2-3 preguntas hipotéticas: |
| | - "¿Qué harías si...?" |
| | - Basadas en desafíos reales del puesto |
| | |
| | ### ❓ Fase 4: Preguntas del Candidato |
| | - "¿Tenés preguntas para nosotros?" |
| | - Sugiere 3 preguntas inteligentes que el candidato podría hacer |
| | |
| | Para CADA pregunta incluye: |
| | - 💡 **Tip**: Qué busca el entrevistador con esta pregunta |
| | - ✅ **Respuesta ideal**: Framework o puntos clave que debería mencionar |
| | - ⚠️ **Red flags**: Qué NO decir |
| | |
| | ### Si el usuario RESPONDE una pregunta de entrevista: |
| | - Evalúa su respuesta (fortalezas y debilidades) |
| | - Da feedback constructivo y específico |
| | - Sugiere cómo mejorar la respuesta |
| | - Después hace la SIGUIENTE pregunta |
| | |
| | Solicitud del usuario: {query}""", |
| | } |
| |
|
| | AVAILABLE_MODELS = [ |
| | "llama-3.3-70b-versatile", |
| | "llama-3.1-8b-instant", |
| | "mixtral-8x7b-32768", |
| | "gemma2-9b-it", |
| | ] |
| |
|
| | def __init__(self, api_key: str, model: str = "llama-3.3-70b-versatile"): |
| | """Initialize the career assistant with Groq API.""" |
| | self.api_key = api_key |
| | self.model = model |
| | self.llm = ChatGroq( |
| | groq_api_key=api_key, |
| | model_name=model, |
| | temperature=0.3, |
| | max_tokens=4096, |
| | ) |
| |
|
| | def _build_messages( |
| | self, |
| | system_prompt: str, |
| | query: str, |
| | chat_history: List[Dict] = None, |
| | ) -> list: |
| | """Build the message list for the LLM.""" |
| | messages = [SystemMessage(content=system_prompt)] |
| |
|
| | |
| | if chat_history: |
| | for msg in chat_history[-8:]: |
| | if msg["role"] == "user": |
| | messages.append(HumanMessage(content=msg["content"])) |
| | elif msg["role"] == "assistant": |
| | |
| | content = msg["content"] |
| | if len(content) > 1000: |
| | content = content[:1000] + "\n... [respuesta anterior truncada]" |
| | messages.append(AIMessage(content=content)) |
| |
|
| | messages.append(HumanMessage(content=query)) |
| | return messages |
| |
|
| | def chat( |
| | self, |
| | query: str, |
| | context: str, |
| | chat_history: List[Dict] = None, |
| | mode: str = "general", |
| | ) -> str: |
| | """Process a query and return a complete response.""" |
| | template = self.PROMPTS.get(mode, self.PROMPTS["general"]) |
| | system_prompt = self.SYSTEM_BASE + "\n\n" + template.format( |
| | context=context, query=query |
| | ) |
| |
|
| | messages = self._build_messages(system_prompt, query, chat_history) |
| |
|
| | try: |
| | response = self.llm.invoke(messages) |
| | return response.content |
| | except Exception as e: |
| | error_msg = str(e) |
| | if "rate_limit" in error_msg.lower(): |
| | return "⏳ **Límite de velocidad alcanzado.** Espera unos segundos e intenta de nuevo. Groq tiene un límite generoso pero puede saturarse con consultas muy seguidas." |
| | elif "authentication" in error_msg.lower() or "api_key" in error_msg.lower(): |
| | return "🔑 **Error de autenticación.** Verifica tu API key de Groq. Puedes obtener una gratis en [console.groq.com](https://console.groq.com)" |
| | else: |
| | return f"❌ **Error al procesar tu consulta:**\n\n`{error_msg}`\n\nVerifica tu API key y conexión a internet." |
| |
|
| | def stream_chat( |
| | self, |
| | query: str, |
| | context: str, |
| | chat_history: List[Dict] = None, |
| | mode: str = "general", |
| | ) -> Generator[str, None, None]: |
| | """Stream a response token by token for real-time display.""" |
| | template = self.PROMPTS.get(mode, self.PROMPTS["general"]) |
| | system_prompt = self.SYSTEM_BASE + "\n\n" + template.format( |
| | context=context, query=query |
| | ) |
| |
|
| | messages = self._build_messages(system_prompt, query, chat_history) |
| |
|
| | try: |
| | for chunk in self.llm.stream(messages): |
| | if chunk.content: |
| | yield chunk.content |
| | except Exception as e: |
| | error_msg = str(e) |
| | if "rate_limit" in error_msg.lower(): |
| | yield "\n\n⏳ **Límite de velocidad alcanzado.** Espera unos segundos e intenta de nuevo." |
| | elif "authentication" in error_msg.lower(): |
| | yield "\n\n🔑 **Error de autenticación.** Verifica tu API key de Groq." |
| | else: |
| | yield f"\n\n❌ **Error:** `{error_msg}`" |
| |
|
| | def detect_mode(self, query: str) -> str: |
| | """Auto-detect the best mode based on the user's query.""" |
| | query_lower = query.lower() |
| |
|
| | interview_keywords = [ |
| | "entrevista", "interview", "simula", "pregunta", |
| | "practica", "preparar entrevista", "mock interview", |
| | "entrevistar", "preguntas técnicas", "behavioral", |
| | ] |
| | job_keywords = [ |
| | "match", "compatib", "oferta", "job", "vacante", "posición", |
| | "requisito", "aplica", "pegan", "encaj", "cumplo", |
| | ] |
| | cover_keywords = [ |
| | "cover letter", "carta", "presentación", "letter", |
| | "aplicar", "postular", "escribir carta", "redacta", |
| | ] |
| | skills_keywords = [ |
| | "skill", "habilidad", "faltan", "gap", "senior", |
| | "mejorar", "aprender", "certificac", "nivel", |
| | "roadmap", "plan", "desarrollo", |
| | ] |
| |
|
| | for kw in interview_keywords: |
| | if kw in query_lower: |
| | return "interview" |
| |
|
| | for kw in cover_keywords: |
| | if kw in query_lower: |
| | return "cover_letter" |
| |
|
| | for kw in job_keywords: |
| | if kw in query_lower: |
| | return "job_match" |
| |
|
| | for kw in skills_keywords: |
| | if kw in query_lower: |
| | return "skills_gap" |
| |
|
| | return "general" |
| |
|