File size: 9,494 Bytes
a058b12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2cf4a9e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a058b12
 
 
 
2cf4a9e
a058b12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0779850
a058b12
0779850
 
a058b12
 
 
 
 
 
0779850
34a8cff
 
0779850
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
# core/integrations/telegram_bot.py
import os
import re
import tempfile
import time

import fitz  # PyMuPDF
from docx import Document
from dotenv import load_dotenv
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, InputFile, Update
from telegram.ext import (
    ApplicationBuilder,
    CallbackQueryHandler,
    CommandHandler,
    ContextTypes,
    MessageHandler,
    filters,
)

from core.integrations.doc_converter import gestionar_descarga, procesar_markdown
from core.logging.usage_logger import registrar_uso
from core.pipeline.edullm_rag_pipeline import edullm_rag_pipeline

# ==== CONFIGURACIÓN GENERAL ====
load_dotenv(dotenv_path="config/.env")
TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN")
DOCX_FILENAME = "material_educativo.docx"
FORMAT_WARNING_IMAGE = "assets/formatos_soportados.png"

if not TELEGRAM_TOKEN:
    raise ValueError("❌ TELEGRAM_TOKEN no está definido en las variables de entorno.")


# ==== FUNCIONES AUXILIARES ====
def extract_text_from_pdf(file_path):
    text = ""
    with fitz.open(file_path) as pdf:
        for page in pdf:
            text += page.get_text()
    return text.strip()


def extract_text_from_docx(file_path):
    doc = Document(file_path)
    return "\n".join(para.text for para in doc.paragraphs if para.text.strip())


def extract_text_from_txt(file_path):
    with open(file_path, "r", encoding="utf-8") as f:
        return f.read().strip()


def escape_markdown(text: str) -> str:
    """
    Escapa caracteres especiales para MarkdownV2 de Telegram.
    """
    escape_chars = r"_*[]()~`>#+-=|{}.!"
    return re.sub(f"([{re.escape(escape_chars)}])", r"\\\1", text)


def detectar_tipo_entrada(user_input) -> str:
    if isinstance(user_input, str):
        return "Texto"
    elif isinstance(user_input, bytes):
        return "Imagen"
    else:
        return "Otro"


# ==== COMANDO /start ====
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
    await update.message.reply_text(
        "👋 *¡Bienvenido a EduLLM Bot!*\n\n"
        "📌 *Formatos aceptados:* Texto, Imagen, PDF, DOCX o TXT.\n"
        "📄 *Formato que genero:* Material educativo listo para descargar en DOCX.\n\n"
        "✅ *¿Qué puedo generar?*\n"
        "Materiales educativos alineados al *CNEB, MBDD y MINEDU – Perú*, como:\n\n"
        "1️⃣ *Ficha*\n"
        "- Incluye: Metadatos, Resumen, Desarrollo, Preguntas DECO, Conclusión, Recomendación, Instrumento (opcional, debes indicar si quieres instrumentos de evaluación).\n\n"
        "2️⃣ *Resumen temático*\n"
        "- Incluye: Metadatos, Ideas clave (mínimo 3), Desarrollo, Conclusión.\n\n"
        "3️⃣ *Banco de preguntas*\n"
        "- Incluye: Metadatos, 10+ Preguntas DECO, Claves o respuestas (opcional, debes indicar que quieres respuestas).\n\n"
        "4️⃣ *Rúbrica o Lista de cotejo*\n"
        "- Incluye: Metadatos, Criterios, Niveles, Descriptores.\n\n"
        "🎯 *¿Qué necesito de ti?*\n"
        "Indícame: *área curricular*, *grado*, *bimestre*, *competencia*, *capacidad* y *desempeño esperado*.\n\n"
        "📌 *Ejemplo:*\n"
        "`Quiero 10 preguntas sobre los animales vertebrados para 4.º primaria (Ciencia y Tecnología, bim 1) con sus respectivas respuestas.`",
        parse_mode="Markdown",
    )



# ==== MANEJO DE MENSAJES ====
async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_input = ""

    try:
        if update.message.text:
            user_input = update.message.text

        elif update.message.photo:
            photo = update.message.photo[-1]
            file = await photo.get_file()
            with tempfile.NamedTemporaryFile(delete=False, suffix=".jpg") as temp_img:
                await file.download_to_drive(temp_img.name)
                with open(temp_img.name, "rb") as img_file:
                    user_input = img_file.read()

        elif update.message.document:
            file = await update.message.document.get_file()
            ext = update.message.document.file_name.split(".")[-1].lower()

            with tempfile.NamedTemporaryFile(delete=False, suffix=f".{ext}") as tmp_doc:
                await file.download_to_drive(tmp_doc.name)

                if ext == "pdf":
                    extracted_text = extract_text_from_pdf(tmp_doc.name)
                elif ext == "docx":
                    extracted_text = extract_text_from_docx(tmp_doc.name)
                elif ext == "txt":
                    extracted_text = extract_text_from_txt(tmp_doc.name)
                else:
                    await enviar_mensaje_formato_no_soportado(update)
                    return

                mensaje_texto = update.message.caption or ""
                user_input = f"{mensaje_texto}\n\n{extracted_text}".strip()

        elif update.message.audio or update.message.voice or update.message.video:
            await update.message.reply_text(
                "🎙️🎥 *Audios y videos no son compatibles.* Solo acepto texto, imágenes o documentos (PDF, DOCX, TXT).",
                parse_mode="Markdown",
            )
            return

        elif update.message.sticker:
            await update.message.reply_text(
                "🟢 Gracias por el sticker, pero necesito texto, imagen o documento educativo."
            )
            return

        elif update.message.location:
            await update.message.reply_text(
                "📍 He recibido tu ubicación, pero solo trabajo con contenido educativo."
            )
            return

        elif update.message.contact:
            await update.message.reply_text(
                "📞 Recibí un contacto, pero por favor envíame contenido académico (texto, imagen o documento)."
            )
            return

        elif update.message.animation:
            await update.message.reply_text(
                "🎞️ Los GIFs no son compatibles. Por favor envía texto, imagen o documentos."
            )
            return

        else:
            await enviar_mensaje_formato_no_soportado(update)
            return

    finally:
        for temp_var in ["temp_img", "tmp_doc"]:
            if temp_var in locals() and os.path.exists(locals()[temp_var].name):
                os.remove(locals()[temp_var].name)

    if not user_input:
        await update.message.reply_text("⚠️ No se pudo obtener contenido válido.")
        return

    await update.message.reply_text("⏳ Generando tu material educativo...")
    start_time = time.time()
    try:
        resultado_md = edullm_rag_pipeline(user_input)
        exito = True
    except Exception as e:
        resultado_md = f"❌ Error: {str(e)}"
        exito = False
    duracion = time.time() - start_time
    registrar_uso(
        user_id=update.effective_user.id,
        username=update.effective_user.username,
        tipo_entrada=detectar_tipo_entrada(user_input),
        duracion_segundos=duracion,
        exito=exito,
    )
    context.user_data["ultimo_markdown"] = resultado_md

    preview = resultado_md[:1000] + ("\n..." if len(resultado_md) > 1000 else "")
    preview_safe = escape_markdown(preview)
    await update.message.reply_text(
        f"✅ *Material generado*:\n\n```\n{preview_safe}\n```", parse_mode="MarkdownV2"
    )

    botones = [[InlineKeyboardButton("📄 Descargar DOCX", callback_data="descargar_docx")]]
    await update.message.reply_text(
        "¿Deseas descargar el material?", reply_markup=InlineKeyboardMarkup(botones)
    )


# ==== MENSAJE DE FORMATO NO SOPORTADO ====
async def enviar_mensaje_formato_no_soportado(update: Update):
    await update.message.reply_photo(
        photo=InputFile(FORMAT_WARNING_IMAGE),
        caption="⚠️ *Formato no soportado.*\n\nAcepto:\n- Texto\n- Imagen\n- PDF (.pdf)\n- Word (.docx)\n- Texto plano (.txt)",
        parse_mode=None,
    )


# ==== CALLBACK BOTONES ====
async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    await query.answer()

    if query.data == "descargar_docx":
        markdown_content = context.user_data.get("ultimo_markdown")
        if not markdown_content:
            await query.edit_message_text("⚠️ No hay material disponible para convertir.")
            return

        resultado = procesar_markdown(markdown_content)
        if "error" in resultado:
            await query.edit_message_text("❌ Error al generar el archivo DOCX.")
            return

        file_id = resultado["file_id"]
        file_response = gestionar_descarga(file_id)

        if isinstance(file_response, dict):
            await query.edit_message_text(f"⚠️ {file_response.get('error')}")
        else:
            await query.edit_message_text("📥 Aquí tienes tu archivo DOCX:")
            await context.bot.send_document(
                chat_id=query.message.chat_id,
                document=file_response.path,
                filename=DOCX_FILENAME,
            )


# ==== INICIAR BOT ====
async def start_bot():
    app = ApplicationBuilder().token(TELEGRAM_TOKEN).build()
    
    app.add_handler(CommandHandler("start", start))
    app.add_handler(MessageHandler(filters.ALL, handle_message))
    app.add_handler(CallbackQueryHandler(button_handler))

    print("🤖 EduLLM Bot en ejecución...")

    # 🔁 Esta secuencia evita que se cierre el event loop
    await app.initialize()
    await app.start()
    await app.updater.start_polling()