mickeywu520's picture
first commit
cd9bca9
"""
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:
# 嘗試解析 JSON 回應
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 "查詢時發生錯誤,請稍後再試。"