Spaces:
No application file
No application file
| 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) |