| import os |
| from datetime import datetime |
| import random |
| import requests |
| from io import BytesIO |
| from datetime import date |
| import tempfile |
| from PIL import Image, ImageDraw, ImageFont |
| from huggingface_hub import upload_file |
|
|
| import pandas as pd |
| from huggingface_hub import HfApi, hf_hub_download, Repository |
| from huggingface_hub.repocard import metadata_load |
|
|
| import gradio as gr |
| from datasets import load_dataset, Dataset |
| from huggingface_hub import whoami |
|
|
| import asyncio |
| from functools import partial |
|
|
| EXAM_DATASET_ID = os.getenv("EXAM_DATASET_ID") or "agents-course/unit_1_quiz" |
| EXAM_MAX_QUESTIONS = os.getenv("EXAM_MAX_QUESTIONS") or 1 |
| EXAM_PASSING_SCORE = os.getenv("EXAM_PASSING_SCORE") or 0.8 |
| CERTIFYING_ORG_LINKEDIN_ID = os.getenv("CERTIFYING_ORG_LINKEDIN_ID", "000000") |
| COURSE_TITLE = os.getenv("COURSE_TITLE", "AI Agents Fundamentals") |
|
|
| ds = load_dataset(EXAM_DATASET_ID, split="train") |
|
|
| DATASET_REPO_URL = "https://huggingface.co/datasets/agents-course/certificates" |
|
|
| |
| quiz_data = ds.to_pandas().to_dict("records") |
| random.shuffle(quiz_data) |
|
|
| |
| if EXAM_MAX_QUESTIONS: |
| quiz_data = quiz_data[: int(EXAM_MAX_QUESTIONS)] |
|
|
|
|
| def on_user_logged_in(token: gr.OAuthToken | None): |
| """ |
| If the user has a valid token, show Start button. |
| Otherwise, keep the login button visible. |
| """ |
| if token is not None: |
| return [ |
| gr.update(visible=False), |
| gr.update(visible=True), |
| gr.update(visible=False), |
| gr.update(visible=False), |
| "", |
| gr.update(choices=[], visible=False), |
| "Click 'Start' to begin the quiz", |
| 0, |
| [], |
| gr.update(visible=False), |
| gr.update(visible=False), |
| token, |
| ] |
| else: |
| return [ |
| gr.update(visible=True), |
| gr.update(visible=False), |
| gr.update(visible=False), |
| gr.update(visible=False), |
| "", |
| gr.update(choices=[], visible=False), |
| "", |
| 0, |
| [], |
| gr.update(visible=False), |
| gr.update(visible=False), |
| None, |
| ] |
|
|
|
|
| def generate_certificate(name: str, profile_url: str): |
| """Generate certificate image and PDF.""" |
| certificate_path = os.path.join( |
| os.path.dirname(__file__), "templates", "certificate.png" |
| ) |
| im = Image.open(certificate_path) |
| d = ImageDraw.Draw(im) |
|
|
| name_font = ImageFont.truetype("Quattrocento-Regular.ttf", 100) |
| date_font = ImageFont.truetype("Quattrocento-Regular.ttf", 48) |
|
|
| name = name.title() |
| d.text((1000, 740), name, fill="black", anchor="mm", font=name_font) |
|
|
| d.text((1480, 1170), str(date.today()), fill="black", anchor="mm", font=date_font) |
|
|
| pdf = im.convert("RGB") |
| pdf.save("certificate.pdf") |
|
|
| return im, "certificate.pdf" |
|
|
|
|
| def create_linkedin_button(username: str, cert_url: str | None) -> str: |
| """Create LinkedIn 'Add to Profile' button HTML.""" |
| current_year = date.today().year |
| current_month = date.today().month |
|
|
| |
| certificate_url = cert_url or "https://huggingface.co/agents-course-finishers" |
|
|
| linkedin_params = { |
| "startTask": "CERTIFICATION_NAME", |
| "name": COURSE_TITLE, |
| "organizationName": "Hugging Face", |
| "organizationId": CERTIFYING_ORG_LINKEDIN_ID, |
| "organizationIdissueYear": str(current_year), |
| "issueMonth": str(current_month), |
| "certUrl": certificate_url, |
| "certId": username, |
| } |
|
|
| |
| base_url = "https://www.linkedin.com/profile/add?" |
| params = "&".join( |
| f"{k}={requests.utils.quote(v)}" for k, v in linkedin_params.items() |
| ) |
| button_url = base_url + params |
|
|
| message = f""" |
| <a href="{button_url}" target="_blank" style="display: block; margin-top: 20px; text-align: center;"> |
| <img src="https://download.linkedin.com/desktop/add2profile/buttons/en_US.png" |
| alt="LinkedIn Add to Profile button"> |
| </a> |
| """ |
| return message |
|
|
|
|
| async def upload_certificate_to_hub(username: str, certificate_img) -> str: |
| """Upload certificate to the dataset hub and return the URL asynchronously.""" |
| |
| with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp: |
| certificate_img.save(tmp.name) |
|
|
| try: |
| |
| loop = asyncio.get_event_loop() |
| upload_func = partial( |
| upload_file, |
| path_or_fileobj=tmp.name, |
| path_in_repo=f"certificates/{username}/{date.today()}.png", |
| repo_id="agents-course/certificates", |
| repo_type="dataset", |
| token=os.getenv("HF_TOKEN"), |
| ) |
| await loop.run_in_executor(None, upload_func) |
|
|
| |
| cert_url = ( |
| f"https://huggingface.co/datasets/agents-course/certificates/" |
| f"resolve/main/certificates/{username}/{date.today()}.png" |
| ) |
|
|
| |
| os.unlink(tmp.name) |
| return cert_url |
|
|
| except Exception as e: |
| print(f"Error uploading certificate: {e}") |
| os.unlink(tmp.name) |
| return None |
|
|
|
|
| async def push_results_to_hub( |
| user_answers, |
| custom_name: str | None, |
| token: gr.OAuthToken | None, |
| profile: gr.OAuthProfile | None, |
| ): |
| """Handle quiz completion and certificate generation.""" |
| if token is None or profile is None: |
| gr.Warning("Please log in to Hugging Face before submitting!") |
| return ( |
| gr.update(visible=True, value="Please login first"), |
| gr.update(visible=False), |
| gr.update(visible=False), |
| gr.update(visible=False), |
| ) |
|
|
| |
| correct_count = sum(1 for answer in user_answers if answer["is_correct"]) |
| total_questions = len(user_answers) |
| grade = correct_count / total_questions if total_questions > 0 else 0 |
|
|
| if grade < float(EXAM_PASSING_SCORE): |
| return ( |
| gr.update(visible=True, value=f"You scored {grade:.1%}..."), |
| gr.update(visible=False), |
| gr.update(visible=False), |
| gr.update(visible=False), |
| ) |
|
|
| try: |
| |
| name = ( |
| custom_name.strip() if custom_name and custom_name.strip() else profile.name |
| ) |
|
|
| |
| certificate_img, _ = generate_certificate( |
| name=name, profile_url=profile.picture |
| ) |
|
|
| |
| gr.Info("Uploading your certificate...") |
| cert_url = await upload_certificate_to_hub(profile.username, certificate_img) |
|
|
| if cert_url is None: |
| gr.Warning("Certificate upload failed, but you still passed!") |
| cert_url = "https://huggingface.co/agents-course" |
|
|
| |
| linkedin_button = create_linkedin_button(profile.username, cert_url) |
|
|
| result_message = f""" |
| 🎉 Congratulations! You passed with a score of {grade:.1%}! |
| |
| {linkedin_button} |
| """ |
|
|
| return ( |
| gr.update(visible=True, value=result_message), |
| gr.update(visible=True, value=certificate_img), |
| gr.update(visible=True), |
| gr.update(visible=True), |
| ) |
|
|
| except Exception as e: |
| print(f"Error generating certificate: {e}") |
| return ( |
| gr.update(visible=True, value=f"🎉 You passed with {grade:.1%}!"), |
| gr.update(visible=False), |
| gr.update(visible=False), |
| gr.update(visible=False), |
| ) |
|
|
|
|
| def handle_quiz( |
| question_idx, |
| user_answers, |
| selected_answer, |
| is_start, |
| token: gr.OAuthToken | None, |
| profile: gr.OAuthProfile | None, |
| ): |
| """Handle quiz state transitions and store answers""" |
| if token is None or profile is None: |
| gr.Warning("Please log in to Hugging Face before starting the quiz!") |
| return [ |
| "", |
| gr.update(choices=[], visible=False), |
| "Please login first", |
| question_idx, |
| user_answers, |
| gr.update(visible=True), |
| gr.update(visible=False), |
| gr.update(visible=False), |
| gr.update(visible=False), |
| gr.update(visible=False), |
| ] |
|
|
| if not is_start and question_idx < len(quiz_data): |
| current_q = quiz_data[question_idx] |
| correct_reference = current_q["correct_answer"] |
| correct_reference = f"answer_{correct_reference}".lower() |
| is_correct = selected_answer == current_q[correct_reference] |
| user_answers.append( |
| { |
| "question": current_q["question"], |
| "selected_answer": selected_answer, |
| "correct_answer": current_q[correct_reference], |
| "is_correct": is_correct, |
| "correct_reference": correct_reference, |
| } |
| ) |
| question_idx += 1 |
|
|
| if question_idx >= len(quiz_data): |
| correct_count = sum(1 for answer in user_answers if answer["is_correct"]) |
| grade = correct_count / len(user_answers) |
| results_text = ( |
| f"**Quiz Complete!**\n\n" |
| f"Your score: {grade:.1%}\n" |
| f"Passing score: {float(EXAM_PASSING_SCORE):.1%}\n\n" |
| ) |
| has_passed = grade >= float(EXAM_PASSING_SCORE) |
| return [ |
| "", |
| gr.update(choices=[], visible=False), |
| f"{'🎉 Passed! Click now on 🎓 Get your certificate!' if has_passed else '❌ Did not pass'}", |
| question_idx, |
| user_answers, |
| gr.update(visible=False), |
| gr.update(visible=False), |
| gr.update( |
| visible=True, |
| value=f"🎓 Get your certificate" if has_passed else "❌ Did not pass", |
| interactive=has_passed, |
| ), |
| gr.update(visible=False), |
| gr.update(visible=False), |
| ] |
|
|
| |
| q = quiz_data[question_idx] |
| return [ |
| f"## Question {question_idx + 1} \n### {q['question']}", |
| gr.update( |
| choices=[q["answer_a"], q["answer_b"], q["answer_c"], q["answer_d"]], |
| value=None, |
| visible=True, |
| ), |
| "Select an answer and click 'Next' to continue.", |
| question_idx, |
| user_answers, |
| gr.update(visible=False), |
| gr.update(visible=True), |
| gr.update(visible=False), |
| gr.update(visible=False), |
| gr.update(visible=False), |
| ] |
|
|
|
|
| def success_message(response): |
| |
| return f"{response}\n\n**Success!**" |
|
|
|
|
| with gr.Blocks() as demo: |
| demo.title = f"Dataset Quiz for {EXAM_DATASET_ID}" |
|
|
| |
| question_idx = gr.State(value=0) |
| user_answers = gr.State(value=[]) |
| user_token = gr.State(value=None) |
|
|
| with gr.Row(variant="compact"): |
| gr.Markdown(f"## Welcome to the {EXAM_DATASET_ID} Quiz") |
|
|
| with gr.Row(variant="compact"): |
| gr.Markdown( |
| "- Log in first, then click 'Start' to begin. \n- Answer each question, click 'Next' \n- click 'Submit' to publish your results to the Hugging Face Hub." |
| ) |
|
|
| with gr.Row(variant="panel"): |
| question_text = gr.Markdown("") |
| radio_choices = gr.Radio( |
| choices=[], label="Your Answer", scale=1, visible=False |
| ) |
|
|
| with gr.Row(variant="compact"): |
| status_text = gr.Markdown("") |
| certificate_img = gr.Image(type="pil", visible=False) |
| linkedin_btn = gr.HTML(visible=False) |
|
|
| with gr.Row(variant="compact"): |
| login_btn = gr.LoginButton(visible=True) |
| start_btn = gr.Button("Start ⏭️", visible=True) |
| next_btn = gr.Button("Next ⏭️", visible=False) |
| submit_btn = gr.Button("🎓 Get your certificate", visible=False) |
|
|
| with gr.Row(variant="panel"): |
| custom_name_input = gr.Textbox( |
| label="Custom Name for Certificate", |
| placeholder="Enter name as you want it to appear on the certificate", |
| info="Leave empty to use your Hugging Face profile name", |
| visible=False, |
| value=None, |
| ) |
|
|
| |
| login_btn.click( |
| fn=on_user_logged_in, |
| inputs=None, |
| outputs=[ |
| login_btn, |
| start_btn, |
| next_btn, |
| submit_btn, |
| question_text, |
| radio_choices, |
| status_text, |
| question_idx, |
| user_answers, |
| certificate_img, |
| linkedin_btn, |
| user_token, |
| ], |
| ) |
|
|
| start_btn.click( |
| fn=handle_quiz, |
| inputs=[question_idx, user_answers, gr.State(""), gr.State(True)], |
| outputs=[ |
| question_text, |
| radio_choices, |
| status_text, |
| question_idx, |
| user_answers, |
| start_btn, |
| next_btn, |
| submit_btn, |
| certificate_img, |
| linkedin_btn, |
| ], |
| ) |
|
|
| next_btn.click( |
| fn=handle_quiz, |
| inputs=[question_idx, user_answers, radio_choices, gr.State(False)], |
| outputs=[ |
| question_text, |
| radio_choices, |
| status_text, |
| question_idx, |
| user_answers, |
| start_btn, |
| next_btn, |
| submit_btn, |
| certificate_img, |
| linkedin_btn, |
| ], |
| ) |
|
|
| submit_btn.click( |
| fn=push_results_to_hub, |
| inputs=[ |
| user_answers, |
| custom_name_input, |
| ], |
| outputs=[ |
| status_text, |
| certificate_img, |
| linkedin_btn, |
| custom_name_input, |
| ], |
| ) |
|
|
| custom_name_input.submit( |
| fn=push_results_to_hub, |
| inputs=[user_answers, custom_name_input], |
| outputs=[status_text, certificate_img, linkedin_btn, custom_name_input], |
| ) |
|
|
| if __name__ == "__main__": |
| |
| |
| demo.queue() |
| demo.launch() |
|
|