Spaces:
Sleeping
Sleeping
Commit ·
4b240c2
1
Parent(s): c33a552
Initialize standalone Quiz_Generation service repo
Browse files- .gitignore +10 -82
- README.md +27 -61
- app/services/quiz_service.py +93 -47
.gitignore
CHANGED
|
@@ -1,85 +1,13 @@
|
|
| 1 |
-
# compiled output
|
| 2 |
-
/dist
|
| 3 |
-
/node_modules
|
| 4 |
-
/build
|
| 5 |
-
|
| 6 |
-
# Logs
|
| 7 |
-
logs
|
| 8 |
-
*.log
|
| 9 |
-
npm-debug.log*
|
| 10 |
-
pnpm-debug.log*
|
| 11 |
-
yarn-debug.log*
|
| 12 |
-
yarn-error.log*
|
| 13 |
-
lerna-debug.log*
|
| 14 |
-
|
| 15 |
-
# OS
|
| 16 |
-
.DS_Store
|
| 17 |
-
|
| 18 |
-
# Tests
|
| 19 |
-
/coverage
|
| 20 |
-
/.nyc_output
|
| 21 |
-
|
| 22 |
-
# IDEs and editors
|
| 23 |
-
/.idea
|
| 24 |
-
.project
|
| 25 |
-
.classpath
|
| 26 |
-
.c9/
|
| 27 |
-
*.launch
|
| 28 |
-
.settings/
|
| 29 |
-
*.sublime-workspace
|
| 30 |
-
|
| 31 |
-
# IDE - VSCode
|
| 32 |
-
.vscode/*
|
| 33 |
-
!.vscode/settings.json
|
| 34 |
-
!.vscode/tasks.json
|
| 35 |
-
!.vscode/launch.json
|
| 36 |
-
!.vscode/extensions.json
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
# Aiven / cloud DB TLS (local only; never commit real certs)
|
| 40 |
-
ca.pem
|
| 41 |
-
|
| 42 |
-
.env
|
| 43 |
-
.env.local
|
| 44 |
-
.env.aiven
|
| 45 |
-
.env.*.local
|
| 46 |
-
.env.test.local
|
| 47 |
-
.env.production.local
|
| 48 |
-
# temp directory
|
| 49 |
-
.temp
|
| 50 |
-
.tmp
|
| 51 |
-
|
| 52 |
-
# Runtime data
|
| 53 |
-
pids
|
| 54 |
-
*.pid
|
| 55 |
-
*.seed
|
| 56 |
-
*.pid.lock
|
| 57 |
-
|
| 58 |
-
# Diagnostic reports (https://nodejs.org/api/report.html)
|
| 59 |
-
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
| 60 |
-
|
| 61 |
-
!.env.example
|
| 62 |
-
|
| 63 |
-
# Python (AI microservices and bytecode)
|
| 64 |
-
__pycache__/
|
| 65 |
-
*.py[cod]
|
| 66 |
-
*$py.class
|
| 67 |
.venv/
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
ai-services/**/.venv/
|
| 72 |
-
ai-services/**/venv/
|
| 73 |
-
ai-services/**/*.pyc
|
| 74 |
-
# Local test images (not committed). README stays tracked so the folder exists in the repo.
|
| 75 |
-
ai-services/attendance/dataset/*
|
| 76 |
-
!ai-services/attendance/dataset/README.txt
|
| 77 |
-
ai-services/attendance/Attendance/
|
| 78 |
-
ai-services/attendance/temp_*
|
| 79 |
|
| 80 |
-
#
|
| 81 |
-
|
| 82 |
-
|
|
|
|
| 83 |
|
| 84 |
-
#
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
.venv/
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.pyc
|
| 4 |
+
*.pyo
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
+
# Runtime/local state
|
| 7 |
+
uploads.json
|
| 8 |
+
quizzes.json
|
| 9 |
+
results/
|
| 10 |
|
| 11 |
+
# OS/editor artifacts
|
| 12 |
+
.DS_Store
|
| 13 |
+
Thumbs.db
|
README.md
CHANGED
|
@@ -13,8 +13,10 @@ A FastAPI-based microservice that generates quizzes from uploaded
|
|
| 13 |
documents using an AI model.
|
| 14 |
|
| 15 |
The API accepts PDF, DOCX, or TXT files, extracts their text, and
|
| 16 |
-
generates different types of questions such as:
|
| 17 |
-
|
|
|
|
|
|
|
| 18 |
|
| 19 |
The questions are generated using the Groq LLM (Llama 3.3 70B) via
|
| 20 |
LangChain.
|
|
@@ -40,21 +42,13 @@ INSTALLATION
|
|
| 40 |
|
| 41 |
---
|
| 42 |
|
| 43 |
-
1.
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
3. Activate the environment
|
| 52 |
-
|
| 53 |
-
Windows (PowerShell) .venv
|
| 54 |
-
|
| 55 |
-
4. Install dependencies
|
| 56 |
-
|
| 57 |
-
pip install -r requirements.txt
|
| 58 |
|
| 59 |
---
|
| 60 |
|
|
@@ -62,9 +56,9 @@ ENVIRONMENT VARIABLES
|
|
| 62 |
|
| 63 |
---
|
| 64 |
|
| 65 |
-
Create a .env file in the project root:
|
| 66 |
|
| 67 |
-
MY_API_KEY=your_groq_api_key_here
|
| 68 |
|
| 69 |
This key is required to access the Groq LLM.
|
| 70 |
|
|
@@ -76,15 +70,15 @@ RUNNING THE API
|
|
| 76 |
|
| 77 |
Start the server:
|
| 78 |
|
| 79 |
-
uvicorn app.main:app
|
| 80 |
|
| 81 |
The API will run at:
|
| 82 |
|
| 83 |
-
http://127.0.0.1:8000
|
| 84 |
|
| 85 |
Interactive API documentation:
|
| 86 |
|
| 87 |
-
http://127.0.0.1:8000/docs
|
| 88 |
|
| 89 |
---
|
| 90 |
|
|
@@ -92,40 +86,12 @@ API ENDPOINTS
|
|
| 92 |
|
| 93 |
---
|
| 94 |
|
| 95 |
-
POST /api/v1/upload Upload a document (PDF, DOCX, TXT)
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
---
|
| 103 |
-
|
| 104 |
-
POST /api/v1/generate
|
| 105 |
-
|
| 106 |
-
Form parameters: uploadId numQuestions questionType difficulty
|
| 107 |
-
saveAsFiles
|
| 108 |
-
|
| 109 |
-
Example response:
|
| 110 |
-
|
| 111 |
-
{ quizId: uuid, status: completed, numQuestions: 5 }
|
| 112 |
-
|
| 113 |
-
---
|
| 114 |
-
|
| 115 |
-
GET /api/v1/quiz/{quiz_id} Returns the generated quiz with questions and
|
| 116 |
-
answer key.
|
| 117 |
-
|
| 118 |
-
---
|
| 119 |
-
|
| 120 |
-
GET /api/v1/download/{filename} Download the generated quiz file.
|
| 121 |
-
|
| 122 |
-
---
|
| 123 |
-
|
| 124 |
-
GET /api/v1/uploads List all uploaded documents.
|
| 125 |
-
|
| 126 |
-
---
|
| 127 |
-
|
| 128 |
-
DELETE /api/v1/upload/{upload_id} Delete an uploaded document.
|
| 129 |
|
| 130 |
---
|
| 131 |
|
|
@@ -133,9 +99,9 @@ EXAMPLE WORKFLOW
|
|
| 133 |
|
| 134 |
---
|
| 135 |
|
| 136 |
-
1.
|
| 137 |
-
2.
|
| 138 |
-
3.
|
| 139 |
|
| 140 |
---
|
| 141 |
|
|
@@ -143,8 +109,8 @@ TECHNOLOGIES USED
|
|
| 143 |
|
| 144 |
---
|
| 145 |
|
| 146 |
-
FastAPI LangChain Groq LLM (Llama 3.3 70B) Python pdfplumber
|
| 147 |
-
FPDF
|
| 148 |
|
| 149 |
---
|
| 150 |
|
|
|
|
| 13 |
documents using an AI model.
|
| 14 |
|
| 15 |
The API accepts PDF, DOCX, or TXT files, extracts their text, and
|
| 16 |
+
generates different types of questions such as:
|
| 17 |
+
- Multiple Choice Questions (MCQ)
|
| 18 |
+
- Fill in the Blank
|
| 19 |
+
- Explanation Questions
|
| 20 |
|
| 21 |
The questions are generated using the Groq LLM (Llama 3.3 70B) via
|
| 22 |
LangChain.
|
|
|
|
| 42 |
|
| 43 |
---
|
| 44 |
|
| 45 |
+
1. Clone the repository
|
| 46 |
+
2. Create a virtual environment
|
| 47 |
+
`python -m venv .venv`
|
| 48 |
+
3. Activate the environment (Windows PowerShell)
|
| 49 |
+
`.\.venv\Scripts\Activate.ps1`
|
| 50 |
+
4. Install dependencies
|
| 51 |
+
`pip install -r requirements.txt`
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
---
|
| 54 |
|
|
|
|
| 56 |
|
| 57 |
---
|
| 58 |
|
| 59 |
+
Create a `.env` file in the project root:
|
| 60 |
|
| 61 |
+
`MY_API_KEY=your_groq_api_key_here`
|
| 62 |
|
| 63 |
This key is required to access the Groq LLM.
|
| 64 |
|
|
|
|
| 70 |
|
| 71 |
Start the server:
|
| 72 |
|
| 73 |
+
`uvicorn app.main:app --reload`
|
| 74 |
|
| 75 |
The API will run at:
|
| 76 |
|
| 77 |
+
`http://127.0.0.1:8000`
|
| 78 |
|
| 79 |
Interactive API documentation:
|
| 80 |
|
| 81 |
+
`http://127.0.0.1:8000/docs`
|
| 82 |
|
| 83 |
---
|
| 84 |
|
|
|
|
| 86 |
|
| 87 |
---
|
| 88 |
|
| 89 |
+
- `POST /api/v1/upload` Upload a document (PDF, DOCX, TXT)
|
| 90 |
+
- `POST /api/v1/generate` Generate questions from uploaded text
|
| 91 |
+
- `GET /api/v1/quiz/{quiz_id}` Returns generated quiz with answer key
|
| 92 |
+
- `GET /api/v1/download/{filename}` Download generated quiz file
|
| 93 |
+
- `GET /api/v1/uploads` List uploaded documents
|
| 94 |
+
- `DELETE /api/v1/upload/{upload_id}` Delete uploaded document
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
|
| 96 |
---
|
| 97 |
|
|
|
|
| 99 |
|
| 100 |
---
|
| 101 |
|
| 102 |
+
1. Upload a file
|
| 103 |
+
2. Generate a quiz
|
| 104 |
+
3. Retrieve the quiz results
|
| 105 |
|
| 106 |
---
|
| 107 |
|
|
|
|
| 109 |
|
| 110 |
---
|
| 111 |
|
| 112 |
+
FastAPI, LangChain, Groq LLM (Llama 3.3 70B), Python, pdfplumber,
|
| 113 |
+
python-docx, FPDF
|
| 114 |
|
| 115 |
---
|
| 116 |
|
app/services/quiz_service.py
CHANGED
|
@@ -16,16 +16,21 @@ def format_standard_question(index: int, body: str) -> str:
|
|
| 16 |
text = text[m.end() :].strip()
|
| 17 |
return f"Question {index + 1}: {text}"
|
| 18 |
|
|
|
|
| 19 |
llm = ChatGroq(
|
| 20 |
api_key=MY_API_KEY,
|
| 21 |
model="llama-3.3-70b-versatile",
|
| 22 |
-
temperature
|
|
|
|
|
|
|
| 23 |
)
|
| 24 |
|
| 25 |
mcq_prompt = PromptTemplate(
|
| 26 |
-
input_variables=["context", "num_questions"],
|
| 27 |
template="""
|
| 28 |
-
Generate {num_questions} multiple-choice questions from the following text.
|
|
|
|
|
|
|
| 29 |
List all questions first, then provide a complete answer key at the end.
|
| 30 |
|
| 31 |
Text:
|
|
@@ -51,13 +56,15 @@ D) [Option D]
|
|
| 51 |
Question 1: [The letter of the correct option]
|
| 52 |
Question 2: [The letter of the correct option]
|
| 53 |
...and so on.
|
| 54 |
-
"""
|
| 55 |
)
|
| 56 |
|
| 57 |
fill_blank_prompt = PromptTemplate(
|
| 58 |
-
input_variables=["context", "num_questions"],
|
| 59 |
template="""
|
| 60 |
Generate {num_questions} fill-in-the-blank questions from the following text.
|
|
|
|
|
|
|
| 61 |
List all questions first, then provide a complete answer key at the end.
|
| 62 |
|
| 63 |
Text:
|
|
@@ -73,13 +80,15 @@ Question 2: [Another sentence with a _____ blank]
|
|
| 73 |
Question 1: [The word that fills the blank]
|
| 74 |
Question 2: [The word that fills the blank]
|
| 75 |
...and so on.
|
| 76 |
-
"""
|
| 77 |
)
|
| 78 |
|
| 79 |
explain_prompt = PromptTemplate(
|
| 80 |
-
input_variables=["context", "num_questions"],
|
| 81 |
template="""
|
| 82 |
Generate {num_questions} explanation questions from the following text. These should require a detailed answer.
|
|
|
|
|
|
|
| 83 |
|
| 84 |
Text:
|
| 85 |
{context}
|
|
@@ -95,7 +104,7 @@ Reference: [A relevant quote or fact from the text]
|
|
| 95 |
|
| 96 |
…continue until you have {num_questions} "## Explanation Question" blocks.
|
| 97 |
Each question line must start with "Question N:" where N matches the block order (1, 2, 3, …).
|
| 98 |
-
"""
|
| 99 |
)
|
| 100 |
|
| 101 |
mcq_chain = mcq_prompt | llm | StrOutputParser()
|
|
@@ -103,27 +112,35 @@ fill_blank_chain = fill_blank_prompt | llm | StrOutputParser()
|
|
| 103 |
explain_chain = explain_prompt | llm | StrOutputParser()
|
| 104 |
|
| 105 |
|
| 106 |
-
def generate_quiz_from_text(
|
|
|
|
|
|
|
| 107 |
final_questions = []
|
| 108 |
answer_key = []
|
| 109 |
raw_ai_output = ""
|
| 110 |
|
| 111 |
if question_type == "MCQ":
|
| 112 |
-
raw_ai_output = mcq_chain.invoke(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
if "--- ANSWER KEY ---" not in raw_ai_output:
|
| 114 |
raise ValueError("AI did not provide a separate answer key.")
|
| 115 |
|
| 116 |
questions_block, answers_block = raw_ai_output.split("--- ANSWER KEY ---")
|
| 117 |
|
| 118 |
answers_map = {}
|
| 119 |
-
for line in answers_block.strip().split(
|
| 120 |
if ":" in line:
|
| 121 |
q_id, answer = line.split(":", 1)
|
| 122 |
answers_map[q_id.strip()] = answer.strip()
|
| 123 |
|
| 124 |
question_chunks = questions_block.strip().split("Question")[1:]
|
| 125 |
for i, chunk in enumerate(question_chunks):
|
| 126 |
-
lines = chunk.strip().split(
|
| 127 |
stem = (
|
| 128 |
lines[0].split(":", 1)[1].strip()
|
| 129 |
if lines and ":" in lines[0]
|
|
@@ -138,26 +155,36 @@ def generate_quiz_from_text(text: str, num_questions: int, question_type: str, d
|
|
| 138 |
|
| 139 |
q_id = f"Question {i+1}"
|
| 140 |
if question_text and options and q_id in answers_map:
|
| 141 |
-
final_questions.append(
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
|
| 152 |
elif question_type == "FillBlank":
|
| 153 |
-
raw_ai_output = fill_blank_chain.invoke(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
if "--- ANSWER KEY ---" not in raw_ai_output:
|
| 155 |
raise ValueError("AI did not provide a separate answer key.")
|
| 156 |
|
| 157 |
questions_block, answers_block = raw_ai_output.split("--- ANSWER KEY ---")
|
| 158 |
|
| 159 |
answers_map = {}
|
| 160 |
-
for line in answers_block.strip().split(
|
| 161 |
if ":" in line:
|
| 162 |
q_id, answer = line.split(":", 1)
|
| 163 |
answers_map[q_id.strip()] = answer.strip()
|
|
@@ -167,27 +194,41 @@ def generate_quiz_from_text(text: str, num_questions: int, question_type: str, d
|
|
| 167 |
# Chunk may start with " 1: ..." after splitting on "Question"; strip that
|
| 168 |
# number so we do not produce "Question 3: 3: ...".
|
| 169 |
first_line = chunk.strip().split("\n", 1)[0]
|
| 170 |
-
stem =
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
rest = chunk.strip().split("\n", 1)
|
| 172 |
body = stem + ("\n" + rest[1] if len(rest) > 1 else "")
|
| 173 |
question_text = format_standard_question(i, body)
|
| 174 |
|
| 175 |
q_id = f"Question {i+1}"
|
| 176 |
if question_text and q_id in answers_map:
|
| 177 |
-
final_questions.append(
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
|
| 187 |
else:
|
| 188 |
-
raw_ai_output = explain_chain.invoke(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
for i, item in enumerate(raw_ai_output.split("## Explanation Question")[1:]):
|
| 190 |
-
lines = item.strip().split(
|
| 191 |
q_body = ""
|
| 192 |
q_ref = ""
|
| 193 |
for line in lines:
|
|
@@ -200,14 +241,19 @@ def generate_quiz_from_text(text: str, num_questions: int, question_type: str, d
|
|
| 200 |
if q_body:
|
| 201 |
q_id = f"Question {i+1}"
|
| 202 |
question_text = format_standard_question(i, q_body)
|
| 203 |
-
final_questions.append(
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
text = text[m.end() :].strip()
|
| 17 |
return f"Question {index + 1}: {text}"
|
| 18 |
|
| 19 |
+
|
| 20 |
llm = ChatGroq(
|
| 21 |
api_key=MY_API_KEY,
|
| 22 |
model="llama-3.3-70b-versatile",
|
| 23 |
+
# Non-zero temperature reduces repeated identical outputs
|
| 24 |
+
# when the same source file is generated multiple times.
|
| 25 |
+
temperature=1.1,
|
| 26 |
)
|
| 27 |
|
| 28 |
mcq_prompt = PromptTemplate(
|
| 29 |
+
input_variables=["context", "num_questions", "difficulty"],
|
| 30 |
template="""
|
| 31 |
+
Generate {num_questions} multiple-choice questions from the following text.
|
| 32 |
+
Target difficulty: {difficulty}.
|
| 33 |
+
Vary the selected concepts and phrasing across questions, and avoid repeating near-identical question stems.
|
| 34 |
List all questions first, then provide a complete answer key at the end.
|
| 35 |
|
| 36 |
Text:
|
|
|
|
| 56 |
Question 1: [The letter of the correct option]
|
| 57 |
Question 2: [The letter of the correct option]
|
| 58 |
...and so on.
|
| 59 |
+
""",
|
| 60 |
)
|
| 61 |
|
| 62 |
fill_blank_prompt = PromptTemplate(
|
| 63 |
+
input_variables=["context", "num_questions", "difficulty"],
|
| 64 |
template="""
|
| 65 |
Generate {num_questions} fill-in-the-blank questions from the following text.
|
| 66 |
+
Target difficulty: {difficulty}.
|
| 67 |
+
Vary the selected concepts and sentence structures, and avoid repeating near-identical stems.
|
| 68 |
List all questions first, then provide a complete answer key at the end.
|
| 69 |
|
| 70 |
Text:
|
|
|
|
| 80 |
Question 1: [The word that fills the blank]
|
| 81 |
Question 2: [The word that fills the blank]
|
| 82 |
...and so on.
|
| 83 |
+
""",
|
| 84 |
)
|
| 85 |
|
| 86 |
explain_prompt = PromptTemplate(
|
| 87 |
+
input_variables=["context", "num_questions", "difficulty"],
|
| 88 |
template="""
|
| 89 |
Generate {num_questions} explanation questions from the following text. These should require a detailed answer.
|
| 90 |
+
Target difficulty: {difficulty}.
|
| 91 |
+
Vary concepts and wording, and avoid near-duplicate question stems.
|
| 92 |
|
| 93 |
Text:
|
| 94 |
{context}
|
|
|
|
| 104 |
|
| 105 |
…continue until you have {num_questions} "## Explanation Question" blocks.
|
| 106 |
Each question line must start with "Question N:" where N matches the block order (1, 2, 3, …).
|
| 107 |
+
""",
|
| 108 |
)
|
| 109 |
|
| 110 |
mcq_chain = mcq_prompt | llm | StrOutputParser()
|
|
|
|
| 112 |
explain_chain = explain_prompt | llm | StrOutputParser()
|
| 113 |
|
| 114 |
|
| 115 |
+
def generate_quiz_from_text(
|
| 116 |
+
text: str, num_questions: int, question_type: str, difficulty: str
|
| 117 |
+
):
|
| 118 |
final_questions = []
|
| 119 |
answer_key = []
|
| 120 |
raw_ai_output = ""
|
| 121 |
|
| 122 |
if question_type == "MCQ":
|
| 123 |
+
raw_ai_output = mcq_chain.invoke(
|
| 124 |
+
{
|
| 125 |
+
"context": text,
|
| 126 |
+
"num_questions": num_questions,
|
| 127 |
+
"difficulty": difficulty,
|
| 128 |
+
}
|
| 129 |
+
)
|
| 130 |
if "--- ANSWER KEY ---" not in raw_ai_output:
|
| 131 |
raise ValueError("AI did not provide a separate answer key.")
|
| 132 |
|
| 133 |
questions_block, answers_block = raw_ai_output.split("--- ANSWER KEY ---")
|
| 134 |
|
| 135 |
answers_map = {}
|
| 136 |
+
for line in answers_block.strip().split("\n"):
|
| 137 |
if ":" in line:
|
| 138 |
q_id, answer = line.split(":", 1)
|
| 139 |
answers_map[q_id.strip()] = answer.strip()
|
| 140 |
|
| 141 |
question_chunks = questions_block.strip().split("Question")[1:]
|
| 142 |
for i, chunk in enumerate(question_chunks):
|
| 143 |
+
lines = chunk.strip().split("\n")
|
| 144 |
stem = (
|
| 145 |
lines[0].split(":", 1)[1].strip()
|
| 146 |
if lines and ":" in lines[0]
|
|
|
|
| 155 |
|
| 156 |
q_id = f"Question {i+1}"
|
| 157 |
if question_text and options and q_id in answers_map:
|
| 158 |
+
final_questions.append(
|
| 159 |
+
{
|
| 160 |
+
"type": "MCQ",
|
| 161 |
+
"difficulty": difficulty,
|
| 162 |
+
"question": question_text,
|
| 163 |
+
"options": options,
|
| 164 |
+
}
|
| 165 |
+
)
|
| 166 |
+
answer_key.append(
|
| 167 |
+
{
|
| 168 |
+
"questionId": q_id,
|
| 169 |
+
"correctAnswer": answers_map[q_id],
|
| 170 |
+
}
|
| 171 |
+
)
|
| 172 |
|
| 173 |
elif question_type == "FillBlank":
|
| 174 |
+
raw_ai_output = fill_blank_chain.invoke(
|
| 175 |
+
{
|
| 176 |
+
"context": text,
|
| 177 |
+
"num_questions": num_questions,
|
| 178 |
+
"difficulty": difficulty,
|
| 179 |
+
}
|
| 180 |
+
)
|
| 181 |
if "--- ANSWER KEY ---" not in raw_ai_output:
|
| 182 |
raise ValueError("AI did not provide a separate answer key.")
|
| 183 |
|
| 184 |
questions_block, answers_block = raw_ai_output.split("--- ANSWER KEY ---")
|
| 185 |
|
| 186 |
answers_map = {}
|
| 187 |
+
for line in answers_block.strip().split("\n"):
|
| 188 |
if ":" in line:
|
| 189 |
q_id, answer = line.split(":", 1)
|
| 190 |
answers_map[q_id.strip()] = answer.strip()
|
|
|
|
| 194 |
# Chunk may start with " 1: ..." after splitting on "Question"; strip that
|
| 195 |
# number so we do not produce "Question 3: 3: ...".
|
| 196 |
first_line = chunk.strip().split("\n", 1)[0]
|
| 197 |
+
stem = (
|
| 198 |
+
first_line.split(":", 1)[1].strip()
|
| 199 |
+
if ":" in first_line
|
| 200 |
+
else first_line.strip()
|
| 201 |
+
)
|
| 202 |
rest = chunk.strip().split("\n", 1)
|
| 203 |
body = stem + ("\n" + rest[1] if len(rest) > 1 else "")
|
| 204 |
question_text = format_standard_question(i, body)
|
| 205 |
|
| 206 |
q_id = f"Question {i+1}"
|
| 207 |
if question_text and q_id in answers_map:
|
| 208 |
+
final_questions.append(
|
| 209 |
+
{
|
| 210 |
+
"type": "FillBlank",
|
| 211 |
+
"difficulty": difficulty,
|
| 212 |
+
"question": question_text,
|
| 213 |
+
}
|
| 214 |
+
)
|
| 215 |
+
answer_key.append(
|
| 216 |
+
{
|
| 217 |
+
"questionId": q_id,
|
| 218 |
+
"answer": answers_map[q_id],
|
| 219 |
+
}
|
| 220 |
+
)
|
| 221 |
|
| 222 |
else:
|
| 223 |
+
raw_ai_output = explain_chain.invoke(
|
| 224 |
+
{
|
| 225 |
+
"context": text,
|
| 226 |
+
"num_questions": num_questions,
|
| 227 |
+
"difficulty": difficulty,
|
| 228 |
+
}
|
| 229 |
+
)
|
| 230 |
for i, item in enumerate(raw_ai_output.split("## Explanation Question")[1:]):
|
| 231 |
+
lines = item.strip().split("\n")
|
| 232 |
q_body = ""
|
| 233 |
q_ref = ""
|
| 234 |
for line in lines:
|
|
|
|
| 241 |
if q_body:
|
| 242 |
q_id = f"Question {i+1}"
|
| 243 |
question_text = format_standard_question(i, q_body)
|
| 244 |
+
final_questions.append(
|
| 245 |
+
{
|
| 246 |
+
"type": "Explain",
|
| 247 |
+
"difficulty": difficulty,
|
| 248 |
+
"question": question_text,
|
| 249 |
+
}
|
| 250 |
+
)
|
| 251 |
+
answer_key.append(
|
| 252 |
+
{
|
| 253 |
+
"questionId": q_id,
|
| 254 |
+
"reference": q_ref,
|
| 255 |
+
}
|
| 256 |
+
)
|
| 257 |
+
|
| 258 |
+
return final_questions, answer_key, raw_ai_output
|
| 259 |
+
|