mickeywu520 commited on
Commit
cd9bca9
·
1 Parent(s): df0c8ab

first commit

Browse files
.env.example ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # LINE Bot 設定
2
+ LINE_CHANNEL_ACCESS_TOKEN=your_line_channel_access_token_here
3
+ LINE_CHANNEL_SECRET=your_line_channel_secret_here
4
+
5
+ # PostgreSQL 資料庫設定
6
+ DB_HOST=your_database_host_here
7
+ DB_PORT=6543
8
+ DB_NAME=your_database_name_here
9
+ DB_USER=your_database_user_here
10
+ DB_PASSWORD=your_database_password_here
11
+
12
+ # OpenRouter 設定 (可選,用於進階 NLP)
13
+ OPENROUTER_API_KEY=your_openrouter_api_key_here
14
+ OPENROUTER_MODEL=anthropic/claude-3-haiku
15
+
16
+ # 其他設定
17
+ DEBUG=False
18
+ LOG_LEVEL=INFO
API_KEYS_GUIDE.md ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🔑 API Keys 取得指南
2
+
3
+ 本文件詳細說明如何取得所有必要的 API Keys 來運行 LINE Bot 系統。
4
+
5
+ ## 📋 必要的 API Keys
6
+
7
+ ### 1. LINE Bot API Keys
8
+
9
+ #### 步驟 1: 建立 LINE Developers 帳號
10
+ 1. 前往 [LINE Developers Console](https://developers.line.biz/)
11
+ 2. 使用 LINE 帳號登入
12
+ 3. 同意開發者條款
13
+
14
+ #### 步驟 2: 建立 Provider
15
+ 1. 點擊 "Create a new provider"
16
+ 2. 輸入 Provider 名稱 (例如: "我的公司")
17
+ 3. 點擊 "Create"
18
+
19
+ #### 步驟 3: 建立 Messaging API Channel
20
+ 1. 在 Provider 頁面點擊 "Create a Messaging API channel"
21
+ 2. 填寫以下資訊:
22
+ - **Channel name**: 您的 Bot 名稱
23
+ - **Channel description**: Bot 描述
24
+ - **Category**: 選擇適合的類別
25
+ - **Subcategory**: 選擇子類別
26
+ 3. 上傳 Channel icon (可選)
27
+ 4. 同意條款並點擊 "Create"
28
+
29
+ #### 步驟 4: 取得 API Keys
30
+ 1. 進入剛建立的 Channel
31
+ 2. 前往 "Basic settings" 頁籤:
32
+ - 複製 **Channel secret** → 這是您的 `LINE_CHANNEL_SECRET`
33
+ 3. 前往 "Messaging API" 頁籤:
34
+ - 點擊 "Issue" 按鈕產生 Channel access token
35
+ - 複製 **Channel access token** → 這是您的 `LINE_CHANNEL_ACCESS_TOKEN`
36
+
37
+ #### 步驟 5: 設定 Webhook (部署後執行)
38
+ 1. 在 "Messaging API" 頁籤中
39
+ 2. 設定 **Webhook URL**: `https://你的用戶名-你的空間名稱.hf.space/webhook`
40
+ 3. 啟用 "Use webhook"
41
+ 4. 關閉 "Auto-reply messages" (可選)
42
+
43
+ ### 2. Supabase API Keys
44
+
45
+ #### 步驟 1: 建立 Supabase 帳號
46
+ 1. 前往 [Supabase](https://supabase.com/)
47
+ 2. 點擊 "Start your project"
48
+ 3. 使用 GitHub 或 Google 帳號註冊
49
+
50
+ #### 步驟 2: 建立新專案
51
+ 1. 點擊 "New project"
52
+ 2. 選擇組織 (或建立新組織)
53
+ 3. 填寫專案資訊:
54
+ - **Name**: 專案名稱
55
+ - **Database Password**: 設定強密碼
56
+ - **Region**: 選擇最近的區域 (建議: Southeast Asia)
57
+ 4. 點擊 "Create new project"
58
+ 5. 等待專案建立完成 (約 2-3 分鐘)
59
+
60
+ #### 步驟 3: 取得 API Keys
61
+ 1. 在專案 Dashboard 中
62
+ 2. 前往左側選單的 "Settings" → "API"
63
+ 3. 複製以下資訊:
64
+ - **URL** → 這是您的 `SUPABASE_URL`
65
+ - **anon public** key → 這是您的 `SUPABASE_KEY`
66
+
67
+ #### 步驟 4: 建立資料表
68
+ 1. 前往左側選單的 "SQL Editor"
69
+ 2. 複製 `setup_guide.md` 中的 SQL 語句
70
+ 3. 點擊 "RUN" 執行
71
+
72
+ ## 🚀 可選的 API Keys
73
+
74
+ ### 3. OpenRouter API Keys (進階 NLP 功能)
75
+
76
+ #### 步驟 1: 建立 OpenRouter 帳號
77
+ 1. 前往 [OpenRouter](https://openrouter.ai/)
78
+ 2. 點擊 "Sign Up"
79
+ 3. 使用 Google 或 GitHub 帳號註冊
80
+
81
+ #### 步驟 2: 充值帳戶
82
+ 1. 前往 [Credits 頁面](https://openrouter.ai/credits)
83
+ 2. 點擊 "Add Credits"
84
+ 3. 選擇充值金額 (建議先充值 $5-10 測試)
85
+ 4. 完成付款
86
+
87
+ #### 步驟 3: 建立 API Key
88
+ 1. 前往 [API Keys 頁面](https://openrouter.ai/keys)
89
+ 2. 點擊 "Create Key"
90
+ 3. 輸入 Key 名稱 (例如: "LINE Bot")
91
+ 4. 複製產生的 API Key → 這是您的 `OPENROUTER_API_KEY`
92
+
93
+ #### 步驟 4: 選擇模型
94
+ 根據您的需求和預算選擇模型:
95
+
96
+ | 模型 | 成本 | 適用場景 | 設定值 |
97
+ |------|------|----------|--------|
98
+ | Claude 3 Haiku | 最低 | 日常查詢 | `anthropic/claude-3-haiku` |
99
+ | GPT-3.5 Turbo | 中等 | 平衡使用 | `openai/gpt-3.5-turbo` |
100
+ | Claude 3 Sonnet | 較高 | 複雜查詢 | `anthropic/claude-3-sonnet` |
101
+
102
+ ## 🔧 環境變數設定
103
+
104
+ ### Hugging Face Spaces 設定
105
+
106
+ 1. 前往您的 Hugging Face Spaces 專案
107
+ 2. 點擊 "Settings" 頁籤
108
+ 3. 在 "Repository secrets" 區域新增以下變數:
109
+
110
+ ```bash
111
+ # 必要設定
112
+ LINE_CHANNEL_ACCESS_TOKEN=你的_LINE_Channel_Access_Token
113
+ LINE_CHANNEL_SECRET=你的_LINE_Channel_Secret
114
+ SUPABASE_URL=你的_Supabase_專案_URL
115
+ SUPABASE_KEY=你的_Supabase_Anon_Key
116
+
117
+ # 可選設定 (進階 NLP)
118
+ OPENROUTER_API_KEY=你的_OpenRouter_API_Key
119
+ OPENROUTER_MODEL=anthropic/claude-3-haiku
120
+
121
+ # 其他設定
122
+ DEBUG=False
123
+ LOG_LEVEL=INFO
124
+ ```
125
+
126
+ ### 本地開發設定
127
+
128
+ 建立 `.env` 檔案:
129
+
130
+ ```bash
131
+ cp .env.example .env
132
+ ```
133
+
134
+ 編輯 `.env` 檔案並填入您的 API Keys。
135
+
136
+ ## ✅ 驗證設定
137
+
138
+ ### 1. 測試 LINE Bot
139
+ ```bash
140
+ # 檢查 LINE Channel 設定
141
+ curl -H "Authorization: Bearer YOUR_CHANNEL_ACCESS_TOKEN" \
142
+ https://api.line.me/v2/bot/info
143
+ ```
144
+
145
+ ### 2. 測試 Supabase
146
+ ```bash
147
+ # 檢查 Supabase 連線
148
+ curl "YOUR_SUPABASE_URL/rest/v1/" \
149
+ -H "apikey: YOUR_SUPABASE_KEY"
150
+ ```
151
+
152
+ ### 3. 測試 OpenRouter
153
+ ```bash
154
+ # 檢查 OpenRouter API
155
+ curl https://openrouter.ai/api/v1/models \
156
+ -H "Authorization: Bearer YOUR_OPENROUTER_KEY"
157
+ ```
158
+
159
+ ## 🔒 安全性注意事項
160
+
161
+ 1. **絕不在程式碼中硬編碼 API Keys**
162
+ 2. **使用環境變數儲存敏感資訊**
163
+ 3. **定期輪換 API Keys**
164
+ 4. **監控 API 使用量和成本**
165
+ 5. **設定適當的權限和限制**
166
+
167
+ ## 💰 成本估算
168
+
169
+ ### LINE Bot
170
+ - **免費額度**: 每月 1,000 則訊息
171
+ - **付費方案**: 超過免費額度後按量計費
172
+
173
+ ### Supabase
174
+ - **免費方案**:
175
+ - 500MB 資料庫
176
+ - 50MB 檔案儲存
177
+ - 2GB 頻寬
178
+ - **Pro 方案**: $25/月起
179
+
180
+ ### OpenRouter
181
+ - **按使用量計費**
182
+ - **Claude 3 Haiku**: ~$0.25/1M input tokens
183
+ - **建議預算**: $5-20/月 (中小型應用)
184
+
185
+ ## 🆘 常見問題
186
+
187
+ ### Q: LINE Bot 無法回應訊息
188
+ A: 檢查以下項目:
189
+ 1. Channel Access Token 是否正確
190
+ 2. Webhook URL 是否設定正確
191
+ 3. 應用程式是否正常運行
192
+
193
+ ### Q: Supabase 連線失敗
194
+ A: 檢查以下項目:
195
+ 1. URL 和 API Key 是否正確
196
+ 2. 資料表是否已建立
197
+ 3. 網路連線是否正常
198
+
199
+ ### Q: OpenRouter 回應錯誤
200
+ A: 檢查以下項目:
201
+ 1. API Key 是否有效
202
+ 2. 帳戶餘額是否充足
203
+ 3. 模型名稱是否正確
204
+
205
+ ## 📞 技術支援
206
+
207
+ 如果您在設定過程中遇到問題:
208
+
209
+ 1. **LINE Bot**: [LINE Developers 文件](https://developers.line.biz/en/docs/)
210
+ 2. **Supabase**: [Supabase 文件](https://supabase.com/docs)
211
+ 3. **OpenRouter**: [OpenRouter 文件](https://openrouter.ai/docs)
212
+
213
+ ---
214
+
215
+ **提醒**: 請妥善保管您的 API Keys,避免洩露給他人使用。
BUSINESS_QUERY_GUIDE.md ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 業務查詢系統使用指南
2
+
3
+ ## 概述
4
+
5
+ 本系統為您的 LINE 官方帳號提供了智能業務查詢功能,用戶可以透過自然語言詢問商品、庫存、訂單等業務資訊。系統整合了 Pydantic AI 進行語意分析,並提供友善的中文回應。
6
+
7
+ ## 系統架構
8
+
9
+ ```
10
+ LINE 用戶訊息 → LINE Bot Service → Business Query Service → NLP Service + Database Service → 回應用戶
11
+ ```
12
+
13
+ ### 核心組件
14
+
15
+ 1. **NLP Service** (`backend/services/nlp_service.py`)
16
+ - 自然語言處理和意圖識別
17
+ - 實體提取和信心度計算
18
+ - 支援業務相關的查詢模式
19
+
20
+ 2. **Database Service** (`backend/services/database_service.py`)
21
+ - 資料庫查詢操作
22
+ - 業務專用查詢方法
23
+ - 支援商品、庫存、訂單等查詢
24
+
25
+ 3. **Business Query Service** (`backend/services/business_query_service.py`)
26
+ - 整合 NLP 和資料庫服務
27
+ - 統一的查詢處理入口
28
+ - 格式化回應訊息
29
+
30
+ 4. **LINE Bot Service** (`backend/services/line_bot_service.py`)
31
+ - 處理 LINE 平台的訊息
32
+ - 用戶管理和對話流程
33
+ - 快速回覆按鈕生成
34
+
35
+ ## 支援的查詢類型
36
+
37
+ ### 1. 商品查詢
38
+ **用戶可以這樣問:**
39
+ - "查詢商品 iPhone"
40
+ - "有什麼筆記型電腦"
41
+ - "商品價格查詢"
42
+ - "找找看有沒有滑鼠"
43
+
44
+ **系統回應範例:**
45
+ ```
46
+ 找到商品:
47
+ 名稱:iPhone 14 Pro
48
+ 描述:最新款智慧型手機
49
+ 價格:$35,900
50
+ 類別:手機
51
+ ```
52
+
53
+ ### 2. 庫存查詢
54
+ **用戶可以這樣問:**
55
+ - "庫存查詢 iPhone"
56
+ - "筆記型電腦還有多少"
57
+ - "查詢存貨狀況"
58
+ - "iPhone 的庫存"
59
+
60
+ **系統回應範例:**
61
+ ```
62
+ 庫存資訊:
63
+ 商品:iPhone 14 Pro
64
+ 目前庫存:25 件
65
+ 類別:手機
66
+ 價格:$35,900
67
+ ```
68
+
69
+ ### 3. 訂單查詢
70
+ **用戶可以這樣問:**
71
+ - "我的訂單"
72
+ - "查詢訂單狀態"
73
+ - "訂單編號 ORD001"
74
+ - "購買記錄"
75
+
76
+ **系統回應範例:**
77
+ ```
78
+ 找到 3 筆訂單:
79
+ 1. ORD001 - 已出貨 - $15,000
80
+ 2. ORD002 - 處理中 - $8,500
81
+ 3. ORD003 - 已完成 - $12,300
82
+ ```
83
+
84
+ ### 4. 低庫存警告
85
+ **用戶可以這樣問:**
86
+ - "低庫存商品"
87
+ - "缺貨商品查詢"
88
+ - "庫存不足的商品"
89
+
90
+ **系統回應範例:**
91
+ ```
92
+ ⚠️ 發現 3 個低庫存商品:
93
+ 1. iPhone 13 - 剩餘:5 件
94
+ 2. MacBook Air - 剩餘:2 件
95
+ 3. AirPods Pro - 剩餘:8 件
96
+ ```
97
+
98
+ ### 5. 業務統計
99
+ **用戶可以這樣問:**
100
+ - "業務摘要"
101
+ - "統計報表"
102
+ - "總計資料"
103
+ - "業務狀況"
104
+
105
+ **系統回應範例:**
106
+ ```
107
+ 📊 業務摘要:
108
+ 商品總數:156 個
109
+ 訂單總數:89 筆
110
+ 用戶總數:45 人
111
+ 低庫存商品:3 個
112
+ 統計時間:2024-01-15T10:30:00
113
+ ```
114
+
115
+ ## 特殊指令
116
+
117
+ ### 幫助指令
118
+ - `help`, `幫助`, `說明`, `指令`
119
+
120
+ ### 選單指令
121
+ - `menu`, `選單`, `功能`
122
+
123
+ ## 快速開始
124
+
125
+ ### 1. 測試業務查詢功能
126
+ ```bash
127
+ python tmp_rovodev_test_business_query.py
128
+ ```
129
+
130
+ ### 2. 測試 LINE Bot 整合
131
+ ```bash
132
+ python tmp_rovodev_test_line_integration.py
133
+ ```
134
+
135
+ ### 3. 在您的應用中使用
136
+
137
+ ```python
138
+ from backend.services.line_bot_service import LineBotService
139
+
140
+ # 初始化服務
141
+ line_service = LineBotService()
142
+
143
+ # 處理用戶訊息
144
+ response = line_service.handle_text_message(
145
+ user_id="U1234567890",
146
+ message_text="查詢商品 iPhone",
147
+ display_name="張小明"
148
+ )
149
+
150
+ print(response['text']) # 系統回應
151
+ ```
152
+
153
+ ## 自訂擴展
154
+
155
+ ### 1. 新增查詢類型
156
+
157
+ 在 `nlp_service.py` 中新增意圖模式:
158
+
159
+ ```python
160
+ self.business_intent_patterns = {
161
+ # 現有模式...
162
+ "new_query_type": [
163
+ r"新查詢.*模式",
164
+ r"特殊.*查詢"
165
+ ]
166
+ }
167
+ ```
168
+
169
+ 在 `database_service.py` 中新增對應方法:
170
+
171
+ ```python
172
+ def new_query_method(self, param1: str = None) -> DatabaseResult:
173
+ """新的查詢方法"""
174
+ # 實作查詢邏輯
175
+ pass
176
+ ```
177
+
178
+ ### 2. 自訂回應格式
179
+
180
+ 在 `nlp_service.py` 中修改格式化方法:
181
+
182
+ ```python
183
+ def _format_custom_response(self, data: List[Dict[str, Any]]) -> str:
184
+ """自訂回應格式"""
185
+ # 實作自訂格式
186
+ pass
187
+ ```
188
+
189
+ ### 3. 整合其他資料源
190
+
191
+ 您可以修改 `database_service.py` 來連接其他資料庫或 API:
192
+
193
+ ```python
194
+ def search_external_data(self, query: str) -> DatabaseResult:
195
+ """查詢外部資料源"""
196
+ # 連接外部 API 或資料庫
197
+ pass
198
+ ```
199
+
200
+ ## 注意事項
201
+
202
+ 1. **資料庫結構**: 目前的實作基於現有的 `Product`, `Order`, `User` 模型。如果您的資料庫結構不同,請相應調整查詢方法。
203
+
204
+ 2. **權限控制**: 系統目前允許所有查詢。在生產環境中,建議實作適當的權限控制。
205
+
206
+ 3. **效能考量**: 對於大量資料,建議加入分頁和快取機制。
207
+
208
+ 4. **錯誤處理**: 系統已包含基本的錯誤處理,但建議根據實際需求進一步完善。
209
+
210
+ ## 參考資料
211
+
212
+ - `another_proj_schemas.py`: 包含完整的業務模型定義
213
+ - `backend/database/models.py`: 資料庫模型定義
214
+ - `backend/models/schemas.py`: API 回應模型定義
215
+
216
+ ## 技術支援
217
+
218
+ 如需��術支援或功能擴展,請參考:
219
+ 1. 測試腳本中的使用範例
220
+ 2. 各服務類別的文檔字串
221
+ 3. 錯誤日誌輸出
222
+
223
+ ---
224
+
225
+ **注意**: 這是一個基礎實作,您可以根據實際業務需求進行擴展和優化。
Dockerfile ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.12-slim
2
+
3
+ RUN useradd -m -u 1000 user
4
+ USER user
5
+ ENV PATH="/home/user/.local/bin:$PATH"
6
+
7
+ WORKDIR /app
8
+
9
+ COPY --chown=user ./requirements.txt requirements.txt
10
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
11
+
12
+ COPY --chown=user . /app
13
+
14
+ CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "7860"]
OPENROUTER_INTEGRATION.md ADDED
@@ -0,0 +1,273 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🚀 OpenRouter 整合文件
2
+
3
+ ## 📋 概述
4
+
5
+ 本專案已整合 OpenRouter API 來提供進階的自然語言處理功能,包括:
6
+ - 更精確的意圖識別
7
+ - 智能實體提取
8
+ - 自然的回應生成
9
+
10
+ ## 🔧 設定方式
11
+
12
+ ### 1. 取得 OpenRouter API Key
13
+
14
+ 1. 前往 [OpenRouter 官網](https://openrouter.ai/)
15
+ 2. 註冊帳號並登入
16
+ 3. 前往 [API Keys 頁面](https://openrouter.ai/keys)
17
+ 4. 建立新的 API Key
18
+ 5. 複製 API Key 備用
19
+
20
+ ### 2. 環境變數設定
21
+
22
+ 在 Hugging Face Spaces 的 Settings 中新增以下環境變數:
23
+
24
+ ```bash
25
+ # 必要設定
26
+ OPENROUTER_API_KEY=your_openrouter_api_key_here
27
+
28
+ # 可選設定 (預設值已設定)
29
+ OPENROUTER_MODEL=anthropic/claude-3-haiku
30
+ ```
31
+
32
+ ### 3. 支援的模型
33
+
34
+ OpenRouter 支援多種 AI 模型,您可以根據需求選擇:
35
+
36
+ #### 推薦模型 (成本效益佳)
37
+ - `anthropic/claude-3-haiku` (預設) - 快速、便宜
38
+ - `openai/gpt-3.5-turbo` - 平衡性能與成本
39
+ - `meta-llama/llama-2-70b-chat` - 開源選項
40
+
41
+ #### 高性能模型
42
+ - `anthropic/claude-3-sonnet` - 更好的理解能力
43
+ - `openai/gpt-4` - 最佳性能
44
+ - `anthropic/claude-3-opus` - 最高品質
45
+
46
+ #### 設定範例
47
+ ```bash
48
+ # 使用 GPT-3.5 Turbo
49
+ OPENROUTER_MODEL=openai/gpt-3.5-turbo
50
+
51
+ # 使用 Claude 3 Sonnet
52
+ OPENROUTER_MODEL=anthropic/claude-3-sonnet
53
+ ```
54
+
55
+ ## 🎯 功能特色
56
+
57
+ ### 1. 進階意圖識別
58
+
59
+ OpenRouter 整合後,系統能更準確地識別用戶意圖:
60
+
61
+ ```python
62
+ # 範例輸入
63
+ "幫我找一下價格在一千到五千之間的手機"
64
+
65
+ # 基礎 NLP (規則引擎)
66
+ {
67
+ "intent": "search_product",
68
+ "confidence": 0.6,
69
+ "entities": {"min_price": 1000, "max_price": 5000}
70
+ }
71
+
72
+ # 進階 NLP (OpenRouter)
73
+ {
74
+ "intent": "search_product",
75
+ "confidence": 0.9,
76
+ "entities": {
77
+ "min_price": 1000,
78
+ "max_price": 5000,
79
+ "category": "手機",
80
+ "product_type": "電子產品"
81
+ }
82
+ }
83
+ ```
84
+
85
+ ### 2. 智能回應生成
86
+
87
+ 系統會根據查詢結果生成更自然的回應:
88
+
89
+ ```python
90
+ # 基礎回應
91
+ "找到 3 筆商品資料。"
92
+
93
+ # 進階回應 (OpenRouter)
94
+ "我為您找到了 3 款符合條件的手機:
95
+ 1. iPhone 15 Pro - NT$ 35,900
96
+ 2. Samsung Galaxy S24 - NT$ 28,900
97
+ 3. Google Pixel 8 - NT$ 24,900
98
+
99
+ 這些都在您的預算範圍內,您想了解哪一款的詳細資訊呢?"
100
+ ```
101
+
102
+ ## 🔄 降級機制
103
+
104
+ 系統具有完整的降級機制:
105
+
106
+ 1. **優先使用 OpenRouter**: 如果 API Key 可用且正常運作
107
+ 2. **自動降級**: 如果 OpenRouter 失敗,自動使用基礎規則引擎
108
+ 3. **錯誤處理**: 完整的錯誤日誌和異常處理
109
+
110
+ ```python
111
+ # 在程式碼中的實作
112
+ def analyze_message(self, message: str, use_advanced: bool = True):
113
+ if use_advanced and self.openrouter_service.api_key:
114
+ try:
115
+ # 嘗試使用 OpenRouter
116
+ return advanced_analysis
117
+ except Exception as e:
118
+ logger.warning(f"進階 NLP 分析失敗,使用基礎分析: {str(e)}")
119
+
120
+ # 降級到基礎規則引擎
121
+ return basic_analysis
122
+ ```
123
+
124
+ ## 💰 成本考量
125
+
126
+ ### 模型成本比較 (每 1M tokens)
127
+
128
+ | 模型 | 輸入成本 | 輸出成本 | 適用場景 |
129
+ |------|----------|----------|----------|
130
+ | claude-3-haiku | $0.25 | $1.25 | 日常查詢 |
131
+ | gpt-3.5-turbo | $0.50 | $1.50 | 平衡使用 |
132
+ | claude-3-sonnet | $3.00 | $15.00 | 複雜查詢 |
133
+ | gpt-4 | $10.00 | $30.00 | 高精度需求 |
134
+
135
+ ### 成本優化建議
136
+
137
+ 1. **使用 claude-3-haiku** 作為預設模型 (成本最低)
138
+ 2. **設定合理的 token 限制** (max_tokens: 300-500)
139
+ 3. **監控使用量** 透過 OpenRouter Dashboard
140
+ 4. **考慮快取機制** 對常見查詢進行快取
141
+
142
+ ## 🛠️ 開發與測試
143
+
144
+ ### 本地測試
145
+
146
+ ```bash
147
+ # 設定環境變數
148
+ export OPENROUTER_API_KEY="your_key_here"
149
+ export OPENROUTER_MODEL="anthropic/claude-3-haiku"
150
+
151
+ # 執行測試
152
+ python test_api.py
153
+ ```
154
+
155
+ ### API 測試範例
156
+
157
+ ```python
158
+ # 測試進階 NLP
159
+ from backend.services.openrouter_service import OpenRouterService
160
+
161
+ service = OpenRouterService()
162
+ result = await service.analyze_intent_advanced("查詢用戶張三的訂單")
163
+ print(result)
164
+ ```
165
+
166
+ ## 🔍 監控與除錯
167
+
168
+ ### 日誌監控
169
+
170
+ 系統會記錄以下資訊:
171
+
172
+ ```python
173
+ # 成功使用 OpenRouter
174
+ logger.info("使用 OpenRouter 進行進階分析")
175
+
176
+ # 降級到基礎引擎
177
+ logger.warning("進階 NLP 分析失敗,使用基礎分析")
178
+
179
+ # API 錯誤
180
+ logger.error("OpenRouter API 錯誤: 401 Unauthorized")
181
+ ```
182
+
183
+ ### 常見問題
184
+
185
+ 1. **API Key 無效**
186
+ - 檢查 API Key 是否正確
187
+ - 確認 OpenRouter 帳戶餘額
188
+
189
+ 2. **模型不存在**
190
+ - 檢查模型名稱是否正確
191
+ - 參考 OpenRouter 模型列表
192
+
193
+ 3. **請求超時**
194
+ - 檢查網路連線
195
+ - 考慮增加 timeout 設定
196
+
197
+ ## 📈 效能優化
198
+
199
+ ### 1. 異步處理
200
+
201
+ ```python
202
+ # 使用 asyncio 避免阻塞
203
+ async def analyze_intent_advanced(self, message: str):
204
+ async with httpx.AsyncClient(timeout=30.0) as client:
205
+ response = await client.post(...)
206
+ ```
207
+
208
+ ### 2. 錯誤重試
209
+
210
+ ```python
211
+ # 實作重試機制
212
+ for attempt in range(3):
213
+ try:
214
+ result = await openrouter_request()
215
+ break
216
+ except Exception as e:
217
+ if attempt == 2:
218
+ raise e
219
+ await asyncio.sleep(1)
220
+ ```
221
+
222
+ ### 3. 快取策略
223
+
224
+ ```python
225
+ # 快取常見查詢結果
226
+ @lru_cache(maxsize=100)
227
+ def cached_analysis(message_hash: str):
228
+ return analysis_result
229
+ ```
230
+
231
+ ## 🚀 進階功能
232
+
233
+ ### 1. 自訂提示詞
234
+
235
+ 您可以修改 `openrouter_service.py` 中的提示詞來優化特定領域的表現:
236
+
237
+ ```python
238
+ def _build_analysis_prompt(self, message: str):
239
+ # 自訂您的提示詞
240
+ prompt = f"""
241
+ 您是一個專業的電商客服助手...
242
+ """
243
+ ```
244
+
245
+ ### 2. 多語言支援
246
+
247
+ ```python
248
+ # 支援多語言分析
249
+ def analyze_message_multilingual(self, message: str, language: str = "zh"):
250
+ prompt = self._build_multilingual_prompt(message, language)
251
+ ```
252
+
253
+ ### 3. 上下文記憶
254
+
255
+ ```python
256
+ # 維護對話上下文
257
+ def analyze_with_context(self, message: str, conversation_history: List[str]):
258
+ context = {"history": conversation_history}
259
+ return await self.analyze_intent_advanced(message, context)
260
+ ```
261
+
262
+ ## 📞 技術支援
263
+
264
+ 如果您在整合 OpenRouter 時遇到問題:
265
+
266
+ 1. 檢查 [OpenRouter 文件](https://openrouter.ai/docs)
267
+ 2. 查看系統日誌檔案
268
+ 3. 參考本專案的 `test_api.py` 進行測試
269
+ 4. 確認環境變數設定正確
270
+
271
+ ---
272
+
273
+ **注意**: OpenRouter 是付費服務,請根據您的使用量選擇適合的模型和方案。
POSTGRESQL_MIGRATION_GUIDE.md ADDED
@@ -0,0 +1,342 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🔄 PostgreSQL 直連遷移指南
2
+
3
+ 本文件說明如何從 Supabase SDK 遷移到 PostgreSQL 直連的詳細步驟和注意事項。
4
+
5
+ ## 📋 遷移概述
6
+
7
+ ### 變更內容
8
+ - ✅ 移除 Supabase Python SDK 依賴
9
+ - ✅ 新增 SQLAlchemy + psycopg 直連
10
+ - ✅ 重構資料庫服務層
11
+ - ✅ 新增 ORM 模型定義
12
+ - ✅ 支援 Alembic 資料庫遷移
13
+
14
+ ### 優勢
15
+ - 🚀 **更好的效能**: 直連減少中間層開銷
16
+ - 🔧 **更多控制**: 完整的 SQL 查詢控制
17
+ - 📊 **ORM 支援**: SQLAlchemy 提供強大的 ORM 功能
18
+ - 🔄 **遷移管理**: Alembic 提供版本化的資料庫遷移
19
+ - 🛡️ **連線池**: 自動連線池管理
20
+
21
+ ## 🔑 環境變數更新
22
+
23
+ ### 舊的 Supabase 設定
24
+ ```bash
25
+ SUPABASE_URL=https://your-project.supabase.co
26
+ SUPABASE_KEY=your-anon-key
27
+ ```
28
+
29
+ ### 新的 PostgreSQL 設定
30
+ ```bash
31
+ DB_HOST=db.your-project.supabase.co
32
+ DB_PORT=6543
33
+ DB_NAME=postgres
34
+ DB_USER=postgres
35
+ DB_PASSWORD=your-database-password
36
+ ```
37
+
38
+ ## 🔍 如何取得 PostgreSQL 連線資訊
39
+
40
+ ### 從 Supabase Dashboard 取得
41
+
42
+ 1. **登入 Supabase Dashboard**
43
+ - 前往 [Supabase Dashboard](https://supabase.com/dashboard)
44
+ - 選擇您的專案
45
+
46
+ 2. **取得連線資訊**
47
+ - 前往 "Settings" → "Database"
48
+ - 在 "Connection info" 區域找到:
49
+ - **Host**: `db.your-project-ref.supabase.co`
50
+ - **Port**: `6543` (PostgreSQL 直連埠)
51
+ - **Database**: `postgres`
52
+ - **User**: `postgres`
53
+ - **Password**: 您設定的資料庫密碼
54
+
55
+ 3. **連線字串範例**
56
+ ```
57
+ postgresql://postgres:your-password@db.your-project-ref.supabase.co:6543/postgres
58
+ ```
59
+
60
+ ### 從其他 PostgreSQL 提供商取得
61
+
62
+ 如果您使用其他 PostgreSQL 服務:
63
+ - **AWS RDS**: 在 RDS Console 中查看端點資訊
64
+ - **Google Cloud SQL**: 在 Cloud Console 中查看連線詳情
65
+ - **Azure Database**: 在 Azure Portal 中查看連線字串
66
+ - **Railway/Render**: 在專案設定中查看資料庫資訊
67
+
68
+ ## 🛠️ 部署步驟
69
+
70
+ ### 1. 更新環境變數
71
+
72
+ 在 Hugging Face Spaces 的 Settings 中更新:
73
+
74
+ ```bash
75
+ # 移除舊的 Supabase 設定
76
+ # SUPABASE_URL=...
77
+ # SUPABASE_KEY=...
78
+
79
+ # 新增 PostgreSQL 設定
80
+ DB_HOST=db.your-project-ref.supabase.co
81
+ DB_PORT=6543
82
+ DB_NAME=postgres
83
+ DB_USER=postgres
84
+ DB_PASSWORD=your-database-password
85
+
86
+ # 其他設定保持不變
87
+ LINE_CHANNEL_ACCESS_TOKEN=your-line-token
88
+ LINE_CHANNEL_SECRET=your-line-secret
89
+ OPENROUTER_API_KEY=your-openrouter-key
90
+ OPENROUTER_MODEL=anthropic/claude-3-haiku
91
+ ```
92
+
93
+ ### 2. 初始化資料庫
94
+
95
+ 部署後,執行資料庫初始化:
96
+
97
+ ```bash
98
+ # 在容器中執行
99
+ python -m backend.database.init_db
100
+ ```
101
+
102
+ 或者透過 API 端點觸發初始化(如果實作了相關端點)。
103
+
104
+ ### 3. 驗證連線
105
+
106
+ 檢查健康檢查端點:
107
+ ```bash
108
+ curl https://your-space-url.hf.space/health
109
+ ```
110
+
111
+ 應該會看到:
112
+ ```json
113
+ {
114
+ "status": "healthy",
115
+ "message": "LINE Bot API is operational",
116
+ "database": "connected",
117
+ "database_url": "postgresql://your-host:6543/postgres"
118
+ }
119
+ ```
120
+
121
+ ## 📊 資料庫模型對應
122
+
123
+ ### 新的 SQLAlchemy 模型
124
+
125
+ ```python
126
+ # backend/database/models.py
127
+
128
+ class User(Base):
129
+ __tablename__ = "users"
130
+ user_id = Column(String(255), primary_key=True)
131
+ name = Column(String(255))
132
+ email = Column(String(255))
133
+ # ... 其他欄位
134
+
135
+ class Product(Base):
136
+ __tablename__ = "products"
137
+ product_id = Column(Integer, primary_key=True)
138
+ name = Column(String(255), nullable=False)
139
+ price = Column(DECIMAL(10, 2), nullable=False)
140
+ # ... 其他欄位
141
+ ```
142
+
143
+ ### 資料表關聯
144
+
145
+ ```python
146
+ # 一對多關聯
147
+ class User(Base):
148
+ orders = relationship("Order", back_populates="user")
149
+
150
+ class Order(Base):
151
+ user = relationship("User", back_populates="orders")
152
+ order_items = relationship("OrderItem", back_populates="order")
153
+ ```
154
+
155
+ ## 🔄 API 變更
156
+
157
+ ### 查詢方式變更
158
+
159
+ #### 舊的 Supabase 方式
160
+ ```python
161
+ result = supabase.table("users").select("*").eq("name", "張三").execute()
162
+ ```
163
+
164
+ #### 新的 SQLAlchemy 方式
165
+ ```python
166
+ db = get_database_session()
167
+ users = db.query(User).filter(User.name == "張三").all()
168
+ ```
169
+
170
+ ### 錯誤處理改進
171
+
172
+ ```python
173
+ def database_operation():
174
+ db = None
175
+ try:
176
+ db = get_database_session()
177
+ # 資料庫操作
178
+ result = db.query(User).all()
179
+ db.commit()
180
+ return result
181
+ except Exception as e:
182
+ if db:
183
+ db.rollback()
184
+ logger.error(f"資料庫操作失敗: {str(e)}")
185
+ raise
186
+ finally:
187
+ if db:
188
+ close_database_session(db)
189
+ ```
190
+
191
+ ## 🚀 效能優化
192
+
193
+ ### 連線池設定
194
+
195
+ ```python
196
+ # backend/database/connection.py
197
+ engine = create_engine(
198
+ settings.DATABASE_URL,
199
+ pool_size=10, # 連線池大小
200
+ max_overflow=20, # 最大溢出連線
201
+ pool_pre_ping=True, # 連線前檢查
202
+ pool_recycle=3600, # 1小時後回收連線
203
+ )
204
+ ```
205
+
206
+ ### 查詢優化
207
+
208
+ ```python
209
+ # 使用 eager loading 避免 N+1 查詢
210
+ from sqlalchemy.orm import joinedload
211
+
212
+ users_with_orders = db.query(User).options(
213
+ joinedload(User.orders)
214
+ ).all()
215
+
216
+ # 使用索引優化查詢
217
+ # 在模型中定義索引
218
+ class User(Base):
219
+ __tablename__ = "users"
220
+ name = Column(String(255), index=True) # 建立索引
221
+ ```
222
+
223
+ ## 🔧 開發工具
224
+
225
+ ### Alembic 遷移
226
+
227
+ ```bash
228
+ # 初始化 Alembic
229
+ alembic init alembic
230
+
231
+ # 建立遷移檔案
232
+ alembic revision --autogenerate -m "Initial migration"
233
+
234
+ # 執行遷移
235
+ alembic upgrade head
236
+
237
+ # 回滾遷移
238
+ alembic downgrade -1
239
+ ```
240
+
241
+ ### 本地開發
242
+
243
+ ```bash
244
+ # 安裝依賴
245
+ pip install -r requirements.txt
246
+
247
+ # 設定環境變數
248
+ export DB_HOST=localhost
249
+ export DB_PORT=5432
250
+ export DB_NAME=linebot_dev
251
+ export DB_USER=postgres
252
+ export DB_PASSWORD=password
253
+
254
+ # 初始化資料庫
255
+ python -m backend.database.init_db
256
+
257
+ # 執行應用程式
258
+ uvicorn backend.main:app --reload --port 7860
259
+ ```
260
+
261
+ ## 🛡️ 安全性考量
262
+
263
+ ### 連線安全
264
+
265
+ 1. **使用 SSL 連線**
266
+ ```python
267
+ DATABASE_URL = f"postgresql+psycopg://{user}:{password}@{host}:{port}/{db}?sslmode=require"
268
+ ```
269
+
270
+ 2. **限制資料庫權限**
271
+ - 建立專用的應用程式使用者
272
+ - 只授予必要的資料表權限
273
+ - 定期輪換密碼
274
+
275
+ 3. **連線字串保護**
276
+ - 使用環境變數儲存敏感資訊
277
+ - 避免在日誌中記錄連線字串
278
+ - 使用密碼管理工具
279
+
280
+ ## 📈 監控與除錯
281
+
282
+ ### 日誌設定
283
+
284
+ ```python
285
+ # 啟用 SQLAlchemy 日誌
286
+ engine = create_engine(
287
+ settings.DATABASE_URL,
288
+ echo=settings.DEBUG, # 在 DEBUG 模式下顯示 SQL
289
+ )
290
+ ```
291
+
292
+ ### 效能監控
293
+
294
+ ```python
295
+ import time
296
+ from functools import wraps
297
+
298
+ def monitor_db_performance(func):
299
+ @wraps(func)
300
+ def wrapper(*args, **kwargs):
301
+ start_time = time.time()
302
+ result = func(*args, **kwargs)
303
+ execution_time = time.time() - start_time
304
+ logger.info(f"{func.__name__} 執行時間: {execution_time:.2f}秒")
305
+ return result
306
+ return wrapper
307
+ ```
308
+
309
+ ## 🚨 常見問題
310
+
311
+ ### Q: 連線失敗怎麼辦?
312
+ A: 檢查以下項目:
313
+ 1. 主機名稱和埠號是否正確
314
+ 2. 使用者名稱和密碼是否正確
315
+ 3. 資料庫是否允許外部連線
316
+ 4. 防火牆設定是否正確
317
+
318
+ ### Q: 效能比 Supabase SDK 慢?
319
+ A: 可能原因:
320
+ 1. 連線池設定不當
321
+ 2. 查詢未優化
322
+ 3. 缺少適當的索引
323
+ 4. 網路延遲問題
324
+
325
+ ### Q: 如何處理資料庫遷移?
326
+ A: 使用 Alembic:
327
+ 1. 建立遷移腳本
328
+ 2. 在部署前測試遷移
329
+ 3. 備份資料庫
330
+ 4. 執行遷移
331
+
332
+ ## 📞 技術支援
333
+
334
+ 如果遇到問題:
335
+ 1. 檢查應用程式日誌
336
+ 2. 驗證環境變數設定
337
+ 3. 測試資料庫連線
338
+ 4. 參考 SQLAlchemy 官方文件
339
+
340
+ ---
341
+
342
+ **注意**: 遷移前請務必備份您的資料庫,並在測試環境中驗證所有功能正常運作。
README.md CHANGED
@@ -1,12 +1,39 @@
1
  ---
2
  title: Linebot Pydantic Fastapi
3
- emoji: 🔥
4
- colorFrom: red
5
- colorTo: pink
6
  sdk: docker
7
  pinned: false
8
  license: mit
9
- short_description: practice pydantic
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: Linebot Pydantic Fastapi
3
+ emoji: 🤖
4
+ colorFrom: blue
5
+ colorTo: green
6
  sdk: docker
7
  pinned: false
8
  license: mit
9
+ short_description: LINE Bot with FastAPI, Pydantic, and Supabase for natural language database queries
10
  ---
11
 
12
+ # 🤖 LINE Bot + FastAPI + Pydantic + Supabase
13
+
14
+ 智能 LINE Bot,支援自然語言查詢資料庫功能。
15
+
16
+ ## ✨ 功能特色
17
+
18
+ - 🔍 **自然語言查詢**: 支援中文自然語言查詢資料庫
19
+ - 📊 **多種查詢類型**: 用戶、商品、訂單查詢及統計分析
20
+ - 🚀 **FastAPI 框架**: 高效能 Web API
21
+ - 📝 **Pydantic 驗證**: 強型別資料驗證
22
+ - 🗄️ **Supabase 資料庫**: PostgreSQL 雲端資料庫
23
+ - 💬 **LINE Bot 整合**: 完整的 LINE 訊息處理
24
+
25
+ ## 🚀 快速開始
26
+
27
+ 1. 設定環境變數(詳見 `setup_guide.md`)
28
+ 2. 建立 Supabase 資料表
29
+ 3. 設定 LINE Bot Webhook
30
+ 4. 開始使用!
31
+
32
+ ## 📖 使用範例
33
+
34
+ - "查詢用戶 張三"
35
+ - "找價格 1000 到 5000 的商品"
36
+ - "統計訂單數量"
37
+ - "我的訂單狀態"
38
+
39
+ 詳細設定說明請參考 [setup_guide.md](./setup_guide.md)
alembic.ini ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # A generic, single database configuration.
2
+
3
+ [alembic]
4
+ # path to migration scripts
5
+ script_location = alembic
6
+
7
+ # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
8
+ # Uncomment the line below if you want the files to be prepended with date and time
9
+ # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
10
+
11
+ # sys.path path, will be prepended to sys.path if present.
12
+ # defaults to the current working directory.
13
+ prepend_sys_path = .
14
+
15
+ # timezone to use when rendering the date within the migration file
16
+ # as well as the filename.
17
+ # If specified, requires the python-dateutil library that can be
18
+ # installed by adding `alembic[tz]` to the pip requirements
19
+ # string value is passed to dateutil.tz.gettz()
20
+ # leave blank for localtime
21
+ # timezone =
22
+
23
+ # max length of characters to apply to the
24
+ # "slug" field
25
+ # truncate_slug_length = 40
26
+
27
+ # set to 'true' to run the environment during
28
+ # the 'revision' command, regardless of autogenerate
29
+ # revision_environment = false
30
+
31
+ # set to 'true' to allow .pyc and .pyo files without
32
+ # a source .py file to be detected as revisions in the
33
+ # versions/ directory
34
+ # sourceless = false
35
+
36
+ # version number format. This value is passed to the
37
+ # "strftime" function from the Python standard library.
38
+ # see https://docs.python.org/library/datetime.html#strftime-strptime-behavior
39
+ # for a list of codes that can be used in this string.
40
+ # version_num_format = %%(year)d%%(month).2d%%(day).2d_%%(hour).2d%%(minute).2d
41
+
42
+ # version path separator; As mentioned above, this is the character used to split
43
+ # version_locations. The default within new alembic.ini files is "os", which uses
44
+ # os.pathsep. If this key is omitted entirely, it falls back to the legacy
45
+ # behavior of splitting on spaces and/or commas.
46
+ # Valid values for version_path_separator are:
47
+ #
48
+ # version_path_separator = :
49
+ # version_path_separator = ;
50
+ # version_path_separator = space
51
+ version_path_separator = os
52
+
53
+ # set to 'true' to search source files recursively
54
+ # in each "version_locations" directory
55
+ # new in Alembic version 1.10
56
+ # recursive_version_locations = false
57
+
58
+ # the output encoding used when revision files
59
+ # are written from script.py.mako
60
+ # output_encoding = utf-8
61
+
62
+ sqlalchemy.url = driver://user:pass@localhost/dbname
63
+
64
+
65
+ [post_write_hooks]
66
+ # post_write_hooks defines scripts or Python functions that are run
67
+ # on newly generated revision scripts. See the documentation for further
68
+ # detail and examples
69
+
70
+ # format using "black" - use the console_scripts runner, against the "black" entrypoint
71
+ # hooks = black
72
+ # black.type = console_scripts
73
+ # black.entrypoint = black
74
+ # black.options = -l 79 REVISION_SCRIPT_FILENAME
75
+
76
+ # lint with attempts to fix using "ruff" - use the exec runner, execute a binary
77
+ # hooks = ruff
78
+ # ruff.type = exec
79
+ # ruff.executable = %(here)s/.venv/bin/ruff
80
+ # ruff.options = --fix REVISION_SCRIPT_FILENAME
81
+
82
+ # Logging configuration
83
+ [loggers]
84
+ keys = root,sqlalchemy,alembic
85
+
86
+ [handlers]
87
+ keys = console
88
+
89
+ [formatters]
90
+ keys = generic
91
+
92
+ [logger_root]
93
+ level = WARN
94
+ handlers = console
95
+ qualname =
96
+
97
+ [logger_sqlalchemy]
98
+ level = WARN
99
+ handlers =
100
+ qualname = sqlalchemy.engine
101
+
102
+ [logger_alembic]
103
+ level = INFO
104
+ handlers =
105
+ qualname = alembic
106
+
107
+ [handler_console]
108
+ class = StreamHandler
109
+ args = (sys.stderr,)
110
+ level = NOTSET
111
+ formatter = generic
112
+
113
+ [formatter_generic]
114
+ format = %(levelname)-5.5s [%(name)s] %(message)s
115
+ datefmt = %H:%M:%S
another_proj_schemas.py ADDED
@@ -0,0 +1,492 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, EmailStr, Field
2
+ from typing import List, Optional, Union
3
+ from datetime import datetime, date
4
+ import enum
5
+
6
+ # Import Enum types from models.py to be used in Pydantic schemas
7
+ # This avoids redefining them and ensures consistency.
8
+ # We might need to adjust models.py if enums are defined in a way that's hard to import directly
9
+ # For now, let's assume we can import them or we'll redefine for Pydantic if necessary.
10
+ # For simplicity in this step, I'll redefine them here. If issues arise, we can refactor.
11
+
12
+ class UserRole(str, enum.Enum):
13
+ ADMIN = "ADMIN"
14
+ USER = "USER"
15
+
16
+ class TransactionType(str, enum.Enum):
17
+ PURCHASE = "PURCHASE"
18
+ SELL = "SELL"
19
+
20
+ class TransactionStatus(str, enum.Enum):
21
+ PENDING = "PENDING"
22
+ PROCESSING = "PROCESSING" # Added PROCESSING status
23
+ COMPLETED = "COMPLETED"
24
+ CANCELLED = "CANCELLED"
25
+
26
+ # CustomerType 和 PaymentCategory 改為字串類型,支援動態值
27
+ # PaymentMethod 改為支援動態字串,如 "月結30天", "下收" 等
28
+
29
+ # Base and Read schemas for Category
30
+ class CategoryBase(BaseModel):
31
+ name: str
32
+
33
+ class CategoryCreate(CategoryBase):
34
+ pass
35
+
36
+ class Category(CategoryBase): # For Read operations
37
+ id: int
38
+
39
+ class Config:
40
+ from_attributes = True
41
+
42
+ # Base and Read schemas for Product - 根據客戶 Excel 欄位重新設計
43
+ class ProductBase(BaseModel):
44
+ productCode: str = Field(..., description="貨品編號,必須唯一")
45
+ productName: str = Field(..., description="貨品名稱")
46
+ unit: str = Field(..., description="單位(箱、盒等)")
47
+ warehouse: Optional[str] = None # 倉別
48
+ unitWeight: Optional[float] = None # 單位重量(KG)
49
+ barcode: Optional[str] = None # 條碼編號
50
+ category_id: int = Field(..., description="類別ID")
51
+
52
+ class ProductCreate(ProductBase):
53
+ pass
54
+
55
+ # For Read operations
56
+ class Product(ProductBase):
57
+ id: int
58
+ stock: int = Field(default=0, description="庫存數量") # 添加庫存欄位
59
+ is_deleted: bool = Field(default=False, description="是否已刪除")
60
+ deleted_at: Optional[datetime] = None
61
+ deleted_by: Optional[int] = None
62
+ createdAt: datetime
63
+ updatedAt: Optional[datetime] = None
64
+ category: Optional[Category] = None # 包含類別詳細資訊
65
+
66
+ class Config:
67
+ from_attributes = True
68
+
69
+ # Schemas for updating a product (all fields optional)
70
+ class ProductUpdate(BaseModel):
71
+ productCode: Optional[str] = None
72
+ productName: Optional[str] = None
73
+ unit: Optional[str] = None
74
+ warehouse: Optional[str] = None
75
+ unitWeight: Optional[float] = None
76
+ barcode: Optional[str] = None
77
+ category_id: Optional[int] = None
78
+ stock: Optional[int] = Field(None, ge=0, description="庫存數量,必須大於等於0") # 添加庫存更新
79
+
80
+
81
+ # Base and Read schemas for Supplier
82
+ class SupplierBase(BaseModel):
83
+ name: str
84
+ contactInfo: Optional[str] = None
85
+ address: Optional[str] = None
86
+
87
+ class SupplierCreate(SupplierBase):
88
+ pass
89
+
90
+ class Supplier(SupplierBase): # For Read operations
91
+ id: int
92
+
93
+ class Config:
94
+ from_attributes = True
95
+
96
+ # Base and Read schemas for Customer
97
+ class CustomerBase(BaseModel):
98
+ customerType: str
99
+ salesPersonId: Optional[str] = None
100
+ salesPersonName: Optional[str] = None
101
+ customerCode: str = Field(..., description="客戶編號,必須唯一")
102
+ customerName: str = Field(..., description="客戶名稱")
103
+ contactPerson: Optional[str] = None
104
+ invoiceTitle: Optional[str] = None
105
+ taxId: Optional[str] = None
106
+ phoneNumber: Optional[str] = None
107
+ faxNumber: Optional[str] = None
108
+ deliveryAddress: Optional[str] = None
109
+ businessHours: Optional[str] = None
110
+ paymentMethod: Optional[str] = None
111
+ paymentCategory: Optional[str] = None
112
+ creditLimit: Optional[float] = 0.0
113
+
114
+ class CustomerCreate(CustomerBase):
115
+ pass
116
+
117
+ class CustomerUpdate(BaseModel):
118
+ customerType: Optional[str] = None
119
+ salesPersonId: Optional[str] = None
120
+ salesPersonName: Optional[str] = None
121
+ customerCode: Optional[str] = None
122
+ customerName: Optional[str] = None
123
+ contactPerson: Optional[str] = None
124
+ invoiceTitle: Optional[str] = None
125
+ taxId: Optional[str] = None
126
+ phoneNumber: Optional[str] = None
127
+ faxNumber: Optional[str] = None
128
+ deliveryAddress: Optional[str] = None
129
+ businessHours: Optional[str] = None
130
+ paymentMethod: Optional[str] = None
131
+ paymentCategory: Optional[str] = None
132
+ creditLimit: Optional[float] = None
133
+
134
+ class Customer(CustomerBase): # For Read operations
135
+ id: int
136
+ createdDate: datetime
137
+ updatedAt: Optional[datetime] = None
138
+
139
+ class Config:
140
+ from_attributes = True
141
+
142
+ # Base and Read schemas for User
143
+ class UserBase(BaseModel):
144
+ name: Optional[str] = None
145
+ email: EmailStr
146
+ phoneNumber: Optional[str] = None
147
+ role: Optional[UserRole] = UserRole.USER
148
+
149
+ class UserCreate(UserBase):
150
+ password: str = Field(..., min_length=4) # Example: make password required on create
151
+
152
+ class UserUpdate(BaseModel): # For updating user profile
153
+ name: Optional[str] = None
154
+ email: Optional[EmailStr] = None
155
+ phoneNumber: Optional[str] = None
156
+ role: Optional[UserRole] = None
157
+ # Role and password updates might be handled by separate, more secure endpoints or admin functions
158
+
159
+ class User(UserBase): # For Read operations (e.g., /users/current)
160
+ id: int
161
+ createdAt: datetime
162
+ # Do not include password in responses
163
+
164
+ class Config:
165
+ from_attributes = True
166
+
167
+
168
+ # Schemas for TransactionProductAssociation (enhanced with pricing information)
169
+ class TransactionProductAssociationBase(BaseModel):
170
+ product_id: int
171
+ quantity: Optional[int] = 1
172
+ unit_price: Optional[float] = None
173
+ line_total: Optional[float] = None
174
+ notes: Optional[str] = None
175
+
176
+ class TransactionProductAssociationCreate(TransactionProductAssociationBase):
177
+ pass
178
+
179
+ class TransactionProductAssociation(TransactionProductAssociationBase): # For Read
180
+ # Include product details for complete information
181
+ product: Optional[Product] = None
182
+
183
+ class Config:
184
+ from_attributes = True
185
+
186
+
187
+ # Base and Read schemas for Transaction
188
+ class TransactionBase(BaseModel):
189
+ totalProducts: int
190
+ totalPrice: float
191
+ transactionType: TransactionType
192
+ transactionStatus: Optional[TransactionStatus] = TransactionStatus.PENDING
193
+ description: Optional[str] = None
194
+ note: Optional[str] = None
195
+ user_id: Optional[int] = None # Assuming user_id is set based on authenticated user
196
+ supplier_id: Optional[int] = None
197
+ customer_id: Optional[int] = None # For sell transactions
198
+ # For creating transactions, the frontend might send a list of products involved
199
+ # This needs to align with how api.service.ts sends data for purchase/sell
200
+ # For example: products_involved: List[TransactionProductAssociationCreate]
201
+
202
+ class TransactionCreate(TransactionBase):
203
+ # The frontend's purchaseProduct/sellProduct takes a 'body'. We need to match that structure.
204
+ # If 'body' contains a list of product IDs and quantities:
205
+ products_involved: List[TransactionProductAssociationCreate]
206
+
207
+
208
+ class Transaction(TransactionBase): # For Read operations
209
+ id: int
210
+ createdAt: datetime
211
+ updatedAt: Optional[datetime] = None
212
+ user: Optional[User] = None # Nested user details
213
+ supplier: Optional[Supplier] = None # Nested supplier details
214
+ customer: Optional[Customer] = None # Nested customer details for sell transactions
215
+ products: List[TransactionProductAssociation] # List of products involved in the transaction
216
+
217
+ class Config:
218
+ from_attributes = True
219
+
220
+ # Schema for updating transaction status (as per frontend api.service.ts)
221
+ class TransactionStatusUpdate(BaseModel):
222
+ status: TransactionStatus # Frontend sends JSON.stringify(status) - need to ensure this matches
223
+
224
+ # Schemas for Authentication
225
+ class Token(BaseModel):
226
+ access_token: str
227
+ token_type: str
228
+
229
+ class TokenData(BaseModel):
230
+ email: Optional[str] = None
231
+
232
+ class UserLogin(BaseModel):
233
+ email: EmailStr
234
+ password: str
235
+
236
+
237
+ # 採購單相關的枚舉類型
238
+ class PurchaseOrderStatus(str, enum.Enum):
239
+ DRAFT = "DRAFT" # 草稿
240
+ PENDING = "PENDING" # 待處理
241
+ CONFIRMED = "CONFIRMED" # 已確認
242
+ RECEIVED = "RECEIVED" # 已收貨
243
+ CANCELLED = "CANCELLED" # 已取消
244
+
245
+ class PaymentStatus(str, enum.Enum):
246
+ UNPAID = "UNPAID" # 未付款
247
+ PARTIAL = "PARTIAL" # 部分付款
248
+ PAID = "PAID" # 已付款
249
+
250
+ class TaxType(str, enum.Enum):
251
+ INCLUSIVE = "INCLUSIVE" # 含稅
252
+ EXCLUSIVE = "EXCLUSIVE" # 未稅
253
+ ADDITIONAL = "ADDITIONAL" # 外加稅
254
+
255
+ # 入庫單相關的枚舉類型
256
+ class GoodsReceiptStatus(str, enum.Enum):
257
+ DRAFT = "DRAFT" # 草稿
258
+ PENDING = "PENDING" # 待處理
259
+ COMPLETED = "COMPLETED" # 已完成
260
+ CANCELLED = "CANCELLED" # 已取消
261
+
262
+ class WarehouseType(str, enum.Enum):
263
+ MAIN = "MAIN" # 主倉
264
+ RAW_MATERIAL = "RAW_MATERIAL" # 原料倉
265
+ FINISHED_GOODS = "FINISHED_GOODS" # 成品倉
266
+ QUARANTINE = "QUARANTINE" # 檢疫倉
267
+ DAMAGED = "DAMAGED" # 損壞品倉
268
+
269
+ # 銷售單相關的枚舉類型
270
+ class SalesOrderStatus(str, enum.Enum):
271
+ DRAFT = "DRAFT" # 草稿
272
+ CONFIRMED = "CONFIRMED" # 已確認
273
+ SHIPPED = "SHIPPED" # 已出貨
274
+ DELIVERED = "DELIVERED" # 已送達
275
+ CANCELLED = "CANCELLED" # 已取消
276
+
277
+ class PaymentTerm(str, enum.Enum):
278
+ CASH = "CASH" # 現金
279
+ MONTHLY = "MONTHLY" # 月結
280
+ TRANSFER = "TRANSFER" # 轉帳
281
+ CREDIT_CARD = "CREDIT_CARD" # 信用卡
282
+ CHECK = "CHECK" # 支票
283
+
284
+
285
+ # 採購明細項目 schemas
286
+ class PurchaseOrderItemBase(BaseModel):
287
+ product_id: int = Field(..., description="產品ID")
288
+ quantity: int = Field(..., gt=0, description="數量,必須大於0")
289
+ unit_price: float = Field(..., ge=0, description="單價,必須大於等於0")
290
+ notes: Optional[str] = None
291
+
292
+ class PurchaseOrderItemCreate(PurchaseOrderItemBase):
293
+ pass
294
+
295
+ class PurchaseOrderItemUpdate(BaseModel):
296
+ product_id: Optional[int] = None
297
+ quantity: Optional[int] = Field(None, gt=0)
298
+ unit_price: Optional[float] = Field(None, ge=0)
299
+ notes: Optional[str] = None
300
+
301
+ class PurchaseOrderItem(PurchaseOrderItemBase):
302
+ id: int
303
+ line_total: float # 小計 (數量 × 單價)
304
+ product: Optional[Product] = None # 包含產品詳細資訊
305
+ created_at: datetime
306
+ updated_at: Optional[datetime] = None
307
+
308
+ class Config:
309
+ from_attributes = True
310
+
311
+
312
+ # 採購單主檔 schemas
313
+ class PurchaseOrderBase(BaseModel):
314
+ purchase_date: date = Field(..., description="採購日期")
315
+ expected_delivery_date: Optional[date] = None
316
+ supplier_id: int = Field(..., description="供應商ID")
317
+ notes: Optional[str] = None
318
+ tax_type: TaxType = TaxType.INCLUSIVE
319
+ tax_rate: float = Field(0.05, ge=0, le=1, description="稅率,0-1之間")
320
+ payment_method: Optional[str] = None
321
+
322
+ class PurchaseOrderCreate(PurchaseOrderBase):
323
+ items: List[PurchaseOrderItemCreate] = Field(..., min_items=1, description="採購明細,至少要有一項")
324
+
325
+ class PurchaseOrderUpdate(BaseModel):
326
+ purchase_date: Optional[date] = None
327
+ expected_delivery_date: Optional[date] = None
328
+ supplier_id: Optional[int] = None
329
+ status: Optional[PurchaseOrderStatus] = None
330
+ notes: Optional[str] = None
331
+ tax_type: Optional[TaxType] = None
332
+ tax_rate: Optional[float] = Field(None, ge=0, le=1)
333
+ payment_method: Optional[str] = None
334
+ payment_status: Optional[PaymentStatus] = None
335
+
336
+ class PurchaseOrder(PurchaseOrderBase):
337
+ id: int
338
+ po_number: str # 採購單號
339
+ purchaser_id: int
340
+ status: PurchaseOrderStatus
341
+ subtotal: float # 小計
342
+ tax_amount: float # 稅額
343
+ total_amount: float # 含稅總額
344
+ payment_status: PaymentStatus
345
+ created_at: datetime
346
+ updated_at: Optional[datetime] = None
347
+
348
+ # 關聯資料
349
+ purchaser: Optional[User] = None
350
+ supplier: Optional[Supplier] = None
351
+ items: List[PurchaseOrderItem] = []
352
+
353
+ class Config:
354
+ from_attributes = True
355
+
356
+
357
+ # 入庫明細項目 schemas
358
+ class GoodsReceiptItemBase(BaseModel):
359
+ purchase_order_item_id: int = Field(..., description="採購明細ID")
360
+ product_id: int = Field(..., description="產品ID")
361
+ ordered_quantity: int = Field(..., gt=0, description="採購數量")
362
+ received_quantity: int = Field(..., ge=0, description="實到數量,必須大於等於0")
363
+ storage_location: Optional[str] = None
364
+ notes: Optional[str] = None
365
+
366
+ class GoodsReceiptItemCreate(GoodsReceiptItemBase):
367
+ pass
368
+
369
+ class GoodsReceiptItemUpdate(BaseModel):
370
+ purchase_order_item_id: Optional[int] = None # 需要用來識別明細項目
371
+ product_id: Optional[int] = None # 需要用來識別產品
372
+ received_quantity: Optional[int] = Field(None, ge=0)
373
+ storage_location: Optional[str] = None
374
+ notes: Optional[str] = None
375
+
376
+ class GoodsReceiptItem(GoodsReceiptItemBase):
377
+ id: int
378
+ product: Optional[Product] = None # 包含產品詳細資訊
379
+ purchase_order_item: Optional[PurchaseOrderItem] = None # 包含採購明細資訊
380
+ created_at: datetime
381
+ updated_at: Optional[datetime] = None
382
+
383
+ class Config:
384
+ from_attributes = True
385
+
386
+
387
+ # 入庫單主檔 schemas
388
+ class GoodsReceiptBase(BaseModel):
389
+ receipt_date: date = Field(..., description="入庫日期")
390
+ purchase_order_id: int = Field(..., description="採購單ID")
391
+ warehouse_type: WarehouseType = WarehouseType.MAIN
392
+ warehouse_location: Optional[str] = None
393
+ notes: Optional[str] = None
394
+
395
+ class GoodsReceiptCreate(GoodsReceiptBase):
396
+ items: List[GoodsReceiptItemCreate] = Field(..., min_items=1, description="入庫明細,至少要有一項")
397
+
398
+ class GoodsReceiptUpdate(BaseModel):
399
+ receipt_date: Optional[date] = None
400
+ warehouse_type: Optional[WarehouseType] = None
401
+ warehouse_location: Optional[str] = None
402
+ status: Optional[GoodsReceiptStatus] = None
403
+ notes: Optional[str] = None
404
+ items: Optional[List[GoodsReceiptItemUpdate]] = None # 新增入庫明細更新
405
+
406
+ class GoodsReceipt(GoodsReceiptBase):
407
+ id: int
408
+ gr_number: str # 入庫單號
409
+ warehouse_staff_id: int
410
+ status: GoodsReceiptStatus
411
+ created_at: datetime
412
+ updated_at: Optional[datetime] = None
413
+
414
+ # 關聯資料
415
+ warehouse_staff: Optional[User] = None
416
+ purchase_order: Optional[PurchaseOrder] = None
417
+ items: List[GoodsReceiptItem] = []
418
+
419
+ class Config:
420
+ from_attributes = True
421
+
422
+
423
+ # 銷售明細項目 schemas
424
+ class SalesOrderItemBase(BaseModel):
425
+ product_id: int = Field(..., description="產品ID")
426
+ quantity: int = Field(..., gt=0, description="數量,必須大於0")
427
+ unit_price: float = Field(..., ge=0, description="單價,必須大於等於0")
428
+ notes: Optional[str] = None
429
+
430
+ class SalesOrderItemCreate(SalesOrderItemBase):
431
+ pass
432
+
433
+ class SalesOrderItemUpdate(BaseModel):
434
+ product_id: Optional[int] = None
435
+ quantity: Optional[int] = Field(None, gt=0)
436
+ unit_price: Optional[float] = Field(None, ge=0)
437
+ notes: Optional[str] = None
438
+
439
+ class SalesOrderItem(SalesOrderItemBase):
440
+ id: int
441
+ line_total: float # 小計 (數量 × 單價)
442
+ product: Optional[Product] = None # 包含產品詳細資訊
443
+ created_at: datetime
444
+ updated_at: Optional[datetime] = None
445
+
446
+ class Config:
447
+ from_attributes = True
448
+
449
+
450
+ # 銷售單主檔 schemas
451
+ class SalesOrderBase(BaseModel):
452
+ sales_date: date = Field(..., description="銷售日期")
453
+ customer_id: int = Field(..., description="客戶ID")
454
+ payment_term: PaymentTerm = PaymentTerm.CASH
455
+ notes: Optional[str] = None
456
+ tax_type: TaxType = TaxType.INCLUSIVE
457
+ tax_rate: float = Field(0.05, ge=0, le=1, description="稅率,0-1之間")
458
+ discount_rate: float = Field(0.0, ge=0, le=1, description="折扣率,0-1之間")
459
+
460
+ class SalesOrderCreate(SalesOrderBase):
461
+ items: List[SalesOrderItemCreate] = Field(..., min_items=1, description="銷售明細,至少要有一項")
462
+
463
+ class SalesOrderUpdate(BaseModel):
464
+ sales_date: Optional[date] = None
465
+ customer_id: Optional[int] = None
466
+ payment_term: Optional[PaymentTerm] = None
467
+ status: Optional[SalesOrderStatus] = None
468
+ notes: Optional[str] = None
469
+ tax_type: Optional[TaxType] = None
470
+ tax_rate: Optional[float] = Field(None, ge=0, le=1)
471
+ discount_rate: Optional[float] = Field(None, ge=0, le=1)
472
+ items: Optional[List[SalesOrderItemUpdate]] = None # 銷售明細更新
473
+
474
+ class SalesOrder(SalesOrderBase):
475
+ id: int
476
+ so_number: str # 銷售單號
477
+ salesperson_id: int
478
+ status: SalesOrderStatus
479
+ subtotal: float # 小計
480
+ tax_amount: float # 稅額
481
+ discount_amount: float # 折扣金額
482
+ total_amount: float # 實收總額
483
+ created_at: datetime
484
+ updated_at: Optional[datetime] = None
485
+
486
+ # 關聯資料
487
+ salesperson: Optional[User] = None
488
+ customer: Optional[Customer] = None
489
+ items: List[SalesOrderItem] = []
490
+
491
+ class Config:
492
+ from_attributes = True
app.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+
3
+ app = FastAPI()
4
+
5
+ @app.get("/")
6
+ def greet_json():
7
+ return {"Hello": "World!"}
backend/__init__.py ADDED
File without changes
backend/config.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from pydantic_settings import BaseSettings
3
+ from typing import Optional
4
+
5
+ class Settings(BaseSettings):
6
+ """應用程式設定"""
7
+
8
+ # LINE Bot 設定
9
+ LINE_CHANNEL_ACCESS_TOKEN: str = os.getenv("LINE_CHANNEL_ACCESS_TOKEN", "")
10
+ LINE_CHANNEL_SECRET: str = os.getenv("LINE_CHANNEL_SECRET", "")
11
+
12
+ # 資料庫設定 (PostgreSQL 直連)
13
+ DB_HOST: str = os.getenv("DB_HOST", "")
14
+ DB_PORT: str = os.getenv("DB_PORT", "6543")
15
+ DB_NAME: str = os.getenv("DB_NAME", "")
16
+ DB_USER: str = os.getenv("DB_USER", "")
17
+ DB_PASSWORD: str = os.getenv("DB_PASSWORD", "")
18
+
19
+ # SQLAlchemy 資料庫 URL
20
+ @property
21
+ def DATABASE_URL(self) -> str:
22
+ return f"postgresql+psycopg://{self.DB_USER}:{self.DB_PASSWORD}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}"
23
+
24
+ # OpenRouter 設定 (用於自然語言處理)
25
+ OPENROUTER_API_KEY: Optional[str] = os.getenv("OPENROUTER_API_KEY", "")
26
+ OPENROUTER_MODEL: str = os.getenv("OPENROUTER_MODEL", "anthropic/claude-3-haiku")
27
+
28
+ # 其他設定
29
+ DEBUG: bool = os.getenv("DEBUG", "False").lower() == "true"
30
+ LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO")
31
+
32
+ class Config:
33
+ env_file = ".env"
34
+ case_sensitive = True
35
+
36
+ settings = Settings()
backend/database/__init__.py ADDED
File without changes
backend/database/connection.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 資料庫連線管理
3
+ """
4
+
5
+ from sqlalchemy import create_engine, text
6
+ from sqlalchemy.ext.declarative import declarative_base
7
+ from sqlalchemy.orm import sessionmaker, Session
8
+ from backend.config import settings
9
+ import logging
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ # SQLAlchemy 設定
14
+ engine = create_engine(
15
+ settings.DATABASE_URL,
16
+ echo=settings.DEBUG, # 在 DEBUG 模式下顯示 SQL 語句
17
+ pool_size=10,
18
+ max_overflow=20,
19
+ pool_pre_ping=True, # 連線前檢查
20
+ pool_recycle=3600, # 1小時後回收連線
21
+ )
22
+
23
+ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
24
+ Base = declarative_base()
25
+
26
+ def get_database_session() -> Session:
27
+ """取得資料庫 session"""
28
+ db = SessionLocal()
29
+ try:
30
+ return db
31
+ except Exception as e:
32
+ db.close()
33
+ raise e
34
+
35
+ def close_database_session(db: Session):
36
+ """關閉資料庫 session"""
37
+ try:
38
+ db.close()
39
+ except Exception as e:
40
+ logger.error(f"關閉資料庫 session 時發生錯誤: {str(e)}")
41
+
42
+ def test_database_connection() -> bool:
43
+ """測試資料庫連線"""
44
+ try:
45
+ with engine.connect() as connection:
46
+ result = connection.execute(text("SELECT 1"))
47
+ return result.fetchone()[0] == 1
48
+ except Exception as e:
49
+ logger.error(f"資料庫連線測試失敗: {str(e)}")
50
+ return False
51
+
52
+ # 依賴注入函數,用於 FastAPI
53
+ def get_db():
54
+ """FastAPI 依賴注入函數"""
55
+ db = SessionLocal()
56
+ try:
57
+ yield db
58
+ finally:
59
+ db.close()
backend/database/init_db.py ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 資料庫初始化腳本
3
+ 使用 SQLAlchemy 建立資料表和插入範例資料
4
+ """
5
+
6
+ from sqlalchemy import create_engine
7
+ from backend.database.connection import Base, engine
8
+ from backend.database.models import User, Product, Order, OrderItem, LineMessage, UserSession
9
+ from backend.config import settings
10
+ import logging
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ def create_tables():
15
+ """建立所有資料表"""
16
+ try:
17
+ logger.info("開始建立資料表...")
18
+ Base.metadata.create_all(bind=engine)
19
+ logger.info("資料表建立完成!")
20
+ return True
21
+ except Exception as e:
22
+ logger.error(f"建立資料表失敗: {str(e)}")
23
+ return False
24
+
25
+ def insert_sample_data():
26
+ """插入範例資料"""
27
+ from backend.database.connection import SessionLocal
28
+
29
+ db = SessionLocal()
30
+ try:
31
+ logger.info("開始插入範例資料...")
32
+
33
+ # 檢查是否已有資料
34
+ existing_products = db.query(Product).count()
35
+ if existing_products > 0:
36
+ logger.info("資料表已有資料,跳過範例資料插入")
37
+ return True
38
+
39
+ # 插入範例商品
40
+ sample_products = [
41
+ Product(
42
+ name="iPhone 15 Pro",
43
+ description="最新款 iPhone,配備 A17 Pro 晶片",
44
+ price=35900,
45
+ stock=50,
46
+ category="手機"
47
+ ),
48
+ Product(
49
+ name="MacBook Air M2",
50
+ description="輕薄筆記型電腦,搭載 M2 晶片",
51
+ price=37900,
52
+ stock=30,
53
+ category="筆電"
54
+ ),
55
+ Product(
56
+ name="AirPods Pro",
57
+ description="主動降噪無線耳機",
58
+ price=7490,
59
+ stock=100,
60
+ category="耳機"
61
+ ),
62
+ Product(
63
+ name="iPad Air",
64
+ description="輕薄平板電腦,適合工作和娛樂",
65
+ price=18900,
66
+ stock=25,
67
+ category="平板"
68
+ ),
69
+ Product(
70
+ name="Apple Watch Series 9",
71
+ description="智慧手錶,健康監測功能",
72
+ price=12900,
73
+ stock=40,
74
+ category="穿戴裝置"
75
+ )
76
+ ]
77
+
78
+ for product in sample_products:
79
+ db.add(product)
80
+
81
+ # 插入範例用戶
82
+ sample_users = [
83
+ User(
84
+ user_id="U001",
85
+ name="張小明",
86
+ email="ming@example.com",
87
+ display_name="小明"
88
+ ),
89
+ User(
90
+ user_id="U002",
91
+ name="李小華",
92
+ email="hua@example.com",
93
+ display_name="小華"
94
+ ),
95
+ User(
96
+ user_id="U003",
97
+ name="王小美",
98
+ email="mei@example.com",
99
+ display_name="小美"
100
+ )
101
+ ]
102
+
103
+ for user in sample_users:
104
+ db.add(user)
105
+
106
+ # 提交變更
107
+ db.commit()
108
+ logger.info("範例資料插入完成!")
109
+ return True
110
+
111
+ except Exception as e:
112
+ db.rollback()
113
+ logger.error(f"插入範例資料失敗: {str(e)}")
114
+ return False
115
+ finally:
116
+ db.close()
117
+
118
+ def init_database():
119
+ """初始化資料庫"""
120
+ logger.info("=== 開始初始化資料庫 ===")
121
+
122
+ # 建立資料表
123
+ if not create_tables():
124
+ logger.error("資料表建立失敗,停止初始化")
125
+ return False
126
+
127
+ # 插入範例資料
128
+ if not insert_sample_data():
129
+ logger.error("範例資料插入失敗")
130
+ return False
131
+
132
+ logger.info("=== 資料庫初始化完成 ===")
133
+ return True
134
+
135
+ def drop_all_tables():
136
+ """刪除所有資料表(謹慎使用)"""
137
+ try:
138
+ logger.warning("正在刪除所有資料表...")
139
+ Base.metadata.drop_all(bind=engine)
140
+ logger.warning("所有資料表已刪除!")
141
+ return True
142
+ except Exception as e:
143
+ logger.error(f"刪除資料表失敗: {str(e)}")
144
+ return False
145
+
146
+ if __name__ == "__main__":
147
+ import sys
148
+
149
+ if len(sys.argv) > 1 and sys.argv[1] == "--drop":
150
+ # 刪除並重建資料表
151
+ drop_all_tables()
152
+ init_database()
153
+ else:
154
+ # 正常初始化
155
+ init_database()
backend/database/models.py ADDED
@@ -0,0 +1,226 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ SQLAlchemy 資料庫模型 - 基於進銷存系統
3
+ """
4
+
5
+ from sqlalchemy import Column, Integer, String, Text, DECIMAL, Boolean, DateTime, ForeignKey, JSON, Date, Float, Enum
6
+ from sqlalchemy.sql import func
7
+ from sqlalchemy.orm import relationship
8
+ from backend.database.connection import Base
9
+ import enum
10
+
11
+ # 枚舉類型定義
12
+ class UserRole(str, enum.Enum):
13
+ ADMIN = "ADMIN"
14
+ USER = "USER"
15
+
16
+ class TransactionType(str, enum.Enum):
17
+ PURCHASE = "PURCHASE"
18
+ SELL = "SELL"
19
+
20
+ class TransactionStatus(str, enum.Enum):
21
+ PENDING = "PENDING"
22
+ PROCESSING = "PROCESSING"
23
+ COMPLETED = "COMPLETED"
24
+ CANCELLED = "CANCELLED"
25
+
26
+ class PurchaseOrderStatus(str, enum.Enum):
27
+ DRAFT = "DRAFT"
28
+ PENDING = "PENDING"
29
+ CONFIRMED = "CONFIRMED"
30
+ RECEIVED = "RECEIVED"
31
+ CANCELLED = "CANCELLED"
32
+
33
+ class SalesOrderStatus(str, enum.Enum):
34
+ DRAFT = "DRAFT"
35
+ CONFIRMED = "CONFIRMED"
36
+ SHIPPED = "SHIPPED"
37
+ DELIVERED = "DELIVERED"
38
+ CANCELLED = "CANCELLED"
39
+
40
+ class PaymentStatus(str, enum.Enum):
41
+ UNPAID = "UNPAID"
42
+ PARTIAL = "PARTIAL"
43
+ PAID = "PAID"
44
+
45
+ # 基礎模型
46
+ class User(Base):
47
+ """用戶模型"""
48
+ __tablename__ = "users"
49
+
50
+ id = Column(Integer, primary_key=True, autoincrement=True)
51
+ name = Column(String(255))
52
+ email = Column(String(255), unique=True, nullable=False)
53
+ phoneNumber = Column(String(50))
54
+ role = Column(Enum(UserRole), default=UserRole.USER)
55
+ createdAt = Column(DateTime(timezone=True), server_default=func.now())
56
+
57
+ # 關聯
58
+ purchase_orders = relationship("PurchaseOrder", back_populates="purchaser")
59
+ sales_orders = relationship("SalesOrder", back_populates="salesperson")
60
+
61
+ class Category(Base):
62
+ """商品類別模型"""
63
+ __tablename__ = "categories"
64
+
65
+ id = Column(Integer, primary_key=True, autoincrement=True)
66
+ name = Column(String(255), nullable=False)
67
+
68
+ # 關聯
69
+ products = relationship("Product", back_populates="category")
70
+
71
+ class Product(Base):
72
+ """商品模型"""
73
+ __tablename__ = "products"
74
+
75
+ id = Column(Integer, primary_key=True, autoincrement=True)
76
+ productCode = Column(String(100), unique=True, nullable=False)
77
+ productName = Column(String(255), nullable=False)
78
+ unit = Column(String(50), nullable=False)
79
+ warehouse = Column(String(100))
80
+ unitWeight = Column(Float)
81
+ barcode = Column(String(100))
82
+ category_id = Column(Integer, ForeignKey("categories.id"))
83
+ stock = Column(Integer, default=0)
84
+ is_deleted = Column(Boolean, default=False)
85
+ deleted_at = Column(DateTime(timezone=True))
86
+ deleted_by = Column(Integer)
87
+ createdAt = Column(DateTime(timezone=True), server_default=func.now())
88
+ updatedAt = Column(DateTime(timezone=True), onupdate=func.now())
89
+
90
+ # 關聯
91
+ category = relationship("Category", back_populates="products")
92
+ purchase_order_items = relationship("PurchaseOrderItem", back_populates="product")
93
+ sales_order_items = relationship("SalesOrderItem", back_populates="product")
94
+
95
+ class Supplier(Base):
96
+ """供應商模型"""
97
+ __tablename__ = "suppliers"
98
+
99
+ id = Column(Integer, primary_key=True, autoincrement=True)
100
+ name = Column(String(255), nullable=False)
101
+ contactInfo = Column(Text)
102
+ address = Column(Text)
103
+
104
+ # 關聯
105
+ purchase_orders = relationship("PurchaseOrder", back_populates="supplier")
106
+
107
+ class Customer(Base):
108
+ """客戶模型"""
109
+ __tablename__ = "customers"
110
+
111
+ id = Column(Integer, primary_key=True, autoincrement=True)
112
+ customerType = Column(String(100))
113
+ salesPersonId = Column(String(100))
114
+ salesPersonName = Column(String(255))
115
+ customerCode = Column(String(100), unique=True, nullable=False)
116
+ customerName = Column(String(255), nullable=False)
117
+ contactPerson = Column(String(255))
118
+ invoiceTitle = Column(String(255))
119
+ taxId = Column(String(50))
120
+ phoneNumber = Column(String(50))
121
+ faxNumber = Column(String(50))
122
+ deliveryAddress = Column(Text)
123
+ businessHours = Column(String(255))
124
+ paymentMethod = Column(String(100))
125
+ paymentCategory = Column(String(100))
126
+ creditLimit = Column(Float, default=0.0)
127
+ createdDate = Column(DateTime(timezone=True), server_default=func.now())
128
+ updatedAt = Column(DateTime(timezone=True), onupdate=func.now())
129
+
130
+ # 關聯
131
+ sales_orders = relationship("SalesOrder", back_populates="customer")
132
+
133
+ class PurchaseOrder(Base):
134
+ """採購單模型"""
135
+ __tablename__ = "purchase_orders"
136
+
137
+ id = Column(Integer, primary_key=True, autoincrement=True)
138
+ po_number = Column(String(100), unique=True, nullable=False)
139
+ purchase_date = Column(Date, nullable=False)
140
+ expected_delivery_date = Column(Date)
141
+ supplier_id = Column(Integer, ForeignKey("suppliers.id"))
142
+ purchaser_id = Column(Integer, ForeignKey("users.id"))
143
+ status = Column(Enum(PurchaseOrderStatus), default=PurchaseOrderStatus.DRAFT)
144
+ subtotal = Column(Float, default=0.0)
145
+ tax_amount = Column(Float, default=0.0)
146
+ total_amount = Column(Float, default=0.0)
147
+ payment_status = Column(Enum(PaymentStatus), default=PaymentStatus.UNPAID)
148
+ notes = Column(Text)
149
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
150
+ updated_at = Column(DateTime(timezone=True), onupdate=func.now())
151
+
152
+ # 關聯
153
+ supplier = relationship("Supplier", back_populates="purchase_orders")
154
+ purchaser = relationship("User", back_populates="purchase_orders")
155
+ items = relationship("PurchaseOrderItem", back_populates="purchase_order")
156
+
157
+ class PurchaseOrderItem(Base):
158
+ """採購單明細模型"""
159
+ __tablename__ = "purchase_order_items"
160
+
161
+ id = Column(Integer, primary_key=True, autoincrement=True)
162
+ purchase_order_id = Column(Integer, ForeignKey("purchase_orders.id"))
163
+ product_id = Column(Integer, ForeignKey("products.id"))
164
+ quantity = Column(Integer, nullable=False)
165
+ unit_price = Column(Float, nullable=False)
166
+ line_total = Column(Float, nullable=False)
167
+ notes = Column(Text)
168
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
169
+ updated_at = Column(DateTime(timezone=True), onupdate=func.now())
170
+
171
+ # 關聯
172
+ purchase_order = relationship("PurchaseOrder", back_populates="items")
173
+ product = relationship("Product", back_populates="purchase_order_items")
174
+
175
+ class SalesOrder(Base):
176
+ """銷售單模型"""
177
+ __tablename__ = "sales_orders"
178
+
179
+ id = Column(Integer, primary_key=True, autoincrement=True)
180
+ so_number = Column(String(100), unique=True, nullable=False)
181
+ sales_date = Column(Date, nullable=False)
182
+ customer_id = Column(Integer, ForeignKey("customers.id"))
183
+ salesperson_id = Column(Integer, ForeignKey("users.id"))
184
+ status = Column(Enum(SalesOrderStatus), default=SalesOrderStatus.DRAFT)
185
+ subtotal = Column(Float, default=0.0)
186
+ tax_amount = Column(Float, default=0.0)
187
+ discount_amount = Column(Float, default=0.0)
188
+ total_amount = Column(Float, default=0.0)
189
+ notes = Column(Text)
190
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
191
+ updated_at = Column(DateTime(timezone=True), onupdate=func.now())
192
+
193
+ # 關聯
194
+ customer = relationship("Customer", back_populates="sales_orders")
195
+ salesperson = relationship("User", back_populates="sales_orders")
196
+ items = relationship("SalesOrderItem", back_populates="sales_order")
197
+
198
+ class SalesOrderItem(Base):
199
+ """銷售單明細模型"""
200
+ __tablename__ = "sales_order_items"
201
+
202
+ id = Column(Integer, primary_key=True, autoincrement=True)
203
+ sales_order_id = Column(Integer, ForeignKey("sales_orders.id"))
204
+ product_id = Column(Integer, ForeignKey("products.id"))
205
+ quantity = Column(Integer, nullable=False)
206
+ unit_price = Column(Float, nullable=False)
207
+ line_total = Column(Float, nullable=False)
208
+ notes = Column(Text)
209
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
210
+ updated_at = Column(DateTime(timezone=True), onupdate=func.now())
211
+
212
+ # 關聯
213
+ sales_order = relationship("SalesOrder", back_populates="items")
214
+ product = relationship("Product", back_populates="sales_order_items")
215
+
216
+ # LINE Bot 相關模型
217
+ class LineMessage(Base):
218
+ """LINE 訊息記錄模型"""
219
+ __tablename__ = "line_messages"
220
+
221
+ id = Column(Integer, primary_key=True, autoincrement=True)
222
+ user_id = Column(String(255))
223
+ message = Column(Text, nullable=False)
224
+ message_type = Column(String(50), default="text")
225
+ timestamp = Column(DateTime(timezone=True), server_default=func.now())
226
+ processed = Column(Boolean, default=False)
backend/main.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from fastapi import FastAPI, Request, HTTPException
3
+ from fastapi.middleware.cors import CORSMiddleware
4
+ from linebot import LineBotApi, WebhookHandler
5
+ from linebot.exceptions import InvalidSignatureError
6
+ from linebot.models import MessageEvent, TextMessage, TextSendMessage
7
+ import logging
8
+ from backend.services.nlp_service import NLPService
9
+ from backend.services.database_service import DatabaseService
10
+ from backend.config import settings
11
+ from backend.database.connection import test_database_connection
12
+
13
+ # 設定日誌
14
+ logging.basicConfig(level=logging.INFO)
15
+ logger = logging.getLogger(__name__)
16
+
17
+ app = FastAPI(title="LINE Bot API", version="1.0.0")
18
+
19
+ # CORS 設定
20
+ app.add_middleware(
21
+ CORSMiddleware,
22
+ allow_origins=["*"],
23
+ allow_credentials=True,
24
+ allow_methods=["*"],
25
+ allow_headers=["*"],
26
+ )
27
+
28
+ # LINE Bot 初始化
29
+ line_bot_api = LineBotApi(settings.LINE_CHANNEL_ACCESS_TOKEN)
30
+ handler = WebhookHandler(settings.LINE_CHANNEL_SECRET)
31
+
32
+ # 服務初始化
33
+ nlp_service = NLPService()
34
+ db_service = DatabaseService()
35
+
36
+ @app.get("/")
37
+ def greet_json():
38
+ return {"Hello": "LINE Bot API is running!"}
39
+
40
+ @app.get("/health")
41
+ def health_check():
42
+ # 檢查資料庫連線
43
+ db_status = "connected" if test_database_connection() else "disconnected"
44
+
45
+ return {
46
+ "status": "healthy",
47
+ "message": "LINE Bot API is operational",
48
+ "database": db_status,
49
+ "database_url": f"postgresql://{settings.DB_HOST}:{settings.DB_PORT}/{settings.DB_NAME}"
50
+ }
51
+
52
+ @app.post("/webhook")
53
+ async def callback(request: Request):
54
+ """LINE Bot Webhook 端點"""
55
+ signature = request.headers.get('X-Line-Signature', '')
56
+ body = await request.body()
57
+
58
+ try:
59
+ handler.handle(body.decode('utf-8'), signature)
60
+ except InvalidSignatureError:
61
+ logger.error("Invalid signature")
62
+ raise HTTPException(status_code=400, detail="Invalid signature")
63
+
64
+ return {"status": "ok"}
65
+
66
+ @handler.add(MessageEvent, message=TextMessage)
67
+ def handle_message(event):
68
+ """處理 LINE 訊息"""
69
+ try:
70
+ user_message = event.message.text
71
+ user_id = event.source.user_id
72
+
73
+ logger.info(f"收到訊息: {user_message} from {user_id}")
74
+
75
+ # 使用 NLP 服務分析訊息
76
+ analysis_result = nlp_service.analyze_message(user_message)
77
+
78
+ # 根據分析結果調用對應的資料庫方法
79
+ response_data = db_service.execute_query(analysis_result)
80
+
81
+ # 格式化回應訊息
82
+ reply_message = nlp_service.format_response(response_data, analysis_result, user_message)
83
+
84
+ # 回覆訊息
85
+ line_bot_api.reply_message(
86
+ event.reply_token,
87
+ TextSendMessage(text=reply_message)
88
+ )
89
+
90
+ except Exception as e:
91
+ logger.error(f"處理訊息時發生錯誤: {str(e)}")
92
+ line_bot_api.reply_message(
93
+ event.reply_token,
94
+ TextSendMessage(text="抱歉,處理您的訊息時發生錯誤,請稍後再試。")
95
+ )
96
+
97
+ if __name__ == "__main__":
98
+ import uvicorn
99
+ uvicorn.run(app, host="0.0.0.0", port=7860)
backend/models/__init__.py ADDED
File without changes
backend/models/schemas.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+ from typing import Optional, Dict, Any, List
3
+ from datetime import datetime
4
+ from enum import Enum
5
+
6
+ class QueryType(str, Enum):
7
+ """查詢類型枚舉"""
8
+ SEARCH = "search"
9
+ CREATE = "create"
10
+ UPDATE = "update"
11
+ DELETE = "delete"
12
+ ANALYTICS = "analytics"
13
+ UNKNOWN = "unknown"
14
+
15
+ class NLPAnalysisRequest(BaseModel):
16
+ """NLP 分析請求模型"""
17
+ message: str = Field(..., description="用戶輸入的訊息")
18
+ user_id: Optional[str] = Field(None, description="用戶ID")
19
+ context: Optional[Dict[str, Any]] = Field(default_factory=dict, description="上下文資訊")
20
+
21
+ class NLPAnalysisResult(BaseModel):
22
+ """NLP 分析結果模型"""
23
+ query_type: QueryType = Field(..., description="查詢類型")
24
+ intent: str = Field(..., description="用戶意圖")
25
+ entities: Dict[str, Any] = Field(default_factory=dict, description="提取的實體")
26
+ confidence: float = Field(..., ge=0.0, le=1.0, description="信心度")
27
+ parameters: Dict[str, Any] = Field(default_factory=dict, description="查詢參數")
28
+
29
+ class DatabaseQuery(BaseModel):
30
+ """資料庫查詢模型"""
31
+ table_name: str = Field(..., description="資料表名稱")
32
+ operation: str = Field(..., description="操作類型")
33
+ conditions: Optional[Dict[str, Any]] = Field(default_factory=dict, description="查詢條件")
34
+ data: Optional[Dict[str, Any]] = Field(default_factory=dict, description="要操作的資料")
35
+ limit: Optional[int] = Field(None, description="查詢限制數量")
36
+ offset: Optional[int] = Field(None, description="查詢偏移量")
37
+
38
+ class DatabaseResult(BaseModel):
39
+ """資料庫查詢結果模型"""
40
+ success: bool = Field(..., description="是否成功")
41
+ data: Optional[List[Dict[str, Any]]] = Field(default_factory=list, description="查詢結果資料")
42
+ count: Optional[int] = Field(None, description="結果數量")
43
+ error: Optional[str] = Field(None, description="錯誤訊息")
44
+
45
+ class LineMessage(BaseModel):
46
+ """LINE 訊息模型"""
47
+ user_id: str = Field(..., description="用戶ID")
48
+ message: str = Field(..., description="訊息內容")
49
+ timestamp: datetime = Field(default_factory=datetime.now, description="時間戳記")
50
+ message_type: str = Field(default="text", description="訊息類型")
51
+
52
+ class UserProfile(BaseModel):
53
+ """用戶資料模型"""
54
+ user_id: str = Field(..., description="用戶ID")
55
+ display_name: Optional[str] = Field(None, description="顯示名稱")
56
+ status_message: Optional[str] = Field(None, description="狀態訊息")
57
+ picture_url: Optional[str] = Field(None, description="頭像URL")
58
+ created_at: datetime = Field(default_factory=datetime.now, description="建立時間")
59
+ updated_at: datetime = Field(default_factory=datetime.now, description="更新時間")
60
+
61
+ class APIResponse(BaseModel):
62
+ """API 回應模型"""
63
+ success: bool = Field(..., description="是否成功")
64
+ message: str = Field(..., description="回應訊息")
65
+ data: Optional[Any] = Field(None, description="回應資料")
66
+ error: Optional[str] = Field(None, description="錯誤訊息")
backend/services/__init__.py ADDED
File without changes
backend/services/business_query_service.py ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 業務查詢服務 - 整合 NLP 分析和資料庫查詢
3
+ 用於處理 LINE 用戶的自然語言業務查詢
4
+ """
5
+
6
+ import logging
7
+ from typing import Dict, Any, Optional
8
+ from backend.services.nlp_service import NLPService
9
+ from backend.services.database_service import DatabaseService
10
+ from backend.models.schemas import DatabaseResult
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ class BusinessQueryService:
15
+ """業務查詢服務 - 整合自然語言處理和資料庫查詢"""
16
+
17
+ def __init__(self):
18
+ self.nlp_service = NLPService()
19
+ self.db_service = DatabaseService()
20
+
21
+ def process_user_query(self, user_message: str, user_id: str = None) -> Dict[str, Any]:
22
+ """
23
+ 處理用戶查詢的主要入口點
24
+
25
+ Args:
26
+ user_message: 用戶的自然語言查詢
27
+ user_id: 用戶ID(可選)
28
+
29
+ Returns:
30
+ 包含查詢結果和格式化回應的字典
31
+ """
32
+ try:
33
+ # 1. 使用 NLP 服務分析用戶查詢
34
+ analysis_result = self.nlp_service.analyze_business_query(user_message, user_id)
35
+
36
+ logger.info(f"NLP 分析結果 - 意圖: {analysis_result.intent}, 信心度: {analysis_result.confidence}")
37
+
38
+ # 2. 根據分析結果執行對應的資料庫查詢
39
+ db_result = self._execute_database_query(analysis_result)
40
+
41
+ # 3. 格式化回應訊息
42
+ response_message = self.nlp_service.format_response_message(db_result, analysis_result.intent)
43
+
44
+ # 4. 儲存查詢記錄(如果有用戶ID)
45
+ if user_id:
46
+ self.db_service.save_message(user_id, user_message, "query")
47
+
48
+ return {
49
+ "success": db_result.success,
50
+ "intent": analysis_result.intent,
51
+ "confidence": analysis_result.confidence,
52
+ "entities": analysis_result.entities,
53
+ "response_message": response_message,
54
+ "data": db_result.data,
55
+ "count": db_result.count,
56
+ "error": db_result.error
57
+ }
58
+
59
+ except Exception as e:
60
+ logger.error(f"處理用戶查詢時發生錯誤: {str(e)}")
61
+ return {
62
+ "success": False,
63
+ "intent": "unknown",
64
+ "confidence": 0.0,
65
+ "entities": {},
66
+ "response_message": f"抱歉,處理您的查詢時發生錯誤:{str(e)}",
67
+ "data": [],
68
+ "count": 0,
69
+ "error": str(e)
70
+ }
71
+
72
+ def _execute_database_query(self, analysis_result) -> DatabaseResult:
73
+ """根據 NLP 分析結果執行對應的資料庫查詢"""
74
+ try:
75
+ method_name = analysis_result.parameters.get("method")
76
+
77
+ if method_name == "search_products":
78
+ return self.db_service.search_products(
79
+ query_text=analysis_result.parameters.get("query_text"),
80
+ category=analysis_result.parameters.get("category"),
81
+ limit=analysis_result.parameters.get("limit", 10)
82
+ )
83
+
84
+ elif method_name == "check_inventory":
85
+ return self.db_service.check_inventory(
86
+ product_name=analysis_result.parameters.get("product_name"),
87
+ category=analysis_result.parameters.get("category")
88
+ )
89
+
90
+ elif method_name == "search_orders":
91
+ return self.db_service.search_orders(
92
+ user_id=analysis_result.parameters.get("user_id"),
93
+ status=analysis_result.parameters.get("status"),
94
+ limit=analysis_result.parameters.get("limit", 10)
95
+ )
96
+
97
+ elif method_name == "get_low_stock_products":
98
+ return self.db_service.get_low_stock_products(
99
+ threshold=analysis_result.parameters.get("threshold", 10)
100
+ )
101
+
102
+ elif method_name == "get_business_summary":
103
+ return self.db_service.get_business_summary()
104
+
105
+ else:
106
+ # 使用通用的自然語言查詢處理
107
+ return self.db_service.process_natural_language_query(
108
+ analysis_result.parameters.get("query_text", ""),
109
+ analysis_result.parameters.get("user_id")
110
+ )
111
+
112
+ except Exception as e:
113
+ logger.error(f"執行資料庫查詢時發生錯誤: {str(e)}")
114
+ return DatabaseResult(
115
+ success=False,
116
+ error=f"資料庫查詢失敗: {str(e)}"
117
+ )
118
+
119
+ def get_query_suggestions(self, user_id: str = None) -> List[str]:
120
+ """提供查詢建議"""
121
+ suggestions = [
122
+ "查詢商品 iPhone",
123
+ "庫存查詢 筆記型電腦",
124
+ "我的訂單",
125
+ "低庫存商品",
126
+ "業務統計摘要",
127
+ "查詢客戶資料",
128
+ "今天的銷售報表"
129
+ ]
130
+ return suggestions
131
+
132
+ def get_help_message(self) -> str:
133
+ """取得幫助訊息"""
134
+ help_text = """
135
+ 🤖 智能查詢助手使用說明:
136
+
137
+ 📦 商品查詢:
138
+ • "查詢商品 iPhone"
139
+ • "有什麼筆記型電腦"
140
+ • "商品價格查詢"
141
+
142
+ 📊 庫存查詢:
143
+ • "庫存查詢 iPhone"
144
+ • "筆記型電腦還有多少"
145
+ • "查詢存貨狀況"
146
+
147
+ 📋 訂單查詢:
148
+ • "我的訂單"
149
+ • "查詢訂單狀態"
150
+ • "訂單編號 ORD001"
151
+
152
+ ⚠️ 庫存警告:
153
+ • "低庫存商品"
154
+ • "缺貨商品查詢"
155
+
156
+ 📈 業務統計:
157
+ • "業務摘要"
158
+ • "統計報表"
159
+ • "總計資料"
160
+
161
+ 💡 您可以用自然語言提問,系統會智能理解您的需求!
162
+ """
163
+ return help_text.strip()
164
+
165
+ def validate_user_permissions(self, user_id: str, query_type: str) -> bool:
166
+ """驗證用戶權限(可根據需要擴展)"""
167
+ # 這裡可以根據用戶角色和查詢類型來驗證權限
168
+ # 目前先返回 True,允許所有查詢
169
+ return True
backend/services/database_service.py ADDED
@@ -0,0 +1,765 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from typing import Dict, Any, List, Optional
3
+ from sqlalchemy.orm import Session
4
+ from sqlalchemy import text, and_, or_
5
+ from backend.models.schemas import NLPAnalysisResult, DatabaseResult, QueryType
6
+ from backend.config import settings
7
+ from backend.database.connection import get_database_session, close_database_session
8
+ from backend.database.models import User, Product, Order, OrderItem, LineMessage, UserSession
9
+ import logging
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ class DatabaseService:
14
+ """資料庫服務"""
15
+
16
+ def __init__(self):
17
+ self.model_mapping = {
18
+ "users": User,
19
+ "products": Product,
20
+ "orders": Order,
21
+ "order_items": OrderItem,
22
+ "line_messages": LineMessage,
23
+ "user_sessions": UserSession
24
+ }
25
+
26
+ # 定義資料表結構
27
+ self.table_schemas = {
28
+ "users": {
29
+ "model": User,
30
+ "searchable": ["name", "email", "display_name"]
31
+ },
32
+ "orders": {
33
+ "model": Order,
34
+ "searchable": ["order_id", "status"]
35
+ },
36
+ "products": {
37
+ "model": Product,
38
+ "searchable": ["name", "category", "description"]
39
+ },
40
+ "line_messages": {
41
+ "model": LineMessage,
42
+ "searchable": ["message", "user_id"]
43
+ },
44
+ "order_items": {
45
+ "model": OrderItem,
46
+ "searchable": []
47
+ },
48
+ "user_sessions": {
49
+ "model": UserSession,
50
+ "searchable": ["user_id"]
51
+ }
52
+ }
53
+
54
+ def execute_query(self, analysis_result: NLPAnalysisResult) -> DatabaseResult:
55
+ """執行資料庫查詢"""
56
+ try:
57
+ query_type = analysis_result.query_type
58
+ parameters = analysis_result.parameters
59
+
60
+ if query_type == QueryType.SEARCH:
61
+ return self._execute_search(parameters)
62
+ elif query_type == QueryType.CREATE:
63
+ return self._execute_create(parameters)
64
+ elif query_type == QueryType.UPDATE:
65
+ return self._execute_update(parameters)
66
+ elif query_type == QueryType.DELETE:
67
+ return self._execute_delete(parameters)
68
+ elif query_type == QueryType.ANALYTICS:
69
+ return self._execute_analytics(parameters)
70
+ else:
71
+ return DatabaseResult(
72
+ success=False,
73
+ error="不支援的查詢類型"
74
+ )
75
+
76
+ except Exception as e:
77
+ logger.error(f"資料庫查詢錯誤: {str(e)}")
78
+ return DatabaseResult(
79
+ success=False,
80
+ error=f"資料庫查詢失敗: {str(e)}"
81
+ )
82
+
83
+ def _execute_search(self, parameters: Dict[str, Any]) -> DatabaseResult:
84
+ """執行搜尋查詢"""
85
+ table_name = parameters.get("table", "users")
86
+ conditions = parameters.get("conditions", {})
87
+ limit = parameters.get("limit", 10)
88
+ offset = parameters.get("offset", 0)
89
+
90
+ db = None
91
+ try:
92
+ db = get_database_session()
93
+
94
+ # 取得對應的模型
95
+ model = self.table_schemas.get(table_name, {}).get("model")
96
+ if not model:
97
+ return DatabaseResult(
98
+ success=False,
99
+ error=f"不支援的資料表: {table_name}"
100
+ )
101
+
102
+ # 建立查詢
103
+ query = db.query(model)
104
+
105
+ # 添加條件
106
+ for field, value in conditions.items():
107
+ if hasattr(model, field):
108
+ column = getattr(model, field)
109
+
110
+ if isinstance(value, dict):
111
+ # 處理範圍查詢
112
+ if "gte" in value:
113
+ query = query.filter(column >= value["gte"])
114
+ if "lte" in value:
115
+ query = query.filter(column <= value["lte"])
116
+ if "gt" in value:
117
+ query = query.filter(column > value["gt"])
118
+ if "lt" in value:
119
+ query = query.filter(column < value["lt"])
120
+ else:
121
+ # 處理精確匹配或模糊搜尋
122
+ searchable_fields = self.table_schemas.get(table_name, {}).get("searchable", [])
123
+ if field in searchable_fields:
124
+ query = query.filter(column.ilike(f"%{value}%"))
125
+ else:
126
+ query = query.filter(column == value)
127
+
128
+ # 添加限制和偏移
129
+ query = query.offset(offset).limit(limit)
130
+
131
+ # 執行查詢
132
+ results = query.all()
133
+
134
+ # 轉換為字典格式
135
+ data = []
136
+ for result in results:
137
+ item = {}
138
+ for column in result.__table__.columns:
139
+ value = getattr(result, column.name)
140
+ # 處理特殊類型
141
+ if hasattr(value, 'isoformat'): # datetime
142
+ item[column.name] = value.isoformat()
143
+ elif isinstance(value, (int, float, str, bool)) or value is None:
144
+ item[column.name] = value
145
+ else:
146
+ item[column.name] = str(value)
147
+ data.append(item)
148
+
149
+ return DatabaseResult(
150
+ success=True,
151
+ data=data,
152
+ count=len(data)
153
+ )
154
+
155
+ except Exception as e:
156
+ logger.error(f"搜尋查詢錯誤: {str(e)}")
157
+ return DatabaseResult(
158
+ success=False,
159
+ error=f"搜尋失敗: {str(e)}"
160
+ )
161
+ finally:
162
+ if db:
163
+ close_database_session(db)
164
+
165
+ def _execute_create(self, parameters: Dict[str, Any]) -> DatabaseResult:
166
+ """執行建立查詢"""
167
+ table_name = parameters.get("table", "users")
168
+ data = parameters.get("data", {})
169
+
170
+ db = None
171
+ try:
172
+ db = get_database_session()
173
+
174
+ # 取得對應的模型
175
+ model = self.table_schemas.get(table_name, {}).get("model")
176
+ if not model:
177
+ return DatabaseResult(
178
+ success=False,
179
+ error=f"不支援的資料表: {table_name}"
180
+ )
181
+
182
+ # 建立新記錄
183
+ new_record = model(**data)
184
+ db.add(new_record)
185
+ db.commit()
186
+ db.refresh(new_record)
187
+
188
+ # 轉換為字典格式
189
+ result_data = {}
190
+ for column in new_record.__table__.columns:
191
+ value = getattr(new_record, column.name)
192
+ if hasattr(value, 'isoformat'): # datetime
193
+ result_data[column.name] = value.isoformat()
194
+ elif isinstance(value, (int, float, str, bool)) or value is None:
195
+ result_data[column.name] = value
196
+ else:
197
+ result_data[column.name] = str(value)
198
+
199
+ return DatabaseResult(
200
+ success=True,
201
+ data=[result_data],
202
+ count=1
203
+ )
204
+
205
+ except Exception as e:
206
+ if db:
207
+ db.rollback()
208
+ logger.error(f"建立查詢錯誤: {str(e)}")
209
+ return DatabaseResult(
210
+ success=False,
211
+ error=f"建立失敗: {str(e)}"
212
+ )
213
+ finally:
214
+ if db:
215
+ close_database_session(db)
216
+
217
+ def _execute_update(self, parameters: Dict[str, Any]) -> DatabaseResult:
218
+ """執行更新查詢"""
219
+ table_name = parameters.get("table", "users")
220
+ conditions = parameters.get("conditions", {})
221
+ data = parameters.get("data", {})
222
+
223
+ db = None
224
+ try:
225
+ db = get_database_session()
226
+
227
+ # 取得對應的模型
228
+ model = self.table_schemas.get(table_name, {}).get("model")
229
+ if not model:
230
+ return DatabaseResult(
231
+ success=False,
232
+ error=f"不支援的資料表: {table_name}"
233
+ )
234
+
235
+ # 建立查詢
236
+ query = db.query(model)
237
+
238
+ # 添加條件
239
+ for field, value in conditions.items():
240
+ if hasattr(model, field):
241
+ column = getattr(model, field)
242
+ query = query.filter(column == value)
243
+
244
+ # 執行更新
245
+ updated_count = query.update(data)
246
+ db.commit()
247
+
248
+ # 取得更新後的資料
249
+ updated_records = query.all()
250
+
251
+ # 轉換為字典格式
252
+ result_data = []
253
+ for record in updated_records:
254
+ item = {}
255
+ for column in record.__table__.columns:
256
+ value = getattr(record, column.name)
257
+ if hasattr(value, 'isoformat'): # datetime
258
+ item[column.name] = value.isoformat()
259
+ elif isinstance(value, (int, float, str, bool)) or value is None:
260
+ item[column.name] = value
261
+ else:
262
+ item[column.name] = str(value)
263
+ result_data.append(item)
264
+
265
+ return DatabaseResult(
266
+ success=True,
267
+ data=result_data,
268
+ count=updated_count
269
+ )
270
+
271
+ except Exception as e:
272
+ if db:
273
+ db.rollback()
274
+ logger.error(f"更新查詢錯誤: {str(e)}")
275
+ return DatabaseResult(
276
+ success=False,
277
+ error=f"更新失敗: {str(e)}"
278
+ )
279
+ finally:
280
+ if db:
281
+ close_database_session(db)
282
+
283
+ def _execute_delete(self, parameters: Dict[str, Any]) -> DatabaseResult:
284
+ """執行刪除查詢"""
285
+ table_name = parameters.get("table", "users")
286
+ conditions = parameters.get("conditions", {})
287
+
288
+ db = None
289
+ try:
290
+ db = get_database_session()
291
+
292
+ # 取得對應的模型
293
+ model = self.table_schemas.get(table_name, {}).get("model")
294
+ if not model:
295
+ return DatabaseResult(
296
+ success=False,
297
+ error=f"不支援的資料表: {table_name}"
298
+ )
299
+
300
+ # 建立查詢
301
+ query = db.query(model)
302
+
303
+ # 添加條件
304
+ for field, value in conditions.items():
305
+ if hasattr(model, field):
306
+ column = getattr(model, field)
307
+ query = query.filter(column == value)
308
+
309
+ # 先取得要刪除的記錄
310
+ records_to_delete = query.all()
311
+
312
+ # 轉換為字典格式
313
+ result_data = []
314
+ for record in records_to_delete:
315
+ item = {}
316
+ for column in record.__table__.columns:
317
+ value = getattr(record, column.name)
318
+ if hasattr(value, 'isoformat'): # datetime
319
+ item[column.name] = value.isoformat()
320
+ elif isinstance(value, (int, float, str, bool)) or value is None:
321
+ item[column.name] = value
322
+ else:
323
+ item[column.name] = str(value)
324
+ result_data.append(item)
325
+
326
+ # 執行刪除
327
+ deleted_count = query.delete()
328
+ db.commit()
329
+
330
+ return DatabaseResult(
331
+ success=True,
332
+ data=result_data,
333
+ count=deleted_count
334
+ )
335
+
336
+ except Exception as e:
337
+ if db:
338
+ db.rollback()
339
+ logger.error(f"刪除查詢錯誤: {str(e)}")
340
+ return DatabaseResult(
341
+ success=False,
342
+ error=f"刪除失敗: {str(e)}"
343
+ )
344
+ finally:
345
+ if db:
346
+ close_database_session(db)
347
+
348
+ def _execute_analytics(self, parameters: Dict[str, Any]) -> DatabaseResult:
349
+ """執行分析查詢"""
350
+ table_name = parameters.get("table", "users")
351
+
352
+ db = None
353
+ try:
354
+ db = get_database_session()
355
+
356
+ # 取得對應的模型
357
+ model = self.table_schemas.get(table_name, {}).get("model")
358
+ if not model:
359
+ return DatabaseResult(
360
+ success=False,
361
+ error=f"不支援的資料表: {table_name}"
362
+ )
363
+
364
+ # 執行計數查詢
365
+ total_count = db.query(model).count()
366
+
367
+ # 取得範例資料
368
+ sample_records = db.query(model).limit(5).all()
369
+
370
+ # 轉換範例資料為字典格式
371
+ sample_data = []
372
+ for record in sample_records:
373
+ item = {}
374
+ for column in record.__table__.columns:
375
+ value = getattr(record, column.name)
376
+ if hasattr(value, 'isoformat'): # datetime
377
+ item[column.name] = value.isoformat()
378
+ elif isinstance(value, (int, float, str, bool)) or value is None:
379
+ item[column.name] = value
380
+ else:
381
+ item[column.name] = str(value)
382
+ sample_data.append(item)
383
+
384
+ analytics_data = [{
385
+ "table": table_name,
386
+ "count": total_count,
387
+ "data_sample": sample_data
388
+ }]
389
+
390
+ return DatabaseResult(
391
+ success=True,
392
+ data=analytics_data,
393
+ count=1
394
+ )
395
+
396
+ except Exception as e:
397
+ logger.error(f"分析查詢錯誤: {str(e)}")
398
+ return DatabaseResult(
399
+ success=False,
400
+ error=f"分析失敗: {str(e)}"
401
+ )
402
+ finally:
403
+ if db:
404
+ close_database_session(db)
405
+
406
+ def save_message(self, user_id: str, message: str, message_type: str = "text") -> bool:
407
+ """儲存訊息記錄"""
408
+ db = None
409
+ try:
410
+ db = get_database_session()
411
+
412
+ new_message = LineMessage(
413
+ user_id=user_id,
414
+ message=message,
415
+ message_type=message_type
416
+ )
417
+
418
+ db.add(new_message)
419
+ db.commit()
420
+ return True
421
+
422
+ except Exception as e:
423
+ if db:
424
+ db.rollback()
425
+ logger.error(f"儲存訊息錯誤: {str(e)}")
426
+ return False
427
+ finally:
428
+ if db:
429
+ close_database_session(db)
430
+
431
+ def get_user_profile(self, user_id: str) -> Optional[Dict[str, Any]]:
432
+ """取得用戶資料"""
433
+ db = None
434
+ try:
435
+ db = get_database_session()
436
+
437
+ user = db.query(User).filter(User.user_id == user_id).first()
438
+ if not user:
439
+ return None
440
+
441
+ # 轉換為字典格式
442
+ user_data = {}
443
+ for column in user.__table__.columns:
444
+ value = getattr(user, column.name)
445
+ if hasattr(value, 'isoformat'): # datetime
446
+ user_data[column.name] = value.isoformat()
447
+ elif isinstance(value, (int, float, str, bool)) or value is None:
448
+ user_data[column.name] = value
449
+ else:
450
+ user_data[column.name] = str(value)
451
+
452
+ return user_data
453
+
454
+ except Exception as e:
455
+ logger.error(f"取得用戶資料錯誤: {str(e)}")
456
+ return None
457
+ finally:
458
+ if db:
459
+ close_database_session(db)
460
+
461
+ def create_user_profile(self, user_data: Dict[str, Any]) -> bool:
462
+ """建立用戶資料"""
463
+ db = None
464
+ try:
465
+ db = get_database_session()
466
+
467
+ new_user = User(**user_data)
468
+ db.add(new_user)
469
+ db.commit()
470
+ return True
471
+
472
+ except Exception as e:
473
+ if db:
474
+ db.rollback()
475
+ logger.error(f"建立用戶資料錯誤: {str(e)}")
476
+ return False
477
+ finally:
478
+ if db:
479
+ close_database_session(db)
480
+
481
+ # ==================== 業務專用查詢方法 ====================
482
+
483
+ def search_products(self, query_text: str = None, category: str = None,
484
+ warehouse: str = None, limit: int = 10) -> DatabaseResult:
485
+ """商品查詢 - 支援自然語言查詢"""
486
+ db = None
487
+ try:
488
+ db = get_database_session()
489
+
490
+ # 建立基本查詢
491
+ query = db.query(Product)
492
+
493
+ # 如果有查詢文字,進行模糊搜尋
494
+ if query_text:
495
+ search_filter = or_(
496
+ Product.name.ilike(f"%{query_text}%"),
497
+ Product.description.ilike(f"%{query_text}%")
498
+ )
499
+ query = query.filter(search_filter)
500
+
501
+ # 類別篩選
502
+ if category:
503
+ query = query.filter(Product.category.ilike(f"%{category}%"))
504
+
505
+ # 執行查詢
506
+ products = query.limit(limit).all()
507
+
508
+ # 轉換為字典格式
509
+ data = []
510
+ for product in products:
511
+ product_data = {
512
+ "id": product.id,
513
+ "name": product.name,
514
+ "description": product.description,
515
+ "price": float(product.price) if product.price else 0.0,
516
+ "category": product.category,
517
+ "stock": product.stock if hasattr(product, 'stock') else 0,
518
+ "created_at": product.created_at.isoformat() if product.created_at else None
519
+ }
520
+ data.append(product_data)
521
+
522
+ return DatabaseResult(
523
+ success=True,
524
+ data=data,
525
+ count=len(data)
526
+ )
527
+
528
+ except Exception as e:
529
+ logger.error(f"商品查詢錯誤: {str(e)}")
530
+ return DatabaseResult(
531
+ success=False,
532
+ error=f"商品查詢失敗: {str(e)}"
533
+ )
534
+ finally:
535
+ if db:
536
+ close_database_session(db)
537
+
538
+ def check_inventory(self, product_name: str = None, category: str = None) -> DatabaseResult:
539
+ """庫存查詢"""
540
+ db = None
541
+ try:
542
+ db = get_database_session()
543
+
544
+ # 建立查詢
545
+ query = db.query(Product)
546
+
547
+ # 根據商品名稱查詢
548
+ if product_name:
549
+ query = query.filter(Product.name.ilike(f"%{product_name}%"))
550
+
551
+ # 類別篩選
552
+ if category:
553
+ query = query.filter(Product.category.ilike(f"%{category}%"))
554
+
555
+ products = query.all()
556
+
557
+ # 準備庫存資料
558
+ inventory_data = []
559
+ for product in products:
560
+ stock_info = {
561
+ "product_name": product.name,
562
+ "category": product.category,
563
+ "current_stock": product.stock if hasattr(product, 'stock') else 0,
564
+ "price": float(product.price) if product.price else 0.0,
565
+ "description": product.description,
566
+ "last_updated": product.updated_at.isoformat() if product.updated_at else None
567
+ }
568
+ inventory_data.append(stock_info)
569
+
570
+ return DatabaseResult(
571
+ success=True,
572
+ data=inventory_data,
573
+ count=len(inventory_data)
574
+ )
575
+
576
+ except Exception as e:
577
+ logger.error(f"庫存查詢錯誤: {str(e)}")
578
+ return DatabaseResult(
579
+ success=False,
580
+ error=f"庫存查詢失敗: {str(e)}"
581
+ )
582
+ finally:
583
+ if db:
584
+ close_database_session(db)
585
+
586
+ def search_orders(self, user_id: str = None, status: str = None, limit: int = 10) -> DatabaseResult:
587
+ """訂單查詢"""
588
+ db = None
589
+ try:
590
+ db = get_database_session()
591
+
592
+ query = db.query(Order)
593
+
594
+ # 用戶篩選
595
+ if user_id:
596
+ query = query.filter(Order.user_id == user_id)
597
+
598
+ # 狀態篩選
599
+ if status:
600
+ query = query.filter(Order.status.ilike(f"%{status}%"))
601
+
602
+ orders = query.limit(limit).all()
603
+
604
+ # 轉換為字典格式
605
+ order_data = []
606
+ for order in orders:
607
+ order_info = {
608
+ "order_id": order.order_id,
609
+ "user_id": order.user_id,
610
+ "status": order.status,
611
+ "total_amount": float(order.total_amount) if order.total_amount else 0.0,
612
+ "created_at": order.created_at.isoformat() if order.created_at else None,
613
+ "updated_at": order.updated_at.isoformat() if order.updated_at else None
614
+ }
615
+ order_data.append(order_info)
616
+
617
+ return DatabaseResult(
618
+ success=True,
619
+ data=order_data,
620
+ count=len(order_data)
621
+ )
622
+
623
+ except Exception as e:
624
+ logger.error(f"訂單查詢錯誤: {str(e)}")
625
+ return DatabaseResult(
626
+ success=False,
627
+ error=f"訂單查詢失敗: {str(e)}"
628
+ )
629
+ finally:
630
+ if db:
631
+ close_database_session(db)
632
+
633
+ def get_low_stock_products(self, threshold: int = 10) -> DatabaseResult:
634
+ """低庫存商品查詢"""
635
+ db = None
636
+ try:
637
+ db = get_database_session()
638
+
639
+ # 查詢庫存低於閾值的商品
640
+ # 注意:這裡假設 Product 模型有 stock 欄位,需要根據實際情況調整
641
+ query = db.query(Product)
642
+ if hasattr(Product, 'stock'):
643
+ query = query.filter(Product.stock <= threshold)
644
+
645
+ low_stock_products = query.all()
646
+
647
+ data = []
648
+ for product in low_stock_products:
649
+ product_data = {
650
+ "product_name": product.name,
651
+ "current_stock": product.stock if hasattr(product, 'stock') else 0,
652
+ "category": product.category,
653
+ "price": float(product.price) if product.price else 0.0,
654
+ "status": "庫存不足"
655
+ }
656
+ data.append(product_data)
657
+
658
+ return DatabaseResult(
659
+ success=True,
660
+ data=data,
661
+ count=len(data)
662
+ )
663
+
664
+ except Exception as e:
665
+ logger.error(f"低庫存查詢錯誤: {str(e)}")
666
+ return DatabaseResult(
667
+ success=False,
668
+ error=f"低庫存查詢失敗: {str(e)}"
669
+ )
670
+ finally:
671
+ if db:
672
+ close_database_session(db)
673
+
674
+ def get_business_summary(self) -> DatabaseResult:
675
+ """業務摘要統計"""
676
+ db = None
677
+ try:
678
+ db = get_database_session()
679
+
680
+ # 基本統計
681
+ total_products = db.query(Product).count()
682
+ total_orders = db.query(Order).count()
683
+ total_users = db.query(User).count()
684
+
685
+ # 計算低庫存商品數量
686
+ low_stock_count = 0
687
+ if hasattr(Product, 'stock'):
688
+ low_stock_count = db.query(Product).filter(Product.stock <= 10).count()
689
+
690
+ # 準備摘要資料
691
+ summary_data = [{
692
+ "total_products": total_products,
693
+ "total_orders": total_orders,
694
+ "total_users": total_users,
695
+ "low_stock_items": low_stock_count,
696
+ "report_date": datetime.now().isoformat()
697
+ }]
698
+
699
+ return DatabaseResult(
700
+ success=True,
701
+ data=summary_data,
702
+ count=1
703
+ )
704
+
705
+ except Exception as e:
706
+ logger.error(f"業務摘要查詢錯誤: {str(e)}")
707
+ return DatabaseResult(
708
+ success=False,
709
+ error=f"業務摘要查詢失敗: {str(e)}"
710
+ )
711
+ finally:
712
+ if db:
713
+ close_database_session(db)
714
+
715
+ def process_natural_language_query(self, user_message: str, user_id: str = None) -> DatabaseResult:
716
+ """處理自然語言查詢的統一入口"""
717
+ try:
718
+ # 簡單的關鍵字匹配來判斷查詢意圖
719
+ message_lower = user_message.lower()
720
+
721
+ # 商品查詢
722
+ if any(keyword in message_lower for keyword in ['商品', '產品', '貨品', '物品']):
723
+ if any(keyword in message_lower for keyword in ['庫存', '存貨', '剩餘']):
724
+ # 庫存查詢
725
+ product_name = self._extract_product_name(user_message)
726
+ return self.check_inventory(product_name=product_name)
727
+ else:
728
+ # 一般商品查詢
729
+ product_name = self._extract_product_name(user_message)
730
+ return self.search_products(query_text=product_name)
731
+
732
+ # 訂單查詢
733
+ elif any(keyword in message_lower for keyword in ['訂單', '訂購', '購買']):
734
+ return self.search_orders(user_id=user_id)
735
+
736
+ # 低庫存查詢
737
+ elif any(keyword in message_lower for keyword in ['低庫存', '缺貨', '不足']):
738
+ return self.get_low_stock_products()
739
+
740
+ # 統計查詢
741
+ elif any(keyword in message_lower for keyword in ['統計', '摘要', '總計', '報表']):
742
+ return self.get_business_summary()
743
+
744
+ else:
745
+ # 預設進行商品搜尋
746
+ return self.search_products(query_text=user_message)
747
+
748
+ except Exception as e:
749
+ logger.error(f"自然語言查詢處理錯誤: {str(e)}")
750
+ return DatabaseResult(
751
+ success=False,
752
+ error=f"查詢處理失敗: {str(e)}"
753
+ )
754
+
755
+ def _extract_product_name(self, message: str) -> str:
756
+ """從訊息中提取商品名稱"""
757
+ # 簡單的商品名稱提取邏輯
758
+ # 移除常見的查詢關鍵字
759
+ keywords_to_remove = ['查詢', '搜尋', '找', '商品', '產品', '庫存', '有沒有', '請問']
760
+
761
+ cleaned_message = message
762
+ for keyword in keywords_to_remove:
763
+ cleaned_message = cleaned_message.replace(keyword, '')
764
+
765
+ return cleaned_message.strip()
backend/services/line_bot_service.py ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ LINE Bot 服務 - 整合業務查詢功能
3
+ 處理來自 LINE 官方帳號的用戶訊息
4
+ """
5
+
6
+ import logging
7
+ from typing import Dict, Any, Optional
8
+ from backend.services.business_query_service import BusinessQueryService
9
+ from backend.services.database_service import DatabaseService
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ class LineBotService:
14
+ """LINE Bot 服務類"""
15
+
16
+ def __init__(self):
17
+ self.business_query_service = BusinessQueryService()
18
+ self.db_service = DatabaseService()
19
+
20
+ def handle_text_message(self, user_id: str, message_text: str, display_name: str = None) -> Dict[str, Any]:
21
+ """
22
+ 處理 LINE 用戶的文字訊息
23
+
24
+ Args:
25
+ user_id: LINE 用戶ID
26
+ message_text: 用戶發送的訊息內容
27
+ display_name: 用戶顯示名稱(可選)
28
+
29
+ Returns:
30
+ 包含回應訊息和相關資料的字典
31
+ """
32
+ try:
33
+ # 記錄用戶訊息
34
+ logger.info(f"收到來自用戶 {user_id} 的訊息: {message_text}")
35
+
36
+ # 檢查是否為特殊指令
37
+ if message_text.lower() in ['help', '幫助', '說明', '指令']:
38
+ return self._handle_help_command(user_id)
39
+
40
+ elif message_text.lower() in ['menu', '選單', '功能']:
41
+ return self._handle_menu_command(user_id)
42
+
43
+ # 確保用戶資料存在
44
+ self._ensure_user_profile(user_id, display_name)
45
+
46
+ # 處理業務查詢
47
+ query_result = self.business_query_service.process_user_query(message_text, user_id)
48
+
49
+ # 準備回應
50
+ response = {
51
+ "type": "text",
52
+ "text": query_result["response_message"],
53
+ "user_id": user_id,
54
+ "success": query_result["success"],
55
+ "intent": query_result["intent"],
56
+ "confidence": query_result["confidence"]
57
+ }
58
+
59
+ # 如果查詢成功且有資料,可以添加快速回覆按鈕
60
+ if query_result["success"] and query_result["data"]:
61
+ response["quick_reply"] = self._generate_quick_reply_buttons(query_result["intent"])
62
+
63
+ return response
64
+
65
+ except Exception as e:
66
+ logger.error(f"處理 LINE 訊息時發生錯誤: {str(e)}")
67
+ return {
68
+ "type": "text",
69
+ "text": "抱歉,系統暫時無法處理您的請求,請稍後再試。",
70
+ "user_id": user_id,
71
+ "success": False,
72
+ "error": str(e)
73
+ }
74
+
75
+ def _handle_help_command(self, user_id: str) -> Dict[str, Any]:
76
+ """處理幫助指令"""
77
+ help_message = self.business_query_service.get_help_message()
78
+
79
+ return {
80
+ "type": "text",
81
+ "text": help_message,
82
+ "user_id": user_id,
83
+ "success": True,
84
+ "intent": "help"
85
+ }
86
+
87
+ def _handle_menu_command(self, user_id: str) -> Dict[str, Any]:
88
+ """處理選單指令"""
89
+ menu_message = """
90
+ 🏪 智能查詢系統主選單
91
+
92
+ 請選擇您需要的功能:
93
+
94
+ 1️⃣ 商品查詢
95
+ 輸入:查詢商品 [商品名稱]
96
+
97
+ 2️⃣ 庫存查詢
98
+ 輸入:庫存查詢 [商品名稱]
99
+
100
+ 3️⃣ 訂單查詢
101
+ 輸入:我的訂單
102
+
103
+ 4️⃣ 低庫存警告
104
+ 輸入:低庫存商品
105
+
106
+ 5️⃣ 業務統計
107
+ 輸入:業務摘要
108
+
109
+ 💡 您也可以直接用自然語言提問!
110
+ 例如:「iPhone 還有多少庫存?」
111
+
112
+ 輸入「幫助」查看詳細說明
113
+ """
114
+
115
+ return {
116
+ "type": "text",
117
+ "text": menu_message.strip(),
118
+ "user_id": user_id,
119
+ "success": True,
120
+ "intent": "menu",
121
+ "quick_reply": {
122
+ "items": [
123
+ {"type": "action", "action": {"type": "message", "label": "商品查詢", "text": "查詢商品"}},
124
+ {"type": "action", "action": {"type": "message", "label": "庫存查詢", "text": "庫存查詢"}},
125
+ {"type": "action", "action": {"type": "message", "label": "我的訂單", "text": "我的訂單"}},
126
+ {"type": "action", "action": {"type": "message", "label": "低庫存", "text": "低庫存商品"}},
127
+ {"type": "action", "action": {"type": "message", "label": "業務統計", "text": "業務摘要"}}
128
+ ]
129
+ }
130
+ }
131
+
132
+ def _ensure_user_profile(self, user_id: str, display_name: str = None):
133
+ """確保用戶資料存在於資料庫中"""
134
+ try:
135
+ # 檢查用戶是否已存在
136
+ existing_user = self.db_service.get_user_profile(user_id)
137
+
138
+ if not existing_user:
139
+ # 建立新用戶資料
140
+ user_data = {
141
+ "user_id": user_id,
142
+ "display_name": display_name or f"用戶_{user_id[:8]}",
143
+ "created_at": "now()"
144
+ }
145
+
146
+ success = self.db_service.create_user_profile(user_data)
147
+ if success:
148
+ logger.info(f"已建立新用戶資料: {user_id}")
149
+ else:
150
+ logger.warning(f"建立用戶資料失敗: {user_id}")
151
+
152
+ except Exception as e:
153
+ logger.error(f"處理用戶資料時發生錯誤: {str(e)}")
154
+
155
+ def _generate_quick_reply_buttons(self, intent: str) -> Dict[str, Any]:
156
+ """根據查詢意圖生成快速回覆按鈕"""
157
+
158
+ if intent == "product_search":
159
+ return {
160
+ "items": [
161
+ {"type": "action", "action": {"type": "message", "label": "查看庫存", "text": "庫存查詢"}},
162
+ {"type": "action", "action": {"type": "message", "label": "相關商品", "text": "查詢商品"}},
163
+ {"type": "action", "action": {"type": "message", "label": "主選單", "text": "選單"}}
164
+ ]
165
+ }
166
+
167
+ elif intent == "inventory_check":
168
+ return {
169
+ "items": [
170
+ {"type": "action", "action": {"type": "message", "label": "低庫存警告", "text": "低庫存商品"}},
171
+ {"type": "action", "action": {"type": "message", "label": "商品詳情", "text": "查詢商品"}},
172
+ {"type": "action", "action": {"type": "message", "label": "主選單", "text": "選單"}}
173
+ ]
174
+ }
175
+
176
+ elif intent == "order_search":
177
+ return {
178
+ "items": [
179
+ {"type": "action", "action": {"type": "message", "label": "業務統計", "text": "業務摘要"}},
180
+ {"type": "action", "action": {"type": "message", "label": "主選單", "text": "選單"}}
181
+ ]
182
+ }
183
+
184
+ else:
185
+ return {
186
+ "items": [
187
+ {"type": "action", "action": {"type": "message", "label": "商品查詢", "text": "查詢商品"}},
188
+ {"type": "action", "action": {"type": "message", "label": "庫存查詢", "text": "庫存查詢"}},
189
+ {"type": "action", "action": {"type": "message", "label": "主選單", "text": "選單"}}
190
+ ]
191
+ }
192
+
193
+ def handle_postback_action(self, user_id: str, postback_data: str) -> Dict[str, Any]:
194
+ """處理 Postback 動作(按鈕點擊等)"""
195
+ try:
196
+ # 解析 postback 資料
197
+ if postback_data.startswith("query_"):
198
+ query_type = postback_data.replace("query_", "")
199
+
200
+ if query_type == "products":
201
+ return self.handle_text_message(user_id, "查詢商品")
202
+ elif query_type == "inventory":
203
+ return self.handle_text_message(user_id, "庫存查詢")
204
+ elif query_type == "orders":
205
+ return self.handle_text_message(user_id, "我的訂單")
206
+ elif query_type == "summary":
207
+ return self.handle_text_message(user_id, "業務摘要")
208
+
209
+ # 預設回應
210
+ return self._handle_menu_command(user_id)
211
+
212
+ except Exception as e:
213
+ logger.error(f"處理 Postback 動作時發生錯誤: {str(e)}")
214
+ return {
215
+ "type": "text",
216
+ "text": "抱歉,無法處理此操作。",
217
+ "user_id": user_id,
218
+ "success": False,
219
+ "error": str(e)
220
+ }
221
+
222
+ def get_user_activity_summary(self, user_id: str, days: int = 7) -> Dict[str, Any]:
223
+ """取得用戶活動摘要"""
224
+ try:
225
+ # 這裡可以實作用戶活動統計
226
+ # 例如:查詢次數、常用功能等
227
+
228
+ summary = {
229
+ "user_id": user_id,
230
+ "period_days": days,
231
+ "total_queries": 0, # 從資料庫查詢
232
+ "most_used_features": [], # 最常使用的功能
233
+ "last_activity": None # 最後活動時間
234
+ }
235
+
236
+ return {
237
+ "success": True,
238
+ "data": summary
239
+ }
240
+
241
+ except Exception as e:
242
+ logger.error(f"取得用戶活動摘要時發生錯誤: {str(e)}")
243
+ return {
244
+ "success": False,
245
+ "error": str(e)
246
+ }
backend/services/nlp_service.py ADDED
@@ -0,0 +1,653 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import json
3
+ from typing import Dict, Any, List
4
+ from backend.models.schemas import NLPAnalysisResult, QueryType, DatabaseResult
5
+ from backend.config import settings
6
+ from backend.services.openrouter_service import OpenRouterService
7
+ import logging
8
+ import asyncio
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ class NLPService:
13
+ """自然語言處理服務"""
14
+
15
+ def __init__(self):
16
+ self.openrouter_service = OpenRouterService()
17
+ self.business_intent_patterns = {
18
+ "product_search": [
19
+ r"查詢.*商品|找.*商品|搜尋.*商品|商品.*資料",
20
+ r"商品.*查詢|商品.*搜尋|產品.*查詢|產品.*搜尋",
21
+ r"有什麼.*商品|商品.*價格|產品.*價格"
22
+ ],
23
+ "inventory_check": [
24
+ r"庫存.*查詢|查詢.*庫存|庫存.*多少|剩餘.*數量",
25
+ r"存貨.*查詢|查詢.*存貨|還有.*多少|庫存.*狀況",
26
+ r".*庫存|.*存貨|.*剩餘"
27
+ ],
28
+ "order_search": [
29
+ r"查詢.*訂單|找.*訂單|搜尋.*訂單|訂單.*資料",
30
+ r"訂單.*查詢|訂單.*搜尋|訂單.*狀態|我的.*訂單",
31
+ r"訂單編號|購買.*記錄"
32
+ ],
33
+ "customer_search": [
34
+ r"查詢.*客戶|找.*客戶|搜尋.*客戶|客戶.*資料",
35
+ r"客戶.*查詢|客戶.*搜尋|客戶.*聯絡"
36
+ ],
37
+ "low_stock_alert": [
38
+ r"低庫存|缺貨|庫存.*不足|存貨.*不足",
39
+ r"快.*沒有|即將.*缺貨|庫存.*警告"
40
+ ],
41
+ "business_summary": [
42
+ r"統計|分析|報表|數據|摘要",
43
+ r"總計|總數|多少.*筆|幾.*筆|業務.*狀況"
44
+ ]
45
+ }
46
+ self.intent_patterns = {
47
+ "search_user": [
48
+ r"查詢.*用戶|找.*用戶|搜尋.*用戶|用戶.*資料",
49
+ r"用戶.*查詢|用戶.*搜尋|用戶.*找",
50
+ r"誰是|哪個用戶|用戶名.*是"
51
+ ],
52
+ "search_order": [
53
+ r"查詢.*訂單|找.*訂單|搜尋.*訂單|訂單.*資料",
54
+ r"訂單.*查詢|訂單.*搜尋|訂單.*狀態",
55
+ r"我的訂單|訂單編號"
56
+ ],
57
+ "search_product": [
58
+ r"查詢.*商品|找.*商品|搜尋.*商品|商品.*資料",
59
+ r"商品.*查詢|商品.*搜尋|產品.*查詢",
60
+ r"有什麼.*商品|商品.*價格"
61
+ ],
62
+ "create_order": [
63
+ r"建立.*訂單|新增.*訂單|下訂|購買",
64
+ r"我要.*買|我想.*買|訂購"
65
+ ],
66
+ "update_profile": [
67
+ r"更新.*資料|修改.*資料|變更.*資料",
68
+ r"更新.*個人|修改.*個人|個人.*資料"
69
+ ],
70
+ "analytics": [
71
+ r"統計|分析|報表|數據",
72
+ r"總計|總數|多少.*筆|幾.*筆"
73
+ ]
74
+ }
75
+
76
+ self.entity_patterns = {
77
+ "user_id": r"用戶ID[::]?\s*([A-Za-z0-9]+)",
78
+ "user_name": r"用戶名[::]?\s*([^\s]+)|名字[::]?\s*([^\s]+)",
79
+ "order_id": r"訂單[編號ID][::]?\s*([A-Za-z0-9\-]+)",
80
+ "product_name": r"商品[::]?\s*([^\s]+)|產品[::]?\s*([^\s]+)",
81
+ "price_range": r"價格.*?(\d+).*?到.*?(\d+)|(\d+).*?元.*?到.*?(\d+).*?元",
82
+ "date_range": r"(\d{4}[-/]\d{1,2}[-/]\d{1,2})",
83
+ "number": r"(\d+)"
84
+ }
85
+
86
+ def analyze_message(self, message: str, use_advanced: bool = True) -> NLPAnalysisResult:
87
+ """分析用戶訊息"""
88
+ try:
89
+ # 如果啟用進階分析且有 OpenRouter API Key
90
+ if use_advanced and self.openrouter_service.api_key:
91
+ try:
92
+ # 使用 asyncio 執行異步分析
93
+ loop = asyncio.new_event_loop()
94
+ asyncio.set_event_loop(loop)
95
+ advanced_result = loop.run_until_complete(
96
+ self.openrouter_service.analyze_intent_advanced(message)
97
+ )
98
+ loop.close()
99
+
100
+ if not advanced_result.get("fallback", False):
101
+ return NLPAnalysisResult(
102
+ query_type=QueryType(advanced_result.get("query_type", "unknown")),
103
+ intent=advanced_result.get("intent", "unknown"),
104
+ entities=advanced_result.get("entities", {}),
105
+ confidence=advanced_result.get("confidence", 0.5),
106
+ parameters=advanced_result.get("parameters", {})
107
+ )
108
+ except Exception as e:
109
+ logger.warning(f"進階 NLP 分析失敗,使用基礎分析: {str(e)}")
110
+
111
+ # 使用基礎規則引擎分析
112
+ return self._basic_analyze_message(message)
113
+
114
+ except Exception as e:
115
+ logger.error(f"NLP 分析錯誤: {str(e)}")
116
+ return NLPAnalysisResult(
117
+ query_type=QueryType.UNKNOWN,
118
+ intent="unknown",
119
+ entities={},
120
+ confidence=0.0,
121
+ parameters={}
122
+ )
123
+
124
+ def _basic_analyze_message(self, message: str) -> NLPAnalysisResult:
125
+ """基礎訊息分析(規則引擎)"""
126
+ # 清理訊息
127
+ cleaned_message = self._clean_message(message)
128
+
129
+ # 識別意圖
130
+ intent, confidence = self._identify_intent(cleaned_message)
131
+
132
+ # 提取實體
133
+ entities = self._extract_entities(cleaned_message)
134
+
135
+ # 確定查詢類型
136
+ query_type = self._determine_query_type(intent)
137
+
138
+ # 生成查詢參數
139
+ parameters = self._generate_parameters(intent, entities)
140
+
141
+ return NLPAnalysisResult(
142
+ query_type=query_type,
143
+ intent=intent,
144
+ entities=entities,
145
+ confidence=confidence,
146
+ parameters=parameters
147
+ )
148
+
149
+ def _clean_message(self, message: str) -> str:
150
+ """清理訊息"""
151
+ # 移除多餘空白
152
+ message = re.sub(r'\s+', ' ', message.strip())
153
+ return message
154
+
155
+ def _identify_intent(self, message: str) -> tuple[str, float]:
156
+ """識別用戶意圖"""
157
+ best_intent = "unknown"
158
+ best_score = 0.0
159
+
160
+ for intent, patterns in self.intent_patterns.items():
161
+ for pattern in patterns:
162
+ if re.search(pattern, message, re.IGNORECASE):
163
+ score = len(re.findall(pattern, message, re.IGNORECASE)) / len(message.split())
164
+ if score > best_score:
165
+ best_score = score
166
+ best_intent = intent
167
+
168
+ # 如果沒有匹配到任何模式,設定基本信心度
169
+ confidence = max(best_score, 0.3) if best_intent != "unknown" else 0.1
170
+
171
+ return best_intent, min(confidence, 1.0)
172
+
173
+ def _extract_entities(self, message: str) -> Dict[str, Any]:
174
+ """提取實體"""
175
+ entities = {}
176
+
177
+ for entity_type, pattern in self.entity_patterns.items():
178
+ matches = re.findall(pattern, message, re.IGNORECASE)
179
+ if matches:
180
+ if entity_type == "price_range":
181
+ # 處理價格範圍
182
+ for match in matches:
183
+ if isinstance(match, tuple):
184
+ prices = [p for p in match if p]
185
+ if len(prices) >= 2:
186
+ entities["min_price"] = int(prices[0])
187
+ entities["max_price"] = int(prices[1])
188
+ elif entity_type == "user_name":
189
+ # 處理用戶名(可能有多個捕獲組)
190
+ for match in matches:
191
+ if isinstance(match, tuple):
192
+ name = next((n for n in match if n), None)
193
+ if name:
194
+ entities[entity_type] = name
195
+ else:
196
+ entities[entity_type] = match
197
+ else:
198
+ entities[entity_type] = matches[0] if isinstance(matches[0], str) else matches[0][0]
199
+
200
+ return entities
201
+
202
+ def _determine_query_type(self, intent: str) -> QueryType:
203
+ """確定查詢類型"""
204
+ if intent.startswith("search_"):
205
+ return QueryType.SEARCH
206
+ elif intent.startswith("create_"):
207
+ return QueryType.CREATE
208
+ elif intent.startswith("update_"):
209
+ return QueryType.UPDATE
210
+ elif intent.startswith("delete_"):
211
+ return QueryType.DELETE
212
+ elif intent == "analytics":
213
+ return QueryType.ANALYTICS
214
+ else:
215
+ return QueryType.UNKNOWN
216
+
217
+ def _generate_parameters(self, intent: str, entities: Dict[str, Any]) -> Dict[str, Any]:
218
+ """生成查詢參數"""
219
+ parameters = {}
220
+
221
+ # 根據意圖設定表名
222
+ if "user" in intent:
223
+ parameters["table"] = "users"
224
+ elif "order" in intent:
225
+ parameters["table"] = "orders"
226
+ elif "product" in intent:
227
+ parameters["table"] = "products"
228
+
229
+ # 設定查詢條件
230
+ conditions = {}
231
+ if "user_id" in entities:
232
+ conditions["user_id"] = entities["user_id"]
233
+ if "user_name" in entities:
234
+ conditions["name"] = entities["user_name"]
235
+ if "order_id" in entities:
236
+ conditions["order_id"] = entities["order_id"]
237
+ if "product_name" in entities:
238
+ conditions["name"] = entities["product_name"]
239
+ if "min_price" in entities and "max_price" in entities:
240
+ conditions["price"] = {
241
+ "gte": entities["min_price"],
242
+ "lte": entities["max_price"]
243
+ }
244
+
245
+ if conditions:
246
+ parameters["conditions"] = conditions
247
+
248
+ # 設定限制
249
+ if "number" in entities:
250
+ parameters["limit"] = min(int(entities["number"]), 50) # 最多50筆
251
+ else:
252
+ parameters["limit"] = 10 # 預設10筆
253
+
254
+ return parameters
255
+
256
+ def format_response(self, db_result: DatabaseResult, analysis_result: NLPAnalysisResult, user_message: str = "", use_advanced: bool = True) -> str:
257
+ """格式化回應訊息"""
258
+ try:
259
+ # 如果啟用進階回應且有 OpenRouter API Key
260
+ if use_advanced and user_message and self.openrouter_service.api_key:
261
+ try:
262
+ # 準備查詢結果資料
263
+ query_result = {
264
+ "success": db_result.success,
265
+ "data": db_result.data,
266
+ "count": db_result.count,
267
+ "error": db_result.error
268
+ }
269
+
270
+ # 使用 asyncio 執行異步回應生成
271
+ loop = asyncio.new_event_loop()
272
+ asyncio.set_event_loop(loop)
273
+ advanced_response = loop.run_until_complete(
274
+ self.openrouter_service.generate_response(query_result, user_message)
275
+ )
276
+ loop.close()
277
+
278
+ if advanced_response and len(advanced_response.strip()) > 0:
279
+ return advanced_response
280
+ except Exception as e:
281
+ logger.warning(f"進階回應生成失敗,使用基礎格式: {str(e)}")
282
+
283
+ # 使用基礎格式化
284
+ return self._basic_format_response(db_result, analysis_result)
285
+
286
+ except Exception as e:
287
+ logger.error(f"格式化回應錯誤: {str(e)}")
288
+ return "資料處理時發生錯誤,請稍後再試。"
289
+
290
+ def _basic_format_response(self, db_result: DatabaseResult, analysis_result: NLPAnalysisResult) -> str:
291
+ """基礎回應格式化"""
292
+ if not db_result.success:
293
+ return f"抱歉,查詢時發生錯誤:{db_result.error}"
294
+
295
+ if not db_result.data:
296
+ return "沒有找到相關資料。"
297
+
298
+ intent = analysis_result.intent
299
+ data = db_result.data
300
+
301
+ if intent.startswith("search_user"):
302
+ return self._format_user_response(data)
303
+ elif intent.startswith("search_order"):
304
+ return self._format_order_response(data)
305
+ elif intent.startswith("search_product"):
306
+ return self._format_product_response(data)
307
+ elif intent == "analytics":
308
+ return self._format_analytics_response(data)
309
+ else:
310
+ return f"找到 {len(data)} 筆資料。"
311
+
312
+ def _format_user_response(self, data: List[Dict[str, Any]]) -> str:
313
+ """格式化用戶查詢回應"""
314
+ if len(data) == 1:
315
+ user = data[0]
316
+ return f"用戶資料:\n名稱:{user.get('name', 'N/A')}\nID:{user.get('user_id', 'N/A')}\n電子郵件:{user.get('email', 'N/A')}"
317
+ else:
318
+ response = f"找到 {len(data)} 位用戶:\n"
319
+ for i, user in enumerate(data[:5], 1): # 最多顯示5筆
320
+ response += f"{i}. {user.get('name', 'N/A')} (ID: {user.get('user_id', 'N/A')})\n"
321
+ if len(data) > 5:
322
+ response += f"... 還有 {len(data) - 5} 筆資料"
323
+ return response
324
+
325
+ def _format_order_response(self, data: List[Dict[str, Any]]) -> str:
326
+ """格式化訂單查詢回應"""
327
+ if len(data) == 1:
328
+ order = data[0]
329
+ return f"訂單資料:\n訂單編號:{order.get('order_id', 'N/A')}\n狀態:{order.get('status', 'N/A')}\n金額:${order.get('total_amount', 'N/A')}"
330
+ else:
331
+ response = f"找到 {len(data)} 筆訂單:\n"
332
+ for i, order in enumerate(data[:5], 1):
333
+ response += f"{i}. {order.get('order_id', 'N/A')} - ${order.get('total_amount', 'N/A')}\n"
334
+ if len(data) > 5:
335
+ response += f"... 還有 {len(data) - 5} 筆資料"
336
+ return response
337
+
338
+ def _format_product_response(self, data: List[Dict[str, Any]]) -> str:
339
+ """格式化商品查詢回應"""
340
+ if len(data) == 1:
341
+ product = data[0]
342
+ return f"商品資料:\n名稱:{product.get('name', 'N/A')}\n價格:${product.get('price', 'N/A')}\n庫存:{product.get('stock', 'N/A')}"
343
+ else:
344
+ response = f"找到 {len(data)} 項商品:\n"
345
+ for i, product in enumerate(data[:5], 1):
346
+ response += f"{i}. {product.get('name', 'N/A')} - ${product.get('price', 'N/A')}\n"
347
+ if len(data) > 5:
348
+ response += f"... 還有 {len(data) - 5} 筆���料"
349
+ return response
350
+
351
+ def _format_analytics_response(self, data: List[Dict[str, Any]]) -> str:
352
+ """格式化分析回應"""
353
+ if data and len(data) > 0:
354
+ if 'count' in data[0]:
355
+ return f"統計結果:共 {data[0]['count']} 筆資料"
356
+ else:
357
+ return f"分析結果:找到 {len(data)} 筆相關資料"
358
+ return "無統計資料"
359
+
360
+ def analyze_business_query(self, message: str, user_id: str = None) -> NLPAnalysisResult:
361
+ """分析業務相關的自然語言查詢"""
362
+ try:
363
+ # 檢測查詢意圖
364
+ intent = self._detect_business_intent(message)
365
+
366
+ # 提取實體
367
+ entities = self._extract_business_entities(message, intent)
368
+
369
+ # 根據意圖設定查詢類型和參數
370
+ query_type, parameters = self._build_query_parameters(intent, entities, user_id)
371
+
372
+ # 計算信心度
373
+ confidence = self._calculate_confidence(message, intent, entities)
374
+
375
+ return NLPAnalysisResult(
376
+ query_type=query_type,
377
+ intent=intent,
378
+ entities=entities,
379
+ confidence=confidence,
380
+ parameters=parameters
381
+ )
382
+
383
+ except Exception as e:
384
+ logger.error(f"業務查詢分析錯誤: {str(e)}")
385
+ return NLPAnalysisResult(
386
+ query_type=QueryType.UNKNOWN,
387
+ intent="unknown",
388
+ entities={},
389
+ confidence=0.0,
390
+ parameters={}
391
+ )
392
+
393
+ def _detect_business_intent(self, message: str) -> str:
394
+ """檢測業務查詢意圖"""
395
+ message_lower = message.lower()
396
+
397
+ # 檢查業務相關的意圖模式
398
+ for intent, patterns in self.business_intent_patterns.items():
399
+ for pattern in patterns:
400
+ if re.search(pattern, message_lower):
401
+ return intent
402
+
403
+ # 如果沒有匹配到業務意圖,使用原有的意圖檢測
404
+ for intent, patterns in self.intent_patterns.items():
405
+ for pattern in patterns:
406
+ if re.search(pattern, message_lower):
407
+ return intent
408
+
409
+ return "general_search"
410
+
411
+ def _extract_business_entities(self, message: str, intent: str) -> Dict[str, Any]:
412
+ """提取業務相關實體"""
413
+ entities = {}
414
+
415
+ # 商品名稱提取
416
+ product_patterns = [
417
+ r"商品[::]?\s*([^\s,。!?]+)",
418
+ r"產品[::]?\s*([^\s,。!?]+)",
419
+ r"貨品[::]?\s*([^\s,。!?]+)"
420
+ ]
421
+
422
+ for pattern in product_patterns:
423
+ match = re.search(pattern, message)
424
+ if match:
425
+ entities["product_name"] = match.group(1)
426
+ break
427
+
428
+ # 如果沒有明確的商品名稱,嘗試提取關鍵字
429
+ if "product_name" not in entities:
430
+ # 移除查詢關鍵字後的剩餘內容可能是商品名稱
431
+ keywords_to_remove = ['查詢', '搜尋', '找', '商品', '產品', '庫存', '有沒有', '請問', '的', '嗎']
432
+ cleaned_message = message
433
+ for keyword in keywords_to_remove:
434
+ cleaned_message = cleaned_message.replace(keyword, '')
435
+
436
+ cleaned_message = cleaned_message.strip()
437
+ if cleaned_message and len(cleaned_message) > 0:
438
+ entities["search_text"] = cleaned_message
439
+
440
+ # 客戶相關實體
441
+ customer_patterns = [
442
+ r"客戶[::]?\s*([^\s,。!?]+)",
443
+ r"客戶編號[::]?\s*([A-Za-z0-9]+)",
444
+ r"客戶名稱[::]?\s*([^\s,。!?]+)"
445
+ ]
446
+
447
+ for pattern in customer_patterns:
448
+ match = re.search(pattern, message)
449
+ if match:
450
+ entities["customer_info"] = match.group(1)
451
+ break
452
+
453
+ # 訂單相關實體
454
+ order_patterns = [
455
+ r"訂單[編號ID][::]?\s*([A-Za-z0-9\-]+)",
456
+ r"訂單[::]?\s*([A-Za-z0-9\-]+)"
457
+ ]
458
+
459
+ for pattern in order_patterns:
460
+ match = re.search(pattern, message)
461
+ if match:
462
+ entities["order_id"] = match.group(1)
463
+ break
464
+
465
+ # 數量相關實體
466
+ quantity_patterns = [
467
+ r"(\d+)\s*個",
468
+ r"(\d+)\s*件",
469
+ r"(\d+)\s*箱",
470
+ r"數量[::]?\s*(\d+)"
471
+ ]
472
+
473
+ for pattern in quantity_patterns:
474
+ match = re.search(pattern, message)
475
+ if match:
476
+ entities["quantity"] = int(match.group(1))
477
+ break
478
+
479
+ # 狀態相關實體
480
+ status_keywords = {
481
+ "待處理": ["待處理", "pending"],
482
+ "已確認": ["已確認", "confirmed"],
483
+ "已出貨": ["已出貨", "shipped"],
484
+ "已完成": ["已完成", "completed"],
485
+ "已取消": ["已取消", "cancelled"]
486
+ }
487
+
488
+ message_lower = message.lower()
489
+ for status, keywords in status_keywords.items():
490
+ if any(keyword in message_lower for keyword in keywords):
491
+ entities["status"] = status
492
+ break
493
+
494
+ return entities
495
+
496
+ def _build_query_parameters(self, intent: str, entities: Dict[str, Any], user_id: str = None) -> tuple:
497
+ """根據意圖和實體建立查詢參數"""
498
+
499
+ if intent == "product_search":
500
+ return QueryType.SEARCH, {
501
+ "method": "search_products",
502
+ "query_text": entities.get("product_name") or entities.get("search_text"),
503
+ "category": entities.get("category"),
504
+ "limit": 10
505
+ }
506
+
507
+ elif intent == "inventory_check":
508
+ return QueryType.SEARCH, {
509
+ "method": "check_inventory",
510
+ "product_name": entities.get("product_name") or entities.get("search_text"),
511
+ "category": entities.get("category")
512
+ }
513
+
514
+ elif intent == "order_search":
515
+ return QueryType.SEARCH, {
516
+ "method": "search_orders",
517
+ "user_id": user_id,
518
+ "order_id": entities.get("order_id"),
519
+ "status": entities.get("status"),
520
+ "limit": 10
521
+ }
522
+
523
+ elif intent == "low_stock_alert":
524
+ return QueryType.SEARCH, {
525
+ "method": "get_low_stock_products",
526
+ "threshold": 10
527
+ }
528
+
529
+ elif intent == "business_summary":
530
+ return QueryType.ANALYTICS, {
531
+ "method": "get_business_summary"
532
+ }
533
+
534
+ else:
535
+ # 預設為商品搜尋
536
+ return QueryType.SEARCH, {
537
+ "method": "search_products",
538
+ "query_text": entities.get("search_text") or entities.get("product_name"),
539
+ "limit": 10
540
+ }
541
+
542
+ def _calculate_confidence(self, message: str, intent: str, entities: Dict[str, Any]) -> float:
543
+ """計算查詢信心度"""
544
+ confidence = 0.5 # 基礎信心度
545
+
546
+ # 如果有明確的意圖匹配,增加信心度
547
+ if intent in self.business_intent_patterns:
548
+ confidence += 0.3
549
+
550
+ # 如果提取到實體,增加信心度
551
+ if entities:
552
+ confidence += 0.2 * len(entities)
553
+
554
+ # 如果訊息長度適中,增加信心度
555
+ if 2 <= len(message) <= 50:
556
+ confidence += 0.1
557
+
558
+ return min(confidence, 1.0)
559
+
560
+ def format_response_message(self, result: DatabaseResult, intent: str) -> str:
561
+ """格式化回應訊息"""
562
+ if not result.success:
563
+ return f"抱歉,查詢時發生錯誤:{result.error}"
564
+
565
+ if not result.data or result.count == 0:
566
+ return "沒有找到相關資料。"
567
+
568
+ # 根據不同的查詢意圖格式化回應
569
+ if intent == "product_search":
570
+ return self._format_product_response(result.data)
571
+ elif intent == "inventory_check":
572
+ return self._format_inventory_response(result.data)
573
+ elif intent == "order_search":
574
+ return self._format_order_response(result.data)
575
+ elif intent == "low_stock_alert":
576
+ return self._format_low_stock_response(result.data)
577
+ elif intent == "business_summary":
578
+ return self._format_summary_response(result.data)
579
+ else:
580
+ return self._format_general_response(result.data)
581
+
582
+ def _format_product_response(self, data: List[Dict[str, Any]]) -> str:
583
+ """格式化商品查詢回應"""
584
+ if len(data) == 1:
585
+ product = data[0]
586
+ return f"找到商品:\n" \
587
+ f"名稱:{product.get('name', 'N/A')}\n" \
588
+ f"描述:{product.get('description', 'N/A')}\n" \
589
+ f"價格:${product.get('price', 0)}\n" \
590
+ f"類別:{product.get('category', 'N/A')}"
591
+ else:
592
+ response = f"找到 {len(data)} 個商品:\n"
593
+ for i, product in enumerate(data[:5], 1):
594
+ response += f"{i}. {product.get('name', 'N/A')} - ${product.get('price', 0)}\n"
595
+ if len(data) > 5:
596
+ response += f"... 還有 {len(data) - 5} 個商品"
597
+ return response
598
+
599
+ def _format_inventory_response(self, data: List[Dict[str, Any]]) -> str:
600
+ """格式化庫存查詢回應"""
601
+ if len(data) == 1:
602
+ item = data[0]
603
+ return f"庫存資訊:\n" \
604
+ f"商品:{item.get('product_name', 'N/A')}\n" \
605
+ f"目前庫存:{item.get('current_stock', 0)} 件\n" \
606
+ f"類別:{item.get('category', 'N/A')}\n" \
607
+ f"價格:${item.get('price', 0)}"
608
+ else:
609
+ response = f"找到 {len(data)} 個商品的庫存:\n"
610
+ for i, item in enumerate(data[:5], 1):
611
+ response += f"{i}. {item.get('product_name', 'N/A')} - 庫存:{item.get('current_stock', 0)}\n"
612
+ return response
613
+
614
+ def _format_order_response(self, data: List[Dict[str, Any]]) -> str:
615
+ """格式化訂單查詢回應"""
616
+ if len(data) == 1:
617
+ order = data[0]
618
+ return f"訂單資訊:\n" \
619
+ f"訂單編號:{order.get('order_id', 'N/A')}\n" \
620
+ f"狀態:{order.get('status', 'N/A')}\n" \
621
+ f"總金額:${order.get('total_amount', 0)}\n" \
622
+ f"建立時間:{order.get('created_at', 'N/A')}"
623
+ else:
624
+ response = f"找到 {len(data)} 筆訂單:\n"
625
+ for i, order in enumerate(data[:5], 1):
626
+ response += f"{i}. {order.get('order_id', 'N/A')} - {order.get('status', 'N/A')} - ${order.get('total_amount', 0)}\n"
627
+ return response
628
+
629
+ def _format_low_stock_response(self, data: List[Dict[str, Any]]) -> str:
630
+ """格式化低庫存警告回應"""
631
+ if not data:
632
+ return "目前沒有低庫存商品。"
633
+
634
+ response = f"⚠️ 發現 {len(data)} 個低庫存商品:\n"
635
+ for i, item in enumerate(data[:10], 1):
636
+ response += f"{i}. {item.get('product_name', 'N/A')} - 剩餘:{item.get('current_stock', 0)} 件\n"
637
+ return response
638
+
639
+ def _format_summary_response(self, data: List[Dict[str, Any]]) -> str:
640
+ """格式化業務摘要回應"""
641
+ if data:
642
+ summary = data[0]
643
+ return f"📊 業務摘要:\n" \
644
+ f"商品總數:{summary.get('total_products', 0)} 個\n" \
645
+ f"訂單總數:{summary.get('total_orders', 0)} 筆\n" \
646
+ f"用戶總數:{summary.get('total_users', 0)} 人\n" \
647
+ f"低庫存商品:{summary.get('low_stock_items', 0)} 個\n" \
648
+ f"統計時間:{summary.get('report_date', 'N/A')}"
649
+ return "無法取得業務摘要資料。"
650
+
651
+ def _format_general_response(self, data: List[Dict[str, Any]]) -> str:
652
+ """格式化一般查詢回應"""
653
+ return f"找到 {len(data)} 筆資料。"
backend/services/openrouter_service.py ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ OpenRouter API 服務
3
+ 用於進階自然語言處理功能
4
+ """
5
+
6
+ import httpx
7
+ import json
8
+ from typing import Dict, Any, Optional
9
+ from backend.config import settings
10
+ import logging
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ class OpenRouterService:
15
+ """OpenRouter API 服務類"""
16
+
17
+ def __init__(self):
18
+ self.api_key = settings.OPENROUTER_API_KEY
19
+ self.model = settings.OPENROUTER_MODEL
20
+ self.base_url = "https://openrouter.ai/api/v1"
21
+ self.headers = {
22
+ "Authorization": f"Bearer {self.api_key}",
23
+ "Content-Type": "application/json",
24
+ "HTTP-Referer": "https://huggingface.co/spaces",
25
+ "X-Title": "LINE Bot NLP Service"
26
+ }
27
+
28
+ async def analyze_intent_advanced(self, message: str, context: Dict[str, Any] = None) -> Dict[str, Any]:
29
+ """
30
+ 使用 OpenRouter 進行進階意圖分析
31
+
32
+ Args:
33
+ message: 用戶訊息
34
+ context: 上下文資訊
35
+
36
+ Returns:
37
+ 分析結果字典
38
+ """
39
+ if not self.api_key:
40
+ logger.warning("OpenRouter API Key 未設定,使用基礎 NLP 服務")
41
+ return self._fallback_analysis(message)
42
+
43
+ try:
44
+ prompt = self._build_analysis_prompt(message, context)
45
+
46
+ async with httpx.AsyncClient(timeout=30.0) as client:
47
+ response = await client.post(
48
+ f"{self.base_url}/chat/completions",
49
+ headers=self.headers,
50
+ json={
51
+ "model": self.model,
52
+ "messages": [
53
+ {
54
+ "role": "system",
55
+ "content": "你是一個專業的中文自然語言處理助手,專門分析用戶查詢意圖並提取相關實體。請以 JSON 格式回應。"
56
+ },
57
+ {
58
+ "role": "user",
59
+ "content": prompt
60
+ }
61
+ ],
62
+ "temperature": 0.1,
63
+ "max_tokens": 500
64
+ }
65
+ )
66
+
67
+ if response.status_code == 200:
68
+ result = response.json()
69
+ content = result["choices"][0]["message"]["content"]
70
+
71
+ try:
72
+ # 嘗試解析 JSON 回應
73
+ analysis = json.loads(content)
74
+ return self._validate_analysis_result(analysis)
75
+ except json.JSONDecodeError:
76
+ logger.error(f"無法解析 OpenRouter 回應: {content}")
77
+ return self._fallback_analysis(message)
78
+ else:
79
+ logger.error(f"OpenRouter API 錯誤: {response.status_code}")
80
+ return self._fallback_analysis(message)
81
+
82
+ except Exception as e:
83
+ logger.error(f"OpenRouter 服務錯誤: {str(e)}")
84
+ return self._fallback_analysis(message)
85
+
86
+ def _build_analysis_prompt(self, message: str, context: Dict[str, Any] = None) -> str:
87
+ """建構分析提示詞"""
88
+ context_info = ""
89
+ if context:
90
+ context_info = f"\n上下文資訊: {json.dumps(context, ensure_ascii=False)}"
91
+
92
+ prompt = f"""
93
+ 請分析以下中文訊息的意圖和實體:
94
+
95
+ 訊息: "{message}"{context_info}
96
+
97
+ 請以以下 JSON 格式回應:
98
+ {{
99
+ "intent": "查詢意圖類型 (search_user/search_product/search_order/create_order/update_profile/analytics/unknown)",
100
+ "confidence": 0.0-1.0之間的信心度,
101
+ "entities": {{
102
+ "user_id": "提取的用戶ID",
103
+ "user_name": "提取的用戶名稱",
104
+ "product_name": "提取的商品名稱",
105
+ "order_id": "提取的訂單ID",
106
+ "min_price": 最低價格數字,
107
+ "max_price": 最高價格數字,
108
+ "category": "商品類別",
109
+ "number": 數量
110
+ }},
111
+ "query_type": "search/create/update/delete/analytics",
112
+ "parameters": {{
113
+ "table": "目標資料表名稱",
114
+ "conditions": "查詢條件",
115
+ "limit": 查詢限制數量
116
+ }}
117
+ }}
118
+
119
+ 範例:
120
+ - "查詢用戶張三" → intent: "search_user", entities: {{"user_name": "張三"}}
121
+ - "找價格1000到5000的手機" → intent: "search_product", entities: {{"min_price": 1000, "max_price": 5000, "category": "手機"}}
122
+ - "統計訂單數量" → intent: "analytics", query_type: "analytics"
123
+ """
124
+ return prompt
125
+
126
+ def _validate_analysis_result(self, analysis: Dict[str, Any]) -> Dict[str, Any]:
127
+ """驗證分析結果"""
128
+ # 確保必要欄位存在
129
+ required_fields = ["intent", "confidence", "entities", "query_type"]
130
+ for field in required_fields:
131
+ if field not in analysis:
132
+ analysis[field] = self._get_default_value(field)
133
+
134
+ # 驗證信心度範圍
135
+ if not 0 <= analysis.get("confidence", 0) <= 1:
136
+ analysis["confidence"] = 0.5
137
+
138
+ # 確保實體是字典
139
+ if not isinstance(analysis.get("entities"), dict):
140
+ analysis["entities"] = {}
141
+
142
+ return analysis
143
+
144
+ def _get_default_value(self, field: str) -> Any:
145
+ """取得欄位預設值"""
146
+ defaults = {
147
+ "intent": "unknown",
148
+ "confidence": 0.1,
149
+ "entities": {},
150
+ "query_type": "unknown",
151
+ "parameters": {}
152
+ }
153
+ return defaults.get(field, None)
154
+
155
+ def _fallback_analysis(self, message: str) -> Dict[str, Any]:
156
+ """備用分析方法"""
157
+ return {
158
+ "intent": "unknown",
159
+ "confidence": 0.1,
160
+ "entities": {},
161
+ "query_type": "unknown",
162
+ "parameters": {},
163
+ "fallback": True
164
+ }
165
+
166
+ async def generate_response(self, query_result: Dict[str, Any], user_message: str) -> str:
167
+ """
168
+ 使用 OpenRouter 生成更自然的回應
169
+
170
+ Args:
171
+ query_result: 資料庫查詢結果
172
+ user_message: 用戶原始訊息
173
+
174
+ Returns:
175
+ 生成的回應文字
176
+ """
177
+ if not self.api_key:
178
+ return self._generate_simple_response(query_result)
179
+
180
+ try:
181
+ prompt = f"""
182
+ 請根據以下資訊生成一個友善、自然的中文回應:
183
+
184
+ 用戶訊息: "{user_message}"
185
+ 查詢結果: {json.dumps(query_result, ensure_ascii=False)}
186
+
187
+ 要求:
188
+ 1. 回應要簡潔明瞭
189
+ 2. 使用友善的語調
190
+ 3. 如果有資料,要清楚呈現重要資訊
191
+ 4. 如果沒有資料,要給出建議
192
+ 5. 回應長度控制在 200 字以內
193
+
194
+ 範例格式:
195
+ - 找到資料時:「找到 X 筆資料:[列出重要資訊]」
196
+ - 沒有資料時:「很抱歉,沒有找到相關資料,您可以嘗試...」
197
+ """
198
+
199
+ async with httpx.AsyncClient(timeout=30.0) as client:
200
+ response = await client.post(
201
+ f"{self.base_url}/chat/completions",
202
+ headers=self.headers,
203
+ json={
204
+ "model": self.model,
205
+ "messages": [
206
+ {
207
+ "role": "system",
208
+ "content": "你是一個友善的客服助手,專門幫助用戶理解查詢結果。"
209
+ },
210
+ {
211
+ "role": "user",
212
+ "content": prompt
213
+ }
214
+ ],
215
+ "temperature": 0.3,
216
+ "max_tokens": 300
217
+ }
218
+ )
219
+
220
+ if response.status_code == 200:
221
+ result = response.json()
222
+ generated_response = result["choices"][0]["message"]["content"].strip()
223
+ return generated_response
224
+ else:
225
+ logger.error(f"OpenRouter 回應生成錯誤: {response.status_code}")
226
+ return self._generate_simple_response(query_result)
227
+
228
+ except Exception as e:
229
+ logger.error(f"OpenRouter 回應生成失敗: {str(e)}")
230
+ return self._generate_simple_response(query_result)
231
+
232
+ def _generate_simple_response(self, query_result: Dict[str, Any]) -> str:
233
+ """簡單回應生成"""
234
+ if query_result.get("success"):
235
+ data_count = len(query_result.get("data", []))
236
+ if data_count > 0:
237
+ return f"找到 {data_count} 筆相關資料。"
238
+ else:
239
+ return "沒有找到相關資料。"
240
+ else:
241
+ return "查詢時發生錯誤,請稍後再試。"
backend/utils/__init__.py ADDED
File without changes
backend/utils/database_init.py ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 資料庫初始化腳本
3
+ 用於在 Supabase 中建立必要的資料表
4
+ """
5
+
6
+ from supabase import create_client
7
+ from backend.config import settings
8
+ import logging
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ # SQL 建表語句
13
+ CREATE_TABLES_SQL = {
14
+ "users": """
15
+ CREATE TABLE IF NOT EXISTS users (
16
+ user_id VARCHAR(255) PRIMARY KEY,
17
+ name VARCHAR(255),
18
+ email VARCHAR(255),
19
+ display_name VARCHAR(255),
20
+ picture_url TEXT,
21
+ status_message TEXT,
22
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
23
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
24
+ );
25
+ """,
26
+
27
+ "products": """
28
+ CREATE TABLE IF NOT EXISTS products (
29
+ product_id SERIAL PRIMARY KEY,
30
+ name VARCHAR(255) NOT NULL,
31
+ description TEXT,
32
+ price DECIMAL(10,2) NOT NULL,
33
+ stock INTEGER DEFAULT 0,
34
+ category VARCHAR(100),
35
+ image_url TEXT,
36
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
37
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
38
+ );
39
+ """,
40
+
41
+ "orders": """
42
+ CREATE TABLE IF NOT EXISTS orders (
43
+ order_id VARCHAR(255) PRIMARY KEY,
44
+ user_id VARCHAR(255) REFERENCES users(user_id),
45
+ total_amount DECIMAL(10,2) NOT NULL,
46
+ status VARCHAR(50) DEFAULT 'pending',
47
+ shipping_address TEXT,
48
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
49
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
50
+ );
51
+ """,
52
+
53
+ "order_items": """
54
+ CREATE TABLE IF NOT EXISTS order_items (
55
+ id SERIAL PRIMARY KEY,
56
+ order_id VARCHAR(255) REFERENCES orders(order_id),
57
+ product_id INTEGER REFERENCES products(product_id),
58
+ quantity INTEGER NOT NULL,
59
+ unit_price DECIMAL(10,2) NOT NULL,
60
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
61
+ );
62
+ """,
63
+
64
+ "line_messages": """
65
+ CREATE TABLE IF NOT EXISTS line_messages (
66
+ id SERIAL PRIMARY KEY,
67
+ user_id VARCHAR(255),
68
+ message TEXT NOT NULL,
69
+ message_type VARCHAR(50) DEFAULT 'text',
70
+ timestamp TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
71
+ processed BOOLEAN DEFAULT FALSE
72
+ );
73
+ """,
74
+
75
+ "user_sessions": """
76
+ CREATE TABLE IF NOT EXISTS user_sessions (
77
+ id SERIAL PRIMARY KEY,
78
+ user_id VARCHAR(255) REFERENCES users(user_id),
79
+ session_data JSONB,
80
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
81
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
82
+ );
83
+ """
84
+ }
85
+
86
+ # 範例資料
87
+ SAMPLE_DATA = {
88
+ "products": [
89
+ {
90
+ "name": "iPhone 15 Pro",
91
+ "description": "最新款 iPhone,配備 A17 Pro 晶片",
92
+ "price": 35900,
93
+ "stock": 50,
94
+ "category": "手機"
95
+ },
96
+ {
97
+ "name": "MacBook Air M2",
98
+ "description": "輕薄筆記型電腦,搭載 M2 晶片",
99
+ "price": 37900,
100
+ "stock": 30,
101
+ "category": "筆電"
102
+ },
103
+ {
104
+ "name": "AirPods Pro",
105
+ "description": "主動降噪無線耳機",
106
+ "price": 7490,
107
+ "stock": 100,
108
+ "category": "耳機"
109
+ }
110
+ ]
111
+ }
112
+
113
+ def init_database():
114
+ """初始化資料庫"""
115
+ try:
116
+ supabase = create_client(settings.SUPABASE_URL, settings.SUPABASE_KEY)
117
+
118
+ logger.info("開始初始化資料庫...")
119
+
120
+ # 建立資料表
121
+ for table_name, sql in CREATE_TABLES_SQL.items():
122
+ try:
123
+ logger.info(f"建立資料表: {table_name}")
124
+ # 注意:Supabase 的 Python 客戶端可能不直接支援 SQL 執行
125
+ # 您需要在 Supabase Dashboard 的 SQL Editor 中執行這些 SQL
126
+ print(f"\n=== {table_name.upper()} TABLE SQL ===")
127
+ print(sql)
128
+
129
+ except Exception as e:
130
+ logger.error(f"建立資料表 {table_name} 失敗: {str(e)}")
131
+
132
+ # 插入範例資料
133
+ logger.info("插入範例資料...")
134
+ for table_name, data in SAMPLE_DATA.items():
135
+ try:
136
+ result = supabase.table(table_name).insert(data).execute()
137
+ logger.info(f"成功插入 {len(result.data)} 筆資料到 {table_name}")
138
+ except Exception as e:
139
+ logger.warning(f"插入範例資料到 {table_name} 失敗: {str(e)}")
140
+
141
+ logger.info("資料庫初始化完成!")
142
+ return True
143
+
144
+ except Exception as e:
145
+ logger.error(f"資料庫初始化失敗: {str(e)}")
146
+ return False
147
+
148
+ if __name__ == "__main__":
149
+ init_database()
backend/utils/line_utils.py ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ LINE Bot 相關工具函數
3
+ """
4
+
5
+ from linebot.models import (
6
+ TextSendMessage, QuickReply, QuickReplyButton, MessageAction,
7
+ FlexSendMessage, BubbleContainer, BoxComponent, TextComponent,
8
+ ButtonComponent, URIAction
9
+ )
10
+ from typing import List, Dict, Any
11
+ import logging
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ class LineMessageBuilder:
16
+ """LINE 訊息建構器"""
17
+
18
+ @staticmethod
19
+ def create_quick_reply_message(text: str, quick_reply_items: List[Dict[str, str]]) -> TextSendMessage:
20
+ """建立快速回覆訊息"""
21
+ quick_reply_buttons = []
22
+
23
+ for item in quick_reply_items:
24
+ button = QuickReplyButton(
25
+ action=MessageAction(
26
+ label=item["label"],
27
+ text=item["text"]
28
+ )
29
+ )
30
+ quick_reply_buttons.append(button)
31
+
32
+ quick_reply = QuickReply(items=quick_reply_buttons)
33
+
34
+ return TextSendMessage(
35
+ text=text,
36
+ quick_reply=quick_reply
37
+ )
38
+
39
+ @staticmethod
40
+ def create_product_flex_message(products: List[Dict[str, Any]]) -> FlexSendMessage:
41
+ """建立商品展示的 Flex 訊息"""
42
+ contents = []
43
+
44
+ for product in products[:10]: # 最多顯示10個商品
45
+ bubble = BubbleContainer(
46
+ body=BoxComponent(
47
+ layout="vertical",
48
+ contents=[
49
+ TextComponent(
50
+ text=product.get("name", "商品名稱"),
51
+ weight="bold",
52
+ size="lg"
53
+ ),
54
+ TextComponent(
55
+ text=f"NT$ {product.get('price', 0):,}",
56
+ size="md",
57
+ color="#ff5551"
58
+ ),
59
+ TextComponent(
60
+ text=f"庫存: {product.get('stock', 0)}",
61
+ size="sm",
62
+ color="#aaaaaa"
63
+ )
64
+ ]
65
+ ),
66
+ footer=BoxComponent(
67
+ layout="vertical",
68
+ contents=[
69
+ ButtonComponent(
70
+ action=MessageAction(
71
+ label="查看詳情",
72
+ text=f"查詢商品 {product.get('name', '')}"
73
+ ),
74
+ style="primary"
75
+ )
76
+ ]
77
+ )
78
+ )
79
+ contents.append(bubble)
80
+
81
+ return FlexSendMessage(
82
+ alt_text="商品列表",
83
+ contents={
84
+ "type": "carousel",
85
+ "contents": [bubble.as_json_dict() for bubble in contents]
86
+ }
87
+ )
88
+
89
+ @staticmethod
90
+ def create_help_message() -> TextSendMessage:
91
+ """建立說明訊息"""
92
+ help_text = """
93
+ 🤖 歡迎使用智能查詢機器人!
94
+
95
+ 📝 支援的查詢類型:
96
+ • 用戶查詢:「查詢用戶 張三」
97
+ • 訂單查詢:「查詢訂單 ORD001」
98
+ • 商品查詢:「查詢商品 iPhone」
99
+ • 價格範圍:「價格 1000 到 5000 的商品」
100
+ • 統計分析:「統計用戶數量」
101
+
102
+ 💡 使用範例:
103
+ • 「找用戶名叫王小明的資料」
104
+ • 「我的訂單狀態如何」
105
+ • 「有什麼手機商品」
106
+ • 「總共有多少筆訂單」
107
+
108
+ ❓ 如需協助,請輸入「幫助」或「說明」
109
+ """
110
+
111
+ quick_reply_items = [
112
+ {"label": "查詢用戶", "text": "查詢用戶"},
113
+ {"label": "查詢訂單", "text": "查詢訂單"},
114
+ {"label": "查詢商品", "text": "查詢商品"},
115
+ {"label": "統計資料", "text": "統計"}
116
+ ]
117
+
118
+ return LineMessageBuilder.create_quick_reply_message(help_text, quick_reply_items)
119
+
120
+ @staticmethod
121
+ def create_error_message(error_msg: str = None) -> TextSendMessage:
122
+ """建立錯誤訊息"""
123
+ default_msg = "抱歉,我無法理解您的訊息。請輸入「幫助」查看使用說明。"
124
+ text = error_msg if error_msg else default_msg
125
+
126
+ quick_reply_items = [
127
+ {"label": "幫助", "text": "幫助"},
128
+ {"label": "重新開始", "text": "開始"}
129
+ ]
130
+
131
+ return LineMessageBuilder.create_quick_reply_message(text, quick_reply_items)
132
+
133
+ def get_user_display_name(line_bot_api, user_id: str) -> str:
134
+ """取得用戶顯示名稱"""
135
+ try:
136
+ profile = line_bot_api.get_profile(user_id)
137
+ return profile.display_name
138
+ except Exception as e:
139
+ logger.error(f"取得用戶資料失敗: {str(e)}")
140
+ return "用戶"
141
+
142
+ def validate_line_signature(body: bytes, signature: str, channel_secret: str) -> bool:
143
+ """驗證 LINE Webhook 簽名"""
144
+ import hashlib
145
+ import hmac
146
+ import base64
147
+
148
+ hash_value = hmac.new(
149
+ channel_secret.encode('utf-8'),
150
+ body,
151
+ hashlib.sha256
152
+ ).digest()
153
+
154
+ expected_signature = base64.b64encode(hash_value).decode('utf-8')
155
+ return hmac.compare_digest(signature, expected_signature)
requirements.txt ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.104.1
2
+ uvicorn[standard]==0.24.0
3
+ line-bot-sdk==3.5.0
4
+ sqlalchemy==2.0.23
5
+ psycopg==3.1.13
6
+ alembic==1.13.0
7
+ pydantic==2.5.0
8
+ pydantic-settings==2.1.0
9
+ python-multipart==0.0.6
10
+ python-dotenv==1.0.0
11
+ requests==2.31.0
12
+ openai==1.3.0
13
+ httpx==0.25.2
setup_guide.md ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # LINE Bot + FastAPI + Supabase 設定指南
2
+
3
+ ## 🚀 快速開始
4
+
5
+ ### 1. 必要的 API Keys 和設定
6
+
7
+ 您需要準備以下 API Keys 和設定:
8
+
9
+ #### LINE Developers Console
10
+ 1. 前往 [LINE Developers Console](https://developers.line.biz/)
11
+ 2. 建立新的 Provider 和 Messaging API Channel
12
+ 3. 取得以下資訊:
13
+ - `Channel Access Token` (長期)
14
+ - `Channel Secret`
15
+
16
+ #### Supabase
17
+ 1. 前往 [Supabase](https://supabase.com/)
18
+ 2. 建立新專案
19
+ 3. 取得以下資訊:
20
+ - `Project URL`
21
+ - `Anon/Public Key`
22
+
23
+ #### OpenRouter (可選)
24
+ 1. 前往 [OpenRouter](https://openrouter.ai/)
25
+ 2. 註冊帳號並取得 `API Key` (用於進階 NLP 功能)
26
+ 3. 選擇適合的模型 (預設: anthropic/claude-3-haiku)
27
+
28
+ ### 2. Hugging Face Spaces 環境變數設定
29
+
30
+ 在 Hugging Face Spaces 的 Settings 中設定以下環境變數:
31
+
32
+ ```
33
+ LINE_CHANNEL_ACCESS_TOKEN=你的_LINE_Channel_Access_Token
34
+ LINE_CHANNEL_SECRET=你的_LINE_Channel_Secret
35
+ SUPABASE_URL=你的_Supabase_專案_URL
36
+ SUPABASE_KEY=你的_Supabase_Anon_Key
37
+ OPENROUTER_API_KEY=你的_OpenRouter_API_Key (可選)
38
+ OPENROUTER_MODEL=anthropic/claude-3-haiku (可選)
39
+ DEBUG=False
40
+ LOG_LEVEL=INFO
41
+ ```
42
+
43
+ ### 3. Supabase 資料庫設定
44
+
45
+ 在 Supabase Dashboard 的 SQL Editor 中執行以下 SQL:
46
+
47
+ ```sql
48
+ -- 建立用戶表
49
+ CREATE TABLE IF NOT EXISTS users (
50
+ user_id VARCHAR(255) PRIMARY KEY,
51
+ name VARCHAR(255),
52
+ email VARCHAR(255),
53
+ display_name VARCHAR(255),
54
+ picture_url TEXT,
55
+ status_message TEXT,
56
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
57
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
58
+ );
59
+
60
+ -- 建立商品表
61
+ CREATE TABLE IF NOT EXISTS products (
62
+ product_id SERIAL PRIMARY KEY,
63
+ name VARCHAR(255) NOT NULL,
64
+ description TEXT,
65
+ price DECIMAL(10,2) NOT NULL,
66
+ stock INTEGER DEFAULT 0,
67
+ category VARCHAR(100),
68
+ image_url TEXT,
69
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
70
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
71
+ );
72
+
73
+ -- 建立訂單表
74
+ CREATE TABLE IF NOT EXISTS orders (
75
+ order_id VARCHAR(255) PRIMARY KEY,
76
+ user_id VARCHAR(255) REFERENCES users(user_id),
77
+ total_amount DECIMAL(10,2) NOT NULL,
78
+ status VARCHAR(50) DEFAULT 'pending',
79
+ shipping_address TEXT,
80
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
81
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
82
+ );
83
+
84
+ -- 建立訂單項目表
85
+ CREATE TABLE IF NOT EXISTS order_items (
86
+ id SERIAL PRIMARY KEY,
87
+ order_id VARCHAR(255) REFERENCES orders(order_id),
88
+ product_id INTEGER REFERENCES products(product_id),
89
+ quantity INTEGER NOT NULL,
90
+ unit_price DECIMAL(10,2) NOT NULL,
91
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
92
+ );
93
+
94
+ -- 建立訊息記錄表
95
+ CREATE TABLE IF NOT EXISTS line_messages (
96
+ id SERIAL PRIMARY KEY,
97
+ user_id VARCHAR(255),
98
+ message TEXT NOT NULL,
99
+ message_type VARCHAR(50) DEFAULT 'text',
100
+ timestamp TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
101
+ processed BOOLEAN DEFAULT FALSE
102
+ );
103
+
104
+ -- 建立用戶會話表
105
+ CREATE TABLE IF NOT EXISTS user_sessions (
106
+ id SERIAL PRIMARY KEY,
107
+ user_id VARCHAR(255) REFERENCES users(user_id),
108
+ session_data JSONB,
109
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
110
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
111
+ );
112
+
113
+ -- 插入範例商品資料
114
+ INSERT INTO products (name, description, price, stock, category) VALUES
115
+ ('iPhone 15 Pro', '最新款 iPhone,配備 A17 Pro 晶片', 35900, 50, '手機'),
116
+ ('MacBook Air M2', '輕薄筆記型電腦,搭載 M2 晶片', 37900, 30, '筆電'),
117
+ ('AirPods Pro', '主動降噪無線耳機', 7490, 100, '耳機');
118
+ ```
119
+
120
+ ### 4. LINE Bot Webhook 設定
121
+
122
+ 1. 部署到 Hugging Face Spaces 後,取得您的應用程式 URL
123
+ 2. 在 LINE Developers Console 中設定 Webhook URL:
124
+ ```
125
+ https://你的用戶名-你的空間名稱.hf.space/webhook
126
+ ```
127
+ 3. 啟用 Webhook 和 Auto-reply messages
128
+
129
+ ### 5. 測試功能
130
+
131
+ 部署完成後,您可以測試以下功能:
132
+
133
+ #### 用戶查詢
134
+ - "查詢用戶 張三"
135
+ - "找用戶名叫王小明的資料"
136
+
137
+ #### 商品查詢
138
+ - "查詢商品 iPhone"
139
+ - "有什麼手機商品"
140
+ - "價格 1000 到 5000 的商品"
141
+
142
+ #### 訂單查詢
143
+ - "查詢訂單 ORD001"
144
+ - "我的訂單狀態如何"
145
+
146
+ #### 統計分析
147
+ - "統計用戶數量"
148
+ - "總共有多少筆訂單"
149
+
150
+ #### 說明功能
151
+ - "幫助"
152
+ - "說明"
153
+
154
+ ## 🔧 本地開發
155
+
156
+ 如果要在本地開發:
157
+
158
+ 1. 複製專案
159
+ 2. 建立 `.env` 檔案(參考 `.env.example`)
160
+ 3. 安裝依賴:`pip install -r requirements.txt`
161
+ 4. 執行:`python -m uvicorn backend.main:app --reload --port 7860`
162
+ 5. 使用 ngrok 建立公開 URL 用於 LINE Webhook 測試
163
+
164
+ ## 📝 架構說明
165
+
166
+ - **FastAPI**: Web 框架和 API 端點
167
+ - **Pydantic**: 資料驗證和序列化
168
+ - **LINE Bot SDK**: 處理 LINE 訊息
169
+ - **Supabase**: PostgreSQL 資料庫
170
+ - **NLP Service**: 自然語言處理和意圖識別
171
+ - **Database Service**: 資料庫操作抽象層
172
+
173
+ ## 🚨 注意事項
174
+
175
+ 1. 確保所有環境變數都��確設定
176
+ 2. Supabase 資料表必須先建立
177
+ 3. LINE Bot Webhook URL 必須是 HTTPS
178
+ 4. 測試時注意 LINE 訊息的回應時間限制(30秒)
test_api.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ API 測試腳本
3
+ 用於測試 LINE Bot 的各種功能
4
+ """
5
+
6
+ import requests
7
+ import json
8
+ from typing import Dict, Any
9
+
10
+ class APITester:
11
+ def __init__(self, base_url: str = "http://localhost:7860"):
12
+ self.base_url = base_url
13
+
14
+ def test_health_check(self):
15
+ """測試健康檢查端點"""
16
+ try:
17
+ response = requests.get(f"{self.base_url}/health")
18
+ print(f"Health Check: {response.status_code}")
19
+ print(f"Response: {response.json()}")
20
+ return response.status_code == 200
21
+ except Exception as e:
22
+ print(f"Health check failed: {e}")
23
+ return False
24
+
25
+ def test_root_endpoint(self):
26
+ """測試根端點"""
27
+ try:
28
+ response = requests.get(f"{self.base_url}/")
29
+ print(f"Root Endpoint: {response.status_code}")
30
+ print(f"Response: {response.json()}")
31
+ return response.status_code == 200
32
+ except Exception as e:
33
+ print(f"Root endpoint test failed: {e}")
34
+ return False
35
+
36
+ def simulate_line_message(self, message: str, user_id: str = "test_user"):
37
+ """模擬 LINE 訊息"""
38
+ # 這是一個簡化的 LINE webhook 格式
39
+ webhook_data = {
40
+ "events": [
41
+ {
42
+ "type": "message",
43
+ "message": {
44
+ "type": "text",
45
+ "text": message
46
+ },
47
+ "source": {
48
+ "type": "user",
49
+ "userId": user_id
50
+ },
51
+ "replyToken": "test_reply_token"
52
+ }
53
+ ]
54
+ }
55
+
56
+ try:
57
+ response = requests.post(
58
+ f"{self.base_url}/webhook",
59
+ json=webhook_data,
60
+ headers={"Content-Type": "application/json"}
61
+ )
62
+ print(f"LINE Message Test: {response.status_code}")
63
+ print(f"Message: {message}")
64
+ return response.status_code == 200
65
+ except Exception as e:
66
+ print(f"LINE message test failed: {e}")
67
+ return False
68
+
69
+ def run_tests():
70
+ """執行所有測試"""
71
+ tester = APITester()
72
+
73
+ print("=== API 測試開始 ===\n")
74
+
75
+ # 測試基本端點
76
+ print("1. 測試根端點...")
77
+ tester.test_root_endpoint()
78
+ print()
79
+
80
+ print("2. 測試健康檢查...")
81
+ tester.test_health_check()
82
+ print()
83
+
84
+ # 測試各種訊息
85
+ test_messages = [
86
+ "查詢用戶 張三",
87
+ "找訂單 ORD001",
88
+ "查詢商品 iPhone",
89
+ "價格 1000 到 5000 的商品",
90
+ "統計用戶數量",
91
+ "幫助"
92
+ ]
93
+
94
+ print("3. 測試 LINE 訊息處理...")
95
+ for i, message in enumerate(test_messages, 1):
96
+ print(f"3.{i} 測試訊息: {message}")
97
+ tester.simulate_line_message(message)
98
+ print()
99
+
100
+ print("=== 測試完成 ===")
101
+
102
+ if __name__ == "__main__":
103
+ run_tests()