Add LLM question import functionality and enhance question model with new fields

This commit is contained in:
Ross
2025-10-20 10:43:12 +01:00
parent 4986c2faa9
commit 93f0bb8927
10 changed files with 565 additions and 3 deletions
+7
View File
@@ -27,6 +27,13 @@
<li><a class="dropdown-item" href="{% url 'sbas:question_create' %}" title="Create a new question"><i class="bi bi-question-circle"></i> Question</a></li>
</ul>
</li>
<li class="nav-item-dropdown">
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-expanded="false"><i class="bi bi-gear"></i> LLM</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{% url 'sbas:llm_prompt_view' %}" title="Generate questions using LLM"><i class="bi bi-robot"></i> LLM prompt</a></li>
<li><a class="dropdown-item" href="{% url 'sbas:import_llm_questions' %}" title="Import questions using LLM"><i class="bi bi-upload"></i> Import Questions</a></li>
</ul>
</li>
{% if request.user.is_superuser %}
<li class="nav-item">
@@ -0,0 +1,11 @@
{% extends 'sbas/base.html' %}
{% block content %}
<h1>Import LLM Questions</h1>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<button class="btn btn-primary" type="submit">Upload and Import</button>
</form>
<p>Upload a single JSON array, a single object, or JSONL (one JSON object per line) matching the agreed schema.</p>
{% endblock %}
+133
View File
@@ -0,0 +1,133 @@
{% extends 'sbas/base.html' %}
{% block content %}
<h2>LLM Prompts</h2>
<p>
Below are the prompts used for interacting with large language models (LLMs) within the SBA system.
</p>
<h3>Question Generation Prompt</h3>
<pre>
You are an expert radiologist tasked with generating high-quality multiple-choice questions for medical imaging education. Each question should consist of a clinical vignette, an image description, and five answer choices (A through E), with one correct answer and four plausible distractors.
Please adhere to the following guidelines when creating each question:
1. Clinical Vignette: Provide a brief clinical scenario that sets the context for the question including relevant patient history and clinical findings.
2. Make sure the question focuses on key radiological concepts, findings, or diagnoses.
3. Answer Choices: Create five answer options. Ensure that one is the correct answer and the others are plausible distractors.
4. Explanation: Provide a concise explanation for why the correct answer is right and why the distractors are incorrect.
5. Difficulty Level: Aim for a moderate difficulty level suitable for training radiologists.
6. Clarity and Precision: Use clear and precise language, avoiding ambiguity.
7. Relevance: Ensure that the question is relevant to current radiological practices and guidelines.
8. Questions should feature metadata tags for finding, structure, condition, presentation, and subspecialty.
9. Each question output should be a json object with the following schema:
10. For article sources pleese reference the article doi within the explanation field in the format [doi:10.xxxx/xxxxx].
11. For statdx sources please reference the article in the format [statdx:article_id].
12. References should be included inline within the explanation field.
13. Questions need to have a radiology focus, please avoid questions that are purely clinical or biochemical without imaging relevance.
JSON Schema (draft-07 compatible)
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "SBA Question",
"description": "Schema for importing LLM-generated questions into the sbas.Question model.",
"type": "object",
"additionalProperties": false,
"required": [
"title",
"stem",
"a_answer",
"b_answer",
"c_answer",
"d_answer",
"e_answer",
"best_answer",
"category"
],
"properties": {
"title": {
"type": "string",
"description": "Short title for the question."
},
"stem": {
"type": "string",
"description": "HTML or plain text question stem. Trim leading/trailing whitespace before saving."
},
"a_answer": { "type": "string" },
"a_feedback": { "type": "string", "default": "" },
"b_answer": { "type": "string" },
"b_feedback": { "type": "string", "default": "" },
"c_answer": { "type": "string" },
"c_feedback": { "type": "string", "default": "" },
"d_answer": { "type": "string" },
"d_feedback": { "type": "string", "default": "" },
"e_answer": { "type": "string" },
"e_feedback": { "type": "string", "default": "" },
"feedback": {
"type": "string",
"description": "General question feedback (may include HTML)."
},
"best_answer": {
"type": "string",
"enum": ["a", "b", "c", "d", "e"],
"description": "One of 'a','b','c','d','e'."
},
"category": {
"oneOf": [
{ "type": "string", "description": "Category name ('Central Nervous and Head & Neck', 'Paediatric', 'Genito-urinary, Adrenal, Obstetrics & Gynaecology and Breast', 'Gastro-intestinal', 'Musculoskeletal and Trauma', 'Cardiothoracic and Vascular')" }
]
},
"finding": {
"type": "array",
"description": "References to Finding objects. Prefer numeric IDs; names allowed if importer resolves them.",
"items": {
"oneOf": [
{ "type": "integer" },
{ "type": "string" }
]
},
"uniqueItems": true
},
"structure": {
"type": "array",
"items": {
"oneOf": [
{ "type": "integer" },
{ "type": "string" }
]
},
"uniqueItems": true
},
"condition": {
"type": "array",
"items": {
"oneOf": [
{ "type": "integer" },
{ "type": "string" }
]
},
"uniqueItems": true
},
"presentation": {
"type": "array",
"items": {
"oneOf": [
{ "type": "integer" },
{ "type": "string" }
]
},
"uniqueItems": true
},
"subspecialty": {
"type": "array",
"items": {
"oneOf": [
{ "type": "integer" },
{ "type": "string", "description": "Subspecialty name (e.g. 'Haematology & Oncology', 'Vascular', 'Uroradiology', 'Thoracic', 'Paediatric', 'Obstetric and Gynaecological', 'Neuroradiology', 'Musculoskeletal', 'Head and Neck', 'Gastrointestinal and hepatobiliary', 'Cardiac', 'Breast')" }
]
},
"uniqueItems": true
},
}
}
</pre>
+67 -3
View File
@@ -29,14 +29,78 @@
<a href="{% url 'sbas:exam_overview' pk=exam.pk %}">{{ exam }}</a>
{% endfor %}
</div>
<div>
Findings: {% if question.finding.exists %}
<ul>
{% for f in question.finding.all %}
<li>{{ f.get_link|safe }}</li>
{% endfor %}
</ul>
{% else %}
None
{% endif %}
</div>
<div>
Structures: {% if question.structure.exists %}
<ul>
{% for s in question.structure.all %}
<li>{{ s.get_link|safe }}</li>
{% endfor %}
</ul>
{% else %}
None
{% endif %}
</div>
<div>
Conditions: {% if question.condition.exists %}
<ul>
{% for c in question.condition.all %}
<li>{{ c.get_link|safe }}</li>
{% endfor %}
</ul>
{% else %}
None
{% endif %}
</div>
<div>
Presentations: {% if question.presentation.exists %}
<ul>
{% for p in question.presentation.all %}
<li>{{ p.get_link|safe }}</li>
{% endfor %}
</ul>
{% else %}
None
{% endif %}
</div>
<div>
Subspecialties: {% if question.subspecialty.exists %}
<ul>
{% for ss in question.subspecialty.all %}
<li>{{ ss.get_link|safe }}</li>
{% endfor %}
</ul>
{% else %}
None
{% endif %}
</div>
<div>
Category: {{ question.category }}
</div>
<div>
Author: {% for user in question.author.all %}
{{ author }},
{% endfor %}
Author(s):
{% if question.author.exists %}
{% for u in question.author.all %}
{{ u.get_full_name|default:u.username }}{% if not forloop.last %}, {% endif %}
{% endfor %}
{% else %}
Unknown
{% endif %}
</div>
<div>
Feedback: {{ question.feedback|linebreaks }}
</div>
{% include 'question_notes.html' %}