File size: 7,397 Bytes
4dcc5d7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
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)