File size: 5,191 Bytes
59f470d
c42fa70
789edd4
59f470d
 
 
5c4a2a6
5298262
 
 
97ef8d5
 
 
 
5298262
5c4a2a6
5298262
 
 
97ef8d5
f530aed
 
 
 
 
 
 
5c4a2a6
 
5298262
d5d3ba6
848228d
 
d5d3ba6
f530aed
5298262
848228d
6a4fcaf
 
 
 
d5d3ba6
789edd4
 
 
 
 
 
 
 
 
 
5298262
c42fa70
6a4fcaf
789edd4
 
 
 
 
 
 
 
 
c42fa70
52d1dce
 
5298262
52d1dce
 
d5d3ba6
f530aed
 
 
5298262
6a4fcaf
d5d3ba6
 
6a4fcaf
 
 
c42fa70
 
59f470d
4f5e4fa
 
16452cd
c42fa70
848228d
6a4fcaf
 
4f5e4fa
 
5c4a2a6
6a4fcaf
f530aed
54d9979
 
f530aed
 
 
d5d3ba6
f530aed
789edd4
97ef8d5
 
6a4fcaf
4f5e4fa
 
 
 
 
 
 
6a4fcaf
16452cd
5c4a2a6
5298262
f530aed
a38aea5
16452cd
6a4fcaf
97ef8d5
6a4fcaf
 
 
16452cd
5c4a2a6
f530aed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
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)