Skip to main content
Glama

estudIA-MCP

by JpAboytes
gemini.py24.5 kB
""" Cliente para Google Gemini AI - Integración con FiscAI """ import asyncio from typing import List, Dict, Any, Optional import google.generativeai as genai from .config import config # Configurar Gemini if config.GEMINI_API_KEY: genai.configure(api_key=config.GEMINI_API_KEY) else: raise ValueError("GEMINI_API_KEY no está configurada") # System Prompt con detección de ubicaciones SYSTEM_PROMPT = """ Eres Juan Pablo, un asistente fiscal experto en México especializado en ayudar a micro y pequeños negocios. **CAPACIDADES ESPECIALES:** 1. **Ubicaciones de Bancos y SAT:** - Cuando el usuario pregunte sobre dónde encontrar un banco Banorte o una oficina del SAT - USA la herramienta 'open_map_location' para abrir el mapa - Ejemplos de preguntas que deben activar el mapa: * "¿Dónde hay un Banorte?" * "¿Dónde está el SAT más cercano?" * "Necesito ir a un banco" * "Muéstrame oficinas del SAT" * "Busca un Banorte en Reforma" * "¿Hay alguna oficina del SAT cerca?" 2. **Asesoría Fiscal:** - Proporciona información sobre régimen fiscal, obligaciones, trámites - USA la herramienta 'get_fiscal_advice' para consultas de formalización 3. **Análisis de Riesgo:** - Evalúa la situación fiscal del usuario - USA la herramienta 'analyze_fiscal_risk' cuando pregunten sobre su nivel de cumplimiento **FORMATO DE RESPUESTA PARA UBICACIONES:** Cuando uses 'open_map_location', tu respuesta debe ser breve y clara: - "¡Claro! Te abro el mapa con los Banorte más cercanos." - "Perfecto, te muestro las oficinas del SAT en tu zona." - "Busco Banorte en Reforma para ti." **NO incluyas las coordenadas o detalles técnicos en tu respuesta, eso lo maneja el mapa automáticamente.** **Tono:** Cercano, profesional pero amigable, como un asesor de confianza. """ def detect_user_intent(message: str) -> Dict[str, Any]: """ Detecta la intención del usuario antes de llamar a Gemini para optimizar el uso de herramientas """ message_lower = message.lower() # Palabras clave para búsqueda de ubicaciones location_keywords = { 'bank': ['banorte', 'banco', 'sucursal bancaria', 'ir al banco', 'sucursal'], 'sat': ['sat', 'oficina del sat', 'servicio de administración tributaria', 'centro tributario', 'módulo de atención', 'oficina tributaria'] } # Verbos que indican búsqueda de ubicación location_verbs = ['dónde', 'donde', 'ubica', 'encuentra', 'busca', 'hay', 'mostrar', 'muestra', 'llevar', 'ir', 'cerca', 'cercano', 'necesito ir', 'quiero ir', 'cómo llegar'] # Detectar tipo de ubicación location_type = None if any(keyword in message_lower for keyword in location_keywords['bank']): location_type = 'bank' elif any(keyword in message_lower for keyword in location_keywords['sat']): location_type = 'sat' # Detectar si es una pregunta de ubicación is_location_query = any(verb in message_lower for verb in location_verbs) # Extraer posible query específica (nombre de lugar) search_query = None location_indicators = [' en ', ' de ', ' cerca de ', ' por '] for indicator in location_indicators: if indicator in message_lower: parts = message_lower.split(indicator, 1) if len(parts) > 1: # Extraer hasta el siguiente espacio o final potential_query = parts[1].split('.')[0].split(',')[0].split('?')[0].strip() # Solo si no es muy corto if len(potential_query) > 2: search_query = potential_query break return { 'is_location_query': is_location_query and location_type is not None, 'location_type': location_type, 'search_query': search_query, 'requires_map': is_location_query and location_type is not None } class GeminiClient: """Cliente para interactuar con Google Gemini AI""" def __init__(self): self.model = genai.GenerativeModel(config.GEMINI_MODEL) async def generate_text(self, prompt: str) -> str: """ Genera texto simple usando Gemini Args: prompt: Prompt para generar texto Returns: Texto generado por Gemini """ try: response = await asyncio.to_thread( self.model.generate_content, prompt ) return response.text if response.text else "No se pudo generar respuesta" except Exception as error: print(f"Error generando texto: {error}") raise error async def generate_embedding(self, text: str) -> List[float]: """ Genera embedding para un texto usando Gemini Compatible con el formato del código Python original Args: text: Texto para generar embedding Returns: Lista de números representando el embedding """ try: result = await asyncio.to_thread( genai.embed_content, model=config.GEMINI_EMBED_MODEL, content=text, task_type="RETRIEVAL_QUERY", # Mayúsculas como en simulate_recomendation.py output_dimensionality=config.EMBED_DIM ) # Extraer embedding según la estructura de respuesta if isinstance(result, dict): if "embedding" in result: emb = result["embedding"] if isinstance(emb, dict) and "values" in emb: return emb["values"] if isinstance(emb, list): return emb if "embeddings" in result and isinstance(result["embeddings"], list) and result["embeddings"]: e0 = result["embeddings"][0] if isinstance(e0, dict) and "values" in e0: return e0["values"] if isinstance(e0, list): return e0 # Si result tiene atributo embedding if hasattr(result, "embedding"): emb = getattr(result, "embedding") if isinstance(emb, dict) and "values" in emb: return emb["values"] if hasattr(emb, "values"): return emb.values if isinstance(emb, list): return emb # Fallback if hasattr(result, "embeddings"): emb_list = getattr(result, "embeddings") or [] if emb_list: e0 = emb_list[0] if isinstance(e0, dict) and "values" in e0: return e0["values"] if hasattr(e0, "values"): return e0.values if isinstance(e0, list): return e0 raise RuntimeError("No se pudo extraer embedding de la respuesta") except Exception as error: print(f"Error generando embedding: {error}") raise error async def generate_recommendation( self, profile: Dict[str, Any], context: str ) -> str: """ Genera recomendación fiscal usando RAG (contexto de documentos relevantes) Usa el mismo prompt que simulate_recomendation.py que funciona Args: profile: Perfil fiscal del usuario context: Contexto construido de documentos relevantes Returns: Recomendación detallada en formato markdown """ try: import json system_instruction = ( "Eres un contador experto en México. " "Responde SOLO con el CONTEXTO provisto. Si no es suficiente, indica claramente " "'Información insuficiente en la base'. Usa lenguaje claro y profesional." ) user_prompt = f""" PERFIL: {json.dumps(profile, ensure_ascii=False, indent=2)} TAREA: Como contador en México, necesito analizar este perfil y sugerir: 1) **Régimen fiscal más conveniente:** - Identifica el régimen fiscal óptimo para este perfil - Explica brevemente por qué es el más adecuado - Menciona alternativas si aplican 2) **Pasos específicos de formalización:** - Lista los pasos concretos para formalizarse (RFC, e.firma, CFDI, declaraciones) - Indica el orden recomendado - Menciona requisitos y documentos necesarios 3) **Fuentes oficiales del SAT consultadas:** - Lista las fuentes utilizadas con formato: Título -> URL - Usa solo las fuentes del CONTEXTO provisto - Cita las secciones relevantes CONTEXTO: {context} """.strip() # Crear modelo con system instruction (igual que simulate_recomendation.py) model = genai.GenerativeModel( model_name=config.GEMINI_MODEL, system_instruction=system_instruction, generation_config={ "temperature": 0.3, "max_output_tokens": 1200 } ) # Generar contenido response = await asyncio.to_thread( model.generate_content, user_prompt ) return response.text or "(Sin texto)" except Exception as error: print(f"Error generando recomendación RAG: {error}") import traceback traceback.print_exc() raise error async def enhance_recommendation( self, lambda_response: Dict[str, Any], similar_cases: List[Dict[str, Any]], user_context: Optional[Dict[str, Any]] = None ) -> Dict[str, Any]: """ Enriquecer recomendación fiscal con Gemini Args: lambda_response: Respuesta base del sistema similar_cases: Casos similares encontrados user_context: Contexto del usuario Returns: Recomendación enriquecida """ try: prompt = f""" Eres un experto asesor fiscal mexicano. Basándote en la siguiente información, genera una recomendación personalizada y detallada: **Recomendación Base:** {lambda_response.get('recommendation', lambda_response)} **Perfil del Usuario:** {lambda_response.get('profile', {})} **Casos Similares (para referencia):** {similar_cases[:3]} {f"**Contexto del Usuario:**\n{user_context}" if user_context else ""} **Instrucciones:** 1. Mantén el formato de la recomendación original con sus secciones (Régimen Fiscal, Pasos, Checklist, etc.) 2. Usa texto en **negrita** para títulos importantes 3. Usa *cursiva* para notas adicionales 4. Mantén los bullets y numeración 5. Asegúrate de incluir las fuentes al final 6. Sé específico con montos, fechas y requisitos 7. Usa lenguaje claro y accesible para micro-negocios Responde SOLO con la recomendación mejorada, sin comentarios adicionales. """ response = await asyncio.to_thread( self.model.generate_content, prompt ) enhanced_text = response.text return { **lambda_response, 'recommendation': enhanced_text, 'enhanced': True, 'enhanced_at': asyncio.get_event_loop().time() } except Exception as error: print(f"Error enriqueciendo recomendación: {error}") # Si falla Gemini, devolver la respuesta original return lambda_response async def chat_with_assistant( self, message: str, user_context: Optional[Dict[str, Any]] = None, chat_history: List[Dict[str, Any]] = None, relevant_docs: List[Dict[str, Any]] = None ) -> str: """ Chat con el asistente fiscal usando Gemini con detección automática de intenciones Args: message: Mensaje del usuario user_context: Contexto del usuario chat_history: Historial de conversación relevant_docs: Documentos relevantes Returns: Respuesta del asistente en formato JSON con texto, deep_link y tool_used """ try: # 1. DETECCIÓN AUTOMÁTICA DE INTENCIONES intent = detect_user_intent(message) # 2. SI ES UNA CONSULTA DE UBICACIÓN, GENERAR RESPUESTA DIRECTAMENTE if intent['requires_map']: print(f"[CHAT] Detección automática: requiere mapa tipo={intent['location_type']}") # Construir deep link directamente (sin llamar a la herramienta decorada) base_url = "fiscai://map" params = [f"type={intent['location_type']}"] if intent['search_query']: params.append(f"query={intent['search_query']}") deep_link = f"{base_url}?{'&'.join(params)}" # Construir mensaje descriptivo location_name = "Banorte" if intent['location_type'] == "bank" else "oficinas del SAT" if intent['search_query']: message_text = f"📍 Busco {location_name} en {intent['search_query']} para ti." else: message_text = f"📍 ¡Claro! Te abro el mapa con los {location_name} más cercanos." # Retornar respuesta estructurada import json return json.dumps({ 'text': message_text, 'deep_link': deep_link, 'tool_used': 'open_map_location', 'details': { 'location_type': intent['location_type'], 'search_query': intent['search_query'] } }, ensure_ascii=False) # 3. SI NO ES UBICACIÓN, CONTINUAR CON FLUJO NORMAL DE CHAT if chat_history is None: chat_history = [] if relevant_docs is None: relevant_docs = [] # Construir historial de chat para contexto history_context = "" if chat_history: history_items = [] for h in chat_history[-5:]: # Últimos 5 mensajes history_items.append(f"Usuario: {h.get('message', '')}") history_items.append(f"Asistente: {h.get('response', '')}") history_context = "\n\n".join(history_items) user_info = "" if user_context: user_info = f"""**Información del Usuario:** - Nombre: {user_context.get('name', 'Usuario')} - Email: {user_context.get('email', 'No disponible')} - Actividad: {user_context.get('actividad', 'No especificada')}""" if user_context.get('ingresos_anuales'): user_info += f"\n- Ingresos anuales: ${user_context['ingresos_anuales']:,}" if user_context.get('estado'): user_info += f"\n- Estado: {user_context['estado']}" docs_context = "" if relevant_docs: docs_list = [] for doc in relevant_docs: content = doc.get('content', '')[:200] docs_list.append(f"- {doc.get('title', 'Documento')}: {content}...") docs_context = f"**Documentos de Referencia:**\n" + "\n".join(docs_list) prompt = f""" {SYSTEM_PROMPT} {user_info} {f"**Conversación Previa:**\n{history_context}" if history_context else ""} {docs_context} **Pregunta Actual del Usuario:** {message} **Instrucciones:** - Responde en español de manera clara y profesional - Usa ejemplos prácticos y específicos para México - Si mencionas montos o fechas, sé específico - Usa **negrita** para conceptos importantes - Usa *cursiva* para notas adicionales - Si no tienes suficiente información, pregunta amablemente - Mantén un tono amigable pero profesional - Si la pregunta requiere información personal del usuario que no tienes, pídela Responde de manera concisa pero completa: """ response = await asyncio.to_thread( self.model.generate_content, prompt ) # Retornar respuesta simple de chat import json return json.dumps({ 'text': response.text, 'deep_link': None, 'tool_used': 'chat', 'details': {} }, ensure_ascii=False) except Exception as error: print(f"Error en chat con asistente: {error}") import json return json.dumps({ 'text': f"Lo siento, hubo un error al procesar tu mensaje: {str(error)}", 'deep_link': None, 'tool_used': 'error', 'details': {'error': str(error)} }, ensure_ascii=False) async def analyze_fiscal_risk(self, profile: Dict[str, Any]) -> Dict[str, Any]: """ Analizar perfil fiscal y calcular riesgo Args: profile: Perfil fiscal del usuario Returns: Análisis de riesgo """ try: prompt = f""" Analiza el siguiente perfil fiscal y calcula un nivel de riesgo: **Perfil:** {profile} Responde en JSON con este formato exacto: {{ "score": <número del 0 al 100, donde 0 es sin riesgo y 100 es alto riesgo>, "level": "<Verde|Amarillo|Rojo>", "message": "<mensaje breve del estado>", "details": {{ "has_rfc": <boolean>, "has_efirma": <boolean>, "emite_cfdi": <boolean>, "declara_mensual": <boolean> }}, "recommendations": [ "<lista de recomendaciones específicas>" ] }} Criterios de evaluación: - Verde (0-30): Cumplimiento fiscal óptimo - Amarillo (31-60): Requiere atención en algunas áreas - Rojo (61-100): Alto riesgo fiscal, acción inmediata requerida """ response = await asyncio.to_thread( self.model.generate_content, prompt ) text = response.text # Extraer JSON de la respuesta import json import re json_match = re.search(r'\{[\s\S]*\}', text) if json_match: return json.loads(json_match.group(0)) raise ValueError("No se pudo parsear la respuesta de análisis de riesgo") except Exception as error: print(f"Error analizando riesgo fiscal: {error}") # Respuesta por defecto en caso de error return { 'score': 0, 'level': 'Verde', 'message': 'Análisis no disponible', 'details': { 'has_rfc': profile.get('has_rfc', False), 'has_efirma': profile.get('has_efirma', False), 'emite_cfdi': profile.get('emite_cfdi', False), 'declara_mensual': profile.get('declara_mensual', False) }, 'recommendations': [] } async def analyze_conversation_for_context_update( self, current_context: str, conversation_messages: List[Dict[str, Any]] ) -> Dict[str, Any]: """ Analiza la conversación del usuario y determina si el contexto debe actualizarse. Args: current_context: Contexto actual del usuario (puede ser texto o JSON string) conversation_messages: Lista de mensajes de la conversación con 'content', 'created_at', 'user_id' Returns: Dict con should_update (bool), new_context (str), y reasons (list) """ try: # Construir el historial de conversación conversation_text = "" for msg in conversation_messages: # Determinar si es del usuario o del asistente (si user_id es None, es del asistente) sender = "Usuario" if msg.get('user_id') else "Asistente" content = msg.get('content', '') timestamp = msg.get('created_at', '') conversation_text += f"\n[{timestamp}] {sender}: {content}" prompt = f"""Eres un asistente educativo que analiza conversaciones para personalizar la experiencia del usuario. **CONTEXTO ACTUAL DEL USUARIO:** {current_context if current_context else "No hay contexto previo"} **CONVERSACIÓN COMPLETA:** {conversation_text} **INSTRUCCIONES:** Analiza la conversación y determina si hay información valiosa para actualizar el contexto del usuario. Busca información sobre: 1. **Nivel educativo**: Grado, carrera, área de estudio 2. **Estilo de aprendizaje**: Visual, auditivo, kinestésico, preferencias de explicación 3. **Intereses académicos**: Temas que le interesan, áreas de especialización 4. **Fortalezas y debilidades**: Materias en las que es fuerte o necesita ayuda 5. **Objetivos educativos**: Metas de aprendizaje, proyectos, exámenes 6. **Preferencias de comunicación**: Cómo prefiere que le expliquen, nivel de detalle 7. **Horarios o hábitos**: Cuándo estudia, con qué frecuencia 8. **Contexto personal relevante**: Cualquier dato que ayude a personalizar el aprendizaje **RESPONDE EN JSON CON ESTE FORMATO:** {{ "should_update": true/false, "new_context": "Contexto actualizado en formato de texto descriptivo. Combina el contexto anterior con la nueva información encontrada.", "reasons": [ "Razón 1 por la que se debe actualizar", "Razón 2..." ], "key_findings": {{ "nivel_educativo": "...", "estilo_aprendizaje": "...", "intereses": ["tema1", "tema2"], "fortalezas": ["..."], "debilidades": ["..."], "objetivos": "...", "preferencias": "...", "otros": "..." }} }} **CRITERIOS PARA ACTUALIZAR:** - Si encuentras información nueva y relevante que no está en el contexto actual - Si hay cambios en el nivel educativo, intereses o objetivos - Si identificas patrones de aprendizaje o preferencias claras - Si hay información contradictoria que necesita corrección **NO ACTUALICES SI:** - La conversación es muy breve o superficial - No hay información personal o educativa relevante - La información ya está en el contexto actual Responde SOLO con el JSON:""" response = await asyncio.to_thread( self.model.generate_content, prompt ) text = response.text.strip() # Extraer JSON de la respuesta import json import re # Limpiar markdown si existe if text.startswith("```json"): text = text[7:] if text.startswith("```"): text = text[3:] if text.endswith("```"): text = text[:-3] text = text.strip() json_match = re.search(r'\{[\s\S]*\}', text) if json_match: result = json.loads(json_match.group(0)) return result raise ValueError("No se pudo parsear la respuesta de análisis de contexto") except Exception as error: print(f"Error analizando conversación para actualizar contexto: {error}") import traceback traceback.print_exc() return { 'should_update': False, 'new_context': current_context, 'reasons': [f"Error en el análisis: {str(error)}"], 'key_findings': {} } # Instancia global del cliente gemini_client = GeminiClient()

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/JpAboytes/estudIA-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server