Commit
·
7e828dc
1
Parent(s):
0503b45
整合 Pydantic AI 框架並優化專案結構
Browse files- .gitignore +162 -0
- API_KEYS_GUIDE.md +0 -215
- BUSINESS_QUERY_GUIDE.md +0 -225
- GROQ_INTEGRATION_COMPLETE.md +0 -237
- LINEBOT_OPERATION_GUIDE.md +296 -0
- OPENROUTER_INTEGRATION.md +0 -273
- POSTGRESQL_MIGRATION_GUIDE.md +0 -342
- PYDANTIC_AI_INTEGRATION.md +227 -0
- another_proj_schemas.py +0 -492
- app.py +0 -7
- backend/main.py +38 -3
- backend/services/enhanced_product_service.py +301 -0
- backend/services/message_router.py +83 -10
- backend/services/pydantic_ai_service.py +426 -0
- groq_python_test.py +0 -67
- requirements.txt +1 -0
- setup_guide.md +0 -178
- test_openrouter_connection.py +0 -223
- test_pydantic_ai_integration.py +168 -0
.gitignore
ADDED
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Byte-compiled / optimized / DLL files
|
2 |
+
__pycache__/
|
3 |
+
*.py[cod]
|
4 |
+
*$py.class
|
5 |
+
|
6 |
+
# C extensions
|
7 |
+
*.so
|
8 |
+
|
9 |
+
# Distribution / packaging
|
10 |
+
.Python
|
11 |
+
build/
|
12 |
+
develop-eggs/
|
13 |
+
dist/
|
14 |
+
downloads/
|
15 |
+
eggs/
|
16 |
+
.eggs/
|
17 |
+
lib/
|
18 |
+
lib64/
|
19 |
+
parts/
|
20 |
+
sdist/
|
21 |
+
var/
|
22 |
+
wheels/
|
23 |
+
pip-wheel-metadata/
|
24 |
+
share/python-wheels/
|
25 |
+
*.egg-info/
|
26 |
+
.installed.cfg
|
27 |
+
*.egg
|
28 |
+
MANIFEST
|
29 |
+
|
30 |
+
# PyInstaller
|
31 |
+
# Usually these files are written by a python script from a template
|
32 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
33 |
+
*.manifest
|
34 |
+
*.spec
|
35 |
+
|
36 |
+
# Installer logs
|
37 |
+
pip-log.txt
|
38 |
+
pip-delete-this-directory.txt
|
39 |
+
|
40 |
+
# Unit test / coverage reports
|
41 |
+
htmlcov/
|
42 |
+
.tox/
|
43 |
+
.nox/
|
44 |
+
.coverage
|
45 |
+
.coverage.*
|
46 |
+
.cache
|
47 |
+
nosetests.xml
|
48 |
+
coverage.xml
|
49 |
+
*.cover
|
50 |
+
*.py,cover
|
51 |
+
.hypothesis/
|
52 |
+
.pytest_cache/
|
53 |
+
|
54 |
+
# Translations
|
55 |
+
*.mo
|
56 |
+
*.pot
|
57 |
+
|
58 |
+
# Django stuff:
|
59 |
+
*.log
|
60 |
+
local_settings.py
|
61 |
+
db.sqlite3
|
62 |
+
db.sqlite3-journal
|
63 |
+
|
64 |
+
# Flask stuff:
|
65 |
+
instance/
|
66 |
+
.webassets-cache
|
67 |
+
|
68 |
+
# Scrapy stuff:
|
69 |
+
.scrapy
|
70 |
+
|
71 |
+
# Sphinx documentation
|
72 |
+
docs/_build/
|
73 |
+
|
74 |
+
# PyBuilder
|
75 |
+
target/
|
76 |
+
|
77 |
+
# Jupyter Notebook
|
78 |
+
.ipynb_checkpoints
|
79 |
+
|
80 |
+
# IPython
|
81 |
+
profile_default/
|
82 |
+
ipython_config.py
|
83 |
+
|
84 |
+
# pyenv
|
85 |
+
.python-version
|
86 |
+
|
87 |
+
# pipenv
|
88 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
89 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
90 |
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
91 |
+
# install all needed dependencies.
|
92 |
+
#Pipfile.lock
|
93 |
+
|
94 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
95 |
+
__pypackages__/
|
96 |
+
|
97 |
+
# Celery stuff
|
98 |
+
celerybeat-schedule
|
99 |
+
celerybeat.pid
|
100 |
+
|
101 |
+
# SageMath parsed files
|
102 |
+
*.sage.py
|
103 |
+
|
104 |
+
# Environments
|
105 |
+
.env
|
106 |
+
.venv
|
107 |
+
env/
|
108 |
+
venv/
|
109 |
+
ENV/
|
110 |
+
env.bak/
|
111 |
+
venv.bak/
|
112 |
+
|
113 |
+
# Spyder project settings
|
114 |
+
.spyderproject
|
115 |
+
.spyproject
|
116 |
+
|
117 |
+
# Rope project settings
|
118 |
+
.ropeproject
|
119 |
+
|
120 |
+
# mkdocs documentation
|
121 |
+
/site
|
122 |
+
|
123 |
+
# mypy
|
124 |
+
.mypy_cache/
|
125 |
+
.dmypy.json
|
126 |
+
dmypy.json
|
127 |
+
|
128 |
+
# Pyre type checker
|
129 |
+
.pyre/
|
130 |
+
|
131 |
+
# IDE
|
132 |
+
.vscode/
|
133 |
+
.idea/
|
134 |
+
*.swp
|
135 |
+
*.swo
|
136 |
+
*~
|
137 |
+
|
138 |
+
# OS
|
139 |
+
.DS_Store
|
140 |
+
.DS_Store?
|
141 |
+
._*
|
142 |
+
.Spotlight-V100
|
143 |
+
.Trashes
|
144 |
+
ehthumbs.db
|
145 |
+
Thumbs.db
|
146 |
+
|
147 |
+
# Project specific
|
148 |
+
*.log
|
149 |
+
.env.local
|
150 |
+
.env.production
|
151 |
+
.env.development
|
152 |
+
|
153 |
+
# Database
|
154 |
+
*.db
|
155 |
+
*.sqlite
|
156 |
+
*.sqlite3
|
157 |
+
|
158 |
+
# Temporary files
|
159 |
+
*.tmp
|
160 |
+
*.temp
|
161 |
+
temp/
|
162 |
+
tmp/
|
API_KEYS_GUIDE.md
DELETED
@@ -1,215 +0,0 @@
|
|
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
DELETED
@@ -1,225 +0,0 @@
|
|
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 |
-
**注意**: 這是一個基礎實作,您可以根據實際業務需求進行擴展和優化。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
GROQ_INTEGRATION_COMPLETE.md
DELETED
@@ -1,237 +0,0 @@
|
|
1 |
-
# 🚀 Groq 整合完成指南
|
2 |
-
|
3 |
-
## 📋 整合概述
|
4 |
-
|
5 |
-
已成功將 Groq AI 服務整合到 LINE Bot 系統中,並實現智能訊息路由功能。
|
6 |
-
|
7 |
-
### ✅ 完成的功能
|
8 |
-
|
9 |
-
1. **Groq 服務整合** (`backend/services/groq_service.py`)
|
10 |
-
- 快速 AI 推理服務
|
11 |
-
- 意圖分析功能
|
12 |
-
- 自然回應生成
|
13 |
-
|
14 |
-
2. **智能訊息路由** (`backend/services/message_router.py`)
|
15 |
-
- 前綴指令路由
|
16 |
-
- 智能意圖識別
|
17 |
-
- 統計功能
|
18 |
-
|
19 |
-
3. **新增 API 端點**
|
20 |
-
- `/chat` - 直接聊天
|
21 |
-
- `/search` - 直接搜尋
|
22 |
-
- `/route` - 智能路由
|
23 |
-
- `/stats` - 使用統計
|
24 |
-
|
25 |
-
4. **LINE Bot 優化**
|
26 |
-
- 避免所有訊息都觸發 NLP
|
27 |
-
- 支援前綴指令
|
28 |
-
- 智能判斷聊天 vs 查詢
|
29 |
-
|
30 |
-
## 🔧 環境變數設定
|
31 |
-
|
32 |
-
### 新增必要變數
|
33 |
-
```bash
|
34 |
-
# Groq 設定 (主要 AI 服務)
|
35 |
-
GROQ_API_KEY=your_groq_api_key_here
|
36 |
-
GROQ_MODEL=qwen/qwen3-32b
|
37 |
-
```
|
38 |
-
|
39 |
-
### 完整環境變數
|
40 |
-
```bash
|
41 |
-
# LINE Bot 設定
|
42 |
-
LINE_CHANNEL_ACCESS_TOKEN=your_line_channel_access_token_here
|
43 |
-
LINE_CHANNEL_SECRET=your_line_channel_secret_here
|
44 |
-
|
45 |
-
# PostgreSQL 資料庫設定
|
46 |
-
DB_HOST=your_database_host_here
|
47 |
-
DB_PORT=6543
|
48 |
-
DB_NAME=your_database_name_here
|
49 |
-
DB_USER=your_database_user_here
|
50 |
-
DB_PASSWORD=your_database_password_here
|
51 |
-
|
52 |
-
# Groq 設定 (主要 AI 服務,推薦)
|
53 |
-
GROQ_API_KEY=your_groq_api_key_here
|
54 |
-
GROQ_MODEL=qwen/qwen3-32b
|
55 |
-
|
56 |
-
# OpenRouter 設定 (可選,備用)
|
57 |
-
OPENROUTER_API_KEY=your_openrouter_api_key_here
|
58 |
-
OPENROUTER_MODEL=anthropic/claude-3-haiku
|
59 |
-
|
60 |
-
# 其他設定
|
61 |
-
DEBUG=False
|
62 |
-
LOG_LEVEL=INFO
|
63 |
-
```
|
64 |
-
|
65 |
-
## 📱 使用方式
|
66 |
-
|
67 |
-
### LINE Bot 指令
|
68 |
-
|
69 |
-
#### 1. 前綴指令模式
|
70 |
-
```
|
71 |
-
💬 聊天模式:
|
72 |
-
/chat 你好,今天天氣如何?
|
73 |
-
|
74 |
-
🔍 商品查詢:
|
75 |
-
/search iPhone 15 Pro
|
76 |
-
/search 價格 1000-5000 的商品
|
77 |
-
|
78 |
-
❓ 幫助:
|
79 |
-
/help 或 幫助
|
80 |
-
```
|
81 |
-
|
82 |
-
#### 2. 智能模式(無前綴)
|
83 |
-
```
|
84 |
-
🤖 系統會自動判斷:
|
85 |
-
|
86 |
-
聊天類型:
|
87 |
-
• "你好!"
|
88 |
-
• "今天天氣如何?"
|
89 |
-
• "推薦一些功能"
|
90 |
-
|
91 |
-
查詢類型:
|
92 |
-
• "iPhone 還有庫存嗎?"
|
93 |
-
• "1000到5000的商品有哪些?"
|
94 |
-
• "我的訂單狀態如何?"
|
95 |
-
```
|
96 |
-
|
97 |
-
### API 端點使用
|
98 |
-
|
99 |
-
#### 直接聊天 API
|
100 |
-
```bash
|
101 |
-
curl -X POST http://localhost:7860/chat \
|
102 |
-
-H "Content-Type: application/json" \
|
103 |
-
-d '{"message": "你好!", "user_id": "test_user"}'
|
104 |
-
```
|
105 |
-
|
106 |
-
#### 直接搜尋 API
|
107 |
-
```bash
|
108 |
-
curl -X POST http://localhost:7860/search \
|
109 |
-
-H "Content-Type: application/json" \
|
110 |
-
-d '{"message": "iPhone 15", "user_id": "test_user"}'
|
111 |
-
```
|
112 |
-
|
113 |
-
#### 智能路由 API
|
114 |
-
```bash
|
115 |
-
curl -X POST http://localhost:7860/route \
|
116 |
-
-H "Content-Type: application/json" \
|
117 |
-
-d '{"message": "iPhone 還有庫存嗎?", "user_id": "test_user"}'
|
118 |
-
```
|
119 |
-
|
120 |
-
#### 統計 API
|
121 |
-
```bash
|
122 |
-
curl http://localhost:7860/stats
|
123 |
-
```
|
124 |
-
|
125 |
-
## 🧪 測試
|
126 |
-
|
127 |
-
### 執行整合測試
|
128 |
-
```bash
|
129 |
-
python tmp_rovodev_test_groq_integration.py
|
130 |
-
```
|
131 |
-
|
132 |
-
### 測試項目
|
133 |
-
- ✅ 健康檢查
|
134 |
-
- ✅ 聊天 API
|
135 |
-
- ✅ 搜尋 API
|
136 |
-
- ✅ 智能路由
|
137 |
-
- ✅ 統計功能
|
138 |
-
- ✅ LINE Webhook 模擬
|
139 |
-
|
140 |
-
## 🔄 路由邏輯
|
141 |
-
|
142 |
-
### 訊息處理流程
|
143 |
-
```
|
144 |
-
用戶訊息 → MessageRouter
|
145 |
-
↓
|
146 |
-
前綴檢查 (/chat, /search, /help)
|
147 |
-
↓
|
148 |
-
智能意圖分析 (Groq)
|
149 |
-
↓
|
150 |
-
路由到對應服務
|
151 |
-
↓
|
152 |
-
生成回應
|
153 |
-
```
|
154 |
-
|
155 |
-
### 意圖分析
|
156 |
-
- **搜尋意圖**: 商品查詢、庫存、訂單等
|
157 |
-
- **聊天意圖**: 問候、閒聊、一般問題
|
158 |
-
- **幫助意圖**: 說明、指令、功能介紹
|
159 |
-
|
160 |
-
## 📊 優勢
|
161 |
-
|
162 |
-
### 相比 OpenRouter
|
163 |
-
- ⚡ **速度更快**: Groq 推理速度顯著提升
|
164 |
-
- 💰 **成本更低**: 更經濟的 API 定價
|
165 |
-
- 🎯 **中文優化**: qwen3-32b 對中文支援更好
|
166 |
-
|
167 |
-
### 智能路由優勢
|
168 |
-
- 🎯 **精準路由**: 避免誤觸發 NLP 分析
|
169 |
-
- 🔀 **靈活切換**: 支援指令和智能模式
|
170 |
-
- 📈 **使用統計**: 追蹤路由效果
|
171 |
-
|
172 |
-
## 🚨 注意事項
|
173 |
-
|
174 |
-
### 1. API Key 安全
|
175 |
-
- 請妥善保管 Groq API Key
|
176 |
-
- 不要將 API Key 提交到版本控制
|
177 |
-
|
178 |
-
### 2. 錯誤處理
|
179 |
-
- Groq 服務不可用時會自動降級
|
180 |
-
- 保持原有功能的向後相容性
|
181 |
-
|
182 |
-
### 3. 效能監控
|
183 |
-
- 使用 `/stats` 監控路由統計
|
184 |
-
- 關注 Groq API 使用量
|
185 |
-
|
186 |
-
## 🔧 故障排除
|
187 |
-
|
188 |
-
### Groq 服務不可用
|
189 |
-
```python
|
190 |
-
# 檢查 API Key 設定
|
191 |
-
print(settings.GROQ_API_KEY)
|
192 |
-
|
193 |
-
# 檢查服務狀態
|
194 |
-
groq_service = GroqService()
|
195 |
-
print(groq_service.is_available())
|
196 |
-
```
|
197 |
-
|
198 |
-
### 路由統計異常
|
199 |
-
```bash
|
200 |
-
# 查看路由統計
|
201 |
-
curl http://localhost:7860/stats
|
202 |
-
```
|
203 |
-
|
204 |
-
### LINE Bot 無回應
|
205 |
-
1. 檢查 Webhook URL 設定
|
206 |
-
2. 確認環境變數正確
|
207 |
-
3. 查看應用程式日誌
|
208 |
-
|
209 |
-
## 📈 後續優化建議
|
210 |
-
|
211 |
-
1. **快取機制**: 為常見查詢添加快取
|
212 |
-
2. **A/B 測試**: 比較不同路由策略效果
|
213 |
-
3. **用戶偏好**: 學習用戶使用習慣
|
214 |
-
4. **多語言支援**: 擴展到其他語言
|
215 |
-
|
216 |
-
## 🎉 部署
|
217 |
-
|
218 |
-
### Hugging Face Spaces
|
219 |
-
1. 更新環境變數(加入 GROQ_API_KEY)
|
220 |
-
2. 推送代碼更新
|
221 |
-
3. 等待自動部署完成
|
222 |
-
|
223 |
-
### 本地測試
|
224 |
-
```bash
|
225 |
-
# 安裝新依賴
|
226 |
-
pip install -r requirements.txt
|
227 |
-
|
228 |
-
# 設定環境變數
|
229 |
-
export GROQ_API_KEY=your_groq_api_key
|
230 |
-
|
231 |
-
# 啟動服務
|
232 |
-
python -m uvicorn backend.main:app --reload --port 7860
|
233 |
-
```
|
234 |
-
|
235 |
-
---
|
236 |
-
|
237 |
-
**🎊 整合完成!** 您的 LINE Bot 現在具備了更快速、更智能的 AI 服務能力!
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
LINEBOT_OPERATION_GUIDE.md
ADDED
@@ -0,0 +1,296 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# LineBot 操作指南
|
2 |
+
|
3 |
+
## 🤖 系統概述
|
4 |
+
|
5 |
+
本 LineBot 系統整合了 Pydantic AI 框架,提供智能商品查詢、聊天和業務查詢功能。系統設計考慮了 LINE 群組使用場景,避免不必要的 AI token 消耗。
|
6 |
+
|
7 |
+
## 📱 用戶操作指南
|
8 |
+
|
9 |
+
### 基本指令
|
10 |
+
|
11 |
+
#### 1. 聊天模式
|
12 |
+
```
|
13 |
+
/chat 你好!今天天氣如何?
|
14 |
+
/chat 推薦一些好用的功能
|
15 |
+
```
|
16 |
+
- **用途**:一般對話、問候、非業務相關問題
|
17 |
+
- **適用場景**:群組聊天、客戶服務
|
18 |
+
- **AI 服務**:使用 Groq 進行自然對話
|
19 |
+
|
20 |
+
#### 2. 商品查詢模式
|
21 |
+
```
|
22 |
+
/search iPhone 15 Pro
|
23 |
+
/search 價格 1000-5000
|
24 |
+
/search 庫存不足的商品
|
25 |
+
```
|
26 |
+
- **用途**:傳統的商品搜尋和業務查詢
|
27 |
+
- **適用場景**:明確的商品查詢需求
|
28 |
+
- **查詢範圍**:商品、訂單、庫存、客戶資料
|
29 |
+
|
30 |
+
#### 3. 幫助指令
|
31 |
+
```
|
32 |
+
/help
|
33 |
+
幫助
|
34 |
+
說明
|
35 |
+
```
|
36 |
+
- **用途**:顯示系統使用說明
|
37 |
+
- **回應**:完整的功能介紹和使用範例
|
38 |
+
|
39 |
+
### 智能模式(無前綴)
|
40 |
+
|
41 |
+
#### 商品推薦查詢
|
42 |
+
```
|
43 |
+
是否有推薦貓砂?
|
44 |
+
有什麼狗糧推薦?
|
45 |
+
推薦一些寵物用品
|
46 |
+
```
|
47 |
+
- **特點**:自動識別推薦意圖
|
48 |
+
- **回應**:智能推薦相關商品,包含庫存資訊
|
49 |
+
- **優勢**:更自然的對話方式
|
50 |
+
|
51 |
+
#### 庫存查詢
|
52 |
+
```
|
53 |
+
查詢iPhone庫存
|
54 |
+
寵物用品還有多少?
|
55 |
+
低庫存商品有哪些?
|
56 |
+
```
|
57 |
+
- **特點**:自動識別庫存查詢意圖
|
58 |
+
- **回應**:詳細的庫存狀態和警告提醒
|
59 |
+
|
60 |
+
#### 一般對話
|
61 |
+
```
|
62 |
+
你好!
|
63 |
+
今天天氣如何?
|
64 |
+
謝謝你的幫助
|
65 |
+
```
|
66 |
+
- **特點**:自動識別為聊天意圖
|
67 |
+
- **回應**:友善的對話回應
|
68 |
+
|
69 |
+
## 🏢 管理員操作指南
|
70 |
+
|
71 |
+
### 系統監控
|
72 |
+
|
73 |
+
#### 1. 健康檢查
|
74 |
+
```bash
|
75 |
+
curl http://localhost:7860/health
|
76 |
+
```
|
77 |
+
**回應內容**:
|
78 |
+
- 系統狀態
|
79 |
+
- 資料庫連線狀態
|
80 |
+
- 資料庫連線資訊
|
81 |
+
|
82 |
+
#### 2. 路由統計
|
83 |
+
在 LINE 中輸入:`統計` 或 `stats`
|
84 |
+
|
85 |
+
**統計內容**:
|
86 |
+
- 各模式使用次數和百分比
|
87 |
+
- Groq 服務狀態
|
88 |
+
- Pydantic AI 服務狀態
|
89 |
+
|
90 |
+
### API 端點管理
|
91 |
+
|
92 |
+
#### 1. 直接聊天 API
|
93 |
+
```bash
|
94 |
+
curl -X POST "http://localhost:7860/chat" \
|
95 |
+
-H "Content-Type: application/json" \
|
96 |
+
-d '{
|
97 |
+
"message": "今天天氣如何?",
|
98 |
+
"user_id": "admin_user"
|
99 |
+
}'
|
100 |
+
```
|
101 |
+
|
102 |
+
#### 2. 直接搜尋 API
|
103 |
+
```bash
|
104 |
+
curl -X POST "http://localhost:7860/search" \
|
105 |
+
-H "Content-Type: application/json" \
|
106 |
+
-d '{
|
107 |
+
"message": "iPhone 15",
|
108 |
+
"user_id": "admin_user"
|
109 |
+
}'
|
110 |
+
```
|
111 |
+
|
112 |
+
#### 3. 商品查詢 API
|
113 |
+
```bash
|
114 |
+
curl -X POST "http://localhost:7860/product-query" \
|
115 |
+
-H "Content-Type: application/json" \
|
116 |
+
-d '{
|
117 |
+
"message": "是否有推薦貓砂?",
|
118 |
+
"user_id": "admin_user"
|
119 |
+
}'
|
120 |
+
```
|
121 |
+
|
122 |
+
#### 4. 智能路由 API
|
123 |
+
```bash
|
124 |
+
curl -X POST "http://localhost:7860/route" \
|
125 |
+
-H "Content-Type: application/json" \
|
126 |
+
-d '{
|
127 |
+
"message": "有什麼狗糧推薦?",
|
128 |
+
"user_id": "admin_user"
|
129 |
+
}'
|
130 |
+
```
|
131 |
+
|
132 |
+
### 系統配置
|
133 |
+
|
134 |
+
#### 環境變數檢查
|
135 |
+
```bash
|
136 |
+
# 檢查必要的環境變數
|
137 |
+
echo $GROQ_API_KEY
|
138 |
+
echo $DB_HOST
|
139 |
+
echo $LINE_CHANNEL_ACCESS_TOKEN
|
140 |
+
```
|
141 |
+
|
142 |
+
#### 服務狀態檢查
|
143 |
+
```python
|
144 |
+
# 在 Python 中檢查服務狀態
|
145 |
+
from backend.services.pydantic_ai_service import ProductQueryService
|
146 |
+
from backend.services.groq_service import GroqService
|
147 |
+
|
148 |
+
product_service = ProductQueryService()
|
149 |
+
groq_service = GroqService()
|
150 |
+
|
151 |
+
print(f"Pydantic AI 可用: {product_service.is_available()}")
|
152 |
+
print(f"Groq 服務可用: {groq_service.is_available()}")
|
153 |
+
```
|
154 |
+
|
155 |
+
## 🔧 故障排除
|
156 |
+
|
157 |
+
### 常見問題
|
158 |
+
|
159 |
+
#### 1. 商品查詢沒有結果
|
160 |
+
**可能原因**:
|
161 |
+
- 資料庫中沒有相關商品
|
162 |
+
- 關鍵字匹配不準確
|
163 |
+
- 商品被標記為已刪除
|
164 |
+
|
165 |
+
**解決方案**:
|
166 |
+
- 檢查資料庫商品資料
|
167 |
+
- 嘗試使用不同的關鍵字
|
168 |
+
- 使用 `/search` 進行傳統查詢
|
169 |
+
|
170 |
+
#### 2. Pydantic AI 服務不可用
|
171 |
+
**錯誤訊息**:「商品查詢服務暫時無法使用」
|
172 |
+
|
173 |
+
**解決方案**:
|
174 |
+
- 檢查 `GROQ_API_KEY` 環境變數
|
175 |
+
- 確認 Groq API 配額
|
176 |
+
- 系統會自動降級到傳統搜尋
|
177 |
+
|
178 |
+
#### 3. 資料庫連線失敗
|
179 |
+
**錯誤訊息**:「資料庫連線測試失敗」
|
180 |
+
|
181 |
+
**解決方案**:
|
182 |
+
- 檢查資料庫連線參數
|
183 |
+
- 確認資料庫服務運行狀態
|
184 |
+
- 檢查網路連線
|
185 |
+
|
186 |
+
### 日誌查看
|
187 |
+
|
188 |
+
#### 應用程式日誌
|
189 |
+
```bash
|
190 |
+
# 查看即時日誌
|
191 |
+
tail -f app.log
|
192 |
+
|
193 |
+
# 搜尋特定錯誤
|
194 |
+
grep "ERROR" app.log
|
195 |
+
grep "商品查詢錯誤" app.log
|
196 |
+
```
|
197 |
+
|
198 |
+
#### 特定功能日誌
|
199 |
+
```bash
|
200 |
+
# Pydantic AI 相關
|
201 |
+
grep "Pydantic AI" app.log
|
202 |
+
|
203 |
+
# 路由統計
|
204 |
+
grep "智能路由" app.log
|
205 |
+
|
206 |
+
# 資料庫查詢
|
207 |
+
grep "資料庫" app.log
|
208 |
+
```
|
209 |
+
|
210 |
+
## 📊 效能監控
|
211 |
+
|
212 |
+
### API 回應時間
|
213 |
+
- **聊天模式**:通常 1-3 秒
|
214 |
+
- **商品查詢**:通常 2-5 秒
|
215 |
+
- **智能路由**:通常 1-4 秒
|
216 |
+
|
217 |
+
### 資源使用
|
218 |
+
- **記憶體**:約 200-500MB
|
219 |
+
- **CPU**:低負載時 < 10%
|
220 |
+
- **網路**:主要是 API 調用
|
221 |
+
|
222 |
+
### Token 使用監控
|
223 |
+
- 監控 Groq API 使用量
|
224 |
+
- 設定使用量警告
|
225 |
+
- 考慮實施使用限制
|
226 |
+
|
227 |
+
## 🚀 部署指南
|
228 |
+
|
229 |
+
### 開發環境
|
230 |
+
```bash
|
231 |
+
# 1. 安裝依賴
|
232 |
+
pip install -r requirements.txt
|
233 |
+
|
234 |
+
# 2. 設定環境變數
|
235 |
+
cp .env.example .env
|
236 |
+
# 編輯 .env 文件
|
237 |
+
|
238 |
+
# 3. 運行測試
|
239 |
+
python test_pydantic_ai_integration.py
|
240 |
+
|
241 |
+
# 4. 啟動服務
|
242 |
+
uvicorn backend.main:app --host 0.0.0.0 --port 7860
|
243 |
+
```
|
244 |
+
|
245 |
+
### 生產環境
|
246 |
+
```bash
|
247 |
+
# 使用 Docker
|
248 |
+
docker build -t linebot-pydantic .
|
249 |
+
docker run -p 7860:7860 --env-file .env linebot-pydantic
|
250 |
+
|
251 |
+
# 或使用 Docker Compose
|
252 |
+
docker-compose up -d
|
253 |
+
```
|
254 |
+
|
255 |
+
### LINE Webhook 設定
|
256 |
+
1. 在 LINE Developers Console 設定 Webhook URL
|
257 |
+
2. URL 格式:`https://your-domain.com/webhook`
|
258 |
+
3. 確認 SSL 憑證有效
|
259 |
+
|
260 |
+
## 📋 維護檢查清單
|
261 |
+
|
262 |
+
### 每日檢查
|
263 |
+
- [ ] 系統健康狀態
|
264 |
+
- [ ] API 回應時間
|
265 |
+
- [ ] 錯誤日誌
|
266 |
+
- [ ] Groq API 使用量
|
267 |
+
|
268 |
+
### 每週檢查
|
269 |
+
- [ ] 路由統計分析
|
270 |
+
- [ ] 資料庫效能
|
271 |
+
- [ ] 商品資料更新
|
272 |
+
- [ ] 用戶回饋收集
|
273 |
+
|
274 |
+
### 每月檢查
|
275 |
+
- [ ] 系統效能評估
|
276 |
+
- [ ] 功能使用分析
|
277 |
+
- [ ] 安全性檢查
|
278 |
+
- [ ] 備份驗證
|
279 |
+
|
280 |
+
## 📞 支援聯絡
|
281 |
+
|
282 |
+
### 技術問題
|
283 |
+
- 檢查日誌文件
|
284 |
+
- 參考故障排除指南
|
285 |
+
- 聯絡系統管理員
|
286 |
+
|
287 |
+
### 功能建議
|
288 |
+
- 記錄用戶需求
|
289 |
+
- 評估實施可行性
|
290 |
+
- 規劃功能更新
|
291 |
+
|
292 |
+
---
|
293 |
+
|
294 |
+
**版本**:v1.0
|
295 |
+
**更新日期**:2025-01-12
|
296 |
+
**維護團隊**:開發團隊
|
OPENROUTER_INTEGRATION.md
DELETED
@@ -1,273 +0,0 @@
|
|
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
DELETED
@@ -1,342 +0,0 @@
|
|
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 |
-
**注意**: 遷移前請務必備份您的資料庫,並在測試環境中驗證所有功能正常運作。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
PYDANTIC_AI_INTEGRATION.md
ADDED
@@ -0,0 +1,227 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Pydantic AI 整合說明文件
|
2 |
+
|
3 |
+
## 📋 概述
|
4 |
+
|
5 |
+
本次更新整合了 Pydantic AI 框架到 LineBot 專案中,主要解決商品查詢準確性問題,特別是像 "是否有推薦貓砂?" 這類自然語言查詢。
|
6 |
+
|
7 |
+
## 🎯 解決的問題
|
8 |
+
|
9 |
+
### 原有問題
|
10 |
+
- 用戶詢問 "是否有推薦貓砂?" 時,系統常常回答沒有相關資訊
|
11 |
+
- 商品查詢意圖識別不準確
|
12 |
+
- 缺乏智能推薦功能
|
13 |
+
|
14 |
+
### 解決方案
|
15 |
+
- 整合 Pydantic AI 框架,提供更精確的意圖識別
|
16 |
+
- 移植 inventory 專案的查詢功能,增強商品搜尋能力
|
17 |
+
- 新增智能商品推薦系統
|
18 |
+
|
19 |
+
## 🔧 技術架構變更
|
20 |
+
|
21 |
+
### 新增組件
|
22 |
+
|
23 |
+
1. **Pydantic AI 服務** (`backend/services/pydantic_ai_service.py`)
|
24 |
+
- 商品查詢意圖分析
|
25 |
+
- 智能推薦系統
|
26 |
+
- 結構化輸出驗證
|
27 |
+
|
28 |
+
2. **增強商品服務** (`backend/services/enhanced_product_service.py`)
|
29 |
+
- 進階商品搜尋
|
30 |
+
- 商品推薦功能
|
31 |
+
- 庫存狀態分析
|
32 |
+
|
33 |
+
3. **新增依賴**
|
34 |
+
- `pydantic-ai[groq]`: Pydantic AI 框架
|
35 |
+
|
36 |
+
### 修改組件
|
37 |
+
|
38 |
+
1. **訊息路由器** (`backend/services/message_router.py`)
|
39 |
+
- 新增商品查詢路由邏輯
|
40 |
+
- 整合 Pydantic AI 意圖分析
|
41 |
+
- 保留原有 `/chat` 和 `/search` 功能
|
42 |
+
|
43 |
+
2. **主應用** (`backend/main.py`)
|
44 |
+
- 新增 `/product-query` API 端點
|
45 |
+
|
46 |
+
## 🚀 功能特性
|
47 |
+
|
48 |
+
### 1. 智能意圖識別
|
49 |
+
```python
|
50 |
+
# 自動識別商品查詢意圖
|
51 |
+
"是否有推薦貓砂?" → 商品推薦查詢 (信心度: 0.8)
|
52 |
+
"有什麼狗糧推薦?" → 商品推薦查詢 (信心度: 0.9)
|
53 |
+
"查詢iPhone庫存" → 庫存查詢 (信心度: 0.7)
|
54 |
+
```
|
55 |
+
|
56 |
+
### 2. 智能商品推薦
|
57 |
+
- 基於關鍵字的商品匹配
|
58 |
+
- 優先顯示有庫存的商品
|
59 |
+
- 提供推薦原因說明
|
60 |
+
|
61 |
+
### 3. 增強的庫存資訊
|
62 |
+
- 實時庫存狀態顯示
|
63 |
+
- 庫存警告提醒
|
64 |
+
- 緊急程度分級
|
65 |
+
|
66 |
+
## 📱 使用指南
|
67 |
+
|
68 |
+
### LINE Bot 使用方式
|
69 |
+
|
70 |
+
#### 1. 指令模式(原有功能保留)
|
71 |
+
```
|
72 |
+
/chat 今天天氣如何? # 聊天模式
|
73 |
+
/search iPhone 15 Pro # 商品查詢模式
|
74 |
+
/help # 顯示幫助
|
75 |
+
```
|
76 |
+
|
77 |
+
#### 2. 智能模式(新增功能)
|
78 |
+
直接輸入訊息,系統自動判斷意圖:
|
79 |
+
```
|
80 |
+
是否有推薦貓砂? # → 智能商品推薦
|
81 |
+
有什麼狗糧推薦? # → 智能商品推薦
|
82 |
+
查詢寵物用品庫存 # → 庫存查詢
|
83 |
+
iPhone 還有庫存嗎? # → 庫存查詢
|
84 |
+
你好! # → 聊天模式
|
85 |
+
```
|
86 |
+
|
87 |
+
### API 端點
|
88 |
+
|
89 |
+
#### 1. 原有端點(保留)
|
90 |
+
- `POST /chat`: 直接聊天
|
91 |
+
- `POST /search`: 直接搜尋
|
92 |
+
- `POST /route`: 智能路由
|
93 |
+
|
94 |
+
#### 2. 新增端點
|
95 |
+
- `POST /product-query`: 專門的商品查詢
|
96 |
+
|
97 |
+
### API 使用範例
|
98 |
+
|
99 |
+
```bash
|
100 |
+
# 商品查詢 API
|
101 |
+
curl -X POST "http://localhost:7860/product-query" \
|
102 |
+
-H "Content-Type: application/json" \
|
103 |
+
-d '{
|
104 |
+
"message": "是否有推薦貓砂?",
|
105 |
+
"user_id": "test_user"
|
106 |
+
}'
|
107 |
+
|
108 |
+
# 智能路由 API
|
109 |
+
curl -X POST "http://localhost:7860/route" \
|
110 |
+
-H "Content-Type: application/json" \
|
111 |
+
-d '{
|
112 |
+
"message": "有什麼狗糧推薦?",
|
113 |
+
"user_id": "test_user"
|
114 |
+
}'
|
115 |
+
```
|
116 |
+
|
117 |
+
## ⚙️ 配置要求
|
118 |
+
|
119 |
+
### 環境變數
|
120 |
+
```env
|
121 |
+
# 必需
|
122 |
+
GROQ_API_KEY=your_groq_api_key_here
|
123 |
+
DB_HOST=your_database_host
|
124 |
+
DB_NAME=your_database_name
|
125 |
+
DB_USER=your_database_user
|
126 |
+
DB_PASSWORD=your_database_password
|
127 |
+
|
128 |
+
# LINE Bot (如果使用)
|
129 |
+
LINE_CHANNEL_ACCESS_TOKEN=your_line_token
|
130 |
+
LINE_CHANNEL_SECRET=your_line_secret
|
131 |
+
```
|
132 |
+
|
133 |
+
### 依賴安裝
|
134 |
+
```bash
|
135 |
+
pip install -r requirements.txt
|
136 |
+
```
|
137 |
+
|
138 |
+
## 🧪 測試
|
139 |
+
|
140 |
+
### 運行測試腳本
|
141 |
+
```bash
|
142 |
+
python test_pydantic_ai_integration.py
|
143 |
+
```
|
144 |
+
|
145 |
+
### 測試案例
|
146 |
+
1. 商品推薦查詢
|
147 |
+
2. 庫存查詢
|
148 |
+
3. 意圖分析準確性
|
149 |
+
4. 路由統計
|
150 |
+
|
151 |
+
## 📊 路由統計
|
152 |
+
|
153 |
+
系統會追蹤各種路由模式的使用情況:
|
154 |
+
- 💬 聊天模式
|
155 |
+
- 🔍 搜尋模式
|
156 |
+
- 🛍️ 商品查詢(新增)
|
157 |
+
- 🧠 智能路由
|
158 |
+
- ❓ 幫助模式
|
159 |
+
- ❌ 錯誤次數
|
160 |
+
|
161 |
+
查看統計:輸入 `統計` 或 `stats`
|
162 |
+
|
163 |
+
## 🔄 路由邏輯
|
164 |
+
|
165 |
+
```
|
166 |
+
用戶訊息
|
167 |
+
↓
|
168 |
+
是否有前綴?
|
169 |
+
├─ /chat → 聊天模式
|
170 |
+
├─ /search → 搜尋模式
|
171 |
+
├─ /help → 幫助模式
|
172 |
+
└─ 無前綴 → 智能路由
|
173 |
+
↓
|
174 |
+
Pydantic AI 意圖分析
|
175 |
+
↓
|
176 |
+
商品查詢?(信心度>0.6)
|
177 |
+
├─ 是 → 商品查詢模式
|
178 |
+
└─ 否 → 關鍵字分析
|
179 |
+
↓
|
180 |
+
搜尋關鍵字?
|
181 |
+
├─ 是 → 搜尋模式
|
182 |
+
└─ 否 → 聊天模式
|
183 |
+
```
|
184 |
+
|
185 |
+
## 🚨 注意事項
|
186 |
+
|
187 |
+
### LINE 群組使用
|
188 |
+
- **保留前綴設計**:在群組中使用 `/chat` 和 `/search` 前綴,避免所有訊息都觸發 AI
|
189 |
+
- **智能路由**:只在私聊或明確需要時使用無前綴訊息
|
190 |
+
|
191 |
+
### 效能考量
|
192 |
+
- Pydantic AI 查詢會消耗 Groq API token
|
193 |
+
- 建議在生產環境中監控 API 使用量
|
194 |
+
- 可以調整意圖識別的信心度閾值
|
195 |
+
|
196 |
+
### 錯誤處理
|
197 |
+
- 當 Pydantic AI 不可用���,會自動降級到原有的搜尋功能
|
198 |
+
- 完整的錯誤日誌記錄
|
199 |
+
|
200 |
+
## 📈 預期效果
|
201 |
+
|
202 |
+
### 查詢準確性提升
|
203 |
+
- **之前**:「是否有推薦貓砂?」→ 沒有相關資訊
|
204 |
+
- **現在**:「是否有推薦貓砂?」→ 顯示貓砂商品列表,包含庫存狀態
|
205 |
+
|
206 |
+
### 用戶體驗改善
|
207 |
+
- 更自然的對話方式
|
208 |
+
- 智能推薦功能
|
209 |
+
- 詳細的庫存資訊
|
210 |
+
|
211 |
+
### 系統可維護性
|
212 |
+
- 模組化設計
|
213 |
+
- 完整的測試覆蓋
|
214 |
+
- 清晰的錯誤處理
|
215 |
+
|
216 |
+
## 🔮 未來擴展
|
217 |
+
|
218 |
+
1. **多語言支援**:擴展到英文查詢
|
219 |
+
2. **個人化推薦**:基於用戶歷史的推薦
|
220 |
+
3. **語音查詢**:整合語音識別
|
221 |
+
4. **圖片查詢**:支援商品圖片搜尋
|
222 |
+
|
223 |
+
---
|
224 |
+
|
225 |
+
**版本**:v1.0
|
226 |
+
**更新日期**:2025-01-12
|
227 |
+
**作者**:Augment Agent
|
another_proj_schemas.py
DELETED
@@ -1,492 +0,0 @@
|
|
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
DELETED
@@ -1,7 +0,0 @@
|
|
1 |
-
from fastapi import FastAPI
|
2 |
-
|
3 |
-
app = FastAPI()
|
4 |
-
|
5 |
-
@app.get("/")
|
6 |
-
def greet_json():
|
7 |
-
return {"Hello": "World!"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/main.py
CHANGED
@@ -147,17 +147,17 @@ async def smart_route(request: dict):
|
|
147 |
try:
|
148 |
message = request.get("message", "")
|
149 |
user_id = request.get("user_id", "api_user")
|
150 |
-
|
151 |
if not message:
|
152 |
return {
|
153 |
"success": False,
|
154 |
"error": "訊息內容不能為空"
|
155 |
}
|
156 |
-
|
157 |
# 使用智能路由
|
158 |
result = message_router.route_message(message, user_id)
|
159 |
return result
|
160 |
-
|
161 |
except Exception as e:
|
162 |
logger.error(f"智能路由 API 錯誤: {str(e)}")
|
163 |
return {
|
@@ -166,6 +166,41 @@ async def smart_route(request: dict):
|
|
166 |
"text": "路由服務暫時無法使用"
|
167 |
}
|
168 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
169 |
@app.get("/stats")
|
170 |
async def get_stats():
|
171 |
"""取得路由統計"""
|
|
|
147 |
try:
|
148 |
message = request.get("message", "")
|
149 |
user_id = request.get("user_id", "api_user")
|
150 |
+
|
151 |
if not message:
|
152 |
return {
|
153 |
"success": False,
|
154 |
"error": "訊息內容不能為空"
|
155 |
}
|
156 |
+
|
157 |
# 使用智能路由
|
158 |
result = message_router.route_message(message, user_id)
|
159 |
return result
|
160 |
+
|
161 |
except Exception as e:
|
162 |
logger.error(f"智能路由 API 錯誤: {str(e)}")
|
163 |
return {
|
|
|
166 |
"text": "路由服務暫時無法使用"
|
167 |
}
|
168 |
|
169 |
+
@app.post("/product-query")
|
170 |
+
async def product_query(request: dict):
|
171 |
+
"""專門的商品查詢 API - 使用 Pydantic AI"""
|
172 |
+
try:
|
173 |
+
message = request.get("message", "")
|
174 |
+
user_id = request.get("user_id", "api_user")
|
175 |
+
|
176 |
+
if not message:
|
177 |
+
return {
|
178 |
+
"success": False,
|
179 |
+
"error": "查詢內容不能為空"
|
180 |
+
}
|
181 |
+
|
182 |
+
# 直接使用商品查詢服務
|
183 |
+
from backend.services.pydantic_ai_service import ProductQueryService
|
184 |
+
product_service = ProductQueryService()
|
185 |
+
|
186 |
+
if not product_service.is_available():
|
187 |
+
return {
|
188 |
+
"success": False,
|
189 |
+
"error": "商品查詢服務不可用,請檢查 GROQ_API_KEY 設定",
|
190 |
+
"text": "商品查詢服務暫時無法使用"
|
191 |
+
}
|
192 |
+
|
193 |
+
result = product_service.process_product_query_sync(message, user_id)
|
194 |
+
return result
|
195 |
+
|
196 |
+
except Exception as e:
|
197 |
+
logger.error(f"商品查詢 API 錯誤: {str(e)}")
|
198 |
+
return {
|
199 |
+
"success": False,
|
200 |
+
"error": str(e),
|
201 |
+
"text": "商品查詢服務暫時無法使用"
|
202 |
+
}
|
203 |
+
|
204 |
@app.get("/stats")
|
205 |
async def get_stats():
|
206 |
"""取得路由統計"""
|
backend/services/enhanced_product_service.py
ADDED
@@ -0,0 +1,301 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
增強的商品查詢服務 - 移植自 inventory_proj_routers
|
3 |
+
提供更精確的商品搜尋和庫存查詢功能
|
4 |
+
"""
|
5 |
+
|
6 |
+
import logging
|
7 |
+
from typing import List, Optional, Dict, Any
|
8 |
+
from sqlalchemy.orm import Session, joinedload
|
9 |
+
from sqlalchemy import or_, and_, func
|
10 |
+
from backend.database.connection import get_database_session, close_database_session
|
11 |
+
from backend.database.models import Product, Category, PurchaseOrder, SalesOrder
|
12 |
+
from backend.models.schemas import DatabaseResult
|
13 |
+
|
14 |
+
logger = logging.getLogger(__name__)
|
15 |
+
|
16 |
+
class EnhancedProductService:
|
17 |
+
"""增強的商品查詢服務"""
|
18 |
+
|
19 |
+
def __init__(self):
|
20 |
+
pass
|
21 |
+
|
22 |
+
def search_products_advanced(
|
23 |
+
self,
|
24 |
+
query_text: str = None,
|
25 |
+
category_name: str = None,
|
26 |
+
warehouse: str = None,
|
27 |
+
include_stock_info: bool = True,
|
28 |
+
min_stock: int = None,
|
29 |
+
max_stock: int = None,
|
30 |
+
limit: int = 20
|
31 |
+
) -> DatabaseResult:
|
32 |
+
"""
|
33 |
+
進階商品搜尋功能
|
34 |
+
|
35 |
+
Args:
|
36 |
+
query_text: 搜尋關鍵字(商品名稱、編號、條碼)
|
37 |
+
category_name: 分類名稱
|
38 |
+
warehouse: 倉庫名稱
|
39 |
+
include_stock_info: 是否包含庫存資訊
|
40 |
+
min_stock: 最小庫存量
|
41 |
+
max_stock: 最大庫存量
|
42 |
+
limit: 查詢限制數量
|
43 |
+
"""
|
44 |
+
db = None
|
45 |
+
try:
|
46 |
+
db = get_database_session()
|
47 |
+
|
48 |
+
# 建立基本查詢,預載入分類資訊
|
49 |
+
query = db.query(Product).options(
|
50 |
+
joinedload(Product.category)
|
51 |
+
).filter(Product.is_deleted == False)
|
52 |
+
|
53 |
+
# 關鍵字搜尋 - 支援商品名稱、編號、條碼
|
54 |
+
if query_text:
|
55 |
+
search_terms = query_text.strip().split()
|
56 |
+
for term in search_terms:
|
57 |
+
search_filter = or_(
|
58 |
+
Product.productName.ilike(f"%{term}%"),
|
59 |
+
Product.productCode.ilike(f"%{term}%"),
|
60 |
+
Product.barcode.ilike(f"%{term}%")
|
61 |
+
)
|
62 |
+
query = query.filter(search_filter)
|
63 |
+
|
64 |
+
# 分類篩選
|
65 |
+
if category_name:
|
66 |
+
query = query.join(Category).filter(
|
67 |
+
Category.name.ilike(f"%{category_name}%")
|
68 |
+
)
|
69 |
+
|
70 |
+
# 倉庫篩選
|
71 |
+
if warehouse:
|
72 |
+
query = query.filter(Product.warehouse.ilike(f"%{warehouse}%"))
|
73 |
+
|
74 |
+
# 庫存範圍篩選
|
75 |
+
if min_stock is not None:
|
76 |
+
query = query.filter(Product.stock >= min_stock)
|
77 |
+
if max_stock is not None:
|
78 |
+
query = query.filter(Product.stock <= max_stock)
|
79 |
+
|
80 |
+
# 執行查詢
|
81 |
+
products = query.limit(limit).all()
|
82 |
+
|
83 |
+
# 轉換為字典格式
|
84 |
+
data = []
|
85 |
+
for product in products:
|
86 |
+
product_data = {
|
87 |
+
"id": product.id,
|
88 |
+
"product_code": product.productCode,
|
89 |
+
"product_name": product.productName,
|
90 |
+
"unit": product.unit,
|
91 |
+
"warehouse": product.warehouse,
|
92 |
+
"unit_weight": product.unitWeight,
|
93 |
+
"barcode": product.barcode,
|
94 |
+
"category_id": product.category_id,
|
95 |
+
"category_name": product.category.name if product.category else None,
|
96 |
+
"created_at": product.createdAt.isoformat() if product.createdAt else None,
|
97 |
+
"updated_at": product.updatedAt.isoformat() if product.updatedAt else None
|
98 |
+
}
|
99 |
+
|
100 |
+
# 包含庫存資訊
|
101 |
+
if include_stock_info:
|
102 |
+
product_data.update({
|
103 |
+
"current_stock": product.stock,
|
104 |
+
"stock_status": self._get_stock_status(product.stock),
|
105 |
+
"is_low_stock": product.stock <= 10
|
106 |
+
})
|
107 |
+
|
108 |
+
data.append(product_data)
|
109 |
+
|
110 |
+
return DatabaseResult(
|
111 |
+
success=True,
|
112 |
+
data=data,
|
113 |
+
count=len(data)
|
114 |
+
)
|
115 |
+
|
116 |
+
except Exception as e:
|
117 |
+
logger.error(f"進階商品搜尋錯誤: {str(e)}")
|
118 |
+
return DatabaseResult(
|
119 |
+
success=False,
|
120 |
+
error=f"商品搜尋失敗: {str(e)}"
|
121 |
+
)
|
122 |
+
finally:
|
123 |
+
if db:
|
124 |
+
close_database_session(db)
|
125 |
+
|
126 |
+
def get_products_by_category(self, category_name: str, limit: int = 20) -> DatabaseResult:
|
127 |
+
"""根據分類獲取商品"""
|
128 |
+
db = None
|
129 |
+
try:
|
130 |
+
db = get_database_session()
|
131 |
+
|
132 |
+
products = db.query(Product).join(Category).filter(
|
133 |
+
and_(
|
134 |
+
Category.name.ilike(f"%{category_name}%"),
|
135 |
+
Product.is_deleted == False
|
136 |
+
)
|
137 |
+
).options(joinedload(Product.category)).limit(limit).all()
|
138 |
+
|
139 |
+
data = []
|
140 |
+
for product in products:
|
141 |
+
data.append({
|
142 |
+
"id": product.id,
|
143 |
+
"product_code": product.productCode,
|
144 |
+
"product_name": product.productName,
|
145 |
+
"current_stock": product.stock,
|
146 |
+
"unit": product.unit,
|
147 |
+
"category_name": product.category.name if product.category else None,
|
148 |
+
"warehouse": product.warehouse
|
149 |
+
})
|
150 |
+
|
151 |
+
return DatabaseResult(
|
152 |
+
success=True,
|
153 |
+
data=data,
|
154 |
+
count=len(data)
|
155 |
+
)
|
156 |
+
|
157 |
+
except Exception as e:
|
158 |
+
logger.error(f"分類商品查詢錯誤: {str(e)}")
|
159 |
+
return DatabaseResult(
|
160 |
+
success=False,
|
161 |
+
error=f"分類商品查詢失敗: {str(e)}"
|
162 |
+
)
|
163 |
+
finally:
|
164 |
+
if db:
|
165 |
+
close_database_session(db)
|
166 |
+
|
167 |
+
def get_low_stock_products(self, threshold: int = 10) -> DatabaseResult:
|
168 |
+
"""獲取低庫存商品"""
|
169 |
+
db = None
|
170 |
+
try:
|
171 |
+
db = get_database_session()
|
172 |
+
|
173 |
+
products = db.query(Product).filter(
|
174 |
+
and_(
|
175 |
+
Product.stock <= threshold,
|
176 |
+
Product.is_deleted == False
|
177 |
+
)
|
178 |
+
).options(joinedload(Product.category)).all()
|
179 |
+
|
180 |
+
data = []
|
181 |
+
for product in products:
|
182 |
+
data.append({
|
183 |
+
"id": product.id,
|
184 |
+
"product_code": product.productCode,
|
185 |
+
"product_name": product.productName,
|
186 |
+
"current_stock": product.stock,
|
187 |
+
"threshold": threshold,
|
188 |
+
"unit": product.unit,
|
189 |
+
"category_name": product.category.name if product.category else None,
|
190 |
+
"warehouse": product.warehouse,
|
191 |
+
"urgency_level": self._get_urgency_level(product.stock)
|
192 |
+
})
|
193 |
+
|
194 |
+
return DatabaseResult(
|
195 |
+
success=True,
|
196 |
+
data=data,
|
197 |
+
count=len(data)
|
198 |
+
)
|
199 |
+
|
200 |
+
except Exception as e:
|
201 |
+
logger.error(f"低庫存查詢錯誤: {str(e)}")
|
202 |
+
return DatabaseResult(
|
203 |
+
success=False,
|
204 |
+
error=f"低庫存查詢失敗: {str(e)}"
|
205 |
+
)
|
206 |
+
finally:
|
207 |
+
if db:
|
208 |
+
close_database_session(db)
|
209 |
+
|
210 |
+
def get_product_recommendations(self, query_text: str, limit: int = 5) -> DatabaseResult:
|
211 |
+
"""
|
212 |
+
商品推薦功能 - 基於關鍵字的智能推薦
|
213 |
+
特別針對像 "推薦貓砂" 這樣的查詢
|
214 |
+
"""
|
215 |
+
db = None
|
216 |
+
try:
|
217 |
+
db = get_database_session()
|
218 |
+
|
219 |
+
# 分析查詢關鍵字
|
220 |
+
keywords = self._extract_keywords(query_text)
|
221 |
+
|
222 |
+
# 建立推薦查詢
|
223 |
+
query = db.query(Product).filter(Product.is_deleted == False)
|
224 |
+
|
225 |
+
# 多關鍵字匹配
|
226 |
+
for keyword in keywords:
|
227 |
+
search_filter = or_(
|
228 |
+
Product.productName.ilike(f"%{keyword}%"),
|
229 |
+
Product.productCode.ilike(f"%{keyword}%")
|
230 |
+
)
|
231 |
+
query = query.filter(search_filter)
|
232 |
+
|
233 |
+
# 優先顯示有庫存的商品
|
234 |
+
query = query.order_by(Product.stock.desc())
|
235 |
+
|
236 |
+
products = query.options(joinedload(Product.category)).limit(limit).all()
|
237 |
+
|
238 |
+
data = []
|
239 |
+
for product in products:
|
240 |
+
data.append({
|
241 |
+
"id": product.id,
|
242 |
+
"product_code": product.productCode,
|
243 |
+
"product_name": product.productName,
|
244 |
+
"current_stock": product.stock,
|
245 |
+
"unit": product.unit,
|
246 |
+
"category_name": product.category.name if product.category else None,
|
247 |
+
"warehouse": product.warehouse,
|
248 |
+
"recommendation_reason": f"符合關鍵字: {', '.join(keywords)}",
|
249 |
+
"availability": "有庫存" if product.stock > 0 else "缺貨"
|
250 |
+
})
|
251 |
+
|
252 |
+
return DatabaseResult(
|
253 |
+
success=True,
|
254 |
+
data=data,
|
255 |
+
count=len(data)
|
256 |
+
)
|
257 |
+
|
258 |
+
except Exception as e:
|
259 |
+
logger.error(f"商品推薦錯誤: {str(e)}")
|
260 |
+
return DatabaseResult(
|
261 |
+
success=False,
|
262 |
+
error=f"商品推薦失敗: {str(e)}"
|
263 |
+
)
|
264 |
+
finally:
|
265 |
+
if db:
|
266 |
+
close_database_session(db)
|
267 |
+
|
268 |
+
def _get_stock_status(self, stock: int) -> str:
|
269 |
+
"""獲取庫存狀態"""
|
270 |
+
if stock <= 0:
|
271 |
+
return "缺��"
|
272 |
+
elif stock <= 5:
|
273 |
+
return "庫存極低"
|
274 |
+
elif stock <= 10:
|
275 |
+
return "庫存偏低"
|
276 |
+
elif stock <= 50:
|
277 |
+
return "庫存正常"
|
278 |
+
else:
|
279 |
+
return "庫存充足"
|
280 |
+
|
281 |
+
def _get_urgency_level(self, stock: int) -> str:
|
282 |
+
"""獲取緊急程度"""
|
283 |
+
if stock <= 0:
|
284 |
+
return "緊急"
|
285 |
+
elif stock <= 3:
|
286 |
+
return "高"
|
287 |
+
elif stock <= 10:
|
288 |
+
return "中"
|
289 |
+
else:
|
290 |
+
return "低"
|
291 |
+
|
292 |
+
def _extract_keywords(self, query_text: str) -> List[str]:
|
293 |
+
"""從查詢文字中提取關鍵字"""
|
294 |
+
# 移除常見的查詢詞彙
|
295 |
+
stop_words = ['推薦', '有沒有', '是否有', '請問', '想要', '需要', '找', '查詢', '搜尋']
|
296 |
+
|
297 |
+
# 分割並清理關鍵字
|
298 |
+
words = query_text.replace('?', '').replace('?', '').split()
|
299 |
+
keywords = [word for word in words if word not in stop_words and len(word) > 1]
|
300 |
+
|
301 |
+
return keywords if keywords else [query_text.strip()]
|
backend/services/message_router.py
CHANGED
@@ -6,6 +6,7 @@ import logging
|
|
6 |
from typing import Dict, Any
|
7 |
from backend.services.groq_service import GroqService
|
8 |
from backend.services.business_query_service import BusinessQueryService
|
|
|
9 |
|
10 |
logger = logging.getLogger(__name__)
|
11 |
|
@@ -15,11 +16,13 @@ class MessageRouter:
|
|
15 |
def __init__(self):
|
16 |
self.groq_service = GroqService()
|
17 |
self.business_service = BusinessQueryService()
|
18 |
-
|
|
|
19 |
# 路由統計
|
20 |
self.route_stats = {
|
21 |
"chat": 0,
|
22 |
-
"search": 0,
|
|
|
23 |
"help": 0,
|
24 |
"smart": 0,
|
25 |
"error": 0
|
@@ -177,14 +180,23 @@ class MessageRouter:
|
|
177 |
def _handle_smart_routing(self, message: str, user_id: str) -> Dict[str, Any]:
|
178 |
"""智能路由 - 根據內容判斷意圖"""
|
179 |
try:
|
180 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
181 |
quick_intent = self._quick_intent_check(message)
|
182 |
-
|
183 |
if quick_intent == "search":
|
184 |
# 高信心度的搜尋關鍵字 -> 直接轉到搜尋模式
|
185 |
logger.info(f"快速意圖識別: 搜尋模式 - {message[:30]}...")
|
186 |
return self._handle_search_mode(message, user_id)
|
187 |
-
|
188 |
elif quick_intent == "help":
|
189 |
# 幫助相關 -> 轉到幫助模式
|
190 |
return self._handle_help_mode(user_id)
|
@@ -223,7 +235,60 @@ class MessageRouter:
|
|
223 |
logger.error(f"智能路由處理錯誤: {str(e)}")
|
224 |
# 錯誤時預設為聊天模式
|
225 |
return self._handle_chat_mode(message, user_id)
|
226 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
227 |
def _quick_intent_check(self, message: str) -> str:
|
228 |
"""快速意圖檢查 - 基於關鍵字"""
|
229 |
message_lower = message.lower()
|
@@ -253,7 +318,7 @@ class MessageRouter:
|
|
253 |
• /chat [訊息] - 聊天模式
|
254 |
範例:/chat 今天天氣如何?
|
255 |
|
256 |
-
• /search [查詢] - 商品查詢模式
|
257 |
範例:/search iPhone 15 Pro
|
258 |
��例:/search 價格 1000-5000
|
259 |
|
@@ -262,16 +327,22 @@ class MessageRouter:
|
|
262 |
🧠 智能模式:
|
263 |
直接輸入訊息,系統會自動判斷是聊天還是查詢!
|
264 |
|
265 |
-
|
|
|
|
|
|
|
266 |
• "iPhone 還有庫存嗎?"
|
267 |
• "1000到5000的商品有哪些?"
|
268 |
-
• "我的訂單狀態如何?"
|
269 |
|
270 |
💬 聊天範例:
|
271 |
• "你好!"
|
272 |
• "今天天氣如何?"
|
273 |
• "推薦一些好用的功能"
|
274 |
|
|
|
|
|
|
|
|
|
275 |
輸入「選單」查看功能選單
|
276 |
輸入「統計」查看使用統計"""
|
277 |
|
@@ -327,11 +398,13 @@ class MessageRouter:
|
|
327 |
|
328 |
💬 聊天模式: {self.route_stats['chat']} ({self.route_stats['chat']/total_routes*100:.1f}%)
|
329 |
🔍 搜尋模式: {self.route_stats['search']} ({self.route_stats['search']/total_routes*100:.1f}%)
|
|
|
330 |
🧠 智能路由: {self.route_stats['smart']} ({self.route_stats['smart']/total_routes*100:.1f}%)
|
331 |
❓ 幫助模式: {self.route_stats['help']} ({self.route_stats['help']/total_routes*100:.1f}%)
|
332 |
❌ 錯誤次數: {self.route_stats['error']} ({self.route_stats['error']/total_routes*100:.1f}%)
|
333 |
|
334 |
-
🤖 Groq 服務: {'✅ 可用' if self.groq_service.is_available() else '❌ 不可用'}
|
|
|
335 |
|
336 |
return {
|
337 |
"type": "text",
|
|
|
6 |
from typing import Dict, Any
|
7 |
from backend.services.groq_service import GroqService
|
8 |
from backend.services.business_query_service import BusinessQueryService
|
9 |
+
from backend.services.pydantic_ai_service import ProductQueryService
|
10 |
|
11 |
logger = logging.getLogger(__name__)
|
12 |
|
|
|
16 |
def __init__(self):
|
17 |
self.groq_service = GroqService()
|
18 |
self.business_service = BusinessQueryService()
|
19 |
+
self.product_query_service = ProductQueryService()
|
20 |
+
|
21 |
# 路由統計
|
22 |
self.route_stats = {
|
23 |
"chat": 0,
|
24 |
+
"search": 0,
|
25 |
+
"product_query": 0,
|
26 |
"help": 0,
|
27 |
"smart": 0,
|
28 |
"error": 0
|
|
|
180 |
def _handle_smart_routing(self, message: str, user_id: str) -> Dict[str, Any]:
|
181 |
"""智能路由 - 根據內容判斷意圖"""
|
182 |
try:
|
183 |
+
# 1. 先檢查是否為商品查詢 (使用 Pydantic AI)
|
184 |
+
if self.product_query_service.is_available():
|
185 |
+
product_intent = self.product_query_service.analyze_query_intent(message)
|
186 |
+
|
187 |
+
if product_intent["is_product_query"] and product_intent["confidence"] > 0.6:
|
188 |
+
logger.info(f"智能路由 -> Pydantic AI 商品查詢 (信心度: {product_intent['confidence']})")
|
189 |
+
self.route_stats["product_query"] += 1
|
190 |
+
return self._handle_product_query_mode(message, user_id)
|
191 |
+
|
192 |
+
# 2. 進行簡單的關鍵字預篩選
|
193 |
quick_intent = self._quick_intent_check(message)
|
194 |
+
|
195 |
if quick_intent == "search":
|
196 |
# 高信心度的搜尋關鍵字 -> 直接轉到搜尋模式
|
197 |
logger.info(f"快速意圖識別: 搜尋模式 - {message[:30]}...")
|
198 |
return self._handle_search_mode(message, user_id)
|
199 |
+
|
200 |
elif quick_intent == "help":
|
201 |
# 幫助相關 -> 轉到幫助模式
|
202 |
return self._handle_help_mode(user_id)
|
|
|
235 |
logger.error(f"智能路由處理錯誤: {str(e)}")
|
236 |
# 錯誤時預設為聊天模式
|
237 |
return self._handle_chat_mode(message, user_id)
|
238 |
+
|
239 |
+
def _handle_product_query_mode(self, message: str, user_id: str) -> Dict[str, Any]:
|
240 |
+
"""處理 Pydantic AI 商品查詢模式"""
|
241 |
+
try:
|
242 |
+
if not message:
|
243 |
+
return {
|
244 |
+
"type": "text",
|
245 |
+
"text": "🛍️ 請告訴我您想查詢什麼商品!\n\n範例:\n• 是否有推薦貓砂?\n• 查詢狗糧庫存\n• 有什麼寵物用品?",
|
246 |
+
"mode": "product_query",
|
247 |
+
"success": True
|
248 |
+
}
|
249 |
+
|
250 |
+
if not self.product_query_service.is_available():
|
251 |
+
return {
|
252 |
+
"type": "text",
|
253 |
+
"text": "🛍️ 商品查詢服務暫時無法使用,請稍後再試。\n\n💡 您可以嘗試使用 /search 進行基本查詢。",
|
254 |
+
"mode": "product_query",
|
255 |
+
"success": False,
|
256 |
+
"error": "Pydantic AI 服務不可用"
|
257 |
+
}
|
258 |
+
|
259 |
+
# 使用同步版本避免異步複雜性
|
260 |
+
result = self.product_query_service.process_product_query_sync(message, user_id)
|
261 |
+
|
262 |
+
if result["success"]:
|
263 |
+
return {
|
264 |
+
"type": "text",
|
265 |
+
"text": f"🛍️ {result['text']}",
|
266 |
+
"mode": "product_query",
|
267 |
+
"success": True,
|
268 |
+
"products_found": result.get("products_found", 0),
|
269 |
+
"has_recommendations": result.get("has_recommendations", False),
|
270 |
+
"user_id": user_id
|
271 |
+
}
|
272 |
+
else:
|
273 |
+
return {
|
274 |
+
"type": "text",
|
275 |
+
"text": f"🛍️ {result.get('text', '商品查詢失敗')}",
|
276 |
+
"mode": "product_query",
|
277 |
+
"success": False,
|
278 |
+
"error": result.get("error"),
|
279 |
+
"user_id": user_id
|
280 |
+
}
|
281 |
+
|
282 |
+
except Exception as e:
|
283 |
+
logger.error(f"商品查詢模式處理錯誤: {str(e)}")
|
284 |
+
return {
|
285 |
+
"type": "text",
|
286 |
+
"text": "🛍️ 商品查詢服務暫時無法使用,請稍後再試。",
|
287 |
+
"mode": "product_query",
|
288 |
+
"success": False,
|
289 |
+
"error": str(e)
|
290 |
+
}
|
291 |
+
|
292 |
def _quick_intent_check(self, message: str) -> str:
|
293 |
"""快速意圖檢查 - 基於關鍵字"""
|
294 |
message_lower = message.lower()
|
|
|
318 |
• /chat [訊息] - 聊天模式
|
319 |
範例:/chat 今天天氣如何?
|
320 |
|
321 |
+
• /search [查詢] - 商品查詢模式
|
322 |
範例:/search iPhone 15 Pro
|
323 |
��例:/search 價格 1000-5000
|
324 |
|
|
|
327 |
🧠 智能模式:
|
328 |
直接輸入訊息,系統會自動判斷是聊天還是查詢!
|
329 |
|
330 |
+
🛍️ 商品查詢範例(AI 智能識別):
|
331 |
+
• "是否有推薦貓砂?"
|
332 |
+
• "有什麼狗糧推薦?"
|
333 |
+
• "查詢寵物用品庫存"
|
334 |
• "iPhone 還有庫存嗎?"
|
335 |
• "1000到5000的商品有哪些?"
|
|
|
336 |
|
337 |
💬 聊天範例:
|
338 |
• "你好!"
|
339 |
• "今天天氣如何?"
|
340 |
• "推薦一些好用的功能"
|
341 |
|
342 |
+
📋 其他查詢:
|
343 |
+
• "我的訂單狀態如何?"
|
344 |
+
• "低庫存商品有哪些?"
|
345 |
+
|
346 |
輸入「選單」查看功能選單
|
347 |
輸入「統計」查看使用統計"""
|
348 |
|
|
|
398 |
|
399 |
💬 聊天模式: {self.route_stats['chat']} ({self.route_stats['chat']/total_routes*100:.1f}%)
|
400 |
🔍 搜尋模式: {self.route_stats['search']} ({self.route_stats['search']/total_routes*100:.1f}%)
|
401 |
+
🛍️ 商品查詢: {self.route_stats['product_query']} ({self.route_stats['product_query']/total_routes*100:.1f}%)
|
402 |
🧠 智能路由: {self.route_stats['smart']} ({self.route_stats['smart']/total_routes*100:.1f}%)
|
403 |
❓ 幫助模式: {self.route_stats['help']} ({self.route_stats['help']/total_routes*100:.1f}%)
|
404 |
❌ 錯誤次數: {self.route_stats['error']} ({self.route_stats['error']/total_routes*100:.1f}%)
|
405 |
|
406 |
+
🤖 Groq 服務: {'✅ 可用' if self.groq_service.is_available() else '❌ 不可用'}
|
407 |
+
🛍️ Pydantic AI: {'✅ 可用' if self.product_query_service.is_available() else '❌ 不可用'}"""
|
408 |
|
409 |
return {
|
410 |
"type": "text",
|
backend/services/pydantic_ai_service.py
ADDED
@@ -0,0 +1,426 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Pydantic AI 服務 - 使用 Pydantic AI 框架整合 Groq 和商品查詢功能
|
3 |
+
解決商品查詢準確性問題,特別是像 "是否有推薦貓砂?" 這類查詢
|
4 |
+
"""
|
5 |
+
|
6 |
+
import logging
|
7 |
+
from typing import Dict, Any, List, Optional
|
8 |
+
from dataclasses import dataclass
|
9 |
+
from pydantic import BaseModel, Field
|
10 |
+
from pydantic_ai import Agent, RunContext
|
11 |
+
|
12 |
+
from backend.services.enhanced_product_service import EnhancedProductService
|
13 |
+
from backend.services.database_service import DatabaseService
|
14 |
+
from backend.config import settings
|
15 |
+
|
16 |
+
logger = logging.getLogger(__name__)
|
17 |
+
|
18 |
+
# 依賴注入類型
|
19 |
+
@dataclass
|
20 |
+
class ProductQueryDependencies:
|
21 |
+
"""商品查詢依賴"""
|
22 |
+
enhanced_product_service: EnhancedProductService
|
23 |
+
database_service: DatabaseService
|
24 |
+
user_id: Optional[str] = None
|
25 |
+
|
26 |
+
# 輸出模型
|
27 |
+
class ProductQueryResult(BaseModel):
|
28 |
+
"""商品查詢結果"""
|
29 |
+
intent: str = Field(description="查詢意圖")
|
30 |
+
response_text: str = Field(description="回應文字")
|
31 |
+
products_found: int = Field(description="找到的商品數量")
|
32 |
+
has_recommendations: bool = Field(description="是否包含推薦")
|
33 |
+
stock_info_included: bool = Field(description="是否包含庫存資訊")
|
34 |
+
search_keywords: List[str] = Field(default_factory=list, description="搜尋關鍵字")
|
35 |
+
|
36 |
+
class ProductQueryService:
|
37 |
+
"""Pydantic AI 商品查詢服務"""
|
38 |
+
|
39 |
+
def __init__(self):
|
40 |
+
self.enhanced_product_service = EnhancedProductService()
|
41 |
+
self.database_service = DatabaseService()
|
42 |
+
|
43 |
+
# 創建 Pydantic AI Agent
|
44 |
+
self.product_agent = Agent(
|
45 |
+
f'groq:{settings.GROQ_MODEL}',
|
46 |
+
deps_type=ProductQueryDependencies,
|
47 |
+
output_type=ProductQueryResult,
|
48 |
+
system_prompt=self._get_system_prompt()
|
49 |
+
)
|
50 |
+
|
51 |
+
# 註冊工具
|
52 |
+
self._register_tools()
|
53 |
+
|
54 |
+
def _get_system_prompt(self) -> str:
|
55 |
+
"""系統提示詞"""
|
56 |
+
return """你是一個專業的商品查詢助手,專門協助用戶查詢商品資訊。
|
57 |
+
|
58 |
+
你的主要任務:
|
59 |
+
1. 理解用戶的商品查詢意圖,包括推薦、搜尋、庫存查詢等
|
60 |
+
2. 使用適當的工具查詢商品資料庫
|
61 |
+
3. 提供準確、有用的商品資訊回應
|
62 |
+
|
63 |
+
特別注意:
|
64 |
+
- 當用戶詢問"推薦"、"有沒有"、"是否有"時,要積極查詢相關商品
|
65 |
+
- 優先顯示有庫存的商品
|
66 |
+
- 提供具體的商品名稱、庫存狀況和分類資訊
|
67 |
+
- 如果沒有找到完全匹配的商品,嘗試提供相似或相關的商品
|
68 |
+
|
69 |
+
回應要求:
|
70 |
+
- 使用繁體中文
|
71 |
+
- 語氣友善專業
|
72 |
+
- 資訊準確完整
|
73 |
+
- 如果有多個商品,按庫存量排序推薦"""
|
74 |
+
|
75 |
+
def _register_tools(self):
|
76 |
+
"""註冊 AI Agent 工具"""
|
77 |
+
|
78 |
+
@self.product_agent.tool
|
79 |
+
async def search_products(
|
80 |
+
ctx: RunContext[ProductQueryDependencies],
|
81 |
+
query_text: str,
|
82 |
+
include_recommendations: bool = False
|
83 |
+
) -> Dict[str, Any]:
|
84 |
+
"""
|
85 |
+
搜尋商品
|
86 |
+
|
87 |
+
Args:
|
88 |
+
query_text: 搜尋關鍵字
|
89 |
+
include_recommendations: 是否包含推薦功能
|
90 |
+
"""
|
91 |
+
try:
|
92 |
+
if include_recommendations:
|
93 |
+
# 使用推薦功能
|
94 |
+
result = ctx.deps.enhanced_product_service.get_product_recommendations(
|
95 |
+
query_text=query_text,
|
96 |
+
limit=10
|
97 |
+
)
|
98 |
+
else:
|
99 |
+
# 使用一般搜尋
|
100 |
+
result = ctx.deps.enhanced_product_service.search_products_advanced(
|
101 |
+
query_text=query_text,
|
102 |
+
include_stock_info=True,
|
103 |
+
limit=10
|
104 |
+
)
|
105 |
+
|
106 |
+
return {
|
107 |
+
"success": result.success,
|
108 |
+
"products": result.data,
|
109 |
+
"count": result.count,
|
110 |
+
"error": result.error
|
111 |
+
}
|
112 |
+
except Exception as e:
|
113 |
+
logger.error(f"商品搜尋工具錯誤: {str(e)}")
|
114 |
+
return {
|
115 |
+
"success": False,
|
116 |
+
"products": [],
|
117 |
+
"count": 0,
|
118 |
+
"error": str(e)
|
119 |
+
}
|
120 |
+
|
121 |
+
@self.product_agent.tool
|
122 |
+
async def get_products_by_category(
|
123 |
+
ctx: RunContext[ProductQueryDependencies],
|
124 |
+
category_name: str
|
125 |
+
) -> Dict[str, Any]:
|
126 |
+
"""
|
127 |
+
根據分類獲取商品
|
128 |
+
|
129 |
+
Args:
|
130 |
+
category_name: 分類名稱
|
131 |
+
"""
|
132 |
+
try:
|
133 |
+
result = ctx.deps.enhanced_product_service.get_products_by_category(
|
134 |
+
category_name=category_name,
|
135 |
+
limit=10
|
136 |
+
)
|
137 |
+
|
138 |
+
return {
|
139 |
+
"success": result.success,
|
140 |
+
"products": result.data,
|
141 |
+
"count": result.count,
|
142 |
+
"error": result.error
|
143 |
+
}
|
144 |
+
except Exception as e:
|
145 |
+
logger.error(f"分類查詢工具錯誤: {str(e)}")
|
146 |
+
return {
|
147 |
+
"success": False,
|
148 |
+
"products": [],
|
149 |
+
"count": 0,
|
150 |
+
"error": str(e)
|
151 |
+
}
|
152 |
+
|
153 |
+
@self.product_agent.tool
|
154 |
+
async def check_low_stock_products(
|
155 |
+
ctx: RunContext[ProductQueryDependencies],
|
156 |
+
threshold: int = 10
|
157 |
+
) -> Dict[str, Any]:
|
158 |
+
"""
|
159 |
+
檢查低庫存商品
|
160 |
+
|
161 |
+
Args:
|
162 |
+
threshold: 庫存閾值
|
163 |
+
"""
|
164 |
+
try:
|
165 |
+
result = ctx.deps.enhanced_product_service.get_low_stock_products(
|
166 |
+
threshold=threshold
|
167 |
+
)
|
168 |
+
|
169 |
+
return {
|
170 |
+
"success": result.success,
|
171 |
+
"products": result.data,
|
172 |
+
"count": result.count,
|
173 |
+
"error": result.error
|
174 |
+
}
|
175 |
+
except Exception as e:
|
176 |
+
logger.error(f"低庫存查詢工具錯誤: {str(e)}")
|
177 |
+
return {
|
178 |
+
"success": False,
|
179 |
+
"products": [],
|
180 |
+
"count": 0,
|
181 |
+
"error": str(e)
|
182 |
+
}
|
183 |
+
|
184 |
+
def process_product_query_sync(self, user_message: str, user_id: str = None) -> Dict[str, Any]:
|
185 |
+
"""
|
186 |
+
同步處理商品查詢 - 為了避免異步複雜性
|
187 |
+
|
188 |
+
Args:
|
189 |
+
user_message: 用戶訊息
|
190 |
+
user_id: 用戶ID
|
191 |
+
|
192 |
+
Returns:
|
193 |
+
查詢結果字典
|
194 |
+
"""
|
195 |
+
try:
|
196 |
+
# 分析查詢意圖
|
197 |
+
intent_analysis = self.analyze_query_intent(user_message)
|
198 |
+
|
199 |
+
if not intent_analysis["is_product_query"]:
|
200 |
+
return {
|
201 |
+
"success": False,
|
202 |
+
"text": "這似乎不是商品查詢,請嘗試其他功能。",
|
203 |
+
"mode": "product_query",
|
204 |
+
"error": "非商品查詢意圖"
|
205 |
+
}
|
206 |
+
|
207 |
+
# 根據意圖類型選擇查詢方法
|
208 |
+
if intent_analysis["is_recommendation"]:
|
209 |
+
# 推薦查詢
|
210 |
+
result = self.enhanced_product_service.get_product_recommendations(
|
211 |
+
query_text=user_message,
|
212 |
+
limit=5
|
213 |
+
)
|
214 |
+
else:
|
215 |
+
# 一般搜尋
|
216 |
+
result = self.enhanced_product_service.search_products_advanced(
|
217 |
+
query_text=user_message,
|
218 |
+
include_stock_info=True,
|
219 |
+
limit=10
|
220 |
+
)
|
221 |
+
|
222 |
+
# 格式化回應
|
223 |
+
if result.success and result.data:
|
224 |
+
response_text = self._format_product_response(result.data, intent_analysis)
|
225 |
+
|
226 |
+
return {
|
227 |
+
"success": True,
|
228 |
+
"intent": "product_query",
|
229 |
+
"text": response_text,
|
230 |
+
"products_found": result.count,
|
231 |
+
"has_recommendations": intent_analysis["is_recommendation"],
|
232 |
+
"stock_info_included": True,
|
233 |
+
"search_keywords": self._extract_keywords_from_message(user_message),
|
234 |
+
"mode": "product_query",
|
235 |
+
"user_id": user_id
|
236 |
+
}
|
237 |
+
else:
|
238 |
+
return {
|
239 |
+
"success": False,
|
240 |
+
"text": f"抱歉,沒有找到相關商品。{result.error if result.error else ''}",
|
241 |
+
"mode": "product_query",
|
242 |
+
"error": result.error,
|
243 |
+
"user_id": user_id
|
244 |
+
}
|
245 |
+
|
246 |
+
except Exception as e:
|
247 |
+
logger.error(f"同步商品查詢錯誤: {str(e)}")
|
248 |
+
return {
|
249 |
+
"success": False,
|
250 |
+
"text": f"抱歉,商品查詢時發生錯誤:{str(e)}",
|
251 |
+
"mode": "product_query",
|
252 |
+
"error": str(e),
|
253 |
+
"user_id": user_id
|
254 |
+
}
|
255 |
+
|
256 |
+
async def process_product_query(self, user_message: str, user_id: str = None) -> Dict[str, Any]:
|
257 |
+
"""
|
258 |
+
處理商品查詢
|
259 |
+
|
260 |
+
Args:
|
261 |
+
user_message: 用戶訊息
|
262 |
+
user_id: 用戶ID
|
263 |
+
|
264 |
+
Returns:
|
265 |
+
查詢結果字典
|
266 |
+
"""
|
267 |
+
try:
|
268 |
+
# 準備依賴
|
269 |
+
deps = ProductQueryDependencies(
|
270 |
+
enhanced_product_service=self.enhanced_product_service,
|
271 |
+
database_service=self.database_service,
|
272 |
+
user_id=user_id
|
273 |
+
)
|
274 |
+
|
275 |
+
# 執行 AI Agent
|
276 |
+
result = await self.product_agent.run(user_message, deps=deps)
|
277 |
+
|
278 |
+
# 記錄查詢
|
279 |
+
if user_id:
|
280 |
+
try:
|
281 |
+
self.database_service.save_message(user_id, user_message, "product_query")
|
282 |
+
except Exception as e:
|
283 |
+
logger.warning(f"記錄查詢失敗: {str(e)}")
|
284 |
+
|
285 |
+
return {
|
286 |
+
"success": True,
|
287 |
+
"intent": result.output.intent,
|
288 |
+
"text": result.output.response_text,
|
289 |
+
"products_found": result.output.products_found,
|
290 |
+
"has_recommendations": result.output.has_recommendations,
|
291 |
+
"stock_info_included": result.output.stock_info_included,
|
292 |
+
"search_keywords": result.output.search_keywords,
|
293 |
+
"mode": "product_query",
|
294 |
+
"user_id": user_id
|
295 |
+
}
|
296 |
+
|
297 |
+
except Exception as e:
|
298 |
+
logger.error(f"Pydantic AI 商品查詢錯誤: {str(e)}")
|
299 |
+
return {
|
300 |
+
"success": False,
|
301 |
+
"text": f"抱歉,商品查詢時發生錯誤:{str(e)}",
|
302 |
+
"mode": "product_query",
|
303 |
+
"error": str(e),
|
304 |
+
"user_id": user_id
|
305 |
+
}
|
306 |
+
|
307 |
+
def is_available(self) -> bool:
|
308 |
+
"""檢查服務是否可用"""
|
309 |
+
return bool(settings.GROQ_API_KEY)
|
310 |
+
|
311 |
+
def analyze_query_intent(self, message: str) -> Dict[str, Any]:
|
312 |
+
"""
|
313 |
+
分析查詢意圖 - 判斷是否為商品查詢
|
314 |
+
|
315 |
+
Args:
|
316 |
+
message: 用戶訊息
|
317 |
+
|
318 |
+
Returns:
|
319 |
+
意圖分析結果
|
320 |
+
"""
|
321 |
+
message_lower = message.lower()
|
322 |
+
|
323 |
+
# 商品查詢關鍵字
|
324 |
+
product_keywords = [
|
325 |
+
'推薦', '有沒有', '是否有', '商品', '產品', '貨品',
|
326 |
+
'查詢', '搜尋', '找', '庫存', '存貨', '價格',
|
327 |
+
'貓砂', '狗糧', '寵物', '食品', '用品' # 常見商品類別
|
328 |
+
]
|
329 |
+
|
330 |
+
# 推薦查詢關鍵字
|
331 |
+
recommendation_keywords = ['推薦', '建議', '介紹', '有什麼', '哪些']
|
332 |
+
|
333 |
+
# 庫存查詢關鍵字
|
334 |
+
inventory_keywords = ['庫存', '存貨', '剩餘', '還有', '現貨']
|
335 |
+
|
336 |
+
is_product_query = any(keyword in message_lower for keyword in product_keywords)
|
337 |
+
is_recommendation = any(keyword in message_lower for keyword in recommendation_keywords)
|
338 |
+
is_inventory_check = any(keyword in message_lower for keyword in inventory_keywords)
|
339 |
+
|
340 |
+
confidence = 0.5
|
341 |
+
if is_product_query:
|
342 |
+
confidence += 0.3
|
343 |
+
if is_recommendation:
|
344 |
+
confidence += 0.2
|
345 |
+
if is_inventory_check:
|
346 |
+
confidence += 0.2
|
347 |
+
|
348 |
+
return {
|
349 |
+
"is_product_query": is_product_query,
|
350 |
+
"is_recommendation": is_recommendation,
|
351 |
+
"is_inventory_check": is_inventory_check,
|
352 |
+
"confidence": min(confidence, 1.0),
|
353 |
+
"intent": "product_query" if is_product_query else "unknown"
|
354 |
+
}
|
355 |
+
|
356 |
+
def _format_product_response(self, products: List[Dict[str, Any]], intent_analysis: Dict[str, Any]) -> str:
|
357 |
+
"""格式化商品查詢回應"""
|
358 |
+
if not products:
|
359 |
+
return "沒有找到相關商品。"
|
360 |
+
|
361 |
+
# 根據意圖類型調整回應風格
|
362 |
+
if intent_analysis["is_recommendation"]:
|
363 |
+
header = f"為您推薦 {len(products)} 個商品:"
|
364 |
+
elif intent_analysis["is_inventory_check"]:
|
365 |
+
header = f"庫存查詢結果,找到 {len(products)} 個商品:"
|
366 |
+
else:
|
367 |
+
header = f"商品搜尋結果,找到 {len(products)} 個商品:"
|
368 |
+
|
369 |
+
response_lines = [header, ""]
|
370 |
+
|
371 |
+
for i, product in enumerate(products[:5], 1): # 最多顯示5個
|
372 |
+
name = product.get('product_name', 'N/A')
|
373 |
+
stock = product.get('current_stock', 0)
|
374 |
+
category = product.get('category_name', '')
|
375 |
+
warehouse = product.get('warehouse', '')
|
376 |
+
|
377 |
+
# 庫存狀態
|
378 |
+
stock_status = product.get('stock_status', self._get_stock_status_text(stock))
|
379 |
+
|
380 |
+
line = f"{i}. {name}"
|
381 |
+
if category:
|
382 |
+
line += f" ({category})"
|
383 |
+
|
384 |
+
line += f"\n 庫存: {stock} - {stock_status}"
|
385 |
+
|
386 |
+
if warehouse:
|
387 |
+
line += f"\n 倉庫: {warehouse}"
|
388 |
+
|
389 |
+
if product.get('recommendation_reason'):
|
390 |
+
line += f"\n 推薦原因: {product['recommendation_reason']}"
|
391 |
+
|
392 |
+
response_lines.append(line)
|
393 |
+
|
394 |
+
if len(products) > 5:
|
395 |
+
response_lines.append(f"\n... 還有 {len(products) - 5} 個商品")
|
396 |
+
|
397 |
+
# 添加庫存提醒
|
398 |
+
low_stock_count = sum(1 for p in products if p.get('current_stock', 0) <= 10)
|
399 |
+
if low_stock_count > 0:
|
400 |
+
response_lines.append(f"\n⚠️ 其中 {low_stock_count} 個商品庫存偏低,建議盡快補貨。")
|
401 |
+
|
402 |
+
return "\n".join(response_lines)
|
403 |
+
|
404 |
+
def _get_stock_status_text(self, stock: int) -> str:
|
405 |
+
"""獲取庫存狀態文字"""
|
406 |
+
if stock <= 0:
|
407 |
+
return "缺貨 ❌"
|
408 |
+
elif stock <= 5:
|
409 |
+
return "庫存極低 🔴"
|
410 |
+
elif stock <= 10:
|
411 |
+
return "庫存偏低 🟡"
|
412 |
+
elif stock <= 50:
|
413 |
+
return "庫存正常 🟢"
|
414 |
+
else:
|
415 |
+
return "庫存充足 ✅"
|
416 |
+
|
417 |
+
def _extract_keywords_from_message(self, message: str) -> List[str]:
|
418 |
+
"""從訊息中提取關鍵字"""
|
419 |
+
# 移除常見的查詢詞彙
|
420 |
+
stop_words = ['推薦', '有沒有', '是否有', '請問', '想要', '需要', '找', '查詢', '搜尋', '?', '?']
|
421 |
+
|
422 |
+
# 分割並清理關鍵字
|
423 |
+
words = message.replace('?', '').replace('?', '').split()
|
424 |
+
keywords = [word for word in words if word not in stop_words and len(word) > 1]
|
425 |
+
|
426 |
+
return keywords if keywords else [message.strip()]
|
groq_python_test.py
DELETED
@@ -1,67 +0,0 @@
|
|
1 |
-
from groq import Groq
|
2 |
-
|
3 |
-
GROQ_API_KEY = "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"
|
4 |
-
|
5 |
-
def chat_with_groq(user_message):
|
6 |
-
"""Send a message to Groq and get the streaming response"""
|
7 |
-
client = Groq(api_key=GROQ_API_KEY)
|
8 |
-
|
9 |
-
try:
|
10 |
-
completion = client.chat.completions.create(
|
11 |
-
model="qwen/qwen3-32b",
|
12 |
-
messages=[
|
13 |
-
{"role": "system", "content": "You are a helpful assistant."},
|
14 |
-
{"role": "user", "content": user_message}
|
15 |
-
],
|
16 |
-
temperature=0.6,
|
17 |
-
max_completion_tokens=4096,
|
18 |
-
top_p=0.95,
|
19 |
-
reasoning_effort="default",
|
20 |
-
stream=True,
|
21 |
-
stop=None,
|
22 |
-
)
|
23 |
-
|
24 |
-
print("Groq: ", end="", flush=True)
|
25 |
-
response_text = ""
|
26 |
-
|
27 |
-
for chunk in completion:
|
28 |
-
content = chunk.choices[0].delta.content or ""
|
29 |
-
print(content, end="", flush=True)
|
30 |
-
response_text += content
|
31 |
-
|
32 |
-
print() # New line after response
|
33 |
-
return response_text
|
34 |
-
|
35 |
-
except Exception as e:
|
36 |
-
return f"Error: {e}"
|
37 |
-
|
38 |
-
def main():
|
39 |
-
"""Main interactive loop"""
|
40 |
-
print("=== Groq Chat Bot ===")
|
41 |
-
print("Enter your questions, type 'quit' or 'exit' to quit")
|
42 |
-
print("=" * 30)
|
43 |
-
|
44 |
-
while True:
|
45 |
-
try:
|
46 |
-
user_input = input("\nYour question: ").strip()
|
47 |
-
|
48 |
-
if user_input.lower() in ['quit', 'exit', 'q']:
|
49 |
-
print("Goodbye!")
|
50 |
-
break
|
51 |
-
|
52 |
-
if not user_input:
|
53 |
-
print("Please enter a question.")
|
54 |
-
continue
|
55 |
-
|
56 |
-
print("\nThinking...")
|
57 |
-
chat_with_groq(user_input)
|
58 |
-
|
59 |
-
except KeyboardInterrupt:
|
60 |
-
print("\n\nProgram interrupted, goodbye!")
|
61 |
-
break
|
62 |
-
except EOFError:
|
63 |
-
print("\n\nInput ended, goodbye!")
|
64 |
-
break
|
65 |
-
|
66 |
-
if __name__ == "__main__":
|
67 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
requirements.txt
CHANGED
@@ -6,6 +6,7 @@ psycopg[binary]==3.1.18
|
|
6 |
alembic
|
7 |
pydantic
|
8 |
pydantic-settings
|
|
|
9 |
python-multipart
|
10 |
python-dotenv
|
11 |
requests
|
|
|
6 |
alembic
|
7 |
pydantic
|
8 |
pydantic-settings
|
9 |
+
pydantic-ai[groq]
|
10 |
python-multipart
|
11 |
python-dotenv
|
12 |
requests
|
setup_guide.md
DELETED
@@ -1,178 +0,0 @@
|
|
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_openrouter_connection.py
DELETED
@@ -1,223 +0,0 @@
|
|
1 |
-
#!/usr/bin/env python3
|
2 |
-
"""
|
3 |
-
OpenRouter API 連線測試腳本
|
4 |
-
測試 DeepSeek V3 是否能正常連接和回應
|
5 |
-
"""
|
6 |
-
|
7 |
-
import requests
|
8 |
-
import json
|
9 |
-
import os
|
10 |
-
from datetime import datetime
|
11 |
-
|
12 |
-
# API 設定
|
13 |
-
OPENROUTER_API_KEY = "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"
|
14 |
-
OPENROUTER_MODEL = "deepseek/deepseek-chat-v3-0324:free"
|
15 |
-
OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions"
|
16 |
-
|
17 |
-
def test_openrouter_connection():
|
18 |
-
"""測試 OpenRouter API 連線"""
|
19 |
-
|
20 |
-
print("🔧 OpenRouter API 連線測試")
|
21 |
-
print("=" * 50)
|
22 |
-
print(f"⏰ 測試時間: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
23 |
-
print(f"🔑 API Key: {OPENROUTER_API_KEY[:20]}...")
|
24 |
-
print(f"🤖 模型: {OPENROUTER_MODEL}")
|
25 |
-
print(f"🌐 API URL: {OPENROUTER_URL}")
|
26 |
-
print("=" * 50)
|
27 |
-
|
28 |
-
# 設定請求標頭
|
29 |
-
headers = {
|
30 |
-
"Authorization": f"Bearer {OPENROUTER_API_KEY}",
|
31 |
-
"Content-Type": "application/json",
|
32 |
-
"HTTP-Referer": "https://huggingface.co/spaces",
|
33 |
-
"X-Title": "OpenRouter Connection Test"
|
34 |
-
}
|
35 |
-
|
36 |
-
# 測試案例
|
37 |
-
test_cases = [
|
38 |
-
{
|
39 |
-
"name": "基本連線測試",
|
40 |
-
"message": "你好,請介紹一下自己"
|
41 |
-
},
|
42 |
-
{
|
43 |
-
"name": "中文理解測試",
|
44 |
-
"message": "請分析這句話的意圖:查詢iPhone庫存"
|
45 |
-
},
|
46 |
-
{
|
47 |
-
"name": "業務查詢測試",
|
48 |
-
"message": "如果用戶說「商品查詢」,這應該被歸類為什麼意圖?請用JSON格式回答"
|
49 |
-
}
|
50 |
-
]
|
51 |
-
|
52 |
-
for i, test_case in enumerate(test_cases, 1):
|
53 |
-
print(f"\n📝 測試 {i}: {test_case['name']}")
|
54 |
-
print("-" * 30)
|
55 |
-
|
56 |
-
# 準備請求資料
|
57 |
-
data = {
|
58 |
-
"model": OPENROUTER_MODEL,
|
59 |
-
"messages": [
|
60 |
-
{
|
61 |
-
"role": "system",
|
62 |
-
"content": "你是 DeepSeek V3,一個強大的中文 AI 助手。請用繁體中文回應。"
|
63 |
-
},
|
64 |
-
{
|
65 |
-
"role": "user",
|
66 |
-
"content": test_case['message']
|
67 |
-
}
|
68 |
-
],
|
69 |
-
"temperature": 0.7,
|
70 |
-
"max_tokens": 300
|
71 |
-
}
|
72 |
-
|
73 |
-
try:
|
74 |
-
print(f"📤 發送請求: {test_case['message']}")
|
75 |
-
|
76 |
-
# 發送 API 請求
|
77 |
-
response = requests.post(
|
78 |
-
OPENROUTER_URL,
|
79 |
-
headers=headers,
|
80 |
-
json=data,
|
81 |
-
timeout=30
|
82 |
-
)
|
83 |
-
|
84 |
-
print(f"📊 狀態碼: {response.status_code}")
|
85 |
-
|
86 |
-
if response.status_code == 200:
|
87 |
-
# 解析回應
|
88 |
-
result = response.json()
|
89 |
-
content = result["choices"][0]["message"]["content"]
|
90 |
-
|
91 |
-
print(f"✅ 請求成功!")
|
92 |
-
print(f"🤖 DeepSeek V3 回應:")
|
93 |
-
print(f" {content[:200]}{'...' if len(content) > 200 else ''}")
|
94 |
-
|
95 |
-
# 顯示使用統計
|
96 |
-
if "usage" in result:
|
97 |
-
usage = result["usage"]
|
98 |
-
print(f"📈 Token 使用: {usage.get('total_tokens', 'N/A')} tokens")
|
99 |
-
|
100 |
-
else:
|
101 |
-
print(f"❌ 請求失敗!")
|
102 |
-
print(f"錯誤回應: {response.text}")
|
103 |
-
|
104 |
-
except requests.exceptions.Timeout:
|
105 |
-
print("❌ 請求超時 (30秒)")
|
106 |
-
|
107 |
-
except requests.exceptions.ConnectionError:
|
108 |
-
print("❌ 網路連線錯誤")
|
109 |
-
|
110 |
-
except json.JSONDecodeError:
|
111 |
-
print("❌ JSON 解析錯誤")
|
112 |
-
print(f"原始回應: {response.text}")
|
113 |
-
|
114 |
-
except Exception as e:
|
115 |
-
print(f"❌ 未知錯誤: {str(e)}")
|
116 |
-
|
117 |
-
def test_api_key_validity():
|
118 |
-
"""測試 API Key 有效性"""
|
119 |
-
|
120 |
-
print("\n🔐 API Key 有效性測試")
|
121 |
-
print("-" * 30)
|
122 |
-
|
123 |
-
headers = {
|
124 |
-
"Authorization": f"Bearer {OPENROUTER_API_KEY}",
|
125 |
-
"Content-Type": "application/json"
|
126 |
-
}
|
127 |
-
|
128 |
-
# 簡單的測試請求
|
129 |
-
data = {
|
130 |
-
"model": OPENROUTER_MODEL,
|
131 |
-
"messages": [{"role": "user", "content": "test"}],
|
132 |
-
"max_tokens": 1
|
133 |
-
}
|
134 |
-
|
135 |
-
try:
|
136 |
-
response = requests.post(OPENROUTER_URL, headers=headers, json=data, timeout=10)
|
137 |
-
|
138 |
-
if response.status_code == 200:
|
139 |
-
print("✅ API Key 有效")
|
140 |
-
return True
|
141 |
-
elif response.status_code == 401:
|
142 |
-
print("❌ API Key 無效或已過期")
|
143 |
-
return False
|
144 |
-
elif response.status_code == 402:
|
145 |
-
print("⚠️ 餘額不足")
|
146 |
-
return False
|
147 |
-
else:
|
148 |
-
print(f"⚠️ 未知狀態: {response.status_code}")
|
149 |
-
print(f"回應: {response.text}")
|
150 |
-
return False
|
151 |
-
|
152 |
-
except Exception as e:
|
153 |
-
print(f"❌ 測試失敗: {str(e)}")
|
154 |
-
return False
|
155 |
-
|
156 |
-
def check_model_availability():
|
157 |
-
"""檢查模型可用性"""
|
158 |
-
|
159 |
-
print("\n🤖 模型可用性檢查")
|
160 |
-
print("-" * 30)
|
161 |
-
|
162 |
-
# 嘗試獲取可用模型列表
|
163 |
-
try:
|
164 |
-
headers = {
|
165 |
-
"Authorization": f"Bearer {OPENROUTER_API_KEY}",
|
166 |
-
}
|
167 |
-
|
168 |
-
response = requests.get("https://openrouter.ai/api/v1/models", headers=headers, timeout=10)
|
169 |
-
|
170 |
-
if response.status_code == 200:
|
171 |
-
models = response.json()
|
172 |
-
|
173 |
-
# 檢查 DeepSeek 模型
|
174 |
-
deepseek_models = [model for model in models.get('data', [])
|
175 |
-
if 'deepseek' in model.get('id', '').lower()]
|
176 |
-
|
177 |
-
print(f"✅ 找到 {len(deepseek_models)} 個 DeepSeek 模型:")
|
178 |
-
for model in deepseek_models[:5]: # 只顯示前5個
|
179 |
-
print(f" - {model.get('id', 'N/A')}")
|
180 |
-
|
181 |
-
# 檢查目標模型是否存在
|
182 |
-
target_model_exists = any(model.get('id') == OPENROUTER_MODEL
|
183 |
-
for model in models.get('data', []))
|
184 |
-
|
185 |
-
if target_model_exists:
|
186 |
-
print(f"✅ 目標模型 {OPENROUTER_MODEL} 可用")
|
187 |
-
else:
|
188 |
-
print(f"⚠️ 目標模型 {OPENROUTER_MODEL} 可能不可用")
|
189 |
-
|
190 |
-
else:
|
191 |
-
print(f"⚠️ 無法獲取模型列表: {response.status_code}")
|
192 |
-
|
193 |
-
except Exception as e:
|
194 |
-
print(f"❌ 檢查失敗: {str(e)}")
|
195 |
-
|
196 |
-
def main():
|
197 |
-
"""主函數"""
|
198 |
-
|
199 |
-
print("🚀 開始 OpenRouter API 完整測試")
|
200 |
-
print("=" * 60)
|
201 |
-
|
202 |
-
# 1. 檢查 API Key 有效性
|
203 |
-
if not test_api_key_validity():
|
204 |
-
print("\n❌ API Key 測試失敗,停止後續測試")
|
205 |
-
return
|
206 |
-
|
207 |
-
# 2. 檢查模型可用性
|
208 |
-
check_model_availability()
|
209 |
-
|
210 |
-
# 3. 進行完整連線測試
|
211 |
-
test_openrouter_connection()
|
212 |
-
|
213 |
-
print("\n" + "=" * 60)
|
214 |
-
print("🎉 測試完成!")
|
215 |
-
print("\n💡 如果所有測試都成功,表示:")
|
216 |
-
print(" ✅ OpenRouter API Key 有效")
|
217 |
-
print(" ✅ DeepSeek V3 模型可用")
|
218 |
-
print(" ✅ 網路連線正常")
|
219 |
-
print(" ✅ API 回應正常")
|
220 |
-
print("\n🔧 如果業務查詢仍有問題,則確定是事件循環衝突導致的技術問題")
|
221 |
-
|
222 |
-
if __name__ == "__main__":
|
223 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
test_pydantic_ai_integration.py
ADDED
@@ -0,0 +1,168 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
測試 Pydantic AI 整合 - 驗證商品查詢功能
|
3 |
+
特別測試 "是否有推薦貓砂?" 這類查詢的準確性
|
4 |
+
"""
|
5 |
+
|
6 |
+
import asyncio
|
7 |
+
import sys
|
8 |
+
import os
|
9 |
+
|
10 |
+
# 添加專案根目錄到 Python 路徑
|
11 |
+
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
12 |
+
|
13 |
+
from backend.services.message_router import MessageRouter
|
14 |
+
from backend.services.pydantic_ai_service import ProductQueryService
|
15 |
+
from backend.services.enhanced_product_service import EnhancedProductService
|
16 |
+
|
17 |
+
def test_message_router():
|
18 |
+
"""測試訊息路由器"""
|
19 |
+
print("🧪 測試訊息路由器...")
|
20 |
+
|
21 |
+
router = MessageRouter()
|
22 |
+
|
23 |
+
# 測試案例
|
24 |
+
test_cases = [
|
25 |
+
"是否有推薦貓砂?",
|
26 |
+
"有什麼狗糧推薦?",
|
27 |
+
"查詢寵物用品庫存",
|
28 |
+
"iPhone 還有庫存嗎?",
|
29 |
+
"推薦一些好用的商品",
|
30 |
+
"你好!今天天氣如何?", # 應該路由到聊天模式
|
31 |
+
"/help", # 應該顯示幫助
|
32 |
+
]
|
33 |
+
|
34 |
+
for i, message in enumerate(test_cases, 1):
|
35 |
+
print(f"\n--- 測試案例 {i}: {message} ---")
|
36 |
+
try:
|
37 |
+
result = router.route_message(message, "test_user")
|
38 |
+
print(f"模式: {result.get('mode', 'unknown')}")
|
39 |
+
print(f"成功: {result.get('success', False)}")
|
40 |
+
print(f"回應: {result.get('text', 'No response')[:100]}...")
|
41 |
+
if result.get('products_found'):
|
42 |
+
print(f"找到商品數量: {result['products_found']}")
|
43 |
+
except Exception as e:
|
44 |
+
print(f"❌ 錯誤: {str(e)}")
|
45 |
+
|
46 |
+
# 顯示統計
|
47 |
+
print(f"\n📊 路由統計:")
|
48 |
+
stats = router.get_route_statistics()
|
49 |
+
for mode, count in stats['stats'].items():
|
50 |
+
print(f" {mode}: {count}")
|
51 |
+
|
52 |
+
def test_product_query_service():
|
53 |
+
"""測試 Pydantic AI 商品查詢服務"""
|
54 |
+
print("\n🛍️ 測試 Pydantic AI 商品查詢服務...")
|
55 |
+
|
56 |
+
try:
|
57 |
+
service = ProductQueryService()
|
58 |
+
|
59 |
+
if not service.is_available():
|
60 |
+
print("❌ Pydantic AI 服務不可用 (請檢查 GROQ_API_KEY)")
|
61 |
+
return
|
62 |
+
|
63 |
+
# 測試意圖分析
|
64 |
+
test_messages = [
|
65 |
+
"是否有推薦貓砂?",
|
66 |
+
"有什麼狗糧推薦?",
|
67 |
+
"查詢iPhone庫存",
|
68 |
+
"今天天氣如何?", # 非商品查詢
|
69 |
+
]
|
70 |
+
|
71 |
+
print("\n🔍 意圖分析測試:")
|
72 |
+
for message in test_messages:
|
73 |
+
intent = service.analyze_query_intent(message)
|
74 |
+
print(f" '{message}' -> 商品查詢: {intent['is_product_query']}, 信心度: {intent['confidence']:.2f}")
|
75 |
+
|
76 |
+
# 測試實際查詢
|
77 |
+
print("\n🛍️ 實際查詢測試:")
|
78 |
+
query_result = service.process_product_query_sync("是否有推薦貓砂?", "test_user")
|
79 |
+
|
80 |
+
print(f"成功: {query_result['success']}")
|
81 |
+
print(f"意圖: {query_result.get('intent', 'unknown')}")
|
82 |
+
print(f"回應: {query_result.get('text', 'No response')}")
|
83 |
+
if query_result.get('products_found'):
|
84 |
+
print(f"找到商品: {query_result['products_found']}")
|
85 |
+
|
86 |
+
except Exception as e:
|
87 |
+
print(f"❌ 測試錯誤: {str(e)}")
|
88 |
+
|
89 |
+
def test_enhanced_product_service():
|
90 |
+
"""測試增強商品服務"""
|
91 |
+
print("\n🔧 測試增強商品服務...")
|
92 |
+
|
93 |
+
try:
|
94 |
+
service = EnhancedProductService()
|
95 |
+
|
96 |
+
# 測試商品推薦
|
97 |
+
print("🎯 測試商品推薦功能:")
|
98 |
+
result = service.get_product_recommendations("貓砂", limit=5)
|
99 |
+
|
100 |
+
print(f"成功: {result.success}")
|
101 |
+
print(f"找到商品數量: {result.count}")
|
102 |
+
|
103 |
+
if result.success and result.data:
|
104 |
+
print("推薦商品:")
|
105 |
+
for i, product in enumerate(result.data[:3], 1):
|
106 |
+
print(f" {i}. {product.get('product_name', 'N/A')} - 庫存: {product.get('current_stock', 0)}")
|
107 |
+
elif result.error:
|
108 |
+
print(f"錯誤: {result.error}")
|
109 |
+
|
110 |
+
# 測試進階搜尋
|
111 |
+
print("\n🔍 測試進階搜尋功能:")
|
112 |
+
result = service.search_products_advanced("寵物", include_stock_info=True, limit=3)
|
113 |
+
|
114 |
+
print(f"成功: {result.success}")
|
115 |
+
print(f"找到商品數量: {result.count}")
|
116 |
+
|
117 |
+
if result.success and result.data:
|
118 |
+
print("搜尋結果:")
|
119 |
+
for i, product in enumerate(result.data, 1):
|
120 |
+
print(f" {i}. {product.get('product_name', 'N/A')} - {product.get('stock_status', 'N/A')}")
|
121 |
+
|
122 |
+
except Exception as e:
|
123 |
+
print(f"❌ 測試錯誤: {str(e)}")
|
124 |
+
|
125 |
+
def test_database_connection():
|
126 |
+
"""測試資料庫連接"""
|
127 |
+
print("\n🗄️ 測試資料庫連接...")
|
128 |
+
|
129 |
+
try:
|
130 |
+
from backend.database.connection import test_database_connection
|
131 |
+
|
132 |
+
if test_database_connection():
|
133 |
+
print("✅ 資料庫連接正常")
|
134 |
+
else:
|
135 |
+
print("❌ 資料庫連接失敗")
|
136 |
+
|
137 |
+
except Exception as e:
|
138 |
+
print(f"❌ 資��庫測試錯誤: {str(e)}")
|
139 |
+
|
140 |
+
def main():
|
141 |
+
"""主測試函數"""
|
142 |
+
print("🚀 開始 Pydantic AI 整合測試\n")
|
143 |
+
|
144 |
+
# 1. 測試資料庫連接
|
145 |
+
test_database_connection()
|
146 |
+
|
147 |
+
# 2. 測試增強商品服務
|
148 |
+
test_enhanced_product_service()
|
149 |
+
|
150 |
+
# 3. 測試 Pydantic AI 服務
|
151 |
+
test_product_query_service()
|
152 |
+
|
153 |
+
# 4. 測試訊息路由器
|
154 |
+
test_message_router()
|
155 |
+
|
156 |
+
print("\n✅ 測試完成!")
|
157 |
+
|
158 |
+
if __name__ == "__main__":
|
159 |
+
# 檢查環境變數
|
160 |
+
from backend.config import settings
|
161 |
+
|
162 |
+
print("🔧 環境檢查:")
|
163 |
+
print(f" GROQ_API_KEY: {'✅ 已設定' if settings.GROQ_API_KEY else '❌ 未設定'}")
|
164 |
+
print(f" DATABASE_URL: {'✅ 已設定' if settings.DATABASE_URL else '❌ 未設定'}")
|
165 |
+
print()
|
166 |
+
|
167 |
+
# 執行測試
|
168 |
+
main()
|