Spaces:
Running
ClassLens — Question Categorization & Bank System
Overview
When a teacher uploads a questions PDF, ClassLens automatically classifies every question into a pre-defined taxonomy using an LLM. This classification feeds three downstream features:
- Richer student reports — wrong-answer blocks show the specific grammar/vocabulary tag that was tested, and the Weakness Summary groups errors by tag.
- Shared question bank — classified questions accumulate across all uploads and all teachers, organized by category and tag.
- Practice question generation — teachers can generate new questions by selecting a category and tag; the system rephrases questions already in the bank rather than hallucinating new ones.
Taxonomy
The taxonomy has 6 main categories. Two of them (字詞理解 and 語法結構) have sub-tags; the other four are used as-is.
字詞理解 (Vocabulary & Word Usage)
| Sub-group | Tags |
|---|---|
| 詞性 | 形容詞用法, 情狀副詞, 頻率副詞, 數量形容詞, 介系詞 |
| 花費動詞 | spend/cost/take/pay 用法 |
| 字彙 | 動詞與動詞片語, 名詞與名詞片語, 形容詞與副詞, 介系詞與連接詞片語, 情境字彙推斷 |
語法結構 (Grammar Structures)
| Sub-group | Tags |
|---|---|
| 時態 | 現在簡單式, 現在簡單式第三人稱單數, 現在進行式, 現在完成式, 過去式be動詞, 過去簡單式, 過去進行式, 過去完成式, 未來式 |
| 語態 | 被動語態 |
| 代名詞 | 人稱代名詞主格, 人稱代名詞受格, 所有格代名詞, 反身代名詞, 不定代名詞, 指示代名詞 |
| 連接詞 | 對等連接詞, when/before/after, if/although, why/because/so, as long as/as soon as, when/while, 相關連接詞 |
| 關係子句 | 關係代名詞, 關係代名詞省略 |
| 動詞型態 | 動名詞, 不定詞, 感官動詞, 使役動詞, 連綴動詞, 授與動詞, 助動詞, used to |
| 句型結構 | 祈使句, 附加問句, 附和句, 間接問句, 比較級, 最高級, There be 句型, It takes 句型, Too...to 句型, So...that 句型, Enough...to 句型, 動名詞當主詞, 虛主詞it |
| 疑問句型 | Who/What/Where/When/Which/How/Why/How often/How much/How long 問答句 |
文意推論 (Inference)
No sub-tags. Covers questions that require reading between the lines.
篇章大意 (Main Idea)
No sub-tags. Covers questions about the overall topic or gist of a passage.
篇章細節 (Reading for Detail)
No sub-tags. Covers questions about specific facts stated in a passage.
篇章結構 (Text Organization)
No sub-tags. Covers questions about how a passage is organized (e.g., ordering paragraphs, identifying transitions).
How Categorization Works
Trigger
Categorization runs once per questions PDF upload, in the background after the upload response is returned to the client. It does not block the upload or report generation.
Flow
Teacher uploads questions PDF
│
▼
Parser extracts [{number, text}, ...] from PDF
│
▼
process_uploaded_files() saves rows to ParsedData (main_category = "", tags = [])
│
▼
Upload response returned to client immediately
│
▼ (asyncio.create_task — background)
categorize_questions(session_id, teacher_id, questions)
│
├── Builds LLM prompt with full taxonomy + all question texts
├── Calls gpt-4o-mini → returns JSON [{question_num, main_category, tags}, ...]
├── Validates: main_category must be one of the 6; tags must match that category
├── update_parsed_data_categories() — writes main_category + tags to ParsedData rows
└── save_question_bank_batch() — inserts into shared question_bank table
If the LLM call or DB write fails, the error is logged and the upload is unaffected.
Files
| File | Purpose |
|---|---|
chatkit/backend/app/taxonomy.py |
Single source of truth for all categories and tags |
chatkit/backend/app/question_categorizer.py |
LLM categorization + DB writes |
chatkit/backend/app/models.py |
QuestionBank ORM model, ParsedData columns main_category + tags |
chatkit/backend/app/database.py |
update_parsed_data_categories, save_question_bank_batch, query_question_bank |
chatkit/backend/alembic/versions/0003_question_bank.py |
Migration: creates the question_bank table |
Question Bank
The question_bank table is shared across all teachers. Questions persist even if the original quiz or teacher account is deleted (FK uses ON DELETE SET NULL).
Schema
| Column | Type | Notes |
|---|---|---|
id |
integer PK | |
quiz_id |
integer, nullable | Source quiz (SET NULL on delete) |
teacher_id |
integer, nullable | Uploading teacher (SET NULL on delete) |
question_text |
text | Full question as extracted from PDF |
answer |
varchar(10) | Correct answer (may be empty if uploaded without answer key) |
main_category |
varchar(64), indexed | One of the 6 taxonomy categories |
tags |
JSON array | Tags from the category's sub-list |
created_at |
timestamptz |
Querying the bank
query_question_bank(main_category, tags, limit) in database.py:
- Fetches up to
limit×3candidates by category (most recent first) - If tags are specified, filters in Python to rows that share at least one tag
- Falls back to category-only results if no tag match is found
Practice Question Generation
When a teacher requests practice questions for a category/tag combination:
query_question_bank(category, tags)fetches matching source questions from the bank.- If no bank questions match the filter, the endpoint returns an empty list — no AI fallback.
- If matches are found,
gpt-4o-miniis asked to rephrase the source questions (same grammar point, varied vocabulary and context) — not to generate new topics.
This ensures generated questions always test real skills from past exams and never drift outside the taxonomy.
How Categories Appear in Reports
When a student report is generated, main.py fetches the ParsedData rows for the session and builds a categories dict (question_num → (main_category, tags)). This is passed to generate_student_report.
Each wrong-answer block in the LLM prompt includes a 分類 line:
**分類**: 語法結構 > 時態, 過去簡單式
Or for tag-free categories:
**分類**: 篇章細節
The Weakness Summary section of the report instructs the LLM to:
- Name the specific tags that recurred most (e.g., 過去簡單式, 被動語態)
- Fall back to the category name for the four tag-free categories
- Group errors by these tags — no question numbers, no vague summaries