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) |