From 52c5759c5d3a64f9fd3f14e9d92dd5f46b8b7b06 Mon Sep 17 00:00:00 2001 From: Yahiewi Date: Mon, 6 Jul 2026 12:55:26 +0200 Subject: [PATCH 1/4] Parse reference solutions and evaluation criteria --- examples/matplotlib_exercices.ipynb | 104 ++++++++++++++++++++++++++-- jupyter_ai_tutor/handlers.py | 24 +++++-- src/api.ts | 27 ++++++-- src/index.ts | 15 +++- src/model.ts | 14 ++-- src/utils.ts | 42 +++++++++++ 6 files changed, 201 insertions(+), 25 deletions(-) diff --git a/examples/matplotlib_exercices.ipynb b/examples/matplotlib_exercices.ipynb index bac0ec7..b11a29d 100644 --- a/examples/matplotlib_exercices.ipynb +++ b/examples/matplotlib_exercices.ipynb @@ -16,7 +16,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "bad3907c-9b3e-446c-aa52-71d63a1d72cb", "metadata": {}, "outputs": [], @@ -46,7 +46,33 @@ "cell_type": "code", "execution_count": null, "id": "15a20896-1822-4b7f-89db-bf5ef6682aaf", - "metadata": {}, + "metadata": { + "deletable": true, + "editable": true, + "evaluation_criteria": { + "base": "max", + "criteria": [ + { + "description": "Correct 'np.linsapce' typo to 'np.linspace'.", + "label": "Typo Correction", + "quarterpoints": 8, + "type": "malus" + }, + { + "description": "Ensure the Sine and Cosine waves plot is correctly rendered without runtime errors.", + "label": "Plot Output", + "quarterpoints": 4, + "type": "bonus" + } + ], + "points": 2 + }, + "reference_solution": "vzcbeg zngcybgyvo.clcybg nf cyg\nvzcbeg ahzcl nf ac\nk = ac.yvafcnpr(0, 2 * ac.cv, 100)\nsvt, nk = cyg.fhocybgf(svtfvmr=(8, 4))\nnk.cybg(k, ac.fva(k), ynory='fva(k)', pbybe='eblnyoyhr')\nnk.cybg(k, ac.pbf(k), ynory='pbf(k)', pbybe='gbzngb', yvarfglyr='--')\nnk.frg_gvgyr('Fvar naq Pbfvar Jnirf')\nnk.frg_kynory('k')\nnk.frg_lynory('l')\nnk.yrtraq()\nnk.tevq(Gehr, nycun=0.3)\ncyg.gvtug_ynlbhg()\ncyg.fubj()", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, "outputs": [], "source": [ "# 🐞 There is a bug in this cell — find and fix it!\n", @@ -86,7 +112,39 @@ "cell_type": "code", "execution_count": null, "id": "7a984913-5581-4ca5-a7ad-c16dc21c108d", - "metadata": {}, + "metadata": { + "deletable": true, + "editable": true, + "evaluation_criteria": { + "base": "0", + "criteria": [ + { + "description": "Uses ax.bar with categories and values.", + "label": "Bar chart creation", + "quarterpoints": 4, + "type": "bonus" + }, + { + "description": "Applies a different color to each bar.", + "label": "Aesthetics & Colors", + "quarterpoints": 4, + "type": "bonus" + }, + { + "description": "Correctly loops over the bars to add text scores above each bar.", + "label": "Value Labels", + "quarterpoints": 4, + "type": "bonus" + } + ], + "points": 3 + }, + "reference_solution": "pngrtbevrf = ['Clguba', 'WninFpevcg', 'Ehfg', 'Tb', 'Whyvn']\ninyhrf = [85, 72, 58, 45, 30]\nsvt, nk = cyg.fhocybgf(svtfvmr=(8, 4))\npbybef = ['eblnyoyhr', 'gbzngb', 'zrqvhzfrnterra', 'benatr', 'zrqvhzchecyr']\nonef = nk.one(pngrtbevrf, inyhrf, pbybe=pbybef)\nnk.frg_gvgyr('Cebtenzzvat Ynathntr Fpberf')\nnk.frg_lynory('Fpber')\nsbe one va onef:\n liny = one.trg_urvtug()\n nk.grkg(one.trg_k() + one.trg_jvqgu()/2.0, liny + 1, fge(liny), un='pragre', in='obggbz')\ncyg.gvtug_ynlbhg()\ncyg.fubj()", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, "outputs": [], "source": [ "# Data is provided — write your bar chart code below!\n", @@ -96,6 +154,10 @@ "# YOUR CODE HERE\n", "fig, ax = plt.subplots(figsize=(8, 4))\n", "\n", + "\n", + "# Intentional syntax error\n", + "ax.bar(categories, values\n", + "\n", "# 1. Create the bar chart with different colors for each bar\n", "# ...\n", "\n", @@ -131,7 +193,39 @@ "cell_type": "code", "execution_count": null, "id": "957d292f-e46d-4e7d-ba1a-4adafce9a820", - "metadata": {}, + "metadata": { + "deletable": true, + "editable": true, + "evaluation_criteria": { + "base": "max", + "criteria": [ + { + "description": "Fails to set random seed 42, generating incorrect data coordinates.", + "label": "Random data seed", + "quarterpoints": 4, + "type": "malus" + }, + { + "description": "Missing colorbar, title, grid, or x/y labels on the axes.", + "label": "Colorbar & Labels", + "quarterpoints": 4, + "type": "malus" + }, + { + "description": "Fails to set alpha=0.6, colormap='viridis', size mapping, or color mapping.", + "label": "Scatter aesthetic parameters", + "quarterpoints": 4, + "type": "malus" + } + ], + "points": 3 + }, + "reference_solution": "ac.enaqbz.frrq(42)\na = 150\nk = ac.enaqbz.enaqa(a)\nl = ac.enaqbz.enaqa(a)\nfvmrf = ac.enaqbz.havsbez(20, 200, a)\npbybef = ac.enaqbz.enaq(a)\nsvt, nk = cyg.fhocybgf(svtfvmr=(8, 6))\nfpnggre = nk.fpnggre(k, l, p=pbybef, f=fvmrf, pznc='ivevqvf', nycun=0.6)\ncyg.pbybeone(fpnggre, nk=nk, ynory='Pbybe inyhr')\nnk.frg_gvgyr('Enaqbz Fpnggre Cybg')\nnk.frg_kynory('K inyhrf')\nnk.frg_lynory('L inyhrf')\nnk.tevq(Gehr, nycun=0.3)\ncyg.gvtug_ynlbhg()\ncyg.fubj()", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, "outputs": [], "source": [ "# YOUR CODE HERE\n", @@ -178,7 +272,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.13" + "version": "3.14.6" } }, "nbformat": 4, diff --git a/jupyter_ai_tutor/handlers.py b/jupyter_ai_tutor/handlers.py index 65e966a..55bad98 100644 --- a/jupyter_ai_tutor/handlers.py +++ b/jupyter_ai_tutor/handlers.py @@ -9,16 +9,26 @@ class ExplainHandler(APIHandler): @tornado.web.authenticated async def post(self): body = self.get_json_body() - if not body or "body" not in body: - raise tornado.web.HTTPError(400, "Missing 'body' field in request") + if not body: + raise tornado.web.HTTPError(400, "Missing request body") - message_body = body["body"] + message_body = body.get("body", "") description = body.get("description", "") + reference_solution = body.get("reference_solution", "") + evaluation_criteria = body.get("evaluation_criteria", "") + + formatted_message = "" if description: - message_body = ( - f"\n{description}\n\n\n" - f"{message_body}" - ) + formatted_message += f"\n{description}\n\n\n" + + formatted_message += f"{message_body}" + + if reference_solution: + formatted_message += f"\n\n\n{reference_solution}\n" + if evaluation_criteria: + formatted_message += f"\n\n\n{evaluation_criteria}\n" + + message_body = formatted_message.strip() config_manager = self.settings.get("jupyternaut.config_manager") if not config_manager: diff --git a/src/api.ts b/src/api.ts index 5f6c01d..beb8b10 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,16 +1,24 @@ import { URLExt } from '@jupyterlab/coreutils'; import { ServerConnection } from '@jupyterlab/services'; +/** + * Options for streaming the explanation request. + */ +export interface IStreamExplanationOptions { + body: string; + description?: string; + signal?: AbortSignal; + referenceSolution?: string; + evaluationCriteria?: string; +} + /** * Streams the tutor explanation for the given message body via SSE. * Yields text chunks as they arrive from the backend. - * @param body - The user message (code + question) - * @param description - Optional exercise description from preceding markdown cells + * @param options - The request options containing body and structured contexts */ export async function* streamExplanation( - body: string, - description?: string, - signal?: AbortSignal + options: IStreamExplanationOptions ): AsyncGenerator { const settings = ServerConnection.makeSettings(); const url = URLExt.join(settings.baseUrl, 'api/jupyter-ai-tutor/explain'); @@ -19,9 +27,14 @@ export async function* streamExplanation( url, { method: 'POST', - body: JSON.stringify({ body, description }), + body: JSON.stringify({ + body: options.body, + description: options.description, + reference_solution: options.referenceSolution, + evaluation_criteria: options.evaluationCriteria + }), headers: { 'Content-Type': 'application/json' }, - signal + signal: options.signal }, settings ); diff --git a/src/index.ts b/src/index.ts index 82bd3d1..3da22a4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,7 +19,7 @@ import { infoIcon } from '@jupyterlab/ui-components'; import { clearItem, stopItem } from './components'; import { TUTOR_USER, TutorChatModel } from './model'; -import { isContinuous } from './utils'; +import { decodeRot13, formatEvaluationCriteria, isContinuous } from './utils'; const INFO_ICON_BASE_64 = btoa(infoIcon.svgstr); @@ -242,6 +242,17 @@ const plugin: JupyterFrontEndPlugin = { } } + // Retrieve and decode reference_solution from metadata + const rawSolution = cell.model.getMetadata('reference_solution'); + const referenceSolution = + typeof rawSolution === 'string' ? decodeRot13(rawSolution) : ''; + + // Retrieve and format evaluation_criteria from metadata + const rawCriteria = + cell.model.getMetadata('evaluation_criteria') || + cell.model.getMetadata('criteria'); + const evaluationCriteria = formatEvaluationCriteria(rawCriteria); + const question = errorSection ? 'Can you explain this code and the error it produced?' : 'Can you explain this code?'; @@ -255,6 +266,8 @@ const plugin: JupyterFrontEndPlugin = { await tutorModel.sendMessageToAI({ body, description, + referenceSolution, + evaluationCriteria, attachments: attachment ? [attachment] : undefined }); }, diff --git a/src/model.ts b/src/model.ts index fb1aa42..5699f99 100644 --- a/src/model.ts +++ b/src/model.ts @@ -15,6 +15,8 @@ import { AI_AVATAR } from './icons'; interface ITutorNewMessage extends INewMessage { attachments?: IAttachment[]; description?: string; + referenceSolution?: string; + evaluationCriteria?: string; } export const TUTOR_USER: IUser = { @@ -87,11 +89,13 @@ export class TutorChatModel extends AbstractChatModel { try { let accumulated = ''; - for await (const chunk of streamExplanation( - message.body, - message.description, - this._abortController.signal - )) { + for await (const chunk of streamExplanation({ + body: message.body, + description: message.description, + signal: this._abortController.signal, + referenceSolution: message.referenceSolution, + evaluationCriteria: message.evaluationCriteria + })) { accumulated += chunk; streamingMsg.update({ body: accumulated }); } diff --git a/src/utils.ts b/src/utils.ts index 18f73c6..d866ac1 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -11,3 +11,45 @@ export function isContinuous(numbers: number[]): boolean { return true; } + +export interface ICriteriaItem { + label?: string; + title?: string; + name?: string; + description?: string; + desc?: string; +} + +/** + * Decodes a ROT13 encoded string. + */ +export function decodeRot13(str: string): string { + return str.replace(/[a-zA-Z]/g, c => { + const base = c <= 'Z' ? 65 : 97; + return String.fromCharCode(((c.charCodeAt(0) - base + 13) % 26) + base); + }); +} + +export function formatEvaluationCriteria(criteriaData: unknown): string { + if (!criteriaData) { + return ''; + } + if (typeof criteriaData === 'string') { + return criteriaData; + } + + const data = criteriaData as { criteria?: ICriteriaItem[] } | ICriteriaItem[]; + const list = Array.isArray(data) ? data : data.criteria || []; + + return list + .map(item => { + if (item && typeof item === 'object') { + const label = item.label || item.title || item.name || ''; + const desc = item.description || item.desc || ''; + return label && desc ? `- ${label}: ${desc}` : `- ${label || desc}`; + } + return typeof item === 'string' ? `- ${item}` : ''; + }) + .filter(Boolean) + .join('\n'); +} From 3420dca930771a503c08e5d401fa717e7d189880 Mon Sep 17 00:00:00 2001 From: Yahiewi Date: Thu, 9 Jul 2026 10:51:22 +0100 Subject: [PATCH 2/4] Simplify evaluation criteria format and backend code --- examples/matplotlib_exercices.ipynb | 82 ++++++----------------------- examples/rich_tutor_example.ipynb | 51 ++++++++++++++++++ jupyter_ai_tutor/handlers.py | 24 +++------ src/api.ts | 27 +++------- src/index.ts | 23 +++++--- src/model.ts | 14 ++--- src/utils.ts | 30 +++-------- 7 files changed, 107 insertions(+), 144 deletions(-) create mode 100644 examples/rich_tutor_example.ipynb diff --git a/examples/matplotlib_exercices.ipynb b/examples/matplotlib_exercices.ipynb index b11a29d..660a778 100644 --- a/examples/matplotlib_exercices.ipynb +++ b/examples/matplotlib_exercices.ipynb @@ -16,7 +16,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "bad3907c-9b3e-446c-aa52-71d63a1d72cb", "metadata": {}, "outputs": [], @@ -49,24 +49,10 @@ "metadata": { "deletable": true, "editable": true, - "evaluation_criteria": { - "base": "max", - "criteria": [ - { - "description": "Correct 'np.linsapce' typo to 'np.linspace'.", - "label": "Typo Correction", - "quarterpoints": 8, - "type": "malus" - }, - { - "description": "Ensure the Sine and Cosine waves plot is correctly rendered without runtime errors.", - "label": "Plot Output", - "quarterpoints": 4, - "type": "bonus" - } - ], - "points": 2 - }, + "evaluation_criteria": [ + "Typo Correction: Correct 'np.linsapce' typo to 'np.linspace'.", + "Plot Output: Ensure the Sine and Cosine waves plot is correctly rendered without runtime errors." + ], "reference_solution": "vzcbeg zngcybgyvo.clcybg nf cyg\nvzcbeg ahzcl nf ac\nk = ac.yvafcnpr(0, 2 * ac.cv, 100)\nsvt, nk = cyg.fhocybgf(svtfvmr=(8, 4))\nnk.cybg(k, ac.fva(k), ynory='fva(k)', pbybe='eblnyoyhr')\nnk.cybg(k, ac.pbf(k), ynory='pbf(k)', pbybe='gbzngb', yvarfglyr='--')\nnk.frg_gvgyr('Fvar naq Pbfvar Jnirf')\nnk.frg_kynory('k')\nnk.frg_lynory('l')\nnk.yrtraq()\nnk.tevq(Gehr, nycun=0.3)\ncyg.gvtug_ynlbhg()\ncyg.fubj()", "slideshow": { "slide_type": "" @@ -115,30 +101,11 @@ "metadata": { "deletable": true, "editable": true, - "evaluation_criteria": { - "base": "0", - "criteria": [ - { - "description": "Uses ax.bar with categories and values.", - "label": "Bar chart creation", - "quarterpoints": 4, - "type": "bonus" - }, - { - "description": "Applies a different color to each bar.", - "label": "Aesthetics & Colors", - "quarterpoints": 4, - "type": "bonus" - }, - { - "description": "Correctly loops over the bars to add text scores above each bar.", - "label": "Value Labels", - "quarterpoints": 4, - "type": "bonus" - } - ], - "points": 3 - }, + "evaluation_criteria": [ + "Bar chart creation: Uses ax.bar with categories and values.", + "Aesthetics & Colors: Applies a different color to each bar.", + "Value Labels: Correctly loops over the bars to add text scores above each bar." + ], "reference_solution": "pngrtbevrf = ['Clguba', 'WninFpevcg', 'Ehfg', 'Tb', 'Whyvn']\ninyhrf = [85, 72, 58, 45, 30]\nsvt, nk = cyg.fhocybgf(svtfvmr=(8, 4))\npbybef = ['eblnyoyhr', 'gbzngb', 'zrqvhzfrnterra', 'benatr', 'zrqvhzchecyr']\nonef = nk.one(pngrtbevrf, inyhrf, pbybe=pbybef)\nnk.frg_gvgyr('Cebtenzzvat Ynathntr Fpberf')\nnk.frg_lynory('Fpber')\nsbe one va onef:\n liny = one.trg_urvtug()\n nk.grkg(one.trg_k() + one.trg_jvqgu()/2.0, liny + 1, fge(liny), un='pragre', in='obggbz')\ncyg.gvtug_ynlbhg()\ncyg.fubj()", "slideshow": { "slide_type": "" @@ -196,30 +163,11 @@ "metadata": { "deletable": true, "editable": true, - "evaluation_criteria": { - "base": "max", - "criteria": [ - { - "description": "Fails to set random seed 42, generating incorrect data coordinates.", - "label": "Random data seed", - "quarterpoints": 4, - "type": "malus" - }, - { - "description": "Missing colorbar, title, grid, or x/y labels on the axes.", - "label": "Colorbar & Labels", - "quarterpoints": 4, - "type": "malus" - }, - { - "description": "Fails to set alpha=0.6, colormap='viridis', size mapping, or color mapping.", - "label": "Scatter aesthetic parameters", - "quarterpoints": 4, - "type": "malus" - } - ], - "points": 3 - }, + "evaluation_criteria": [ + "Random data seed: Fails to set random seed 42, generating incorrect data coordinates.", + "Colorbar & Labels: Missing colorbar, title, grid, or x/y labels on the axes.", + "Scatter aesthetic parameters: Fails to set alpha=0.6, colormap='viridis', size mapping, or color mapping." + ], "reference_solution": "ac.enaqbz.frrq(42)\na = 150\nk = ac.enaqbz.enaqa(a)\nl = ac.enaqbz.enaqa(a)\nfvmrf = ac.enaqbz.havsbez(20, 200, a)\npbybef = ac.enaqbz.enaq(a)\nsvt, nk = cyg.fhocybgf(svtfvmr=(8, 6))\nfpnggre = nk.fpnggre(k, l, p=pbybef, f=fvmrf, pznc='ivevqvf', nycun=0.6)\ncyg.pbybeone(fpnggre, nk=nk, ynory='Pbybe inyhr')\nnk.frg_gvgyr('Enaqbz Fpnggre Cybg')\nnk.frg_kynory('K inyhrf')\nnk.frg_lynory('L inyhrf')\nnk.tevq(Gehr, nycun=0.3)\ncyg.gvtug_ynlbhg()\ncyg.fubj()", "slideshow": { "slide_type": "" diff --git a/examples/rich_tutor_example.ipynb b/examples/rich_tutor_example.ipynb new file mode 100644 index 0000000..8d139a9 --- /dev/null +++ b/examples/rich_tutor_example.ipynb @@ -0,0 +1,51 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "exercise-1-desc", + "metadata": {}, + "source": [ + "## ➕ Exercise 1 — Add Two Numbers\n", + "\n", + "Implement the function `add(a, b)` that returns the sum of `a` and `b`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "exercise-1-code", + "metadata": { + "reference_solution": "qrs nqq(n, o):\n erghea n + o", + "evaluation_criteria": { + "points": 2, + "base": "max", + "criteria": [ + { + "label": "Correct operation", + "description": "The function must return the sum of a and b using the + operator." + }, + { + "label": "Syntax check", + "description": "The code must be valid Python without syntax errors." + } + ] + } + }, + "outputs": [], + "source": [ + "def add(a, b):\n", + " # Write your solution here\n", + " return a - b # Bug: subtracting instead of adding" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/jupyter_ai_tutor/handlers.py b/jupyter_ai_tutor/handlers.py index 55bad98..65e966a 100644 --- a/jupyter_ai_tutor/handlers.py +++ b/jupyter_ai_tutor/handlers.py @@ -9,26 +9,16 @@ class ExplainHandler(APIHandler): @tornado.web.authenticated async def post(self): body = self.get_json_body() - if not body: - raise tornado.web.HTTPError(400, "Missing request body") + if not body or "body" not in body: + raise tornado.web.HTTPError(400, "Missing 'body' field in request") - message_body = body.get("body", "") + message_body = body["body"] description = body.get("description", "") - reference_solution = body.get("reference_solution", "") - evaluation_criteria = body.get("evaluation_criteria", "") - - formatted_message = "" if description: - formatted_message += f"\n{description}\n\n\n" - - formatted_message += f"{message_body}" - - if reference_solution: - formatted_message += f"\n\n\n{reference_solution}\n" - if evaluation_criteria: - formatted_message += f"\n\n\n{evaluation_criteria}\n" - - message_body = formatted_message.strip() + message_body = ( + f"\n{description}\n\n\n" + f"{message_body}" + ) config_manager = self.settings.get("jupyternaut.config_manager") if not config_manager: diff --git a/src/api.ts b/src/api.ts index beb8b10..5f6c01d 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,24 +1,16 @@ import { URLExt } from '@jupyterlab/coreutils'; import { ServerConnection } from '@jupyterlab/services'; -/** - * Options for streaming the explanation request. - */ -export interface IStreamExplanationOptions { - body: string; - description?: string; - signal?: AbortSignal; - referenceSolution?: string; - evaluationCriteria?: string; -} - /** * Streams the tutor explanation for the given message body via SSE. * Yields text chunks as they arrive from the backend. - * @param options - The request options containing body and structured contexts + * @param body - The user message (code + question) + * @param description - Optional exercise description from preceding markdown cells */ export async function* streamExplanation( - options: IStreamExplanationOptions + body: string, + description?: string, + signal?: AbortSignal ): AsyncGenerator { const settings = ServerConnection.makeSettings(); const url = URLExt.join(settings.baseUrl, 'api/jupyter-ai-tutor/explain'); @@ -27,14 +19,9 @@ export async function* streamExplanation( url, { method: 'POST', - body: JSON.stringify({ - body: options.body, - description: options.description, - reference_solution: options.referenceSolution, - evaluation_criteria: options.evaluationCriteria - }), + body: JSON.stringify({ body, description }), headers: { 'Content-Type': 'application/json' }, - signal: options.signal + signal }, settings ); diff --git a/src/index.ts b/src/index.ts index 3da22a4..d204544 100644 --- a/src/index.ts +++ b/src/index.ts @@ -248,15 +248,25 @@ const plugin: JupyterFrontEndPlugin = { typeof rawSolution === 'string' ? decodeRot13(rawSolution) : ''; // Retrieve and format evaluation_criteria from metadata - const rawCriteria = - cell.model.getMetadata('evaluation_criteria') || - cell.model.getMetadata('criteria'); + const rawCriteria = cell.model.getMetadata('evaluation_criteria'); const evaluationCriteria = formatEvaluationCriteria(rawCriteria); const question = errorSection ? 'Can you explain this code and the error it produced?' : 'Can you explain this code?'; - const body = `${question}\n\n\`\`\`${language}\n${source}\n\`\`\`${errorSection}\n`; + const bodyContent = `${question}\n\n\`\`\`${language}\n${source}\n\`\`\`${errorSection}\n`; + + let formattedBody = ''; + if (description) { + formattedBody += `\n${description}\n\n\n`; + } + formattedBody += bodyContent; + if (referenceSolution) { + formattedBody += `\n\n\n${referenceSolution}\n`; + } + if (evaluationCriteria) { + formattedBody += `\n\n\n${evaluationCriteria}\n`; + } if (!chatWidget.isAttached) { app.shell.add(chatWidget, 'right'); @@ -264,10 +274,7 @@ const plugin: JupyterFrontEndPlugin = { app.shell.activateById(chatWidget.id); await tutorModel.sendMessageToAI({ - body, - description, - referenceSolution, - evaluationCriteria, + body: formattedBody, attachments: attachment ? [attachment] : undefined }); }, diff --git a/src/model.ts b/src/model.ts index 5699f99..fb1aa42 100644 --- a/src/model.ts +++ b/src/model.ts @@ -15,8 +15,6 @@ import { AI_AVATAR } from './icons'; interface ITutorNewMessage extends INewMessage { attachments?: IAttachment[]; description?: string; - referenceSolution?: string; - evaluationCriteria?: string; } export const TUTOR_USER: IUser = { @@ -89,13 +87,11 @@ export class TutorChatModel extends AbstractChatModel { try { let accumulated = ''; - for await (const chunk of streamExplanation({ - body: message.body, - description: message.description, - signal: this._abortController.signal, - referenceSolution: message.referenceSolution, - evaluationCriteria: message.evaluationCriteria - })) { + for await (const chunk of streamExplanation( + message.body, + message.description, + this._abortController.signal + )) { accumulated += chunk; streamingMsg.update({ body: accumulated }); } diff --git a/src/utils.ts b/src/utils.ts index d866ac1..fbaafbf 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -12,14 +12,6 @@ export function isContinuous(numbers: number[]): boolean { return true; } -export interface ICriteriaItem { - label?: string; - title?: string; - name?: string; - description?: string; - desc?: string; -} - /** * Decodes a ROT13 encoded string. */ @@ -37,19 +29,11 @@ export function formatEvaluationCriteria(criteriaData: unknown): string { if (typeof criteriaData === 'string') { return criteriaData; } - - const data = criteriaData as { criteria?: ICriteriaItem[] } | ICriteriaItem[]; - const list = Array.isArray(data) ? data : data.criteria || []; - - return list - .map(item => { - if (item && typeof item === 'object') { - const label = item.label || item.title || item.name || ''; - const desc = item.description || item.desc || ''; - return label && desc ? `- ${label}: ${desc}` : `- ${label || desc}`; - } - return typeof item === 'string' ? `- ${item}` : ''; - }) - .filter(Boolean) - .join('\n'); + if (Array.isArray(criteriaData)) { + return criteriaData + .map(item => (typeof item === 'string' ? `- ${item.trim()}` : '')) + .filter(Boolean) + .join('\n'); + } + return ''; } From 7c4ad25b3bfcc8b41d0de83d0dede8bded3ba194 Mon Sep 17 00:00:00 2001 From: Yahiewi Date: Thu, 9 Jul 2026 10:57:01 +0100 Subject: [PATCH 3/4] Remove redundant example file --- examples/rich_tutor_example.ipynb | 51 ------------------------------- 1 file changed, 51 deletions(-) delete mode 100644 examples/rich_tutor_example.ipynb diff --git a/examples/rich_tutor_example.ipynb b/examples/rich_tutor_example.ipynb deleted file mode 100644 index 8d139a9..0000000 --- a/examples/rich_tutor_example.ipynb +++ /dev/null @@ -1,51 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "exercise-1-desc", - "metadata": {}, - "source": [ - "## ➕ Exercise 1 — Add Two Numbers\n", - "\n", - "Implement the function `add(a, b)` that returns the sum of `a` and `b`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "exercise-1-code", - "metadata": { - "reference_solution": "qrs nqq(n, o):\n erghea n + o", - "evaluation_criteria": { - "points": 2, - "base": "max", - "criteria": [ - { - "label": "Correct operation", - "description": "The function must return the sum of a and b using the + operator." - }, - { - "label": "Syntax check", - "description": "The code must be valid Python without syntax errors." - } - ] - } - }, - "outputs": [], - "source": [ - "def add(a, b):\n", - " # Write your solution here\n", - " return a - b # Bug: subtracting instead of adding" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} From e0098bf326a7e2aabaab3c563f6e0d8eb9406796 Mon Sep 17 00:00:00 2001 From: Yahiewi Date: Thu, 9 Jul 2026 13:36:42 +0100 Subject: [PATCH 4/4] Remove redundant description parameter --- jupyter_ai_tutor/handlers.py | 6 ------ src/api.ts | 5 ++--- src/model.ts | 2 -- 3 files changed, 2 insertions(+), 11 deletions(-) diff --git a/jupyter_ai_tutor/handlers.py b/jupyter_ai_tutor/handlers.py index 5789256..605b51a 100644 --- a/jupyter_ai_tutor/handlers.py +++ b/jupyter_ai_tutor/handlers.py @@ -16,12 +16,6 @@ async def post(self): raise tornado.web.HTTPError(400, "Missing 'body' field in request") message_body = body["body"] - description = body.get("description", "") - if description: - message_body = ( - f"\n{description}\n\n\n" - f"{message_body}" - ) config_manager = self.settings.get("jupyternaut.config_manager") if not config_manager: diff --git a/src/api.ts b/src/api.ts index 5f6c01d..e3c382d 100644 --- a/src/api.ts +++ b/src/api.ts @@ -5,11 +5,10 @@ import { ServerConnection } from '@jupyterlab/services'; * Streams the tutor explanation for the given message body via SSE. * Yields text chunks as they arrive from the backend. * @param body - The user message (code + question) - * @param description - Optional exercise description from preceding markdown cells + * @param signal - Optional abort signal */ export async function* streamExplanation( body: string, - description?: string, signal?: AbortSignal ): AsyncGenerator { const settings = ServerConnection.makeSettings(); @@ -19,7 +18,7 @@ export async function* streamExplanation( url, { method: 'POST', - body: JSON.stringify({ body, description }), + body: JSON.stringify({ body }), headers: { 'Content-Type': 'application/json' }, signal }, diff --git a/src/model.ts b/src/model.ts index fb1aa42..f7d3888 100644 --- a/src/model.ts +++ b/src/model.ts @@ -14,7 +14,6 @@ import { AI_AVATAR } from './icons'; interface ITutorNewMessage extends INewMessage { attachments?: IAttachment[]; - description?: string; } export const TUTOR_USER: IUser = { @@ -89,7 +88,6 @@ export class TutorChatModel extends AbstractChatModel { let accumulated = ''; for await (const chunk of streamExplanation( message.body, - message.description, this._abortController.signal )) { accumulated += chunk;