File size: 10,023 Bytes
80bf7f7 0ef5320 ed5060c dc4e03b d7ddcbf dd228dd d7ddcbf dc4e03b d7ddcbf cd8885f ed5060c dc4e03b d7ddcbf dd228dd 98858d7 d7ddcbf 98858d7 d7ddcbf 98858d7 1365ed0 98858d7 1365ed0 98858d7 e65b8fe d39cbd7 98858d7 d39cbd7 d7ddcbf ed5060c d7ddcbf a6dfc22 0ef5320 d7ddcbf ed5060c d7ddcbf 0ef5320 d7ddcbf dd228dd d7ddcbf 0ef5320 d7ddcbf 0ef5320 d7ddcbf dc4e03b d7ddcbf dc4e03b 2647873 d7ddcbf dc4e03b a6dfc22 d7ddcbf a6dfc22 0ef5320 dd228dd 98858d7 dd228dd d7ddcbf 0ef5320 dd228dd 98858d7 dd228dd d7ddcbf a6dfc22 d7ddcbf dd228dd d7ddcbf dd228dd d7ddcbf dd228dd d7ddcbf dd228dd | 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 | 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'Estadística</h2>"
"<p>T'ajudo a arribar tu mateix a la solució: no et dono l'exercici resolt. Pregunta en català 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=[])
|