CoolFace
Apppublic

chuyk/Math_Adaptation

sourceHugging Faceupdated 5mo agoView on Hugging Face
0likes
app.py455 linesDownload Raw Back to root
1import os2import gradio as gr3from groq import Groq4 5GROQ_API_KEY = os.environ.get("GROQ_API_KEY")6assert GROQ_API_KEY and GROQ_API_KEY.startswith("gsk_")7client = Groq(api_key=GROQ_API_KEY)8 9# --------- 全集中管理的PROMPT ---------10MATH_BASE_PROMPT = """11你是一位數學教師,請直接產生改編題目和簡短說明,**只產生一個全新題目,不要產生多於一題。不要顯示任何推理、思考過程、<think>內容,只輸出[改編題目]與[說明]。**12If you see this, DO NOT output <think>, reasoning or COT section.13假設輸入題目為一數學問題,請依據勾選規則進行下方改編(未被勾選的維度,必須與原題完全相同)。14"""15 16CONTEXT_PROMPT_GENERAL = """17- 必須更換完整題目中的生活情境/敘述描述,換成與原題不同的全新情境與描述,題目敘述結構也可改變,必須確保考點不變,不得更動原考題想測驗的觀念與知識。18"""19# --------- 修正後:注重敘事邏輯的創新情境 PROMPT ---------20CONTEXT_PROMPT_CREATIVE = """21- **任務:將數學題改編為具備「敘事感」與「當代風格」的段考試題。**22- **核心要求**:情境必須「合乎邏輯且好讀」,不要將不相干的詞彙硬湊在一起。23- **推薦命題方向(隨機選一,並深化其細節)**:24    1. **【現代生活痛點】**:如:外送平台抽成與利潤分配、社群媒體的觸及率演算、手搖飲配方的黃金比例、排隊買限量球鞋的心理博弈。25    2. **【專業職人領域】**:如:調香師的香氣前中後調配置、遊戲開發者的角色屬性平衡、咖啡師的萃取率與水溫關係、建築師的空間受力分配。26    3. **【奇幻/科幻冒險】**:如:RPG 遊戲中的技能點分配、星際航行中的燃料消耗比、煉金術的材料融合規律。27- **寫作指南**:28    - **單一主線**:整題只講一個故事,不要同時出現阿嬤、克蘇魯跟掃地機器人。29    - **專有名詞**:適度加入該領域的術語(如:觸及率、萃取率、HP/MP、毛利),增加真實感。30    - **優化數據**:務必調整數值使常數 k 為整數。31- **輸出限制**:敘述必須流暢,像是一篇短小的報導或故事背景說明。32"""33GUFENG_PROMPT = """34 35# 核心指令36請將原本的數學題改編為「符合[指定朝代]背景」的古風題目,並遵守以下規則:37 38# 風格調控系統391. **敘事風格**(AI自主選擇以下之一):40   - 🅰️ 白話古風:「用現代人易懂的古裝劇對白風格」  41     *特徵*:保留「在下/掌櫃」等稱謂,但避免艱澀文言  42   - 🅱️ 典雅書面:「模仿明清小說敘事,適度用典」  43     *特徵*:可出現「蓋聞」「然則」等連接詞  44 45# 防護機制461. **文言過濾器**:  47   - 禁止使用「之乎者也」等虛詞,除非直接引用典籍  48   - 複雜單位自動換算(如「3石6斗」→「約270公斤」)  49   - 每題含「白話註解」如「註:1文錢可買2個炊餅」50 51# 輸出結構52   -描述情境有敘事流暢度,包括故事化條件描述,及明確問題53 54"""55 56# ---------- 主組合prompt函數 ----------57def make_full_prompt(58    change_number, change_option, change_unit, change_code, 59    change_context, change_context_creative, change_context_gufeng, question60):61    instructions = []62 63    # 數字64    if change_number:65        instructions.append(66            "- 題目中的數字必須與原題**不同**(請更換為合理新數字,符合邏輯與原意)。"67        )68    else:69        instructions.append("- 題目中的數字必須與原題**相同**。")70 71    # 選項72    if change_option:73        instructions.append(74            "- 題目的選項內容(若有)必須與原題**不同**,且配合題目新數字或情境合理設計,須含正確答案。"75        )76    else:77        instructions.append("- 題目的選項必須與原題**相同**(包括數字與排列)。")78 79    # 單位80    if change_unit:81        instructions.append(82            "- 單位必須更換成合理的新單位(如公尺換為公分、平方公尺變平方公分等,並確保數值一致性)。"83        )84    else:85        instructions.append("- 單位必須與原題**相同**。")86 87    # 英文代碼88    if change_code:89        instructions.append(90            "- 變數、英文代碼(如a、b、x等)必須更換新的,不可以與原題相同。"91        )92    else:93        instructions.append("- 變數、英文代碼必須與原題**相同**。")94 95    # 情境/描述(三選一,但允許都不選)96    if change_context_gufeng:97        instructions.append(GUFENG_PROMPT)98    elif change_context_creative:99        instructions.append(CONTEXT_PROMPT_CREATIVE)100    elif change_context:101        instructions.append(CONTEXT_PROMPT_GENERAL)102    else:103        instructions.append("- 題目的情境、描述方式必須與原題**相同**。")104 105    # 是否需要新增四選一選項106    auto_add_choices_instruction = ""107    if change_option:108        if not any(x in question for x in ["選項", "是", "否", "A)", "A.", "B)", "B.", "(A)", "(B)", "(C)", "(D)", "①", "②", "③", "④"]):109            auto_add_choices_instruction = """110- 原題沒有選項,請為此題目新增**四個數值選項(四選一)**。111    1. 請先完整嚴謹地計算題目的正確答案(僅一次)。112    2. 設計三個誘導錯誤的答案(常見誤算近似值)。113    3. 將正確答案和三個誘答隨機排列成為四個選項,並明示正確答案。114    4. 正確答案必須直接包含於四個選項之一,且僅做一次答案與選項設計,不得檢查產生出的正確答案是否在選項中,也不得再次計算或產生更多誘答。所有步驟請嚴格一次完成,不得反覆修正。115    5. 禁止在選項產生後再檢查或重新計算,不得出現「答案不在四個選項」這類提示。116    6. 僅允許一次推導及選項製作,嚴格遵守請勿反覆修改。117"""118 119    prompt = MATH_BASE_PROMPT + "\n"120    prompt += "\n".join(instructions) + "\n"121    if auto_add_choices_instruction:122        prompt += auto_add_choices_instruction123    prompt += "備註:未被上述條件要求變動的部分,都必須「與原題完全相同」。\n"124 125    extra_output = change_number or change_option126 127    prompt += """128### 輸出要求129請只**產生一個**改編後的完整題目(不要產生多題!),並提供下列內容:1301. 改編後的完整題目叙述(包含圖形描述及選項,若原題有或根據上方指示需要新增)。1312. 簡短說明,解釋改編如何滿足上述條件(例如哪些部分更換,哪些保持,如何確保合理性)。132"""133    if extra_output:134        prompt += """1353. 請直接給出改編後題目的正確答案與詳細解題過程。136"""137    prompt += """138輸出格式:139**改編題目**  140[題目敘述,必須包含圖形說明和選項(若有或本題新增)]141 142**說明**  143[簡短說明改編如何符合條件]144"""145    if extra_output:146        prompt += """147**正確答案與解題過程**  148[答案與完整解題步驟]149"""150    151    # 嚴格禁止模型輸出 markdown 程式碼區塊外衣,確保渲染正常152    prompt += "\n⚠️ 注意:請直接輸出排版好的文本,**絕對不要**使用 ```markdown 與 ``` 將整個回答包裝成程式碼區塊!"153    154    return prompt155 156# 將 Chatbot 的內容轉換成 Markdown 文字格式157def export_to_markdown(history):158    if not history:159        return ""160    try:161        last_msg = history[-1]162        if isinstance(last_msg, dict):163            return last_msg.get('content', '')164        else:165            return getattr(last_msg, 'content', str(last_msg))166    except Exception:167        return ""168 169MODEL_OPTIONS = [170    ("GPT-OSS 120B", "openai/gpt-oss-120b"),171    ("Llama 3.3 70B Versatile", "llama-3.3-70b-versatile"),172    ("Llama 4 Scout", "meta-llama/llama-4-scout-17b-16e-instruct"),173    ("Compound Beta", "groq/compound"),174    ("Llama 3.1 8B Instant", "llama-3.1-8b-instant"),175    ("Qwen3 32B", "qwen/qwen3-32b"),176]177 178# 通用的文本清洗函數,確保不被程式碼區塊吃掉渲染179def clean_output(reply):180    text = reply.strip()181    182    # 剝除頭部的 ```markdown183    if text.startswith("```markdown"):184        text = text.replace("```markdown\n", "", 1).replace("```markdown", "", 1)185    # 剝除可能出現的 ``` 包裝186    if text.startswith("```"):187        text = text.replace("```\n", "", 1).replace("```", "", 1)188        189    if "**改編題目**" in text:190        idx = text.find("**改編題目**")191        text = text[idx:]192        193    # 如果結尾還有殘留的 ```,也予以剝除194    if text.endswith("```"):195        text = text[:-3].strip()196        197    return text198 199def chat_app_stream(200    history, question, model, qwen_streaming,201    change_number, change_option, change_unit, change_code,202    change_context, change_context_creative, change_context_gufeng203):204 205    # -------- 防呆區段開始 ---------206    if not (change_number or change_option or change_unit or change_code or change_context or change_context_creative or change_context_gufeng):207        user_msg = {"role": "user", "content": question}208        assistant_msg = {"role": "assistant", "content": "⚠️ 請至少勾選一個改編選項!"}209        yield (history + [user_msg, assistant_msg], question, model, qwen_streaming,210               change_number, change_option, change_unit, change_code, change_context, change_context_creative, change_context_gufeng)211        return212 213    num_context_style = sum([change_context, change_context_creative, change_context_gufeng])214    if num_context_style > 1:215        user_msg = {"role": "user", "content": question}216        assistant_msg = {"role": "assistant", "content": "⚠️『改情境描述(一般)』『改情境描述(創新)』『改情境描述(古風)』三者只能擇一勾選,請修正!"}217        yield (history + [user_msg, assistant_msg], question, model, qwen_streaming,218               change_number, change_option, change_unit, change_code, change_context, change_context_creative, change_context_gufeng)219        return220 221    if not question or len(question.strip()) < 5:222        user_msg = {"role": "user", "content": question}223        assistant_msg = {"role": "assistant", "content": "⚠️ 請提供一個完整的數學題目描述!"}224        yield (history + [user_msg, assistant_msg], question, model, qwen_streaming,225               change_number, change_option, change_unit, change_code, change_context, change_context_creative, change_context_gufeng)226        return227    # ----------- 防呆區段結束 ----------228 229    if not history:230        history = []231    system_prompt = make_full_prompt(232        change_number, change_option, change_unit, change_code, 233        change_context, change_context_creative, change_context_gufeng, question234    )235    question_new = question + "\n(請直接輸出改編題目與說明,嚴禁顯示推理、<think>內容。)"236    messages = [{"role": "system", "content": system_prompt},237                {"role": "user", "content": question_new}]238 239    try:240        # 將串流與非串流統一使用 clean_output 洗淨241        if model in ["qwen/qwen3-32b"] and qwen_streaming:242            stream = client.chat.completions.create(243                model=model,244                messages=messages,245                temperature=0.6,246                max_completion_tokens=4096,247                top_p=0.95,248                stream=True,249                stop=None250            )251            reply = ""252            user_msg = {"role": "user", "content": question}253            assistant_msg = {"role": "assistant", "content": ""}254            cur_history = history + [user_msg, assistant_msg]255            for chunk in stream:256                delta = getattr(chunk.choices[0].delta, "content", "") or ""257                reply += delta258                cleaned = clean_output(reply)259                assistant_msg['content'] = cleaned260                yield ([dict(m) for m in cur_history],261                       question, model, qwen_streaming,262                       change_number, change_option, change_unit, change_code, change_context, change_context_creative, change_context_gufeng)263        elif model in ["qwen/qwen3-32b"]:264            resp = client.chat.completions.create(265                model=model,266                messages=messages,267                temperature=0.6,268                max_completion_tokens=4096,269                top_p=0.95,270                stream=False,271                stop=None272            )273            answer = getattr(resp.choices[0].message, "content", None)274            if not answer:275                answer = "[API 無內容]"276            else:277                answer = clean_output(answer)278            user_msg = {"role": "user", "content": question}279            assistant_msg = {"role": "assistant", "content": answer}280            yield (history + [user_msg, assistant_msg],281                   question, model, qwen_streaming,282                   change_number, change_option, change_unit, change_code, change_context, change_context_creative, change_context_gufeng)283        else:284            stream = client.chat.completions.create(285                model=model,286                messages=messages,287                temperature=1,288                max_tokens=4096,289                top_p=1,290                stream=True,291            )292            reply = ""293            user_msg = {"role": "user", "content": question}294            assistant_msg = {"role": "assistant", "content": ""}295            cur_history = history + [user_msg, assistant_msg]296            for chunk in stream:297                delta = getattr(chunk.choices[0].delta, "content", "") or ""298                reply += delta299                cleaned = clean_output(reply)300                assistant_msg['content'] = cleaned301                yield ([dict(m) for m in cur_history],302                       question, model, qwen_streaming,303                       change_number, change_option, change_unit, change_code, change_context, change_context_creative, change_context_gufeng)304    except Exception as e:305        user_msg = {"role": "user", "content": question}306        assistant_msg = {"role": "assistant", "content": f"[API錯誤] {e}"}307        yield (history + [user_msg, assistant_msg],308               question, model, qwen_streaming,309               change_number, change_option, change_unit, change_code, change_context, change_context_creative, change_context_gufeng)310 311 312CORRECT_CODE = "kai"313 314with gr.Blocks() as demo:315    gr.Markdown("## 🚀 數學改題助手")316    317    # 增加 latex_delimiters 參數,確保數學符號能夠完美渲染!318    chatbot = gr.Chatbot(319        label="對話框", 320        type="messages",321        render_markdown=True,322        latex_delimiters=[323            {"left": "$$", "right": "$$", "display": True},324            {"left": "$", "right": "$", "display": False},325            {"left": "\\(", "right": "\\)", "display": False},326            {"left": "\\[", "right": "\\]", "display": True}327        ]328    )329    330    markdown_output = gr.Textbox(label="產出題目的 Markdown 原始碼 (可直接複製)", lines=8, show_copy_button=True)331    332    question = gr.Textbox(label="請輸入數學題目", lines=3)333 334    activation_code_box = gr.Textbox(label="啟動碼", type="password", value="")335    code_status = gr.Markdown("⚠️ 請輸入啟動碼。先驗證啟動碼才能送出或清除!")336 337    with gr.Row():338        change_number = gr.Checkbox(label="改數字", value=False)339        change_option = gr.Checkbox(label="改選項", value=False)340        change_unit = gr.Checkbox(label="改單位", value=False)341        change_code = gr.Checkbox(label="改英文代碼", value=False)342        change_context = gr.Checkbox(label="改情境描述(一般)", value=False)343        change_context_creative = gr.Checkbox(label="改情境描述(創新)", value=False)344        change_context_gufeng = gr.Checkbox(label="改情境描述(古風)", value=False)345 346    send_btn = gr.Button("送出", interactive=False)347    clear_btn = gr.Button("清除", interactive=False)348    qwen_streaming = gr.Checkbox(label="Qwen 使用 stream", value=True)349    model_selection = gr.Dropdown(350        label="模型",351        choices=[(name, ident) for name, ident in MODEL_OPTIONS],352        value="qwen/qwen3-32b"353    )354    state = gr.State([])355 356    CORRECT_CODE = "kai"357    def activation_code_feedback_and_toggle_btn(activation_code):358        code = activation_code.strip()359        if code == "":360            return (361                "⚠️ 請輸入啟動碼",362                gr.update(interactive=False),363                gr.update(interactive=False),364            )365        elif code == CORRECT_CODE:366            return (367                "✅ 啟動碼正確,可以送出或清除。",368                gr.update(interactive=True),369                gr.update(interactive=True),370            )371        else:372            return (373                "❌ 啟動碼錯誤,請重新輸入",374                gr.update(interactive=False),375                gr.update(interactive=False),376            )377 378    activation_code_box.change(379        activation_code_feedback_and_toggle_btn,380        inputs=activation_code_box,381        outputs=[code_status, send_btn, clear_btn]382    )383 384    def safe_chat_app_stream(385        history, question, model, qwen_streaming,386        change_number, change_option, change_unit, change_code,387        change_context, change_context_creative, change_context_gufeng,388        activation_code389    ):390        code = activation_code.strip()391        if code == "" or code != CORRECT_CODE:392            user_msg = {"role": "user", "content": question}393            assistant_msg = {"role": "assistant", "content": "⚠️ 請先輸入正確啟動碼!"}394            return (395                [user_msg, assistant_msg],396                question, model, qwen_streaming,397                change_number, change_option, change_unit, change_code,398                change_context, change_context_creative, change_context_gufeng,399                "⚠️ 請輸入正確啟動碼"400            )401        for result in chat_app_stream(history, question, model, qwen_streaming,402                      change_number, change_option, change_unit, change_code,403                      change_context, change_context_creative, change_context_gufeng):404            yield (*result, "")405 406    def safe_clear_input(question, chatbot, activation_code, model, qwen_streaming,407                         change_number, change_option, change_unit, change_code,408                         change_context, change_context_creative, change_context_gufeng):409        code = activation_code.strip()410        if code == "" or code != CORRECT_CODE:411            return (412                question, chatbot, model, qwen_streaming,413                change_number, change_option, change_unit, change_code,414                change_context, change_context_creative, change_context_gufeng,415                "⚠️ 請輸入正確啟動碼"416            )417        return (418            "", [], model, qwen_streaming,419            change_number, change_option, change_unit, change_code,420            change_context, change_context_creative, change_context_gufeng,421            ""422        )423 424    send_btn.click(425        safe_chat_app_stream,426        [state, question, model_selection, qwen_streaming,427         change_number, change_option, change_unit, change_code,428         change_context, change_context_creative, change_context_gufeng,429         activation_code_box],430        [chatbot, question, model_selection, qwen_streaming,431         change_number, change_option, change_unit, change_code,432         change_context, change_context_creative, change_context_gufeng,433         code_status]434    )435    436    clear_btn.click(437        safe_clear_input,438        [question, chatbot, activation_code_box, model_selection, qwen_streaming,439         change_number, change_option, change_unit, change_code,440         change_context, change_context_creative, change_context_gufeng],441        [question, chatbot, model_selection, qwen_streaming,442         change_number, change_option, change_unit, change_code,443         change_context, change_context_creative, change_context_gufeng,444         code_status]445    )446    447    # 綁定 Chatbot 內容變更事件448    chatbot.change(449        fn=export_to_markdown,450        inputs=[chatbot],451        outputs=[markdown_output]452    )453 454if __name__ == "__main__":455    demo.launch()