import json import re import gradio as gr from llama_cpp import Llama, LlamaGrammar from huggingface_hub import hf_hub_download # ========================================== # 1. LOAD THE MODEL # ========================================== print("Checking LLM status...") global llm try: if 'llm' in globals() and llm is not None: print("Model already loaded in memory!") else: raise NameError except NameError: print("Downloading Gemma Model...") model_path = hf_hub_download( repo_id="bartowski/google_gemma-4-E4B-it-GGUF", filename="google_gemma-4-E4B-it-Q4_K_M.gguf" ) print("Loading LLM into RAM...") llm = Llama(model_path=model_path, n_ctx=4096, n_threads=2, verbose=False) print("Model Loaded Successfully!") # ========================================== # 2. TEMPLATES & GRAMMARS # ========================================== TEMPLATES = { "Fill-in-the-blank": { "dna": [{"question": "If she ___ (miss) the flight, she would buy a new ticket.", "answer": "missed"}], "grammar": r""" root ::= "[" ws item ("," ws item)* ws "]" item ::= "{" ws "\"question\"" ws ":" ws string "," ws "\"answer\"" ws ":" ws string ws "}" string ::= "\"" ([^"\\\n] | "\\" (["\\/bfnrt] | "u"[0-9a-fA-F]{4}))* "\"" ws ::= [ \t\n\r]* """ }, "Multiple Choice": { "dna": [{"question": "The company ___ a profit last year.", "options": ["make", "made", "makes", "making"], "answer": "made"}], "grammar": r""" root ::= "[" ws item ("," ws item)* ws "]" item ::= "{" ws "\"question\"" ws ":" ws string "," ws "\"options\"" ws ":" ws "[" ws string ("," ws string)* ws "]" "," ws "\"answer\"" ws ":" ws string ws "}" string ::= "\"" ([^"\\\n] | "\\" (["\\/bfnrt] | "u"[0-9a-fA-F]{4}))* "\"" ws ::= [ \t\n\r]* """ }, "Sentence Reorder": { "dna": [{"scrambled": "yesterday / went / to / the / store / I", "correct_order": "I went to the store yesterday."}], "grammar": r""" root ::= "[" ws item ("," ws item)* ws "]" item ::= "{" ws "\"scrambled\"" ws ":" ws string "," ws "\"correct_order\"" ws ":" ws string ws "}" string ::= "\"" ([^"\\\n] | "\\" (["\\/bfnrt] | "u"[0-9a-fA-F]{4}))* "\"" ws ::= [ \t\n\r]* """ }, "Free QA": { "dna": [{"question": "What is the main advantage of open-source software?", "suggested_answer": "It allows anyone to inspect, modify, and enhance the source code."}], "grammar": r""" root ::= "[" ws item ("," ws item)* ws "]" item ::= "{" ws "\"question\"" ws ":" ws string "," ws "\"suggested_answer\"" ws ":" ws string ws "}" string ::= "\"" ([^"\\\n] | "\\" (["\\/bfnrt] | "u"[0-9a-fA-F]{4}))* "\"" ws ::= [ \t\n\r]* """ } } # ========================================== # 3. GENERATION LOGIC # ========================================== def generate_expansion(form_choice, user_requirement): selected_dna = TEMPLATES[form_choice]["dna"] quiz_grammar = LlamaGrammar.from_string(TEMPLATES[form_choice]["grammar"]) prompt = f"""userYou are an expert educational content creator.### TASK REQUIREMENTS{user_requirement}### STYLE GUIDE (DNA)Follow this exact JSON structure and keys:{json.dumps(selected_dna, indent=2)}### INSTRUCTIONS- Generate one exact question.- Output ONLY a raw JSON array matching the format above.model""" output = llm( prompt, max_tokens=1500, temperature=0.5, stop=[""], grammar=quiz_grammar, echo=False ) try: raw_text = output['choices'][0]['text'].strip() quiz_data = json.loads(raw_text) json_str = json.dumps(quiz_data, indent=2, ensure_ascii=False) # Package types and data into a single, unified state object state_payload = {"type": form_choice, "questions": quiz_data} return state_payload, json_str except Exception as e: error_payload = {"type": form_choice, "questions": []} return error_payload, f"Error: {e}\n\nRaw Output:\n{output['choices'][0]['text']}" # ========================================== # 4. GRADIO INTERFACE # ========================================== with gr.Blocks(theme=gr.themes.Soft()) as demo: gr.Markdown(""" # 🚀 RA-ICL: Generative Expansion & Template Engine Welcome! This Space demonstrates a highly advanced application of **Retrieval-Augmented In-Context Learning (RA-ICL)** known as **1-to-N Generative Expansion**. """) # FIX: Combined single state container avoids double-rendering browser freezes entirely quiz_state = gr.State({"type": "", "questions": []}) with gr.Row(): with gr.Column(scale=1): form_choice = gr.Radio( choices=["Multiple Choice", "Fill-in-the-blank", "Sentence Reorder", "Free QA"], value="Multiple Choice", label="1. Output Structure" ) user_requirement = gr.Textbox( label="2. Content Requirement", placeholder="e.g., Generate 3 questions about German past tense verbs (Level Advanced). Theme: Holiday.", lines=5 ) generate_btn = gr.Button("Generate Interactive Quiz", variant="primary", size="lg") with gr.Column(scale=2): with gr.Tabs(): with gr.TabItem("🎮 Playable Quiz"): # Reacts perfectly to the unified single state change after LLM completion @gr.render(inputs=quiz_state) def render_quiz(state_data): f_type = state_data.get("type", "") q_data = state_data.get("questions", []) if not q_data: gr.Markdown("*Generate a quiz to see it here.*") return gr.Markdown(f"### {f_type} Quiz") for i, q in enumerate(q_data): with gr.Group(key=f"group_{f_type}_{i}"): # Multiple Choice if f_type == "Multiple Choice": gr.Markdown(f"**Q{i+1}:** {q['question']}", key=f"md_q_{i}") user_ans = gr.Radio(choices=q['options'], label="Your Answer", key=f"rad_ans_{i}") check_btn = gr.Button("Check Answer", size="sm", key=f"btn_chk_{i}") feedback = gr.Markdown(key=f"md_fb_{i}") def check_mcq(ans, correct=q['answer']): if not ans: return "⚠️ Please select an answer." if ans == correct: return "✅ **Correct!**" return f"❌ **Incorrect.** The correct answer is: {correct}" check_btn.click(fn=check_mcq, inputs=[user_ans], outputs=[feedback]) # Fill in the blank elif f_type == "Fill-in-the-blank": gr.Markdown(f"**Q{i+1}:** {q['question']}", key=f"md_q_{i}") user_ans = gr.Textbox(label="Your Answer", placeholder="Type here...", key=f"txt_ans_{i}") check_btn = gr.Button("Check Answer", size="sm", key=f"btn_chk_{i}") feedback = gr.Markdown(key=f"md_fb_{i}") def check_fitb(ans, correct=q['answer']): if not ans: return "⚠️ Please type an answer." if ans.strip().lower() == correct.strip().lower(): return "✅ **Correct!**" return f"❌ **Incorrect.** The correct answer is: {correct}" check_btn.click(fn=check_fitb, inputs=[user_ans], outputs=[feedback]) # Sentence Reorder elif f_type == "Sentence Reorder": gr.Markdown(f"**Q{i+1} (Rearrange):**\n*{q['scrambled']}*", key=f"md_q_{i}") user_ans = gr.Textbox(label="Correct Order", placeholder="Type the full sentence here...", key=f"txt_ans_{i}") check_btn = gr.Button("Check Answer", size="sm", key=f"btn_chk_{i}") feedback = gr.Markdown(key=f"md_fb_{i}") def check_reorder(ans, correct=q['correct_order']): if not ans: return "⚠️ Please type an answer." clean_ans = re.sub(r'[^a-z0-9äöüß]', '', ans.lower()) clean_correct = re.sub(r'[^a-z0-9äöüß]', '', correct.lower()) if clean_ans == clean_correct: return "✅ **Correct!**" return f"❌ **Incorrect.** The correct sentence is:\n{correct}" check_btn.click(fn=check_reorder, inputs=[user_ans], outputs=[feedback]) # Free QA elif f_type == "Free QA": gr.Markdown(f"**Q{i+1}:** {q['question']}", key=f"md_q_{i}") user_ans = gr.Textbox(label="Your Answer", lines=2, key=f"txt_ans_{i}") show_btn = gr.Button("Show Suggested Answer", size="sm", key=f"btn_show_{i}") feedback = gr.Markdown(visible=False, key=f"md_fb_{i}") def show_ans(correct=q['suggested_answer']): return gr.update(value=f"**Suggested Answer:** {correct}", visible=True) show_btn.click(fn=show_ans, outputs=[feedback]) with gr.TabItem("⚙️ Raw Data (JSON)"): output_json = gr.Code(label="Generated JSON Output", language="json") # Connect the generate button to update the single state object generate_btn.click( fn=generate_expansion, inputs=[form_choice, user_requirement], outputs=[quiz_state, output_json] ) if __name__ == "__main__": demo.launch(server_name="0.0.0.0", server_port=7860)