linebot_pydantic_fastapi / backend /services /pydantic_ai_service.py
mickeywu520's picture
新增debug訊息
3887148
"""
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()
# 創建 Pydantic AI Agent
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
)
# 執行 AI Agent
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): # 最多顯示5個
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()]