|
""" |
|
OpenRouter API 服務 |
|
用於進階自然語言處理功能 |
|
""" |
|
|
|
import httpx |
|
import json |
|
from typing import Dict, Any, Optional |
|
from backend.config import settings |
|
import logging |
|
|
|
logger = logging.getLogger(__name__) |
|
|
|
class OpenRouterService: |
|
"""OpenRouter API 服務類""" |
|
|
|
def __init__(self): |
|
self.api_key = settings.OPENROUTER_API_KEY |
|
self.model = settings.OPENROUTER_MODEL |
|
self.base_url = "https://openrouter.ai/api/v1" |
|
self.headers = { |
|
"Authorization": f"Bearer {self.api_key}", |
|
"Content-Type": "application/json", |
|
"HTTP-Referer": "https://huggingface.co/spaces", |
|
"X-Title": "LINE Bot NLP Service" |
|
} |
|
|
|
async def analyze_intent_advanced(self, message: str, context: Dict[str, Any] = None) -> Dict[str, Any]: |
|
""" |
|
使用 OpenRouter 進行進階意圖分析 |
|
|
|
Args: |
|
message: 用戶訊息 |
|
context: 上下文資訊 |
|
|
|
Returns: |
|
分析結果字典 |
|
""" |
|
if not self.api_key: |
|
logger.warning("OpenRouter API Key 未設定,使用基礎 NLP 服務") |
|
return self._fallback_analysis(message) |
|
|
|
try: |
|
prompt = self._build_analysis_prompt(message, context) |
|
|
|
async with httpx.AsyncClient(timeout=30.0) as client: |
|
response = await client.post( |
|
f"{self.base_url}/chat/completions", |
|
headers=self.headers, |
|
json={ |
|
"model": self.model, |
|
"messages": [ |
|
{ |
|
"role": "system", |
|
"content": "你是一個專業的中文自然語言處理助手,專門分析用戶查詢意圖並提取相關實體。請以 JSON 格式回應。" |
|
}, |
|
{ |
|
"role": "user", |
|
"content": prompt |
|
} |
|
], |
|
"temperature": 0.1, |
|
"max_tokens": 500 |
|
} |
|
) |
|
|
|
if response.status_code == 200: |
|
result = response.json() |
|
content = result["choices"][0]["message"]["content"] |
|
|
|
try: |
|
|
|
analysis = json.loads(content) |
|
return self._validate_analysis_result(analysis) |
|
except json.JSONDecodeError: |
|
logger.error(f"無法解析 OpenRouter 回應: {content}") |
|
return self._fallback_analysis(message) |
|
else: |
|
logger.error(f"OpenRouter API 錯誤: {response.status_code}") |
|
return self._fallback_analysis(message) |
|
|
|
except Exception as e: |
|
logger.error(f"OpenRouter 服務錯誤: {str(e)}") |
|
return self._fallback_analysis(message) |
|
|
|
def _build_analysis_prompt(self, message: str, context: Dict[str, Any] = None) -> str: |
|
"""建構分析提示詞""" |
|
context_info = "" |
|
if context: |
|
context_info = f"\n上下文資訊: {json.dumps(context, ensure_ascii=False)}" |
|
|
|
prompt = f""" |
|
請分析以下中文訊息的意圖和實體: |
|
|
|
訊息: "{message}"{context_info} |
|
|
|
請以以下 JSON 格式回應: |
|
{{ |
|
"intent": "查詢意圖類型 (search_user/search_product/search_order/create_order/update_profile/analytics/unknown)", |
|
"confidence": 0.0-1.0之間的信心度, |
|
"entities": {{ |
|
"user_id": "提取的用戶ID", |
|
"user_name": "提取的用戶名稱", |
|
"product_name": "提取的商品名稱", |
|
"order_id": "提取的訂單ID", |
|
"min_price": 最低價格數字, |
|
"max_price": 最高價格數字, |
|
"category": "商品類別", |
|
"number": 數量 |
|
}}, |
|
"query_type": "search/create/update/delete/analytics", |
|
"parameters": {{ |
|
"table": "目標資料表名稱", |
|
"conditions": "查詢條件", |
|
"limit": 查詢限制數量 |
|
}} |
|
}} |
|
|
|
範例: |
|
- "查詢用戶張三" → intent: "search_user", entities: {{"user_name": "張三"}} |
|
- "找價格1000到5000的手機" → intent: "search_product", entities: {{"min_price": 1000, "max_price": 5000, "category": "手機"}} |
|
- "統計訂單數量" → intent: "analytics", query_type: "analytics" |
|
""" |
|
return prompt |
|
|
|
def _validate_analysis_result(self, analysis: Dict[str, Any]) -> Dict[str, Any]: |
|
"""驗證分析結果""" |
|
|
|
required_fields = ["intent", "confidence", "entities", "query_type"] |
|
for field in required_fields: |
|
if field not in analysis: |
|
analysis[field] = self._get_default_value(field) |
|
|
|
|
|
if not 0 <= analysis.get("confidence", 0) <= 1: |
|
analysis["confidence"] = 0.5 |
|
|
|
|
|
if not isinstance(analysis.get("entities"), dict): |
|
analysis["entities"] = {} |
|
|
|
return analysis |
|
|
|
def _get_default_value(self, field: str) -> Any: |
|
"""取得欄位預設值""" |
|
defaults = { |
|
"intent": "unknown", |
|
"confidence": 0.1, |
|
"entities": {}, |
|
"query_type": "unknown", |
|
"parameters": {} |
|
} |
|
return defaults.get(field, None) |
|
|
|
def _fallback_analysis(self, message: str) -> Dict[str, Any]: |
|
"""備用分析方法""" |
|
return { |
|
"intent": "unknown", |
|
"confidence": 0.1, |
|
"entities": {}, |
|
"query_type": "unknown", |
|
"parameters": {}, |
|
"fallback": True |
|
} |
|
|
|
async def generate_response(self, query_result: Dict[str, Any], user_message: str) -> str: |
|
""" |
|
使用 OpenRouter 生成更自然的回應 |
|
|
|
Args: |
|
query_result: 資料庫查詢結果 |
|
user_message: 用戶原始訊息 |
|
|
|
Returns: |
|
生成的回應文字 |
|
""" |
|
if not self.api_key: |
|
return self._generate_simple_response(query_result) |
|
|
|
try: |
|
prompt = f""" |
|
請根據以下資訊生成一個友善、自然的中文回應: |
|
|
|
用戶訊息: "{user_message}" |
|
查詢結果: {json.dumps(query_result, ensure_ascii=False)} |
|
|
|
要求: |
|
1. 回應要簡潔明瞭 |
|
2. 使用友善的語調 |
|
3. 如果有資料,要清楚呈現重要資訊 |
|
4. 如果沒有資料,要給出建議 |
|
5. 回應長度控制在 200 字以內 |
|
|
|
範例格式: |
|
- 找到資料時:「找到 X 筆資料:[列出重要資訊]」 |
|
- 沒有資料時:「很抱歉,沒有找到相關資料,您可以嘗試...」 |
|
""" |
|
|
|
async with httpx.AsyncClient(timeout=30.0) as client: |
|
response = await client.post( |
|
f"{self.base_url}/chat/completions", |
|
headers=self.headers, |
|
json={ |
|
"model": self.model, |
|
"messages": [ |
|
{ |
|
"role": "system", |
|
"content": "你是一個友善的客服助手,專門幫助用戶理解查詢結果。" |
|
}, |
|
{ |
|
"role": "user", |
|
"content": prompt |
|
} |
|
], |
|
"temperature": 0.3, |
|
"max_tokens": 300 |
|
} |
|
) |
|
|
|
if response.status_code == 200: |
|
result = response.json() |
|
generated_response = result["choices"][0]["message"]["content"].strip() |
|
return generated_response |
|
else: |
|
logger.error(f"OpenRouter 回應生成錯誤: {response.status_code}") |
|
return self._generate_simple_response(query_result) |
|
|
|
except Exception as e: |
|
logger.error(f"OpenRouter 回應生成失敗: {str(e)}") |
|
return self._generate_simple_response(query_result) |
|
|
|
def _generate_simple_response(self, query_result: Dict[str, Any]) -> str: |
|
"""簡單回應生成""" |
|
if query_result.get("success"): |
|
data_count = len(query_result.get("data", [])) |
|
if data_count > 0: |
|
return f"找到 {data_count} 筆相關資料。" |
|
else: |
|
return "沒有找到相關資料。" |
|
else: |
|
return "查詢時發生錯誤,請稍後再試。" |