Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 46 additions & 4 deletions examples/matplotlib_exercices.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -178,7 +220,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.13"
"version": "3.14.6"
}
},
"nbformat": 4,
Expand Down
6 changes: 0 additions & 6 deletions jupyter_ai_tutor/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"<exercise_description>\n{description}\n</exercise_description>\n\n"
f"{message_body}"
)

config_manager = self.settings.get("jupyternaut.config_manager")
if not config_manager:
Expand Down
5 changes: 2 additions & 3 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, void, undefined> {
const settings = ServerConnection.makeSettings();
Expand All @@ -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
},
Expand Down
28 changes: 24 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -242,19 +242,39 @@ const plugin: JupyterFrontEndPlugin<void> = {
}
}

// 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 += `<exercise_description>\n${description}\n</exercise_description>\n\n`;
}
formattedBody += bodyContent;
if (referenceSolution) {
formattedBody += `\n\n<reference_solution>\n${referenceSolution}\n</reference_solution>`;
}
if (evaluationCriteria) {
formattedBody += `\n\n<evaluation_criteria>\n${evaluationCriteria}\n</evaluation_criteria>`;
}

if (!chatWidget.isAttached) {
app.shell.add(chatWidget, 'right');
}
app.shell.activateById(chatWidget.id);

await tutorModel.sendMessageToAI({
body,
description,
body: formattedBody,
attachments: attachment ? [attachment] : undefined
});
},
Expand Down
2 changes: 0 additions & 2 deletions src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { AI_AVATAR } from './icons';

interface ITutorNewMessage extends INewMessage {
attachments?: IAttachment[];
description?: string;
}

export const TUTOR_USER: IUser = {
Expand Down Expand Up @@ -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;
Expand Down
26 changes: 26 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,29 @@ export function isContinuous(numbers: number[]): boolean {

return true;
}

/**
* Decodes a ROT13 encoded string.
*/
export function decodeRot13(str: string): string {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this necessary ? We can probably keep the solution in plain text.
Otherwise we should really encrypt it using a passphrase, but this involve more complexity for now.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was something that was mentioned during the last meeting and can be found in the codimd document. I think the idea isn't necessarily to have encryption as ROT13 can be easily decoded, but to at least make it so inspecting the metadata doesn't directly give you the answer, while keeping things simple (not using any actual secure encryption).
image

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK let's keep it for now then.

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 {
Comment thread
Yahiewi marked this conversation as resolved.
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 '';
}
Loading