Complete Project Flow
Plain text
GenerateNotebookRequest
↓
main.py
↓
Qwen LLM
↓
NotebookDraft
↓
notebook_draft.json
↓
DraftMapper
↓
AuthoringSpec
↓
NotebookFormatter
↓
example.ipynb
Step 1: User Creates Request
In main.py:
Python
request_schema = GenerateNotebookRequest(
title="Data Preprocessing Assignment",
dataset_name="Titanic Dataset",
difficulty="Easy",
concepts=[
"Shape of dataset",
"Missing values",
"Filling missing values"
]
)
This is the notebook requirement.
Think of it as:
JSON
{
"title": "Data Preprocessing Assignment",
"dataset": "Titanic",
"difficulty": "Easy",
"concepts": [
"Shape of dataset",
"Missing values",
"Filling missing values"
]
}
The TA/Admin provides this.
Step 2: Initialize LLM
From llm.py
Python
self.llm = ChatOllama(
model="qwen3:14b",
base_url="http://172.20.203.53:11433"
)
This means:
Plain text
NotebookSync
↓
LangChain
↓
Ollama
↓
Qwen3 14B
The model is running on your internal server.
Step 3: Structured Output
I saw:
Python
request = llm.with_structured_output(
NotebookDraft,
method="json_mode"
)
This is a very important line.
Normally LLM returns:
Plain text
Here is your notebook...
which is unstructured.
Instead:
Python
NotebookDraft
forces Qwen to return:
JSON
{
"theory_content": {},
"user_tasks": [],
"reference_solutions": []
}
directly.
So LLM behaves like an API.
Step 4: Load Prompt
Python
with open("./prompts/authoring_prompt.txt")
Prompt is loaded.
Then:
Python
full_prompt = f"""
{prompt}
User Request:
{json.dumps(request_schema.model_dump())}
"""
Generated prompt becomes:
Plain text
You are notebook authoring assistant.
Generate notebook draft.
User Request:
{
"title":"Data Preprocessing Assignment",
"dataset_name":"Titanic Dataset",
"difficulty":"Easy"
}
Step 5: Invoke LLM
Python
notebook_draft = request.invoke(full_prompt)
This is where AI generates content.
Possible result:
JSON
{
"theory_content": {
"Dataset Shape":"..."
},
"user_tasks":[
"Find dataset shape",
"Find missing values"
],
"reference_solutions":[
"df.shape",
"df.isnull().sum()"
]
}
Step 6: Save Draft
Python
with open("notebook_draft.json","w")
Then:
Python
f.write(
notebook_draft.model_dump_json()
)
So AI output is stored.
Generated file:
Plain text
notebook_draft.json
Useful for debugging.
Step 7: DraftMapper
This line:
Python
draft_mapper = DraftMapper(
notebook_draft,
"Data Preprocessing Test",
"Happy learning"
)
creates mapper.
Purpose:
Convert raw LLM output into structured notebook specification.
Step 8: map_to_spec()
Inside:
Python
spec = draft_mapper.map_to_spec()
This creates:
Python
AuthoringSpec
Think of:
Plain text
NotebookDraft
as
Plain text
Raw Material
and
Plain text
AuthoringSpec
as
Plain text
Blueprint
Step 9: map_theory_content()
I saw:
Python
for title,content in theory_content.items():
Meaning:
LLM returns:
JSON
{
"Introduction":"...",
"Missing Values":"..."
}
Mapper converts it into:
Python
[
"Introduction content",
"Missing Values content"
]
Step 10: map_user_tasks()
This is interesting.
For every task:
Python
for idx,task in enumerate(...)
creates:
Python
UserTask(
name=f"Question{idx}",
description=task,
starter_code="#Write the code here",
function_name=f"question{idx}",
method_name=f"Question{idx}"
)
Generated result:
Python
Question1
Question2
Question3
Student will see:
Python
# Write code here
@evaluate
def question1():
pass
Step 11: map_test_cases()
This converts:
Python
reference_solutions
into:
Python
TestCaseSpec
Example:
Python
TestCaseSpec(
function_name="question1",
method_name="Question1",
max_mark=10,
solution_code="df.shape"
)
These are hidden.
Step 12: AuthoringSpec
This becomes:
Python
AuthoringSpec
containing:
Python
title
description
dataset
theory
concepts
user_tasks
test_cases
This is the central object.
Everything afterward uses it.
Step 13: NotebookFormatter
Now:
Python
formatter = NotebookFormatter()
and:
Python
nb = formatter.render(spec)
This is where notebook generation starts.
What render() Actually Does
I saw:
Python
render_user_scaffold()
render_user_content()
render_submission_section()
render_admin_boundary()
render_admin_scaffold()
render_admin_content()
So notebook is built in two halves.
USER HALF
Generated first.
Structure:
Plain text
SET SESSION
FETCH ASSESSMENT
PLAY NOW
Question 1
Question 2
Question 3
Submit
Admin HALF
Generated later.
Structure:
Plain text
ADMIN SECTION
Reference Solutions
Hidden Test Cases
Timing
Evaluation Logic
Students only interact with first half.
Hidden Evaluation System
This is the clever part.
I saw:
Python
test.make_test_case(...)
and:
Python
Task.test_case(...)
Meaning:
The admin section generates grading code automatically.
How To Explain Project In Meeting
Say:
Problem
Today trainers manually create:
Theory
Questions
Solutions
Test cases
This is slow and inconsistent.
Solution
We automate notebook generation using LLM.
Input:
JSON
{
"title":"Data Preprocessing",
"difficulty":"Easy",
"dataset":"Titanic"
}
Output:
Plain text
Learner Notebook
+
Admin Notebook
Backend Architecture
Explain like this:
Plain text
Request
↓
LLM Layer
↓
NotebookDraft
↓
DraftMapper
↓
AuthoringSpec
↓
NotebookFormatter
↓
.ipynb Notebook
What Is NotebookDraft?
Likely generated by AI.
Contains:
Python
theory_content
user_tasks
reference_solutions
Raw content.
What Is DraftMapper?
Converts AI response into structured format.
Example:
Python
NotebookDraft
↓
AuthoringSpec
Purpose:
Standardization.
What Is AuthoringSpec?
Most important object.
Contains:
Python
title
description
dataset
tasks
testcases
Everything uses this.
Think:
Plain text
Blueprint of notebook
What Is NotebookFormatter?
Responsible for:
Python
AuthoringSpec
↓
.ipynb
Creates notebook cells.
Difference Between User and Admin Sections
User Section:
Plain text
Theory
Question 1
Question 2
Submit
Admin Section:
Plain text
Reference Solution
Test Cases
Marks
Timing
Hidden from learners.
Sprint 2 Status (Based On Screenshots)
Looks like Sprint 2 completed:
✅ Request Model
✅ LLM Integration
✅ Notebook Draft Generation
✅ Mapper Layer
✅ Notebook Formatter
✅ User Section
✅ Admin Section
✅ Test Case Generation
✅ Notebook Export