Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import requests | |
| import csv | |
| import PyPDF2 | |
| from io import BytesIO | |
| import re | |
| def run_app(): | |
| st.title('MCQ to CSV from Notes') | |
| api_key = st.sidebar.text_input("Enter your OpenAI API key", type="password") | |
| uploaded_file = st.file_uploader("Upload your notes file", type=['pdf', 'txt']) | |
| num_questions = st.number_input("Number of questions to generate", min_value=5, max_value=20, value=10) | |
| submit_button = st.button('Generate MCQs') | |
| if submit_button and api_key and uploaded_file: | |
| notes_text = extract_text_from_file(uploaded_file) | |
| if notes_text: | |
| generate_mcqs(api_key, notes_text, num_questions) | |
| else: | |
| st.error("Failed to extract text from file. Please check the file format.") | |
| def extract_text_from_file(uploaded_file): | |
| if uploaded_file.type == "application/pdf": | |
| try: | |
| reader = PyPDF2.PdfReader(BytesIO(uploaded_file.getvalue())) | |
| text = "" | |
| for page in reader.pages: | |
| text += page.extract_text() + " " | |
| return text.strip() | |
| except Exception as e: | |
| st.error(f"Error extracting text from PDF: {e}") | |
| return None | |
| elif uploaded_file.type == "text/plain": | |
| try: | |
| return uploaded_file.getvalue().decode("utf-8").strip() | |
| except Exception as e: | |
| st.error(f"Error reading text file: {e}") | |
| return None | |
| else: | |
| st.error("Unsupported file type.") | |
| return None | |
| def generate_mcqs(api_key, notes_text, num_questions): | |
| prompt = (f"Generate {num_questions} multiple-choice questions (MCQs) for final-year medical students based on the uploaded notes. \ | |
| Format the output with each field separated by a semicolon (;) as follows: id; question; option A; option B; option C; option D; answer (as 'A', 'B', 'C', or 'D'); explanation. \ | |
| Do not use additional semicolons in questions, options, or explanations. Provide each MCQ and its components on a single line, with a new line separating each MCQ. \ | |
| Example format for one question:\n\n1; A 30-year-old female self-detects a peri-areolar breast mass measuring 2cm in diameter. The mass is described as regular, smooth, and mobile upon examination. Which of the following statements is true?; Arrange for genetic studies immediately.; Perform an incisional biopsy.; Conduct an MRI to rule out malignancy.; A Phyllodes tumor cannot be ruled out.; D; The presentation of a peri-areolar breast mass that is regular, smooth, and mobile is suggestive but not definitive of any specific condition without histological evaluation. While genetic studies may be prompted by family history or other risk factors, they are not immediately indicated based exclusively on the described mass. An MRI could be considered in some diagnostic pathways but is not the primary immediate action. Incisional biopsy could potentially aid in diagnosis but may not be the first step depending on clinical judgment and other diagnostic findings. Phyllodes tumors, which can be benign or malignant, often present as well-circumscribed, mobile breast masses that could resemble fibroadenomas. Given the nature of Phyllodes tumors to exhibit a wide range of behaviors and the fact that they cannot be reliably distinguished from fibroadenomas without histological diagnosis, it is true that a Phyllodes tumor cannot be ruled out based solely on physical examination and mass characteristics. \ | |
| The stem should fulfil the following requirements: It should be meaningful and define a problem, it should contain only relevant information, it should not be a negative prompt unless it is necessary (avoid questions such as 'Which of the following is NOT .......'), it should pass the hand cover test (i.e. students should be able to answer the question without looking at the options), a question stem is preferred and a stem with interior blanks should be avoided. \ | |
| The items of choice should fulfil the following requirements: There should be four choices A – D, there should only be one correct answer, each item should be of similar in length and language, be concise, options should be arranged in alphabetical order from A to D, the distribution of correct answers across the four options should be similar. \ | |
| The following text is the uploaded notes (starts below) \ | |
| \n\n{notes_text}\n\n") | |
| data = { | |
| "model": "gpt-4o", #"gpt-4-turbo-preview", # Adjust if necessary for the model you have access to | |
| "messages": [{"role": "system", "content": "You are a helpful assistant capable of generating MCQ based on provided text."}, | |
| {"role": "user", "content": prompt}], | |
| "temperature": 0.7, | |
| "max_tokens": 300 * num_questions | |
| } | |
| headers = { | |
| "Authorization": f"Bearer {api_key}", | |
| "Content-Type": "application/json", | |
| } | |
| # Adjusting endpoint for chat model interactions | |
| chat_completion_endpoint = "https://api.openai.com/v1/chat/completions" | |
| response = requests.post(chat_completion_endpoint, json=data, headers=headers) | |
| if response.status_code == 200: | |
| # Adjust your parsing of the response based on the output structure of chat completions | |
| responses = response.json()["choices"][0]["message"]["content"] # This path may vary; adjust based on actual response | |
| mcqs = responses.strip() | |
| export_mcqs_to_csv(mcqs, num_questions) | |
| else: | |
| st.error(f"Failed to generate MCQs: {response.text}") | |
| def smart_split_mcqs(mcqs_text): | |
| # Pattern now also considers the start of the string for the first MCQ | |
| pattern = re.compile(r'(^|\n)(\d+;)') | |
| mcqs = [] | |
| # Adding initial position to handle the first MCQ starting without a newline | |
| positions = [0] + [match.start() for match in pattern.finditer(mcqs_text)] | |
| for i in range(len(positions)-1): | |
| start_pos = positions[i] | |
| end_pos = positions[i+1] | |
| mcqs.append(mcqs_text[start_pos:end_pos].strip()) | |
| # Ensure to add the last MCQ not captured by the loop | |
| if positions[-1] < len(mcqs_text): | |
| mcqs.append(mcqs_text[positions[-1]:].strip()) | |
| return mcqs | |
| # Adjust the parsing in the export function | |
| def export_mcqs_to_csv(mcqs, num_questions): | |
| csv_file_name = 'generated_mcqs.csv' | |
| # Use the smart_split_mcqs function to split the concatenated string into individual MCQs | |
| mcqs_list = smart_split_mcqs(mcqs) | |
| with open(csv_file_name, mode='w', newline='', encoding='utf-8') as file: | |
| writer = csv.writer(file) | |
| writer.writerow(['id', 'question', 'option A', 'option B', 'option C', 'option D', 'answer', 'explanation']) | |
| for mcq in mcqs_list[:num_questions+1]: | |
| mcq_parts = mcq.split(";") | |
| if len(mcq_parts) < 8: | |
| print(f"Skipped due to formatting: {mcq_parts}") | |
| continue | |
| writer.writerow(mcq_parts[:8]) | |
| st.success(mcqs) #Check step | |
| st.success("MCQs generated successfully. Download the CSV file below.") | |
| with open(csv_file_name, "rb") as file: | |
| st.download_button(label="Download MCQs", data=file, file_name=csv_file_name, mime='text/csv') | |
| print(f"MCQs exported successfully to {csv_file_name}.") | |
| if __name__ == "__main__": | |
| run_app() |