File size: 11,635 Bytes
bb6efdb
 
9b6cc0b
 
bb6efdb
dd6f32f
9b6cc0b
bb6efdb
9b6cc0b
dd6f32f
bb6efdb
 
 
 
 
9b6cc0b
dd6f32f
9b6cc0b
 
 
 
 
 
 
dd6f32f
9b6cc0b
 
 
bb6efdb
 
 
 
dd6f32f
 
bb6efdb
 
 
 
 
 
 
 
9b6cc0b
bb6efdb
 
 
dd6f32f
bb6efdb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9b6cc0b
 
 
 
 
 
 
 
 
bb6efdb
 
9b6cc0b
 
 
 
 
 
 
 
 
bb6efdb
 
9b6cc0b
 
 
 
 
 
 
 
 
bb6efdb
 
9b6cc0b
 
 
 
 
 
 
 
 
bb6efdb
 
 
 
 
 
 
 
9b6cc0b
 
 
 
dd6f32f
9b6cc0b
 
dd6f32f
9b6cc0b
 
 
 
 
 
 
dd6f32f
9b6cc0b
 
 
 
bb6efdb
9b6cc0b
dd6f32f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bb6efdb
dd6f32f
 
 
 
 
 
 
 
 
bb6efdb
dd6f32f
 
 
 
 
 
 
 
 
9b6cc0b
dd6f32f
 
 
 
 
 
 
 
 
9b6cc0b
dd6f32f
 
 
 
 
bb6efdb
dd6f32f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
# modules/chatbot/chat_process.py
import os
import json
import boto3
import logging
import time
import base64
from typing import Generator
from botocore.config import Config
from botocore.exceptions import ClientError

logger = logging.getLogger(__name__)

class ChatProcessor:
    def __init__(self):
        """Inicializa el procesador de chat con AWS Bedrock (Jamba 1.5 Large)"""
        # Configurar cliente de Bedrock con más reintentos
        self.bedrock = boto3.client(
            'bedrock-runtime',
            region_name=os.environ.get("AWS_REGION", "us-east-1"),
            aws_access_key_id=os.environ.get("AWS_ACCESS_KEY_ID"),
            aws_secret_access_key=os.environ.get("AWS_SECRET_ACCESS_KEY"),
            config=Config(
                retries={
                    'max_attempts': 5,
                    'mode': 'adaptive'
                }
            )
        )
        self.conversation_history = []
        self.semantic_context = None
        self.current_lang = 'en'
        self.last_request_time = 0
        self.min_request_interval = 2.0  # Mínimo 2 segundos entre peticiones

    def set_semantic_context(self, text, metrics, graph_data, lang_code='en'):
        """Configura el contexto semántico completo para el chat"""
        if not text or not metrics:
            logger.error("Faltan datos esenciales para el contexto semántico")
            raise ValueError("Texto y métricas son requeridos")
            
        self.semantic_context = {
            'full_text': text,
            'key_concepts': metrics.get('key_concepts', []),
            'concept_centrality': metrics.get('concept_centrality', {}),
            'graph_available': graph_data is not None,
            'graph_data': graph_data,
            'language': lang_code
        }
        self.current_lang = lang_code
        self.conversation_history = []
        logger.info("Contexto semántico configurado correctamente")

    def _get_system_prompt(self):
        """Genera el prompt del sistema con todo el contexto necesario"""
        if not self.semantic_context:
            return "You are a helpful assistant."
            
        concepts = self.semantic_context['key_concepts']
        top_concepts = ", ".join([f"{c[0]} ({c[1]:.2f})" for c in concepts[:5]])
        
        prompts = {
            'en': f"""You are a semantic analysis expert. The user analyzed a research article.
Full text available (abbreviated for context).
Key concepts: {top_concepts}
Graph available: {self.semantic_context['graph_available']}

Your tasks:
1. Answer questions about concepts and their relationships
2. Explain the semantic network structure
3. Suggest text improvements
4. Provide insights based on concept centrality""",
            
            'es': f"""Eres un experto en análisis semántico. El usuario analizó un artículo de investigación.
Texto completo disponible (abreviado para contexto).
Conceptos clave: {top_concepts}
Gráfico disponible: {self.semantic_context['graph_available']}

Tus tareas:
1. Responder preguntas sobre conceptos y sus relaciones
2. Explicar la estructura de la red semántica
3. Sugerir mejoras al texto
4. Proporcionar insights basados en centralidad de conceptos""",
            
            'pt': f"""Você é um especialista em análise semântica. O usuário analisou um artigo de pesquisa.
Texto completo disponível (abreviado para contexto).
Conceitos-chave: {top_concepts}
Gráfico disponível: {self.semantic_context['graph_available']}

Suas tarefas:
1. Responder perguntas sobre conceitos e suas relações
2. Explicar a estrutura da rede semântica
3. Sugerir melhorias no texto
4. Fornecer insights com base na centralidade dos conceitos""",
            
            'fr': f"""Vous êtes un expert en analyse sémantique. L'utilisateur a analysé un article de recherche.
Texte complet disponible (abrégé pour le contexte).
Concepts clés: {top_concepts}
Graphique disponible: {self.semantic_context['graph_available']}

Vos tâches:
1. Répondre aux questions sur les concepts et leurs relations
2. Expliquer la structure du réseau sémantique
3. Suggérer des améliorations de texte
4. Fournir des insights basés sur la centralité des concepts"""
        }
        
        return prompts.get(self.current_lang, prompts['en'])

    def clean_generated_text(self, text):
        """Limpia caracteres especiales del texto generado"""
        return text.replace("\u2588", "").replace("▌", "").strip()

    def _build_multimodal_content(self, message):
        """Construye el contenido multimodal con texto + grafo si está disponible"""
        content_parts = []
        
        # 1. Añadir el texto del documento (reducido para ahorrar tokens)
        if self.semantic_context and 'full_text' in self.semantic_context:
            content_parts.append(
                f"Documento analizado (extracto):\n{self.semantic_context['full_text'][:1000]}..."
            )
        
        # 2. Añadir conceptos clave
        if self.semantic_context and 'key_concepts' in self.semantic_context:
            concepts = self.semantic_context['key_concepts'][:5]
            content_parts.append(f"Conceptos clave: {concepts}")
        
        # 3. Añadir el mensaje actual del usuario
        content_parts.append(f"Pregunta del usuario: {message}")
        
        return "\n\n".join(content_parts)

    def process_chat_input(self, message: str, lang_code: str) -> Generator[str, None, None]:
        """Procesa el mensaje con todo el contexto disponible usando Jamba 1.5 en Bedrock"""
        max_retries = 3
        base_delay = 5
        
        for attempt in range(max_retries):
            try:
                if not self.semantic_context:
                    yield "Error: Contexto semántico no configurado. Recargue el análisis."
                    return
                    
                # Actualizar idioma si es diferente
                if lang_code != self.current_lang:
                    self.current_lang = lang_code
                    logger.info(f"Idioma cambiado a: {lang_code}")

                # Control de tasa simple (no más de 1 petición cada 2 segundos)
                current_time = time.time()
                time_since_last = current_time - self.last_request_time
                if time_since_last < self.min_request_interval:
                    sleep_time = self.min_request_interval - time_since_last
                    logger.info(f"Respetando intervalo mínimo: esperando {sleep_time:.2f}s")
                    time.sleep(sleep_time)

                # Construir el contenido multimodal
                user_content = self._build_multimodal_content(message)

                # Construir mensajes para Jamba
                messages = []
                
                # Añadir system prompt
                messages.append({
                    "role": "system",
                    "content": self._get_system_prompt()
                })
                
                # Añadir historial de conversación (últimos 4 intercambios)
                for msg in self.conversation_history[-8:]:
                    messages.append(msg)
                
                # Añadir mensaje actual del usuario
                messages.append({
                    "role": "user",
                    "content": user_content
                })

                # Preparar el cuerpo de la petición para Jamba 1.5 Large
                request_body = {
                    "messages": messages,
                    "max_tokens": 1500,  # Reducido de 2000 a 1500 para ahorrar tokens
                    "temperature": 0.7,
                    "top_p": 0.9,
                    "stop": [],
                    "n": 1
                }

                logger.info(f"Enviando petición a Jamba (intento {attempt + 1}/{max_retries})")
                
                # Llamar a Bedrock
                response = self.bedrock.invoke_model(
                    modelId='ai21.jamba-1-5-large-v1:0',
                    contentType='application/json',
                    accept='application/json',
                    body=json.dumps(request_body)
                )

                # Actualizar tiempo de última petición
                self.last_request_time = time.time()

                # Procesar la respuesta
                response_body = json.loads(response['body'].read())
                
                # Extraer el texto de la respuesta
                if 'choices' in response_body and len(response_body['choices']) > 0:
                    full_response = response_body['choices'][0]['message']['content']
                else:
                    full_response = "Lo siento, no pude generar una respuesta."

                # Limpiar la respuesta
                clean_response = self.clean_generated_text(full_response)

                # Simular streaming
                chunk_size = 50
                for i in range(0, len(clean_response), chunk_size):
                    yield clean_response[i:i+chunk_size]
                
                # Guardar respuesta en historial
                self.conversation_history.append({"role": "user", "content": message})
                self.conversation_history.append({"role": "assistant", "content": clean_response})
                
                # Mantener historial manejable
                if len(self.conversation_history) > 40:
                    self.conversation_history = self.conversation_history[-40:]
                    
                logger.info("Respuesta generada y guardada en historial")
                return  # Éxito, salir del bucle

            except ClientError as e:
                error_code = e.response['Error']['Code']
                error_message = e.response['Error']['Message']
                
                if error_code == 'ThrottlingException' and attempt < max_retries - 1:
                    wait_time = base_delay * (2 ** attempt)  # 5, 10, 20 segundos
                    logger.warning(f"Throttling detectado. Esperando {wait_time}s (intento {attempt+1}/{max_retries})")
                    
                    # Mensaje amigable para el usuario
                    if attempt == 0:
                        yield "⏳ El sistema está procesando muchas solicitudes. Espera un momento..."
                    
                    time.sleep(wait_time)
                else:
                    logger.error(f"Error en process_chat_input: {error_code} - {error_message}", exc_info=True)
                    error_messages = {
                        'en': "Error processing message. Please try again in a moment.",
                        'es': "Error al procesar mensaje. Intente nuevamente en un momento.",
                        'pt': "Erro ao processar mensagem. Tente novamente em um momento.",
                        'fr': "Erreur lors du traitement. Réessayez dans un moment."
                    }
                    yield error_messages.get(self.current_lang, "Processing error")
                    return
                    
            except Exception as e:
                logger.error(f"Error inesperado en process_chat_input: {str(e)}", exc_info=True)
                error_messages = {
                    'en': "Unexpected error. Please try again.",
                    'es': "Error inesperado. Intente nuevamente.",
                    'pt': "Erro inesperado. Tente novamente.",
                    'fr': "Erreur inattendue. Réessayez."
                }
                yield error_messages.get(self.current_lang, "Processing error")
                return