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 '';
+}