APP_SPACE / app.py
esab's picture
Distinguir pregunta conceptual (responder a fondo) de ejercicio concreto (guiar)
1365ed0 verified
Raw History Blame Contribute Delete
10 kB
import os
import re
import asyncio
import gradio as gr
from google import genai
from google.genai import types
from langdetect import detect, DetectorFactory
# Make language detection deterministic across restarts
DetectorFactory.seed = 0
api_key = os.getenv("GEMINI_API")
if not api_key:
raise RuntimeError(
"Falta la variable de entorno GEMINI_API. "
"Configúrala como secreto del Space (Settings > Variables and secrets)."
)
client = genai.Client(api_key=api_key)
MODEL_NAME = os.getenv("GEMINI_MODEL", "gemini-2.5-flash")
GENERATION_CONFIG = {
"temperature": 0.7,
"top_p": 0.9,
"top_k": 30,
"max_output_tokens": 2500,
}
# La detección automática de idioma no es fiable en frases cortas sin acentos,
# así que la instrucción pide además responder en el idioma que use el estudiante.
LANGUAGE_RULE_ES = (
"Responde siempre en el mismo idioma que utilice el estudiante "
"(castellano o catalán), aunque esta instrucción esté en otro idioma."
)
LANGUAGE_RULE_CA = (
"Respon sempre en el mateix idioma que faci servir l'estudiant "
"(català o castellà), encara que aquesta instrucció estigui en un altre idioma."
)
# Rol pedagógico: acompañar, no resolver. El objetivo es que el estudiante
# llegue por sí mismo a la solución, no entregarle el resultado hecho.
PEDAGOGY_ES = """Eres un tutor de estadística que acompaña a estudiantes de grado. \
Tu objetivo es que el estudiante aprenda a resolver por sí mismo. No eres un resolvedor de ejercicios.
Antes de responder, distingue SIEMPRE entre dos situaciones:
(a) PREGUNTA CONCEPTUAL O TEÓRICA ("qué es", "qué diferencia hay entre", "cómo se interpreta", \
"cuándo se usa", "por qué"). Respóndela directamente, a fondo y con ejemplos. NO le preguntes \
primero qué ha intentado: sería frustrante y no tiene sentido. Puedes terminar con una pregunta \
breve para comprobar que lo ha entendido, pero solo después de haber explicado.
(b) EJERCICIO CONCRETO que el estudiante debe resolver o entregar (trae datos, un enunciado, o pide \
"calcula", "resuélveme", "cuánto da"). Aquí es donde NO das el resultado: guías.
Cómo debes actuar en el caso (b):
- Cuando el estudiante plantee un ejercicio concreto, NO lo resuelvas. Empieza preguntándole qué ha \
intentado y qué concepto cree que se aplica.
- Guía con pistas graduales: primero una orientación general; si sigue atascado, una pista más concreta; \
y solo después el siguiente paso del razonamiento.
- Explica los conceptos con toda la profundidad que haga falta. Explicar teoría, definiciones e \
interpretaciones es tu trabajo principal y ahí debes ser generoso.
- Lo que no debes dar es el resultado final del ejercicio concreto que le han mandado.
- Si necesitas ilustrar un método, usa un ejemplo análogo con datos distintos a los del enunciado.
- Si el estudiante propone una respuesta, revísala: dile si el razonamiento es correcto o dónde falla, \
y por qué.
- Si insiste en que le des la solución, recuérdale con amabilidad que tu papel es ayudarle a llegar él \
mismo, y ofrécele la siguiente pista.
- Haz las preguntas de una en una y mantén las respuestas breves. No abrumes.
Sobre el código:
- Todo el código debe estar en R.
- Puedes mostrar funciones y sintaxis, pero aplicadas a un ejemplo propio, no al ejercicio del estudiante.
Límites:
- Trata únicamente temas de estadística básica. Si preguntan otra cosa, redirige con amabilidad."""
PEDAGOGY_CA = """Ets un tutor d'estadística que acompanya estudiants de grau. \
El teu objectiu és que l'estudiant aprengui a resoldre pel seu compte. No ets un resoledor d'exercicis.
Abans de respondre, distingeix SEMPRE entre dues situacions:
(a) PREGUNTA CONCEPTUAL O TEÒRICA ("què és", "quina diferència hi ha entre", "com s'interpreta", \
"quan es fa servir", "per què"). Respon-la directament, a fons i amb exemples. NO li preguntis \
primer què ha provat: seria frustrant i no té sentit. Pots acabar amb una pregunta breu per \
comprovar que ho ha entès, però només després d'haver explicat.
(b) EXERCICI CONCRET que l'estudiant ha de resoldre o entregar (porta dades, un enunciat, o demana \
"calcula", "resol-me", "quant dona"). Aquí és on NO dones el resultat: guies.
Com has d'actuar en el cas (b):
- Quan l'estudiant plantegi un exercici concret, NO el resolguis. Comença preguntant-li què ha provat \
i quin concepte creu que s'hi aplica.
- Guia amb pistes graduals: primer una orientació general; si continua encallat, una pista més concreta; \
i només després el pas següent del raonament.
- Explica els conceptes amb tota la profunditat que calgui. Explicar teoria, definicions i \
interpretacions és la teva feina principal i aquí has de ser generós.
- El que no has de donar és el resultat final de l'exercici concret que li han manat.
- Si necessites il·lustrar un mètode, fes servir un exemple anàleg amb dades diferents de les de \
l'enunciat.
- Si l'estudiant proposa una resposta, revisa-la: digues-li si el raonament és correcte o on falla, \
i per què.
- Si insisteix que li donis la solució, recorda-li amablement que el teu paper és ajudar-lo a arribar-hi \
ell mateix, i ofereix-li la pista següent.
- Fes les preguntes d'una en una i mantén les respostes breus. No aclaparis.
Sobre el codi:
- Tot el codi ha de ser en R.
- Pots mostrar funcions i sintaxi, però aplicades a un exemple propi, no a l'exercici de l'estudiant.
Límits:
- Tracta únicament temes d'estadística bàsica. Si pregunten una altra cosa, redirigeix amablement."""
instructions = {
"es": PEDAGOGY_ES + "\n\n" + LANGUAGE_RULE_ES,
"ca": PEDAGOGY_CA + "\n\n" + LANGUAGE_RULE_CA,
}
INAPPROPRIATE_PATTERNS = [
re.compile(r"\b(joder|maleducado)\b", re.IGNORECASE),
]
def detect_language(user_input):
try:
lang = detect(user_input)
except Exception:
return "es"
return lang if lang in instructions else "es"
def contains_inappropriate_language(text):
return any(pattern.search(text) for pattern in INAPPROPRIATE_PATTERNS)
def create_chat_session(system_instruction):
return client.chats.create(
model=MODEL_NAME,
config=types.GenerateContentConfig(
system_instruction=system_instruction,
**GENERATION_CONFIG,
),
)
async def chatbot_async(user_input, history, chat_session):
user_input = (user_input or "").strip()
if not user_input:
return history, history, chat_session, ""
history = history + [{"role": "user", "content": user_input}]
if contains_inappropriate_language(user_input):
warning = (
"El lenguaje inapropiado no está permitido. "
"Por favor, formula tu pregunta de manera respetuosa."
)
history.append({"role": "assistant", "content": warning})
return history, history, chat_session, ""
try:
if chat_session is None:
lang = detect_language(user_input)
chat_session = create_chat_session(instructions[lang])
response = await asyncio.to_thread(chat_session.send_message, user_input)
reply = (response.text or "").strip() or "No he podido generar una respuesta."
except Exception as exc:
reply = f"Ha ocurrido un error: {exc}"
history.append({"role": "assistant", "content": reply})
return history, history, chat_session, ""
def reset_chat():
return [], [], None, ""
# Paleta alineada con Atenea / Moodle Boost (azul primario #0f6cbf)
UPC_BLUE = gr.themes.Color(
c50="#e9f2fa", c100="#cfe2f3", c200="#a4c8e8", c300="#78addc",
c400="#4d93d1", c500="#0f6cbf", c600="#0d5da4", c700="#0a4a83",
c800="#083762", c900="#052440", c950="#03182b",
)
theme = gr.themes.Soft(
primary_hue=UPC_BLUE,
secondary_hue=UPC_BLUE,
neutral_hue="slate",
font=[gr.themes.GoogleFont("Open Sans"), "system-ui", "sans-serif"],
).set(
body_background_fill="#ffffff",
block_background_fill="#ffffff",
block_border_width="1px",
block_shadow="none",
button_primary_background_fill="#0f6cbf",
button_primary_background_fill_hover="#0d5da4",
button_primary_text_color="#ffffff",
)
# Oculta el pie de Gradio y ajusta el marco para que no compita con la pagina de Atenea
CUSTOM_CSS = """
footer { display: none !important; }
.gradio-container { max-width: 100% !important; padding: 8px !important; }
#chat-header { margin-bottom: 4px; }
#chat-header h2 { color: #0f6cbf; font-size: 1.15rem; margin: 0 0 2px 0; }
#chat-header p { color: #555; font-size: 0.9rem; margin: 0; }
"""
with gr.Blocks(title="Tutor d'Estadística") as demo:
gr.HTML(
"<div id='chat-header'>"
"<h2>Tutor virtual d&apos;Estad&iacute;stica</h2>"
"<p>T&apos;ajudo a arribar tu mateix a la soluci&oacute;: no et dono l&apos;exercici resolt. Pregunta en catal&agrave; o en castellano. El codi, en R.</p>"
"</div>"
)
chatbot_interface = gr.Chatbot(
height=430,
show_label=False,
buttons=["copy"],
placeholder="<em>Explica'm qu\u00e8 has provat i on t'has encallat.</em>",
)
state = gr.State([]) # chat history in messages format
chat_session = gr.State(None) # google-genai chat session
with gr.Row():
txt = gr.Textbox(
show_label=False,
placeholder="Escribe tu pregunta aquí…",
scale=8,
autofocus=True,
container=False,
)
send_button = gr.Button("Enviar", variant="primary", scale=1, min_width=110)
clear_button = gr.Button("Netejar", variant="secondary", scale=1, min_width=110)
inputs = [txt, state, chat_session]
outputs = [chatbot_interface, state, chat_session, txt]
txt.submit(chatbot_async, inputs, outputs)
send_button.click(chatbot_async, inputs, outputs)
clear_button.click(reset_chat, None, outputs)
if __name__ == "__main__":
demo.launch(theme=theme, css=CUSTOM_CSS, footer_links=[])