Skip to content

Commit 65820d1

Browse files
committed
improve context collection
1 parent d47c872 commit 65820d1

4 files changed

Lines changed: 103 additions & 40 deletions

File tree

jupyter_ai_tutor/handlers.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,28 @@ class ExplainHandler(APIHandler):
1010
@tornado.web.authenticated
1111
async def post(self):
1212
body = self.get_json_body()
13-
if not body or "body" not in body:
14-
raise tornado.web.HTTPError(400, "Missing 'body' field in request")
13+
if not body:
14+
raise tornado.web.HTTPError(400, "Missing request body")
1515

16-
message_body = body["body"]
17-
description = body.get("description", "")
18-
if description:
19-
message_body = (
20-
f"<exercise_description>\n{description}\n</exercise_description>\n\n"
21-
f"{message_body}"
22-
)
16+
student_context = body.get("student_context", "")
17+
student_answer = body.get("student_answer", "")
18+
19+
if student_context or student_answer:
20+
message_body = ""
21+
if student_context:
22+
message_body += f"<student_context>\n{student_context}\n</student_context>\n\n"
23+
if student_answer:
24+
message_body += f"<student_answer>\n{student_answer}\n</student_answer>\n\n"
25+
else:
26+
if "body" not in body:
27+
raise tornado.web.HTTPError(400, "Missing 'body' field in request")
28+
message_body = body["body"]
29+
description = body.get("description", "")
30+
if description:
31+
message_body = (
32+
f"<exercise_description>\n{description}\n</exercise_description>\n\n"
33+
f"{message_body}"
34+
)
2335

2436
config_manager = self.settings.get("jupyternaut.config_manager")
2537
if not config_manager:

src/api.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
11
import { URLExt } from '@jupyterlab/coreutils';
22
import { ServerConnection } from '@jupyterlab/services';
33

4+
/**
5+
* Options for streaming the explanation request.
6+
*/
7+
export interface IStreamExplanationOptions {
8+
body: string;
9+
description?: string;
10+
signal?: AbortSignal;
11+
studentContext?: string;
12+
studentAnswer?: string;
13+
}
14+
415
/**
516
* Streams the tutor explanation for the given message body via SSE.
617
* Yields text chunks as they arrive from the backend.
7-
* @param body - The user message (code + question)
8-
* @param description - Optional exercise description from preceding markdown cells
18+
* @param options - The request options containing body and structured contexts
919
*/
1020
export async function* streamExplanation(
11-
body: string,
12-
description?: string,
13-
signal?: AbortSignal
21+
options: IStreamExplanationOptions
1422
): AsyncGenerator<string, void, undefined> {
1523
const settings = ServerConnection.makeSettings();
1624
const url = URLExt.join(settings.baseUrl, 'api/jupyter-ai-tutor/explain');
@@ -19,9 +27,14 @@ export async function* streamExplanation(
1927
url,
2028
{
2129
method: 'POST',
22-
body: JSON.stringify({ body, description }),
30+
body: JSON.stringify({
31+
body: options.body,
32+
description: options.description,
33+
student_context: options.studentContext,
34+
student_answer: options.studentAnswer
35+
}),
2336
headers: { 'Content-Type': 'application/json' },
24-
signal
37+
signal: options.signal
2538
},
2639
settings
2740
);

src/index.ts

Lines changed: 53 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,11 @@ const plugin: JupyterFrontEndPlugin<void> = {
184184
const codeModel = cell.model as ICodeCellModel;
185185
const outputs = codeModel.outputs;
186186
let errorSection = '';
187+
let jsonError: {
188+
ename: string;
189+
evalue: string;
190+
traceback: string[];
191+
} | null = null;
187192

188193
for (let i = 0; i < outputs.length; i++) {
189194
const output = outputs.get(i);
@@ -193,6 +198,7 @@ const plugin: JupyterFrontEndPlugin<void> = {
193198
evalue: string;
194199
traceback: string[];
195200
};
201+
jsonError = json;
196202
const traceback = json.traceback
197203
.map(line => line.replace(ANSI_ESCAPE, ''))
198204
.join('\n');
@@ -203,45 +209,72 @@ const plugin: JupyterFrontEndPlugin<void> = {
203209
}
204210
}
205211

206-
// Collect preceding markdown cells as exercise description context.
212+
// Collect preceding cells context.
207213
const notebook = notebookTracker?.currentWidget?.content;
208214
const notebookPath = notebookTracker?.currentWidget?.context.path ?? '';
209-
let description: string | undefined;
215+
let studentContext = '';
210216
let attachment: INotebookAttachment | undefined;
211217

212218
if (notebook) {
213219
const activeCellIndex = notebook.activeCellIndex;
214-
const markdownCells: { id: string; source: string }[] = [];
220+
let lastMdIdx = -1;
215221

222+
// Find the index of the most recent markdown cell above the active cell
216223
for (let i = activeCellIndex - 1; i >= 0; i--) {
217224
const precedingCell = notebook.widgets[i];
218-
if (precedingCell.model.type === 'code') {
225+
if (precedingCell.model.type === 'markdown') {
226+
lastMdIdx = i;
219227
break;
220228
}
221-
if (precedingCell.model.type === 'markdown') {
222-
const mdSource = precedingCell.model.sharedModel.source.trim();
223-
if (mdSource) {
224-
markdownCells.unshift({
225-
id: precedingCell.model.id,
226-
source: mdSource
227-
});
228-
}
229+
}
230+
231+
// Gather all cells from that markdown cell up to activeCellIndex - 1
232+
const startIdx = lastMdIdx !== -1 ? lastMdIdx : 0;
233+
const contextCells = [];
234+
for (let i = startIdx; i < activeCellIndex; i++) {
235+
contextCells.push(notebook.widgets[i]);
236+
}
237+
238+
let contextStr = '';
239+
const markdownCellsForAttachment = [];
240+
241+
for (const cCell of contextCells) {
242+
const cSource = cCell.model.sharedModel.source.trim();
243+
if (!cSource) {
244+
continue;
245+
}
246+
247+
if (cCell.model.type === 'markdown') {
248+
contextStr += `${cSource}\n\n`;
249+
markdownCellsForAttachment.push({
250+
id: cCell.model.id,
251+
input_type: 'markdown' as const
252+
});
253+
} else if (cCell.model.type === 'code') {
254+
contextStr += `Preceding Code:\n\`\`\`${language}\n${cSource}\n\`\`\`\n\n`;
229255
}
230256
}
231257

232-
if (markdownCells.length > 0) {
233-
description = markdownCells.map(c => c.source).join('\n\n');
258+
studentContext = contextStr.trim();
259+
260+
if (markdownCellsForAttachment.length > 0) {
234261
attachment = {
235262
type: 'notebook',
236263
value: notebookPath,
237-
cells: markdownCells.map(c => ({
238-
id: c.id,
239-
input_type: 'markdown' as const
240-
}))
264+
cells: markdownCellsForAttachment
241265
};
242266
}
243267
}
244268

269+
// Format student answer
270+
let studentAnswer = source;
271+
if (jsonError) {
272+
const traceback = jsonError.traceback
273+
.map(line => line.replace(ANSI_ESCAPE, ''))
274+
.join('\n');
275+
studentAnswer += `\n\nError:\n${jsonError.ename}: ${jsonError.evalue}\n${traceback}`;
276+
}
277+
245278
const question = errorSection
246279
? 'Can you explain this code and the error it produced?'
247280
: 'Can you explain this code?';
@@ -254,7 +287,8 @@ const plugin: JupyterFrontEndPlugin<void> = {
254287

255288
await tutorModel.sendMessageToAI({
256289
body,
257-
description,
290+
studentContext,
291+
studentAnswer,
258292
attachments: attachment ? [attachment] : undefined
259293
});
260294
},

src/model.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import { AI_AVATAR } from './icons';
1515
interface ITutorNewMessage extends INewMessage {
1616
attachments?: IAttachment[];
1717
description?: string;
18+
studentContext?: string;
19+
studentAnswer?: string;
1820
}
1921

2022
export const TUTOR_USER: IUser = {
@@ -87,11 +89,13 @@ export class TutorChatModel extends AbstractChatModel {
8789

8890
try {
8991
let accumulated = '';
90-
for await (const chunk of streamExplanation(
91-
message.body,
92-
message.description,
93-
this._abortController.signal
94-
)) {
92+
for await (const chunk of streamExplanation({
93+
body: message.body,
94+
description: message.description,
95+
signal: this._abortController.signal,
96+
studentContext: message.studentContext,
97+
studentAnswer: message.studentAnswer
98+
})) {
9599
accumulated += chunk;
96100
streamingMsg.update({ body: accumulated });
97101
}

0 commit comments

Comments
 (0)