Commit
·
4514838
1
Parent(s):
184558c
something update...
Browse files- .env.example +4 -0
- GROQ_INTEGRATION_COMPLETE.md +237 -0
- backend/config.py +4 -0
- backend/main.py +98 -10
- backend/services/groq_service.py +280 -0
- backend/services/message_router.py +361 -0
- groq_python_test.py +67 -0
- requirements.txt +2 -1
- tmp_rovodev_test_groq_integration.py +340 -0
.env.example
CHANGED
@@ -13,6 +13,10 @@ DB_PASSWORD=your_database_password_here
|
|
13 |
OPENROUTER_API_KEY=your_openrouter_api_key_here
|
14 |
OPENROUTER_MODEL=anthropic/claude-3-haiku
|
15 |
|
|
|
|
|
|
|
|
|
16 |
# 其他設定
|
17 |
DEBUG=False
|
18 |
LOG_LEVEL=INFO
|
|
|
13 |
OPENROUTER_API_KEY=your_openrouter_api_key_here
|
14 |
OPENROUTER_MODEL=anthropic/claude-3-haiku
|
15 |
|
16 |
+
# Groq 設定 (主要 AI 服務,推薦)
|
17 |
+
GROQ_API_KEY=your_groq_api_key_here
|
18 |
+
GROQ_MODEL=qwen/qwen3-32b
|
19 |
+
|
20 |
# 其他設定
|
21 |
DEBUG=False
|
22 |
LOG_LEVEL=INFO
|
GROQ_INTEGRATION_COMPLETE.md
ADDED
@@ -0,0 +1,237 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 服務能力!
|
backend/config.py
CHANGED
@@ -25,6 +25,10 @@ class Settings(BaseSettings):
|
|
25 |
OPENROUTER_API_KEY: Optional[str] = os.getenv("OPENROUTER_API_KEY", "")
|
26 |
OPENROUTER_MODEL: str = os.getenv("OPENROUTER_MODEL", "anthropic/claude-3-haiku")
|
27 |
|
|
|
|
|
|
|
|
|
28 |
# 其他設定
|
29 |
DEBUG: bool = os.getenv("DEBUG", "False").lower() == "true"
|
30 |
LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO")
|
|
|
25 |
OPENROUTER_API_KEY: Optional[str] = os.getenv("OPENROUTER_API_KEY", "")
|
26 |
OPENROUTER_MODEL: str = os.getenv("OPENROUTER_MODEL", "anthropic/claude-3-haiku")
|
27 |
|
28 |
+
# Groq 設定 (主要 AI 服務)
|
29 |
+
GROQ_API_KEY: Optional[str] = os.getenv("GROQ_API_KEY", "")
|
30 |
+
GROQ_MODEL: str = os.getenv("GROQ_MODEL", "qwen/qwen3-32b")
|
31 |
+
|
32 |
# 其他設定
|
33 |
DEBUG: bool = os.getenv("DEBUG", "False").lower() == "true"
|
34 |
LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO")
|
backend/main.py
CHANGED
@@ -7,6 +7,7 @@ from linebot.models import MessageEvent, TextMessage, TextSendMessage
|
|
7 |
import logging
|
8 |
from backend.services.nlp_service import NLPService
|
9 |
from backend.services.database_service import DatabaseService
|
|
|
10 |
from backend.config import settings
|
11 |
from backend.database.connection import test_database_connection
|
12 |
|
@@ -32,6 +33,7 @@ handler = WebhookHandler(settings.LINE_CHANNEL_SECRET)
|
|
32 |
# 服務初始化
|
33 |
nlp_service = NLPService()
|
34 |
db_service = DatabaseService()
|
|
|
35 |
|
36 |
@app.get("/")
|
37 |
def greet_json():
|
@@ -65,26 +67,20 @@ async def callback(request: Request):
|
|
65 |
|
66 |
@handler.add(MessageEvent, message=TextMessage)
|
67 |
def handle_message(event):
|
68 |
-
"""處理 LINE 訊息"""
|
69 |
try:
|
70 |
user_message = event.message.text
|
71 |
user_id = event.source.user_id
|
72 |
|
73 |
logger.info(f"收到訊息: {user_message} from {user_id}")
|
74 |
|
75 |
-
#
|
76 |
-
|
77 |
-
|
78 |
-
# 根據分析結果調用對應的資料庫方法
|
79 |
-
response_data = db_service.execute_query(analysis_result)
|
80 |
-
|
81 |
-
# 格式化回應訊息
|
82 |
-
reply_message = nlp_service.format_response(response_data, analysis_result, user_message)
|
83 |
|
84 |
# 回覆訊息
|
85 |
line_bot_api.reply_message(
|
86 |
event.reply_token,
|
87 |
-
TextSendMessage(text=
|
88 |
)
|
89 |
|
90 |
except Exception as e:
|
@@ -94,6 +90,98 @@ def handle_message(event):
|
|
94 |
TextSendMessage(text="抱歉,處理您的訊息時發生錯誤,請稍後再試。")
|
95 |
)
|
96 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
97 |
if __name__ == "__main__":
|
98 |
import uvicorn
|
99 |
uvicorn.run(app, host="0.0.0.0", port=7860)
|
|
|
7 |
import logging
|
8 |
from backend.services.nlp_service import NLPService
|
9 |
from backend.services.database_service import DatabaseService
|
10 |
+
from backend.services.message_router import MessageRouter
|
11 |
from backend.config import settings
|
12 |
from backend.database.connection import test_database_connection
|
13 |
|
|
|
33 |
# 服務初始化
|
34 |
nlp_service = NLPService()
|
35 |
db_service = DatabaseService()
|
36 |
+
message_router = MessageRouter()
|
37 |
|
38 |
@app.get("/")
|
39 |
def greet_json():
|
|
|
67 |
|
68 |
@handler.add(MessageEvent, message=TextMessage)
|
69 |
def handle_message(event):
|
70 |
+
"""處理 LINE 訊息 - 使用新的路由系統"""
|
71 |
try:
|
72 |
user_message = event.message.text
|
73 |
user_id = event.source.user_id
|
74 |
|
75 |
logger.info(f"收到訊息: {user_message} from {user_id}")
|
76 |
|
77 |
+
# 使用訊息路由器處理
|
78 |
+
response_data = message_router.route_message(user_message, user_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
79 |
|
80 |
# 回覆訊息
|
81 |
line_bot_api.reply_message(
|
82 |
event.reply_token,
|
83 |
+
TextSendMessage(text=response_data["text"])
|
84 |
)
|
85 |
|
86 |
except Exception as e:
|
|
|
90 |
TextSendMessage(text="抱歉,處理您的訊息時發生錯誤,請稍後再試。")
|
91 |
)
|
92 |
|
93 |
+
# 新增直接 API 端點
|
94 |
+
@app.post("/chat")
|
95 |
+
async def direct_chat(request: dict):
|
96 |
+
"""直接聊天 API"""
|
97 |
+
try:
|
98 |
+
message = request.get("message", "")
|
99 |
+
user_id = request.get("user_id", "api_user")
|
100 |
+
|
101 |
+
if not message:
|
102 |
+
return {
|
103 |
+
"success": False,
|
104 |
+
"error": "訊息內容不能為空"
|
105 |
+
}
|
106 |
+
|
107 |
+
# 強制使用聊天模式
|
108 |
+
result = message_router._handle_chat_mode(message, user_id)
|
109 |
+
return result
|
110 |
+
|
111 |
+
except Exception as e:
|
112 |
+
logger.error(f"直接聊天 API 錯誤: {str(e)}")
|
113 |
+
return {
|
114 |
+
"success": False,
|
115 |
+
"error": str(e),
|
116 |
+
"text": "聊天服務暫時無法使用"
|
117 |
+
}
|
118 |
+
|
119 |
+
@app.post("/search")
|
120 |
+
async def direct_search(request: dict):
|
121 |
+
"""直接搜尋 API"""
|
122 |
+
try:
|
123 |
+
message = request.get("message", "")
|
124 |
+
user_id = request.get("user_id", "api_user")
|
125 |
+
|
126 |
+
if not message:
|
127 |
+
return {
|
128 |
+
"success": False,
|
129 |
+
"error": "查詢內容不能為空"
|
130 |
+
}
|
131 |
+
|
132 |
+
# 強制使用搜尋模式
|
133 |
+
result = message_router._handle_search_mode(message, user_id)
|
134 |
+
return result
|
135 |
+
|
136 |
+
except Exception as e:
|
137 |
+
logger.error(f"直接搜尋 API 錯誤: {str(e)}")
|
138 |
+
return {
|
139 |
+
"success": False,
|
140 |
+
"error": str(e),
|
141 |
+
"text": "搜尋服務暫時無法使用"
|
142 |
+
}
|
143 |
+
|
144 |
+
@app.post("/route")
|
145 |
+
async def smart_route(request: dict):
|
146 |
+
"""智能路由 API"""
|
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 {
|
164 |
+
"success": False,
|
165 |
+
"error": str(e),
|
166 |
+
"text": "路由服務暫時無法使用"
|
167 |
+
}
|
168 |
+
|
169 |
+
@app.get("/stats")
|
170 |
+
async def get_stats():
|
171 |
+
"""取得路由統計"""
|
172 |
+
try:
|
173 |
+
stats = message_router.get_route_statistics()
|
174 |
+
return {
|
175 |
+
"success": True,
|
176 |
+
"data": stats
|
177 |
+
}
|
178 |
+
except Exception as e:
|
179 |
+
logger.error(f"統計 API 錯誤: {str(e)}")
|
180 |
+
return {
|
181 |
+
"success": False,
|
182 |
+
"error": str(e)
|
183 |
+
}
|
184 |
+
|
185 |
if __name__ == "__main__":
|
186 |
import uvicorn
|
187 |
uvicorn.run(app, host="0.0.0.0", port=7860)
|
backend/services/groq_service.py
ADDED
@@ -0,0 +1,280 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Groq API 服務
|
3 |
+
提供快速的 AI 推理功能
|
4 |
+
"""
|
5 |
+
|
6 |
+
from groq import Groq
|
7 |
+
from backend.config import settings
|
8 |
+
import logging
|
9 |
+
import json
|
10 |
+
import re
|
11 |
+
|
12 |
+
logger = logging.getLogger(__name__)
|
13 |
+
|
14 |
+
class GroqService:
|
15 |
+
"""Groq AI 服務類"""
|
16 |
+
|
17 |
+
def __init__(self):
|
18 |
+
self.api_key = settings.GROQ_API_KEY
|
19 |
+
self.model = getattr(settings, 'GROQ_MODEL', 'qwen/qwen3-32b')
|
20 |
+
|
21 |
+
if not self.api_key:
|
22 |
+
logger.warning("GROQ_API_KEY 未設定,Groq 服務將無法使用")
|
23 |
+
self.client = None
|
24 |
+
else:
|
25 |
+
self.client = Groq(api_key=self.api_key)
|
26 |
+
|
27 |
+
def is_available(self) -> bool:
|
28 |
+
"""檢查 Groq 服務是否可用"""
|
29 |
+
return self.client is not None
|
30 |
+
|
31 |
+
def chat_completion(self, message: str, system_prompt: str = None, temperature: float = 0.6) -> str:
|
32 |
+
"""
|
33 |
+
聊天完成
|
34 |
+
|
35 |
+
Args:
|
36 |
+
message: 用戶訊息
|
37 |
+
system_prompt: 系統提示詞
|
38 |
+
temperature: 溫度參數 (0.0-1.0)
|
39 |
+
|
40 |
+
Returns:
|
41 |
+
AI 回應內容
|
42 |
+
"""
|
43 |
+
if not self.is_available():
|
44 |
+
raise Exception("Groq 服務不可用,請檢查 API Key 設定")
|
45 |
+
|
46 |
+
try:
|
47 |
+
messages = []
|
48 |
+
if system_prompt:
|
49 |
+
messages.append({"role": "system", "content": system_prompt})
|
50 |
+
messages.append({"role": "user", "content": message})
|
51 |
+
|
52 |
+
completion = self.client.chat.completions.create(
|
53 |
+
model=self.model,
|
54 |
+
messages=messages,
|
55 |
+
temperature=temperature,
|
56 |
+
max_completion_tokens=4096,
|
57 |
+
top_p=0.95,
|
58 |
+
reasoning_effort="default",
|
59 |
+
stop=None,
|
60 |
+
)
|
61 |
+
|
62 |
+
return completion.choices[0].message.content
|
63 |
+
|
64 |
+
except Exception as e:
|
65 |
+
logger.error(f"Groq API 調用失敗: {str(e)}")
|
66 |
+
raise Exception(f"AI 服務暫時無法使用: {str(e)}")
|
67 |
+
|
68 |
+
def analyze_intent(self, message: str) -> dict:
|
69 |
+
"""
|
70 |
+
分析用戶訊息意圖
|
71 |
+
|
72 |
+
Args:
|
73 |
+
message: 用戶訊息
|
74 |
+
|
75 |
+
Returns:
|
76 |
+
意圖分析結果
|
77 |
+
"""
|
78 |
+
if not self.is_available():
|
79 |
+
return {
|
80 |
+
"intent": "unknown",
|
81 |
+
"confidence": 0.0,
|
82 |
+
"entities": {},
|
83 |
+
"error": "Groq 服務不可用"
|
84 |
+
}
|
85 |
+
|
86 |
+
system_prompt = """你是一個意圖分析助手,分析用戶訊息的意圖。
|
87 |
+
|
88 |
+
請分析用戶訊息並回傳 JSON 格式:
|
89 |
+
{
|
90 |
+
"intent": "search|chat|help|order|inventory|unknown",
|
91 |
+
"confidence": 0.0-1.0,
|
92 |
+
"entities": {
|
93 |
+
"product": "商品名稱",
|
94 |
+
"action": "動作類型",
|
95 |
+
"price_range": "價格範圍",
|
96 |
+
"category": "商品分類"
|
97 |
+
},
|
98 |
+
"reasoning": "分析原因"
|
99 |
+
}
|
100 |
+
|
101 |
+
意圖類型說明:
|
102 |
+
- search: 商品查詢、搜尋相關
|
103 |
+
- chat: 一般聊天、問候、閒聊
|
104 |
+
- help: 求助、說明、指令
|
105 |
+
- order: 訂單查詢、訂單相關
|
106 |
+
- inventory: 庫存查詢、庫存相關
|
107 |
+
- unknown: 無法確定意圖
|
108 |
+
|
109 |
+
請用繁體中文回應,並確保回傳有效的 JSON 格式。"""
|
110 |
+
|
111 |
+
try:
|
112 |
+
response = self.chat_completion(message, system_prompt, temperature=0.3)
|
113 |
+
return self._parse_intent_response(response)
|
114 |
+
|
115 |
+
except Exception as e:
|
116 |
+
logger.error(f"意圖分析失敗: {str(e)}")
|
117 |
+
return {
|
118 |
+
"intent": "unknown",
|
119 |
+
"confidence": 0.0,
|
120 |
+
"entities": {},
|
121 |
+
"error": str(e)
|
122 |
+
}
|
123 |
+
|
124 |
+
def _parse_intent_response(self, response: str) -> dict:
|
125 |
+
"""解析意圖分析回應"""
|
126 |
+
try:
|
127 |
+
# 嘗試提取 JSON 部分
|
128 |
+
json_match = re.search(r'\{.*\}', response, re.DOTALL)
|
129 |
+
if json_match:
|
130 |
+
json_str = json_match.group()
|
131 |
+
result = json.loads(json_str)
|
132 |
+
|
133 |
+
# 驗證必要欄位
|
134 |
+
if "intent" not in result:
|
135 |
+
result["intent"] = "unknown"
|
136 |
+
if "confidence" not in result:
|
137 |
+
result["confidence"] = 0.5
|
138 |
+
if "entities" not in result:
|
139 |
+
result["entities"] = {}
|
140 |
+
|
141 |
+
# 確保信心度在有效範圍內
|
142 |
+
result["confidence"] = max(0.0, min(1.0, float(result["confidence"])))
|
143 |
+
|
144 |
+
return result
|
145 |
+
else:
|
146 |
+
# 如果沒有找到 JSON,嘗試簡單解析
|
147 |
+
return self._fallback_intent_parsing(response)
|
148 |
+
|
149 |
+
except json.JSONDecodeError as e:
|
150 |
+
logger.warning(f"JSON 解析失敗: {str(e)}, 回應內容: {response}")
|
151 |
+
return self._fallback_intent_parsing(response)
|
152 |
+
except Exception as e:
|
153 |
+
logger.error(f"意圖回應解析錯誤: {str(e)}")
|
154 |
+
return {
|
155 |
+
"intent": "unknown",
|
156 |
+
"confidence": 0.0,
|
157 |
+
"entities": {},
|
158 |
+
"error": f"解析錯誤: {str(e)}"
|
159 |
+
}
|
160 |
+
|
161 |
+
def _fallback_intent_parsing(self, response: str) -> dict:
|
162 |
+
"""備用的意圖解析方法"""
|
163 |
+
response_lower = response.lower()
|
164 |
+
|
165 |
+
# 簡單的關鍵字匹配
|
166 |
+
if any(keyword in response_lower for keyword in ['查詢', '搜尋', '找', '商品', '產品']):
|
167 |
+
return {
|
168 |
+
"intent": "search",
|
169 |
+
"confidence": 0.6,
|
170 |
+
"entities": {},
|
171 |
+
"reasoning": "關鍵字匹配: 搜尋相關"
|
172 |
+
}
|
173 |
+
elif any(keyword in response_lower for keyword in ['訂單', 'order']):
|
174 |
+
return {
|
175 |
+
"intent": "order",
|
176 |
+
"confidence": 0.6,
|
177 |
+
"entities": {},
|
178 |
+
"reasoning": "關鍵字匹配: 訂單相關"
|
179 |
+
}
|
180 |
+
elif any(keyword in response_lower for keyword in ['庫存', 'inventory', '存貨']):
|
181 |
+
return {
|
182 |
+
"intent": "inventory",
|
183 |
+
"confidence": 0.6,
|
184 |
+
"entities": {},
|
185 |
+
"reasoning": "關鍵字匹配: 庫存相關"
|
186 |
+
}
|
187 |
+
elif any(keyword in response_lower for keyword in ['幫助', 'help', '說明', '指令']):
|
188 |
+
return {
|
189 |
+
"intent": "help",
|
190 |
+
"confidence": 0.8,
|
191 |
+
"entities": {},
|
192 |
+
"reasoning": "關鍵字匹配: 幫助相關"
|
193 |
+
}
|
194 |
+
else:
|
195 |
+
return {
|
196 |
+
"intent": "chat",
|
197 |
+
"confidence": 0.4,
|
198 |
+
"entities": {},
|
199 |
+
"reasoning": "預設為聊天模式"
|
200 |
+
}
|
201 |
+
|
202 |
+
def generate_business_response(self, query_result: dict, original_message: str) -> str:
|
203 |
+
"""
|
204 |
+
根據業務查詢結果生成自然回應
|
205 |
+
|
206 |
+
Args:
|
207 |
+
query_result: 業務查詢結果
|
208 |
+
original_message: 原始用戶訊息
|
209 |
+
|
210 |
+
Returns:
|
211 |
+
自然語言回應
|
212 |
+
"""
|
213 |
+
if not self.is_available():
|
214 |
+
return self._fallback_business_response(query_result)
|
215 |
+
|
216 |
+
system_prompt = f"""你是一個友善的客服助手,需要根據查詢結果為用戶生成自然的回應。
|
217 |
+
|
218 |
+
用戶原始訊息:{original_message}
|
219 |
+
|
220 |
+
查詢結果:
|
221 |
+
- 成功: {query_result.get('success', False)}
|
222 |
+
- 資料筆數: {len(query_result.get('data', []))}
|
223 |
+
- 意圖: {query_result.get('intent', 'unknown')}
|
224 |
+
|
225 |
+
請用繁體中文生成一個友善、自然的回應,包含查詢結果的摘要。
|
226 |
+
如果有具體資料,請整理成易讀的格式。
|
227 |
+
回應長度控制在 200 字以內。"""
|
228 |
+
|
229 |
+
try:
|
230 |
+
# 準備查詢結果摘要
|
231 |
+
data_summary = self._prepare_data_summary(query_result.get('data', []))
|
232 |
+
full_prompt = f"{system_prompt}\n\n資料摘要:\n{data_summary}"
|
233 |
+
|
234 |
+
response = self.chat_completion(original_message, full_prompt, temperature=0.4)
|
235 |
+
return response
|
236 |
+
|
237 |
+
except Exception as e:
|
238 |
+
logger.error(f"生成業務回應失敗: {str(e)}")
|
239 |
+
return self._fallback_business_response(query_result)
|
240 |
+
|
241 |
+
def _prepare_data_summary(self, data: list) -> str:
|
242 |
+
"""準備資料摘要"""
|
243 |
+
if not data:
|
244 |
+
return "沒有找到相關資料"
|
245 |
+
|
246 |
+
summary_lines = []
|
247 |
+
for i, item in enumerate(data[:5]): # 最多顯示 5 筆
|
248 |
+
if isinstance(item, dict):
|
249 |
+
# 提取重要欄位
|
250 |
+
name = item.get('name', item.get('product_name', ''))
|
251 |
+
price = item.get('price', item.get('unit_price', ''))
|
252 |
+
stock = item.get('stock', item.get('quantity', ''))
|
253 |
+
|
254 |
+
line_parts = []
|
255 |
+
if name:
|
256 |
+
line_parts.append(f"名稱: {name}")
|
257 |
+
if price:
|
258 |
+
line_parts.append(f"價格: ${price}")
|
259 |
+
if stock:
|
260 |
+
line_parts.append(f"庫存: {stock}")
|
261 |
+
|
262 |
+
if line_parts:
|
263 |
+
summary_lines.append(f"{i+1}. {', '.join(line_parts)}")
|
264 |
+
|
265 |
+
if len(data) > 5:
|
266 |
+
summary_lines.append(f"... 還有 {len(data) - 5} 筆資料")
|
267 |
+
|
268 |
+
return '\n'.join(summary_lines) if summary_lines else "資料格式無法解析"
|
269 |
+
|
270 |
+
def _fallback_business_response(self, query_result: dict) -> str:
|
271 |
+
"""備用的業務回應生成"""
|
272 |
+
if query_result.get('success'):
|
273 |
+
data_count = len(query_result.get('data', []))
|
274 |
+
if data_count > 0:
|
275 |
+
return f"✅ 查詢成功!找到 {data_count} 筆相關資料。"
|
276 |
+
else:
|
277 |
+
return "✅ 查詢完成,但沒有找到符合條件的資料。"
|
278 |
+
else:
|
279 |
+
error_msg = query_result.get('error', '未知錯誤')
|
280 |
+
return f"❌ 查詢失敗:{error_msg}"
|
backend/services/message_router.py
ADDED
@@ -0,0 +1,361 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
訊息路由器 - 根據前綴和內容智能路由訊息
|
3 |
+
"""
|
4 |
+
|
5 |
+
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 |
+
|
12 |
+
class MessageRouter:
|
13 |
+
"""訊息路由器類"""
|
14 |
+
|
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
|
26 |
+
}
|
27 |
+
|
28 |
+
def route_message(self, message: str, user_id: str) -> Dict[str, Any]:
|
29 |
+
"""
|
30 |
+
根據前綴和內容路由訊息
|
31 |
+
|
32 |
+
Args:
|
33 |
+
message: 用戶訊息
|
34 |
+
user_id: 用戶ID
|
35 |
+
|
36 |
+
Returns:
|
37 |
+
處理結果字典
|
38 |
+
"""
|
39 |
+
try:
|
40 |
+
# 清理訊息
|
41 |
+
clean_message = message.strip()
|
42 |
+
|
43 |
+
if not clean_message:
|
44 |
+
return self._handle_empty_message(user_id)
|
45 |
+
|
46 |
+
logger.info(f"路由訊息 - 用戶: {user_id}, 內容: {clean_message[:50]}...")
|
47 |
+
|
48 |
+
# 1. 聊天模式 - /chat 前綴
|
49 |
+
if clean_message.startswith('/chat '):
|
50 |
+
self.route_stats["chat"] += 1
|
51 |
+
return self._handle_chat_mode(clean_message[6:].strip(), user_id)
|
52 |
+
|
53 |
+
# 2. 商品查詢 - /search 前綴
|
54 |
+
elif clean_message.startswith('/search '):
|
55 |
+
self.route_stats["search"] += 1
|
56 |
+
return self._handle_search_mode(clean_message[8:].strip(), user_id)
|
57 |
+
|
58 |
+
# 3. 幫助指令
|
59 |
+
elif clean_message.lower() in ['/help', '幫助', 'help', '說明', '指令']:
|
60 |
+
self.route_stats["help"] += 1
|
61 |
+
return self._handle_help_mode(user_id)
|
62 |
+
|
63 |
+
# 4. 特殊指令
|
64 |
+
elif clean_message.lower() in ['選單', 'menu', '功能']:
|
65 |
+
return self._handle_menu_mode(user_id)
|
66 |
+
|
67 |
+
elif clean_message.lower() in ['統計', 'stats', '路由統計']:
|
68 |
+
return self._handle_stats_mode(user_id)
|
69 |
+
|
70 |
+
# 5. 智能路由 - 無前綴時進行意圖分析
|
71 |
+
else:
|
72 |
+
self.route_stats["smart"] += 1
|
73 |
+
return self._handle_smart_routing(clean_message, user_id)
|
74 |
+
|
75 |
+
except Exception as e:
|
76 |
+
self.route_stats["error"] += 1
|
77 |
+
logger.error(f"訊息路由錯誤: {str(e)}")
|
78 |
+
return {
|
79 |
+
"type": "text",
|
80 |
+
"text": "❌ 系統處理訊息時發生錯誤,請稍後再試。",
|
81 |
+
"mode": "error",
|
82 |
+
"success": False,
|
83 |
+
"error": str(e)
|
84 |
+
}
|
85 |
+
|
86 |
+
def _handle_chat_mode(self, message: str, user_id: str) -> Dict[str, Any]:
|
87 |
+
"""處理聊天模式"""
|
88 |
+
try:
|
89 |
+
if not message:
|
90 |
+
return {
|
91 |
+
"type": "text",
|
92 |
+
"text": "💬 請輸入您想聊的內容!\n\n範例:/chat 今天天氣如何?",
|
93 |
+
"mode": "chat",
|
94 |
+
"success": True
|
95 |
+
}
|
96 |
+
|
97 |
+
if not self.groq_service.is_available():
|
98 |
+
return {
|
99 |
+
"type": "text",
|
100 |
+
"text": "💬 聊天服務暫時無法使用,請稍後再試。\n\n💡 您可以嘗試使用 /search 進行商品查詢。",
|
101 |
+
"mode": "chat",
|
102 |
+
"success": False,
|
103 |
+
"error": "Groq 服務不可用"
|
104 |
+
}
|
105 |
+
|
106 |
+
system_prompt = """你是一個友善的客服助手,專門協助用戶解答問題。
|
107 |
+
請用繁體中文回應,語氣要親切自然。
|
108 |
+
如果用戶詢問商品或業務相關問題,建議他們使用 /search 指令進行查詢。
|
109 |
+
回應長度控制在 150 字以內。"""
|
110 |
+
|
111 |
+
response = self.groq_service.chat_completion(message, system_prompt)
|
112 |
+
|
113 |
+
return {
|
114 |
+
"type": "text",
|
115 |
+
"text": f"💬 {response}",
|
116 |
+
"mode": "chat",
|
117 |
+
"success": True,
|
118 |
+
"user_id": user_id
|
119 |
+
}
|
120 |
+
|
121 |
+
except Exception as e:
|
122 |
+
logger.error(f"聊天模式處理錯誤: {str(e)}")
|
123 |
+
return {
|
124 |
+
"type": "text",
|
125 |
+
"text": "💬 聊天服務暫時無法使用,請稍後再試。",
|
126 |
+
"mode": "chat",
|
127 |
+
"success": False,
|
128 |
+
"error": str(e)
|
129 |
+
}
|
130 |
+
|
131 |
+
def _handle_search_mode(self, message: str, user_id: str) -> Dict[str, Any]:
|
132 |
+
"""處理搜尋模式"""
|
133 |
+
try:
|
134 |
+
if not message:
|
135 |
+
return {
|
136 |
+
"type": "text",
|
137 |
+
"text": "🔍 請��入您要查詢的內容!\n\n範例:\n• /search iPhone 15\n• /search 價格 1000-5000\n• /search 庫存不足的商品",
|
138 |
+
"mode": "search",
|
139 |
+
"success": True
|
140 |
+
}
|
141 |
+
|
142 |
+
# 使用業務查詢服務
|
143 |
+
result = self.business_service.process_user_query(message, user_id)
|
144 |
+
|
145 |
+
# 如果有 Groq 服務,使用它來生成更自然的回應
|
146 |
+
if self.groq_service.is_available() and result.get("success"):
|
147 |
+
try:
|
148 |
+
natural_response = self.groq_service.generate_business_response(result, message)
|
149 |
+
response_text = f"🔍 {natural_response}"
|
150 |
+
except Exception as e:
|
151 |
+
logger.warning(f"Groq 回應生成失敗,使用原始回應: {str(e)}")
|
152 |
+
response_text = f"🔍 {result['response_message']}"
|
153 |
+
else:
|
154 |
+
response_text = f"🔍 {result['response_message']}"
|
155 |
+
|
156 |
+
return {
|
157 |
+
"type": "text",
|
158 |
+
"text": response_text,
|
159 |
+
"mode": "search",
|
160 |
+
"success": result["success"],
|
161 |
+
"data": result.get("data", []),
|
162 |
+
"intent": result.get("intent", "unknown"),
|
163 |
+
"confidence": result.get("confidence", 0.0),
|
164 |
+
"user_id": user_id
|
165 |
+
}
|
166 |
+
|
167 |
+
except Exception as e:
|
168 |
+
logger.error(f"搜尋模式處理錯誤: {str(e)}")
|
169 |
+
return {
|
170 |
+
"type": "text",
|
171 |
+
"text": "🔍 搜尋服務暫時無法使用,請稍後再試。",
|
172 |
+
"mode": "search",
|
173 |
+
"success": False,
|
174 |
+
"error": str(e)
|
175 |
+
}
|
176 |
+
|
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)
|
191 |
+
|
192 |
+
# 使用 Groq 進行深度意圖分析
|
193 |
+
if self.groq_service.is_available():
|
194 |
+
try:
|
195 |
+
intent_result = self.groq_service.analyze_intent(message)
|
196 |
+
logger.info(f"Groq 意圖分析: {intent_result}")
|
197 |
+
|
198 |
+
# 根據意圖和信心度決定路由
|
199 |
+
if intent_result["intent"] in ["search", "order", "inventory"] and intent_result["confidence"] > 0.6:
|
200 |
+
logger.info(f"智能路由 -> 搜尋模式 (信心度: {intent_result['confidence']})")
|
201 |
+
return self._handle_search_mode(message, user_id)
|
202 |
+
|
203 |
+
elif intent_result["intent"] == "help" and intent_result["confidence"] > 0.7:
|
204 |
+
return self._handle_help_mode(user_id)
|
205 |
+
|
206 |
+
else:
|
207 |
+
# 其他情況或低信心度 -> 轉到聊天模式
|
208 |
+
logger.info(f"智能路由 -> 聊天模式 (意圖: {intent_result['intent']}, 信心度: {intent_result['confidence']})")
|
209 |
+
return self._handle_chat_mode(message, user_id)
|
210 |
+
|
211 |
+
except Exception as e:
|
212 |
+
logger.warning(f"Groq 意圖分析失敗,使用預設路由: {str(e)}")
|
213 |
+
# 錯誤時預設為聊天模式
|
214 |
+
return self._handle_chat_mode(message, user_id)
|
215 |
+
else:
|
216 |
+
# Groq 不可用時,根據簡單規則路由
|
217 |
+
if quick_intent == "search":
|
218 |
+
return self._handle_search_mode(message, user_id)
|
219 |
+
else:
|
220 |
+
return self._handle_chat_mode(message, user_id)
|
221 |
+
|
222 |
+
except Exception as e:
|
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()
|
230 |
+
|
231 |
+
# 搜尋相關關鍵字
|
232 |
+
search_keywords = [
|
233 |
+
'查詢', '搜尋', '找', '商品', '產品', '價格', '庫存', '訂單',
|
234 |
+
'多少錢', '有沒有', '還有嗎', '剩多少', '存貨', '現貨',
|
235 |
+
'iphone', 'macbook', 'ipad', 'airpods' # 常見商品名
|
236 |
+
]
|
237 |
+
|
238 |
+
# 幫助相關關鍵字
|
239 |
+
help_keywords = ['幫助', 'help', '說明', '���麼用', '指令', '功能']
|
240 |
+
|
241 |
+
if any(keyword in message_lower for keyword in search_keywords):
|
242 |
+
return "search"
|
243 |
+
elif any(keyword in message_lower for keyword in help_keywords):
|
244 |
+
return "help"
|
245 |
+
else:
|
246 |
+
return "chat"
|
247 |
+
|
248 |
+
def _handle_help_mode(self, user_id: str) -> Dict[str, Any]:
|
249 |
+
"""處理幫助模式"""
|
250 |
+
help_text = """🤖 智能客服助手使用指南
|
251 |
+
|
252 |
+
📝 指令模式:
|
253 |
+
• /chat [訊息] - 聊天模式
|
254 |
+
範例:/chat 今天天氣如何?
|
255 |
+
|
256 |
+
• /search [查詢] - 商品查詢模式
|
257 |
+
範例:/search iPhone 15 Pro
|
258 |
+
範例:/search 價格 1000-5000
|
259 |
+
|
260 |
+
• /help - 顯示此說明
|
261 |
+
|
262 |
+
🧠 智能模式:
|
263 |
+
直接輸入訊息,系統會自動判斷是聊天還是查詢!
|
264 |
+
|
265 |
+
💡 查詢範例:
|
266 |
+
• "iPhone 還有庫存嗎?"
|
267 |
+
• "1000到5000的商品有哪些?"
|
268 |
+
• "我的訂單狀態如何?"
|
269 |
+
|
270 |
+
💬 聊天範例:
|
271 |
+
• "你好!"
|
272 |
+
• "今天天氣如何?"
|
273 |
+
• "推薦一些好用的功能"
|
274 |
+
|
275 |
+
輸入「選單」查看功能選單
|
276 |
+
輸入「統計」查看使用統計"""
|
277 |
+
|
278 |
+
return {
|
279 |
+
"type": "text",
|
280 |
+
"text": help_text,
|
281 |
+
"mode": "help",
|
282 |
+
"success": True,
|
283 |
+
"user_id": user_id
|
284 |
+
}
|
285 |
+
|
286 |
+
def _handle_menu_mode(self, user_id: str) -> Dict[str, Any]:
|
287 |
+
"""處理選單模式"""
|
288 |
+
menu_text = """🏪 功能選單
|
289 |
+
|
290 |
+
請選擇您需要的功能:
|
291 |
+
|
292 |
+
🔍 商品查詢
|
293 |
+
• /search [商品名稱]
|
294 |
+
• /search 價格 [範圍]
|
295 |
+
|
296 |
+
💬 智能聊天
|
297 |
+
• /chat [您的問題]
|
298 |
+
|
299 |
+
📊 常用查詢
|
300 |
+
• "庫存查詢"
|
301 |
+
• "我的訂單"
|
302 |
+
• "低庫存商品"
|
303 |
+
• "業務統計"
|
304 |
+
|
305 |
+
❓ 說明
|
306 |
+
• /help - 詳細說明
|
307 |
+
• 統計 - 使用統計
|
308 |
+
|
309 |
+
💡 小提示:您也可以直接輸入問題,系統會智能判斷處理方式!"""
|
310 |
+
|
311 |
+
return {
|
312 |
+
"type": "text",
|
313 |
+
"text": menu_text,
|
314 |
+
"mode": "menu",
|
315 |
+
"success": True,
|
316 |
+
"user_id": user_id
|
317 |
+
}
|
318 |
+
|
319 |
+
def _handle_stats_mode(self, user_id: str) -> Dict[str, Any]:
|
320 |
+
"""處理統計模式"""
|
321 |
+
total_routes = sum(self.route_stats.values())
|
322 |
+
|
323 |
+
if total_routes == 0:
|
324 |
+
stats_text = "📊 路由統計\n\n尚無使用記錄"
|
325 |
+
else:
|
326 |
+
stats_text = f"""📊 路由統計 (總計: {total_routes})
|
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",
|
338 |
+
"text": stats_text,
|
339 |
+
"mode": "stats",
|
340 |
+
"success": True,
|
341 |
+
"user_id": user_id,
|
342 |
+
"data": self.route_stats
|
343 |
+
}
|
344 |
+
|
345 |
+
def _handle_empty_message(self, user_id: str) -> Dict[str, Any]:
|
346 |
+
"""處理空訊息"""
|
347 |
+
return {
|
348 |
+
"type": "text",
|
349 |
+
"text": "🤔 您似乎沒有輸入任何內容。\n\n輸入 /help 查看使用說明,或直接告訴我您需要什麼幫助!",
|
350 |
+
"mode": "empty",
|
351 |
+
"success": True,
|
352 |
+
"user_id": user_id
|
353 |
+
}
|
354 |
+
|
355 |
+
def get_route_statistics(self) -> Dict[str, Any]:
|
356 |
+
"""取得路由統計資訊"""
|
357 |
+
return {
|
358 |
+
"stats": self.route_stats.copy(),
|
359 |
+
"total": sum(self.route_stats.values()),
|
360 |
+
"groq_available": self.groq_service.is_available()
|
361 |
+
}
|
groq_python_test.py
ADDED
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
@@ -10,4 +10,5 @@ python-multipart
|
|
10 |
python-dotenv
|
11 |
requests
|
12 |
openai
|
13 |
-
httpx
|
|
|
|
10 |
python-dotenv
|
11 |
requests
|
12 |
openai
|
13 |
+
httpx
|
14 |
+
groq
|
tmp_rovodev_test_groq_integration.py
ADDED
@@ -0,0 +1,340 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python3
|
2 |
+
"""
|
3 |
+
Groq 整合測試腳本
|
4 |
+
測試新的路由系統和 Groq 服務
|
5 |
+
"""
|
6 |
+
|
7 |
+
import requests
|
8 |
+
import json
|
9 |
+
import os
|
10 |
+
from typing import Dict, Any
|
11 |
+
|
12 |
+
class GroqIntegrationTester:
|
13 |
+
def __init__(self, base_url: str = "http://localhost:7860"):
|
14 |
+
self.base_url = base_url
|
15 |
+
self.test_results = []
|
16 |
+
|
17 |
+
def test_health_check(self):
|
18 |
+
"""測試健康檢查"""
|
19 |
+
print("🔍 測試健康檢查...")
|
20 |
+
try:
|
21 |
+
response = requests.get(f"{self.base_url}/health")
|
22 |
+
success = response.status_code == 200
|
23 |
+
result = {
|
24 |
+
"test": "health_check",
|
25 |
+
"success": success,
|
26 |
+
"status_code": response.status_code,
|
27 |
+
"response": response.json() if success else response.text
|
28 |
+
}
|
29 |
+
self.test_results.append(result)
|
30 |
+
print(f"✅ 健康檢查: {response.status_code}")
|
31 |
+
return success
|
32 |
+
except Exception as e:
|
33 |
+
print(f"❌ 健康檢查失敗: {e}")
|
34 |
+
self.test_results.append({
|
35 |
+
"test": "health_check",
|
36 |
+
"success": False,
|
37 |
+
"error": str(e)
|
38 |
+
})
|
39 |
+
return False
|
40 |
+
|
41 |
+
def test_chat_api(self):
|
42 |
+
"""測試聊天 API"""
|
43 |
+
print("\n💬 測試聊天 API...")
|
44 |
+
test_messages = [
|
45 |
+
"你好!",
|
46 |
+
"今天天氣如何?",
|
47 |
+
"請介紹一下你自己",
|
48 |
+
"推薦一些好用的功能"
|
49 |
+
]
|
50 |
+
|
51 |
+
for message in test_messages:
|
52 |
+
try:
|
53 |
+
response = requests.post(
|
54 |
+
f"{self.base_url}/chat",
|
55 |
+
json={"message": message, "user_id": "test_user"},
|
56 |
+
headers={"Content-Type": "application/json"}
|
57 |
+
)
|
58 |
+
|
59 |
+
success = response.status_code == 200
|
60 |
+
result = {
|
61 |
+
"test": "chat_api",
|
62 |
+
"message": message,
|
63 |
+
"success": success,
|
64 |
+
"status_code": response.status_code,
|
65 |
+
"response": response.json() if success else response.text
|
66 |
+
}
|
67 |
+
self.test_results.append(result)
|
68 |
+
|
69 |
+
if success:
|
70 |
+
data = response.json()
|
71 |
+
print(f"✅ 聊天: {message[:20]}... -> {data.get('text', '')[:50]}...")
|
72 |
+
else:
|
73 |
+
print(f"❌ 聊天失敗: {message[:20]}... -> {response.status_code}")
|
74 |
+
|
75 |
+
except Exception as e:
|
76 |
+
print(f"❌ 聊天 API 錯誤: {e}")
|
77 |
+
self.test_results.append({
|
78 |
+
"test": "chat_api",
|
79 |
+
"message": message,
|
80 |
+
"success": False,
|
81 |
+
"error": str(e)
|
82 |
+
})
|
83 |
+
|
84 |
+
def test_search_api(self):
|
85 |
+
"""測試搜尋 API"""
|
86 |
+
print("\n🔍 測試搜尋 API...")
|
87 |
+
test_queries = [
|
88 |
+
"iPhone 15 Pro",
|
89 |
+
"價格 1000-5000",
|
90 |
+
"庫存查詢",
|
91 |
+
"我的訂單",
|
92 |
+
"低庫存商品"
|
93 |
+
]
|
94 |
+
|
95 |
+
for query in test_queries:
|
96 |
+
try:
|
97 |
+
response = requests.post(
|
98 |
+
f"{self.base_url}/search",
|
99 |
+
json={"message": query, "user_id": "test_user"},
|
100 |
+
headers={"Content-Type": "application/json"}
|
101 |
+
)
|
102 |
+
|
103 |
+
success = response.status_code == 200
|
104 |
+
result = {
|
105 |
+
"test": "search_api",
|
106 |
+
"query": query,
|
107 |
+
"success": success,
|
108 |
+
"status_code": response.status_code,
|
109 |
+
"response": response.json() if success else response.text
|
110 |
+
}
|
111 |
+
self.test_results.append(result)
|
112 |
+
|
113 |
+
if success:
|
114 |
+
data = response.json()
|
115 |
+
print(f"✅ 搜尋: {query} -> {data.get('text', '')[:50]}...")
|
116 |
+
else:
|
117 |
+
print(f"❌ 搜尋失敗: {query} -> {response.status_code}")
|
118 |
+
|
119 |
+
except Exception as e:
|
120 |
+
print(f"❌ 搜尋 API 錯誤: {e}")
|
121 |
+
self.test_results.append({
|
122 |
+
"test": "search_api",
|
123 |
+
"query": query,
|
124 |
+
"success": False,
|
125 |
+
"error": str(e)
|
126 |
+
})
|
127 |
+
|
128 |
+
def test_smart_routing(self):
|
129 |
+
"""測試智能路由"""
|
130 |
+
print("\n🧠 測試智能路由...")
|
131 |
+
test_messages = [
|
132 |
+
# 應該路由到聊天
|
133 |
+
"你好,今天天氣如何?",
|
134 |
+
"請介紹一下你的功能",
|
135 |
+
|
136 |
+
# 應該路由到搜尋
|
137 |
+
"iPhone 還有庫存嗎?",
|
138 |
+
"查詢價格在 2000 到 5000 的商品",
|
139 |
+
"我想看看訂單狀態",
|
140 |
+
|
141 |
+
# 前綴指令
|
142 |
+
"/chat 你好!",
|
143 |
+
"/search iPhone 15",
|
144 |
+
"/help"
|
145 |
+
]
|
146 |
+
|
147 |
+
for message in test_messages:
|
148 |
+
try:
|
149 |
+
response = requests.post(
|
150 |
+
f"{self.base_url}/route",
|
151 |
+
json={"message": message, "user_id": "test_user"},
|
152 |
+
headers={"Content-Type": "application/json"}
|
153 |
+
)
|
154 |
+
|
155 |
+
success = response.status_code == 200
|
156 |
+
result = {
|
157 |
+
"test": "smart_routing",
|
158 |
+
"message": message,
|
159 |
+
"success": success,
|
160 |
+
"status_code": response.status_code,
|
161 |
+
"response": response.json() if success else response.text
|
162 |
+
}
|
163 |
+
self.test_results.append(result)
|
164 |
+
|
165 |
+
if success:
|
166 |
+
data = response.json()
|
167 |
+
mode = data.get('mode', 'unknown')
|
168 |
+
print(f"✅ 路由: {message[:30]}... -> {mode}")
|
169 |
+
else:
|
170 |
+
print(f"❌ 路由失敗: {message[:30]}... -> {response.status_code}")
|
171 |
+
|
172 |
+
except Exception as e:
|
173 |
+
print(f"❌ 智能路由錯誤: {e}")
|
174 |
+
self.test_results.append({
|
175 |
+
"test": "smart_routing",
|
176 |
+
"message": message,
|
177 |
+
"success": False,
|
178 |
+
"error": str(e)
|
179 |
+
})
|
180 |
+
|
181 |
+
def test_stats_api(self):
|
182 |
+
"""測試統計 API"""
|
183 |
+
print("\n📊 測試統計 API...")
|
184 |
+
try:
|
185 |
+
response = requests.get(f"{self.base_url}/stats")
|
186 |
+
success = response.status_code == 200
|
187 |
+
result = {
|
188 |
+
"test": "stats_api",
|
189 |
+
"success": success,
|
190 |
+
"status_code": response.status_code,
|
191 |
+
"response": response.json() if success else response.text
|
192 |
+
}
|
193 |
+
self.test_results.append(result)
|
194 |
+
|
195 |
+
if success:
|
196 |
+
data = response.json()
|
197 |
+
stats = data.get('data', {}).get('stats', {})
|
198 |
+
total = data.get('data', {}).get('total', 0)
|
199 |
+
print(f"✅ 統計: 總計 {total} 次路由")
|
200 |
+
for mode, count in stats.items():
|
201 |
+
print(f" {mode}: {count}")
|
202 |
+
else:
|
203 |
+
print(f"❌ 統計失敗: {response.status_code}")
|
204 |
+
|
205 |
+
except Exception as e:
|
206 |
+
print(f"❌ 統計 API 錯誤: {e}")
|
207 |
+
self.test_results.append({
|
208 |
+
"test": "stats_api",
|
209 |
+
"success": False,
|
210 |
+
"error": str(e)
|
211 |
+
})
|
212 |
+
|
213 |
+
def test_line_webhook_simulation(self):
|
214 |
+
"""模擬 LINE Webhook"""
|
215 |
+
print("\n📱 測試 LINE Webhook 模擬...")
|
216 |
+
test_messages = [
|
217 |
+
"/chat 你好!",
|
218 |
+
"/search iPhone",
|
219 |
+
"iPhone 還有庫存嗎?",
|
220 |
+
"幫助"
|
221 |
+
]
|
222 |
+
|
223 |
+
for message in test_messages:
|
224 |
+
webhook_data = {
|
225 |
+
"events": [
|
226 |
+
{
|
227 |
+
"type": "message",
|
228 |
+
"message": {
|
229 |
+
"type": "text",
|
230 |
+
"text": message
|
231 |
+
},
|
232 |
+
"source": {
|
233 |
+
"type": "user",
|
234 |
+
"userId": "test_user_webhook"
|
235 |
+
},
|
236 |
+
"replyToken": "test_reply_token"
|
237 |
+
}
|
238 |
+
]
|
239 |
+
}
|
240 |
+
|
241 |
+
try:
|
242 |
+
response = requests.post(
|
243 |
+
f"{self.base_url}/webhook",
|
244 |
+
json=webhook_data,
|
245 |
+
headers={"Content-Type": "application/json"}
|
246 |
+
)
|
247 |
+
|
248 |
+
success = response.status_code == 200
|
249 |
+
result = {
|
250 |
+
"test": "line_webhook",
|
251 |
+
"message": message,
|
252 |
+
"success": success,
|
253 |
+
"status_code": response.status_code
|
254 |
+
}
|
255 |
+
self.test_results.append(result)
|
256 |
+
|
257 |
+
print(f"{'✅' if success else '❌'} Webhook: {message} -> {response.status_code}")
|
258 |
+
|
259 |
+
except Exception as e:
|
260 |
+
print(f"❌ Webhook 錯誤: {e}")
|
261 |
+
self.test_results.append({
|
262 |
+
"test": "line_webhook",
|
263 |
+
"message": message,
|
264 |
+
"success": False,
|
265 |
+
"error": str(e)
|
266 |
+
})
|
267 |
+
|
268 |
+
def generate_report(self):
|
269 |
+
"""生成測試報告"""
|
270 |
+
print("\n" + "="*50)
|
271 |
+
print("📋 測試報告")
|
272 |
+
print("="*50)
|
273 |
+
|
274 |
+
total_tests = len(self.test_results)
|
275 |
+
successful_tests = sum(1 for result in self.test_results if result.get('success', False))
|
276 |
+
|
277 |
+
print(f"總測試數: {total_tests}")
|
278 |
+
print(f"���功: {successful_tests}")
|
279 |
+
print(f"失敗: {total_tests - successful_tests}")
|
280 |
+
print(f"成功率: {successful_tests/total_tests*100:.1f}%")
|
281 |
+
|
282 |
+
# 按測試類型分組
|
283 |
+
test_types = {}
|
284 |
+
for result in self.test_results:
|
285 |
+
test_type = result['test']
|
286 |
+
if test_type not in test_types:
|
287 |
+
test_types[test_type] = {'total': 0, 'success': 0}
|
288 |
+
test_types[test_type]['total'] += 1
|
289 |
+
if result.get('success', False):
|
290 |
+
test_types[test_type]['success'] += 1
|
291 |
+
|
292 |
+
print("\n📊 各功能測試結果:")
|
293 |
+
for test_type, stats in test_types.items():
|
294 |
+
success_rate = stats['success'] / stats['total'] * 100
|
295 |
+
print(f" {test_type}: {stats['success']}/{stats['total']} ({success_rate:.1f}%)")
|
296 |
+
|
297 |
+
# 顯示失敗的測試
|
298 |
+
failed_tests = [result for result in self.test_results if not result.get('success', False)]
|
299 |
+
if failed_tests:
|
300 |
+
print(f"\n❌ 失敗的測試 ({len(failed_tests)}):")
|
301 |
+
for result in failed_tests:
|
302 |
+
print(f" - {result['test']}: {result.get('error', 'Unknown error')}")
|
303 |
+
|
304 |
+
return {
|
305 |
+
'total': total_tests,
|
306 |
+
'successful': successful_tests,
|
307 |
+
'failed': total_tests - successful_tests,
|
308 |
+
'success_rate': successful_tests/total_tests*100,
|
309 |
+
'details': test_types,
|
310 |
+
'failed_tests': failed_tests
|
311 |
+
}
|
312 |
+
|
313 |
+
def run_all_tests():
|
314 |
+
"""執行所有測試"""
|
315 |
+
print("🚀 開始 Groq 整合測試")
|
316 |
+
print("="*50)
|
317 |
+
|
318 |
+
tester = GroqIntegrationTester()
|
319 |
+
|
320 |
+
# 執行所有測試
|
321 |
+
tester.test_health_check()
|
322 |
+
tester.test_chat_api()
|
323 |
+
tester.test_search_api()
|
324 |
+
tester.test_smart_routing()
|
325 |
+
tester.test_stats_api()
|
326 |
+
tester.test_line_webhook_simulation()
|
327 |
+
|
328 |
+
# 生成報告
|
329 |
+
report = tester.generate_report()
|
330 |
+
|
331 |
+
return report
|
332 |
+
|
333 |
+
if __name__ == "__main__":
|
334 |
+
report = run_all_tests()
|
335 |
+
|
336 |
+
# 保存詳細結果到文件
|
337 |
+
with open("tmp_rovodev_test_results.json", "w", encoding="utf-8") as f:
|
338 |
+
json.dump(report, f, ensure_ascii=False, indent=2)
|
339 |
+
|
340 |
+
print(f"\n📄 詳細測試結果已保存到: tmp_rovodev_test_results.json")
|