diff --git a/examples/matplotlib_exercices.ipynb b/examples/matplotlib_exercices.ipynb index bac0ec7..660a778 100644 --- a/examples/matplotlib_exercices.ipynb +++ b/examples/matplotlib_exercices.ipynb @@ -46,7 +46,19 @@ "cell_type": "code", "execution_count": null, "id": "15a20896-1822-4b7f-89db-bf5ef6682aaf", - "metadata": {}, + "metadata": { + "deletable": true, + "editable": true, + "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": "" + }, + "tags": [] + }, "outputs": [], "source": [ "# 🐞 There is a bug in this cell — find and fix it!\n", @@ -86,7 +98,20 @@ "cell_type": "code", "execution_count": null, "id": "7a984913-5581-4ca5-a7ad-c16dc21c108d", - "metadata": {}, + "metadata": { + "deletable": true, + "editable": true, + "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": "" + }, + "tags": [] + }, "outputs": [], "source": [ "# Data is provided — write your bar chart code below!\n", @@ -96,6 +121,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 +160,20 @@ "cell_type": "code", "execution_count": null, "id": "957d292f-e46d-4e7d-ba1a-4adafce9a820", - "metadata": {}, + "metadata": { + "deletable": true, + "editable": true, + "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": "" + }, + "tags": [] + }, "outputs": [], "source": [ "# YOUR CODE HERE\n", @@ -178,7 +220,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 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/index.ts b/src/index.ts index 82bd3d1..d204544 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,10 +242,31 @@ 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'); + 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'); @@ -253,8 +274,7 @@ const plugin: JupyterFrontEndPlugin = { app.shell.activateById(chatWidget.id); await tutorModel.sendMessageToAI({ - body, - description, + body: formattedBody, attachments: attachment ? [attachment] : undefined }); }, 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; diff --git a/src/utils.ts b/src/utils.ts index 18f73c6..fbaafbf 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -11,3 +11,29 @@ export function isContinuous(numbers: number[]): boolean { return true; } + +/** + * 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; + } + if (Array.isArray(criteriaData)) { + return criteriaData + .map(item => (typeof item === 'string' ? `- ${item.trim()}` : '')) + .filter(Boolean) + .join('\n'); + } + return ''; +}