first push of 4-track learning_objectives_generator
Browse files- app/helpers/exercise_standardizer.py +2 -2
- app/helpers/study_text_standardizer.py +30 -0
- app/ui/diagnoser_tab.py +1 -1
- app/ui/distractors_tab.py +1 -1
- app/ui/learning_objectives_tab.py +54 -0
- chains/learning_objectives_generator/learning_objectives_chain.py +0 -9
- chains/learning_objectives_generator/runner.py +82 -0
- config/chain_configs.py +19 -4
- config/format_mappings.py +37 -1
- config/templates.py +134 -4
- main.py +23 -20
app/helpers/exercise_standardizer.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
# app/helpers/exercise_standardizer.py
|
| 2 |
from langchain_core.prompts import ChatPromptTemplate
|
| 3 |
from typing import Any
|
| 4 |
-
from config.format_mappings import
|
| 5 |
|
| 6 |
|
| 7 |
|
|
@@ -12,7 +12,7 @@ async def standardize_exercise(user_query: str, exercise_format: str, template:
|
|
| 12 |
if exercise_format == "Raw (original)":
|
| 13 |
return user_query # No transformation needed
|
| 14 |
|
| 15 |
-
formatting_instructions =
|
| 16 |
exercise_format,
|
| 17 |
"Please reformat the given exercise to ease further processing."
|
| 18 |
)
|
|
|
|
| 1 |
# app/helpers/exercise_standardizer.py
|
| 2 |
from langchain_core.prompts import ChatPromptTemplate
|
| 3 |
from typing import Any
|
| 4 |
+
from config.format_mappings import FORMAT_MAPPINGS_EXERCISES
|
| 5 |
|
| 6 |
|
| 7 |
|
|
|
|
| 12 |
if exercise_format == "Raw (original)":
|
| 13 |
return user_query # No transformation needed
|
| 14 |
|
| 15 |
+
formatting_instructions = FORMAT_MAPPINGS_EXERCISES.get(
|
| 16 |
exercise_format,
|
| 17 |
"Please reformat the given exercise to ease further processing."
|
| 18 |
)
|
app/helpers/study_text_standardizer.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/helpers/exercise_standardizer.py
|
| 2 |
+
from langchain_core.prompts import ChatPromptTemplate
|
| 3 |
+
from typing import Any
|
| 4 |
+
from config.format_mappings import FORMAT_MAPPINGS_STUDY_TEXTS
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
async def standardize_studytext(user_query: str, studytext_format: str, template: ChatPromptTemplate, llm: Any) -> str:
|
| 9 |
+
"""
|
| 10 |
+
Standardizes a studytext's format using the specified template and LLM, and updates the UI via standardized_format_state.
|
| 11 |
+
"""
|
| 12 |
+
if studytext_format == "Raw (original)":
|
| 13 |
+
return user_query # No transformation needed
|
| 14 |
+
|
| 15 |
+
formatting_instructions = FORMAT_MAPPINGS_STUDY_TEXTS.get(
|
| 16 |
+
studytext_format,
|
| 17 |
+
"Please reformat the given studytext to ease further processing."
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
prompt_std = await template.aformat_prompt(
|
| 21 |
+
user_input=user_query,
|
| 22 |
+
formatting_instructions=formatting_instructions
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
std_messages = prompt_std.to_messages()
|
| 26 |
+
response = await llm.ainvoke(std_messages)
|
| 27 |
+
standardized_studytext = getattr(response, "content", response)
|
| 28 |
+
|
| 29 |
+
return standardized_studytext
|
| 30 |
+
|
app/ui/diagnoser_tab.py
CHANGED
|
@@ -46,7 +46,7 @@ def build_diagnoser_tab():
|
|
| 46 |
diagnoser_input = gr.Textbox(label="Enter exercise in any format",
|
| 47 |
placeholder="Exercise body: <mc:exercise xmlns:mc= ...")
|
| 48 |
# A button to run the chain
|
| 49 |
-
diagnoser_button = gr.Button("
|
| 50 |
|
| 51 |
# Create 10 Response textboxes
|
| 52 |
diagnoser_responses = [
|
|
|
|
| 46 |
diagnoser_input = gr.Textbox(label="Enter exercise in any format",
|
| 47 |
placeholder="Exercise body: <mc:exercise xmlns:mc= ...")
|
| 48 |
# A button to run the chain
|
| 49 |
+
diagnoser_button = gr.Button("Diagnose")
|
| 50 |
|
| 51 |
# Create 10 Response textboxes
|
| 52 |
diagnoser_responses = [
|
app/ui/distractors_tab.py
CHANGED
|
@@ -73,7 +73,7 @@ def build_distractors_tab():
|
|
| 73 |
|
| 74 |
distractors_input = gr.Textbox(label="Enter exercise(s) in any format",
|
| 75 |
placeholder="Stelling: Dit is een ..... voorbeeld van een stelling. A. Mooi B. Lelijk ...")
|
| 76 |
-
distractors_button = gr.Button("
|
| 77 |
|
| 78 |
# Create 10 Response textboxes
|
| 79 |
distractors_responses = [
|
|
|
|
| 73 |
|
| 74 |
distractors_input = gr.Textbox(label="Enter exercise(s) in any format",
|
| 75 |
placeholder="Stelling: Dit is een ..... voorbeeld van een stelling. A. Mooi B. Lelijk ...")
|
| 76 |
+
distractors_button = gr.Button("Brainstorm")
|
| 77 |
|
| 78 |
# Create 10 Response textboxes
|
| 79 |
distractors_responses = [
|
app/ui/learning_objectives_tab.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
from chains.learning_objectives_generator.runner import run_learning_objectives_generator
|
| 3 |
+
from config.llm_config import llms
|
| 4 |
+
|
| 5 |
+
def build_learning_objectives_tab():
|
| 6 |
+
with gr.TabItem("🧠 Identify Learning Objectives"):
|
| 7 |
+
gr.HTML(
|
| 8 |
+
"""
|
| 9 |
+
<div style="margin-bottom: 10px;">
|
| 10 |
+
<span style="font-size: 1.5em; cursor: help;" title="Generate learning objectives for the given study text">
|
| 11 |
+
ℹ️
|
| 12 |
+
</span>
|
| 13 |
+
</div>
|
| 14 |
+
"""
|
| 15 |
+
)
|
| 16 |
+
# 2 dropdowns for the user-chosen LLMs:
|
| 17 |
+
model_choice_1 = gr.Dropdown(
|
| 18 |
+
choices=list(llms.keys()),
|
| 19 |
+
label="LLM 1"
|
| 20 |
+
)
|
| 21 |
+
model_choice_2 = gr.Dropdown(
|
| 22 |
+
choices=list(llms.keys()),
|
| 23 |
+
label="LLM 2"
|
| 24 |
+
)
|
| 25 |
+
text_format = gr.Dropdown(
|
| 26 |
+
choices=["Markdown", "XML", "Plaintext", "Raw (input not reformatted)"],
|
| 27 |
+
value="Markdown",
|
| 28 |
+
label="Studytext Reformat",
|
| 29 |
+
interactive=True,
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
studytext_input = gr.Textbox(label="Enter a study text in any format", placeholder="<h3>Infusie en infuussystemen</h3> <h4>Inleiding</h4> ...")
|
| 33 |
+
learning_objectives_button = gr.Button("Identify LOs")
|
| 34 |
+
|
| 35 |
+
# 2×2 textboxes => 4 total
|
| 36 |
+
# For clarity:
|
| 37 |
+
# row 1 => (box_0, box_1)
|
| 38 |
+
# row 2 => (box_2, box_3)
|
| 39 |
+
with gr.Row():
|
| 40 |
+
box_0 = gr.Textbox(label="Prompt A + LLM 1", interactive=False)
|
| 41 |
+
box_1 = gr.Textbox(label="Prompt B + LLM 1", interactive=False)
|
| 42 |
+
with gr.Row():
|
| 43 |
+
box_2 = gr.Textbox(label="Prompt A + LLM 2", interactive=False)
|
| 44 |
+
box_3 = gr.Textbox(label="Prompt B + LLM 2", interactive=False)
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
# Return references if needed
|
| 49 |
+
return (model_choice_1,
|
| 50 |
+
model_choice_2,
|
| 51 |
+
text_format,
|
| 52 |
+
studytext_input,
|
| 53 |
+
learning_objectives_button,
|
| 54 |
+
[box_0, box_1, box_2, box_3])
|
chains/learning_objectives_generator/learning_objectives_chain.py
CHANGED
|
@@ -6,16 +6,7 @@ from langchain_core.prompts.chat import ChatPromptTemplate
|
|
| 6 |
|
| 7 |
class LearningObjectivesChain(BaseModel):
|
| 8 |
"""
|
| 9 |
-
Orchestrates multi-step generation of learning objectives:
|
| 10 |
-
1) Two parallel calls to 'learning_objective_generator' (LLM1, LLM2).
|
| 11 |
-
2) Combine those outputs.
|
| 12 |
-
3) 'learning_objective_eliminator' on the combined output (Main LLM).
|
| 13 |
-
4) 'learning_objective_finetuner' on the output of that (Main LLM).
|
| 14 |
-
5) 'learning_objective_presenter' to finalize (Main LLM).
|
| 15 |
|
| 16 |
-
Each step can have its own ChatPromptTemplate and uses the relevant LLM.
|
| 17 |
-
|
| 18 |
-
If you want a separate alt LLM for step #3 or #4, just expand as needed.
|
| 19 |
"""
|
| 20 |
|
| 21 |
# Templates
|
|
|
|
| 6 |
|
| 7 |
class LearningObjectivesChain(BaseModel):
|
| 8 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
|
|
|
|
|
|
|
|
|
| 10 |
"""
|
| 11 |
|
| 12 |
# Templates
|
chains/learning_objectives_generator/runner.py
CHANGED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
from typing import AsyncGenerator
|
| 3 |
+
from config.llm_config import llms
|
| 4 |
+
from config.chain_configs import chain_configs
|
| 5 |
+
from app.helpers.study_text_standardizer import standardize_studytext
|
| 6 |
+
|
| 7 |
+
async def run_learning_objectives_generator(
|
| 8 |
+
user_input_text: str,
|
| 9 |
+
model_choice_1: str,
|
| 10 |
+
model_choice_2: str,
|
| 11 |
+
text_format: str
|
| 12 |
+
) -> AsyncGenerator:
|
| 13 |
+
"""
|
| 14 |
+
Orchestrates the entire pipeline:
|
| 15 |
+
1) Standardize the study text
|
| 16 |
+
2) Generate (2 prompts × 2 LLMs) => 4 partial results
|
| 17 |
+
3) Sanitize each partial result
|
| 18 |
+
4) Yield partial updates in real time as each track completes
|
| 19 |
+
"""
|
| 20 |
+
# 1) Standardize the text once
|
| 21 |
+
config = chain_configs["learning_objectives"] # you define this in chain_configs.py
|
| 22 |
+
standardized = await standardize_studytext(
|
| 23 |
+
user_input_text, text_format,
|
| 24 |
+
config["template_studytext_standardize"],
|
| 25 |
+
config["llm_standardize_studytext"]
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
# Prepare references for the generation prompts:
|
| 29 |
+
prompt_a = config["template_gen_prompt_a"]
|
| 30 |
+
prompt_b = config["template_gen_prompt_b"]
|
| 31 |
+
sanitize_prompt = config["template_sanitize"]
|
| 32 |
+
|
| 33 |
+
# pick the LLMs from user choices (with fallback to config)
|
| 34 |
+
llm_a = llms.get(model_choice_1, config["default_llm_a"])
|
| 35 |
+
llm_b = llms.get(model_choice_2, config["default_llm_b"])
|
| 36 |
+
|
| 37 |
+
# We will store the final sanitized results in an array of 4 strings
|
| 38 |
+
# (2 prompts × 2 LLMs)
|
| 39 |
+
partial_results = ["", "", "", ""]
|
| 40 |
+
|
| 41 |
+
# We'll define a short async helper for each track:
|
| 42 |
+
# 'track_index' is 0..3 so we know which of the 4 textboxes to fill
|
| 43 |
+
# 'gen_prompt' is either prompt_a or prompt_b
|
| 44 |
+
# 'gen_llm' is either llm_a or llm_b
|
| 45 |
+
async def run_track(track_index: int, gen_prompt, gen_llm):
|
| 46 |
+
# Step: generate
|
| 47 |
+
gen_msg = await gen_prompt.aformat_prompt(standardized_text=standardized)
|
| 48 |
+
gen_resp = await gen_llm.ainvoke(gen_msg.to_messages())
|
| 49 |
+
generation_output = getattr(gen_resp, "content", gen_resp)
|
| 50 |
+
|
| 51 |
+
# Step: sanitize
|
| 52 |
+
sanitize_msg = await sanitize_prompt.aformat_prompt(raw_output=generation_output)
|
| 53 |
+
sanitize_resp = await llm_a.ainvoke(sanitize_msg.to_messages()) # or use a separate LLM for sanitization
|
| 54 |
+
sanitized_output = getattr(sanitize_resp, "content", sanitize_resp)
|
| 55 |
+
|
| 56 |
+
return (track_index, sanitized_output)
|
| 57 |
+
|
| 58 |
+
# Build the 4 tasks:
|
| 59 |
+
# - track 0 => prompt A, LLM 1
|
| 60 |
+
# - track 1 => prompt B, LLM 1
|
| 61 |
+
# - track 2 => prompt A, LLM 2
|
| 62 |
+
# - track 3 => prompt B, LLM 2
|
| 63 |
+
|
| 64 |
+
tasks = [
|
| 65 |
+
run_track(0, prompt_a, llm_a),
|
| 66 |
+
run_track(1, prompt_b, llm_a),
|
| 67 |
+
run_track(2, prompt_a, llm_b),
|
| 68 |
+
run_track(3, prompt_b, llm_b),
|
| 69 |
+
]
|
| 70 |
+
|
| 71 |
+
# We'll run them in parallel and yield updates as each finishes
|
| 72 |
+
done_count = 0
|
| 73 |
+
for coro in asyncio.as_completed(tasks):
|
| 74 |
+
track_index, final_text = await coro
|
| 75 |
+
partial_results[track_index] = final_text
|
| 76 |
+
done_count += 1
|
| 77 |
+
|
| 78 |
+
# yield partial update
|
| 79 |
+
# We yield a tuple with the 4 track results.
|
| 80 |
+
# The UI will map each item to the correct textbox.
|
| 81 |
+
yield tuple(partial_results) + (standardized, )
|
| 82 |
+
|
config/chain_configs.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
# config/chain_configs.py
|
| 2 |
from config.templates import (
|
| 3 |
-
|
|
|
|
| 4 |
template_diagnose_double_negation,
|
| 5 |
template_diagnose_correct_answer_stands_out,
|
| 6 |
template_diagnose_distractor_clearly_wrong,
|
|
@@ -8,17 +9,21 @@ from config.templates import (
|
|
| 8 |
diagnose_scorecard_template,
|
| 9 |
template_distractors_brainstorm_1,
|
| 10 |
template_distractors_brainstorm_2,
|
| 11 |
-
template_consolidate_distractors
|
|
|
|
|
|
|
| 12 |
)
|
| 13 |
from chains.diagnoser.diagnoser_chain import DiagnoserChain
|
| 14 |
from chains.distractors.distractors_chain import DistractorsChain
|
|
|
|
| 15 |
from config.llm_config import llms
|
| 16 |
|
| 17 |
# Note: The default LLM here is GPT-4o (low temp); the UI can override this choice.
|
|
|
|
| 18 |
chain_configs = {
|
| 19 |
"diagnoser": {
|
| 20 |
"class": DiagnoserChain,
|
| 21 |
-
"template_standardize":
|
| 22 |
"llm_standardize": llms["GPT-4o-mini (zero temp)"], # Always fixed
|
| 23 |
"llm_4o_mini": llms["GPT-4o-mini (low temp)"],
|
| 24 |
"llm_4o": llms["GPT-4o (low temp)"],
|
|
@@ -34,7 +39,7 @@ chain_configs = {
|
|
| 34 |
},
|
| 35 |
"distractors": {
|
| 36 |
"class": DistractorsChain,
|
| 37 |
-
"template_standardize":
|
| 38 |
"llm_standardize": llms["GPT-4o-mini (zero temp)"], # Always fixed
|
| 39 |
"template_distractors_brainstorm_1": template_distractors_brainstorm_1,
|
| 40 |
"template_distractors_brainstorm_2": template_distractors_brainstorm_2,
|
|
@@ -43,4 +48,14 @@ chain_configs = {
|
|
| 43 |
"template_consolidate": template_consolidate_distractors,
|
| 44 |
"llm_consolidate": llms["GPT-4o (low temp)"], # or something else
|
| 45 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
}
|
|
|
|
|
|
| 1 |
# config/chain_configs.py
|
| 2 |
from config.templates import (
|
| 3 |
+
template_standardize_exercise,
|
| 4 |
+
template_standardize_studytext,
|
| 5 |
template_diagnose_double_negation,
|
| 6 |
template_diagnose_correct_answer_stands_out,
|
| 7 |
template_diagnose_distractor_clearly_wrong,
|
|
|
|
| 9 |
diagnose_scorecard_template,
|
| 10 |
template_distractors_brainstorm_1,
|
| 11 |
template_distractors_brainstorm_2,
|
| 12 |
+
template_consolidate_distractors,
|
| 13 |
+
template_gen_prompt_a,
|
| 14 |
+
template_gen_prompt_b
|
| 15 |
)
|
| 16 |
from chains.diagnoser.diagnoser_chain import DiagnoserChain
|
| 17 |
from chains.distractors.distractors_chain import DistractorsChain
|
| 18 |
+
from chains.learning_objectives_generator.learning_objectives_chain import LearningObjectivesChain
|
| 19 |
from config.llm_config import llms
|
| 20 |
|
| 21 |
# Note: The default LLM here is GPT-4o (low temp); the UI can override this choice.
|
| 22 |
+
|
| 23 |
chain_configs = {
|
| 24 |
"diagnoser": {
|
| 25 |
"class": DiagnoserChain,
|
| 26 |
+
"template_standardize": template_standardize_exercise,
|
| 27 |
"llm_standardize": llms["GPT-4o-mini (zero temp)"], # Always fixed
|
| 28 |
"llm_4o_mini": llms["GPT-4o-mini (low temp)"],
|
| 29 |
"llm_4o": llms["GPT-4o (low temp)"],
|
|
|
|
| 39 |
},
|
| 40 |
"distractors": {
|
| 41 |
"class": DistractorsChain,
|
| 42 |
+
"template_standardize": template_standardize_exercise,
|
| 43 |
"llm_standardize": llms["GPT-4o-mini (zero temp)"], # Always fixed
|
| 44 |
"template_distractors_brainstorm_1": template_distractors_brainstorm_1,
|
| 45 |
"template_distractors_brainstorm_2": template_distractors_brainstorm_2,
|
|
|
|
| 48 |
"template_consolidate": template_consolidate_distractors,
|
| 49 |
"llm_consolidate": llms["GPT-4o (low temp)"], # or something else
|
| 50 |
},
|
| 51 |
+
"learning_objectives": {
|
| 52 |
+
"class": LearningObjectivesChain,
|
| 53 |
+
"template_standardize_studytext": template_standardize_studytext,
|
| 54 |
+
"llm_standardize": llms["GPT-4o-mini (zero temp)"], # Always fixed
|
| 55 |
+
"template_gen_prompt_a": template_gen_prompt_a,
|
| 56 |
+
"template_gen_prompt_b": template_gen_prompt_b,
|
| 57 |
+
"default_llm_a": llms["o1"],
|
| 58 |
+
"default_llm_b": llms["o3-mini (high reasoning_effort)"]
|
| 59 |
+
},
|
| 60 |
}
|
| 61 |
+
|
config/format_mappings.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
# format_mappings.py
|
| 2 |
|
| 3 |
-
|
|
|
|
| 4 |
"Markdown": (
|
| 5 |
"Please reformat in Markdown, following this example:\n"
|
| 6 |
"**Theorie:** \n"
|
|
@@ -50,3 +51,38 @@ FORMAT_MAPPINGS = {
|
|
| 50 |
"1. Het gevoel geen connectie te hebben met anderen"
|
| 51 |
)
|
| 52 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# format_mappings.py
|
| 2 |
|
| 3 |
+
# Intermediate plain processing
|
| 4 |
+
FORMAT_MAPPINGS_EXERCISES = {
|
| 5 |
"Markdown": (
|
| 6 |
"Please reformat in Markdown, following this example:\n"
|
| 7 |
"**Theorie:** \n"
|
|
|
|
| 51 |
"1. Het gevoel geen connectie te hebben met anderen"
|
| 52 |
)
|
| 53 |
}
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
FORMAT_MAPPINGS_STUDY_TEXTS = {
|
| 57 |
+
"Markdown": (
|
| 58 |
+
"Please reformat into Markdown."
|
| 59 |
+
),
|
| 60 |
+
"XML": (
|
| 61 |
+
"""
|
| 62 |
+
Please reformat into XML, use tags like <title></title> and <b></b>.
|
| 63 |
+
"""
|
| 64 |
+
),
|
| 65 |
+
"Plaintext": (
|
| 66 |
+
"Please reformat into neat plaintext, without any tags or other formatting."
|
| 67 |
+
)
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
# Final processing
|
| 71 |
+
studytext_HTML = (
|
| 72 |
+
"""
|
| 73 |
+
Please reformat into XML. The target conventions are:
|
| 74 |
+
- always start with a title like this: <h3>TITLE</h3>;
|
| 75 |
+
- subheadings are just in bold. Usually there won't be any subheadings though, just different paragraphs;
|
| 76 |
+
- for bold text, use <strong></strong>;
|
| 77 |
+
- divide the text up into <p></p>-blocks;
|
| 78 |
+
- for lists, adhere to the following capitalization and interpunction rules:
|
| 79 |
+
<p>This is an example list, pay attention to lowercase beginnings and final interpunction endings of each item:</p>
|
| 80 |
+
<ul>
|
| 81 |
+
<li>each item starts with lowercase;</li>
|
| 82 |
+
<li>always. Even if it's several sentences long. Like this one;</li>
|
| 83 |
+
<li>each item ends with a semicolon;</li>
|
| 84 |
+
<li>except for the very latest item;</li>
|
| 85 |
+
<li>that (this) final item, ends with a full-stop, a period.</li>
|
| 86 |
+
</ul>
|
| 87 |
+
"""
|
| 88 |
+
)
|
config/templates.py
CHANGED
|
@@ -1,8 +1,7 @@
|
|
| 1 |
# config/templates.py
|
| 2 |
from langchain_core.prompts.chat import ChatPromptTemplate
|
| 3 |
|
| 4 |
-
|
| 5 |
-
standardize_template = ChatPromptTemplate(
|
| 6 |
messages=[
|
| 7 |
("system", "You reformat a given multiple choice exercise into a standardized format. {formatting_instructions}\n\n"
|
| 8 |
"Only 3 elements are always mandatory:\n"
|
|
@@ -18,6 +17,15 @@ standardize_template = ChatPromptTemplate(
|
|
| 18 |
input_variables=["user_input", "formatting_instructions"]
|
| 19 |
)
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
template_diagnose_double_negation = ChatPromptTemplate(
|
| 22 |
messages=[
|
| 23 |
("system", """Analyze a multiple-choice exercise for the presence of double negatives: either two negations in the question/statement itself, or a negation in the question/statement AND in an answer option.
|
|
@@ -221,8 +229,6 @@ template_distractors_brainstorm_2 = ChatPromptTemplate(
|
|
| 221 |
)
|
| 222 |
|
| 223 |
|
| 224 |
-
|
| 225 |
-
|
| 226 |
template_consolidate_distractors = ChatPromptTemplate(
|
| 227 |
messages=[
|
| 228 |
("system", "You are given several lists of potential distractors (answer options to a multiple choice exercise), that need to be consolidated and/or trimmed down into one list. "
|
|
@@ -238,3 +244,127 @@ template_consolidate_distractors = ChatPromptTemplate(
|
|
| 238 |
input_variables=["standardized_exercise", "brainstorm_outputs", "final_distractors_specification"]
|
| 239 |
)
|
| 240 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# config/templates.py
|
| 2 |
from langchain_core.prompts.chat import ChatPromptTemplate
|
| 3 |
|
| 4 |
+
template_standardize_exercise = ChatPromptTemplate(
|
|
|
|
| 5 |
messages=[
|
| 6 |
("system", "You reformat a given multiple choice exercise into a standardized format. {formatting_instructions}\n\n"
|
| 7 |
"Only 3 elements are always mandatory:\n"
|
|
|
|
| 17 |
input_variables=["user_input", "formatting_instructions"]
|
| 18 |
)
|
| 19 |
|
| 20 |
+
template_standardize_studytext = ChatPromptTemplate(
|
| 21 |
+
messages=[
|
| 22 |
+
("system", "You reformat a given study text into a standardized format. {formatting_instructions}\n\n"
|
| 23 |
+
),
|
| 24 |
+
("human", "Here's the given study text:\n{user_input}")
|
| 25 |
+
],
|
| 26 |
+
input_variables=["user_input", "formatting_instructions"]
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
template_diagnose_double_negation = ChatPromptTemplate(
|
| 30 |
messages=[
|
| 31 |
("system", """Analyze a multiple-choice exercise for the presence of double negatives: either two negations in the question/statement itself, or a negation in the question/statement AND in an answer option.
|
|
|
|
| 229 |
)
|
| 230 |
|
| 231 |
|
|
|
|
|
|
|
| 232 |
template_consolidate_distractors = ChatPromptTemplate(
|
| 233 |
messages=[
|
| 234 |
("system", "You are given several lists of potential distractors (answer options to a multiple choice exercise), that need to be consolidated and/or trimmed down into one list. "
|
|
|
|
| 244 |
input_variables=["standardized_exercise", "brainstorm_outputs", "final_distractors_specification"]
|
| 245 |
)
|
| 246 |
|
| 247 |
+
|
| 248 |
+
template_gen_prompt_a = ChatPromptTemplate(
|
| 249 |
+
messages=[
|
| 250 |
+
("system", """
|
| 251 |
+
You are given a study text that is part of an e-learning and an accompanying list of learning objectives based on the text. Your goal is to refine the learning objectives, such that they adhere to the requirements as closely as possible. These learning objectives will later serve as the basis for multiple-choice exercises, and for this purpose it is crucial that they live up to the requirements in every way.
|
| 252 |
+
|
| 253 |
+
# General approach
|
| 254 |
+
- First intensely study and really internalize the requirements for good learning objectives (listed below).
|
| 255 |
+
- Then, rewrite and improve the learning objectives to better fall in line with the requirements.
|
| 256 |
+
|
| 257 |
+
# Requirements for individual learning objectives
|
| 258 |
+
Good learning objectives:
|
| 259 |
+
- Start with 'The student knows that '
|
| 260 |
+
- Are unambiguous, and contain what later will become the specific correct answer for any multiple choice exercises that would test the learning objective
|
| 261 |
+
- Represent exactly the knowledge as written in the study text
|
| 262 |
+
- Use exactly the same terminology that's used in the study text
|
| 263 |
+
- Mirror also the general language level of the study text. If the text is written with very simple words, then the learning objectives should be also written in very simple words
|
| 264 |
+
- Mirror also the voice of the text (passive or active voice) and the perspective of the text (second or third person)
|
| 265 |
+
- Are as concise as can be: they contain the smallest possible knowledge element. A learning objective does not combine multiple facts, but rather isolates individual facts
|
| 266 |
+
- Avoid absolute terms that overstate their universality, like 'always' and 'never', unless that actually is true 100% of the time (usually there are exceptions to every rule, so account for those in your phrasing)
|
| 267 |
+
- Alternatively avoid vague terms that make what they wanna say too meaningless, like 'can', 'could', 'might' and 'may' (many things 'can', 'could' or 'might be', this doesn't say much)
|
| 268 |
+
- Also avoid subjective terms like 'often', 'sometimes', 'many', 'few', 'common', 'rare'. Instead, make more specific and falsifiable claims like 'in most cases' or 'A is more common than B'
|
| 269 |
+
- Avoid the use of 'important', again a signal word indicating subjectivity. Only use 'important' in statements that you cannot rephrase, yet are actually indisputable ánd meaningful to know when phrased in this way
|
| 270 |
+
|
| 271 |
+
# Process
|
| 272 |
+
- For each learning objective, go over all of the requirements, like methodically checking off a checklist.
|
| 273 |
+
- For any aspect of any learning objective that upon reflection doesn't adhere to the requirements as well as it could, carry out a rewrite (or split up one learning objective into two, for example) of the learning objective.
|
| 274 |
+
- Iteratively keep doing this for each of the individual learning objectives again and again, until you are certain that they are the best versions they can be: each entirely and maximally satisfying the requirements for good learning objectives.
|
| 275 |
+
- Take as much time as you need to get it perfect, then return the list of final learning objectives (for this, use the same language as the study text).
|
| 276 |
+
"""),
|
| 277 |
+
("human", "{standardized_text}")
|
| 278 |
+
],
|
| 279 |
+
input_variables=["standardized_text"]
|
| 280 |
+
)
|
| 281 |
+
|
| 282 |
+
template_gen_prompt_b = ChatPromptTemplate(
|
| 283 |
+
messages=[
|
| 284 |
+
("system", """
|
| 285 |
+
You are given a study text. Based on this, you will identify learning objectives. Follow the following protocol meticulously:
|
| 286 |
+
|
| 287 |
+
# Protocol for Creating Exercises for eLearning Modules
|
| 288 |
+
## General Working Method
|
| 289 |
+
|
| 290 |
+
### Text Orientation
|
| 291 |
+
Assigned a study text, your initial task is to read it to understand the topic for creating exercises.
|
| 292 |
+
|
| 293 |
+
### Learning objectives
|
| 294 |
+
Based on the text, define clear, concise learning objectives. Make sure you have enough learning objectives so that all information is covered, but not too many so that learning objectives won't overlap. It's really important that every learning objective only states 1 single fact and doesn't combine multiple facts. Choose objectives based on text analysis and audience level. Objectives always start with 'The student knows that'.
|
| 295 |
+
|
| 296 |
+
Observe the following rules meticulously when writing learning objectives:
|
| 297 |
+
|
| 298 |
+
#### Avoid 'always' and 'never'
|
| 299 |
+
Don't use words like 'always' or 'never' in learning objectives, it is likely an exception exists. Instead use constructions like 'X *fits with* Y', 'X *suggests* Y', 'X *is more common than* Y'
|
| 300 |
+
|
| 301 |
+
So not:
|
| 302 |
+
> BAD: The students knows that fever *never* occurs with a viral infection.
|
| 303 |
+
> BAD: The students knows that fever *always* occurs with a viral infection.
|
| 304 |
+
|
| 305 |
+
But:
|
| 306 |
+
> GOOD: The students knows that fever is a symptom *that fits with* a viral infection.
|
| 307 |
+
|
| 308 |
+
#### Avoid 'can', 'could', 'may' or 'might'
|
| 309 |
+
Don't use words like *'can'*, *'could'*, *'may'* or *'might'* in learning objectives, because almost everything can, could or might be something, so too suggestive. Instead use constructions like 'X *fits with* Y', 'X *suggests* Y', 'X *is more common than* Y'
|
| 310 |
+
|
| 311 |
+
So not:
|
| 312 |
+
> BAD: The students knows that pain *might* occur with rheumatoid arthritis.
|
| 313 |
+
> BAD: The students knows that pain *can* occur with rheumatoid arthritis.
|
| 314 |
+
> BAD: The students knows that pain *could* occur with rheumatoid arthritis.
|
| 315 |
+
> BAD: The students knows that pain *may* occur with rheumatoid arthritis.
|
| 316 |
+
|
| 317 |
+
But:
|
| 318 |
+
> GOOD: The students knows that pain *is a symptom of* rheumatoid arthritis.
|
| 319 |
+
|
| 320 |
+
#### Avoid subjective terminology like many, few and common
|
| 321 |
+
Don't use words like *'many'*, *'few'*, *'common'*, *'rare'* et cetera as these are subjective. Instead be specific. Instead use terminology like *'in most cases'* or compare: *'symptom A is more common than symptom B'*.
|
| 322 |
+
|
| 323 |
+
Example of a bad learning objective, leading to suggestive or subjective answers:
|
| 324 |
+
> BAD: The student knows that fever commonly occurs with a viral infection.
|
| 325 |
+
|
| 326 |
+
#### Avoid important, essential significant
|
| 327 |
+
Don't use words like *'important'*, *'essential'*, *'significant'* et cetera in learning objectives, as these are prone to subjectivity.
|
| 328 |
+
|
| 329 |
+
Examples of a bad learning objective, leading to suggestive or subjective answers:
|
| 330 |
+
> BAD: The students knows that it's *important* to rest when having a viral infection.
|
| 331 |
+
|
| 332 |
+
The objective uses the word *'important'* which is subjective.
|
| 333 |
+
|
| 334 |
+
> BAD: The students knows that it's *essential* to rest when having a viral infection.
|
| 335 |
+
|
| 336 |
+
The objective uses the word *'essential'* which is subjective.
|
| 337 |
+
|
| 338 |
+
> BAD: The students knows that fever has a *significant* effect on how people feel when they are sick.
|
| 339 |
+
|
| 340 |
+
The objective uses the word *'significant'* which is subjective.
|
| 341 |
+
|
| 342 |
+
> BAD: The student knows that drinking *enough* water is *important* to stay hydrated.
|
| 343 |
+
|
| 344 |
+
The objective uses the word *'important'* which is subjective. Also the word *'enough'* is subjective.
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
A good example is:
|
| 348 |
+
|
| 349 |
+
> GOOD: The student knows that proteinuria is a symptom of nephrotic syndrome.
|
| 350 |
+
|
| 351 |
+
An example of a bad learning objective:
|
| 352 |
+
|
| 353 |
+
> BAD: The student knows the symptoms of nephrotic syndrome.
|
| 354 |
+
|
| 355 |
+
The bad objective does not specify a single fact as symptoms are not specified.
|
| 356 |
+
|
| 357 |
+
A good example is:
|
| 358 |
+
|
| 359 |
+
> GOOD: The student knows that besides pain, rheumatoid arthritis also causes loss of mobility.
|
| 360 |
+
|
| 361 |
+
An example of a bad learning objective:
|
| 362 |
+
|
| 363 |
+
> BAD: The student knows that problems with movement due to joint problems, such as rheumatism, can be painful or completely limit movement
|
| 364 |
+
|
| 365 |
+
The latter objective does not specify a single fact but combines two (can be painful or completely limit movement). The first objective focuses on the 'loss of mobility' element, while the 'pain- element' is already considered known. The exercises generated by this learning objective will test the 'loss of mobility' element (so not the 'pain-element')
|
| 366 |
+
"""),
|
| 367 |
+
("human", "{standardized_text}")
|
| 368 |
+
],
|
| 369 |
+
input_variables=["standardized_text"]
|
| 370 |
+
)
|
main.py
CHANGED
|
@@ -3,8 +3,10 @@ import gradio as gr
|
|
| 3 |
import logging
|
| 4 |
from app.ui.diagnoser_tab import build_diagnoser_tab
|
| 5 |
from app.ui.distractors_tab import build_distractors_tab
|
|
|
|
| 6 |
from chains.diagnoser.runner import run_diagnoser
|
| 7 |
from chains.distractors.runner import run_distractors
|
|
|
|
| 8 |
from utils.auth import login as auth_login
|
| 9 |
|
| 10 |
logger = logging.getLogger(__name__)
|
|
@@ -30,7 +32,7 @@ with gr.Blocks() as interface:
|
|
| 30 |
# --- Main App (initially hidden) ---
|
| 31 |
with gr.Column(visible=False, elem_id="main_app") as app_container:
|
| 32 |
|
| 33 |
-
# --- Standardized Exercise/
|
| 34 |
# A row for Title & the standardized text & copy button
|
| 35 |
with gr.Row():
|
| 36 |
with gr.Column(scale=3):
|
|
@@ -73,25 +75,14 @@ with gr.Blocks() as interface:
|
|
| 73 |
final_distractors_specification,
|
| 74 |
) = build_distractors_tab()
|
| 75 |
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
</div>
|
| 85 |
-
"""
|
| 86 |
-
)
|
| 87 |
-
learning_objectives_input = gr.Textbox(label="Enter a study text in any format", placeholder="<h3>Infusie en infuussystemen</h3> <h4>Inleiding</h4> ...")
|
| 88 |
-
learning_objectives_button = gr.Button("Submit")
|
| 89 |
-
gr.Markdown("**Response(s):**")
|
| 90 |
-
# Create 5 Response textboxes
|
| 91 |
-
distractors_responses = [
|
| 92 |
-
gr.Textbox(label=f"Response {i + 1}", interactive=False, visible=(i == 0))
|
| 93 |
-
for i in range(10)
|
| 94 |
-
]
|
| 95 |
|
| 96 |
# -------------------------------
|
| 97 |
# Set Up Interactions
|
|
@@ -124,5 +115,17 @@ with gr.Blocks() as interface:
|
|
| 124 |
outputs=distractors_responses + [standardized_format_display],
|
| 125 |
)
|
| 126 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
# Launch the app.
|
| 128 |
interface.launch()
|
|
|
|
| 3 |
import logging
|
| 4 |
from app.ui.diagnoser_tab import build_diagnoser_tab
|
| 5 |
from app.ui.distractors_tab import build_distractors_tab
|
| 6 |
+
from app.ui.learning_objectives_tab import build_learning_objectives_tab
|
| 7 |
from chains.diagnoser.runner import run_diagnoser
|
| 8 |
from chains.distractors.runner import run_distractors
|
| 9 |
+
from chains.learning_objectives_generator.runner import run_learning_objectives_generator
|
| 10 |
from utils.auth import login as auth_login
|
| 11 |
|
| 12 |
logger = logging.getLogger(__name__)
|
|
|
|
| 32 |
# --- Main App (initially hidden) ---
|
| 33 |
with gr.Column(visible=False, elem_id="main_app") as app_container:
|
| 34 |
|
| 35 |
+
# --- Standardized Exercise/Study text Display (Initially Invisible Because it's empty) ---
|
| 36 |
# A row for Title & the standardized text & copy button
|
| 37 |
with gr.Row():
|
| 38 |
with gr.Column(scale=3):
|
|
|
|
| 75 |
final_distractors_specification,
|
| 76 |
) = build_distractors_tab()
|
| 77 |
|
| 78 |
+
# Build Learning Objectives Generator tab
|
| 79 |
+
(model_choice_1,
|
| 80 |
+
model_choice_2,
|
| 81 |
+
text_format,
|
| 82 |
+
studytext_input,
|
| 83 |
+
learning_objectives_button,
|
| 84 |
+
[box_0, box_1, box_2, box_3]
|
| 85 |
+
) = build_learning_objectives_tab()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
# -------------------------------
|
| 88 |
# Set Up Interactions
|
|
|
|
| 115 |
outputs=distractors_responses + [standardized_format_display],
|
| 116 |
)
|
| 117 |
|
| 118 |
+
learning_objectives_button.click(
|
| 119 |
+
fn=run_learning_objectives_generator, # Our async generator
|
| 120 |
+
inputs=[studytext_input, model_choice_1, model_choice_2, text_format],
|
| 121 |
+
outputs=[box_0, box_1, box_2, box_3, standardized_format_display],
|
| 122 |
+
# 'stream=True' is needed to allow partial updates
|
| 123 |
+
# in a generator function.
|
| 124 |
+
# In recent Gradio versions, we just do `every=1` or `queue=True`
|
| 125 |
+
queue=True,
|
| 126 |
+
api_name=None,
|
| 127 |
+
# or "stream=True" depending on your version of Gradio
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
# Launch the app.
|
| 131 |
interface.launch()
|