|
""" |
|
Groq API 服務 |
|
提供快速的 AI 推理功能 |
|
""" |
|
|
|
from groq import Groq |
|
from backend.config import settings |
|
import logging |
|
import json |
|
import re |
|
|
|
logger = logging.getLogger(__name__) |
|
|
|
class GroqService: |
|
"""Groq AI 服務類""" |
|
|
|
def __init__(self): |
|
self.api_key = settings.GROQ_API_KEY |
|
self.model = getattr(settings, 'GROQ_MODEL', 'qwen/qwen3-32b') |
|
|
|
if not self.api_key: |
|
logger.warning("GROQ_API_KEY 未設定,Groq 服務將無法使用") |
|
self.client = None |
|
else: |
|
self.client = Groq(api_key=self.api_key) |
|
|
|
def is_available(self) -> bool: |
|
"""檢查 Groq 服務是否可用""" |
|
return self.client is not None |
|
|
|
def chat_completion(self, message: str, system_prompt: str = None, temperature: float = 0.6) -> str: |
|
""" |
|
聊天完成 |
|
|
|
Args: |
|
message: 用戶訊息 |
|
system_prompt: 系統提示詞 |
|
temperature: 溫度參數 (0.0-1.0) |
|
|
|
Returns: |
|
AI 回應內容 |
|
""" |
|
if not self.is_available(): |
|
raise Exception("Groq 服務不可用,請檢查 API Key 設定") |
|
|
|
try: |
|
messages = [] |
|
if system_prompt: |
|
messages.append({"role": "system", "content": system_prompt}) |
|
messages.append({"role": "user", "content": message}) |
|
|
|
completion = self.client.chat.completions.create( |
|
model=self.model, |
|
messages=messages, |
|
temperature=temperature, |
|
max_completion_tokens=4096, |
|
top_p=0.95, |
|
reasoning_effort="default", |
|
stop=None, |
|
) |
|
|
|
return completion.choices[0].message.content |
|
|
|
except Exception as e: |
|
logger.error(f"Groq API 調用失敗: {str(e)}") |
|
raise Exception(f"AI 服務暫時無法使用: {str(e)}") |
|
|
|
def analyze_intent(self, message: str) -> dict: |
|
""" |
|
分析用戶訊息意圖 |
|
|
|
Args: |
|
message: 用戶訊息 |
|
|
|
Returns: |
|
意圖分析結果 |
|
""" |
|
if not self.is_available(): |
|
return { |
|
"intent": "unknown", |
|
"confidence": 0.0, |
|
"entities": {}, |
|
"error": "Groq 服務不可用" |
|
} |
|
|
|
system_prompt = """你是一個意圖分析助手,分析用戶訊息的意圖。 |
|
|
|
請分析用戶訊息並回傳 JSON 格式: |
|
{ |
|
"intent": "search|chat|help|order|inventory|unknown", |
|
"confidence": 0.0-1.0, |
|
"entities": { |
|
"product": "商品名稱", |
|
"action": "動作類型", |
|
"price_range": "價格範圍", |
|
"category": "商品分類" |
|
}, |
|
"reasoning": "分析原因" |
|
} |
|
|
|
意圖類型說明: |
|
- search: 商品查詢、搜尋相關 |
|
- chat: 一般聊天、問候、閒聊 |
|
- help: 求助、說明、指令 |
|
- order: 訂單查詢、訂單相關 |
|
- inventory: 庫存查詢、庫存相關 |
|
- unknown: 無法確定意圖 |
|
|
|
請用繁體中文回應,並確保回傳有效的 JSON 格式。""" |
|
|
|
try: |
|
response = self.chat_completion(message, system_prompt, temperature=0.3) |
|
return self._parse_intent_response(response) |
|
|
|
except Exception as e: |
|
logger.error(f"意圖分析失敗: {str(e)}") |
|
return { |
|
"intent": "unknown", |
|
"confidence": 0.0, |
|
"entities": {}, |
|
"error": str(e) |
|
} |
|
|
|
def _parse_intent_response(self, response: str) -> dict: |
|
"""解析意圖分析回應""" |
|
try: |
|
|
|
json_match = re.search(r'\{.*\}', response, re.DOTALL) |
|
if json_match: |
|
json_str = json_match.group() |
|
result = json.loads(json_str) |
|
|
|
|
|
if "intent" not in result: |
|
result["intent"] = "unknown" |
|
if "confidence" not in result: |
|
result["confidence"] = 0.5 |
|
if "entities" not in result: |
|
result["entities"] = {} |
|
|
|
|
|
result["confidence"] = max(0.0, min(1.0, float(result["confidence"]))) |
|
|
|
return result |
|
else: |
|
|
|
return self._fallback_intent_parsing(response) |
|
|
|
except json.JSONDecodeError as e: |
|
logger.warning(f"JSON 解析失敗: {str(e)}, 回應內容: {response}") |
|
return self._fallback_intent_parsing(response) |
|
except Exception as e: |
|
logger.error(f"意圖回應解析錯誤: {str(e)}") |
|
return { |
|
"intent": "unknown", |
|
"confidence": 0.0, |
|
"entities": {}, |
|
"error": f"解析錯誤: {str(e)}" |
|
} |
|
|
|
def _fallback_intent_parsing(self, response: str) -> dict: |
|
"""備用的意圖解析方法""" |
|
response_lower = response.lower() |
|
|
|
|
|
if any(keyword in response_lower for keyword in ['查詢', '搜尋', '找', '商品', '產品']): |
|
return { |
|
"intent": "search", |
|
"confidence": 0.6, |
|
"entities": {}, |
|
"reasoning": "關鍵字匹配: 搜尋相關" |
|
} |
|
elif any(keyword in response_lower for keyword in ['訂單', 'order']): |
|
return { |
|
"intent": "order", |
|
"confidence": 0.6, |
|
"entities": {}, |
|
"reasoning": "關鍵字匹配: 訂單相關" |
|
} |
|
elif any(keyword in response_lower for keyword in ['庫存', 'inventory', '存貨']): |
|
return { |
|
"intent": "inventory", |
|
"confidence": 0.6, |
|
"entities": {}, |
|
"reasoning": "關鍵字匹配: 庫存相關" |
|
} |
|
elif any(keyword in response_lower for keyword in ['幫助', 'help', '說明', '指令']): |
|
return { |
|
"intent": "help", |
|
"confidence": 0.8, |
|
"entities": {}, |
|
"reasoning": "關鍵字匹配: 幫助相關" |
|
} |
|
else: |
|
return { |
|
"intent": "chat", |
|
"confidence": 0.4, |
|
"entities": {}, |
|
"reasoning": "預設為聊天模式" |
|
} |
|
|
|
def generate_business_response(self, query_result: dict, original_message: str) -> str: |
|
""" |
|
根據業務查詢結果生成自然回應 |
|
|
|
Args: |
|
query_result: 業務查詢結果 |
|
original_message: 原始用戶訊息 |
|
|
|
Returns: |
|
自然語言回應 |
|
""" |
|
if not self.is_available(): |
|
return self._fallback_business_response(query_result) |
|
|
|
system_prompt = f"""你是一個友善的客服助手,需要根據查詢結果為用戶生成自然的回應。 |
|
|
|
用戶原始訊息:{original_message} |
|
|
|
查詢結果: |
|
- 成功: {query_result.get('success', False)} |
|
- 資料筆數: {len(query_result.get('data', []))} |
|
- 意圖: {query_result.get('intent', 'unknown')} |
|
|
|
請用繁體中文生成一個友善、自然的回應,包含查詢結果的摘要。 |
|
如果有具體資料,請整理成易讀的格式。 |
|
回應長度控制在 200 字以內。""" |
|
|
|
try: |
|
|
|
data_summary = self._prepare_data_summary(query_result.get('data', [])) |
|
full_prompt = f"{system_prompt}\n\n資料摘要:\n{data_summary}" |
|
|
|
response = self.chat_completion(original_message, full_prompt, temperature=0.4) |
|
return response |
|
|
|
except Exception as e: |
|
logger.error(f"生成業務回應失敗: {str(e)}") |
|
return self._fallback_business_response(query_result) |
|
|
|
def _prepare_data_summary(self, data: list) -> str: |
|
"""準備資料摘要""" |
|
if not data: |
|
return "沒有找到相關資料" |
|
|
|
summary_lines = [] |
|
for i, item in enumerate(data[:5]): |
|
if isinstance(item, dict): |
|
|
|
name = item.get('name', item.get('product_name', '')) |
|
price = item.get('price', item.get('unit_price', '')) |
|
stock = item.get('stock', item.get('quantity', '')) |
|
|
|
line_parts = [] |
|
if name: |
|
line_parts.append(f"名稱: {name}") |
|
if price: |
|
line_parts.append(f"價格: ${price}") |
|
if stock: |
|
line_parts.append(f"庫存: {stock}") |
|
|
|
if line_parts: |
|
summary_lines.append(f"{i+1}. {', '.join(line_parts)}") |
|
|
|
if len(data) > 5: |
|
summary_lines.append(f"... 還有 {len(data) - 5} 筆資料") |
|
|
|
return '\n'.join(summary_lines) if summary_lines else "資料格式無法解析" |
|
|
|
def _fallback_business_response(self, query_result: dict) -> str: |
|
"""備用的業務回應生成""" |
|
if query_result.get('success'): |
|
data_count = len(query_result.get('data', [])) |
|
if data_count > 0: |
|
return f"✅ 查詢成功!找到 {data_count} 筆相關資料。" |
|
else: |
|
return "✅ 查詢完成,但沒有找到符合條件的資料。" |
|
else: |
|
error_msg = query_result.get('error', '未知錯誤') |
|
return f"❌ 查詢失敗:{error_msg}" |