from __future__ import annotations import logging import os from functools import lru_cache from threading import Thread from typing import Generator, List, Tuple import gradio as gr import regex import spaces import torch from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) def convert_latex_brackets_to_dollars(text: str) -> str: """Convert LaTeX bracket/paren sections into dollar-based math mode.""" def replace_display_latex(match): return f"\n $$ {match.group(1).strip()} $$ \n" text = regex.sub(r"(?r)\\\[\s*([^\[\]]+?)\s*\\\]", replace_display_latex, text) def replace_paren_latex(match): return f" $ {match.group(1).strip()} $ " text = regex.sub(r"(?r)\\\(\s*(.+?)\s*\\\)", replace_paren_latex, text) return text MODEL_NAME = os.getenv("MODEL_NAME", "Intel/hebrew-math-tutor-v1") @lru_cache(maxsize=1) def load_model_and_tokenizer(): logger.info(f"Loading model: {MODEL_NAME}") tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME) model = AutoModelForCausalLM.from_pretrained( MODEL_NAME, torch_dtype=torch.float16, device_map="auto", ) logger.info("Model loaded successfully") return model, tokenizer model, tokenizer = load_model_and_tokenizer() DEFAULT_LANG = "he" labels = { "he": { "title": "מתמטיבוט 🧮", "intro": ( """ ברוכים הבאים לדמו! 💡 כאן תוכלו להתרשם **ממודל השפה החדש** שלנו; מודל בגודל 4 מיליארד פרמטרים שאומן לענות על שאלות מתמטיות בעברית, על המחשב שלכם, ללא חיבור לרשת. קישור למודל, פרטים נוספים, יצירת קשר ותנאי שימוש: https://huggingface.co/Intel/hebrew-math-tutor-v1 ----- """ ), "select_label": "בחרו שאלה מוכנה או צרו שאלה חדשה:", "new_question": "שאלה חדשה...", "text_label": "שאלה:", "placeholder": "הזינו את השאלה כאן...", "send": "שלח", "reset": "שיחה חדשה", "toggle_to": "English 🇬🇧", "predefined": [ "שאלה חדשה...", " מהו סכום הסדרה הבאה: 1 + 1/2 + 1/4 + 1/8 + ...", "פתח את הביטוי: (a-b)^4", "פתרו את המשוואה הבאה: sin(2x) = 0.5", ], "summary_text": "לחץ כדי לראות את תהליך החשיבה", "thinking_prefix": "🤔 חושב", "thinking_done": "🤔 *תהליך החשיבה הושלם, מכין תשובה...*", "final_label": "📝 תשובה סופית:", "chat_label": "צ'אט", }, "en": { "title": "MathBot 🧮", "intro": ( """ Welcome to the demo! 💡 Here you can try our **new language model** — a 4-billion-parameter model trained to answer math questions in Hebrew while maintaining its English capabilities. It runs locally on your machine without requiring an internet connection. For the model page and more details see: https://huggingface.co/Intel/hebrew-math-tutor-v1 ----- """ ), "select_label": "Choose a prepared question or create a new one:", "new_question": "New question...", "text_label": "Question:", "placeholder": "Type your question here...", "send": "Send", "reset": "New Conversation", "toggle_to": "עברית 🇮🇱", "predefined": [ "New question...", "What is the sum of the series: 1 + 1/2 + 1/4 + 1/8 + ...", "Expand the expression: (a-b)^4", "Solve the equation: sin(2x) = 0.5", ], "summary_text": "Click to view the thinking process", "thinking_prefix": "🤔 Thinking", "thinking_done": "🤔 *Thinking complete, preparing answer...*", "final_label": "📝 Final answer:", "chat_label": "Chat", }, } def dir_and_alignment(lang: str) -> Tuple[str, str]: if lang == "he": return "rtl", "right" return "ltr", "left" _details_template = ( '
' "🤔 {summary}" '
{content}
' "
" ) def wrap_text_with_direction(text: str, lang: str, emphasized: bool = False) -> str: direction, align = dir_and_alignment(lang) weight = "font-weight: 600;" if emphasized else "" return f'
{text}
' def build_system_prompt(lang: str) -> str: if lang == "he": return ( "You are a helpful AI assistant specialized in mathematics and problem-solving " "who can answer math questions with the correct answer. Answer shortly, not more than 500 " "tokens, but outline the process step by step. Answer ONLY in Hebrew!" ) return ( "You are a helpful AI assistant specialized in mathematics and problem-solving who can answer " "math questions with the correct answer. Answer shortly, not more than 500 tokens, but outline " "the process step by step." ) def thinking_indicator(lang: str, progress_token_count: int) -> str: direction, align = dir_and_alignment(lang) border_side = "right" if direction == "rtl" else "left" dots = "." * (progress_token_count % 6 or 1) prefix = labels[lang]["thinking_prefix"] return ( f'
' f'

{prefix}{dots}

' "
" ) def build_assistant_markdown( lang: str, final_answer: str, thinking_text: str | None, ) -> str: direction, align = dir_and_alignment(lang) localized = labels[lang] parts: List[str] = [] if thinking_text: details = _details_template.format( dir=direction, align=align, summary=localized["summary_text"], content=thinking_text, ) parts.append(details) parts.append(wrap_text_with_direction(localized["thinking_done"], lang)) parts.append(wrap_text_with_direction(localized["final_label"], lang, emphasized=True)) converted_answer = convert_latex_brackets_to_dollars(final_answer.strip()) parts.append(wrap_text_with_direction(converted_answer or "…", lang)) return "\n\n".join(parts) @spaces.GPU def handle_user_message( user_input: str, lang: str, chat_history: List[Tuple[str, str]] | None, ) -> Generator[tuple, None, None]: lang = lang or DEFAULT_LANG localized = labels[lang] chat_history = chat_history or [] prompt = (user_input or "").strip() if not prompt: yield ( chat_history, gr.Textbox.update(value=""), gr.Dropdown.update(value=localized["new_question"]), chat_history, ) return formatted_user = wrap_text_with_direction(prompt, lang) chat_history = chat_history + [(formatted_user, "")] dropdown_reset = gr.Dropdown.update(value=localized["new_question"]) yield chat_history, gr.Textbox.update(value=""), dropdown_reset, chat_history system_prompt = build_system_prompt(lang) # Format as chat template chat_messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": prompt}, ] # Apply chat template input_text = tokenizer.apply_chat_template( chat_messages, tokenize=False, add_generation_prompt=True, ) inputs = tokenizer(input_text, return_tensors="pt").to(model.device) thinking_buffer = "" thinking_text: str | None = None final_answer = "" response_fallback = "" in_thinking = False try: streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True) generation_kwargs = dict( **inputs, streamer=streamer, max_new_tokens=2400, temperature=0.6, top_p=0.95, top_k=20, do_sample=True, ) thread = Thread(target=model.generate, kwargs=generation_kwargs) thread.start() for delta in streamer: if not delta: continue response_fallback += delta if "" in delta: in_thinking = True if in_thinking: thinking_buffer += delta if "" in delta: in_thinking = False thinking_text = ( thinking_buffer.replace("", "").replace("", "").strip() ) current_answer = thinking_indicator(lang, len(thinking_buffer)) else: final_answer += delta current_answer = build_assistant_markdown( lang=lang, final_answer=final_answer, thinking_text=thinking_text, ) chat_history[-1] = (formatted_user, current_answer) yield chat_history, gr.Textbox.update(value=""), dropdown_reset, chat_history thread.join() except Exception as exc: error_html = wrap_text_with_direction(f"⚠️ Error generating response: {exc}", lang) chat_history[-1] = (formatted_user, error_html) yield chat_history, gr.Textbox.update(value=prompt), dropdown_reset, chat_history return if not final_answer: final_answer = response_fallback chat_history[-1] = ( formatted_user, build_assistant_markdown(lang=lang, final_answer=final_answer, thinking_text=thinking_text), ) yield chat_history, gr.Textbox.update(value=""), dropdown_reset, chat_history def reset_conversation(lang: str): localized = labels[lang] return ( [], gr.Textbox.update( value="", label=localized["text_label"], placeholder=localized["placeholder"] ), gr.Dropdown.update( choices=localized["predefined"], value=localized["new_question"], label=localized["select_label"], ), [], ) def sync_question_text(selected_option: str, lang: str): localized = labels[lang] if selected_option == localized["new_question"]: return gr.Textbox.update(value="") return gr.Textbox.update(value=selected_option) def toggle_language(lang: str): new_lang = "en" if lang == "he" else "he" localized = labels[new_lang] return ( new_lang, gr.Markdown.update(value=f"# {localized['title']}"), gr.Markdown.update(value=localized["intro"]), gr.Dropdown.update( choices=localized["predefined"], value=localized["new_question"], label=localized["select_label"], ), gr.Textbox.update( label=localized["text_label"], placeholder=localized["placeholder"], value="", ), gr.Button.update(value=localized["send"]), gr.Button.update(value=localized["reset"]), gr.Button.update(value=localized["toggle_to"]), ) CUSTOM_CSS = """ body { font-family: 'Rubik', 'Segoe UI', 'Helvetica Neue', Arial, sans-serif; } details > summary { cursor: pointer; } .gradio-container .prose p { margin-bottom: 0.5rem; } """ def build_demo() -> gr.Blocks: localized = labels[DEFAULT_LANG] with gr.Blocks(css=CUSTOM_CSS, title="Hebrew Math Tutor") as demo: lang_state = gr.State(DEFAULT_LANG) chat_state = gr.State([]) title_md = gr.Markdown(f"# {localized['title']}") intro_md = gr.Markdown(localized["intro"]) with gr.Row(): preset_dropdown = gr.Dropdown( label=localized["select_label"], choices=localized["predefined"], value=localized["new_question"], interactive=True, ) lang_button = gr.Button(localized["toggle_to"], variant="secondary") question_box = gr.Textbox( label=localized["text_label"], placeholder=localized["placeholder"], lines=5, ) with gr.Row(): reset_button = gr.Button(localized["reset"], variant="secondary") send_button = gr.Button(localized["send"], variant="primary") chatbot = gr.Chatbot( label=localized["chat_label"], height=520, bubble_full_width=False, render_markdown=True, ) preset_dropdown.change( fn=sync_question_text, inputs=[preset_dropdown, lang_state], outputs=question_box, ) reset_button.click( fn=reset_conversation, inputs=[lang_state], outputs=[chatbot, question_box, preset_dropdown, chat_state], ) send_button.click( fn=handle_user_message, inputs=[question_box, lang_state, chat_state], outputs=[chatbot, question_box, preset_dropdown, chat_state], ) question_box.submit( fn=handle_user_message, inputs=[question_box, lang_state, chat_state], outputs=[chatbot, question_box, preset_dropdown, chat_state], ) lang_button.click( fn=toggle_language, inputs=[lang_state], outputs=[ lang_state, title_md, intro_md, preset_dropdown, question_box, send_button, reset_button, lang_button, ], ) return demo demo = build_demo() if __name__ == "__main__": demo.queue().launch()