Spaces:
Sleeping
Sleeping
| 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) | |