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&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=[])