from __future__ import annotations from functools import partial import gradio as gr from config import APP_NAME, APP_SUBTITLE, get_openai_api_key, get_openai_model from openai_service import format_openai_error, stream_tutor_response from prompts import ABOUT_PAK_ANGELS, MODULES, build_system_instructions BUILD_ID = "pak-angels-zerogpu-2026-07-11-v2" try: import spaces except ImportError: class _SpacesFallback: @staticmethod def GPU(*args, **kwargs): if args and callable(args[0]) and len(args) == 1 and not kwargs: return args[0] def decorator(function): return function return decorator spaces = _SpacesFallback() print(f"Starting {APP_NAME} build {BUILD_ID}") PRIVACY_NOTICE = ( "Privacy notice: Do not enter confidential, proprietary, financial, medical, " "personal, or otherwise sensitive information into the AI Tutor." ) CSS = """ :root { --pak-blue: #0b5cab; --pak-blue-dark: #073f78; --pak-blue-soft: #eaf4ff; --pak-line: #d8e5f2; --pak-text: #14213d; } body, .gradio-container { background: #f7fbff !important; color: var(--pak-text); } .main-shell { max-width: 1180px; margin: 0 auto; } .hero { background: linear-gradient(135deg, #ffffff 0%, #eaf4ff 62%, #d6ebff 100%); border: 1px solid var(--pak-line); border-radius: 8px; padding: 24px; margin-bottom: 14px; } .hero h1 { color: var(--pak-blue-dark); font-size: 38px; line-height: 1.1; margin: 0 0 8px 0; } .hero p { margin: 5px 0; font-size: 16px; } .mode-label { border-left: 5px solid var(--pak-blue); background: #ffffff; border-radius: 8px; padding: 14px 16px; box-shadow: 0 1px 4px rgba(11, 92, 171, 0.08); } .privacy { background: #fff8e8; border: 1px solid #f1d28c; border-radius: 8px; padding: 12px 14px; font-size: 14px; } .side-panel { background: #ffffff; border: 1px solid var(--pak-line); border-radius: 8px; padding: 14px; } .suggestion-button { min-height: 46px; } button.primary { background: var(--pak-blue) !important; border-color: var(--pak-blue) !important; } """ def hero_html() -> str: return f"""

{APP_NAME}

{APP_SUBTITLE}

Pak Angels AI Tutor helps students, faculty, professionals, entrepreneurs, and startup founders learn Artificial Intelligence, build practical applications, design intelligent workflows, automate business processes, and develop AI-powered startups.

""" def module_summary_html(module_name: str) -> str: module = MODULES[module_name] about = "" if module_name == "About Pak Angels": about = f"

{ABOUT_PAK_ANGELS}

" return f"""
Selected learning mode: {module_name}
{module["summary"]} {about}
""" def topics_markdown(module_name: str) -> str: topics = "\n".join(f"- {topic}" for topic in MODULES[module_name]["topics"]) return f"### Topics in this mode\n{topics}" def get_suggestion(module_name: str, index: int) -> str: suggestions = MODULES[module_name]["suggestions"] return suggestions[index] if index < len(suggestions) else "" def update_module(module_name: str): suggestions = MODULES[module_name]["suggestions"] button_updates = [ gr.update(value=suggestion, visible=True) for suggestion in suggestions[:5] ] while len(button_updates) < 5: button_updates.append(gr.update(value="", visible=False)) return ( module_summary_html(module_name), topics_markdown(module_name), *button_updates, ) def add_user_message(message: str, history: list[dict[str, str]] | None): history = list(history or []) message = (message or "").strip() if not message: return "", history history.append({"role": "user", "content": message}) return "", history def generate_response(history: list[dict[str, str]] | None, module_name: str): history = list(history or []) if not history or history[-1]["role"] != "user": yield history return api_key = get_openai_api_key() if not api_key: message = ( "OPENAI_API_KEY is not configured. In Hugging Face Spaces, add it under " "Settings -> Variables and secrets -> New secret, then restart the Space." ) history.append({"role": "assistant", "content": message}) yield history return history.append({"role": "assistant", "content": ""}) try: for delta in stream_tutor_response( api_key=api_key, model=get_openai_model(), system_instructions=build_system_instructions(module_name), messages=history[:-1], ): history[-1]["content"] += delta yield history except Exception as error: history[-1]["content"] = format_openai_error(error) yield history @spaces.GPU(duration=120) def submit_message(message: str, history: list[dict[str, str]] | None, module_name: str): textbox, updated_history = add_user_message(message, history) yield textbox, updated_history for streamed_history in generate_response(updated_history, module_name): yield textbox, streamed_history def submit_suggestion( suggestion_index: int, history: list[dict[str, str]] | None, module_name: str, ): question = get_suggestion(module_name, suggestion_index) yield from submit_message(question, history, module_name) def clear_conversation(): return [] def build_app() -> gr.Blocks: with gr.Blocks( title=APP_NAME, css=CSS, theme=gr.themes.Soft(primary_hue="blue", neutral_hue="slate"), ) as demo: with gr.Column(elem_classes=["main-shell"]): gr.HTML(hero_html()) with gr.Row(equal_height=False): with gr.Column(scale=1, min_width=260, elem_classes=["side-panel"]): module_selector = gr.Radio( choices=list(MODULES.keys()), value="Home", label="Learning mode", ) gr.Textbox( value=get_openai_model(), label="OpenAI model", interactive=False, ) new_button = gr.Button("New Conversation") clear_button = gr.Button("Clear Chat") with gr.Accordion("About Pak Angels", open=False): gr.Markdown(ABOUT_PAK_ANGELS) with gr.Column(scale=3, min_width=420): module_summary = gr.HTML(module_summary_html("Home")) gr.HTML(f'
{PRIVACY_NOTICE}
') topics = gr.Markdown(topics_markdown("Home")) gr.Markdown("### Suggested questions") suggestion_buttons = [] with gr.Row(): suggestion_buttons.append( gr.Button(get_suggestion("Home", 0), elem_classes=["suggestion-button"]) ) suggestion_buttons.append( gr.Button(get_suggestion("Home", 1), elem_classes=["suggestion-button"]) ) with gr.Row(): suggestion_buttons.append( gr.Button(get_suggestion("Home", 2), elem_classes=["suggestion-button"]) ) suggestion_buttons.append( gr.Button(get_suggestion("Home", 3), elem_classes=["suggestion-button"]) ) suggestion_buttons.append( gr.Button(get_suggestion("Home", 4), elem_classes=["suggestion-button"]) ) chatbot = gr.Chatbot( label="Pak Angels AI Tutor", type="messages", height=520, show_copy_button=True, allow_tags=False, ) message_box = gr.Textbox( label="Ask Pak Angels AI Tutor", placeholder="Ask a question or choose a suggested question above.", lines=3, ) send_button = gr.Button("Send", variant="primary") module_selector.change( update_module, inputs=[module_selector], outputs=[module_summary, topics, *suggestion_buttons], ) send_button.click( submit_message, inputs=[message_box, chatbot, module_selector], outputs=[message_box, chatbot], ) message_box.submit( submit_message, inputs=[message_box, chatbot, module_selector], outputs=[message_box, chatbot], ) for index, button in enumerate(suggestion_buttons): button.click( partial(submit_suggestion, index), inputs=[chatbot, module_selector], outputs=[message_box, chatbot], ) new_button.click(clear_conversation, outputs=[chatbot]) clear_button.click(clear_conversation, outputs=[chatbot]) return demo demo = build_app() if __name__ == "__main__": demo.queue().launch(server_name="0.0.0.0", server_port=7860, ssr_mode=False)