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( "
" "

Tutor virtual d'Estadística

" "

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.

" "
" ) chatbot_interface = gr.Chatbot( height=430, show_label=False, buttons=["copy"], placeholder="Explica'm qu\u00e8 has provat i on t'has encallat.", ) 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=[])