from typing import Dict, List, Optional, Union import spacy from transformers import AutoTokenizer, AutoModel import torch import numpy as np import re from patterns import ( PATRONES_AMBIGUEDAD_LEXICA, PATRONES_AMBIGUEDAD_SINTACTICA, SUGERENCIAS_MEJORA, USER_STORY_PATTERNS ) class TextAnalyzer: """ Analizador de texto que puede procesar tanto historias de usuario como preguntas generales. Integra análisis semántico, detección de ambigüedades y análisis estructural. """ def __init__(self, model_name: str = "PlanTL-GOB-ES/roberta-base-bne"): """ Inicializa el analizador de texto. Args: model_name (str): Nombre del modelo de HuggingFace a utilizar """ try: self.nlp = spacy.load("es_core_news_sm") self.tokenizer = AutoTokenizer.from_pretrained(model_name) self.model = AutoModel.from_pretrained(model_name) except Exception as e: raise RuntimeError(f"Error inicializando el analizador: {str(e)}") def _get_embedding(self, texto: str) -> np.ndarray: """ Obtiene el embedding de un texto usando el modelo de transformers. Args: texto (str): Texto a procesar Returns: np.ndarray: Vector de embedding """ inputs = self.tokenizer(texto, return_tensors="pt", padding=True, truncation=True) with torch.no_grad(): outputs = self.model(**inputs) return outputs.last_hidden_state.mean(dim=1).numpy()[0] def calcular_similitud(self, texto1: str, texto2: str) -> float: """ Compara la similitud semántica entre dos textos. Args: texto1 (str): Primer texto texto2 (str): Segundo texto Returns: float: Score de similitud entre 0 y 1 """ emb1 = self._get_embedding(texto1) emb2 = self._get_embedding(texto2) similarity = np.dot(emb1, emb2) / (np.linalg.norm(emb1) * np.linalg.norm(emb2)) return float(similarity) def is_user_story(self, text: str) -> bool: """ Determina si el texto es una historia de usuario. Args: text (str): Texto a analizar Returns: bool: True si es una historia de usuario, False en caso contrario """ # Verificar patrones comunes de historias de usuario for pattern in USER_STORY_PATTERNS.values(): if re.match(pattern, text): return True # Verificar palabras clave comunes en historias de usuario keywords = ["como", "quiero", "para", "necesito", "debe", "debería"] text_lower = text.lower() keyword_count = sum(1 for keyword in keywords if keyword in text_lower) return keyword_count >= 2 def analyze_user_story(self, text: str) -> Dict: """ Analiza una historia de usuario en busca de ambigüedades. Args: text (str): Historia de usuario a analizar Returns: Dict: Resultado del análisis con tipos de ambigüedad y sugerencias """ doc = self.nlp(text.strip()) # Detectar ambigüedades léxicas ambiguedades_lexicas = [] for patron in PATRONES_AMBIGUEDAD_LEXICA: if re.search(patron["patron"], text, re.IGNORECASE): ambiguedades_lexicas.append({ "tipo": patron["tipo"], "descripcion": patron["descripcion"] }) # Detectar ambigüedades sintácticas ambiguedades_sintacticas = [] for patron in PATRONES_AMBIGUEDAD_SINTACTICA: if re.search(patron["patron"], text, re.IGNORECASE): ambiguedades_sintacticas.append({ "tipo": patron["tipo"], "descripcion": patron["descripcion"] }) # Generar sugerencias sugerencias = [] if ambiguedades_lexicas or ambiguedades_sintacticas: for ambiguedad in ambiguedades_lexicas + ambiguedades_sintacticas: tipo = ambiguedad["tipo"] if tipo in SUGERENCIAS_MEJORA: sugerencias.extend(SUGERENCIAS_MEJORA[tipo]) # Calcular score de ambigüedad score = len(ambiguedades_lexicas) * 0.4 + len(ambiguedades_sintacticas) * 0.6 score_normalizado = min(1.0, score / 5.0) return { "tipo": "historia_usuario", "tiene_ambiguedad": bool(ambiguedades_lexicas or ambiguedades_sintacticas), "ambiguedad_lexica": [amb["descripcion"] for amb in ambiguedades_lexicas], "ambiguedad_sintactica": [amb["descripcion"] for amb in ambiguedades_sintacticas], "sugerencias": sugerencias if sugerencias else ["No se encontraron ambigüedades"], "score_ambiguedad": round(score_normalizado, 2) } def analyze_general_question(self, text: str) -> Dict: """ Analiza una pregunta general y proporciona una respuesta contextual. Args: text (str): Pregunta a analizar Returns: Dict: Resultado del análisis con información estructural y contextual """ doc = self.nlp(text.strip()) # Identificar el tipo de pregunta question_words = {"qué", "cuál", "cómo", "dónde", "cuándo", "por qué", "quién", "cuánto"} is_question = any(token.text.lower() in question_words for token in doc) # Extraer entidades nombradas entities = [(ent.text, ent.label_) for ent in doc.ents] # Analizar la estructura sintáctica root = [token for token in doc if token.dep_ == "ROOT"][0] main_verb = root.text if root.pos_ == "VERB" else None # Determinar el contexto de la pregunta context = { "is_question": is_question, "question_type": next((word for word in question_words if word in text.lower()), None), "entities": entities, "main_verb": main_verb, "key_phrases": [chunk.text for chunk in doc.noun_chunks] } return { "tipo": "pregunta_general", "analisis": context, "sugerencias": [ "Esta es una pregunta general que requiere información específica.", "Considera usar herramientas de búsqueda o consulta de datos para responderla." ] } def __call__(self, text: str) -> Dict: """ Procesa el texto y determina si es una historia de usuario o una pregunta general. Args: text (str): Texto a analizar Returns: Dict: Resultado del análisis según el tipo de texto """ if not text or not isinstance(text, str): return { "error": "El texto está vacío o no es válido", "tipo": "desconocido" } # Determinar el tipo de texto y analizarlo if self.is_user_story(text): return self.analyze_user_story(text) else: return self.analyze_general_question(text)