|
""" |
|
Pydantic AI 服務 - 使用 Pydantic AI 框架整合 Groq 和商品查詢功能 |
|
解決商品查詢準確性問題,特別是像 "是否有推薦貓砂?" 這類查詢 |
|
""" |
|
|
|
import logging |
|
from typing import Dict, Any, List, Optional |
|
from dataclasses import dataclass |
|
from pydantic import BaseModel, Field |
|
from pydantic_ai import Agent, RunContext |
|
|
|
from backend.services.enhanced_product_service import EnhancedProductService |
|
from backend.services.database_service import DatabaseService |
|
from backend.config import settings |
|
|
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
@dataclass |
|
class ProductQueryDependencies: |
|
"""商品查詢依賴""" |
|
enhanced_product_service: EnhancedProductService |
|
database_service: DatabaseService |
|
user_id: Optional[str] = None |
|
|
|
|
|
class ProductQueryResult(BaseModel): |
|
"""商品查詢結果""" |
|
intent: str = Field(description="查詢意圖") |
|
response_text: str = Field(description="回應文字") |
|
products_found: int = Field(description="找到的商品數量") |
|
has_recommendations: bool = Field(description="是否包含推薦") |
|
stock_info_included: bool = Field(description="是否包含庫存資訊") |
|
search_keywords: List[str] = Field(default_factory=list, description="搜尋關鍵字") |
|
|
|
class ProductQueryService: |
|
"""Pydantic AI 商品查詢服務""" |
|
|
|
def __init__(self): |
|
self.enhanced_product_service = EnhancedProductService() |
|
self.database_service = DatabaseService() |
|
|
|
|
|
self.product_agent = Agent( |
|
f'groq:{settings.GROQ_MODEL}', |
|
deps_type=ProductQueryDependencies, |
|
output_type=ProductQueryResult, |
|
system_prompt=self._get_system_prompt() |
|
) |
|
|
|
|
|
self._register_tools() |
|
|
|
def _get_system_prompt(self) -> str: |
|
"""系統提示詞""" |
|
return """你是一個專業的商品查詢助手,專門協助用戶查詢商品資訊。 |
|
|
|
你的主要任務: |
|
1. 理解用戶的商品查詢意圖,包括推薦、搜尋、庫存查詢等 |
|
2. 使用適當的工具查詢商品資料庫 |
|
3. 提供準確、有用的商品資訊回應 |
|
|
|
特別注意: |
|
- 當用戶詢問"推薦"、"有沒有"、"是否有"時,要積極查詢相關商品 |
|
- 優先顯示有庫存的商品 |
|
- 提供具體的商品名稱、庫存狀況和分類資訊 |
|
- 如果沒有找到完全匹配的商品,嘗試提供相似或相關的商品 |
|
|
|
回應要求: |
|
- 使用繁體中文 |
|
- 語氣友善專業 |
|
- 資訊準確完整 |
|
- 如果有多個商品,按庫存量排序推薦""" |
|
|
|
def _register_tools(self): |
|
"""註冊 AI Agent 工具""" |
|
|
|
@self.product_agent.tool |
|
async def search_products( |
|
ctx: RunContext[ProductQueryDependencies], |
|
query_text: str, |
|
include_recommendations: bool = False |
|
) -> Dict[str, Any]: |
|
""" |
|
搜尋商品 |
|
|
|
Args: |
|
query_text: 搜尋關鍵字 |
|
include_recommendations: 是否包含推薦功能 |
|
""" |
|
try: |
|
if include_recommendations: |
|
|
|
result = ctx.deps.enhanced_product_service.get_product_recommendations( |
|
query_text=query_text, |
|
limit=10 |
|
) |
|
else: |
|
|
|
result = ctx.deps.enhanced_product_service.search_products_advanced( |
|
query_text=query_text, |
|
include_stock_info=True, |
|
limit=10 |
|
) |
|
|
|
return { |
|
"success": result.success, |
|
"products": result.data, |
|
"count": result.count, |
|
"error": result.error |
|
} |
|
except Exception as e: |
|
logger.error(f"商品搜尋工具錯誤: {str(e)}") |
|
return { |
|
"success": False, |
|
"products": [], |
|
"count": 0, |
|
"error": str(e) |
|
} |
|
|
|
@self.product_agent.tool |
|
async def get_products_by_category( |
|
ctx: RunContext[ProductQueryDependencies], |
|
category_name: str |
|
) -> Dict[str, Any]: |
|
""" |
|
根據分類獲取商品 |
|
|
|
Args: |
|
category_name: 分類名稱 |
|
""" |
|
try: |
|
result = ctx.deps.enhanced_product_service.get_products_by_category( |
|
category_name=category_name, |
|
limit=10 |
|
) |
|
|
|
return { |
|
"success": result.success, |
|
"products": result.data, |
|
"count": result.count, |
|
"error": result.error |
|
} |
|
except Exception as e: |
|
logger.error(f"分類查詢工具錯誤: {str(e)}") |
|
return { |
|
"success": False, |
|
"products": [], |
|
"count": 0, |
|
"error": str(e) |
|
} |
|
|
|
@self.product_agent.tool |
|
async def check_low_stock_products( |
|
ctx: RunContext[ProductQueryDependencies], |
|
threshold: int = 10 |
|
) -> Dict[str, Any]: |
|
""" |
|
檢查低庫存商品 |
|
|
|
Args: |
|
threshold: 庫存閾值 |
|
""" |
|
try: |
|
result = ctx.deps.enhanced_product_service.get_low_stock_products( |
|
threshold=threshold |
|
) |
|
|
|
return { |
|
"success": result.success, |
|
"products": result.data, |
|
"count": result.count, |
|
"error": result.error |
|
} |
|
except Exception as e: |
|
logger.error(f"低庫存查詢工具錯誤: {str(e)}") |
|
return { |
|
"success": False, |
|
"products": [], |
|
"count": 0, |
|
"error": str(e) |
|
} |
|
|
|
def process_product_query_sync(self, user_message: str, user_id: str = None) -> Dict[str, Any]: |
|
""" |
|
同步處理商品查詢 - 為了避免異步複雜性 |
|
|
|
Args: |
|
user_message: 用戶訊息 |
|
user_id: 用戶ID |
|
|
|
Returns: |
|
查詢結果字典 |
|
""" |
|
try: |
|
logger.info(f"🔍 Pydantic AI 開始處理商品查詢: '{user_message}'") |
|
|
|
|
|
intent_analysis = self.analyze_query_intent(user_message) |
|
logger.info(f"📊 意圖分析結果: {intent_analysis}") |
|
|
|
if not intent_analysis["is_product_query"]: |
|
logger.warning(f"❌ 非商品查詢意圖,拒絕處理") |
|
return { |
|
"success": False, |
|
"text": "這似乎不是商品查詢,請嘗試其他功能。", |
|
"mode": "product_query", |
|
"error": "非商品查詢意圖" |
|
} |
|
|
|
|
|
if intent_analysis["is_recommendation"]: |
|
logger.info(f"🛍️ 執行推薦查詢") |
|
|
|
result = self.enhanced_product_service.get_product_recommendations( |
|
query_text=user_message, |
|
limit=5 |
|
) |
|
else: |
|
logger.info(f"🔍 執行一般搜尋") |
|
|
|
result = self.enhanced_product_service.search_products_advanced( |
|
query_text=user_message, |
|
include_stock_info=True, |
|
limit=10 |
|
) |
|
|
|
logger.info(f"📋 查詢結果: 成功={result.success}, 數量={result.count}, 錯誤={result.error}") |
|
|
|
|
|
if result.success and result.data: |
|
response_text = self._format_product_response(result.data, intent_analysis) |
|
|
|
return { |
|
"success": True, |
|
"intent": "product_query", |
|
"text": response_text, |
|
"products_found": result.count, |
|
"has_recommendations": intent_analysis["is_recommendation"], |
|
"stock_info_included": True, |
|
"search_keywords": self._extract_keywords_from_message(user_message), |
|
"mode": "product_query", |
|
"user_id": user_id |
|
} |
|
else: |
|
return { |
|
"success": False, |
|
"text": f"抱歉,沒有找到相關商品。{result.error if result.error else ''}", |
|
"mode": "product_query", |
|
"error": result.error, |
|
"user_id": user_id |
|
} |
|
|
|
except Exception as e: |
|
logger.error(f"❌ 同步商品查詢錯誤: {str(e)}") |
|
import traceback |
|
logger.error(f"📋 錯誤詳情: {traceback.format_exc()}") |
|
return { |
|
"success": False, |
|
"text": f"抱歉,商品查詢時發生錯誤:{str(e)}", |
|
"mode": "product_query", |
|
"error": str(e), |
|
"user_id": user_id |
|
} |
|
|
|
async def process_product_query(self, user_message: str, user_id: str = None) -> Dict[str, Any]: |
|
""" |
|
處理商品查詢 |
|
|
|
Args: |
|
user_message: 用戶訊息 |
|
user_id: 用戶ID |
|
|
|
Returns: |
|
查詢結果字典 |
|
""" |
|
try: |
|
|
|
deps = ProductQueryDependencies( |
|
enhanced_product_service=self.enhanced_product_service, |
|
database_service=self.database_service, |
|
user_id=user_id |
|
) |
|
|
|
|
|
result = await self.product_agent.run(user_message, deps=deps) |
|
|
|
|
|
if user_id: |
|
try: |
|
self.database_service.save_message(user_id, user_message, "product_query") |
|
except Exception as e: |
|
logger.warning(f"記錄查詢失敗: {str(e)}") |
|
|
|
return { |
|
"success": True, |
|
"intent": result.output.intent, |
|
"text": result.output.response_text, |
|
"products_found": result.output.products_found, |
|
"has_recommendations": result.output.has_recommendations, |
|
"stock_info_included": result.output.stock_info_included, |
|
"search_keywords": result.output.search_keywords, |
|
"mode": "product_query", |
|
"user_id": user_id |
|
} |
|
|
|
except Exception as e: |
|
logger.error(f"Pydantic AI 商品查詢錯誤: {str(e)}") |
|
return { |
|
"success": False, |
|
"text": f"抱歉,商品查詢時發生錯誤:{str(e)}", |
|
"mode": "product_query", |
|
"error": str(e), |
|
"user_id": user_id |
|
} |
|
|
|
def is_available(self) -> bool: |
|
"""檢查服務是否可用""" |
|
return bool(settings.GROQ_API_KEY) |
|
|
|
def analyze_query_intent(self, message: str) -> Dict[str, Any]: |
|
""" |
|
分析查詢意圖 - 判斷是否為商品查詢 |
|
|
|
Args: |
|
message: 用戶訊息 |
|
|
|
Returns: |
|
意圖分析結果 |
|
""" |
|
message_lower = message.lower() |
|
|
|
|
|
product_keywords = [ |
|
'推薦', '有沒有', '是否有', '請問有', '商品', '產品', '貨品', |
|
'查詢', '搜尋', '找', '庫存', '存貨', '價格', '多少錢', |
|
'貓砂', '狗糧', '寵物', '食品', '用品', '貓', '狗', '寵物用品', |
|
'cat', 'dog', 'pet', 'litter', 'food' |
|
] |
|
|
|
|
|
recommendation_keywords = ['推薦', '建議', '介紹', '有什麼', '哪些', '什麼好', '推薦一些'] |
|
|
|
|
|
inventory_keywords = ['庫存', '存貨', '剩餘', '還有', '現貨', '有多少', '剩多少'] |
|
|
|
is_product_query = any(keyword in message_lower for keyword in product_keywords) |
|
is_recommendation = any(keyword in message_lower for keyword in recommendation_keywords) |
|
is_inventory_check = any(keyword in message_lower for keyword in inventory_keywords) |
|
|
|
confidence = 0.5 |
|
if is_product_query: |
|
confidence += 0.3 |
|
if is_recommendation: |
|
confidence += 0.2 |
|
if is_inventory_check: |
|
confidence += 0.2 |
|
|
|
return { |
|
"is_product_query": is_product_query, |
|
"is_recommendation": is_recommendation, |
|
"is_inventory_check": is_inventory_check, |
|
"confidence": min(confidence, 1.0), |
|
"intent": "product_query" if is_product_query else "unknown" |
|
} |
|
|
|
def _format_product_response(self, products: List[Dict[str, Any]], intent_analysis: Dict[str, Any]) -> str: |
|
"""格式化商品查詢回應""" |
|
if not products: |
|
return "沒有找到相關商品。" |
|
|
|
|
|
if intent_analysis["is_recommendation"]: |
|
header = f"為您推薦 {len(products)} 個商品:" |
|
elif intent_analysis["is_inventory_check"]: |
|
header = f"庫存查詢結果,找到 {len(products)} 個商品:" |
|
else: |
|
header = f"商品搜尋結果,找到 {len(products)} 個商品:" |
|
|
|
response_lines = [header, ""] |
|
|
|
for i, product in enumerate(products[:5], 1): |
|
name = product.get('product_name', 'N/A') |
|
stock = product.get('current_stock', 0) |
|
category = product.get('category_name', '') |
|
warehouse = product.get('warehouse', '') |
|
|
|
|
|
stock_status = product.get('stock_status', self._get_stock_status_text(stock)) |
|
|
|
line = f"{i}. {name}" |
|
if category: |
|
line += f" ({category})" |
|
|
|
line += f"\n 庫存: {stock} - {stock_status}" |
|
|
|
if warehouse: |
|
line += f"\n 倉庫: {warehouse}" |
|
|
|
if product.get('recommendation_reason'): |
|
line += f"\n 推薦原因: {product['recommendation_reason']}" |
|
|
|
response_lines.append(line) |
|
|
|
if len(products) > 5: |
|
response_lines.append(f"\n... 還有 {len(products) - 5} 個商品") |
|
|
|
|
|
low_stock_count = sum(1 for p in products if p.get('current_stock', 0) <= 10) |
|
if low_stock_count > 0: |
|
response_lines.append(f"\n⚠️ 其中 {low_stock_count} 個商品庫存偏低,建議盡快補貨。") |
|
|
|
return "\n".join(response_lines) |
|
|
|
def _get_stock_status_text(self, stock: int) -> str: |
|
"""獲取庫存狀態文字""" |
|
if stock <= 0: |
|
return "缺貨 ❌" |
|
elif stock <= 5: |
|
return "庫存極低 🔴" |
|
elif stock <= 10: |
|
return "庫存偏低 🟡" |
|
elif stock <= 50: |
|
return "庫存正常 🟢" |
|
else: |
|
return "庫存充足 ✅" |
|
|
|
def _extract_keywords_from_message(self, message: str) -> List[str]: |
|
"""從訊息中提取關鍵字""" |
|
|
|
stop_words = ['推薦', '有沒有', '是否有', '請問', '想要', '需要', '找', '查詢', '搜尋', '?', '?'] |
|
|
|
|
|
words = message.replace('?', '').replace('?', '').split() |
|
keywords = [word for word in words if word not in stop_words and len(word) > 1] |
|
|
|
return keywords if keywords else [message.strip()] |
|
|