|
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 |
|
""" |
|
|
|
for pattern in USER_STORY_PATTERNS.values(): |
|
if re.match(pattern, text): |
|
return True |
|
|
|
|
|
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()) |
|
|
|
|
|
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"] |
|
}) |
|
|
|
|
|
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"] |
|
}) |
|
|
|
|
|
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]) |
|
|
|
|
|
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()) |
|
|
|
|
|
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) |
|
|
|
|
|
entities = [(ent.text, ent.label_) for ent in doc.ents] |
|
|
|
|
|
root = [token for token in doc if token.dep_ == "ROOT"][0] |
|
main_verb = root.text if root.pos_ == "VERB" else None |
|
|
|
|
|
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" |
|
} |
|
|
|
|
|
if self.is_user_story(text): |
|
return self.analyze_user_story(text) |
|
else: |
|
return self.analyze_general_question(text) |