import os import re import traceback import numpy as np import gradio as gr from openai import OpenAI # ======== 1. HF Secret 讀取與安全檢查 ======== secret_key = os.environ.get("OPENAIAPIKEY") if not secret_key: raise ValueError( "⚠️ HF Space Secret 'OPENAIAPIKEY' 未設定或名稱錯誤!" "請先在 HF Space Secrets 裡建立此 Secret 並重新部署。" ) os.environ["OPENAI_API_KEY"] = secret_key # ======== 2. 初始化 OpenAI SDK ======== client = OpenAI() # ======== 3. 專業領域定義 ======== PROFESSIONS = { "程式設計": "你是一位資深程式設計師,回答必須專業、詳細,附上程式範例與步驟。", "行銷": "你是一位行銷專家,回答必須專業、詳細,提供可執行行銷策略與步驟。", "法律": "你是一位律師,回答必須專業、法律依據明確、可操作建議清楚。", "醫療": "你是一位醫師,回答必須專業、以健康與安全為前提,提供可操作建議。", "財務": "你是一位財務顧問,回答必須專業、符合台灣會計與稅務法規,提供可執行建議。", "設計": "你是一位設計師,回答必須專業、詳細,提供設計步驟與案例。" } # ======== 4. UTF-8 安全函數 ======== def safe_utf8(text): if not isinstance(text, str): text = str(text) return text.encode("utf-8", errors="ignore").decode("utf-8") # ======== 5. Embedding 初始化 ======== profession_embeddings = {} def ensure_embeddings(): global profession_embeddings if not profession_embeddings: for field in PROFESSIONS.keys(): try: emb = client.embeddings.create( model="text-embedding-3-small", input=safe_utf8(field) ) profession_embeddings[field] = np.array(emb.data[0].embedding) except Exception as e: print(f"Embedding 初始化失敗 ({field}):", e) profession_embeddings[field] = np.zeros(1536) # ======== 6. 自動判斷職業 ======== def detect_profession(user_input: str) -> str: ensure_embeddings() try: text_emb = client.embeddings.create( model="text-embedding-3-small", input=safe_utf8(user_input) ) text_vector = np.array(text_emb.data[0].embedding) except Exception as e: print("Embedding 呼叫錯誤:", traceback.format_exc()) return "你是一個專業顧問,回答必須專業、詳細、可操作。" scores = {} for field, emb in profession_embeddings.items(): if np.linalg.norm(text_vector) == 0 or np.linalg.norm(emb) == 0: scores[field] = -1 else: scores[field] = np.dot(text_vector, emb) / (np.linalg.norm(text_vector)*np.linalg.norm(emb)) best_field = max(scores, key=scores.get) return PROFESSIONS[best_field] # ======== 7. AI Agent 回答 ======== def professional_agent(user_input, state): user_input = safe_utf8(user_input) if state.get("profession_prompt") is None: profession_prompt = detect_profession(user_input) state["profession_prompt"] = profession_prompt question = re.sub(r"我是.*?(,|,)", "", user_input) if not question.strip(): answer = f"✅ 已設定你的專業領域:\n{profession_prompt}\n請提出問題。" state["chat_history"].append({"role":"user","content":user_input}) state["chat_history"].append({"role":"assistant","content":answer}) return state["chat_history"], state else: user_input = question messages = [{"role": "system", "content": state["profession_prompt"]}] for msg in state["chat_history"]: messages.append({"role": msg["role"], "content": msg["content"]}) messages.append({"role": "user", "content": user_input}) try: response = client.chat.completions.create( model="gpt-4o-mini", messages=messages, temperature=0.2 ) answer = safe_utf8(response.choices[0].message.content) except Exception as e: tb = traceback.format_exc() print(tb) # 後端完整 log answer = f"⚠️ 發生錯誤,請查看後端 logs: {str(e)}" # Append using dict 格式 state["chat_history"].append({"role":"user","content":user_input}) state["chat_history"].append({"role":"assistant","content":answer}) # 保留最近 10 條訊息 if len(state["chat_history"]) > 20: state["chat_history"] = state["chat_history"][-20:] return state["chat_history"], state # ======== 8. Gradio 介面 ======== with gr.Blocks() as demo: gr.Markdown("## 🧑‍💼AI 專業領域顧問") gr.Markdown("第一次輸入可以同時輸入職業 + 問題,例如:我是會計師,我想知道台灣稅務分析") chatbot = gr.Chatbot(type="messages") msg = gr.Textbox(label="輸入訊息") state = gr.State({"chat_history": [], "profession_prompt": None}) msg.submit(professional_agent, [msg, state], [chatbot, state]) demo.launch(server_name="0.0.0.0", server_port=7860)