Spaces:
Running
on
Zero
Running
on
Zero
Upload 19 files
Browse files- .gitattributes +1 -0
- adaptive_score_distribution.py +448 -0
- animal_detector.db +0 -0
- breed_recommendation_enhanced.py +9 -15
- constraint_manager.py +211 -5
- dimension_score_calculator.py +42 -8
- dynamic_weight_calculator.py +415 -0
- inference_engine.py +408 -0
- matching_score_calculator.py +141 -3
- multi_head_scorer.py +361 -155
- priority_detector.py +462 -0
- query_understanding.py +48 -6
- recommendation_css.py +687 -0
- recommendation_formatter.py +129 -18
- recommendation_html_format.py +18 -5
- recommendation_html_formatter.py +19 -579
- scoring_calculation_system.py +5 -2
- semantic_breed_recommender.py +589 -64
- smart_breed_filter.py +454 -0
- user_query_analyzer.py +62 -1
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
animal_detector.db filter=lfs diff=lfs merge=lfs -text
|
adaptive_score_distribution.py
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# %%writefile adaptive_score_distribution.py
|
| 2 |
+
import numpy as np
|
| 3 |
+
from typing import List, Tuple, Dict, Optional, Any
|
| 4 |
+
from dataclasses import dataclass, field
|
| 5 |
+
import traceback
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
@dataclass
|
| 9 |
+
class GradientAnalysis:
|
| 10 |
+
"""梯度分析結果"""
|
| 11 |
+
top_score: float
|
| 12 |
+
bottom_score: float
|
| 13 |
+
score_range: float
|
| 14 |
+
top5_std: float
|
| 15 |
+
top5_range: float
|
| 16 |
+
gradient_type: str # 'steep', 'moderate', 'flat'
|
| 17 |
+
score_distribution: List[float] = field(default_factory=list)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@dataclass
|
| 21 |
+
class ScenarioClassification:
|
| 22 |
+
"""情境分類結果"""
|
| 23 |
+
scenario_type: str # 'perfect_match', 'good_choices', 'moderate_fit', 'challenging'
|
| 24 |
+
confidence: float
|
| 25 |
+
reasoning: str
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
@dataclass
|
| 29 |
+
class DistributionResult:
|
| 30 |
+
"""分數分佈結果"""
|
| 31 |
+
final_scores: List[Tuple[str, float]] = field(default_factory=list)
|
| 32 |
+
gradient_analysis: Optional[GradientAnalysis] = None
|
| 33 |
+
scenario_classification: Optional[ScenarioClassification] = None
|
| 34 |
+
adjustment_applied: str = 'none'
|
| 35 |
+
adjustment_notes: List[str] = field(default_factory=list)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class AdaptiveScoreDistribution:
|
| 39 |
+
"""
|
| 40 |
+
自適應分數分佈系統
|
| 41 |
+
根據情境梯度自然形成分數分佈,不強制固定範圍
|
| 42 |
+
|
| 43 |
+
核心理念:
|
| 44 |
+
- 完美匹配 → 自然高分 (90+)
|
| 45 |
+
- 多個選擇 → 自然接近 (差距2-5分)
|
| 46 |
+
- 不適合 → 自然偏低 (60-70)
|
| 47 |
+
- 保證最低分 >= 60
|
| 48 |
+
"""
|
| 49 |
+
|
| 50 |
+
def __init__(self):
|
| 51 |
+
"""初始化自適應分數分佈系統"""
|
| 52 |
+
self.min_score = 0.60 # 全域最低分(觸底保護)
|
| 53 |
+
self.no_intervention_threshold = 0.10
|
| 54 |
+
self.gradient_thresholds = {
|
| 55 |
+
'steep_std': 0.04,
|
| 56 |
+
'steep_range': 0.12,
|
| 57 |
+
'flat_std': 0.02,
|
| 58 |
+
'flat_range': 0.05
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
def distribute_scores(self,
|
| 62 |
+
raw_scores: List[Tuple[str, float]]) -> DistributionResult:
|
| 63 |
+
"""
|
| 64 |
+
自適應分數分佈
|
| 65 |
+
|
| 66 |
+
Args:
|
| 67 |
+
raw_scores: 原始分數列表 [(breed_name, score), ...]
|
| 68 |
+
|
| 69 |
+
Returns:
|
| 70 |
+
DistributionResult: 分佈結果
|
| 71 |
+
"""
|
| 72 |
+
try:
|
| 73 |
+
if not raw_scores:
|
| 74 |
+
return DistributionResult()
|
| 75 |
+
|
| 76 |
+
# Step 1: 分析梯度
|
| 77 |
+
gradient_analysis = self._analyze_gradient(raw_scores)
|
| 78 |
+
|
| 79 |
+
# Step 2: 判斷情境
|
| 80 |
+
scenario = self._classify_scenario(gradient_analysis)
|
| 81 |
+
|
| 82 |
+
# Step 3: 決定調整策略
|
| 83 |
+
adjusted_scores, adjustment_type, notes = self._apply_adaptive_strategy(
|
| 84 |
+
raw_scores, scenario, gradient_analysis
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
# Step 4: 應用最低分保護
|
| 88 |
+
final_scores = self._apply_floor_protection(adjusted_scores)
|
| 89 |
+
|
| 90 |
+
return DistributionResult(
|
| 91 |
+
final_scores=final_scores,
|
| 92 |
+
gradient_analysis=gradient_analysis,
|
| 93 |
+
scenario_classification=scenario,
|
| 94 |
+
adjustment_applied=adjustment_type,
|
| 95 |
+
adjustment_notes=notes
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
except Exception as e:
|
| 99 |
+
print(f"Error distributing scores: {str(e)}")
|
| 100 |
+
print(traceback.format_exc())
|
| 101 |
+
return DistributionResult(
|
| 102 |
+
final_scores=raw_scores,
|
| 103 |
+
adjustment_applied='error_fallback'
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
def _analyze_gradient(self,
|
| 107 |
+
scores: List[Tuple[str, float]]) -> GradientAnalysis:
|
| 108 |
+
"""
|
| 109 |
+
分析分數梯度特徵
|
| 110 |
+
|
| 111 |
+
Args:
|
| 112 |
+
scores: 分數列表
|
| 113 |
+
|
| 114 |
+
Returns:
|
| 115 |
+
GradientAnalysis: 梯度分析結果
|
| 116 |
+
"""
|
| 117 |
+
try:
|
| 118 |
+
sorted_scores = sorted(scores, key=lambda x: x[1], reverse=True)
|
| 119 |
+
score_values = [s[1] for s in sorted_scores]
|
| 120 |
+
|
| 121 |
+
top_score = score_values[0] if score_values else 0.5
|
| 122 |
+
bottom_score = score_values[-1] if score_values else 0.5
|
| 123 |
+
score_range = top_score - bottom_score
|
| 124 |
+
|
| 125 |
+
# 前5名統計
|
| 126 |
+
top5_scores = score_values[:min(5, len(score_values))]
|
| 127 |
+
top5_std = float(np.std(top5_scores)) if len(top5_scores) > 1 else 0.0
|
| 128 |
+
top5_range = top5_scores[0] - top5_scores[-1] if len(top5_scores) >= 2 else 0.0
|
| 129 |
+
|
| 130 |
+
# 梯度類型判斷
|
| 131 |
+
if top5_std > self.gradient_thresholds['steep_std'] or \
|
| 132 |
+
top5_range > self.gradient_thresholds['steep_range']:
|
| 133 |
+
gradient_type = 'steep'
|
| 134 |
+
elif top5_std < self.gradient_thresholds['flat_std'] or \
|
| 135 |
+
top5_range < self.gradient_thresholds['flat_range']:
|
| 136 |
+
gradient_type = 'flat'
|
| 137 |
+
else:
|
| 138 |
+
gradient_type = 'moderate'
|
| 139 |
+
|
| 140 |
+
return GradientAnalysis(
|
| 141 |
+
top_score=top_score,
|
| 142 |
+
bottom_score=bottom_score,
|
| 143 |
+
score_range=score_range,
|
| 144 |
+
top5_std=top5_std,
|
| 145 |
+
top5_range=top5_range,
|
| 146 |
+
gradient_type=gradient_type,
|
| 147 |
+
score_distribution=score_values
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
except Exception as e:
|
| 151 |
+
print(f"Error analyzing gradient: {str(e)}")
|
| 152 |
+
return GradientAnalysis(
|
| 153 |
+
top_score=0.5,
|
| 154 |
+
bottom_score=0.5,
|
| 155 |
+
score_range=0.0,
|
| 156 |
+
top5_std=0.0,
|
| 157 |
+
top5_range=0.0,
|
| 158 |
+
gradient_type='moderate',
|
| 159 |
+
score_distribution=[]
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
def _classify_scenario(self,
|
| 163 |
+
gradient_analysis: GradientAnalysis) -> ScenarioClassification:
|
| 164 |
+
"""
|
| 165 |
+
根據梯度分析分類情境
|
| 166 |
+
|
| 167 |
+
情境類型:
|
| 168 |
+
1. perfect_match: 完美匹配(第1名分數高且梯度陡峭)
|
| 169 |
+
2. good_choices: 多個好選擇(前5名分數都高且梯度平坦)
|
| 170 |
+
3. moderate_fit: 中等匹配(第1名分數中等)
|
| 171 |
+
4. challenging: 挑戰情境(第1名分數偏低)
|
| 172 |
+
|
| 173 |
+
Args:
|
| 174 |
+
gradient_analysis: 梯度分析結果
|
| 175 |
+
|
| 176 |
+
Returns:
|
| 177 |
+
ScenarioClassification: 情境分類結果
|
| 178 |
+
"""
|
| 179 |
+
top_score = gradient_analysis.top_score
|
| 180 |
+
gradient_type = gradient_analysis.gradient_type
|
| 181 |
+
|
| 182 |
+
if top_score >= 0.88 and gradient_type == 'steep': # Increased from 0.85
|
| 183 |
+
return ScenarioClassification(
|
| 184 |
+
scenario_type='perfect_match',
|
| 185 |
+
confidence=0.9,
|
| 186 |
+
reasoning="High top score with clear differentiation indicates perfect match"
|
| 187 |
+
)
|
| 188 |
+
|
| 189 |
+
elif top_score >= 0.78 and gradient_type == 'flat': # Increased from 0.75
|
| 190 |
+
return ScenarioClassification(
|
| 191 |
+
scenario_type='good_choices',
|
| 192 |
+
confidence=0.85,
|
| 193 |
+
reasoning="Multiple high-scoring breeds with similar fitness"
|
| 194 |
+
)
|
| 195 |
+
|
| 196 |
+
elif top_score >= 0.68: # Reduced from 0.70 to be less inflating
|
| 197 |
+
return ScenarioClassification(
|
| 198 |
+
scenario_type='moderate_fit',
|
| 199 |
+
confidence=0.75,
|
| 200 |
+
reasoning="Moderate match quality with acceptable options"
|
| 201 |
+
)
|
| 202 |
+
|
| 203 |
+
else:
|
| 204 |
+
return ScenarioClassification(
|
| 205 |
+
scenario_type='challenging',
|
| 206 |
+
confidence=0.65,
|
| 207 |
+
reasoning="Lower overall match quality, may need requirement adjustment"
|
| 208 |
+
)
|
| 209 |
+
|
| 210 |
+
def _apply_adaptive_strategy(self,
|
| 211 |
+
raw_scores: List[Tuple[str, float]],
|
| 212 |
+
scenario: ScenarioClassification,
|
| 213 |
+
gradient_analysis: GradientAnalysis) -> Tuple[List[Tuple[str, float]], str, List[str]]:
|
| 214 |
+
"""
|
| 215 |
+
根據情境類型應用不同的調整策略
|
| 216 |
+
|
| 217 |
+
Args:
|
| 218 |
+
raw_scores: 原始分數
|
| 219 |
+
scenario: 情境分類
|
| 220 |
+
gradient_analysis: 梯度分析
|
| 221 |
+
|
| 222 |
+
Returns:
|
| 223 |
+
Tuple: (調整後分數, 調整類型, 調整註記)
|
| 224 |
+
"""
|
| 225 |
+
sorted_scores = sorted(raw_scores, key=lambda x: x[1], reverse=True)
|
| 226 |
+
notes = []
|
| 227 |
+
|
| 228 |
+
if scenario.scenario_type == 'perfect_match':
|
| 229 |
+
# 完美匹配: 不調整,保持自然
|
| 230 |
+
notes.append("Perfect match scenario: No adjustment needed")
|
| 231 |
+
return sorted_scores, 'no_adjustment', notes
|
| 232 |
+
|
| 233 |
+
elif scenario.scenario_type == 'good_choices':
|
| 234 |
+
# 多個好選擇: 確保最小區分度
|
| 235 |
+
adjusted, adjustment_notes = self._ensure_minimum_differentiation(
|
| 236 |
+
sorted_scores, gradient_analysis
|
| 237 |
+
)
|
| 238 |
+
notes.extend(adjustment_notes)
|
| 239 |
+
return adjusted, 'minimum_differentiation', notes
|
| 240 |
+
|
| 241 |
+
elif scenario.scenario_type == 'moderate_fit':
|
| 242 |
+
# 中等匹配: 溫和提升
|
| 243 |
+
adjusted, adjustment_notes = self._gentle_uplift(
|
| 244 |
+
sorted_scores, target_top=0.80
|
| 245 |
+
)
|
| 246 |
+
notes.extend(adjustment_notes)
|
| 247 |
+
return adjusted, 'gentle_uplift', notes
|
| 248 |
+
|
| 249 |
+
elif scenario.scenario_type == 'challenging':
|
| 250 |
+
# 挑戰情境: 適度提升但不過度
|
| 251 |
+
adjusted, adjustment_notes = self._moderate_uplift(
|
| 252 |
+
sorted_scores, target_top=0.72
|
| 253 |
+
)
|
| 254 |
+
notes.extend(adjustment_notes)
|
| 255 |
+
return adjusted, 'moderate_uplift', notes
|
| 256 |
+
|
| 257 |
+
return sorted_scores, 'no_adjustment', notes
|
| 258 |
+
|
| 259 |
+
def _ensure_minimum_differentiation(self,
|
| 260 |
+
scores: List[Tuple[str, float]],
|
| 261 |
+
gradient_analysis: GradientAnalysis) -> Tuple[List[Tuple[str, float]], List[str]]:
|
| 262 |
+
"""
|
| 263 |
+
確保最小區分度(當分數過於接近時)
|
| 264 |
+
|
| 265 |
+
Args:
|
| 266 |
+
scores: 分數列表
|
| 267 |
+
gradient_analysis: 梯度分析
|
| 268 |
+
|
| 269 |
+
Returns:
|
| 270 |
+
Tuple: (調整後分數, 註記)
|
| 271 |
+
"""
|
| 272 |
+
notes = []
|
| 273 |
+
top5_range = gradient_analysis.top5_range
|
| 274 |
+
|
| 275 |
+
# 如果前5名差距 >= 5%,不需要調整
|
| 276 |
+
if top5_range >= 0.05:
|
| 277 |
+
notes.append(f"Differentiation sufficient (range: {top5_range:.3f})")
|
| 278 |
+
return scores, notes
|
| 279 |
+
|
| 280 |
+
# 需要擴展區分度
|
| 281 |
+
top5 = scores[:5]
|
| 282 |
+
rest = scores[5:]
|
| 283 |
+
|
| 284 |
+
target_range = 0.05
|
| 285 |
+
current_top = top5[0][1] if top5 else 0.5
|
| 286 |
+
current_bottom = top5[-1][1] if len(top5) > 0 else 0.5
|
| 287 |
+
|
| 288 |
+
adjusted_top5 = []
|
| 289 |
+
for i, (breed, score) in enumerate(top5):
|
| 290 |
+
if len(top5) > 1:
|
| 291 |
+
position = i / (len(top5) - 1)
|
| 292 |
+
new_score = current_top - (position * target_range)
|
| 293 |
+
else:
|
| 294 |
+
new_score = score
|
| 295 |
+
adjusted_top5.append((breed, new_score))
|
| 296 |
+
|
| 297 |
+
notes.append(f"Expanded top 5 differentiation to {target_range:.1%}")
|
| 298 |
+
return adjusted_top5 + rest, notes
|
| 299 |
+
|
| 300 |
+
def _gentle_uplift(self,
|
| 301 |
+
scores: List[Tuple[str, float]],
|
| 302 |
+
target_top: float = 0.75) -> Tuple[List[Tuple[str, float]], List[str]]:
|
| 303 |
+
"""
|
| 304 |
+
溫和提升(保持分數分佈形狀)
|
| 305 |
+
|
| 306 |
+
Args:
|
| 307 |
+
scores: 分數列表
|
| 308 |
+
target_top: 目標第1名分數 (reduced from 0.80 to 0.75)
|
| 309 |
+
|
| 310 |
+
Returns:
|
| 311 |
+
Tuple: (調整後分數, 註記)
|
| 312 |
+
"""
|
| 313 |
+
notes = []
|
| 314 |
+
|
| 315 |
+
if not scores:
|
| 316 |
+
return scores, notes
|
| 317 |
+
|
| 318 |
+
current_top = scores[0][1]
|
| 319 |
+
|
| 320 |
+
if current_top >= target_top:
|
| 321 |
+
notes.append(f"Top score already sufficient ({current_top:.3f})")
|
| 322 |
+
return scores, notes
|
| 323 |
+
|
| 324 |
+
# 計算提升量
|
| 325 |
+
uplift = target_top - current_top
|
| 326 |
+
|
| 327 |
+
# 所有品種統一提升
|
| 328 |
+
adjusted = [(breed, min(1.0, score + uplift)) for breed, score in scores]
|
| 329 |
+
|
| 330 |
+
notes.append(f"Applied gentle uplift: +{uplift:.3f} to all breeds")
|
| 331 |
+
return adjusted, notes
|
| 332 |
+
|
| 333 |
+
def _moderate_uplift(self,
|
| 334 |
+
scores: List[Tuple[str, float]],
|
| 335 |
+
target_top: float = 0.68) -> Tuple[List[Tuple[str, float]], List[str]]:
|
| 336 |
+
"""
|
| 337 |
+
適度提升(挑戰情境)
|
| 338 |
+
|
| 339 |
+
Args:
|
| 340 |
+
scores: 分數列表
|
| 341 |
+
target_top: 目標第1名分數 (reduced from 0.72 to 0.68)
|
| 342 |
+
|
| 343 |
+
Returns:
|
| 344 |
+
Tuple: (調整後分數, 註記)
|
| 345 |
+
"""
|
| 346 |
+
notes = []
|
| 347 |
+
|
| 348 |
+
if not scores:
|
| 349 |
+
return scores, notes
|
| 350 |
+
|
| 351 |
+
current_top = scores[0][1]
|
| 352 |
+
current_bottom = scores[-1][1] if scores else 0.5
|
| 353 |
+
|
| 354 |
+
adjusted = []
|
| 355 |
+
for breed, score in scores:
|
| 356 |
+
# 非線性提升: 分數越高提升越多
|
| 357 |
+
if current_top > current_bottom:
|
| 358 |
+
relative_position = (score - current_bottom) / (current_top - current_bottom + 0.001)
|
| 359 |
+
else:
|
| 360 |
+
relative_position = 1.0
|
| 361 |
+
|
| 362 |
+
uplift_factor = 1.0 + (relative_position * 0.12) # 最多提升12% (reduced from 15%)
|
| 363 |
+
new_score = min(1.0, score * uplift_factor)
|
| 364 |
+
adjusted.append((breed, new_score))
|
| 365 |
+
|
| 366 |
+
notes.append("Applied moderate uplift with position-based scaling")
|
| 367 |
+
return adjusted, notes
|
| 368 |
+
|
| 369 |
+
def _apply_floor_protection(self,
|
| 370 |
+
scores: List[Tuple[str, float]]) -> List[Tuple[str, float]]:
|
| 371 |
+
"""
|
| 372 |
+
應用最低分保護(確保沒有品種低於60分)
|
| 373 |
+
|
| 374 |
+
Args:
|
| 375 |
+
scores: 分數列表
|
| 376 |
+
|
| 377 |
+
Returns:
|
| 378 |
+
List[Tuple[str, float]]: 保護後分數
|
| 379 |
+
"""
|
| 380 |
+
protected = []
|
| 381 |
+
for breed, score in scores:
|
| 382 |
+
protected_score = max(self.min_score, score)
|
| 383 |
+
protected.append((breed, protected_score))
|
| 384 |
+
|
| 385 |
+
return protected
|
| 386 |
+
|
| 387 |
+
def get_distribution_summary(self, result: DistributionResult) -> Dict[str, Any]:
|
| 388 |
+
"""
|
| 389 |
+
獲取分佈摘要
|
| 390 |
+
|
| 391 |
+
Args:
|
| 392 |
+
result: 分佈結果
|
| 393 |
+
|
| 394 |
+
Returns:
|
| 395 |
+
Dict[str, Any]: 分佈摘要
|
| 396 |
+
"""
|
| 397 |
+
if not result.final_scores:
|
| 398 |
+
return {'error': 'No scores to summarize'}
|
| 399 |
+
|
| 400 |
+
score_values = [s[1] for s in result.final_scores]
|
| 401 |
+
|
| 402 |
+
return {
|
| 403 |
+
'scenario_type': result.scenario_classification.scenario_type if result.scenario_classification else 'unknown',
|
| 404 |
+
'adjustment_applied': result.adjustment_applied,
|
| 405 |
+
'score_statistics': {
|
| 406 |
+
'top_score': max(score_values) if score_values else 0,
|
| 407 |
+
'bottom_score': min(score_values) if score_values else 0,
|
| 408 |
+
'mean_score': float(np.mean(score_values)) if score_values else 0,
|
| 409 |
+
'std_score': float(np.std(score_values)) if score_values else 0,
|
| 410 |
+
'range': max(score_values) - min(score_values) if score_values else 0
|
| 411 |
+
},
|
| 412 |
+
'gradient_info': {
|
| 413 |
+
'type': result.gradient_analysis.gradient_type if result.gradient_analysis else 'unknown',
|
| 414 |
+
'top5_std': result.gradient_analysis.top5_std if result.gradient_analysis else 0,
|
| 415 |
+
'top5_range': result.gradient_analysis.top5_range if result.gradient_analysis else 0
|
| 416 |
+
},
|
| 417 |
+
'adjustment_notes': result.adjustment_notes,
|
| 418 |
+
'top_3_breeds': result.final_scores[:3] if result.final_scores else []
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
|
| 422 |
+
def distribute_breed_scores(raw_scores: List[Tuple[str, float]]) -> DistributionResult:
|
| 423 |
+
"""
|
| 424 |
+
便利函數: 分佈品種分數
|
| 425 |
+
|
| 426 |
+
Args:
|
| 427 |
+
raw_scores: 原始分數列表
|
| 428 |
+
|
| 429 |
+
Returns:
|
| 430 |
+
DistributionResult: 分佈結果
|
| 431 |
+
"""
|
| 432 |
+
distributor = AdaptiveScoreDistribution()
|
| 433 |
+
return distributor.distribute_scores(raw_scores)
|
| 434 |
+
|
| 435 |
+
|
| 436 |
+
def get_distribution_summary(raw_scores: List[Tuple[str, float]]) -> Dict[str, Any]:
|
| 437 |
+
"""
|
| 438 |
+
便利函數: 獲取分佈摘要
|
| 439 |
+
|
| 440 |
+
Args:
|
| 441 |
+
raw_scores: 原始分數列表
|
| 442 |
+
|
| 443 |
+
Returns:
|
| 444 |
+
Dict[str, Any]: 分佈摘要
|
| 445 |
+
"""
|
| 446 |
+
distributor = AdaptiveScoreDistribution()
|
| 447 |
+
result = distributor.distribute_scores(raw_scores)
|
| 448 |
+
return distributor.get_distribution_summary(result)
|
animal_detector.db
CHANGED
|
Binary files a/animal_detector.db and b/animal_detector.db differ
|
|
|
breed_recommendation_enhanced.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
from typing import Dict, List, Any, Optional
|
| 3 |
import traceback
|
|
@@ -23,14 +24,12 @@ def create_description_examples():
|
|
| 23 |
font-size: 1.1em;
|
| 24 |
font-weight: 600;
|
| 25 |
'>💡 Example Descriptions - Try These Expression Styles:</h4>
|
| 26 |
-
|
| 27 |
<div style='
|
| 28 |
display: grid;
|
| 29 |
grid-template-columns: 1fr 1fr;
|
| 30 |
gap: 15px;
|
| 31 |
margin-top: 10px;
|
| 32 |
'>
|
| 33 |
-
|
| 34 |
<!-- 左上:冷色(藍) -->
|
| 35 |
<div style='
|
| 36 |
background: white;
|
|
@@ -39,12 +38,11 @@ def create_description_examples():
|
|
| 39 |
border: 1px solid #e2e8f0;
|
| 40 |
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
| 41 |
'>
|
| 42 |
-
<strong style='color: #4299e1;'>🏡
|
| 43 |
<span style='color: #4a5568; font-size: 0.9em;'>
|
| 44 |
-
"
|
| 45 |
</span>
|
| 46 |
</div>
|
| 47 |
-
|
| 48 |
<!-- 右上:暖色(橘) -->
|
| 49 |
<div style='
|
| 50 |
background: white;
|
|
@@ -53,12 +51,11 @@ def create_description_examples():
|
|
| 53 |
border: 1px solid #e2e8f0;
|
| 54 |
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
| 55 |
'>
|
| 56 |
-
<strong style='color: #ed8936;'>🎾
|
| 57 |
<span style='color: #4a5568; font-size: 0.9em;'>
|
| 58 |
-
"I
|
| 59 |
</span>
|
| 60 |
</div>
|
| 61 |
-
|
| 62 |
<!-- 左下:冷色(紫) -->
|
| 63 |
<div style='
|
| 64 |
background: white;
|
|
@@ -67,12 +64,11 @@ def create_description_examples():
|
|
| 67 |
border: 1px solid #e2e8f0;
|
| 68 |
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
| 69 |
'>
|
| 70 |
-
<strong style='color: #805ad5;'
|
| 71 |
<span style='color: #4a5568; font-size: 0.9em;'>
|
| 72 |
-
"I live in a
|
| 73 |
</span>
|
| 74 |
</div>
|
| 75 |
-
|
| 76 |
<!-- 右下:暖色(琥珀橘) -->
|
| 77 |
<div style='
|
| 78 |
background: white;
|
|
@@ -81,13 +77,12 @@ def create_description_examples():
|
|
| 81 |
border: 1px solid #e2e8f0;
|
| 82 |
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
| 83 |
'>
|
| 84 |
-
<strong style='color: #276749;'>🤫
|
| 85 |
<span style='color: #4a5568; font-size: 0.9em;'>
|
| 86 |
-
"I
|
| 87 |
</span>
|
| 88 |
</div>
|
| 89 |
</div>
|
| 90 |
-
|
| 91 |
<div style='
|
| 92 |
margin-top: 15px;
|
| 93 |
padding: 12px;
|
|
@@ -126,7 +121,6 @@ def create_recommendation_tab(
|
|
| 126 |
background: linear-gradient(to right, rgba(66, 153, 225, 0.1), rgba(72, 187, 120, 0.1));
|
| 127 |
border-radius: 10px;
|
| 128 |
'>
|
| 129 |
-
|
| 130 |
<p style='
|
| 131 |
font-size: 1.2em;
|
| 132 |
margin: 0;
|
|
|
|
| 1 |
+
# %%writefile breed_recommendation_enhanced.py
|
| 2 |
import gradio as gr
|
| 3 |
from typing import Dict, List, Any, Optional
|
| 4 |
import traceback
|
|
|
|
| 24 |
font-size: 1.1em;
|
| 25 |
font-weight: 600;
|
| 26 |
'>💡 Example Descriptions - Try These Expression Styles:</h4>
|
|
|
|
| 27 |
<div style='
|
| 28 |
display: grid;
|
| 29 |
grid-template-columns: 1fr 1fr;
|
| 30 |
gap: 15px;
|
| 31 |
margin-top: 10px;
|
| 32 |
'>
|
|
|
|
| 33 |
<!-- 左上:冷色(藍) -->
|
| 34 |
<div style='
|
| 35 |
background: white;
|
|
|
|
| 38 |
border: 1px solid #e2e8f0;
|
| 39 |
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
| 40 |
'>
|
| 41 |
+
<strong style='color: #4299e1;'>🏡 Priority: Quiet Environment</strong><br>
|
| 42 |
<span style='color: #4a5568; font-size: 0.9em;'>
|
| 43 |
+
"Most importantly I need a quiet dog. I live in a small apartment with thin walls, and my neighbors are very noise sensitive."
|
| 44 |
</span>
|
| 45 |
</div>
|
|
|
|
| 46 |
<!-- 右上:暖色(橘) -->
|
| 47 |
<div style='
|
| 48 |
background: white;
|
|
|
|
| 51 |
border: 1px solid #e2e8f0;
|
| 52 |
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
| 53 |
'>
|
| 54 |
+
<strong style='color: #ed8936;'>🎾 Multiple Priorities:</strong><br>
|
| 55 |
<span style='color: #4a5568; font-size: 0.9em;'>
|
| 56 |
+
"First I need a dog that's good with kids, second prefer low maintenance grooming, and third would like an active breed for weekend hiking."
|
| 57 |
</span>
|
| 58 |
</div>
|
|
|
|
| 59 |
<!-- 左下:冷色(紫) -->
|
| 60 |
<div style='
|
| 61 |
background: white;
|
|
|
|
| 64 |
border: 1px solid #e2e8f0;
|
| 65 |
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
| 66 |
'>
|
| 67 |
+
<strong style='color: #805ad5;'>🏠 Beginner Owner:</strong><br>
|
| 68 |
<span style='color: #4a5568; font-size: 0.9em;'>
|
| 69 |
+
"This is my first dog. I live in a house with a small yard, work full time, and really want a low-maintenance breed that's easy to train."
|
| 70 |
</span>
|
| 71 |
</div>
|
|
|
|
| 72 |
<!-- 右下:暖色(琥珀橘) -->
|
| 73 |
<div style='
|
| 74 |
background: white;
|
|
|
|
| 77 |
border: 1px solid #e2e8f0;
|
| 78 |
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
| 79 |
'>
|
| 80 |
+
<strong style='color: #276749;'>🤫 Active Lifestyle Priority:</strong><br>
|
| 81 |
<span style='color: #4a5568; font-size: 0.9em;'>
|
| 82 |
+
"I absolutely need an energetic dog for daily running and hiking. Size doesn't matter, but the dog must be able to keep up with intense exercise."
|
| 83 |
</span>
|
| 84 |
</div>
|
| 85 |
</div>
|
|
|
|
| 86 |
<div style='
|
| 87 |
margin-top: 15px;
|
| 88 |
padding: 12px;
|
|
|
|
| 121 |
background: linear-gradient(to right, rgba(66, 153, 225, 0.1), rgba(72, 187, 120, 0.1));
|
| 122 |
border-radius: 10px;
|
| 123 |
'>
|
|
|
|
| 124 |
<p style='
|
| 125 |
font-size: 1.2em;
|
| 126 |
margin: 0;
|
constraint_manager.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
import sqlite3
|
| 2 |
import json
|
| 3 |
import numpy as np
|
|
@@ -138,6 +139,22 @@ class ConstraintManager:
|
|
| 138 |
relaxation_allowed=False,
|
| 139 |
safety_critical=True
|
| 140 |
),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
|
| 142 |
# Priority 2: High priority constraints
|
| 143 |
ConstraintRule(
|
|
@@ -309,6 +326,15 @@ class ConstraintManager:
|
|
| 309 |
if rule.name == "severe_allergy_constraint":
|
| 310 |
return 'hypoallergenic' in dimensions.special_requirements
|
| 311 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 312 |
# Low activity constraint
|
| 313 |
if rule.name == "low_activity_constraint":
|
| 314 |
return 'low' in dimensions.activity_level
|
|
@@ -452,19 +478,46 @@ class ConstraintManager:
|
|
| 452 |
|
| 453 |
def filter_child_safety(self, candidates: Set[str],
|
| 454 |
dimensions: QueryDimensions) -> Dict[str, str]:
|
| 455 |
-
"""Child safety filtering"""
|
| 456 |
filtered = {}
|
| 457 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 458 |
for breed in list(candidates):
|
| 459 |
breed_info = self.breed_cache.get(breed, {})
|
| 460 |
good_with_children = breed_info.get('good_with_children', 'Yes')
|
| 461 |
-
size = breed_info.get('size', '')
|
| 462 |
-
temperament = breed_info.get('temperament', '')
|
| 463 |
|
| 464 |
-
# Breeds explicitly not suitable for children
|
| 465 |
if good_with_children == 'No':
|
| 466 |
filtered[breed] = "Not suitable for children"
|
| 467 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 468 |
elif ('large' in size and good_with_children != 'Yes' and
|
| 469 |
any(trait in temperament for trait in ['aggressive', 'dominant', 'protective'])):
|
| 470 |
filtered[breed] = "Large breed with uncertain child compatibility"
|
|
@@ -566,6 +619,159 @@ class ConstraintManager:
|
|
| 566 |
|
| 567 |
return filtered
|
| 568 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 569 |
def filter_experience_level(self, candidates: Set[str],
|
| 570 |
dimensions: QueryDimensions) -> Dict[str, str]:
|
| 571 |
"""Experience level filtering"""
|
|
|
|
| 1 |
+
# %%writefile constraint_manager.py
|
| 2 |
import sqlite3
|
| 3 |
import json
|
| 4 |
import numpy as np
|
|
|
|
| 139 |
relaxation_allowed=False,
|
| 140 |
safety_critical=True
|
| 141 |
),
|
| 142 |
+
ConstraintRule(
|
| 143 |
+
name="beginner_critical_exclusion",
|
| 144 |
+
priority=ConstraintPriority.CRITICAL,
|
| 145 |
+
description="Exclude breeds absolutely unsuitable for beginners",
|
| 146 |
+
filter_function="filter_beginner_critical",
|
| 147 |
+
relaxation_allowed=False,
|
| 148 |
+
safety_critical=True
|
| 149 |
+
),
|
| 150 |
+
ConstraintRule(
|
| 151 |
+
name="senior_friendly_constraint",
|
| 152 |
+
priority=ConstraintPriority.CRITICAL,
|
| 153 |
+
description="Exclude breeds unsuitable for senior owners",
|
| 154 |
+
filter_function="filter_senior_friendly",
|
| 155 |
+
relaxation_allowed=False,
|
| 156 |
+
safety_critical=True
|
| 157 |
+
),
|
| 158 |
|
| 159 |
# Priority 2: High priority constraints
|
| 160 |
ConstraintRule(
|
|
|
|
| 326 |
if rule.name == "severe_allergy_constraint":
|
| 327 |
return 'hypoallergenic' in dimensions.special_requirements
|
| 328 |
|
| 329 |
+
# Beginner critical exclusion - applies when user is a beginner
|
| 330 |
+
if rule.name == "beginner_critical_exclusion":
|
| 331 |
+
return ('beginner' in dimensions.experience_level or
|
| 332 |
+
'first_time' in dimensions.special_requirements)
|
| 333 |
+
|
| 334 |
+
# Senior friendly constraint - applies when user is elderly
|
| 335 |
+
if rule.name == "senior_friendly_constraint":
|
| 336 |
+
return 'senior' in dimensions.special_requirements
|
| 337 |
+
|
| 338 |
# Low activity constraint
|
| 339 |
if rule.name == "low_activity_constraint":
|
| 340 |
return 'low' in dimensions.activity_level
|
|
|
|
| 478 |
|
| 479 |
def filter_child_safety(self, candidates: Set[str],
|
| 480 |
dimensions: QueryDimensions) -> Dict[str, str]:
|
| 481 |
+
"""Child safety filtering - enhanced for young children"""
|
| 482 |
filtered = {}
|
| 483 |
|
| 484 |
+
# 檢查是否有兒童相關需求
|
| 485 |
+
has_children = 'children' in dimensions.family_context
|
| 486 |
+
|
| 487 |
+
# 如果沒有偵測到,也不執行過濾
|
| 488 |
+
if not has_children:
|
| 489 |
+
return filtered
|
| 490 |
+
|
| 491 |
+
# 假設有提到 children/kids 就可能有幼童風險,對巨型犬保守處理
|
| 492 |
+
# 這是安全優先的設計
|
| 493 |
+
has_young_children = True # 保守假設
|
| 494 |
+
|
| 495 |
for breed in list(candidates):
|
| 496 |
breed_info = self.breed_cache.get(breed, {})
|
| 497 |
good_with_children = breed_info.get('good_with_children', 'Yes')
|
| 498 |
+
size = breed_info.get('size', '').lower()
|
| 499 |
+
temperament = breed_info.get('temperament', '').lower()
|
| 500 |
|
| 501 |
+
# 1. Breeds explicitly not suitable for children
|
| 502 |
if good_with_children == 'No':
|
| 503 |
filtered[breed] = "Not suitable for children"
|
| 504 |
+
continue
|
| 505 |
+
|
| 506 |
+
# 2. 對幼童家庭,排除巨型犬(體型風險)
|
| 507 |
+
if has_young_children:
|
| 508 |
+
if 'giant' in size:
|
| 509 |
+
filtered[breed] = "Giant breed poses physical risk to young children"
|
| 510 |
+
continue
|
| 511 |
+
# 大型犬需要額外檢查性格
|
| 512 |
+
if 'large' in size:
|
| 513 |
+
# 如果沒有明確標示適合兒童,且沒有溫和性格特徵
|
| 514 |
+
gentle_traits = ['gentle', 'patient', 'calm', 'friendly']
|
| 515 |
+
has_gentle_trait = any(t in temperament for t in gentle_traits)
|
| 516 |
+
if good_with_children != 'Yes' and not has_gentle_trait:
|
| 517 |
+
filtered[breed] = "Large breed without confirmed child-friendly temperament"
|
| 518 |
+
continue
|
| 519 |
+
|
| 520 |
+
# 3. Large breeds without clear child compatibility indicators should be cautious
|
| 521 |
elif ('large' in size and good_with_children != 'Yes' and
|
| 522 |
any(trait in temperament for trait in ['aggressive', 'dominant', 'protective'])):
|
| 523 |
filtered[breed] = "Large breed with uncertain child compatibility"
|
|
|
|
| 619 |
|
| 620 |
return filtered
|
| 621 |
|
| 622 |
+
def filter_beginner_critical(self, candidates: Set[str],
|
| 623 |
+
dimensions: QueryDimensions) -> Dict[str, str]:
|
| 624 |
+
"""
|
| 625 |
+
Critical filtering for beginner owners - absolute exclusion rules
|
| 626 |
+
|
| 627 |
+
This filter removes breeds that are absolutely unsuitable for first-time owners
|
| 628 |
+
based on temperament traits that require experienced handling.
|
| 629 |
+
|
| 630 |
+
通用性設計原則:
|
| 631 |
+
1. 基於品種特性(性格、照護需求),不針對特定品種名稱
|
| 632 |
+
2. 只排除���明確危險或極度不適合的品種
|
| 633 |
+
3. 同時考慮多個負面因素的組合效應
|
| 634 |
+
"""
|
| 635 |
+
filtered = {}
|
| 636 |
+
|
| 637 |
+
# 定義對新手絕對危險或極度不適合的特徵
|
| 638 |
+
# 這些是基於行為學和犬隻專家共識的特徵
|
| 639 |
+
critical_negative_traits = {
|
| 640 |
+
'aggressive': 'Aggressive temperament requires experienced handling',
|
| 641 |
+
'dominant': 'Dominant personality requires firm, experienced leadership',
|
| 642 |
+
}
|
| 643 |
+
|
| 644 |
+
# 需要特殊技能的特徵組合
|
| 645 |
+
challenging_trait_combinations = [
|
| 646 |
+
# (特徵列表, 最少需要匹配數量, 排除原因)
|
| 647 |
+
(['sensitive', 'nervous', 'timid', 'shy'], 2,
|
| 648 |
+
'Multiple anxiety-related traits require experienced behavioral management'),
|
| 649 |
+
(['stubborn', 'independent', 'strong-willed'], 2,
|
| 650 |
+
'Strong-willed combination requires advanced training experience'),
|
| 651 |
+
(['protective', 'territorial', 'alert'], 2,
|
| 652 |
+
'Guard dog traits require experienced socialization and control'),
|
| 653 |
+
]
|
| 654 |
+
|
| 655 |
+
# 絕對排除:高照護 + 敏感性格的組合(如 Italian Greyhound)
|
| 656 |
+
high_care_sensitive_exclusion = True
|
| 657 |
+
|
| 658 |
+
for breed in list(candidates):
|
| 659 |
+
breed_info = self.breed_cache.get(breed, {})
|
| 660 |
+
temperament = breed_info.get('temperament', '').lower()
|
| 661 |
+
care_level = breed_info.get('care_level', '').lower()
|
| 662 |
+
good_with_children = breed_info.get('good_with_children', 'Yes')
|
| 663 |
+
|
| 664 |
+
# 檢查 1: 單一致命特徵
|
| 665 |
+
for trait, reason in critical_negative_traits.items():
|
| 666 |
+
if trait in temperament:
|
| 667 |
+
filtered[breed] = reason
|
| 668 |
+
break
|
| 669 |
+
|
| 670 |
+
if breed in filtered:
|
| 671 |
+
continue
|
| 672 |
+
|
| 673 |
+
# 檢查 2: 危險特徵組合
|
| 674 |
+
for traits, min_count, reason in challenging_trait_combinations:
|
| 675 |
+
matched_count = sum(1 for t in traits if t in temperament)
|
| 676 |
+
if matched_count >= min_count:
|
| 677 |
+
filtered[breed] = reason
|
| 678 |
+
break
|
| 679 |
+
|
| 680 |
+
if breed in filtered:
|
| 681 |
+
continue
|
| 682 |
+
|
| 683 |
+
# 檢查 3: 敏感性格 + 其他負面因素的組合
|
| 684 |
+
# 這是針對如 Italian Greyhound 這類品種的通用規則
|
| 685 |
+
if 'sensitive' in temperament:
|
| 686 |
+
negative_factors = 0
|
| 687 |
+
exclusion_reasons = []
|
| 688 |
+
|
| 689 |
+
# 敏感 + 不適合兒童(暗示難以處理)
|
| 690 |
+
if good_with_children == 'No':
|
| 691 |
+
negative_factors += 1
|
| 692 |
+
exclusion_reasons.append('not child-friendly')
|
| 693 |
+
|
| 694 |
+
# 敏感 + 警覺性高(容易過度反應)
|
| 695 |
+
if 'alert' in temperament:
|
| 696 |
+
negative_factors += 1
|
| 697 |
+
exclusion_reasons.append('high alertness')
|
| 698 |
+
|
| 699 |
+
# 敏感 + 需要中高照護
|
| 700 |
+
if care_level in ['moderate', 'high']:
|
| 701 |
+
negative_factors += 0.5
|
| 702 |
+
|
| 703 |
+
# 敏感 + 緊張/害羞
|
| 704 |
+
if any(t in temperament for t in ['nervous', 'shy', 'timid']):
|
| 705 |
+
negative_factors += 1
|
| 706 |
+
exclusion_reasons.append('anxiety tendencies')
|
| 707 |
+
|
| 708 |
+
# 累積超過閾值則排除
|
| 709 |
+
if negative_factors >= 1.5:
|
| 710 |
+
reason = f"Sensitive breed with {', '.join(exclusion_reasons)} - challenging for beginners"
|
| 711 |
+
filtered[breed] = reason
|
| 712 |
+
continue
|
| 713 |
+
|
| 714 |
+
# 檢查 4: 需要專業訓練的工作犬
|
| 715 |
+
working_dog_indicators = ['working', 'herding', 'guard', 'protection']
|
| 716 |
+
if any(ind in temperament for ind in working_dog_indicators):
|
| 717 |
+
if care_level in ['high', 'expert']:
|
| 718 |
+
filtered[breed] = "Working/guard breed with high care needs - requires experienced owner"
|
| 719 |
+
|
| 720 |
+
return filtered
|
| 721 |
+
|
| 722 |
+
def filter_senior_friendly(self, candidates: Set[str],
|
| 723 |
+
dimensions: QueryDimensions) -> Dict[str, str]:
|
| 724 |
+
"""
|
| 725 |
+
Filter breeds unsuitable for senior owners
|
| 726 |
+
|
| 727 |
+
通用性設計原則:
|
| 728 |
+
1. 基於品種體型、力量、運動需求等客觀特性
|
| 729 |
+
2. 考慮老年人的身體限制(力量、敏捷度、體力)
|
| 730 |
+
3. 優先推薦易於處理、低運動需求的品種
|
| 731 |
+
"""
|
| 732 |
+
filtered = {}
|
| 733 |
+
|
| 734 |
+
for breed in list(candidates):
|
| 735 |
+
breed_info = self.breed_cache.get(breed, {})
|
| 736 |
+
size = breed_info.get('size', '').lower()
|
| 737 |
+
exercise_needs = breed_info.get('exercise_needs', '').lower()
|
| 738 |
+
temperament = breed_info.get('temperament', '').lower()
|
| 739 |
+
care_level = breed_info.get('care_level', '').lower()
|
| 740 |
+
|
| 741 |
+
# 1. 排除巨型犬 - 對老年人太難控制
|
| 742 |
+
if 'giant' in size:
|
| 743 |
+
filtered[breed] = "Giant breed too difficult for senior to handle physically"
|
| 744 |
+
continue
|
| 745 |
+
|
| 746 |
+
# 2. 排除大型犬 - 對老年人通常太難處理
|
| 747 |
+
if 'large' in size:
|
| 748 |
+
filtered[breed] = "Large breed may be difficult for senior to handle"
|
| 749 |
+
continue
|
| 750 |
+
|
| 751 |
+
# 3. 排除需要大量運動的品種
|
| 752 |
+
if 'very high' in exercise_needs or 'high' in exercise_needs:
|
| 753 |
+
filtered[breed] = "High exercise needs exceed typical senior lifestyle"
|
| 754 |
+
continue
|
| 755 |
+
|
| 756 |
+
# 4. 排除敏感/焦慮品種 - 對老年人心理負擔大
|
| 757 |
+
anxiety_traits = ['sensitive', 'nervous', 'anxious', 'timid', 'shy']
|
| 758 |
+
if any(t in temperament for t in anxiety_traits):
|
| 759 |
+
filtered[breed] = "Sensitive/anxious breed requires more emotional attention than ideal for senior"
|
| 760 |
+
continue
|
| 761 |
+
|
| 762 |
+
# 5. 排除需要專業訓練的難以控制品種
|
| 763 |
+
difficult_traits = ['dominant', 'stubborn', 'independent', 'strong-willed']
|
| 764 |
+
if any(t in temperament for t in difficult_traits):
|
| 765 |
+
filtered[breed] = "Strong-willed breed may be challenging for senior to manage"
|
| 766 |
+
continue
|
| 767 |
+
|
| 768 |
+
# 6. 排除高照護需求品種
|
| 769 |
+
if care_level in ['high', 'expert']:
|
| 770 |
+
filtered[breed] = "High care needs challenging for senior lifestyle"
|
| 771 |
+
continue
|
| 772 |
+
|
| 773 |
+
return filtered
|
| 774 |
+
|
| 775 |
def filter_experience_level(self, candidates: Set[str],
|
| 776 |
dimensions: QueryDimensions) -> Dict[str, str]:
|
| 777 |
"""Experience level filtering"""
|
dimension_score_calculator.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
import traceback
|
| 2 |
from typing import Dict, Any
|
| 3 |
from breed_health_info import breed_health_info
|
|
@@ -318,10 +319,11 @@ class DimensionScoreCalculator:
|
|
| 318 |
|
| 319 |
return max(0.1, min(1.0, final_score))
|
| 320 |
|
| 321 |
-
def calculate_grooming_score(self, breed_needs: str, user_commitment: str, breed_size: str) -> float:
|
| 322 |
"""
|
| 323 |
計算美容需求分數,強化美容維護需求與使用者承諾度的匹配評估。
|
| 324 |
這個函數特別注意品種大小對美容工作的影響,以及不同程度的美容需求對時間投入的要求。
|
|
|
|
| 325 |
"""
|
| 326 |
# 重新設計基礎分數矩陣,讓美容需求的差異更加明顯
|
| 327 |
base_scores = {
|
|
@@ -436,7 +438,31 @@ class DimensionScoreCalculator:
|
|
| 436 |
seasonal_adjustment = get_seasonal_adjustment("", user_commitment)
|
| 437 |
professional_adjustment = get_professional_grooming_adjustment("", user_commitment)
|
| 438 |
|
| 439 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 440 |
|
| 441 |
# 確保分數在有意義的範圍內,但允許更大的差異
|
| 442 |
return max(0.1, min(1.0, final_score))
|
|
@@ -486,16 +512,24 @@ class DimensionScoreCalculator:
|
|
| 486 |
'protective': -0.10,
|
| 487 |
'aloof': -0.08,
|
| 488 |
'energetic': -0.08,
|
| 489 |
-
'aggressive': -0.20
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 490 |
}
|
| 491 |
|
| 492 |
easy_traits = {
|
| 493 |
'gentle': 0.08, # 提高獎勵以平衡
|
| 494 |
-
'friendly': 0.
|
| 495 |
-
'eager to please': 0.
|
| 496 |
-
'patient': 0.
|
| 497 |
-
'adaptable': 0.
|
| 498 |
-
'calm': 0.
|
|
|
|
|
|
|
|
|
|
| 499 |
}
|
| 500 |
|
| 501 |
# 計算特徵調整
|
|
|
|
| 1 |
+
# %%writefile dimension_score_calculator.py
|
| 2 |
import traceback
|
| 3 |
from typing import Dict, Any
|
| 4 |
from breed_health_info import breed_health_info
|
|
|
|
| 319 |
|
| 320 |
return max(0.1, min(1.0, final_score))
|
| 321 |
|
| 322 |
+
def calculate_grooming_score(self, breed_needs: str, user_commitment: str, breed_size: str, breed_name: str = "", temperament: str = "") -> float:
|
| 323 |
"""
|
| 324 |
計算美容需求分數,強化美容維護需求與使用者承諾度的匹配評估。
|
| 325 |
這個函數特別注意品種大小對美容工作的影響,以及不同程度的美容需求對時間投入的要求。
|
| 326 |
+
新增:考慮品種特殊護理需求(如敏感皮膚、需保暖等)
|
| 327 |
"""
|
| 328 |
# 重新設計基礎分數矩陣,讓美容需求的差異更加明顯
|
| 329 |
base_scores = {
|
|
|
|
| 438 |
seasonal_adjustment = get_seasonal_adjustment("", user_commitment)
|
| 439 |
professional_adjustment = get_professional_grooming_adjustment("", user_commitment)
|
| 440 |
|
| 441 |
+
# 新增:特殊護理需求評估(針對敏感品種、需保暖品種等)
|
| 442 |
+
special_care_adjustment = 0.0
|
| 443 |
+
temperament_lower = temperament.lower()
|
| 444 |
+
breed_name_lower = breed_name.lower()
|
| 445 |
+
|
| 446 |
+
# 對於低承諾使用者,敏感品種需要額外懲罰
|
| 447 |
+
if user_commitment == "low":
|
| 448 |
+
# 敏感性格需要更多細心照顧
|
| 449 |
+
if 'sensitive' in temperament_lower:
|
| 450 |
+
special_care_adjustment -= 0.15
|
| 451 |
+
|
| 452 |
+
# 某些品種需要特殊護理(保暖、皮膚護理等)
|
| 453 |
+
# Italian Greyhound, Whippet等細瘦品種需要保暖衣物
|
| 454 |
+
if any(keyword in breed_name_lower for keyword in ['italian', 'greyhound', 'whippet', 'hairless']):
|
| 455 |
+
special_care_adjustment -= 0.12
|
| 456 |
+
|
| 457 |
+
# 皺褶皮膚品種需要特殊清潔
|
| 458 |
+
if any(keyword in breed_name_lower for keyword in ['bulldog', 'pug', 'shar pei']):
|
| 459 |
+
special_care_adjustment -= 0.10
|
| 460 |
+
|
| 461 |
+
# 白色毛髮品種容易髒,需要更頻繁清潔
|
| 462 |
+
if 'white' in breed_name_lower or 'maltese' in breed_name_lower:
|
| 463 |
+
special_care_adjustment -= 0.08
|
| 464 |
+
|
| 465 |
+
final_score = current_score + coat_adjustment + seasonal_adjustment + professional_adjustment + special_care_adjustment
|
| 466 |
|
| 467 |
# 確保分數在有意義的範圍內,但允許更大的差異
|
| 468 |
return max(0.1, min(1.0, final_score))
|
|
|
|
| 512 |
'protective': -0.10,
|
| 513 |
'aloof': -0.08,
|
| 514 |
'energetic': -0.08,
|
| 515 |
+
'aggressive': -0.20, # 保持較高懲罰,因為安全考慮
|
| 516 |
+
'sensitive': -0.18, # 敏感品種對新手很具挑戰性(需小心對待、易受驚)
|
| 517 |
+
'alert': -0.05, # 過度警覺可能導致過度吠叫
|
| 518 |
+
'nervous': -0.15, # 緊張性格對新手困難
|
| 519 |
+
'shy': -0.12, # 害羞需要耐心社會化
|
| 520 |
+
'timid': -0.12 # 膽小需要特殊處理
|
| 521 |
}
|
| 522 |
|
| 523 |
easy_traits = {
|
| 524 |
'gentle': 0.08, # 提高獎勵以平衡
|
| 525 |
+
'friendly': 0.10, # 友善對新手很重要
|
| 526 |
+
'eager to please': 0.12, # 渴望取悅主人,容易訓練
|
| 527 |
+
'patient': 0.10,
|
| 528 |
+
'adaptable': 0.10,
|
| 529 |
+
'calm': 0.10,
|
| 530 |
+
'outgoing': 0.08, # 外向性格easier for beginners
|
| 531 |
+
'confident': 0.08, # 自信的狗對新手較友善
|
| 532 |
+
'tolerant': 0.10 # 容忍度高對新手很重要
|
| 533 |
}
|
| 534 |
|
| 535 |
# 計算特徵調整
|
dynamic_weight_calculator.py
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# %%writefile dynamic_weight_calculator.py
|
| 2 |
+
import numpy as np
|
| 3 |
+
from typing import Dict, List, Tuple, Set, Optional, Any
|
| 4 |
+
from dataclasses import dataclass, field
|
| 5 |
+
import traceback
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
@dataclass
|
| 9 |
+
class WeightAllocationResult:
|
| 10 |
+
"""權重分配結果"""
|
| 11 |
+
dynamic_weights: Dict[str, float] = field(default_factory=dict)
|
| 12 |
+
allocation_method: str = 'balanced'
|
| 13 |
+
high_priority_count: int = 0
|
| 14 |
+
mentioned_dimensions: Set[str] = field(default_factory=set)
|
| 15 |
+
weight_sum: float = 1.0
|
| 16 |
+
allocation_notes: List[str] = field(default_factory=list)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class DynamicWeightCalculator:
|
| 20 |
+
"""
|
| 21 |
+
動態權重計算器
|
| 22 |
+
根據使用者優先級動態調整維度權重
|
| 23 |
+
|
| 24 |
+
策略:
|
| 25 |
+
- 1個高優先級 → 固定預留 40%
|
| 26 |
+
- 2個高優先級 → 固定預留 40% + 25%
|
| 27 |
+
- 3個高優先級 → 固定預留 30% + 27% + 23%
|
| 28 |
+
- 4+個高優先級 → 倍數正規化法
|
| 29 |
+
"""
|
| 30 |
+
|
| 31 |
+
def __init__(self):
|
| 32 |
+
"""初始化動態權重計算器"""
|
| 33 |
+
self.default_weights = self._initialize_default_weights()
|
| 34 |
+
self.dimension_name_mapping = self._initialize_dimension_mapping()
|
| 35 |
+
self.high_priority_threshold = 1.4 # Balanced threshold (not too high, not too low)
|
| 36 |
+
self.min_weight_floor = 0.05
|
| 37 |
+
self.contextual_weight_distribution = {
|
| 38 |
+
'critical_dimensions_weight': 0.50, # Moderate emphasis on critical dimensions
|
| 39 |
+
'mentioned_dimensions_weight': 0.35, # Good weight for mentioned dimensions
|
| 40 |
+
'other_dimensions_weight': 0.15 # Reasonable baseline for other dimensions
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
def _initialize_default_weights(self) -> Dict[str, float]:
|
| 44 |
+
"""初始化預設權重(平衡配置)"""
|
| 45 |
+
return {
|
| 46 |
+
'activity_compatibility': 0.18,
|
| 47 |
+
'noise_compatibility': 0.16,
|
| 48 |
+
'spatial_compatibility': 0.13,
|
| 49 |
+
'family_compatibility': 0.13,
|
| 50 |
+
'maintenance_compatibility': 0.13,
|
| 51 |
+
'experience_compatibility': 0.15, # 新增獨立的experience維度
|
| 52 |
+
'health_compatibility': 0.12
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
def _initialize_dimension_mapping(self) -> Dict[str, str]:
|
| 56 |
+
"""初始化維度名稱映射"""
|
| 57 |
+
return {
|
| 58 |
+
'noise': 'noise_compatibility',
|
| 59 |
+
'size': 'spatial_compatibility', # size更適合映射到spatial
|
| 60 |
+
'exercise': 'activity_compatibility',
|
| 61 |
+
'activity': 'activity_compatibility',
|
| 62 |
+
'grooming': 'maintenance_compatibility',
|
| 63 |
+
'maintenance': 'maintenance_compatibility',
|
| 64 |
+
'family': 'family_compatibility',
|
| 65 |
+
'experience': 'experience_compatibility', # 獨立映射
|
| 66 |
+
'health': 'health_compatibility', # 獨立映射
|
| 67 |
+
'spatial': 'spatial_compatibility',
|
| 68 |
+
'space': 'spatial_compatibility'
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
def calculate_dynamic_weights(self,
|
| 72 |
+
dimension_priorities: Dict[str, float],
|
| 73 |
+
user_mentions: Optional[Set[str]] = None,
|
| 74 |
+
use_contextual: bool = True) -> WeightAllocationResult:
|
| 75 |
+
"""
|
| 76 |
+
計算動態權重
|
| 77 |
+
|
| 78 |
+
Args:
|
| 79 |
+
dimension_priorities: 維度優先級 {dimension: priority_score}
|
| 80 |
+
user_mentions: 使用者明確提到的維度
|
| 81 |
+
use_contextual: 是否使用情境相對評分(關鍵維度80%)
|
| 82 |
+
|
| 83 |
+
Returns:
|
| 84 |
+
WeightAllocationResult: 權重分配結果
|
| 85 |
+
"""
|
| 86 |
+
try:
|
| 87 |
+
if user_mentions is None:
|
| 88 |
+
user_mentions = set()
|
| 89 |
+
|
| 90 |
+
# Step 1: 標準化維度名稱
|
| 91 |
+
normalized_priorities = self._normalize_dimension_names(dimension_priorities)
|
| 92 |
+
|
| 93 |
+
# Step 2: 分類維度
|
| 94 |
+
high_priority_dims = {
|
| 95 |
+
dim: score for dim, score in normalized_priorities.items()
|
| 96 |
+
if score >= self.high_priority_threshold
|
| 97 |
+
}
|
| 98 |
+
high_count = len(high_priority_dims)
|
| 99 |
+
|
| 100 |
+
# Step 3: 根據高優先級數量選擇策略
|
| 101 |
+
if high_count == 0:
|
| 102 |
+
# 無優先級 → 使用預設權重
|
| 103 |
+
result = self._allocate_default_weights(user_mentions)
|
| 104 |
+
result.allocation_method = 'default_balanced'
|
| 105 |
+
|
| 106 |
+
elif use_contextual:
|
| 107 |
+
# 使用情境相對評分(關鍵維度80%)
|
| 108 |
+
result = self._allocate_contextual_weights(
|
| 109 |
+
normalized_priorities, user_mentions
|
| 110 |
+
)
|
| 111 |
+
result.allocation_method = 'contextual_relative'
|
| 112 |
+
|
| 113 |
+
elif high_count == 1:
|
| 114 |
+
# 單一高優先級 → 固定預留40%
|
| 115 |
+
result = self._allocate_single_priority(
|
| 116 |
+
normalized_priorities, user_mentions
|
| 117 |
+
)
|
| 118 |
+
result.allocation_method = 'single_fixed'
|
| 119 |
+
|
| 120 |
+
elif high_count <= 3:
|
| 121 |
+
# 2-3個高優先級 → 階梯固定預留法
|
| 122 |
+
result = self._allocate_multiple_priorities_fixed(
|
| 123 |
+
normalized_priorities, user_mentions, high_count
|
| 124 |
+
)
|
| 125 |
+
result.allocation_method = f'multiple_fixed_{high_count}'
|
| 126 |
+
|
| 127 |
+
else:
|
| 128 |
+
# 4+個高優先級 → 倍數正規化法
|
| 129 |
+
result = self._allocate_multiple_priorities_proportional(
|
| 130 |
+
normalized_priorities, user_mentions
|
| 131 |
+
)
|
| 132 |
+
result.allocation_method = 'proportional'
|
| 133 |
+
|
| 134 |
+
# Step 4: 應用最低權重保護
|
| 135 |
+
result.dynamic_weights = self._apply_weight_floor(result.dynamic_weights)
|
| 136 |
+
|
| 137 |
+
# Step 5: 正規化確保總和為1.0
|
| 138 |
+
result.dynamic_weights = self._normalize_weights(result.dynamic_weights)
|
| 139 |
+
result.weight_sum = sum(result.dynamic_weights.values())
|
| 140 |
+
|
| 141 |
+
result.high_priority_count = high_count
|
| 142 |
+
result.mentioned_dimensions = user_mentions
|
| 143 |
+
|
| 144 |
+
return result
|
| 145 |
+
|
| 146 |
+
except Exception as e:
|
| 147 |
+
print(f"Error calculating dynamic weights: {str(e)}")
|
| 148 |
+
print(traceback.format_exc())
|
| 149 |
+
return WeightAllocationResult(
|
| 150 |
+
dynamic_weights=self.default_weights.copy(),
|
| 151 |
+
allocation_method='fallback'
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
def _normalize_dimension_names(self,
|
| 155 |
+
priorities: Dict[str, float]) -> Dict[str, float]:
|
| 156 |
+
"""標準化維度名稱"""
|
| 157 |
+
normalized = {}
|
| 158 |
+
for dim, score in priorities.items():
|
| 159 |
+
mapped_dim = self.dimension_name_mapping.get(dim, dim)
|
| 160 |
+
# 如果mapped_dim不在default_weights中,保留原維度名
|
| 161 |
+
if mapped_dim not in self.default_weights:
|
| 162 |
+
mapped_dim = dim
|
| 163 |
+
normalized[mapped_dim] = max(normalized.get(mapped_dim, 1.0), score)
|
| 164 |
+
return normalized
|
| 165 |
+
|
| 166 |
+
def _allocate_default_weights(self,
|
| 167 |
+
user_mentions: Set[str]) -> WeightAllocationResult:
|
| 168 |
+
"""分配預設平衡權重"""
|
| 169 |
+
weights = self.default_weights.copy()
|
| 170 |
+
notes = ["Using default balanced weights (no priorities detected)"]
|
| 171 |
+
|
| 172 |
+
return WeightAllocationResult(
|
| 173 |
+
dynamic_weights=weights,
|
| 174 |
+
allocation_notes=notes
|
| 175 |
+
)
|
| 176 |
+
|
| 177 |
+
def _allocate_contextual_weights(self,
|
| 178 |
+
priorities: Dict[str, float],
|
| 179 |
+
user_mentions: Set[str]) -> WeightAllocationResult:
|
| 180 |
+
"""
|
| 181 |
+
情境相對權重分配(關鍵維度50%)
|
| 182 |
+
"""
|
| 183 |
+
weights = {}
|
| 184 |
+
notes = []
|
| 185 |
+
|
| 186 |
+
# 標準化user_mentions維度名稱
|
| 187 |
+
normalized_mentions = set()
|
| 188 |
+
for mention in user_mentions:
|
| 189 |
+
normalized_name = self.dimension_name_mapping.get(mention, mention)
|
| 190 |
+
if normalized_name in self.default_weights:
|
| 191 |
+
normalized_mentions.add(normalized_name)
|
| 192 |
+
|
| 193 |
+
# 分類維度
|
| 194 |
+
critical_dims = [d for d, s in priorities.items() if s >= self.high_priority_threshold]
|
| 195 |
+
mentioned_dims = [d for d in normalized_mentions if d not in critical_dims]
|
| 196 |
+
other_dims = [d for d in self.default_weights.keys()
|
| 197 |
+
if d not in critical_dims and d not in mentioned_dims]
|
| 198 |
+
|
| 199 |
+
# 權重分配
|
| 200 |
+
total_critical = self.contextual_weight_distribution['critical_dimensions_weight']
|
| 201 |
+
total_mentioned = self.contextual_weight_distribution['mentioned_dimensions_weight']
|
| 202 |
+
total_other = self.contextual_weight_distribution['other_dimensions_weight']
|
| 203 |
+
|
| 204 |
+
# 關鍵維度:按優先級比例分配50%
|
| 205 |
+
if critical_dims:
|
| 206 |
+
critical_priority_sum = sum(priorities.get(d, 1.0) for d in critical_dims)
|
| 207 |
+
for dim in critical_dims:
|
| 208 |
+
weight = (priorities.get(dim, 1.0) / critical_priority_sum) * total_critical
|
| 209 |
+
weights[dim] = weight
|
| 210 |
+
notes.append(f"Critical dimensions ({len(critical_dims)}): {total_critical:.0%} weight")
|
| 211 |
+
|
| 212 |
+
# 提及維度:平均分配35%
|
| 213 |
+
if mentioned_dims:
|
| 214 |
+
for dim in mentioned_dims:
|
| 215 |
+
weights[dim] = total_mentioned / len(mentioned_dims)
|
| 216 |
+
notes.append(f"Mentioned dimensions ({len(mentioned_dims)}): {total_mentioned:.0%} weight")
|
| 217 |
+
|
| 218 |
+
# 其他維度:平均分配15%
|
| 219 |
+
if other_dims:
|
| 220 |
+
for dim in other_dims:
|
| 221 |
+
weights[dim] = total_other / len(other_dims)
|
| 222 |
+
notes.append(f"Other dimensions ({len(other_dims)}): {total_other:.0%} weight")
|
| 223 |
+
|
| 224 |
+
# 填充未覆蓋的維度
|
| 225 |
+
for dim in self.default_weights.keys():
|
| 226 |
+
if dim not in weights:
|
| 227 |
+
weights[dim] = 0.05
|
| 228 |
+
|
| 229 |
+
return WeightAllocationResult(
|
| 230 |
+
dynamic_weights=weights,
|
| 231 |
+
allocation_notes=notes
|
| 232 |
+
)
|
| 233 |
+
|
| 234 |
+
def _allocate_single_priority(self,
|
| 235 |
+
priorities: Dict[str, float],
|
| 236 |
+
user_mentions: Set[str]) -> WeightAllocationResult:
|
| 237 |
+
"""單一高優先級:固定預留40%"""
|
| 238 |
+
weights = {}
|
| 239 |
+
notes = []
|
| 240 |
+
|
| 241 |
+
# 找到高優先級維度
|
| 242 |
+
high_priority_dim = None
|
| 243 |
+
max_priority = 0
|
| 244 |
+
for dim, score in priorities.items():
|
| 245 |
+
if score >= self.high_priority_threshold and score > max_priority:
|
| 246 |
+
high_priority_dim = dim
|
| 247 |
+
max_priority = score
|
| 248 |
+
|
| 249 |
+
if high_priority_dim:
|
| 250 |
+
# 高優先級維度:40%
|
| 251 |
+
weights[high_priority_dim] = 0.40
|
| 252 |
+
notes.append(f"{high_priority_dim}: 40% (high priority)")
|
| 253 |
+
|
| 254 |
+
# 其他維度:平均分配剩餘60%
|
| 255 |
+
other_dims = [d for d in self.default_weights.keys() if d != high_priority_dim]
|
| 256 |
+
remaining_weight = 0.60
|
| 257 |
+
for dim in other_dims:
|
| 258 |
+
weights[dim] = remaining_weight / len(other_dims)
|
| 259 |
+
else:
|
| 260 |
+
weights = self.default_weights.copy()
|
| 261 |
+
|
| 262 |
+
return WeightAllocationResult(
|
| 263 |
+
dynamic_weights=weights,
|
| 264 |
+
allocation_notes=notes
|
| 265 |
+
)
|
| 266 |
+
|
| 267 |
+
def _allocate_multiple_priorities_fixed(self,
|
| 268 |
+
priorities: Dict[str, float],
|
| 269 |
+
user_mentions: Set[str],
|
| 270 |
+
high_count: int) -> WeightAllocationResult:
|
| 271 |
+
"""2-3個高優先級:階梯固定預留法"""
|
| 272 |
+
weights = {}
|
| 273 |
+
notes = []
|
| 274 |
+
|
| 275 |
+
# 排序高優先級維度
|
| 276 |
+
high_priority_dims = sorted(
|
| 277 |
+
[(dim, score) for dim, score in priorities.items()
|
| 278 |
+
if score >= self.high_priority_threshold],
|
| 279 |
+
key=lambda x: x[1],
|
| 280 |
+
reverse=True
|
| 281 |
+
)
|
| 282 |
+
|
| 283 |
+
# 根據數量分配固定權重
|
| 284 |
+
if high_count == 2:
|
| 285 |
+
# 2個高優先級:40% + 25%
|
| 286 |
+
fixed_weights = [0.40, 0.25]
|
| 287 |
+
remaining = 0.35
|
| 288 |
+
notes.append("2 high priorities: 40% + 25%, others share 35%")
|
| 289 |
+
elif high_count == 3:
|
| 290 |
+
# 3個高優先級:30% + 27% + 23%
|
| 291 |
+
fixed_weights = [0.30, 0.27, 0.23]
|
| 292 |
+
remaining = 0.20
|
| 293 |
+
notes.append("3 high priorities: 30% + 27% + 23%, others share 20%")
|
| 294 |
+
else:
|
| 295 |
+
# 降級處理
|
| 296 |
+
return self._allocate_multiple_priorities_proportional(
|
| 297 |
+
priorities, user_mentions
|
| 298 |
+
)
|
| 299 |
+
|
| 300 |
+
# 分配固定權重
|
| 301 |
+
for i, (dim, score) in enumerate(high_priority_dims[:high_count]):
|
| 302 |
+
weights[dim] = fixed_weights[i]
|
| 303 |
+
|
| 304 |
+
# 其他維度:平均分配剩餘
|
| 305 |
+
other_dims = [d for d in self.default_weights.keys()
|
| 306 |
+
if d not in [dim for dim, _ in high_priority_dims[:high_count]]]
|
| 307 |
+
if other_dims:
|
| 308 |
+
for dim in other_dims:
|
| 309 |
+
weights[dim] = remaining / len(other_dims)
|
| 310 |
+
|
| 311 |
+
return WeightAllocationResult(
|
| 312 |
+
dynamic_weights=weights,
|
| 313 |
+
allocation_notes=notes
|
| 314 |
+
)
|
| 315 |
+
|
| 316 |
+
def _allocate_multiple_priorities_proportional(self,
|
| 317 |
+
priorities: Dict[str, float],
|
| 318 |
+
user_mentions: Set[str]) -> WeightAllocationResult:
|
| 319 |
+
"""4+個高優先級:倍數正規化法"""
|
| 320 |
+
weights = {}
|
| 321 |
+
notes = []
|
| 322 |
+
|
| 323 |
+
# 計算原始權重(基於優先級倍數)
|
| 324 |
+
raw_weights = {}
|
| 325 |
+
for dim in self.default_weights.keys():
|
| 326 |
+
priority_score = priorities.get(dim, 1.0)
|
| 327 |
+
raw_weights[dim] = 1.0 * priority_score
|
| 328 |
+
|
| 329 |
+
# 正規化
|
| 330 |
+
total_raw = sum(raw_weights.values())
|
| 331 |
+
for dim, raw_weight in raw_weights.items():
|
| 332 |
+
weights[dim] = raw_weight / total_raw
|
| 333 |
+
|
| 334 |
+
notes.append(f"Proportional allocation for {len([d for d, s in priorities.items() if s >= self.high_priority_threshold])} high priorities")
|
| 335 |
+
|
| 336 |
+
return WeightAllocationResult(
|
| 337 |
+
dynamic_weights=weights,
|
| 338 |
+
allocation_notes=notes
|
| 339 |
+
)
|
| 340 |
+
|
| 341 |
+
def _apply_weight_floor(self, weights: Dict[str, float]) -> Dict[str, float]:
|
| 342 |
+
"""應用最低權重保護"""
|
| 343 |
+
protected_weights = {}
|
| 344 |
+
for dim, weight in weights.items():
|
| 345 |
+
protected_weights[dim] = max(self.min_weight_floor, weight)
|
| 346 |
+
return protected_weights
|
| 347 |
+
|
| 348 |
+
def _normalize_weights(self, weights: Dict[str, float]) -> Dict[str, float]:
|
| 349 |
+
"""正規化權重確保總和為1.0"""
|
| 350 |
+
total = sum(weights.values())
|
| 351 |
+
if total == 0:
|
| 352 |
+
return self.default_weights.copy()
|
| 353 |
+
|
| 354 |
+
normalized = {dim: weight / total for dim, weight in weights.items()}
|
| 355 |
+
return normalized
|
| 356 |
+
|
| 357 |
+
def get_weight_summary(self, result: WeightAllocationResult) -> Dict[str, Any]:
|
| 358 |
+
"""
|
| 359 |
+
獲取權重分配摘要
|
| 360 |
+
|
| 361 |
+
Args:
|
| 362 |
+
result: 權重分配結果
|
| 363 |
+
|
| 364 |
+
Returns:
|
| 365 |
+
Dict[str, Any]: 權重摘要
|
| 366 |
+
"""
|
| 367 |
+
return {
|
| 368 |
+
'allocation_method': result.allocation_method,
|
| 369 |
+
'high_priority_count': result.high_priority_count,
|
| 370 |
+
'weight_sum': result.weight_sum,
|
| 371 |
+
'weights': result.dynamic_weights,
|
| 372 |
+
'top_3_dimensions': sorted(
|
| 373 |
+
result.dynamic_weights.items(),
|
| 374 |
+
key=lambda x: x[1],
|
| 375 |
+
reverse=True
|
| 376 |
+
)[:3],
|
| 377 |
+
'allocation_notes': result.allocation_notes
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
|
| 381 |
+
def calculate_weights_from_priorities(dimension_priorities: Dict[str, float],
|
| 382 |
+
user_mentions: Optional[Set[str]] = None,
|
| 383 |
+
use_contextual: bool = True) -> WeightAllocationResult:
|
| 384 |
+
"""
|
| 385 |
+
便利函數: 從優先級計算權重
|
| 386 |
+
|
| 387 |
+
Args:
|
| 388 |
+
dimension_priorities: 維度優先級
|
| 389 |
+
user_mentions: 使用者提及的維度
|
| 390 |
+
use_contextual: 使用情境相對評分
|
| 391 |
+
|
| 392 |
+
Returns:
|
| 393 |
+
WeightAllocationResult: 權重分配結果
|
| 394 |
+
"""
|
| 395 |
+
calculator = DynamicWeightCalculator()
|
| 396 |
+
return calculator.calculate_dynamic_weights(
|
| 397 |
+
dimension_priorities, user_mentions, use_contextual
|
| 398 |
+
)
|
| 399 |
+
|
| 400 |
+
|
| 401 |
+
def get_weight_summary(dimension_priorities: Dict[str, float],
|
| 402 |
+
user_mentions: Optional[Set[str]] = None) -> Dict[str, Any]:
|
| 403 |
+
"""
|
| 404 |
+
便利函數: 獲取權重摘要
|
| 405 |
+
|
| 406 |
+
Args:
|
| 407 |
+
dimension_priorities: 維度優先級
|
| 408 |
+
user_mentions: 使用者提及的維度
|
| 409 |
+
|
| 410 |
+
Returns:
|
| 411 |
+
Dict[str, Any]: 權重摘要
|
| 412 |
+
"""
|
| 413 |
+
calculator = DynamicWeightCalculator()
|
| 414 |
+
result = calculator.calculate_dynamic_weights(dimension_priorities, user_mentions)
|
| 415 |
+
return calculator.get_weight_summary(result)
|
inference_engine.py
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# %%writefile inference_engine.py
|
| 2 |
+
import re
|
| 3 |
+
import numpy as np
|
| 4 |
+
from typing import Dict, List, Tuple, Set, Optional, Any, Callable
|
| 5 |
+
from dataclasses import dataclass, field
|
| 6 |
+
import traceback
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
@dataclass
|
| 10 |
+
class InferenceRule:
|
| 11 |
+
"""推理規則結構"""
|
| 12 |
+
name: str
|
| 13 |
+
condition: Callable[[str, Dict[str, Any]], bool]
|
| 14 |
+
imply: Callable[[str, Dict[str, Any]], Dict[str, float]]
|
| 15 |
+
reasoning: str
|
| 16 |
+
priority: int = 1 # 規則優先級
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
@dataclass
|
| 20 |
+
class InferenceResult:
|
| 21 |
+
"""推理結果結構"""
|
| 22 |
+
implicit_priorities: Dict[str, float] = field(default_factory=dict)
|
| 23 |
+
triggered_rules: List[str] = field(default_factory=list)
|
| 24 |
+
reasoning_chains: List[str] = field(default_factory=list)
|
| 25 |
+
confidence: float = 1.0
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class BreedRecommendationInferenceEngine:
|
| 29 |
+
"""
|
| 30 |
+
品種推薦推理引擎
|
| 31 |
+
從使用者明確輸入中推斷隱含需求,補充優先級設定
|
| 32 |
+
|
| 33 |
+
核心邏輯:
|
| 34 |
+
1. 居住環境推理 (公寓 → 安靜、中小型)
|
| 35 |
+
2. 家庭情況推理 (有小孩 → 溫和、耐心)
|
| 36 |
+
3. 經驗程度推理 (新手 → 易照顧)
|
| 37 |
+
4. 生活方式推理 (忙碌 → 低維護)
|
| 38 |
+
"""
|
| 39 |
+
|
| 40 |
+
def __init__(self):
|
| 41 |
+
"""初始化推理引擎"""
|
| 42 |
+
self.inference_rules = self._build_inference_rules()
|
| 43 |
+
self.spatial_keywords = self._initialize_spatial_keywords()
|
| 44 |
+
self.lifestyle_keywords = self._initialize_lifestyle_keywords()
|
| 45 |
+
|
| 46 |
+
def _initialize_spatial_keywords(self) -> Dict[str, List[str]]:
|
| 47 |
+
"""初始化空間相關關鍵字"""
|
| 48 |
+
return {
|
| 49 |
+
'apartment': [
|
| 50 |
+
'apartment', 'flat', 'condo', 'studio',
|
| 51 |
+
'small space', 'limited space', 'city living', 'urban'
|
| 52 |
+
],
|
| 53 |
+
'small_house': [
|
| 54 |
+
'small house', 'townhouse', 'small home'
|
| 55 |
+
],
|
| 56 |
+
'large_house': [
|
| 57 |
+
'large house', 'big house', 'spacious home',
|
| 58 |
+
'yard', 'garden', 'backyard', 'outdoor space'
|
| 59 |
+
]
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
def _initialize_lifestyle_keywords(self) -> Dict[str, List[str]]:
|
| 63 |
+
"""初始化生活方式關鍵字"""
|
| 64 |
+
return {
|
| 65 |
+
'has_children': [
|
| 66 |
+
'kids', 'children', 'toddler', 'baby', 'school age',
|
| 67 |
+
'child', 'son', 'daughter', 'family with kids'
|
| 68 |
+
],
|
| 69 |
+
'beginner': [
|
| 70 |
+
'first dog', 'first time', 'beginner', 'never had',
|
| 71 |
+
'new to dogs', 'inexperienced', 'no experience'
|
| 72 |
+
],
|
| 73 |
+
'busy': [
|
| 74 |
+
'busy', 'limited time', 'work full time', 'not much time',
|
| 75 |
+
'long hours', 'busy schedule', 'hectic lifestyle'
|
| 76 |
+
],
|
| 77 |
+
'active': [
|
| 78 |
+
'active', 'athletic', 'outdoor', 'hiking', 'running',
|
| 79 |
+
'jogging', 'sports', 'exercise enthusiast'
|
| 80 |
+
]
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
def _build_inference_rules(self) -> List[InferenceRule]:
|
| 84 |
+
"""構建推理規則庫"""
|
| 85 |
+
return [
|
| 86 |
+
# 規則1: 公寓居住推理
|
| 87 |
+
InferenceRule(
|
| 88 |
+
name="apartment_living",
|
| 89 |
+
condition=lambda input_text, ctx: self._check_apartment(input_text, ctx),
|
| 90 |
+
imply=lambda input_text, ctx: {
|
| 91 |
+
'noise': 1.3, # 公寓暗示需要安靜 (reduced from 1.4)
|
| 92 |
+
'size': 1.2, # 公寓暗示偏好中小型 (reduced from 1.3)
|
| 93 |
+
'exercise': 1.15 # 公寓暗示可能運動空間有限 (reduced from 1.2)
|
| 94 |
+
},
|
| 95 |
+
reasoning="Apartment living typically requires quieter, smaller dogs with moderate exercise needs",
|
| 96 |
+
priority=1
|
| 97 |
+
),
|
| 98 |
+
|
| 99 |
+
# 規則2: 有小孩推理
|
| 100 |
+
InferenceRule(
|
| 101 |
+
name="has_children",
|
| 102 |
+
condition=lambda input_text, ctx: self._check_children(input_text, ctx),
|
| 103 |
+
imply=lambda input_text, ctx: {
|
| 104 |
+
'family': 1.4, # 明確需要家庭友善 (reduced from 1.5)
|
| 105 |
+
'experience': 1.15, # 暗示希望容易訓練 (reduced from 1.2)
|
| 106 |
+
'noise': 1.15 # 有小孩通常希望狗較安靜 (reduced from 1.2)
|
| 107 |
+
},
|
| 108 |
+
reasoning="Families with children need gentle, patient, child-safe breeds",
|
| 109 |
+
priority=1
|
| 110 |
+
),
|
| 111 |
+
|
| 112 |
+
# 規則3: 新手飼主推理
|
| 113 |
+
InferenceRule(
|
| 114 |
+
name="beginner_owner",
|
| 115 |
+
condition=lambda input_text, ctx: self._check_beginner(input_text, ctx),
|
| 116 |
+
imply=lambda input_text, ctx: {
|
| 117 |
+
'experience': 1.3, # 需要容易照顧 (reduced from 1.4)
|
| 118 |
+
'grooming': 1.25, # 偏好低維護 (reduced from 1.3)
|
| 119 |
+
'health': 1.15 # 希望健康問題少 (reduced from 1.2)
|
| 120 |
+
},
|
| 121 |
+
reasoning="Beginners benefit from easier-to-care-for, low-maintenance breeds",
|
| 122 |
+
priority=1
|
| 123 |
+
),
|
| 124 |
+
|
| 125 |
+
# 規則4: 忙碌生活方式推理
|
| 126 |
+
InferenceRule(
|
| 127 |
+
name="busy_lifestyle",
|
| 128 |
+
condition=lambda input_text, ctx: self._check_busy(input_text, ctx),
|
| 129 |
+
imply=lambda input_text, ctx: {
|
| 130 |
+
'grooming': 1.3, # 需要低維護 (reduced from 1.4)
|
| 131 |
+
'exercise': 1.25, # 不能需要太多運動 (reduced from 1.3)
|
| 132 |
+
'experience': 1.15 # 希望獨立性強 (reduced from 1.2)
|
| 133 |
+
},
|
| 134 |
+
reasoning="Busy owners need lower-maintenance breeds with moderate exercise needs",
|
| 135 |
+
priority=1
|
| 136 |
+
),
|
| 137 |
+
|
| 138 |
+
# 規則5: 大型住宅推理
|
| 139 |
+
InferenceRule(
|
| 140 |
+
name="large_house",
|
| 141 |
+
condition=lambda input_text, ctx: self._check_large_house(input_text, ctx),
|
| 142 |
+
imply=lambda input_text, ctx: {
|
| 143 |
+
'size': 1.15, # 可以接受大型犬 (reduced from 1.2)
|
| 144 |
+
'exercise': 1.2 # 可能有更多運動空間 (reduced from 1.3)
|
| 145 |
+
},
|
| 146 |
+
reasoning="Large homes can accommodate more active, larger breeds",
|
| 147 |
+
priority=2
|
| 148 |
+
),
|
| 149 |
+
|
| 150 |
+
# 規則6: 有院子推理
|
| 151 |
+
InferenceRule(
|
| 152 |
+
name="has_yard",
|
| 153 |
+
condition=lambda input_text, ctx: self._check_yard(input_text, ctx),
|
| 154 |
+
imply=lambda input_text, ctx: {
|
| 155 |
+
'exercise': 1.2, # 有院子可以支持更活躍的品種 (reduced from 1.3)
|
| 156 |
+
'size': 1.15 # 可以考慮較大的品種 (reduced from 1.2)
|
| 157 |
+
},
|
| 158 |
+
reasoning="Yards provide exercise space for more active breeds",
|
| 159 |
+
priority=2
|
| 160 |
+
),
|
| 161 |
+
|
| 162 |
+
# 規則7: 噪音敏感環境推理
|
| 163 |
+
InferenceRule(
|
| 164 |
+
name="noise_sensitive",
|
| 165 |
+
condition=lambda input_text, ctx: self._check_noise_sensitive(input_text, ctx),
|
| 166 |
+
imply=lambda input_text, ctx: {
|
| 167 |
+
'noise': 1.5 # 強調需要安靜 (reduced from 1.6)
|
| 168 |
+
},
|
| 169 |
+
reasoning="Noise-sensitive environments require quieter breeds",
|
| 170 |
+
priority=1
|
| 171 |
+
),
|
| 172 |
+
|
| 173 |
+
# 規則8: 過敏體質推理
|
| 174 |
+
InferenceRule(
|
| 175 |
+
name="allergy_concerns",
|
| 176 |
+
condition=lambda input_text, ctx: self._check_allergies(input_text, ctx),
|
| 177 |
+
imply=lambda input_text, ctx: {
|
| 178 |
+
'grooming': 1.4, # 需要低掉毛品種 (reduced from 1.5)
|
| 179 |
+
'health': 1.25 # 關注健康問題 (reduced from 1.3)
|
| 180 |
+
},
|
| 181 |
+
reasoning="Allergy concerns require hypoallergenic, low-shedding breeds",
|
| 182 |
+
priority=1
|
| 183 |
+
),
|
| 184 |
+
|
| 185 |
+
# 規則9: 活躍生活方式推理
|
| 186 |
+
InferenceRule(
|
| 187 |
+
name="active_lifestyle",
|
| 188 |
+
condition=lambda input_text, ctx: self._check_active(input_text, ctx),
|
| 189 |
+
imply=lambda input_text, ctx: {
|
| 190 |
+
'exercise': 1.3, # 需要高運動量品種 (reduced from 1.4)
|
| 191 |
+
'size': 1.15 # 可能偏好中大型犬 (reduced from 1.2)
|
| 192 |
+
},
|
| 193 |
+
reasoning="Active lifestyle matches well with energetic, athletic breeds",
|
| 194 |
+
priority=1
|
| 195 |
+
),
|
| 196 |
+
|
| 197 |
+
# 規則10: 小型空間推理
|
| 198 |
+
InferenceRule(
|
| 199 |
+
name="small_space",
|
| 200 |
+
condition=lambda input_text, ctx: self._check_small_space(input_text, ctx),
|
| 201 |
+
imply=lambda input_text, ctx: {
|
| 202 |
+
'size': 1.3, # 強調需要小型犬 (reduced from 1.4)
|
| 203 |
+
'noise': 1.25, # 小空間需要安靜 (reduced from 1.3)
|
| 204 |
+
'exercise': 1.15 # 運動需求不宜過高 (reduced from 1.2)
|
| 205 |
+
},
|
| 206 |
+
reasoning="Small spaces require compact, quiet dogs with moderate energy",
|
| 207 |
+
priority=1
|
| 208 |
+
)
|
| 209 |
+
]
|
| 210 |
+
|
| 211 |
+
def _check_apartment(self, input_text: str, ctx: Dict[str, Any]) -> bool:
|
| 212 |
+
"""檢查是否提到公寓"""
|
| 213 |
+
text_lower = input_text.lower()
|
| 214 |
+
return (any(keyword in text_lower for keyword in self.spatial_keywords['apartment']) or
|
| 215 |
+
ctx.get('living_space') == 'apartment')
|
| 216 |
+
|
| 217 |
+
def _check_children(self, input_text: str, ctx: Dict[str, Any]) -> bool:
|
| 218 |
+
"""檢查是否有小孩"""
|
| 219 |
+
text_lower = input_text.lower()
|
| 220 |
+
return (any(keyword in text_lower for keyword in self.lifestyle_keywords['has_children']) or
|
| 221 |
+
ctx.get('has_children') == True)
|
| 222 |
+
|
| 223 |
+
def _check_beginner(self, input_text: str, ctx: Dict[str, Any]) -> bool:
|
| 224 |
+
"""檢查是否為新手"""
|
| 225 |
+
text_lower = input_text.lower()
|
| 226 |
+
return (any(keyword in text_lower for keyword in self.lifestyle_keywords['beginner']) or
|
| 227 |
+
ctx.get('experience_level') == 'beginner')
|
| 228 |
+
|
| 229 |
+
def _check_busy(self, input_text: str, ctx: Dict[str, Any]) -> bool:
|
| 230 |
+
"""檢查是否為忙碌��活方式"""
|
| 231 |
+
text_lower = input_text.lower()
|
| 232 |
+
return (any(keyword in text_lower for keyword in self.lifestyle_keywords['busy']) or
|
| 233 |
+
ctx.get('time_availability') == 'limited')
|
| 234 |
+
|
| 235 |
+
def _check_large_house(self, input_text: str, ctx: Dict[str, Any]) -> bool:
|
| 236 |
+
"""檢查是否有大房子"""
|
| 237 |
+
text_lower = input_text.lower()
|
| 238 |
+
return (any(keyword in text_lower for keyword in self.spatial_keywords['large_house']) or
|
| 239 |
+
ctx.get('living_space') in ['house_large', 'house'])
|
| 240 |
+
|
| 241 |
+
def _check_yard(self, input_text: str, ctx: Dict[str, Any]) -> bool:
|
| 242 |
+
"""檢查是否有院子"""
|
| 243 |
+
text_lower = input_text.lower()
|
| 244 |
+
return (any(keyword in text_lower for keyword in ['yard', 'garden', 'backyard', 'outdoor space']) or
|
| 245 |
+
ctx.get('yard_access') in ['shared_yard', 'private_yard'])
|
| 246 |
+
|
| 247 |
+
def _check_noise_sensitive(self, input_text: str, ctx: Dict[str, Any]) -> bool:
|
| 248 |
+
"""檢查是否為噪音敏感環境"""
|
| 249 |
+
text_lower = input_text.lower()
|
| 250 |
+
noise_keywords = ['noise sensitive', 'thin walls', 'neighbors close', 'townhouse', 'condo']
|
| 251 |
+
return any(keyword in text_lower for keyword in noise_keywords)
|
| 252 |
+
|
| 253 |
+
def _check_allergies(self, input_text: str, ctx: Dict[str, Any]) -> bool:
|
| 254 |
+
"""檢查是否有過敏體質"""
|
| 255 |
+
text_lower = input_text.lower()
|
| 256 |
+
allergy_keywords = ['allergies', 'hypoallergenic', 'sensitive to fur', 'asthma', 'allergy']
|
| 257 |
+
return (any(keyword in text_lower for keyword in allergy_keywords) or
|
| 258 |
+
ctx.get('has_allergies') == True)
|
| 259 |
+
|
| 260 |
+
def _check_active(self, input_text: str, ctx: Dict[str, Any]) -> bool:
|
| 261 |
+
"""檢查是否為活躍生活方式"""
|
| 262 |
+
text_lower = input_text.lower()
|
| 263 |
+
return (any(keyword in text_lower for keyword in self.lifestyle_keywords['active']) or
|
| 264 |
+
ctx.get('activity_level') == 'high')
|
| 265 |
+
|
| 266 |
+
def _check_small_space(self, input_text: str, ctx: Dict[str, Any]) -> bool:
|
| 267 |
+
"""檢查是否為小型空間"""
|
| 268 |
+
text_lower = input_text.lower()
|
| 269 |
+
small_space_keywords = ['small space', 'limited space', 'tiny', 'compact', 'studio']
|
| 270 |
+
return any(keyword in text_lower for keyword in small_space_keywords)
|
| 271 |
+
|
| 272 |
+
def infer_implicit_priorities(self,
|
| 273 |
+
explicit_input: str,
|
| 274 |
+
user_context: Optional[Dict[str, Any]] = None) -> InferenceResult:
|
| 275 |
+
"""
|
| 276 |
+
從明確輸入和使用者上下文推斷隱含優先級
|
| 277 |
+
|
| 278 |
+
Args:
|
| 279 |
+
explicit_input: 使用者明確輸入
|
| 280 |
+
user_context: 使用者上下文資訊
|
| 281 |
+
|
| 282 |
+
Returns:
|
| 283 |
+
InferenceResult: 推理結果
|
| 284 |
+
"""
|
| 285 |
+
try:
|
| 286 |
+
if user_context is None:
|
| 287 |
+
user_context = {}
|
| 288 |
+
|
| 289 |
+
implicit_priorities = {}
|
| 290 |
+
triggered_rules = []
|
| 291 |
+
reasoning_chains = []
|
| 292 |
+
|
| 293 |
+
# 按優先級排序規則
|
| 294 |
+
sorted_rules = sorted(self.inference_rules, key=lambda r: r.priority)
|
| 295 |
+
|
| 296 |
+
# 應用推理規則
|
| 297 |
+
for rule in sorted_rules:
|
| 298 |
+
try:
|
| 299 |
+
if rule.condition(explicit_input, user_context):
|
| 300 |
+
# 規則觸發
|
| 301 |
+
implied = rule.imply(explicit_input, user_context)
|
| 302 |
+
triggered_rules.append(rule.name)
|
| 303 |
+
reasoning_chains.append(rule.reasoning)
|
| 304 |
+
|
| 305 |
+
# 合併隱含優先級(取最大值)
|
| 306 |
+
for dim, score in implied.items():
|
| 307 |
+
implicit_priorities[dim] = max(
|
| 308 |
+
implicit_priorities.get(dim, 1.0),
|
| 309 |
+
score
|
| 310 |
+
)
|
| 311 |
+
except Exception as e:
|
| 312 |
+
print(f"Error applying rule {rule.name}: {str(e)}")
|
| 313 |
+
continue
|
| 314 |
+
|
| 315 |
+
# 計算信心度
|
| 316 |
+
confidence = self._calculate_inference_confidence(
|
| 317 |
+
triggered_rules, explicit_input
|
| 318 |
+
)
|
| 319 |
+
|
| 320 |
+
return InferenceResult(
|
| 321 |
+
implicit_priorities=implicit_priorities,
|
| 322 |
+
triggered_rules=triggered_rules,
|
| 323 |
+
reasoning_chains=reasoning_chains,
|
| 324 |
+
confidence=confidence
|
| 325 |
+
)
|
| 326 |
+
|
| 327 |
+
except Exception as e:
|
| 328 |
+
print(f"Error inferring implicit priorities: {str(e)}")
|
| 329 |
+
print(traceback.format_exc())
|
| 330 |
+
return InferenceResult()
|
| 331 |
+
|
| 332 |
+
def _calculate_inference_confidence(self,
|
| 333 |
+
triggered_rules: List[str],
|
| 334 |
+
input_text: str) -> float:
|
| 335 |
+
"""
|
| 336 |
+
計算推理信心度
|
| 337 |
+
|
| 338 |
+
Args:
|
| 339 |
+
triggered_rules: 觸發的規則列表
|
| 340 |
+
input_text: 輸入文字
|
| 341 |
+
|
| 342 |
+
Returns:
|
| 343 |
+
float: 信心度 (0-1)
|
| 344 |
+
"""
|
| 345 |
+
base_confidence = 0.6
|
| 346 |
+
|
| 347 |
+
# 觸發的規則越多,信心度越高
|
| 348 |
+
rule_bonus = min(0.3, len(triggered_rules) * 0.1)
|
| 349 |
+
|
| 350 |
+
# 輸入文字越詳細,信心度越高
|
| 351 |
+
word_count = len(input_text.split())
|
| 352 |
+
detail_bonus = min(0.1, word_count / 100)
|
| 353 |
+
|
| 354 |
+
return min(1.0, base_confidence + rule_bonus + detail_bonus)
|
| 355 |
+
|
| 356 |
+
def get_inference_summary(self, result: InferenceResult) -> Dict[str, Any]:
|
| 357 |
+
"""
|
| 358 |
+
獲取推理摘要
|
| 359 |
+
|
| 360 |
+
Args:
|
| 361 |
+
result: 推理結果
|
| 362 |
+
|
| 363 |
+
Returns:
|
| 364 |
+
Dict[str, Any]: 推理摘要
|
| 365 |
+
"""
|
| 366 |
+
return {
|
| 367 |
+
'total_implicit_priorities': len(result.implicit_priorities),
|
| 368 |
+
'implicit_priorities': result.implicit_priorities,
|
| 369 |
+
'triggered_rules': result.triggered_rules,
|
| 370 |
+
'reasoning_chains': result.reasoning_chains,
|
| 371 |
+
'inference_confidence': result.confidence,
|
| 372 |
+
'high_confidence_inferences': [
|
| 373 |
+
dim for dim, score in result.implicit_priorities.items() if score >= 1.4
|
| 374 |
+
]
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
|
| 378 |
+
def infer_user_priorities(user_input: str,
|
| 379 |
+
user_context: Optional[Dict[str, Any]] = None) -> InferenceResult:
|
| 380 |
+
"""
|
| 381 |
+
便利函數: 推斷使用者隱含優先級
|
| 382 |
+
|
| 383 |
+
Args:
|
| 384 |
+
user_input: 使用者輸入
|
| 385 |
+
user_context: 使用者上下文
|
| 386 |
+
|
| 387 |
+
Returns:
|
| 388 |
+
InferenceResult: 推理結果
|
| 389 |
+
"""
|
| 390 |
+
engine = BreedRecommendationInferenceEngine()
|
| 391 |
+
return engine.infer_implicit_priorities(user_input, user_context)
|
| 392 |
+
|
| 393 |
+
|
| 394 |
+
def get_inference_summary(user_input: str,
|
| 395 |
+
user_context: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
| 396 |
+
"""
|
| 397 |
+
便利函數: 獲取推理摘要
|
| 398 |
+
|
| 399 |
+
Args:
|
| 400 |
+
user_input: 使用者輸入
|
| 401 |
+
user_context: 使用者上下文
|
| 402 |
+
|
| 403 |
+
Returns:
|
| 404 |
+
Dict[str, Any]: 推理摘要
|
| 405 |
+
"""
|
| 406 |
+
engine = BreedRecommendationInferenceEngine()
|
| 407 |
+
result = engine.infer_implicit_priorities(user_input, user_context)
|
| 408 |
+
return engine.get_inference_summary(result)
|
matching_score_calculator.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
import random
|
| 2 |
import hashlib
|
| 3 |
import numpy as np
|
|
@@ -483,7 +484,13 @@ class MatchingScoreCalculator:
|
|
| 483 |
# 家庭相容性 (10% 權重)
|
| 484 |
family_score = self._calculate_family_compatibility(family_requirements, breed_good_with_children, breed_temperament)
|
| 485 |
dimension_scores['family'] = family_score
|
| 486 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 487 |
|
| 488 |
# 應用硬約束過濾
|
| 489 |
constraint_penalty = self._apply_hard_constraints_enhanced(user_desc, breed_info)
|
|
@@ -786,6 +793,134 @@ class MatchingScoreCalculator:
|
|
| 786 |
|
| 787 |
return 0.7
|
| 788 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 789 |
def _apply_hard_constraints_enhanced(self, user_desc: str, breed_info: dict) -> float:
|
| 790 |
"""應用品種特性感知的動態懲罰機制"""
|
| 791 |
penalty = 0.0
|
|
@@ -869,12 +1004,15 @@ class MatchingScoreCalculator:
|
|
| 869 |
|
| 870 |
# 添加特殊品種適應性補償機制
|
| 871 |
# 對於邊界適配品種,給予適度補償
|
|
|
|
| 872 |
boundary_adaptable_breeds = {
|
| 873 |
-
'Italian_Greyhound'
|
| 874 |
'Boston_Bull': 0.06, # 適應性強的小型犬
|
| 875 |
'Havanese': 0.05, # 友好適應的小型犬
|
| 876 |
'Silky_terrier': 0.04, # 安靜的玩具犬
|
| 877 |
-
'Basset': 0.07
|
|
|
|
|
|
|
| 878 |
}
|
| 879 |
|
| 880 |
if breed_name in boundary_adaptable_breeds:
|
|
|
|
| 1 |
+
# %%writefile matching_score_calculator.py
|
| 2 |
import random
|
| 3 |
import hashlib
|
| 4 |
import numpy as np
|
|
|
|
| 484 |
# 家庭相容性 (10% 權重)
|
| 485 |
family_score = self._calculate_family_compatibility(family_requirements, breed_good_with_children, breed_temperament)
|
| 486 |
dimension_scores['family'] = family_score
|
| 487 |
+
|
| 488 |
+
# 經驗相容性 - 使用真實品種特性計算
|
| 489 |
+
experience_requirements = self._analyze_experience_requirements(user_desc)
|
| 490 |
+
experience_score = self._calculate_experience_compatibility(
|
| 491 |
+
experience_requirements, breed_info, breed_temperament
|
| 492 |
+
)
|
| 493 |
+
dimension_scores['experience'] = experience_score
|
| 494 |
|
| 495 |
# 應用硬約束過濾
|
| 496 |
constraint_penalty = self._apply_hard_constraints_enhanced(user_desc, breed_info)
|
|
|
|
| 793 |
|
| 794 |
return 0.7
|
| 795 |
|
| 796 |
+
def _analyze_experience_requirements(self, user_desc: str) -> dict:
|
| 797 |
+
"""分析用戶經驗水平需求"""
|
| 798 |
+
requirements = {'level': 'intermediate', 'importance': 0.5}
|
| 799 |
+
|
| 800 |
+
# 新手識別 - 關鍵詞匹配
|
| 801 |
+
beginner_terms = ['first dog', 'first time', 'beginner', 'new to dogs', 'inexperienced',
|
| 802 |
+
'never owned', 'never had a dog', 'first-time owner', 'my first']
|
| 803 |
+
if any(term in user_desc for term in beginner_terms):
|
| 804 |
+
requirements['level'] = 'beginner'
|
| 805 |
+
requirements['importance'] = 0.95 # 對新手非常重要
|
| 806 |
+
|
| 807 |
+
# 高級用戶識別
|
| 808 |
+
advanced_terms = ['experienced', 'advanced', 'expert', 'breeder', 'trainer', 'many dogs']
|
| 809 |
+
if any(term in user_desc for term in advanced_terms):
|
| 810 |
+
requirements['level'] = 'advanced'
|
| 811 |
+
requirements['importance'] = 0.6
|
| 812 |
+
|
| 813 |
+
# 易於訓練需求
|
| 814 |
+
if any(term in user_desc for term in ['easy to train', 'trainable', 'obedient', 'well-behaved']):
|
| 815 |
+
requirements['needs_easy_training'] = True
|
| 816 |
+
requirements['importance'] = max(requirements['importance'], 0.85)
|
| 817 |
+
|
| 818 |
+
# 低維護需求通常暗示需要更易處理的品種
|
| 819 |
+
if any(term in user_desc for term in ['low maintenance', 'low-maintenance', 'easy care']):
|
| 820 |
+
requirements['needs_easy_care'] = True
|
| 821 |
+
if requirements['level'] == 'intermediate':
|
| 822 |
+
requirements['level'] = 'beginner' # 低維護需求暗示初學者
|
| 823 |
+
requirements['importance'] = 0.85
|
| 824 |
+
|
| 825 |
+
return requirements
|
| 826 |
+
|
| 827 |
+
def _calculate_experience_compatibility(self, experience_req: dict, breed_info: dict, temperament: str) -> float:
|
| 828 |
+
"""
|
| 829 |
+
計算經驗相容性分數 - 基於品種特性和用戶經驗水平
|
| 830 |
+
|
| 831 |
+
這是修復的關鍵函數!確保敏感/難以處理的品種對新手有低分數
|
| 832 |
+
"""
|
| 833 |
+
care_level = breed_info.get('Care Level', 'Moderate').lower()
|
| 834 |
+
temperament_lower = temperament.lower()
|
| 835 |
+
user_level = experience_req.get('level', 'intermediate')
|
| 836 |
+
|
| 837 |
+
# 基礎分數矩陣
|
| 838 |
+
base_scores = {
|
| 839 |
+
'high': {
|
| 840 |
+
'beginner': 0.45, # 高照護品種對新手困難
|
| 841 |
+
'intermediate': 0.75,
|
| 842 |
+
'advanced': 0.90
|
| 843 |
+
},
|
| 844 |
+
'moderate': {
|
| 845 |
+
'beginner': 0.65,
|
| 846 |
+
'intermediate': 0.85,
|
| 847 |
+
'advanced': 0.90
|
| 848 |
+
},
|
| 849 |
+
'low': {
|
| 850 |
+
'beginner': 0.85, # 低照護品種對新手友善
|
| 851 |
+
'intermediate': 0.90,
|
| 852 |
+
'advanced': 0.90
|
| 853 |
+
}
|
| 854 |
+
}
|
| 855 |
+
|
| 856 |
+
# 獲取基礎分數
|
| 857 |
+
score = base_scores.get(care_level, base_scores['moderate']).get(user_level, 0.70)
|
| 858 |
+
|
| 859 |
+
# 性格特徵調整 - 對新手特別重要
|
| 860 |
+
if user_level == 'beginner':
|
| 861 |
+
# 困難性格懲罰
|
| 862 |
+
difficult_traits = {
|
| 863 |
+
'sensitive': -0.20, # 敏感品種對新手非常困難(需要細心處理)
|
| 864 |
+
'stubborn': -0.15,
|
| 865 |
+
'independent': -0.12,
|
| 866 |
+
'dominant': -0.15,
|
| 867 |
+
'aggressive': -0.25,
|
| 868 |
+
'nervous': -0.15,
|
| 869 |
+
'alert': -0.08, # 過度警覺可能導致吠叫問題
|
| 870 |
+
'shy': -0.12,
|
| 871 |
+
'timid': -0.12,
|
| 872 |
+
'strong-willed': -0.12,
|
| 873 |
+
'protective': -0.10
|
| 874 |
+
}
|
| 875 |
+
|
| 876 |
+
for trait, penalty in difficult_traits.items():
|
| 877 |
+
if trait in temperament_lower:
|
| 878 |
+
score += penalty
|
| 879 |
+
|
| 880 |
+
# 友善性格獎勵
|
| 881 |
+
easy_traits = {
|
| 882 |
+
'gentle': 0.10,
|
| 883 |
+
'friendly': 0.12,
|
| 884 |
+
'eager to please': 0.15,
|
| 885 |
+
'patient': 0.10,
|
| 886 |
+
'calm': 0.10,
|
| 887 |
+
'outgoing': 0.08,
|
| 888 |
+
'affectionate': 0.08,
|
| 889 |
+
'playful': 0.05, # 輕微加分(可能太活潑)
|
| 890 |
+
'loyal': 0.05
|
| 891 |
+
}
|
| 892 |
+
|
| 893 |
+
for trait, bonus in easy_traits.items():
|
| 894 |
+
if trait in temperament_lower:
|
| 895 |
+
score += bonus
|
| 896 |
+
|
| 897 |
+
# 易於訓練需求額外懲罰/獎勵
|
| 898 |
+
if experience_req.get('needs_easy_training'):
|
| 899 |
+
if any(term in temperament_lower for term in ['stubborn', 'independent', 'strong-willed']):
|
| 900 |
+
score -= 0.12
|
| 901 |
+
elif any(term in temperament_lower for term in ['eager to please', 'intelligent', 'trainable']):
|
| 902 |
+
score += 0.10
|
| 903 |
+
|
| 904 |
+
# Good with Children = No 對新手也是警示
|
| 905 |
+
good_with_children = breed_info.get('Good with Children', 'Yes')
|
| 906 |
+
if good_with_children == 'No':
|
| 907 |
+
score -= 0.08 # 額外扣分:不適合兒童的狗通常對新手也更具挑戰
|
| 908 |
+
|
| 909 |
+
elif user_level == 'intermediate':
|
| 910 |
+
# 中級用戶的適度調整
|
| 911 |
+
if 'stubborn' in temperament_lower:
|
| 912 |
+
score -= 0.05
|
| 913 |
+
if 'independent' in temperament_lower:
|
| 914 |
+
score -= 0.03
|
| 915 |
+
|
| 916 |
+
elif user_level == 'advanced':
|
| 917 |
+
# 高級用戶可以處理具挑戰性的品種
|
| 918 |
+
if any(term in temperament_lower for term in ['intelligent', 'working', 'athletic']):
|
| 919 |
+
score += 0.05
|
| 920 |
+
|
| 921 |
+
# 確保分數在合理範圍內
|
| 922 |
+
return max(0.15, min(0.95, score))
|
| 923 |
+
|
| 924 |
def _apply_hard_constraints_enhanced(self, user_desc: str, breed_info: dict) -> float:
|
| 925 |
"""應用品種特性感知的動態懲罰機制"""
|
| 926 |
penalty = 0.0
|
|
|
|
| 1004 |
|
| 1005 |
# 添加特殊品種適應性補償機制
|
| 1006 |
# 對於邊界適配品種,給予適度補償
|
| 1007 |
+
# 注意:僅補償真正對新手友善的品種
|
| 1008 |
boundary_adaptable_breeds = {
|
| 1009 |
+
# 'Italian_Greyhound' 已移除:Sensitive 性格對新手不友好
|
| 1010 |
'Boston_Bull': 0.06, # 適應性強的小型犬
|
| 1011 |
'Havanese': 0.05, # 友好適應的小型犬
|
| 1012 |
'Silky_terrier': 0.04, # 安靜的玩具犬
|
| 1013 |
+
'Basset': 0.07, # 低能量但友好的中型犬
|
| 1014 |
+
'Cavalier_King_Charles_Spaniel': 0.08, # 溫和友善,適合新手
|
| 1015 |
+
'Bichon_Frise': 0.06 # 友善易訓練
|
| 1016 |
}
|
| 1017 |
|
| 1018 |
if breed_name in boundary_adaptable_breeds:
|
multi_head_scorer.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
import numpy as np
|
| 2 |
import json
|
| 3 |
from typing import Dict, List, Tuple, Optional, Any, Set
|
|
@@ -11,6 +12,9 @@ from breed_health_info import breed_health_info
|
|
| 11 |
from breed_noise_info import breed_noise_info
|
| 12 |
from query_understanding import QueryDimensions
|
| 13 |
from constraint_manager import FilterResult
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
@dataclass
|
| 16 |
class DimensionalScores:
|
|
@@ -142,165 +146,190 @@ class SemanticScoringHead(ScoringHead):
|
|
| 142 |
return f"{breed_name} is a dog breed with various characteristics"
|
| 143 |
|
| 144 |
class AttributeScoringHead(ScoringHead):
|
| 145 |
-
"""
|
|
|
|
|
|
|
|
|
|
| 146 |
|
| 147 |
def __init__(self):
|
| 148 |
-
self.
|
| 149 |
-
|
| 150 |
-
def _initialize_scoring_matrices(self) -> Dict[str, Dict[str, float]]:
|
| 151 |
-
"""初始化評分矩陣"""
|
| 152 |
-
return {
|
| 153 |
-
'spatial_scoring': {
|
| 154 |
-
# (user_preference, breed_attribute) -> score
|
| 155 |
-
('apartment', 'small'): 1.0,
|
| 156 |
-
('apartment', 'medium'): 0.6,
|
| 157 |
-
('apartment', 'large'): 0.2,
|
| 158 |
-
('apartment', 'giant'): 0.0,
|
| 159 |
-
('house', 'small'): 0.7,
|
| 160 |
-
('house', 'medium'): 0.9,
|
| 161 |
-
('house', 'large'): 1.0,
|
| 162 |
-
('house', 'giant'): 1.0,
|
| 163 |
-
},
|
| 164 |
-
'activity_scoring': {
|
| 165 |
-
('low', 'low'): 1.0,
|
| 166 |
-
('low', 'moderate'): 0.7,
|
| 167 |
-
('low', 'high'): 0.2,
|
| 168 |
-
('low', 'very high'): 0.0,
|
| 169 |
-
('moderate', 'low'): 0.8,
|
| 170 |
-
('moderate', 'moderate'): 1.0,
|
| 171 |
-
('moderate', 'high'): 0.8,
|
| 172 |
-
('high', 'moderate'): 0.7,
|
| 173 |
-
('high', 'high'): 1.0,
|
| 174 |
-
('high', 'very high'): 1.0,
|
| 175 |
-
},
|
| 176 |
-
'noise_scoring': {
|
| 177 |
-
('low', 'low'): 1.0,
|
| 178 |
-
('low', 'moderate'): 0.6,
|
| 179 |
-
('low', 'high'): 0.1,
|
| 180 |
-
('moderate', 'low'): 0.8,
|
| 181 |
-
('moderate', 'moderate'): 1.0,
|
| 182 |
-
('moderate', 'high'): 0.7,
|
| 183 |
-
('high', 'low'): 0.7,
|
| 184 |
-
('high', 'moderate'): 0.9,
|
| 185 |
-
('high', 'high'): 1.0,
|
| 186 |
-
},
|
| 187 |
-
'size_scoring': {
|
| 188 |
-
('small', 'small'): 1.0,
|
| 189 |
-
('small', 'medium'): 0.5,
|
| 190 |
-
('small', 'large'): 0.2,
|
| 191 |
-
('medium', 'small'): 0.6,
|
| 192 |
-
('medium', 'medium'): 1.0,
|
| 193 |
-
('medium', 'large'): 0.6,
|
| 194 |
-
('large', 'medium'): 0.7,
|
| 195 |
-
('large', 'large'): 1.0,
|
| 196 |
-
('large', 'giant'): 0.9,
|
| 197 |
-
},
|
| 198 |
-
'maintenance_scoring': {
|
| 199 |
-
('low', 'low'): 1.0,
|
| 200 |
-
('low', 'moderate'): 0.6,
|
| 201 |
-
('low', 'high'): 0.2,
|
| 202 |
-
('moderate', 'low'): 0.8,
|
| 203 |
-
('moderate', 'moderate'): 1.0,
|
| 204 |
-
('moderate', 'high'): 0.7,
|
| 205 |
-
('high', 'low'): 0.6,
|
| 206 |
-
('high', 'moderate'): 0.8,
|
| 207 |
-
('high', 'high'): 1.0,
|
| 208 |
-
}
|
| 209 |
-
}
|
| 210 |
|
| 211 |
def score_dimension(self, breed_info: Dict[str, Any],
|
| 212 |
dimensions: QueryDimensions,
|
| 213 |
dimension_type: str) -> float:
|
| 214 |
-
"""屬性維度評分"""
|
| 215 |
try:
|
| 216 |
-
if
|
| 217 |
return self._score_spatial_compatibility(breed_info, dimensions)
|
| 218 |
-
elif
|
| 219 |
return self._score_activity_compatibility(breed_info, dimensions)
|
| 220 |
-
elif
|
| 221 |
return self._score_noise_compatibility(breed_info, dimensions)
|
| 222 |
-
elif
|
| 223 |
return self._score_size_compatibility(breed_info, dimensions)
|
| 224 |
-
elif
|
| 225 |
return self._score_family_compatibility(breed_info, dimensions)
|
| 226 |
-
elif
|
| 227 |
return self._score_maintenance_compatibility(breed_info, dimensions)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
else:
|
| 229 |
-
return 0.5
|
| 230 |
|
| 231 |
except Exception as e:
|
| 232 |
print(f"Error in attribute scoring for {dimension_type}: {str(e)}")
|
| 233 |
return 0.5
|
| 234 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
def _score_spatial_compatibility(self, breed_info: Dict[str, Any],
|
| 236 |
dimensions: QueryDimensions) -> float:
|
| 237 |
-
"""空間相容性評分"""
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
total_score += score
|
| 248 |
|
| 249 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
|
| 251 |
def _score_activity_compatibility(self, breed_info: Dict[str, Any],
|
| 252 |
dimensions: QueryDimensions) -> float:
|
| 253 |
-
"""活動相容性評分"""
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
total_score = 0.0
|
| 269 |
-
for activity_level in dimensions.activity_level:
|
| 270 |
-
key = (activity_level, breed_exercise)
|
| 271 |
-
score = self.scoring_matrices['activity_scoring'].get(key, 0.5)
|
| 272 |
-
total_score += score
|
| 273 |
-
|
| 274 |
-
return total_score / len(dimensions.activity_level)
|
| 275 |
|
| 276 |
def _score_noise_compatibility(self, breed_info: Dict[str, Any],
|
| 277 |
dimensions: QueryDimensions) -> float:
|
| 278 |
-
"""噪音相容性評分"""
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 291 |
|
| 292 |
def _score_size_compatibility(self, breed_info: Dict[str, Any],
|
| 293 |
dimensions: QueryDimensions) -> float:
|
| 294 |
"""尺寸相容性評分"""
|
| 295 |
if not dimensions.size_preferences:
|
| 296 |
-
return 0.
|
| 297 |
|
| 298 |
breed_size = breed_info.get('size', 'medium').lower()
|
| 299 |
total_score = 0.0
|
| 300 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
for size_pref in dimensions.size_preferences:
|
| 302 |
-
key = (size_pref, breed_size)
|
| 303 |
-
score =
|
| 304 |
total_score += score
|
| 305 |
|
| 306 |
return total_score / len(dimensions.size_preferences)
|
|
@@ -309,7 +338,7 @@ class AttributeScoringHead(ScoringHead):
|
|
| 309 |
dimensions: QueryDimensions) -> float:
|
| 310 |
"""家庭相容性評分"""
|
| 311 |
if not dimensions.family_context:
|
| 312 |
-
return 0.
|
| 313 |
|
| 314 |
good_with_children = breed_info.get('good_with_children', 'Yes')
|
| 315 |
temperament = breed_info.get('temperament', '').lower()
|
|
@@ -319,15 +348,18 @@ class AttributeScoringHead(ScoringHead):
|
|
| 319 |
|
| 320 |
for family_context in dimensions.family_context:
|
| 321 |
if family_context == 'children':
|
| 322 |
-
if good_with_children == 'Yes':
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 326 |
else:
|
| 327 |
-
total_score += 0.
|
| 328 |
score_count += 1
|
| 329 |
elif family_context == 'elderly':
|
| 330 |
-
# 溫和、冷靜的品種適合老年人
|
| 331 |
if any(trait in temperament for trait in ['gentle', 'calm', 'docile']):
|
| 332 |
total_score += 1.0
|
| 333 |
elif any(trait in temperament for trait in ['energetic', 'hyperactive']):
|
|
@@ -336,7 +368,6 @@ class AttributeScoringHead(ScoringHead):
|
|
| 336 |
total_score += 0.7
|
| 337 |
score_count += 1
|
| 338 |
elif family_context == 'single':
|
| 339 |
-
# 大多數品種都適合單身人士
|
| 340 |
total_score += 0.8
|
| 341 |
score_count += 1
|
| 342 |
|
|
@@ -344,19 +375,44 @@ class AttributeScoringHead(ScoringHead):
|
|
| 344 |
|
| 345 |
def _score_maintenance_compatibility(self, breed_info: Dict[str, Any],
|
| 346 |
dimensions: QueryDimensions) -> float:
|
| 347 |
-
"""維護相容性評分"""
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 358 |
|
| 359 |
-
return
|
|
|
|
|
|
|
|
|
|
| 360 |
|
| 361 |
class MultiHeadScorer:
|
| 362 |
"""
|
|
@@ -370,26 +426,31 @@ class MultiHeadScorer:
|
|
| 370 |
self.attribute_head = AttributeScoringHead()
|
| 371 |
self.dimension_weights = self._initialize_dimension_weights()
|
| 372 |
self.head_fusion_weights = self._initialize_head_fusion_weights()
|
|
|
|
|
|
|
| 373 |
|
| 374 |
def _initialize_dimension_weights(self) -> Dict[str, float]:
|
| 375 |
"""初始化維度權重"""
|
| 376 |
return {
|
| 377 |
-
'activity_compatibility': 0.
|
| 378 |
-
'noise_compatibility': 0.
|
| 379 |
-
'spatial_compatibility': 0.
|
| 380 |
-
'family_compatibility': 0.
|
| 381 |
-
'maintenance_compatibility': 0.
|
| 382 |
-
'
|
|
|
|
| 383 |
}
|
| 384 |
|
| 385 |
def _initialize_head_fusion_weights(self) -> Dict[str, Dict[str, float]]:
|
| 386 |
"""初始化頭融合權重"""
|
| 387 |
return {
|
| 388 |
-
'activity_compatibility': {'semantic': 0.
|
| 389 |
-
'noise_compatibility': {'semantic': 0.
|
| 390 |
-
'spatial_compatibility': {'semantic': 0.
|
| 391 |
-
'family_compatibility': {'semantic': 0.
|
| 392 |
-
'maintenance_compatibility': {'semantic': 0.
|
|
|
|
|
|
|
| 393 |
'size_compatibility': {'semantic': 0.2, 'attribute': 0.8}
|
| 394 |
}
|
| 395 |
|
|
@@ -417,6 +478,19 @@ class MultiHeadScorer:
|
|
| 417 |
# 按最終分數排序
|
| 418 |
breed_scores.sort(key=lambda x: x.final_score, reverse=True)
|
| 419 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 420 |
return breed_scores
|
| 421 |
|
| 422 |
except Exception as e:
|
|
@@ -468,9 +542,20 @@ class MultiHeadScorer:
|
|
| 468 |
semantic_total = 0.0
|
| 469 |
attribute_total = 0.0
|
| 470 |
|
| 471 |
-
#
|
| 472 |
-
|
| 473 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 474 |
|
| 475 |
# 為每個活躍維度評分
|
| 476 |
for dimension, weight in adjusted_weights.items():
|
|
@@ -508,8 +593,14 @@ class MultiHeadScorer:
|
|
| 508 |
base_score = sum(score * adjusted_weights[dim]
|
| 509 |
for dim, score in dimensional_scores.items())
|
| 510 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 511 |
# Apply corrections
|
| 512 |
-
final_score = max(0.0, min(1.0, base_score + bidirectional_bonus + bias_correction))
|
| 513 |
|
| 514 |
# 信心度評估
|
| 515 |
confidence_score = self._calculate_confidence(dimensions)
|
|
@@ -549,9 +640,40 @@ class MultiHeadScorer:
|
|
| 549 |
active.add('family_compatibility')
|
| 550 |
if dimensions.maintenance_level:
|
| 551 |
active.add('maintenance_compatibility')
|
|
|
|
|
|
|
| 552 |
|
| 553 |
return active
|
| 554 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 555 |
def _adjust_dimension_weights(self, active_dimensions: Set[str]) -> Dict[str, float]:
|
| 556 |
"""調整維度權重"""
|
| 557 |
if not active_dimensions:
|
|
@@ -569,6 +691,90 @@ class MultiHeadScorer:
|
|
| 569 |
|
| 570 |
return active_weights
|
| 571 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 572 |
def _calculate_bidirectional_bonus(self, breed_info: Dict[str, Any],
|
| 573 |
dimensions: QueryDimensions) -> float:
|
| 574 |
"""計算雙向相容性獎勵"""
|
|
|
|
| 1 |
+
# %%writefile multi_head_scorer.py
|
| 2 |
import numpy as np
|
| 3 |
import json
|
| 4 |
from typing import Dict, List, Tuple, Optional, Any, Set
|
|
|
|
| 12 |
from breed_noise_info import breed_noise_info
|
| 13 |
from query_understanding import QueryDimensions
|
| 14 |
from constraint_manager import FilterResult
|
| 15 |
+
from dynamic_weight_calculator import DynamicWeightCalculator, WeightAllocationResult
|
| 16 |
+
from adaptive_score_distribution import AdaptiveScoreDistribution, DistributionResult
|
| 17 |
+
from dimension_score_calculator import DimensionScoreCalculator
|
| 18 |
|
| 19 |
@dataclass
|
| 20 |
class DimensionalScores:
|
|
|
|
| 146 |
return f"{breed_name} is a dog breed with various characteristics"
|
| 147 |
|
| 148 |
class AttributeScoringHead(ScoringHead):
|
| 149 |
+
"""
|
| 150 |
+
屬性評分頭 - 使用DimensionScoreCalculator進行精確評分
|
| 151 |
+
這個類別整合了dimension_score_calculator.py中的精確評分邏輯
|
| 152 |
+
"""
|
| 153 |
|
| 154 |
def __init__(self):
|
| 155 |
+
self.dimension_calculator = DimensionScoreCalculator()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
|
| 157 |
def score_dimension(self, breed_info: Dict[str, Any],
|
| 158 |
dimensions: QueryDimensions,
|
| 159 |
dimension_type: str) -> float:
|
| 160 |
+
"""屬性維度評分 - 使用精確的DimensionScoreCalculator"""
|
| 161 |
try:
|
| 162 |
+
if 'spatial' in dimension_type:
|
| 163 |
return self._score_spatial_compatibility(breed_info, dimensions)
|
| 164 |
+
elif 'activity' in dimension_type:
|
| 165 |
return self._score_activity_compatibility(breed_info, dimensions)
|
| 166 |
+
elif 'noise' in dimension_type:
|
| 167 |
return self._score_noise_compatibility(breed_info, dimensions)
|
| 168 |
+
elif 'size' in dimension_type:
|
| 169 |
return self._score_size_compatibility(breed_info, dimensions)
|
| 170 |
+
elif 'family' in dimension_type:
|
| 171 |
return self._score_family_compatibility(breed_info, dimensions)
|
| 172 |
+
elif 'maintenance' in dimension_type:
|
| 173 |
return self._score_maintenance_compatibility(breed_info, dimensions)
|
| 174 |
+
elif 'experience' in dimension_type:
|
| 175 |
+
return self._score_experience_compatibility(breed_info, dimensions)
|
| 176 |
+
elif 'health' in dimension_type:
|
| 177 |
+
return self._score_health_compatibility(breed_info, dimensions)
|
| 178 |
else:
|
| 179 |
+
return 0.5
|
| 180 |
|
| 181 |
except Exception as e:
|
| 182 |
print(f"Error in attribute scoring for {dimension_type}: {str(e)}")
|
| 183 |
return 0.5
|
| 184 |
|
| 185 |
+
def _get_user_living_space(self, dimensions: QueryDimensions) -> str:
|
| 186 |
+
"""從dimensions提取居住空間類型"""
|
| 187 |
+
if dimensions.spatial_constraints:
|
| 188 |
+
for constraint in dimensions.spatial_constraints:
|
| 189 |
+
if 'apartment' in constraint.lower():
|
| 190 |
+
return 'apartment'
|
| 191 |
+
elif 'house' in constraint.lower():
|
| 192 |
+
if 'small' in constraint.lower():
|
| 193 |
+
return 'house_small'
|
| 194 |
+
return 'house_large'
|
| 195 |
+
return 'house_small'
|
| 196 |
+
|
| 197 |
+
def _get_user_has_yard(self, dimensions: QueryDimensions) -> bool:
|
| 198 |
+
"""從dimensions提取是否有院子"""
|
| 199 |
+
if dimensions.spatial_constraints:
|
| 200 |
+
for constraint in dimensions.spatial_constraints:
|
| 201 |
+
if 'yard' in constraint.lower():
|
| 202 |
+
return True
|
| 203 |
+
return False
|
| 204 |
+
|
| 205 |
+
def _get_user_exercise_time(self, dimensions: QueryDimensions) -> int:
|
| 206 |
+
"""從dimensions提取運動時間"""
|
| 207 |
+
if dimensions.activity_level:
|
| 208 |
+
for level in dimensions.activity_level:
|
| 209 |
+
if 'low' in level.lower():
|
| 210 |
+
return 30
|
| 211 |
+
elif 'high' in level.lower():
|
| 212 |
+
return 120
|
| 213 |
+
return 60
|
| 214 |
+
|
| 215 |
+
def _get_user_exercise_type(self, dimensions: QueryDimensions) -> str:
|
| 216 |
+
"""從dimensions提取運動類型"""
|
| 217 |
+
if dimensions.activity_level:
|
| 218 |
+
for level in dimensions.activity_level:
|
| 219 |
+
if 'high' in level.lower() or 'active' in level.lower():
|
| 220 |
+
return 'active_training'
|
| 221 |
+
elif 'low' in level.lower():
|
| 222 |
+
return 'light_walks'
|
| 223 |
+
return 'moderate_activity'
|
| 224 |
+
|
| 225 |
+
def _get_user_grooming_commitment(self, dimensions: QueryDimensions) -> str:
|
| 226 |
+
"""從dimensions提取美容承諾度"""
|
| 227 |
+
if dimensions.maintenance_level:
|
| 228 |
+
for level in dimensions.maintenance_level:
|
| 229 |
+
if 'low' in level.lower():
|
| 230 |
+
return 'low'
|
| 231 |
+
elif 'high' in level.lower():
|
| 232 |
+
return 'high'
|
| 233 |
+
return 'medium'
|
| 234 |
+
|
| 235 |
+
def _get_user_experience_level(self, dimensions: QueryDimensions) -> str:
|
| 236 |
+
"""從dimensions提取經驗等級"""
|
| 237 |
+
if dimensions.experience_level:
|
| 238 |
+
for level in dimensions.experience_level:
|
| 239 |
+
if 'beginner' in level.lower() or 'first' in level.lower():
|
| 240 |
+
return 'beginner'
|
| 241 |
+
elif 'advanced' in level.lower() or 'expert' in level.lower():
|
| 242 |
+
return 'advanced'
|
| 243 |
+
return 'intermediate'
|
| 244 |
+
|
| 245 |
def _score_spatial_compatibility(self, breed_info: Dict[str, Any],
|
| 246 |
dimensions: QueryDimensions) -> float:
|
| 247 |
+
"""空間相容性評分 - 使用DimensionScoreCalculator"""
|
| 248 |
+
breed_size = breed_info.get('size', 'medium').capitalize()
|
| 249 |
+
if breed_size.lower() in ['small', 'medium', 'large', 'giant']:
|
| 250 |
+
breed_size = breed_size.capitalize()
|
| 251 |
+
else:
|
| 252 |
+
breed_size = 'Medium'
|
| 253 |
|
| 254 |
+
living_space = self._get_user_living_space(dimensions)
|
| 255 |
+
has_yard = self._get_user_has_yard(dimensions)
|
| 256 |
+
exercise_needs = breed_info.get('exercise_needs', 'Moderate').capitalize()
|
|
|
|
| 257 |
|
| 258 |
+
return self.dimension_calculator.calculate_space_score(
|
| 259 |
+
size=breed_size,
|
| 260 |
+
living_space=living_space,
|
| 261 |
+
has_yard=has_yard,
|
| 262 |
+
exercise_needs=exercise_needs
|
| 263 |
+
)
|
| 264 |
|
| 265 |
def _score_activity_compatibility(self, breed_info: Dict[str, Any],
|
| 266 |
dimensions: QueryDimensions) -> float:
|
| 267 |
+
"""活動相容性評分 - 使用DimensionScoreCalculator"""
|
| 268 |
+
breed_exercise = breed_info.get('exercise_needs', 'Moderate').capitalize()
|
| 269 |
+
exercise_time = self._get_user_exercise_time(dimensions)
|
| 270 |
+
exercise_type = self._get_user_exercise_type(dimensions)
|
| 271 |
+
breed_size = breed_info.get('size', 'Medium').capitalize()
|
| 272 |
+
living_space = self._get_user_living_space(dimensions)
|
| 273 |
+
|
| 274 |
+
return self.dimension_calculator.calculate_exercise_score(
|
| 275 |
+
breed_needs=breed_exercise,
|
| 276 |
+
exercise_time=exercise_time,
|
| 277 |
+
exercise_type=exercise_type,
|
| 278 |
+
breed_size=breed_size,
|
| 279 |
+
living_space=living_space
|
| 280 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
|
| 282 |
def _score_noise_compatibility(self, breed_info: Dict[str, Any],
|
| 283 |
dimensions: QueryDimensions) -> float:
|
| 284 |
+
"""噪音相容性評分 - 使用DimensionScoreCalculator"""
|
| 285 |
+
breed_name = breed_info.get('breed_name', '')
|
| 286 |
+
noise_tolerance = 'medium'
|
| 287 |
+
if dimensions.noise_preferences:
|
| 288 |
+
for pref in dimensions.noise_preferences:
|
| 289 |
+
if 'low' in pref.lower() or 'quiet' in pref.lower():
|
| 290 |
+
noise_tolerance = 'low'
|
| 291 |
+
elif 'high' in pref.lower():
|
| 292 |
+
noise_tolerance = 'high'
|
| 293 |
+
|
| 294 |
+
living_space = self._get_user_living_space(dimensions)
|
| 295 |
+
has_children = 'children' in str(dimensions.family_context).lower()
|
| 296 |
+
children_age = 'school_age'
|
| 297 |
+
|
| 298 |
+
return self.dimension_calculator.calculate_noise_score(
|
| 299 |
+
breed_name=breed_name,
|
| 300 |
+
noise_tolerance=noise_tolerance,
|
| 301 |
+
living_space=living_space,
|
| 302 |
+
has_children=has_children,
|
| 303 |
+
children_age=children_age
|
| 304 |
+
)
|
| 305 |
|
| 306 |
def _score_size_compatibility(self, breed_info: Dict[str, Any],
|
| 307 |
dimensions: QueryDimensions) -> float:
|
| 308 |
"""尺寸相容性評分"""
|
| 309 |
if not dimensions.size_preferences:
|
| 310 |
+
return 0.7
|
| 311 |
|
| 312 |
breed_size = breed_info.get('size', 'medium').lower()
|
| 313 |
total_score = 0.0
|
| 314 |
|
| 315 |
+
size_compatibility = {
|
| 316 |
+
('small', 'small'): 1.0,
|
| 317 |
+
('small', 'medium'): 0.5,
|
| 318 |
+
('small', 'large'): 0.2,
|
| 319 |
+
('small', 'giant'): 0.1,
|
| 320 |
+
('medium', 'small'): 0.6,
|
| 321 |
+
('medium', 'medium'): 1.0,
|
| 322 |
+
('medium', 'large'): 0.7,
|
| 323 |
+
('medium', 'giant'): 0.4,
|
| 324 |
+
('large', 'small'): 0.3,
|
| 325 |
+
('large', 'medium'): 0.7,
|
| 326 |
+
('large', 'large'): 1.0,
|
| 327 |
+
('large', 'giant'): 0.9,
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
for size_pref in dimensions.size_preferences:
|
| 331 |
+
key = (size_pref.lower(), breed_size)
|
| 332 |
+
score = size_compatibility.get(key, 0.5)
|
| 333 |
total_score += score
|
| 334 |
|
| 335 |
return total_score / len(dimensions.size_preferences)
|
|
|
|
| 338 |
dimensions: QueryDimensions) -> float:
|
| 339 |
"""家庭相容性評分"""
|
| 340 |
if not dimensions.family_context:
|
| 341 |
+
return 0.7
|
| 342 |
|
| 343 |
good_with_children = breed_info.get('good_with_children', 'Yes')
|
| 344 |
temperament = breed_info.get('temperament', '').lower()
|
|
|
|
| 348 |
|
| 349 |
for family_context in dimensions.family_context:
|
| 350 |
if family_context == 'children':
|
| 351 |
+
if good_with_children == 'Yes' or good_with_children == True:
|
| 352 |
+
# 進一步檢查temperament
|
| 353 |
+
if any(trait in temperament for trait in ['gentle', 'friendly', 'patient']):
|
| 354 |
+
total_score += 1.0
|
| 355 |
+
else:
|
| 356 |
+
total_score += 0.85
|
| 357 |
+
elif good_with_children == 'No' or good_with_children == False:
|
| 358 |
+
total_score += 0.15
|
| 359 |
else:
|
| 360 |
+
total_score += 0.5
|
| 361 |
score_count += 1
|
| 362 |
elif family_context == 'elderly':
|
|
|
|
| 363 |
if any(trait in temperament for trait in ['gentle', 'calm', 'docile']):
|
| 364 |
total_score += 1.0
|
| 365 |
elif any(trait in temperament for trait in ['energetic', 'hyperactive']):
|
|
|
|
| 368 |
total_score += 0.7
|
| 369 |
score_count += 1
|
| 370 |
elif family_context == 'single':
|
|
|
|
| 371 |
total_score += 0.8
|
| 372 |
score_count += 1
|
| 373 |
|
|
|
|
| 375 |
|
| 376 |
def _score_maintenance_compatibility(self, breed_info: Dict[str, Any],
|
| 377 |
dimensions: QueryDimensions) -> float:
|
| 378 |
+
"""維護相容性評分 - 使用DimensionScoreCalculator"""
|
| 379 |
+
breed_grooming = breed_info.get('grooming_needs', 'Moderate').capitalize()
|
| 380 |
+
user_commitment = self._get_user_grooming_commitment(dimensions)
|
| 381 |
+
breed_size = breed_info.get('size', 'Medium').capitalize()
|
| 382 |
+
breed_name = breed_info.get('breed_name', '')
|
| 383 |
+
temperament = breed_info.get('temperament', '')
|
| 384 |
+
|
| 385 |
+
return self.dimension_calculator.calculate_grooming_score(
|
| 386 |
+
breed_needs=breed_grooming,
|
| 387 |
+
user_commitment=user_commitment,
|
| 388 |
+
breed_size=breed_size,
|
| 389 |
+
breed_name=breed_name,
|
| 390 |
+
temperament=temperament
|
| 391 |
+
)
|
| 392 |
+
|
| 393 |
+
def _score_experience_compatibility(self, breed_info: Dict[str, Any],
|
| 394 |
+
dimensions: QueryDimensions) -> float:
|
| 395 |
+
"""經驗相容性評分 - 使用DimensionScoreCalculator"""
|
| 396 |
+
care_level = breed_info.get('care_level', 'Moderate').capitalize()
|
| 397 |
+
user_experience = self._get_user_experience_level(dimensions)
|
| 398 |
+
temperament = breed_info.get('temperament', '')
|
| 399 |
+
|
| 400 |
+
return self.dimension_calculator.calculate_experience_score(
|
| 401 |
+
care_level=care_level,
|
| 402 |
+
user_experience=user_experience,
|
| 403 |
+
temperament=temperament
|
| 404 |
+
)
|
| 405 |
+
|
| 406 |
+
def _score_health_compatibility(self, breed_info: Dict[str, Any],
|
| 407 |
+
dimensions: QueryDimensions) -> float:
|
| 408 |
+
"""健康相容性評分 - 使用DimensionScoreCalculator"""
|
| 409 |
+
breed_name = breed_info.get('breed_name', '')
|
| 410 |
+
health_sensitivity = 'medium'
|
| 411 |
|
| 412 |
+
return self.dimension_calculator.calculate_health_score(
|
| 413 |
+
breed_name=breed_name,
|
| 414 |
+
health_sensitivity=health_sensitivity
|
| 415 |
+
)
|
| 416 |
|
| 417 |
class MultiHeadScorer:
|
| 418 |
"""
|
|
|
|
| 426 |
self.attribute_head = AttributeScoringHead()
|
| 427 |
self.dimension_weights = self._initialize_dimension_weights()
|
| 428 |
self.head_fusion_weights = self._initialize_head_fusion_weights()
|
| 429 |
+
self.weight_calculator = DynamicWeightCalculator()
|
| 430 |
+
self.score_distributor = AdaptiveScoreDistribution()
|
| 431 |
|
| 432 |
def _initialize_dimension_weights(self) -> Dict[str, float]:
|
| 433 |
"""初始化維度權重"""
|
| 434 |
return {
|
| 435 |
+
'activity_compatibility': 0.20, # 生活方式匹配
|
| 436 |
+
'noise_compatibility': 0.15, # 居住和諧
|
| 437 |
+
'spatial_compatibility': 0.12, # 物理約束
|
| 438 |
+
'family_compatibility': 0.12, # 社交相容性
|
| 439 |
+
'maintenance_compatibility': 0.15, # 持續護理評估
|
| 440 |
+
'experience_compatibility': 0.18, # 經驗匹配(對新手非常重要)
|
| 441 |
+
'health_compatibility': 0.08 # 健康考量
|
| 442 |
}
|
| 443 |
|
| 444 |
def _initialize_head_fusion_weights(self) -> Dict[str, Dict[str, float]]:
|
| 445 |
"""初始化頭融合權重"""
|
| 446 |
return {
|
| 447 |
+
'activity_compatibility': {'semantic': 0.3, 'attribute': 0.7},
|
| 448 |
+
'noise_compatibility': {'semantic': 0.2, 'attribute': 0.8},
|
| 449 |
+
'spatial_compatibility': {'semantic': 0.2, 'attribute': 0.8},
|
| 450 |
+
'family_compatibility': {'semantic': 0.4, 'attribute': 0.6},
|
| 451 |
+
'maintenance_compatibility': {'semantic': 0.2, 'attribute': 0.8},
|
| 452 |
+
'experience_compatibility': {'semantic': 0.2, 'attribute': 0.8}, # 經驗主要看attribute
|
| 453 |
+
'health_compatibility': {'semantic': 0.3, 'attribute': 0.7},
|
| 454 |
'size_compatibility': {'semantic': 0.2, 'attribute': 0.8}
|
| 455 |
}
|
| 456 |
|
|
|
|
| 478 |
# 按最終分數排序
|
| 479 |
breed_scores.sort(key=lambda x: x.final_score, reverse=True)
|
| 480 |
|
| 481 |
+
# 應用自適應分數分佈
|
| 482 |
+
raw_scores = [(bs.breed_name, bs.final_score) for bs in breed_scores]
|
| 483 |
+
distribution_result = self.score_distributor.distribute_scores(raw_scores)
|
| 484 |
+
|
| 485 |
+
# 更新品種分數
|
| 486 |
+
score_mapping = {breed: score for breed, score in distribution_result.final_scores}
|
| 487 |
+
for breed_score in breed_scores:
|
| 488 |
+
if breed_score.breed_name in score_mapping:
|
| 489 |
+
breed_score.final_score = score_mapping[breed_score.breed_name]
|
| 490 |
+
|
| 491 |
+
# 重新排序
|
| 492 |
+
breed_scores.sort(key=lambda x: x.final_score, reverse=True)
|
| 493 |
+
|
| 494 |
return breed_scores
|
| 495 |
|
| 496 |
except Exception as e:
|
|
|
|
| 542 |
semantic_total = 0.0
|
| 543 |
attribute_total = 0.0
|
| 544 |
|
| 545 |
+
# 動態權重分配(優先使用dimension_priorities)
|
| 546 |
+
if dimensions.dimension_priorities:
|
| 547 |
+
# 使用動態權重計算器
|
| 548 |
+
user_mentions = self._extract_user_mentions(dimensions)
|
| 549 |
+
weight_result = self.weight_calculator.calculate_dynamic_weights(
|
| 550 |
+
dimensions.dimension_priorities,
|
| 551 |
+
user_mentions,
|
| 552 |
+
use_contextual=True
|
| 553 |
+
)
|
| 554 |
+
adjusted_weights = weight_result.dynamic_weights
|
| 555 |
+
else:
|
| 556 |
+
# 降級到原有邏輯
|
| 557 |
+
active_dimensions = self._get_active_dimensions(dimensions)
|
| 558 |
+
adjusted_weights = self._adjust_dimension_weights(active_dimensions)
|
| 559 |
|
| 560 |
# 為每個活躍維度評分
|
| 561 |
for dimension, weight in adjusted_weights.items():
|
|
|
|
| 593 |
base_score = sum(score * adjusted_weights[dim]
|
| 594 |
for dim, score in dimensional_scores.items())
|
| 595 |
|
| 596 |
+
# 關鍵維度低分懲罰機制
|
| 597 |
+
# 當用戶明確提到某維度且該維度分數很低時,施加額外懲罰
|
| 598 |
+
critical_penalty = self._calculate_critical_dimension_penalty(
|
| 599 |
+
dimensional_scores, dimensions, adjusted_weights
|
| 600 |
+
)
|
| 601 |
+
|
| 602 |
# Apply corrections
|
| 603 |
+
final_score = max(0.0, min(1.0, base_score + bidirectional_bonus + bias_correction + critical_penalty))
|
| 604 |
|
| 605 |
# 信心度評估
|
| 606 |
confidence_score = self._calculate_confidence(dimensions)
|
|
|
|
| 640 |
active.add('family_compatibility')
|
| 641 |
if dimensions.maintenance_level:
|
| 642 |
active.add('maintenance_compatibility')
|
| 643 |
+
if hasattr(dimensions, 'experience_level') and dimensions.experience_level:
|
| 644 |
+
active.add('experience_compatibility')
|
| 645 |
|
| 646 |
return active
|
| 647 |
|
| 648 |
+
def _extract_user_mentions(self, dimensions: QueryDimensions) -> Set[str]:
|
| 649 |
+
"""
|
| 650 |
+
提取使用者明確提到的維度
|
| 651 |
+
|
| 652 |
+
Args:
|
| 653 |
+
dimensions: 查詢維度
|
| 654 |
+
|
| 655 |
+
Returns:
|
| 656 |
+
Set[str]: 使用者提到的維度集合
|
| 657 |
+
"""
|
| 658 |
+
mentioned = set()
|
| 659 |
+
|
| 660 |
+
if dimensions.spatial_constraints:
|
| 661 |
+
mentioned.add('spatial_compatibility')
|
| 662 |
+
if dimensions.activity_level:
|
| 663 |
+
mentioned.add('activity_compatibility')
|
| 664 |
+
if dimensions.noise_preferences:
|
| 665 |
+
mentioned.add('noise_compatibility')
|
| 666 |
+
if dimensions.size_preferences:
|
| 667 |
+
mentioned.add('size_compatibility')
|
| 668 |
+
if dimensions.family_context:
|
| 669 |
+
mentioned.add('family_compatibility')
|
| 670 |
+
if dimensions.maintenance_level:
|
| 671 |
+
mentioned.add('maintenance_compatibility')
|
| 672 |
+
if hasattr(dimensions, 'experience_level') and dimensions.experience_level:
|
| 673 |
+
mentioned.add('experience_compatibility')
|
| 674 |
+
|
| 675 |
+
return mentioned
|
| 676 |
+
|
| 677 |
def _adjust_dimension_weights(self, active_dimensions: Set[str]) -> Dict[str, float]:
|
| 678 |
"""調整維度權重"""
|
| 679 |
if not active_dimensions:
|
|
|
|
| 691 |
|
| 692 |
return active_weights
|
| 693 |
|
| 694 |
+
def _calculate_critical_dimension_penalty(self,
|
| 695 |
+
dimensional_scores: Dict[str, float],
|
| 696 |
+
dimensions: QueryDimensions,
|
| 697 |
+
weights: Dict[str, float]) -> float:
|
| 698 |
+
"""
|
| 699 |
+
計算關鍵維度低分懲罰
|
| 700 |
+
|
| 701 |
+
當用戶明確提到某維度(通過 dimension_priorities 或活躍維度)
|
| 702 |
+
且該維度的分數低於閾值時,施加額外懲罰。
|
| 703 |
+
|
| 704 |
+
這確保了「不合適」的品種不會因為其他維度的高分而排名過高。
|
| 705 |
+
|
| 706 |
+
Args:
|
| 707 |
+
dimensional_scores: 各維度的分數
|
| 708 |
+
dimensions: 查詢維度
|
| 709 |
+
weights: 當前使用的維度權重
|
| 710 |
+
|
| 711 |
+
Returns:
|
| 712 |
+
float: 懲罰值(負數)
|
| 713 |
+
"""
|
| 714 |
+
total_penalty = 0.0
|
| 715 |
+
|
| 716 |
+
# 定義低分閾值和懲罰係數
|
| 717 |
+
LOW_SCORE_THRESHOLD = 0.55 # 低於此分數視為不匹配
|
| 718 |
+
VERY_LOW_THRESHOLD = 0.40 # 極低分數
|
| 719 |
+
PENALTY_MULTIPLIER = 0.25 # 基礎懲罰乘數(提高以增強效果)
|
| 720 |
+
|
| 721 |
+
# 檢測用戶關心的維度
|
| 722 |
+
user_priorities = {}
|
| 723 |
+
|
| 724 |
+
# 從 dimension_priorities 獲取優先級
|
| 725 |
+
if dimensions.dimension_priorities:
|
| 726 |
+
for dim, priority in dimensions.dimension_priorities.items():
|
| 727 |
+
# 映射維度名稱
|
| 728 |
+
mapped_dim = self._map_priority_dimension(dim)
|
| 729 |
+
if mapped_dim:
|
| 730 |
+
user_priorities[mapped_dim] = priority
|
| 731 |
+
|
| 732 |
+
# 從活躍維度補充(確保提到的維度都被考慮)
|
| 733 |
+
active_dims = self._get_active_dimensions(dimensions)
|
| 734 |
+
for dim in active_dims:
|
| 735 |
+
if dim not in user_priorities:
|
| 736 |
+
user_priorities[dim] = 1.2 # 給予基本優先級
|
| 737 |
+
|
| 738 |
+
# 對用戶關心的維度檢查低分情況
|
| 739 |
+
for dim, priority in user_priorities.items():
|
| 740 |
+
if dim in dimensional_scores:
|
| 741 |
+
score = dimensional_scores[dim]
|
| 742 |
+
|
| 743 |
+
# 只對低分維度施加懲罰
|
| 744 |
+
if score < LOW_SCORE_THRESHOLD:
|
| 745 |
+
# 懲罰程度與以下因素成正比:
|
| 746 |
+
# 1. 分數有多低(距離閾值的差距)
|
| 747 |
+
# 2. 用戶對該維度的優先級
|
| 748 |
+
score_gap = LOW_SCORE_THRESHOLD - score
|
| 749 |
+
priority_factor = min(2.0, priority) # 限制優先級影響
|
| 750 |
+
|
| 751 |
+
penalty = -score_gap * priority_factor * PENALTY_MULTIPLIER
|
| 752 |
+
|
| 753 |
+
# 極低分數額外懲罰
|
| 754 |
+
if score < VERY_LOW_THRESHOLD:
|
| 755 |
+
penalty *= 1.5
|
| 756 |
+
|
| 757 |
+
total_penalty += penalty
|
| 758 |
+
|
| 759 |
+
return total_penalty
|
| 760 |
+
|
| 761 |
+
def _map_priority_dimension(self, dim: str) -> str:
|
| 762 |
+
"""將 priority_detector 的維度名稱映射到 multi_head_scorer 使用的名稱"""
|
| 763 |
+
mapping = {
|
| 764 |
+
'noise': 'noise_compatibility',
|
| 765 |
+
'size': 'spatial_compatibility',
|
| 766 |
+
'exercise': 'activity_compatibility',
|
| 767 |
+
'activity': 'activity_compatibility',
|
| 768 |
+
'grooming': 'maintenance_compatibility',
|
| 769 |
+
'maintenance': 'maintenance_compatibility',
|
| 770 |
+
'family': 'family_compatibility',
|
| 771 |
+
'experience': 'experience_compatibility',
|
| 772 |
+
'health': 'health_compatibility',
|
| 773 |
+
'spatial': 'spatial_compatibility',
|
| 774 |
+
'space': 'spatial_compatibility'
|
| 775 |
+
}
|
| 776 |
+
return mapping.get(dim, dim if dim.endswith('_compatibility') else None)
|
| 777 |
+
|
| 778 |
def _calculate_bidirectional_bonus(self, breed_info: Dict[str, Any],
|
| 779 |
dimensions: QueryDimensions) -> float:
|
| 780 |
"""計算雙向相容性獎勵"""
|
priority_detector.py
ADDED
|
@@ -0,0 +1,462 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# %%writefile priority_detector.py
|
| 2 |
+
import re
|
| 3 |
+
import numpy as np
|
| 4 |
+
from typing import Dict, List, Tuple, Set, Optional, Any
|
| 5 |
+
from dataclasses import dataclass, field
|
| 6 |
+
import traceback
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
@dataclass
|
| 10 |
+
class PriorityDetectionResult:
|
| 11 |
+
"""優先級檢測結果"""
|
| 12 |
+
dimension_priorities: Dict[str, float] = field(default_factory=dict)
|
| 13 |
+
detected_emphases: Dict[str, List[float]] = field(default_factory=dict)
|
| 14 |
+
detected_rankings: Dict[str, int] = field(default_factory=dict)
|
| 15 |
+
detected_negatives: List[str] = field(default_factory=list)
|
| 16 |
+
detection_confidence: float = 1.0
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class PriorityDetector:
|
| 20 |
+
"""
|
| 21 |
+
優先級檢測器
|
| 22 |
+
檢測使用者輸入中的優先級表達,包括強調關鍵字、排序詞、負面約束
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
def __init__(self):
|
| 26 |
+
"""初始化優先級檢測器"""
|
| 27 |
+
self.emphasis_keywords = self._initialize_emphasis_keywords()
|
| 28 |
+
self.ranking_keywords = self._initialize_ranking_keywords()
|
| 29 |
+
self.negative_keywords = self._initialize_negative_keywords()
|
| 30 |
+
self.dimension_keywords = self._initialize_dimension_keywords()
|
| 31 |
+
self.absolute_max_priority = 2.5
|
| 32 |
+
|
| 33 |
+
def _initialize_emphasis_keywords(self) -> Dict[str, Dict[str, List[str]]]:
|
| 34 |
+
"""初始化強調關鍵字"""
|
| 35 |
+
return {
|
| 36 |
+
'strong_emphasis': {
|
| 37 |
+
'en': [
|
| 38 |
+
'most important', 'most importantly', 'must have', 'absolutely need',
|
| 39 |
+
'critical', 'essential', 'top priority', 'crucial',
|
| 40 |
+
'absolutely', 'definitely', 'certainly', 'paramount',
|
| 41 |
+
'vital', 'indispensable', 'mandatory', 'imperative'
|
| 42 |
+
]
|
| 43 |
+
},
|
| 44 |
+
'medium_emphasis': {
|
| 45 |
+
'en': [
|
| 46 |
+
'really want', 'prefer', 'hope for', 'would like',
|
| 47 |
+
'strongly prefer', 'important', 'significant',
|
| 48 |
+
'really need', 'very important', 'highly prefer'
|
| 49 |
+
]
|
| 50 |
+
},
|
| 51 |
+
'mild_emphasis': {
|
| 52 |
+
'en': [
|
| 53 |
+
'nice to have', 'ideally', 'if possible', 'bonus if',
|
| 54 |
+
'preferably', 'would be nice', 'hopefully',
|
| 55 |
+
'optimally', 'wish for'
|
| 56 |
+
]
|
| 57 |
+
}
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
def _initialize_ranking_keywords(self) -> Dict[str, List[str]]:
|
| 61 |
+
"""初始化排序關鍵字"""
|
| 62 |
+
return {
|
| 63 |
+
'en': [
|
| 64 |
+
'first', 'second', 'third', 'fourth', 'fifth',
|
| 65 |
+
'1st', '2nd', '3rd', '4th', '5th',
|
| 66 |
+
'firstly', 'secondly', 'thirdly',
|
| 67 |
+
'primary', 'secondary', 'tertiary'
|
| 68 |
+
]
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
def _initialize_negative_keywords(self) -> Dict[str, List[str]]:
|
| 72 |
+
"""初始化負面約束關鍵字"""
|
| 73 |
+
return {
|
| 74 |
+
'en': [
|
| 75 |
+
'must not', 'cannot', "don't want", "don't need",
|
| 76 |
+
'absolutely no', 'cannot tolerate', 'no way',
|
| 77 |
+
'avoid', 'never', 'not', 'refuse',
|
| 78 |
+
'unacceptable', 'won\'t accept'
|
| 79 |
+
]
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
def _initialize_dimension_keywords(self) -> Dict[str, List[str]]:
|
| 83 |
+
"""初始化維度關鍵字映射"""
|
| 84 |
+
return {
|
| 85 |
+
'noise': [
|
| 86 |
+
'quiet', 'silent', 'not noisy', "doesn't bark", 'peaceful',
|
| 87 |
+
'noise', 'barking', 'vocal', 'loud', 'sound'
|
| 88 |
+
],
|
| 89 |
+
'size': [
|
| 90 |
+
'small', 'medium', 'large', 'tiny', 'big', 'compact',
|
| 91 |
+
'size', 'giant', 'toy', 'miniature'
|
| 92 |
+
],
|
| 93 |
+
'grooming': [
|
| 94 |
+
'low maintenance', 'easy care', 'minimal grooming', 'low-maintenance',
|
| 95 |
+
'grooming', 'care', 'maintenance', 'brush', 'shed', 'shedding'
|
| 96 |
+
],
|
| 97 |
+
'family': [
|
| 98 |
+
'good with kids', 'child friendly', 'family dog',
|
| 99 |
+
'children', 'kids', 'family', 'toddler', 'baby'
|
| 100 |
+
],
|
| 101 |
+
'exercise': [
|
| 102 |
+
'active', 'exercise', 'energy', 'activity',
|
| 103 |
+
'lazy', 'calm', 'energetic', 'athletic', 'work full time'
|
| 104 |
+
],
|
| 105 |
+
'experience': [
|
| 106 |
+
'first time', 'first dog', 'beginner', 'new to dogs', 'inexperienced',
|
| 107 |
+
'easy to train', 'trainable', 'obedient', 'never owned', 'never had'
|
| 108 |
+
],
|
| 109 |
+
'health': [
|
| 110 |
+
'healthy', 'health', 'lifespan', 'longevity',
|
| 111 |
+
'medical', 'genetic issues'
|
| 112 |
+
]
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
def detect_priorities(self, user_input: str) -> PriorityDetectionResult:
|
| 116 |
+
"""
|
| 117 |
+
檢測使用者輸入中的優先級
|
| 118 |
+
|
| 119 |
+
Args:
|
| 120 |
+
user_input: 使用者輸入文字
|
| 121 |
+
|
| 122 |
+
Returns:
|
| 123 |
+
PriorityDetectionResult: 優先級檢測結果
|
| 124 |
+
"""
|
| 125 |
+
try:
|
| 126 |
+
if not user_input or not user_input.strip():
|
| 127 |
+
return PriorityDetectionResult()
|
| 128 |
+
|
| 129 |
+
normalized_input = user_input.lower().strip()
|
| 130 |
+
|
| 131 |
+
# Step 1: 檢測強調關鍵字
|
| 132 |
+
detected_emphases = self._detect_emphasis_keywords(normalized_input)
|
| 133 |
+
|
| 134 |
+
# Step 2: 檢測排序詞
|
| 135 |
+
detected_rankings = self._detect_explicit_ranking(normalized_input)
|
| 136 |
+
|
| 137 |
+
# Step 3: 檢測負面約束
|
| 138 |
+
detected_negatives = self._detect_negative_constraints(normalized_input)
|
| 139 |
+
|
| 140 |
+
# Step 4: 檢測所有提及的維度(即使沒有強調詞)
|
| 141 |
+
mentioned_dimensions = self._detect_mentioned_dimensions(normalized_input)
|
| 142 |
+
|
| 143 |
+
# Step 5: 計算疊加優先級(包括提及的維度)
|
| 144 |
+
dimension_priorities = self._calculate_final_priorities(
|
| 145 |
+
detected_emphases, detected_rankings, mentioned_dimensions
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
# Step 6: 計算信心度
|
| 149 |
+
detection_confidence = self._calculate_detection_confidence(
|
| 150 |
+
detected_emphases, detected_rankings, normalized_input
|
| 151 |
+
)
|
| 152 |
+
|
| 153 |
+
return PriorityDetectionResult(
|
| 154 |
+
dimension_priorities=dimension_priorities,
|
| 155 |
+
detected_emphases=detected_emphases,
|
| 156 |
+
detected_rankings=detected_rankings,
|
| 157 |
+
detected_negatives=detected_negatives,
|
| 158 |
+
detection_confidence=detection_confidence
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
except Exception as e:
|
| 162 |
+
print(f"Error detecting priorities: {str(e)}")
|
| 163 |
+
print(traceback.format_exc())
|
| 164 |
+
return PriorityDetectionResult()
|
| 165 |
+
|
| 166 |
+
def _detect_mentioned_dimensions(self, text: str) -> Set[str]:
|
| 167 |
+
"""
|
| 168 |
+
檢測文字中提及的所有維度(不需要強調詞)
|
| 169 |
+
|
| 170 |
+
Args:
|
| 171 |
+
text: 正規化後的輸入文字
|
| 172 |
+
|
| 173 |
+
Returns:
|
| 174 |
+
Set[str]: 提及的維度集合
|
| 175 |
+
"""
|
| 176 |
+
mentioned = set()
|
| 177 |
+
|
| 178 |
+
for dimension, keywords in self.dimension_keywords.items():
|
| 179 |
+
for keyword in keywords:
|
| 180 |
+
if keyword in text:
|
| 181 |
+
mentioned.add(dimension)
|
| 182 |
+
break # 一個維度只需匹配一次
|
| 183 |
+
|
| 184 |
+
return mentioned
|
| 185 |
+
|
| 186 |
+
def _detect_emphasis_keywords(self, text: str) -> Dict[str, List[float]]:
|
| 187 |
+
"""檢測強調關鍵字"""
|
| 188 |
+
detected = {}
|
| 189 |
+
|
| 190 |
+
# 定義權重倍數
|
| 191 |
+
emphasis_weights = {
|
| 192 |
+
'strong_emphasis': 2.0,
|
| 193 |
+
'medium_emphasis': 1.5,
|
| 194 |
+
'mild_emphasis': 1.2
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
# 為每個強調級別檢測
|
| 198 |
+
for emphasis_level, keywords_dict in self.emphasis_keywords.items():
|
| 199 |
+
weight = emphasis_weights[emphasis_level]
|
| 200 |
+
|
| 201 |
+
for lang, keywords in keywords_dict.items():
|
| 202 |
+
for keyword in keywords:
|
| 203 |
+
if keyword in text:
|
| 204 |
+
# 找到關鍵字附近的維度詞
|
| 205 |
+
dimensions = self._extract_nearby_dimensions(text, keyword)
|
| 206 |
+
for dimension in dimensions:
|
| 207 |
+
if dimension not in detected:
|
| 208 |
+
detected[dimension] = []
|
| 209 |
+
detected[dimension].append(weight)
|
| 210 |
+
|
| 211 |
+
return detected
|
| 212 |
+
|
| 213 |
+
def _detect_explicit_ranking(self, text: str) -> Dict[str, int]:
|
| 214 |
+
"""檢測明確排序詞"""
|
| 215 |
+
detected = {}
|
| 216 |
+
|
| 217 |
+
# 排序詞到排名的映射
|
| 218 |
+
ranking_map = {
|
| 219 |
+
'first': 1, '1st': 1, 'firstly': 1, 'primary': 1,
|
| 220 |
+
'second': 2, '2nd': 2, 'secondly': 2, 'secondary': 2,
|
| 221 |
+
'third': 3, '3rd': 3, 'thirdly': 3, 'tertiary': 3,
|
| 222 |
+
'fourth': 4, '4th': 4,
|
| 223 |
+
'fifth': 5, '5th': 5
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
for keyword in self.ranking_keywords['en']:
|
| 227 |
+
if keyword in text:
|
| 228 |
+
rank = ranking_map.get(keyword, 0)
|
| 229 |
+
if rank > 0:
|
| 230 |
+
# 找到排序詞附近的維度詞
|
| 231 |
+
dimensions = self._extract_nearby_dimensions(text, keyword)
|
| 232 |
+
for dimension in dimensions:
|
| 233 |
+
# 如果已經有排名,取較高優先級(較小的數字)
|
| 234 |
+
if dimension in detected:
|
| 235 |
+
detected[dimension] = min(detected[dimension], rank)
|
| 236 |
+
else:
|
| 237 |
+
detected[dimension] = rank
|
| 238 |
+
|
| 239 |
+
return detected
|
| 240 |
+
|
| 241 |
+
def _detect_negative_constraints(self, text: str) -> List[str]:
|
| 242 |
+
"""檢測負面約束"""
|
| 243 |
+
detected = []
|
| 244 |
+
|
| 245 |
+
for lang, keywords in self.negative_keywords.items():
|
| 246 |
+
for keyword in keywords:
|
| 247 |
+
if keyword in text:
|
| 248 |
+
# 找到負面關鍵字附近的維度詞
|
| 249 |
+
dimensions = self._extract_nearby_dimensions(text, keyword)
|
| 250 |
+
detected.extend(dimensions)
|
| 251 |
+
|
| 252 |
+
return list(set(detected))
|
| 253 |
+
|
| 254 |
+
def _extract_nearby_dimensions(self, text: str, keyword: str, window: int = 50) -> List[str]:
|
| 255 |
+
"""
|
| 256 |
+
提取關鍵字附近的維度詞
|
| 257 |
+
|
| 258 |
+
Args:
|
| 259 |
+
text: 文字
|
| 260 |
+
keyword: 關鍵字
|
| 261 |
+
window: 搜尋窗口大小(字元數)
|
| 262 |
+
|
| 263 |
+
Returns:
|
| 264 |
+
List[str]: 檢測到的維度列表
|
| 265 |
+
"""
|
| 266 |
+
detected_dimensions = []
|
| 267 |
+
|
| 268 |
+
# 找到關鍵字位置
|
| 269 |
+
keyword_positions = [m.start() for m in re.finditer(re.escape(keyword), text)]
|
| 270 |
+
|
| 271 |
+
for pos in keyword_positions:
|
| 272 |
+
# 定義搜尋窗口
|
| 273 |
+
start = max(0, pos - window)
|
| 274 |
+
end = min(len(text), pos + len(keyword) + window)
|
| 275 |
+
window_text = text[start:end]
|
| 276 |
+
|
| 277 |
+
# 在窗口中搜尋維度關鍵字
|
| 278 |
+
for dimension, dimension_keywords in self.dimension_keywords.items():
|
| 279 |
+
for dim_keyword in dimension_keywords:
|
| 280 |
+
if dim_keyword in window_text:
|
| 281 |
+
detected_dimensions.append(dimension)
|
| 282 |
+
break # 找到一個就夠了,不重複添加
|
| 283 |
+
|
| 284 |
+
return list(set(detected_dimensions))
|
| 285 |
+
|
| 286 |
+
def _calculate_final_priorities(self,
|
| 287 |
+
detected_emphases: Dict[str, List[float]],
|
| 288 |
+
detected_rankings: Dict[str, int],
|
| 289 |
+
mentioned_dimensions: Set[str] = None) -> Dict[str, float]:
|
| 290 |
+
"""
|
| 291 |
+
計算最終優先級(疊加邏輯)
|
| 292 |
+
|
| 293 |
+
Args:
|
| 294 |
+
detected_emphases: 檢測到的強調 {dimension: [weights]}
|
| 295 |
+
detected_rankings: 檢測到的排序 {dimension: rank}
|
| 296 |
+
mentioned_dimensions: 被提及但沒有強調詞的維度
|
| 297 |
+
|
| 298 |
+
Returns:
|
| 299 |
+
Dict[str, float]: 最終優先級分數
|
| 300 |
+
"""
|
| 301 |
+
final_priorities = {}
|
| 302 |
+
|
| 303 |
+
if mentioned_dimensions is None:
|
| 304 |
+
mentioned_dimensions = set()
|
| 305 |
+
|
| 306 |
+
# 合併所有提及的維度(包括強調、排序、和一般提及)
|
| 307 |
+
all_dimensions = set(detected_emphases.keys()) | set(detected_rankings.keys()) | mentioned_dimensions
|
| 308 |
+
|
| 309 |
+
for dimension in all_dimensions:
|
| 310 |
+
emphasis_scores = detected_emphases.get(dimension, [])
|
| 311 |
+
ranking = detected_rankings.get(dimension, 0)
|
| 312 |
+
is_mentioned = dimension in mentioned_dimensions
|
| 313 |
+
|
| 314 |
+
# 計算疊加分數
|
| 315 |
+
if emphasis_scores or ranking > 0:
|
| 316 |
+
# 有強調詞或排序詞
|
| 317 |
+
final_score = self._calculate_stacked_priority(emphasis_scores, ranking)
|
| 318 |
+
elif is_mentioned:
|
| 319 |
+
# 僅被提及(沒有強調詞),給予基本優先級提升
|
| 320 |
+
final_score = 1.3 # 基本提升,讓系統知道這個維度是使用者關心的
|
| 321 |
+
else:
|
| 322 |
+
final_score = 1.0
|
| 323 |
+
|
| 324 |
+
final_priorities[dimension] = final_score
|
| 325 |
+
|
| 326 |
+
return final_priorities
|
| 327 |
+
|
| 328 |
+
def _calculate_stacked_priority(self,
|
| 329 |
+
emphases: List[float],
|
| 330 |
+
ranking: int = 0) -> float:
|
| 331 |
+
"""
|
| 332 |
+
計算疊加後的優先級分數
|
| 333 |
+
|
| 334 |
+
邏輯:
|
| 335 |
+
1. 取最高強調作為基礎
|
| 336 |
+
2. 其他強調提供遞減加成
|
| 337 |
+
3. 排序詞轉換為權重並疊加
|
| 338 |
+
4. 確保不超過絕對上限 2.5
|
| 339 |
+
|
| 340 |
+
Args:
|
| 341 |
+
emphases: 強調權重列表
|
| 342 |
+
ranking: 排序位置 (1=first, 2=second, etc.)
|
| 343 |
+
|
| 344 |
+
Returns:
|
| 345 |
+
float: 最終優先級分數
|
| 346 |
+
"""
|
| 347 |
+
if not emphases and ranking == 0:
|
| 348 |
+
return 1.0
|
| 349 |
+
|
| 350 |
+
# 轉換排序為權重
|
| 351 |
+
ranking_weights = {
|
| 352 |
+
1: 2.0, # first
|
| 353 |
+
2: 1.7, # second
|
| 354 |
+
3: 1.4, # third
|
| 355 |
+
4: 1.2, # fourth
|
| 356 |
+
5: 1.1 # fifth
|
| 357 |
+
}
|
| 358 |
+
ranking_weight = ranking_weights.get(ranking, 0.0)
|
| 359 |
+
|
| 360 |
+
# 合併所有權重
|
| 361 |
+
all_weights = emphases.copy()
|
| 362 |
+
if ranking_weight > 0:
|
| 363 |
+
all_weights.append(ranking_weight)
|
| 364 |
+
|
| 365 |
+
if not all_weights:
|
| 366 |
+
return 1.0
|
| 367 |
+
|
| 368 |
+
# 排序取最高作為基礎
|
| 369 |
+
sorted_weights = sorted(all_weights, reverse=True)
|
| 370 |
+
base_score = sorted_weights[0]
|
| 371 |
+
|
| 372 |
+
# 額外權重提供遞減加成 (reduced stacking bonus)
|
| 373 |
+
bonus = 0.0
|
| 374 |
+
for i, weight in enumerate(sorted_weights[1:], start=1):
|
| 375 |
+
# 遞減加成: 第2個給30%, 第3個給15%, 第4個給7.5% (reduced from 50/25/12.5)
|
| 376 |
+
bonus += (weight - 1.0) * (0.3 / i)
|
| 377 |
+
|
| 378 |
+
final_score = min(base_score + bonus, self.absolute_max_priority)
|
| 379 |
+
return final_score
|
| 380 |
+
|
| 381 |
+
def _calculate_detection_confidence(self,
|
| 382 |
+
detected_emphases: Dict[str, List[float]],
|
| 383 |
+
detected_rankings: Dict[str, int],
|
| 384 |
+
text: str) -> float:
|
| 385 |
+
"""
|
| 386 |
+
計算檢測信心度
|
| 387 |
+
|
| 388 |
+
Args:
|
| 389 |
+
detected_emphases: 檢測到的強調
|
| 390 |
+
detected_rankings: 檢測到的排序
|
| 391 |
+
text: 原始文字
|
| 392 |
+
|
| 393 |
+
Returns:
|
| 394 |
+
float: 信心度 (0-1)
|
| 395 |
+
"""
|
| 396 |
+
confidence = 0.5 # 基礎信心度
|
| 397 |
+
|
| 398 |
+
# 有明確強調 +0.3
|
| 399 |
+
if detected_emphases:
|
| 400 |
+
confidence += 0.3
|
| 401 |
+
|
| 402 |
+
# 有明確排序 +0.2
|
| 403 |
+
if detected_rankings:
|
| 404 |
+
confidence += 0.2
|
| 405 |
+
|
| 406 |
+
# 文字長度適中 +0.1
|
| 407 |
+
word_count = len(text.split())
|
| 408 |
+
if 10 <= word_count <= 100:
|
| 409 |
+
confidence += 0.1
|
| 410 |
+
|
| 411 |
+
return min(1.0, confidence)
|
| 412 |
+
|
| 413 |
+
def get_detection_summary(self, result: PriorityDetectionResult) -> Dict[str, Any]:
|
| 414 |
+
"""
|
| 415 |
+
獲取檢測摘要
|
| 416 |
+
|
| 417 |
+
Args:
|
| 418 |
+
result: 優先級檢測結果
|
| 419 |
+
|
| 420 |
+
Returns:
|
| 421 |
+
Dict[str, Any]: 檢測摘要
|
| 422 |
+
"""
|
| 423 |
+
return {
|
| 424 |
+
'total_dimensions_detected': len(result.dimension_priorities),
|
| 425 |
+
'high_priority_dimensions': [
|
| 426 |
+
dim for dim, score in result.dimension_priorities.items() if score >= 1.5
|
| 427 |
+
],
|
| 428 |
+
'dimension_priorities': result.dimension_priorities,
|
| 429 |
+
'emphases_detected': len(result.detected_emphases),
|
| 430 |
+
'rankings_detected': len(result.detected_rankings),
|
| 431 |
+
'negative_constraints': result.detected_negatives,
|
| 432 |
+
'detection_confidence': result.detection_confidence
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
|
| 436 |
+
def detect_user_priorities(user_input: str) -> PriorityDetectionResult:
|
| 437 |
+
"""
|
| 438 |
+
便利函數: 檢測使用者優先級
|
| 439 |
+
|
| 440 |
+
Args:
|
| 441 |
+
user_input: 使用者輸入
|
| 442 |
+
|
| 443 |
+
Returns:
|
| 444 |
+
PriorityDetectionResult: 檢測結果
|
| 445 |
+
"""
|
| 446 |
+
detector = PriorityDetector()
|
| 447 |
+
return detector.detect_priorities(user_input)
|
| 448 |
+
|
| 449 |
+
|
| 450 |
+
def get_priority_summary(user_input: str) -> Dict[str, Any]:
|
| 451 |
+
"""
|
| 452 |
+
便利函數: 獲取優先級摘要
|
| 453 |
+
|
| 454 |
+
Args:
|
| 455 |
+
user_input: 使用者輸入
|
| 456 |
+
|
| 457 |
+
Returns:
|
| 458 |
+
Dict[str, Any]: 優先級摘要
|
| 459 |
+
"""
|
| 460 |
+
detector = PriorityDetector()
|
| 461 |
+
result = detector.detect_priorities(user_input)
|
| 462 |
+
return detector.get_detection_summary(result)
|
query_understanding.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
import re
|
| 2 |
import json
|
| 3 |
import numpy as np
|
|
@@ -9,6 +10,7 @@ from sentence_transformers import SentenceTransformer
|
|
| 9 |
from dog_database import get_dog_description
|
| 10 |
from breed_health_info import breed_health_info
|
| 11 |
from breed_noise_info import breed_noise_info
|
|
|
|
| 12 |
|
| 13 |
@dataclass
|
| 14 |
class QueryDimensions:
|
|
@@ -19,9 +21,11 @@ class QueryDimensions:
|
|
| 19 |
size_preferences: List[str] = field(default_factory=list)
|
| 20 |
family_context: List[str] = field(default_factory=list)
|
| 21 |
maintenance_level: List[str] = field(default_factory=list)
|
|
|
|
| 22 |
special_requirements: List[str] = field(default_factory=list)
|
| 23 |
breed_mentions: List[str] = field(default_factory=list)
|
| 24 |
confidence_scores: Dict[str, float] = field(default_factory=dict)
|
|
|
|
| 25 |
|
| 26 |
@dataclass
|
| 27 |
class DimensionalSynonyms:
|
|
@@ -47,6 +51,7 @@ class QueryUnderstandingEngine:
|
|
| 47 |
self.breed_list = self._load_breed_list()
|
| 48 |
self.synonyms = self._initialize_synonyms()
|
| 49 |
self.semantic_templates = {}
|
|
|
|
| 50 |
# 延遲SBERT載入直到需要時才在GPU環境中進行
|
| 51 |
print("QueryUnderstandingEngine initialized (SBERT loading deferred)")
|
| 52 |
|
|
@@ -129,9 +134,12 @@ class QueryUnderstandingEngine:
|
|
| 129 |
},
|
| 130 |
family={
|
| 131 |
'children': ['children', 'kids', 'family', 'child-friendly', 'toddler',
|
| 132 |
-
'baby', 'school age'
|
| 133 |
-
|
| 134 |
-
|
|
|
|
|
|
|
|
|
|
| 135 |
},
|
| 136 |
maintenance={
|
| 137 |
'low': ['low maintenance', 'easy care', 'simple', 'minimal grooming',
|
|
@@ -148,7 +156,9 @@ class QueryUnderstandingEngine:
|
|
| 148 |
'hypoallergenic': ['hypoallergenic', 'allergies', 'non-shedding',
|
| 149 |
'allergy-friendly', 'no shed'],
|
| 150 |
'first_time': ['first time', 'beginner', 'new to dogs', 'inexperienced',
|
| 151 |
-
'never owned']
|
|
|
|
|
|
|
| 152 |
}
|
| 153 |
)
|
| 154 |
|
|
@@ -209,7 +219,7 @@ class QueryUnderstandingEngine:
|
|
| 209 |
# 如果 SBERT 可用,進行語義分析增強
|
| 210 |
if self.sbert_model is None:
|
| 211 |
self._initialize_sbert_model()
|
| 212 |
-
|
| 213 |
if self.sbert_model:
|
| 214 |
semantic_dimensions = self._extract_semantic_dimensions(user_input)
|
| 215 |
dimensions = self._merge_dimensions(dimensions, semantic_dimensions)
|
|
@@ -220,6 +230,18 @@ class QueryUnderstandingEngine:
|
|
| 220 |
# 計算信心分數
|
| 221 |
dimensions.confidence_scores = self._calculate_confidence_scores(dimensions, user_input)
|
| 222 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
return dimensions
|
| 224 |
|
| 225 |
except Exception as e:
|
|
@@ -266,6 +288,22 @@ class QueryUnderstandingEngine:
|
|
| 266 |
for requirement, keywords in self.synonyms.special.items():
|
| 267 |
if any(keyword in text for keyword in keywords):
|
| 268 |
dimensions.special_requirements.append(requirement)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
|
| 270 |
return dimensions
|
| 271 |
|
|
@@ -375,6 +413,9 @@ class QueryUnderstandingEngine:
|
|
| 375 |
merged.maintenance_level = list(set(
|
| 376 |
keyword_dims.maintenance_level + semantic_dims.maintenance_level
|
| 377 |
))
|
|
|
|
|
|
|
|
|
|
| 378 |
merged.special_requirements = list(set(
|
| 379 |
keyword_dims.special_requirements + semantic_dims.special_requirements
|
| 380 |
))
|
|
@@ -453,6 +494,7 @@ class QueryUnderstandingEngine:
|
|
| 453 |
])
|
| 454 |
}
|
| 455 |
|
|
|
|
| 456 |
def analyze_user_query(user_input: str) -> QueryDimensions:
|
| 457 |
"""
|
| 458 |
便利函數:分析使用者查詢
|
|
@@ -478,4 +520,4 @@ def get_query_summary(user_input: str) -> Dict[str, Any]:
|
|
| 478 |
"""
|
| 479 |
engine = QueryUnderstandingEngine()
|
| 480 |
dimensions = engine.analyze_query(user_input)
|
| 481 |
-
return engine.get_dimension_summary(dimensions)
|
|
|
|
| 1 |
+
# %%writefile query_understanding.py
|
| 2 |
import re
|
| 3 |
import json
|
| 4 |
import numpy as np
|
|
|
|
| 10 |
from dog_database import get_dog_description
|
| 11 |
from breed_health_info import breed_health_info
|
| 12 |
from breed_noise_info import breed_noise_info
|
| 13 |
+
from priority_detector import PriorityDetector
|
| 14 |
|
| 15 |
@dataclass
|
| 16 |
class QueryDimensions:
|
|
|
|
| 21 |
size_preferences: List[str] = field(default_factory=list)
|
| 22 |
family_context: List[str] = field(default_factory=list)
|
| 23 |
maintenance_level: List[str] = field(default_factory=list)
|
| 24 |
+
experience_level: List[str] = field(default_factory=list) # 用戶經驗等級
|
| 25 |
special_requirements: List[str] = field(default_factory=list)
|
| 26 |
breed_mentions: List[str] = field(default_factory=list)
|
| 27 |
confidence_scores: Dict[str, float] = field(default_factory=dict)
|
| 28 |
+
dimension_priorities: Dict[str, float] = field(default_factory=dict)
|
| 29 |
|
| 30 |
@dataclass
|
| 31 |
class DimensionalSynonyms:
|
|
|
|
| 51 |
self.breed_list = self._load_breed_list()
|
| 52 |
self.synonyms = self._initialize_synonyms()
|
| 53 |
self.semantic_templates = {}
|
| 54 |
+
self.priority_detector = PriorityDetector() # 初始化優先級檢測器
|
| 55 |
# 延遲SBERT載入直到需要時才在GPU環境中進行
|
| 56 |
print("QueryUnderstandingEngine initialized (SBERT loading deferred)")
|
| 57 |
|
|
|
|
| 134 |
},
|
| 135 |
family={
|
| 136 |
'children': ['children', 'kids', 'family', 'child-friendly', 'toddler',
|
| 137 |
+
'baby', 'school age', 'young kids', 'young children',
|
| 138 |
+
'aged 1', 'aged 2', 'aged 3', 'aged 4', 'aged 5',
|
| 139 |
+
'1 year', '2 year', '3 year', '4 year', '5 year',
|
| 140 |
+
'infant', 'preschool'],
|
| 141 |
+
'elderly': ['elderly', 'senior', 'old people', 'retirement', 'aged', 'retired'],
|
| 142 |
+
'single': ['single', 'alone', 'individual', 'solo', 'myself', 'living alone']
|
| 143 |
},
|
| 144 |
maintenance={
|
| 145 |
'low': ['low maintenance', 'easy care', 'simple', 'minimal grooming',
|
|
|
|
| 156 |
'hypoallergenic': ['hypoallergenic', 'allergies', 'non-shedding',
|
| 157 |
'allergy-friendly', 'no shed'],
|
| 158 |
'first_time': ['first time', 'beginner', 'new to dogs', 'inexperienced',
|
| 159 |
+
'never owned'],
|
| 160 |
+
'senior': ['senior', 'elderly', 'retired', 'older person', 'old age',
|
| 161 |
+
'aging', 'older adult', 'golden years', 'retirement']
|
| 162 |
}
|
| 163 |
)
|
| 164 |
|
|
|
|
| 219 |
# 如果 SBERT 可用,進行語義分析增強
|
| 220 |
if self.sbert_model is None:
|
| 221 |
self._initialize_sbert_model()
|
| 222 |
+
|
| 223 |
if self.sbert_model:
|
| 224 |
semantic_dimensions = self._extract_semantic_dimensions(user_input)
|
| 225 |
dimensions = self._merge_dimensions(dimensions, semantic_dimensions)
|
|
|
|
| 230 |
# 計算信心分數
|
| 231 |
dimensions.confidence_scores = self._calculate_confidence_scores(dimensions, user_input)
|
| 232 |
|
| 233 |
+
# **關鍵修復:使用 PriorityDetector 檢測維度優先級**
|
| 234 |
+
priority_result = self.priority_detector.detect_priorities(user_input)
|
| 235 |
+
dimensions.dimension_priorities = priority_result.dimension_priorities
|
| 236 |
+
|
| 237 |
+
# Debug 輸出
|
| 238 |
+
print(f"=== Query Analysis Debug ===")
|
| 239 |
+
print(f" experience_level: {dimensions.experience_level}")
|
| 240 |
+
print(f" maintenance_level: {dimensions.maintenance_level}")
|
| 241 |
+
print(f" spatial_constraints: {dimensions.spatial_constraints}")
|
| 242 |
+
print(f" dimension_priorities: {dimensions.dimension_priorities}")
|
| 243 |
+
print(f"============================")
|
| 244 |
+
|
| 245 |
return dimensions
|
| 246 |
|
| 247 |
except Exception as e:
|
|
|
|
| 288 |
for requirement, keywords in self.synonyms.special.items():
|
| 289 |
if any(keyword in text for keyword in keywords):
|
| 290 |
dimensions.special_requirements.append(requirement)
|
| 291 |
+
# 如果檢測到first_time,同時設置experience_level
|
| 292 |
+
if requirement == 'first_time':
|
| 293 |
+
dimensions.experience_level.append('beginner')
|
| 294 |
+
|
| 295 |
+
# 額外的經驗等級檢測
|
| 296 |
+
experience_keywords = {
|
| 297 |
+
'beginner': ['first dog', 'first time', 'beginner', 'new to dogs', 'inexperienced',
|
| 298 |
+
'never owned', 'never had a dog', 'first-time owner', 'first-time dog owner',
|
| 299 |
+
'first time owner', 'first time dog owner', 'new dog owner', 'new owner'],
|
| 300 |
+
'intermediate': ['some experience', 'had dogs before', 'owned dogs'],
|
| 301 |
+
'advanced': ['experienced', 'expert', 'professional', 'breeder', 'dog trainer']
|
| 302 |
+
}
|
| 303 |
+
for level, keywords in experience_keywords.items():
|
| 304 |
+
if any(keyword in text for keyword in keywords):
|
| 305 |
+
if level not in dimensions.experience_level:
|
| 306 |
+
dimensions.experience_level.append(level)
|
| 307 |
|
| 308 |
return dimensions
|
| 309 |
|
|
|
|
| 413 |
merged.maintenance_level = list(set(
|
| 414 |
keyword_dims.maintenance_level + semantic_dims.maintenance_level
|
| 415 |
))
|
| 416 |
+
merged.experience_level = list(set(
|
| 417 |
+
keyword_dims.experience_level + semantic_dims.experience_level
|
| 418 |
+
))
|
| 419 |
merged.special_requirements = list(set(
|
| 420 |
keyword_dims.special_requirements + semantic_dims.special_requirements
|
| 421 |
))
|
|
|
|
| 494 |
])
|
| 495 |
}
|
| 496 |
|
| 497 |
+
# 便利函數
|
| 498 |
def analyze_user_query(user_input: str) -> QueryDimensions:
|
| 499 |
"""
|
| 500 |
便利函數:分析使用者查詢
|
|
|
|
| 520 |
"""
|
| 521 |
engine = QueryUnderstandingEngine()
|
| 522 |
dimensions = engine.analyze_query(user_input)
|
| 523 |
+
return engine.get_dimension_summary(dimensions)
|
recommendation_css.py
ADDED
|
@@ -0,0 +1,687 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# recommendation_css.py
|
| 2 |
+
"""
|
| 3 |
+
CSS 樣式模組 - 專門處理推薦結果的 CSS 樣式
|
| 4 |
+
將所有 CSS 定義集中管理,提高可維護性
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
# Description Search (Find by Description) 模式的 CSS 樣式
|
| 8 |
+
DESCRIPTION_SEARCH_CSS = """
|
| 9 |
+
<style>
|
| 10 |
+
.recommendations-container {
|
| 11 |
+
display: flex;
|
| 12 |
+
flex-direction: column;
|
| 13 |
+
gap: 20px;
|
| 14 |
+
padding: 20px;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
.breed-card {
|
| 18 |
+
border: 2px solid #e5e7eb;
|
| 19 |
+
border-radius: 12px;
|
| 20 |
+
padding: 20px;
|
| 21 |
+
background: white;
|
| 22 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 23 |
+
transition: all 0.3s ease;
|
| 24 |
+
position: relative;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
.breed-card:hover {
|
| 28 |
+
box-shadow: 0 8px 12px rgba(0, 0, 0, 0.15);
|
| 29 |
+
transform: translateY(-2px);
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
.rank-badge {
|
| 33 |
+
position: absolute;
|
| 34 |
+
top: 15px;
|
| 35 |
+
left: 15px;
|
| 36 |
+
padding: 8px 14px;
|
| 37 |
+
border-radius: 8px;
|
| 38 |
+
font-weight: 800;
|
| 39 |
+
font-size: 20px;
|
| 40 |
+
min-width: 45px;
|
| 41 |
+
text-align: center;
|
| 42 |
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
.rank-1 {
|
| 46 |
+
background: linear-gradient(135deg, #FEF3C7 0%, #FDE68A 50%, #F59E0B 100%);
|
| 47 |
+
color: #92400E;
|
| 48 |
+
font-size: 32px;
|
| 49 |
+
font-weight: 900;
|
| 50 |
+
animation: pulse 2s infinite;
|
| 51 |
+
border: 3px solid rgba(251, 191, 36, 0.4);
|
| 52 |
+
box-shadow: 0 6px 20px rgba(245, 158, 11, 0.3);
|
| 53 |
+
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.8);
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
.rank-2 {
|
| 57 |
+
background: linear-gradient(135deg, #F1F5F9 0%, #E2E8F0 50%, #94A3B8 100%);
|
| 58 |
+
color: #475569;
|
| 59 |
+
font-size: 30px;
|
| 60 |
+
font-weight: 800;
|
| 61 |
+
border: 3px solid rgba(148, 163, 184, 0.4);
|
| 62 |
+
box-shadow: 0 5px 15px rgba(148, 163, 184, 0.3);
|
| 63 |
+
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.8);
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
.rank-3 {
|
| 67 |
+
background: linear-gradient(135deg, #FEF2F2 0%, #FED7AA 50%, #FB923C 100%);
|
| 68 |
+
color: #9A3412;
|
| 69 |
+
font-size: 28px;
|
| 70 |
+
font-weight: 800;
|
| 71 |
+
border: 3px solid rgba(251, 146, 60, 0.4);
|
| 72 |
+
box-shadow: 0 4px 12px rgba(251, 146, 60, 0.3);
|
| 73 |
+
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.8);
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
.rank-other {
|
| 77 |
+
background: linear-gradient(135deg, #F8FAFC 0%, #E2E8F0 50%, #CBD5E1 100%);
|
| 78 |
+
color: #475569;
|
| 79 |
+
font-size: 26px;
|
| 80 |
+
font-weight: 700;
|
| 81 |
+
border: 2px solid rgba(203, 213, 225, 0.6);
|
| 82 |
+
box-shadow: 0 3px 8px rgba(203, 213, 225, 0.4);
|
| 83 |
+
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.8);
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
@keyframes pulse {
|
| 87 |
+
0% {
|
| 88 |
+
box-shadow: 0 6px 20px rgba(245, 158, 11, 0.3);
|
| 89 |
+
transform: scale(1);
|
| 90 |
+
}
|
| 91 |
+
50% {
|
| 92 |
+
box-shadow: 0 8px 25px rgba(245, 158, 11, 0.5), 0 0 0 4px rgba(245, 158, 11, 0.15);
|
| 93 |
+
transform: scale(1.05);
|
| 94 |
+
}
|
| 95 |
+
100% {
|
| 96 |
+
box-shadow: 0 6px 20px rgba(245, 158, 11, 0.3);
|
| 97 |
+
transform: scale(1);
|
| 98 |
+
}
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.breed-header {
|
| 102 |
+
display: flex;
|
| 103 |
+
justify-content: space-between;
|
| 104 |
+
align-items: center;
|
| 105 |
+
margin-bottom: 15px;
|
| 106 |
+
padding-left: 70px;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
.breed-name {
|
| 110 |
+
font-size: 26px;
|
| 111 |
+
font-weight: 800;
|
| 112 |
+
color: #1F2937;
|
| 113 |
+
margin: 0;
|
| 114 |
+
letter-spacing: -0.025em;
|
| 115 |
+
line-height: 1.2;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
.match-score {
|
| 119 |
+
display: flex;
|
| 120 |
+
flex-direction: column;
|
| 121 |
+
align-items: flex-end;
|
| 122 |
+
padding: 12px 16px;
|
| 123 |
+
background: linear-gradient(135deg, #F0F9FF 0%, #E0F2FE 100%);
|
| 124 |
+
border-radius: 12px;
|
| 125 |
+
border: 2px solid rgba(6, 182, 212, 0.2);
|
| 126 |
+
box-shadow: 0 4px 12px rgba(6, 182, 212, 0.1);
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
.match-percentage {
|
| 130 |
+
font-size: 48px;
|
| 131 |
+
font-weight: 900;
|
| 132 |
+
margin-bottom: 8px;
|
| 133 |
+
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
| 134 |
+
line-height: 1;
|
| 135 |
+
letter-spacing: -0.02em;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
.match-label {
|
| 139 |
+
font-size: 12px;
|
| 140 |
+
text-transform: uppercase;
|
| 141 |
+
letter-spacing: 2px;
|
| 142 |
+
opacity: 0.9;
|
| 143 |
+
font-weight: 800;
|
| 144 |
+
margin-bottom: 6px;
|
| 145 |
+
color: #0369A1;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
.score-excellent { color: #22C55E; }
|
| 149 |
+
.score-good { color: #F59E0B; }
|
| 150 |
+
.score-moderate { color: #6B7280; }
|
| 151 |
+
|
| 152 |
+
.score-bar {
|
| 153 |
+
width: 220px;
|
| 154 |
+
height: 14px;
|
| 155 |
+
background: rgba(226, 232, 240, 0.8);
|
| 156 |
+
border-radius: 8px;
|
| 157 |
+
overflow: hidden;
|
| 158 |
+
margin-top: 8px;
|
| 159 |
+
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
|
| 160 |
+
border: 1px solid rgba(6, 182, 212, 0.2);
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
.score-fill {
|
| 164 |
+
height: 100%;
|
| 165 |
+
border-radius: 4px;
|
| 166 |
+
transition: width 1s ease;
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
.fill-excellent { background: linear-gradient(90deg, #22C55E, #16A34A); }
|
| 170 |
+
.fill-good { background: linear-gradient(90deg, #F59E0B, #DC2626); }
|
| 171 |
+
.fill-moderate { background: linear-gradient(90deg, #6B7280, #4B5563); }
|
| 172 |
+
|
| 173 |
+
/* Tooltip styles for Find by Description */
|
| 174 |
+
.tooltip {
|
| 175 |
+
position: relative;
|
| 176 |
+
display: inline-block;
|
| 177 |
+
cursor: help;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
.tooltip-icon {
|
| 181 |
+
display: inline-block;
|
| 182 |
+
width: 18px;
|
| 183 |
+
height: 18px;
|
| 184 |
+
background: linear-gradient(135deg, #06b6d4, #0891b2);
|
| 185 |
+
color: white;
|
| 186 |
+
border-radius: 50%;
|
| 187 |
+
text-align: center;
|
| 188 |
+
line-height: 18px;
|
| 189 |
+
font-size: 12px;
|
| 190 |
+
font-weight: bold;
|
| 191 |
+
margin-left: 8px;
|
| 192 |
+
cursor: help;
|
| 193 |
+
box-shadow: 0 2px 4px rgba(6, 182, 212, 0.3);
|
| 194 |
+
transition: all 0.2s ease;
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
.tooltip-icon:hover {
|
| 198 |
+
background: linear-gradient(135deg, #0891b2, #0e7490);
|
| 199 |
+
transform: scale(1.1);
|
| 200 |
+
box-shadow: 0 3px 6px rgba(6, 182, 212, 0.4);
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
.tooltip-text {
|
| 204 |
+
visibility: hidden;
|
| 205 |
+
width: 320px;
|
| 206 |
+
background: linear-gradient(145deg, #1e293b, #334155);
|
| 207 |
+
color: #f1f5f9;
|
| 208 |
+
text-align: left;
|
| 209 |
+
border-radius: 12px;
|
| 210 |
+
padding: 16px;
|
| 211 |
+
position: absolute;
|
| 212 |
+
z-index: 1000;
|
| 213 |
+
bottom: 125%;
|
| 214 |
+
left: 50%;
|
| 215 |
+
margin-left: -160px;
|
| 216 |
+
opacity: 0;
|
| 217 |
+
transition: opacity 0.3s ease, transform 0.3s ease;
|
| 218 |
+
transform: translateY(10px);
|
| 219 |
+
font-size: 14px;
|
| 220 |
+
line-height: 1.5;
|
| 221 |
+
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
|
| 222 |
+
border: 1px solid rgba(148, 163, 184, 0.2);
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
.tooltip-text::after {
|
| 226 |
+
content: "";
|
| 227 |
+
position: absolute;
|
| 228 |
+
top: 100%;
|
| 229 |
+
left: 50%;
|
| 230 |
+
margin-left: -8px;
|
| 231 |
+
border-width: 8px;
|
| 232 |
+
border-style: solid;
|
| 233 |
+
border-color: #334155 transparent transparent transparent;
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
.tooltip:hover .tooltip-text {
|
| 237 |
+
visibility: visible;
|
| 238 |
+
opacity: 1;
|
| 239 |
+
transform: translateY(0);
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
.tooltip-text strong {
|
| 243 |
+
color: #06b6d4;
|
| 244 |
+
font-weight: 700;
|
| 245 |
+
display: block;
|
| 246 |
+
margin-bottom: 8px;
|
| 247 |
+
font-size: 15px;
|
| 248 |
+
}
|
| 249 |
+
</style>
|
| 250 |
+
"""
|
| 251 |
+
|
| 252 |
+
# Criteria Search (Find by Criteria) 模式的 CSS 樣式
|
| 253 |
+
CRITERIA_SEARCH_CSS = """
|
| 254 |
+
<style>
|
| 255 |
+
.recommendations-container {
|
| 256 |
+
display: flex;
|
| 257 |
+
flex-direction: column;
|
| 258 |
+
gap: 15px;
|
| 259 |
+
padding: 15px;
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
.breed-card {
|
| 263 |
+
border: 1px solid #d1d5db;
|
| 264 |
+
border-radius: 8px;
|
| 265 |
+
padding: 16px;
|
| 266 |
+
background: #ffffff;
|
| 267 |
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
| 268 |
+
transition: all 0.2s ease;
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
.breed-card:hover {
|
| 272 |
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
| 273 |
+
transform: translateY(-1px);
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
.breed-header {
|
| 277 |
+
display: flex;
|
| 278 |
+
justify-content: space-between;
|
| 279 |
+
align-items: center;
|
| 280 |
+
margin-bottom: 12px;
|
| 281 |
+
padding-bottom: 8px;
|
| 282 |
+
border-bottom: 1px solid #f3f4f6;
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
.breed-title {
|
| 286 |
+
display: flex;
|
| 287 |
+
align-items: center;
|
| 288 |
+
gap: 16px;
|
| 289 |
+
justify-content: flex-start;
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
.trophy-rank {
|
| 293 |
+
font-size: 24px;
|
| 294 |
+
font-weight: 800;
|
| 295 |
+
color: #1f2937;
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
.breed-name {
|
| 299 |
+
font-size: 42px;
|
| 300 |
+
font-weight: 900;
|
| 301 |
+
color: #1f2937;
|
| 302 |
+
margin: 0;
|
| 303 |
+
padding: 8px 16px;
|
| 304 |
+
background: linear-gradient(135deg, #D1FAE5 0%, #A7F3D0 100%);
|
| 305 |
+
border: 2px solid #22C55E;
|
| 306 |
+
border-radius: 12px;
|
| 307 |
+
display: inline-block;
|
| 308 |
+
box-shadow: 0 2px 8px rgba(34, 197, 94, 0.2);
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
.overall-score {
|
| 312 |
+
display: flex;
|
| 313 |
+
flex-direction: column;
|
| 314 |
+
align-items: flex-end;
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
.score-percentage {
|
| 318 |
+
font-size: 32px;
|
| 319 |
+
font-weight: 900;
|
| 320 |
+
margin-bottom: 4px;
|
| 321 |
+
line-height: 1;
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
.score-label {
|
| 325 |
+
font-size: 10px;
|
| 326 |
+
text-transform: uppercase;
|
| 327 |
+
letter-spacing: 1px;
|
| 328 |
+
opacity: 0.7;
|
| 329 |
+
font-weight: 600;
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
.score-bar-wide {
|
| 333 |
+
width: 200px;
|
| 334 |
+
height: 8px;
|
| 335 |
+
background: #f3f4f6;
|
| 336 |
+
border-radius: 4px;
|
| 337 |
+
overflow: hidden;
|
| 338 |
+
margin-top: 6px;
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
.score-fill-wide {
|
| 342 |
+
height: 100%;
|
| 343 |
+
border-radius: 4px;
|
| 344 |
+
transition: width 0.8s ease;
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
.score-excellent { color: #22C55E; }
|
| 348 |
+
.score-good { color: #65a30d; }
|
| 349 |
+
.score-moderate { color: #d4a332; }
|
| 350 |
+
.score-fair { color: #e67e22; }
|
| 351 |
+
.score-poor { color: #e74c3c; }
|
| 352 |
+
|
| 353 |
+
.fill-excellent { background: #22C55E; }
|
| 354 |
+
.fill-good { background: #65a30d; }
|
| 355 |
+
.fill-moderate { background: #d4a332; }
|
| 356 |
+
.fill-fair { background: #e67e22; }
|
| 357 |
+
.fill-poor { background: #e74c3c; }
|
| 358 |
+
|
| 359 |
+
.breed-details {
|
| 360 |
+
margin-top: 12px;
|
| 361 |
+
padding-top: 12px;
|
| 362 |
+
border-top: 1px solid #e5e7eb;
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
/* 通用樣式(兩個模式都需要) */
|
| 366 |
+
.progress {
|
| 367 |
+
transition: all 0.3s ease-in-out;
|
| 368 |
+
border-radius: 4px;
|
| 369 |
+
height: 12px;
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
.progress-bar {
|
| 373 |
+
background-color: #f5f5f5;
|
| 374 |
+
border-radius: 4px;
|
| 375 |
+
overflow: hidden;
|
| 376 |
+
position: relative;
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
.score-item {
|
| 380 |
+
margin: 10px 0;
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
.percentage {
|
| 384 |
+
margin-left: 8px;
|
| 385 |
+
font-weight: 500;
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
/* White Tooltip Styles */
|
| 389 |
+
.tooltip {
|
| 390 |
+
position: relative;
|
| 391 |
+
display: inline-flex;
|
| 392 |
+
align-items: center;
|
| 393 |
+
gap: 4px;
|
| 394 |
+
cursor: help;
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
.tooltip .tooltip-icon {
|
| 398 |
+
font-size: 14px;
|
| 399 |
+
color: #666;
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
.tooltip .tooltip-text {
|
| 403 |
+
visibility: hidden;
|
| 404 |
+
width: 250px;
|
| 405 |
+
background-color: rgba(44, 62, 80, 0.95);
|
| 406 |
+
color: white;
|
| 407 |
+
text-align: left;
|
| 408 |
+
border-radius: 8px;
|
| 409 |
+
padding: 8px 10px;
|
| 410 |
+
position: absolute;
|
| 411 |
+
z-index: 100;
|
| 412 |
+
bottom: 150%;
|
| 413 |
+
left: 50%;
|
| 414 |
+
transform: translateX(-50%);
|
| 415 |
+
opacity: 0;
|
| 416 |
+
transition: all 0.3s ease;
|
| 417 |
+
font-size: 14px;
|
| 418 |
+
line-height: 1.3;
|
| 419 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
|
| 420 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 421 |
+
margin-bottom: 10px;
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
.tooltip:hover .tooltip-text {
|
| 425 |
+
visibility: visible;
|
| 426 |
+
opacity: 1;
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
.tooltip .tooltip-text::after {
|
| 430 |
+
content: "";
|
| 431 |
+
position: absolute;
|
| 432 |
+
top: 100%;
|
| 433 |
+
left: 50%;
|
| 434 |
+
transform: translateX(-50%);
|
| 435 |
+
border-width: 8px;
|
| 436 |
+
border-style: solid;
|
| 437 |
+
border-color: rgba(44, 62, 80, 0.95) transparent transparent transparent;
|
| 438 |
+
}
|
| 439 |
+
</style>
|
| 440 |
+
"""
|
| 441 |
+
|
| 442 |
+
# Unified Recommendations (統一推薦結果) 的 CSS 樣式
|
| 443 |
+
UNIFIED_CSS = """
|
| 444 |
+
<style>
|
| 445 |
+
.unified-recommendations {
|
| 446 |
+
max-width: 1200px;
|
| 447 |
+
margin: 0 auto;
|
| 448 |
+
padding: 20px;
|
| 449 |
+
}
|
| 450 |
+
|
| 451 |
+
.unified-breed-card {
|
| 452 |
+
background: linear-gradient(145deg, #ffffff 0%, #f8fafc 100%);
|
| 453 |
+
border-radius: 16px;
|
| 454 |
+
padding: 24px;
|
| 455 |
+
margin-bottom: 20px;
|
| 456 |
+
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
| 457 |
+
border: 1px solid #e2e8f0;
|
| 458 |
+
transition: all 0.3s ease;
|
| 459 |
+
}
|
| 460 |
+
|
| 461 |
+
.unified-breed-card:hover {
|
| 462 |
+
transform: translateY(-2px);
|
| 463 |
+
box-shadow: 0 12px 35px rgba(0, 0, 0, 0.15);
|
| 464 |
+
}
|
| 465 |
+
|
| 466 |
+
.unified-breed-header {
|
| 467 |
+
margin-bottom: 20px;
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
.unified-rank-section {
|
| 471 |
+
display: flex;
|
| 472 |
+
align-items: center;
|
| 473 |
+
gap: 15px;
|
| 474 |
+
}
|
| 475 |
+
|
| 476 |
+
.unified-rank-badge {
|
| 477 |
+
background: linear-gradient(135deg, #E0F2FE 0%, #BAE6FD 100%);
|
| 478 |
+
color: #0C4A6E;
|
| 479 |
+
padding: 8px 16px;
|
| 480 |
+
border-radius: 8px;
|
| 481 |
+
font-weight: 900;
|
| 482 |
+
font-size: 24px;
|
| 483 |
+
box-shadow: 0 2px 8px rgba(14, 165, 233, 0.2);
|
| 484 |
+
border: 2px solid #0EA5E9;
|
| 485 |
+
display: inline-block;
|
| 486 |
+
min-width: 80px;
|
| 487 |
+
text-align: center;
|
| 488 |
+
}
|
| 489 |
+
|
| 490 |
+
.unified-breed-info {
|
| 491 |
+
flex-grow: 1;
|
| 492 |
+
}
|
| 493 |
+
|
| 494 |
+
.unified-breed-title {
|
| 495 |
+
font-size: 24px;
|
| 496 |
+
font-weight: 800;
|
| 497 |
+
color: #0C4A6E;
|
| 498 |
+
margin: 0;
|
| 499 |
+
letter-spacing: -0.02em;
|
| 500 |
+
background: linear-gradient(135deg, #F0F9FF, #E0F2FE);
|
| 501 |
+
padding: 12px 20px;
|
| 502 |
+
border-radius: 10px;
|
| 503 |
+
border: 2px solid #0EA5E9;
|
| 504 |
+
display: inline-block;
|
| 505 |
+
box-shadow: 0 2px 8px rgba(14, 165, 233, 0.1);
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
.unified-match-score {
|
| 509 |
+
font-size: 24px;
|
| 510 |
+
font-weight: 900;
|
| 511 |
+
color: #0F5132;
|
| 512 |
+
background: linear-gradient(135deg, #D1FAE5, #A7F3D0);
|
| 513 |
+
padding: 12px 20px;
|
| 514 |
+
border-radius: 10px;
|
| 515 |
+
display: inline-block;
|
| 516 |
+
text-align: center;
|
| 517 |
+
border: 2px solid #22C55E;
|
| 518 |
+
box-shadow: 0 2px 8px rgba(34, 197, 94, 0.2);
|
| 519 |
+
margin: 0;
|
| 520 |
+
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.5);
|
| 521 |
+
letter-spacing: -0.02em;
|
| 522 |
+
}
|
| 523 |
+
|
| 524 |
+
.unified-overall-section {
|
| 525 |
+
background: linear-gradient(135deg, #f0f9ff, #ecfeff);
|
| 526 |
+
border-radius: 12px;
|
| 527 |
+
padding: 20px;
|
| 528 |
+
margin-bottom: 24px;
|
| 529 |
+
border: 2px solid #06b6d4;
|
| 530 |
+
box-shadow: 0 4px 12px rgba(6, 182, 212, 0.1);
|
| 531 |
+
}
|
| 532 |
+
|
| 533 |
+
.unified-dimension-grid {
|
| 534 |
+
display: grid;
|
| 535 |
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
| 536 |
+
gap: 16px;
|
| 537 |
+
margin-bottom: 24px;
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
+
.unified-dimension-item {
|
| 541 |
+
background: white;
|
| 542 |
+
padding: 16px;
|
| 543 |
+
border-radius: 10px;
|
| 544 |
+
border: 1px solid #e2e8f0;
|
| 545 |
+
transition: all 0.2s ease;
|
| 546 |
+
}
|
| 547 |
+
|
| 548 |
+
.unified-dimension-item:hover {
|
| 549 |
+
background: #f8fafc;
|
| 550 |
+
border-color: #cbd5e1;
|
| 551 |
+
}
|
| 552 |
+
|
| 553 |
+
.unified-breed-info {
|
| 554 |
+
display: grid;
|
| 555 |
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
| 556 |
+
gap: 12px;
|
| 557 |
+
margin: 20px 0;
|
| 558 |
+
}
|
| 559 |
+
|
| 560 |
+
.unified-info-item {
|
| 561 |
+
background: #f8fafc;
|
| 562 |
+
padding: 12px;
|
| 563 |
+
border-radius: 8px;
|
| 564 |
+
border-left: 4px solid #6366f1;
|
| 565 |
+
}
|
| 566 |
+
|
| 567 |
+
.unified-info-label {
|
| 568 |
+
font-weight: 600;
|
| 569 |
+
color: #4b5563;
|
| 570 |
+
font-size: 0.85em;
|
| 571 |
+
margin-bottom: 4px;
|
| 572 |
+
}
|
| 573 |
+
|
| 574 |
+
.unified-info-value {
|
| 575 |
+
color: #1f2937;
|
| 576 |
+
font-weight: 500;
|
| 577 |
+
}
|
| 578 |
+
|
| 579 |
+
/* Tooltip styles for unified recommendations */
|
| 580 |
+
.tooltip {
|
| 581 |
+
position: relative;
|
| 582 |
+
display: inline-block;
|
| 583 |
+
cursor: help;
|
| 584 |
+
}
|
| 585 |
+
|
| 586 |
+
.tooltip-icon {
|
| 587 |
+
display: inline-block;
|
| 588 |
+
width: 18px;
|
| 589 |
+
height: 18px;
|
| 590 |
+
background: linear-gradient(135deg, #06b6d4, #0891b2);
|
| 591 |
+
color: white;
|
| 592 |
+
border-radius: 50%;
|
| 593 |
+
text-align: center;
|
| 594 |
+
line-height: 18px;
|
| 595 |
+
font-size: 12px;
|
| 596 |
+
font-weight: bold;
|
| 597 |
+
margin-left: 8px;
|
| 598 |
+
cursor: help;
|
| 599 |
+
box-shadow: 0 2px 4px rgba(6, 182, 212, 0.3);
|
| 600 |
+
transition: all 0.2s ease;
|
| 601 |
+
}
|
| 602 |
+
|
| 603 |
+
.tooltip-icon:hover {
|
| 604 |
+
background: linear-gradient(135deg, #0891b2, #0e7490);
|
| 605 |
+
transform: scale(1.1);
|
| 606 |
+
box-shadow: 0 3px 6px rgba(6, 182, 212, 0.4);
|
| 607 |
+
}
|
| 608 |
+
|
| 609 |
+
.tooltip-text {
|
| 610 |
+
visibility: hidden;
|
| 611 |
+
width: 320px;
|
| 612 |
+
background: linear-gradient(145deg, #1e293b, #334155);
|
| 613 |
+
color: #f1f5f9;
|
| 614 |
+
text-align: left;
|
| 615 |
+
border-radius: 12px;
|
| 616 |
+
padding: 16px;
|
| 617 |
+
position: absolute;
|
| 618 |
+
z-index: 1000;
|
| 619 |
+
bottom: 125%;
|
| 620 |
+
left: 50%;
|
| 621 |
+
margin-left: -160px;
|
| 622 |
+
opacity: 0;
|
| 623 |
+
transition: opacity 0.3s ease, transform 0.3s ease;
|
| 624 |
+
transform: translateY(10px);
|
| 625 |
+
font-size: 14px;
|
| 626 |
+
line-height: 1.5;
|
| 627 |
+
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
|
| 628 |
+
border: 1px solid rgba(148, 163, 184, 0.2);
|
| 629 |
+
}
|
| 630 |
+
|
| 631 |
+
.tooltip-text::after {
|
| 632 |
+
content: "";
|
| 633 |
+
position: absolute;
|
| 634 |
+
top: 100%;
|
| 635 |
+
left: 50%;
|
| 636 |
+
margin-left: -8px;
|
| 637 |
+
border-width: 8px;
|
| 638 |
+
border-style: solid;
|
| 639 |
+
border-color: #334155 transparent transparent transparent;
|
| 640 |
+
}
|
| 641 |
+
|
| 642 |
+
.tooltip:hover .tooltip-text {
|
| 643 |
+
visibility: visible;
|
| 644 |
+
opacity: 1;
|
| 645 |
+
transform: translateY(0);
|
| 646 |
+
}
|
| 647 |
+
|
| 648 |
+
.tooltip-text strong {
|
| 649 |
+
color: #06b6d4;
|
| 650 |
+
font-weight: 700;
|
| 651 |
+
display: block;
|
| 652 |
+
margin-bottom: 8px;
|
| 653 |
+
font-size: 15px;
|
| 654 |
+
}
|
| 655 |
+
|
| 656 |
+
.akc-button:hover {
|
| 657 |
+
transform: translateY(-2px) !important;
|
| 658 |
+
box-shadow: 0 6px 16px rgba(59, 130, 246, 0.5) !important;
|
| 659 |
+
}
|
| 660 |
+
</style>
|
| 661 |
+
"""
|
| 662 |
+
|
| 663 |
+
|
| 664 |
+
def get_recommendation_css_styles(is_description_search: bool) -> str:
|
| 665 |
+
"""
|
| 666 |
+
根據搜尋類型返回對應的推薦結果 CSS 樣式
|
| 667 |
+
|
| 668 |
+
Args:
|
| 669 |
+
is_description_search: 是否為 Description Search 模式
|
| 670 |
+
|
| 671 |
+
Returns:
|
| 672 |
+
str: 對應的 CSS 樣式字串
|
| 673 |
+
"""
|
| 674 |
+
if is_description_search:
|
| 675 |
+
return DESCRIPTION_SEARCH_CSS
|
| 676 |
+
else:
|
| 677 |
+
return CRITERIA_SEARCH_CSS
|
| 678 |
+
|
| 679 |
+
|
| 680 |
+
def get_unified_css() -> str:
|
| 681 |
+
"""
|
| 682 |
+
獲取統一推薦結果的 CSS 樣式
|
| 683 |
+
|
| 684 |
+
Returns:
|
| 685 |
+
str: 統一推薦 CSS 樣式字串
|
| 686 |
+
"""
|
| 687 |
+
return UNIFIED_CSS
|
recommendation_formatter.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
import sqlite3
|
| 2 |
import traceback
|
| 3 |
import random
|
|
@@ -293,33 +294,143 @@ def parse_health_information(health_info: dict) -> tuple:
|
|
| 293 |
return health_considerations, health_screenings
|
| 294 |
|
| 295 |
|
| 296 |
-
def generate_dimension_scores_for_display(base_score: float, rank: int, breed: str,
|
| 297 |
semantic_score: float = 0.7,
|
| 298 |
comparative_bonus: float = 0.0,
|
| 299 |
lifestyle_bonus: float = 0.0,
|
| 300 |
-
is_description_search: bool = False
|
| 301 |
-
|
| 302 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
|
| 304 |
if is_description_search:
|
| 305 |
-
#
|
| 306 |
-
|
| 307 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 308 |
scores = {
|
| 309 |
-
'space': max(0.
|
| 310 |
-
|
| 311 |
-
'
|
| 312 |
-
|
| 313 |
-
'
|
| 314 |
-
|
| 315 |
-
'experience': max(0.50, min(0.95,
|
| 316 |
-
base_score * 0.87 + (lifestyle_bonus * 0.3) + random.uniform(-score_variance, score_variance))),
|
| 317 |
-
'noise': max(0.50, min(0.95,
|
| 318 |
-
base_score * 0.83 + (lifestyle_bonus * 0.6) + random.uniform(-score_variance, score_variance))),
|
| 319 |
'overall': base_score
|
| 320 |
}
|
| 321 |
else:
|
| 322 |
# 傳統搜尋結果的分數結構會在呼叫處理中傳入
|
| 323 |
scores = {'overall': base_score}
|
| 324 |
-
|
| 325 |
return scores
|
|
|
|
| 1 |
+
# %%writefile recommendation_formatter.py
|
| 2 |
import sqlite3
|
| 3 |
import traceback
|
| 4 |
import random
|
|
|
|
| 294 |
return health_considerations, health_screenings
|
| 295 |
|
| 296 |
|
| 297 |
+
def generate_dimension_scores_for_display(base_score: float, rank: int, breed: str,
|
| 298 |
semantic_score: float = 0.7,
|
| 299 |
comparative_bonus: float = 0.0,
|
| 300 |
lifestyle_bonus: float = 0.0,
|
| 301 |
+
is_description_search: bool = False,
|
| 302 |
+
user_input: str = "") -> dict:
|
| 303 |
+
"""
|
| 304 |
+
為顯示生成維度分數 - 基於真實品種特性
|
| 305 |
+
|
| 306 |
+
這個函數現在會考慮品種的實際特性來計算分數,
|
| 307 |
+
而不是僅僅基於總分生成假分數。
|
| 308 |
+
"""
|
| 309 |
+
from dog_database import get_dog_description
|
| 310 |
+
|
| 311 |
+
# 獲取品種資訊
|
| 312 |
+
breed_name = breed.replace(' ', '_')
|
| 313 |
+
breed_info = get_dog_description(breed_name) or {}
|
| 314 |
+
|
| 315 |
+
temperament = breed_info.get('Temperament', '').lower()
|
| 316 |
+
size = breed_info.get('Size', 'Medium').lower()
|
| 317 |
+
exercise_needs = breed_info.get('Exercise Needs', 'Moderate').lower()
|
| 318 |
+
grooming_needs = breed_info.get('Grooming Needs', 'Moderate').lower()
|
| 319 |
+
good_with_children = breed_info.get('Good with Children', 'Yes')
|
| 320 |
+
care_level = breed_info.get('Care Level', 'Moderate').lower()
|
| 321 |
+
|
| 322 |
+
user_text = user_input.lower() if user_input else ""
|
| 323 |
|
| 324 |
if is_description_search:
|
| 325 |
+
# === 真實維度評分 ===
|
| 326 |
+
|
| 327 |
+
# 1. Space Compatibility
|
| 328 |
+
space_score = 0.75
|
| 329 |
+
if 'small' in size:
|
| 330 |
+
space_score = 0.85
|
| 331 |
+
elif 'medium' in size:
|
| 332 |
+
space_score = 0.75
|
| 333 |
+
elif 'large' in size:
|
| 334 |
+
space_score = 0.65
|
| 335 |
+
elif 'giant' in size:
|
| 336 |
+
space_score = 0.55
|
| 337 |
+
|
| 338 |
+
# 2. Exercise Compatibility
|
| 339 |
+
exercise_score = 0.75
|
| 340 |
+
if 'low' in exercise_needs:
|
| 341 |
+
exercise_score = 0.85
|
| 342 |
+
elif 'moderate' in exercise_needs:
|
| 343 |
+
exercise_score = 0.75
|
| 344 |
+
elif 'high' in exercise_needs:
|
| 345 |
+
exercise_score = 0.60
|
| 346 |
+
elif 'very high' in exercise_needs:
|
| 347 |
+
exercise_score = 0.50
|
| 348 |
+
|
| 349 |
+
# 3. Grooming/Maintenance
|
| 350 |
+
grooming_score = 0.75
|
| 351 |
+
if 'low' in grooming_needs:
|
| 352 |
+
grooming_score = 0.85
|
| 353 |
+
elif 'moderate' in grooming_needs:
|
| 354 |
+
grooming_score = 0.70
|
| 355 |
+
elif 'high' in grooming_needs:
|
| 356 |
+
grooming_score = 0.55
|
| 357 |
+
|
| 358 |
+
# 敏感品種需要額外照顧
|
| 359 |
+
if 'sensitive' in temperament:
|
| 360 |
+
grooming_score -= 0.10
|
| 361 |
+
|
| 362 |
+
# 4. Experience Compatibility - 關鍵!
|
| 363 |
+
# DEBUG: 如果這個函數被呼叫,experience 會基於真實品種特性計算
|
| 364 |
+
experience_score = 0.70
|
| 365 |
+
|
| 366 |
+
# 基於 care level 的基礎分數
|
| 367 |
+
if 'low' in care_level:
|
| 368 |
+
experience_score = 0.85
|
| 369 |
+
elif 'moderate' in care_level:
|
| 370 |
+
experience_score = 0.70
|
| 371 |
+
elif 'high' in care_level:
|
| 372 |
+
experience_score = 0.55
|
| 373 |
+
|
| 374 |
+
# DEBUG 標記:打印正在計算的品種
|
| 375 |
+
print(f"[REAL_SCORE] Calculating experience for {breed}: care_level={care_level}, temperament={temperament}")
|
| 376 |
+
|
| 377 |
+
# 性格對經驗的影響
|
| 378 |
+
difficult_traits = {
|
| 379 |
+
'sensitive': -0.15,
|
| 380 |
+
'stubborn': -0.12,
|
| 381 |
+
'independent': -0.10,
|
| 382 |
+
'dominant': -0.12,
|
| 383 |
+
'aggressive': -0.20,
|
| 384 |
+
'nervous': -0.12,
|
| 385 |
+
'alert': -0.05,
|
| 386 |
+
'shy': -0.08,
|
| 387 |
+
'timid': -0.08
|
| 388 |
+
}
|
| 389 |
+
for trait, penalty in difficult_traits.items():
|
| 390 |
+
if trait in temperament:
|
| 391 |
+
experience_score += penalty
|
| 392 |
+
|
| 393 |
+
easy_traits = {
|
| 394 |
+
'friendly': 0.08,
|
| 395 |
+
'gentle': 0.08,
|
| 396 |
+
'eager to please': 0.10,
|
| 397 |
+
'patient': 0.08,
|
| 398 |
+
'calm': 0.08,
|
| 399 |
+
'outgoing': 0.05,
|
| 400 |
+
'playful': 0.03
|
| 401 |
+
}
|
| 402 |
+
for trait, bonus in easy_traits.items():
|
| 403 |
+
if trait in temperament:
|
| 404 |
+
experience_score += bonus
|
| 405 |
+
|
| 406 |
+
# 5. Noise Compatibility
|
| 407 |
+
noise_score = 0.75
|
| 408 |
+
if any(term in temperament for term in ['quiet', 'calm', 'gentle']):
|
| 409 |
+
noise_score = 0.85
|
| 410 |
+
elif any(term in temperament for term in ['alert', 'vocal']):
|
| 411 |
+
noise_score = 0.60
|
| 412 |
+
|
| 413 |
+
# 6. Family Compatibility
|
| 414 |
+
family_score = 0.70
|
| 415 |
+
if good_with_children == 'Yes' or good_with_children == True:
|
| 416 |
+
family_score = 0.80
|
| 417 |
+
if any(term in temperament for term in ['gentle', 'patient', 'friendly']):
|
| 418 |
+
family_score = 0.90
|
| 419 |
+
elif good_with_children == 'No' or good_with_children == False:
|
| 420 |
+
family_score = 0.45
|
| 421 |
+
|
| 422 |
+
# 確保分數在合理範圍內
|
| 423 |
scores = {
|
| 424 |
+
'space': max(0.30, min(0.95, space_score)),
|
| 425 |
+
'exercise': max(0.30, min(0.95, exercise_score)),
|
| 426 |
+
'grooming': max(0.30, min(0.95, grooming_score)),
|
| 427 |
+
'experience': max(0.30, min(0.95, experience_score)),
|
| 428 |
+
'noise': max(0.30, min(0.95, noise_score)),
|
| 429 |
+
'family': max(0.30, min(0.95, family_score)),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 430 |
'overall': base_score
|
| 431 |
}
|
| 432 |
else:
|
| 433 |
# 傳統搜尋結果的分數結構會在呼叫處理中傳入
|
| 434 |
scores = {'overall': base_score}
|
| 435 |
+
|
| 436 |
return scores
|
recommendation_html_format.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
import random
|
| 2 |
from typing import List, Dict
|
| 3 |
from breed_health_info import breed_health_info, default_health_note
|
|
@@ -259,7 +260,7 @@ def format_recommendation_html(recommendations: List[Dict], is_description_searc
|
|
| 259 |
|
| 260 |
def format_unified_recommendation_html(recommendations: List[Dict], is_description_search: bool = False) -> str:
|
| 261 |
"""統一推薦HTML格式化主函數,確保視覺呈現與數值計算完全一致"""
|
| 262 |
-
|
| 263 |
# 創建HTML格式器實例
|
| 264 |
formatter = RecommendationHTMLFormatter()
|
| 265 |
|
|
@@ -272,21 +273,33 @@ def format_unified_recommendation_html(recommendations: List[Dict], is_descripti
|
|
| 272 |
</div>
|
| 273 |
'''
|
| 274 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
# 使用格式器的統一CSS樣式
|
| 276 |
html_content = formatter.unified_css + "<div class='unified-recommendations'>"
|
| 277 |
|
| 278 |
-
for rec in
|
| 279 |
breed = rec['breed']
|
| 280 |
rank = rec.get('rank', 0)
|
| 281 |
|
| 282 |
-
# 統一分數處理
|
| 283 |
-
overall_score = rec.get('
|
| 284 |
scores = rec.get('scores', {})
|
| 285 |
|
| 286 |
# 如果沒有維度分數,基於總分生成一致的維度分數
|
| 287 |
if not scores:
|
| 288 |
scores = generate_dimension_scores_for_display(
|
| 289 |
-
overall_score,
|
|
|
|
|
|
|
|
|
|
| 290 |
)
|
| 291 |
|
| 292 |
# 獲取品種資訊
|
|
|
|
| 1 |
+
# %%writefile recommendation_html_format.py
|
| 2 |
import random
|
| 3 |
from typing import List, Dict
|
| 4 |
from breed_health_info import breed_health_info, default_health_note
|
|
|
|
| 260 |
|
| 261 |
def format_unified_recommendation_html(recommendations: List[Dict], is_description_search: bool = False) -> str:
|
| 262 |
"""統一推薦HTML格式化主函數,確保視覺呈現與數值計算完全一致"""
|
| 263 |
+
|
| 264 |
# 創建HTML格式器實例
|
| 265 |
formatter = RecommendationHTMLFormatter()
|
| 266 |
|
|
|
|
| 273 |
</div>
|
| 274 |
'''
|
| 275 |
|
| 276 |
+
# 確保按分數降序排序,並更新排名
|
| 277 |
+
sorted_recommendations = sorted(
|
| 278 |
+
recommendations,
|
| 279 |
+
key=lambda x: x.get('final_score', x.get('overall_score', 0)),
|
| 280 |
+
reverse=True
|
| 281 |
+
)
|
| 282 |
+
for i, rec in enumerate(sorted_recommendations):
|
| 283 |
+
rec['rank'] = i + 1
|
| 284 |
+
|
| 285 |
# 使用格式器的統一CSS樣式
|
| 286 |
html_content = formatter.unified_css + "<div class='unified-recommendations'>"
|
| 287 |
|
| 288 |
+
for rec in sorted_recommendations:
|
| 289 |
breed = rec['breed']
|
| 290 |
rank = rec.get('rank', 0)
|
| 291 |
|
| 292 |
+
# 統一分數處理 - 優先使用 final_score(經過風險調整後的分數)
|
| 293 |
+
overall_score = rec.get('final_score', rec.get('overall_score', 0.7))
|
| 294 |
scores = rec.get('scores', {})
|
| 295 |
|
| 296 |
# 如果沒有維度分數,基於總分生成一致的維度分數
|
| 297 |
if not scores:
|
| 298 |
scores = generate_dimension_scores_for_display(
|
| 299 |
+
base_score=overall_score,
|
| 300 |
+
rank=rank,
|
| 301 |
+
breed=breed,
|
| 302 |
+
is_description_search=is_description_search
|
| 303 |
)
|
| 304 |
|
| 305 |
# 獲取品種資訊
|
recommendation_html_formatter.py
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import random
|
| 2 |
from typing import List, Dict
|
| 3 |
from breed_health_info import breed_health_info, default_health_note
|
|
@@ -10,588 +15,23 @@ from recommendation_formatter import (
|
|
| 10 |
calculate_breed_bonus_factors,
|
| 11 |
generate_dimension_scores_for_display
|
| 12 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
-
class RecommendationHTMLFormatter:
|
| 15 |
-
"""處理推薦結果的HTML和CSS格式化"""
|
| 16 |
-
|
| 17 |
-
def __init__(self):
|
| 18 |
-
self.description_search_css = """
|
| 19 |
-
<style>
|
| 20 |
-
.recommendations-container {
|
| 21 |
-
display: flex;
|
| 22 |
-
flex-direction: column;
|
| 23 |
-
gap: 20px;
|
| 24 |
-
padding: 20px;
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
.breed-card {
|
| 28 |
-
border: 2px solid #e5e7eb;
|
| 29 |
-
border-radius: 12px;
|
| 30 |
-
padding: 20px;
|
| 31 |
-
background: white;
|
| 32 |
-
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 33 |
-
transition: all 0.3s ease;
|
| 34 |
-
position: relative;
|
| 35 |
-
}
|
| 36 |
-
|
| 37 |
-
.breed-card:hover {
|
| 38 |
-
box-shadow: 0 8px 12px rgba(0, 0, 0, 0.15);
|
| 39 |
-
transform: translateY(-2px);
|
| 40 |
-
}
|
| 41 |
-
|
| 42 |
-
.rank-badge {
|
| 43 |
-
position: absolute;
|
| 44 |
-
top: 15px;
|
| 45 |
-
left: 15px;
|
| 46 |
-
padding: 8px 14px;
|
| 47 |
-
border-radius: 8px;
|
| 48 |
-
font-weight: 800;
|
| 49 |
-
font-size: 20px;
|
| 50 |
-
min-width: 45px;
|
| 51 |
-
text-align: center;
|
| 52 |
-
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
| 53 |
-
}
|
| 54 |
-
|
| 55 |
-
.rank-1 {
|
| 56 |
-
background: linear-gradient(135deg, #FEF3C7 0%, #FDE68A 50%, #F59E0B 100%);
|
| 57 |
-
color: #92400E;
|
| 58 |
-
font-size: 32px;
|
| 59 |
-
font-weight: 900;
|
| 60 |
-
animation: pulse 2s infinite;
|
| 61 |
-
border: 3px solid rgba(251, 191, 36, 0.4);
|
| 62 |
-
box-shadow: 0 6px 20px rgba(245, 158, 11, 0.3);
|
| 63 |
-
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.8);
|
| 64 |
-
}
|
| 65 |
-
|
| 66 |
-
.rank-2 {
|
| 67 |
-
background: linear-gradient(135deg, #F1F5F9 0%, #E2E8F0 50%, #94A3B8 100%);
|
| 68 |
-
color: #475569;
|
| 69 |
-
font-size: 30px;
|
| 70 |
-
font-weight: 800;
|
| 71 |
-
border: 3px solid rgba(148, 163, 184, 0.4);
|
| 72 |
-
box-shadow: 0 5px 15px rgba(148, 163, 184, 0.3);
|
| 73 |
-
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.8);
|
| 74 |
-
}
|
| 75 |
-
|
| 76 |
-
.rank-3 {
|
| 77 |
-
background: linear-gradient(135deg, #FEF2F2 0%, #FED7AA 50%, #FB923C 100%);
|
| 78 |
-
color: #9A3412;
|
| 79 |
-
font-size: 28px;
|
| 80 |
-
font-weight: 800;
|
| 81 |
-
border: 3px solid rgba(251, 146, 60, 0.4);
|
| 82 |
-
box-shadow: 0 4px 12px rgba(251, 146, 60, 0.3);
|
| 83 |
-
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.8);
|
| 84 |
-
}
|
| 85 |
-
|
| 86 |
-
.rank-other {
|
| 87 |
-
background: linear-gradient(135deg, #F8FAFC 0%, #E2E8F0 50%, #CBD5E1 100%);
|
| 88 |
-
color: #475569;
|
| 89 |
-
font-size: 26px;
|
| 90 |
-
font-weight: 700;
|
| 91 |
-
border: 2px solid rgba(203, 213, 225, 0.6);
|
| 92 |
-
box-shadow: 0 3px 8px rgba(203, 213, 225, 0.4);
|
| 93 |
-
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.8);
|
| 94 |
-
}
|
| 95 |
-
|
| 96 |
-
@keyframes pulse {
|
| 97 |
-
0% {
|
| 98 |
-
box-shadow: 0 6px 20px rgba(245, 158, 11, 0.3);
|
| 99 |
-
transform: scale(1);
|
| 100 |
-
}
|
| 101 |
-
50% {
|
| 102 |
-
box-shadow: 0 8px 25px rgba(245, 158, 11, 0.5), 0 0 0 4px rgba(245, 158, 11, 0.15);
|
| 103 |
-
transform: scale(1.05);
|
| 104 |
-
}
|
| 105 |
-
100% {
|
| 106 |
-
box-shadow: 0 6px 20px rgba(245, 158, 11, 0.3);
|
| 107 |
-
transform: scale(1);
|
| 108 |
-
}
|
| 109 |
-
}
|
| 110 |
-
|
| 111 |
-
.breed-header {
|
| 112 |
-
display: flex;
|
| 113 |
-
justify-content: space-between;
|
| 114 |
-
align-items: center;
|
| 115 |
-
margin-bottom: 15px;
|
| 116 |
-
padding-left: 70px;
|
| 117 |
-
}
|
| 118 |
-
|
| 119 |
-
.breed-name {
|
| 120 |
-
font-size: 26px;
|
| 121 |
-
font-weight: 800;
|
| 122 |
-
color: #1F2937;
|
| 123 |
-
margin: 0;
|
| 124 |
-
letter-spacing: -0.025em;
|
| 125 |
-
line-height: 1.2;
|
| 126 |
-
}
|
| 127 |
-
|
| 128 |
-
.match-score {
|
| 129 |
-
display: flex;
|
| 130 |
-
flex-direction: column;
|
| 131 |
-
align-items: flex-end;
|
| 132 |
-
padding: 12px 16px;
|
| 133 |
-
background: linear-gradient(135deg, #F0F9FF 0%, #E0F2FE 100%);
|
| 134 |
-
border-radius: 12px;
|
| 135 |
-
border: 2px solid rgba(6, 182, 212, 0.2);
|
| 136 |
-
box-shadow: 0 4px 12px rgba(6, 182, 212, 0.1);
|
| 137 |
-
}
|
| 138 |
-
|
| 139 |
-
.match-percentage {
|
| 140 |
-
font-size: 48px;
|
| 141 |
-
font-weight: 900;
|
| 142 |
-
margin-bottom: 8px;
|
| 143 |
-
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
| 144 |
-
line-height: 1;
|
| 145 |
-
letter-spacing: -0.02em;
|
| 146 |
-
}
|
| 147 |
-
|
| 148 |
-
.match-label {
|
| 149 |
-
font-size: 12px;
|
| 150 |
-
text-transform: uppercase;
|
| 151 |
-
letter-spacing: 2px;
|
| 152 |
-
opacity: 0.9;
|
| 153 |
-
font-weight: 800;
|
| 154 |
-
margin-bottom: 6px;
|
| 155 |
-
color: #0369A1;
|
| 156 |
-
}
|
| 157 |
-
|
| 158 |
-
.score-excellent { color: #22C55E; }
|
| 159 |
-
.score-good { color: #F59E0B; }
|
| 160 |
-
.score-moderate { color: #6B7280; }
|
| 161 |
-
|
| 162 |
-
.score-bar {
|
| 163 |
-
width: 220px;
|
| 164 |
-
height: 14px;
|
| 165 |
-
background: rgba(226, 232, 240, 0.8);
|
| 166 |
-
border-radius: 8px;
|
| 167 |
-
overflow: hidden;
|
| 168 |
-
margin-top: 8px;
|
| 169 |
-
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
|
| 170 |
-
border: 1px solid rgba(6, 182, 212, 0.2);
|
| 171 |
-
}
|
| 172 |
-
|
| 173 |
-
.score-fill {
|
| 174 |
-
height: 100%;
|
| 175 |
-
border-radius: 4px;
|
| 176 |
-
transition: width 1s ease;
|
| 177 |
-
}
|
| 178 |
-
|
| 179 |
-
.fill-excellent { background: linear-gradient(90deg, #22C55E, #16A34A); }
|
| 180 |
-
.fill-good { background: linear-gradient(90deg, #F59E0B, #DC2626); }
|
| 181 |
-
.fill-moderate { background: linear-gradient(90deg, #6B7280, #4B5563); }
|
| 182 |
-
|
| 183 |
-
/* Tooltip styles for Find by Description */
|
| 184 |
-
.tooltip {
|
| 185 |
-
position: relative;
|
| 186 |
-
display: inline-block;
|
| 187 |
-
cursor: help;
|
| 188 |
-
}
|
| 189 |
-
|
| 190 |
-
.tooltip-icon {
|
| 191 |
-
display: inline-block;
|
| 192 |
-
width: 18px;
|
| 193 |
-
height: 18px;
|
| 194 |
-
background: linear-gradient(135deg, #06b6d4, #0891b2);
|
| 195 |
-
color: white;
|
| 196 |
-
border-radius: 50%;
|
| 197 |
-
text-align: center;
|
| 198 |
-
line-height: 18px;
|
| 199 |
-
font-size: 12px;
|
| 200 |
-
font-weight: bold;
|
| 201 |
-
margin-left: 8px;
|
| 202 |
-
cursor: help;
|
| 203 |
-
box-shadow: 0 2px 4px rgba(6, 182, 212, 0.3);
|
| 204 |
-
transition: all 0.2s ease;
|
| 205 |
-
}
|
| 206 |
-
|
| 207 |
-
.tooltip-icon:hover {
|
| 208 |
-
background: linear-gradient(135deg, #0891b2, #0e7490);
|
| 209 |
-
transform: scale(1.1);
|
| 210 |
-
box-shadow: 0 3px 6px rgba(6, 182, 212, 0.4);
|
| 211 |
-
}
|
| 212 |
-
|
| 213 |
-
.tooltip-text {
|
| 214 |
-
visibility: hidden;
|
| 215 |
-
width: 320px;
|
| 216 |
-
background: linear-gradient(145deg, #1e293b, #334155);
|
| 217 |
-
color: #f1f5f9;
|
| 218 |
-
text-align: left;
|
| 219 |
-
border-radius: 12px;
|
| 220 |
-
padding: 16px;
|
| 221 |
-
position: absolute;
|
| 222 |
-
z-index: 1000;
|
| 223 |
-
bottom: 125%;
|
| 224 |
-
left: 50%;
|
| 225 |
-
margin-left: -160px;
|
| 226 |
-
opacity: 0;
|
| 227 |
-
transition: opacity 0.3s ease, transform 0.3s ease;
|
| 228 |
-
transform: translateY(10px);
|
| 229 |
-
font-size: 14px;
|
| 230 |
-
line-height: 1.5;
|
| 231 |
-
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
|
| 232 |
-
border: 1px solid rgba(148, 163, 184, 0.2);
|
| 233 |
-
}
|
| 234 |
-
|
| 235 |
-
.tooltip-text::after {
|
| 236 |
-
content: "";
|
| 237 |
-
position: absolute;
|
| 238 |
-
top: 100%;
|
| 239 |
-
left: 50%;
|
| 240 |
-
margin-left: -8px;
|
| 241 |
-
border-width: 8px;
|
| 242 |
-
border-style: solid;
|
| 243 |
-
border-color: #334155 transparent transparent transparent;
|
| 244 |
-
}
|
| 245 |
-
|
| 246 |
-
.tooltip:hover .tooltip-text {
|
| 247 |
-
visibility: visible;
|
| 248 |
-
opacity: 1;
|
| 249 |
-
transform: translateY(0);
|
| 250 |
-
}
|
| 251 |
-
|
| 252 |
-
.tooltip-text strong {
|
| 253 |
-
color: #06b6d4;
|
| 254 |
-
font-weight: 700;
|
| 255 |
-
display: block;
|
| 256 |
-
margin-bottom: 8px;
|
| 257 |
-
font-size: 15px;
|
| 258 |
-
}
|
| 259 |
-
</style>
|
| 260 |
-
"""
|
| 261 |
-
|
| 262 |
-
self.criteria_search_css = """
|
| 263 |
-
<style>
|
| 264 |
-
.recommendations-container {
|
| 265 |
-
display: flex;
|
| 266 |
-
flex-direction: column;
|
| 267 |
-
gap: 15px;
|
| 268 |
-
padding: 15px;
|
| 269 |
-
}
|
| 270 |
-
|
| 271 |
-
.breed-card {
|
| 272 |
-
border: 1px solid #d1d5db;
|
| 273 |
-
border-radius: 8px;
|
| 274 |
-
padding: 16px;
|
| 275 |
-
background: #ffffff;
|
| 276 |
-
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
| 277 |
-
transition: all 0.2s ease;
|
| 278 |
-
}
|
| 279 |
-
|
| 280 |
-
.breed-card:hover {
|
| 281 |
-
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
| 282 |
-
transform: translateY(-1px);
|
| 283 |
-
}
|
| 284 |
-
|
| 285 |
-
.breed-header {
|
| 286 |
-
display: flex;
|
| 287 |
-
justify-content: space-between;
|
| 288 |
-
align-items: center;
|
| 289 |
-
margin-bottom: 12px;
|
| 290 |
-
padding-bottom: 8px;
|
| 291 |
-
border-bottom: 1px solid #f3f4f6;
|
| 292 |
-
}
|
| 293 |
-
|
| 294 |
-
.breed-title {
|
| 295 |
-
display: flex;
|
| 296 |
-
align-items: center;
|
| 297 |
-
gap: 16px;
|
| 298 |
-
justify-content: flex-start;
|
| 299 |
-
}
|
| 300 |
-
|
| 301 |
-
.trophy-rank {
|
| 302 |
-
font-size: 24px;
|
| 303 |
-
font-weight: 800;
|
| 304 |
-
color: #1f2937;
|
| 305 |
-
}
|
| 306 |
-
|
| 307 |
-
.breed-name {
|
| 308 |
-
font-size: 42px;
|
| 309 |
-
font-weight: 900;
|
| 310 |
-
color: #1f2937;
|
| 311 |
-
margin: 0;
|
| 312 |
-
padding: 8px 16px;
|
| 313 |
-
background: linear-gradient(135deg, #D1FAE5 0%, #A7F3D0 100%);
|
| 314 |
-
border: 2px solid #22C55E;
|
| 315 |
-
border-radius: 12px;
|
| 316 |
-
display: inline-block;
|
| 317 |
-
box-shadow: 0 2px 8px rgba(34, 197, 94, 0.2);
|
| 318 |
-
}
|
| 319 |
-
|
| 320 |
-
.overall-score {
|
| 321 |
-
display: flex;
|
| 322 |
-
flex-direction: column;
|
| 323 |
-
align-items: flex-end;
|
| 324 |
-
}
|
| 325 |
-
|
| 326 |
-
.score-percentage {
|
| 327 |
-
font-size: 32px;
|
| 328 |
-
font-weight: 900;
|
| 329 |
-
margin-bottom: 4px;
|
| 330 |
-
line-height: 1;
|
| 331 |
-
}
|
| 332 |
-
|
| 333 |
-
.score-label {
|
| 334 |
-
font-size: 10px;
|
| 335 |
-
text-transform: uppercase;
|
| 336 |
-
letter-spacing: 1px;
|
| 337 |
-
opacity: 0.7;
|
| 338 |
-
font-weight: 600;
|
| 339 |
-
}
|
| 340 |
-
|
| 341 |
-
.score-bar-wide {
|
| 342 |
-
width: 200px;
|
| 343 |
-
height: 8px;
|
| 344 |
-
background: #f3f4f6;
|
| 345 |
-
border-radius: 4px;
|
| 346 |
-
overflow: hidden;
|
| 347 |
-
margin-top: 6px;
|
| 348 |
-
}
|
| 349 |
-
|
| 350 |
-
.score-fill-wide {
|
| 351 |
-
height: 100%;
|
| 352 |
-
border-radius: 4px;
|
| 353 |
-
transition: width 0.8s ease;
|
| 354 |
-
}
|
| 355 |
-
|
| 356 |
-
.score-excellent { color: #22C55E; }
|
| 357 |
-
.score-good { color: #65a30d; }
|
| 358 |
-
.score-moderate { color: #d4a332; }
|
| 359 |
-
.score-fair { color: #e67e22; }
|
| 360 |
-
.score-poor { color: #e74c3c; }
|
| 361 |
-
|
| 362 |
-
.fill-excellent { background: #22C55E; }
|
| 363 |
-
.fill-good { background: #65a30d; }
|
| 364 |
-
.fill-moderate { background: #d4a332; }
|
| 365 |
-
.fill-fair { background: #e67e22; }
|
| 366 |
-
.fill-poor { background: #e74c3c; }
|
| 367 |
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
padding-top: 12px;
|
| 371 |
-
border-top: 1px solid #e5e7eb;
|
| 372 |
-
}
|
| 373 |
-
|
| 374 |
-
/* 通用樣式(兩個模式都需要) */
|
| 375 |
-
.progress {
|
| 376 |
-
transition: all 0.3s ease-in-out;
|
| 377 |
-
border-radius: 4px;
|
| 378 |
-
height: 12px;
|
| 379 |
-
}
|
| 380 |
-
|
| 381 |
-
.progress-bar {
|
| 382 |
-
background-color: #f5f5f5;
|
| 383 |
-
border-radius: 4px;
|
| 384 |
-
overflow: hidden;
|
| 385 |
-
position: relative;
|
| 386 |
-
}
|
| 387 |
-
|
| 388 |
-
.score-item {
|
| 389 |
-
margin: 10px 0;
|
| 390 |
-
}
|
| 391 |
-
|
| 392 |
-
.percentage {
|
| 393 |
-
margin-left: 8px;
|
| 394 |
-
font-weight: 500;
|
| 395 |
-
}
|
| 396 |
|
| 397 |
-
|
| 398 |
-
.
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
gap: 4px;
|
| 403 |
-
cursor: help;
|
| 404 |
-
}
|
| 405 |
-
.tooltip .tooltip-icon {
|
| 406 |
-
font-size: 14px;
|
| 407 |
-
color: #666;
|
| 408 |
-
}
|
| 409 |
-
.tooltip .tooltip-text {
|
| 410 |
-
visibility: hidden;
|
| 411 |
-
width: 250px;
|
| 412 |
-
background-color: rgba(44, 62, 80, 0.95);
|
| 413 |
-
color: white;
|
| 414 |
-
text-align: left;
|
| 415 |
-
border-radius: 8px;
|
| 416 |
-
padding: 8px 10px;
|
| 417 |
-
position: absolute;
|
| 418 |
-
z-index: 100;
|
| 419 |
-
bottom: 150%;
|
| 420 |
-
left: 50%;
|
| 421 |
-
transform: translateX(-50%);
|
| 422 |
-
opacity: 0;
|
| 423 |
-
transition: all 0.3s ease;
|
| 424 |
-
font-size: 14px;
|
| 425 |
-
line-height: 1.3;
|
| 426 |
-
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
|
| 427 |
-
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 428 |
-
margin-bottom: 10px;
|
| 429 |
-
}
|
| 430 |
-
.tooltip:hover .tooltip-text {
|
| 431 |
-
visibility: visible;
|
| 432 |
-
opacity: 1;
|
| 433 |
-
}
|
| 434 |
-
.tooltip .tooltip-text::after {
|
| 435 |
-
content: "";
|
| 436 |
-
position: absolute;
|
| 437 |
-
top: 100%;
|
| 438 |
-
left: 50%;
|
| 439 |
-
transform: translateX(-50%);
|
| 440 |
-
border-width: 8px;
|
| 441 |
-
border-style: solid;
|
| 442 |
-
border-color: rgba(44, 62, 80, 0.95) transparent transparent transparent;
|
| 443 |
-
}
|
| 444 |
-
|
| 445 |
-
</style>
|
| 446 |
-
"""
|
| 447 |
-
|
| 448 |
-
self.unified_css = """
|
| 449 |
-
<style>
|
| 450 |
-
.unified-recommendations { max-width: 1200px; margin: 0 auto; padding: 20px; }
|
| 451 |
-
.unified-breed-card {
|
| 452 |
-
background: linear-gradient(145deg, #ffffff 0%, #f8fafc 100%);
|
| 453 |
-
border-radius: 16px; padding: 24px; margin-bottom: 20px;
|
| 454 |
-
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1); border: 1px solid #e2e8f0;
|
| 455 |
-
transition: all 0.3s ease;
|
| 456 |
-
}
|
| 457 |
-
.unified-breed-card:hover { transform: translateY(-2px); box-shadow: 0 12px 35px rgba(0, 0, 0, 0.15); }
|
| 458 |
-
.unified-breed-header { margin-bottom: 20px; }
|
| 459 |
-
.unified-rank-section { display: flex; align-items: center; gap: 15px; }
|
| 460 |
-
.unified-rank-badge {
|
| 461 |
-
background: linear-gradient(135deg, #E0F2FE 0%, #BAE6FD 100%);
|
| 462 |
-
color: #0C4A6E; padding: 8px 16px; border-radius: 8px;
|
| 463 |
-
font-weight: 900; font-size: 24px;
|
| 464 |
-
box-shadow: 0 2px 8px rgba(14, 165, 233, 0.2);
|
| 465 |
-
border: 2px solid #0EA5E9;
|
| 466 |
-
display: inline-block;
|
| 467 |
-
min-width: 80px; text-align: center;
|
| 468 |
-
}
|
| 469 |
-
.unified-breed-info { flex-grow: 1; }
|
| 470 |
-
.unified-breed-title {
|
| 471 |
-
font-size: 24px; font-weight: 800; color: #0C4A6E;
|
| 472 |
-
margin: 0; letter-spacing: -0.02em;
|
| 473 |
-
background: linear-gradient(135deg, #F0F9FF, #E0F2FE);
|
| 474 |
-
padding: 12px 20px; border-radius: 10px;
|
| 475 |
-
border: 2px solid #0EA5E9;
|
| 476 |
-
display: inline-block; box-shadow: 0 2px 8px rgba(14, 165, 233, 0.1);
|
| 477 |
-
}
|
| 478 |
-
.unified-match-score {
|
| 479 |
-
font-size: 24px; font-weight: 900;
|
| 480 |
-
color: #0F5132; background: linear-gradient(135deg, #D1FAE5, #A7F3D0);
|
| 481 |
-
padding: 12px 20px; border-radius: 10px; display: inline-block;
|
| 482 |
-
text-align: center; border: 2px solid #22C55E;
|
| 483 |
-
box-shadow: 0 2px 8px rgba(34, 197, 94, 0.2);
|
| 484 |
-
margin: 0; text-shadow: 0 1px 2px rgba(255, 255, 255, 0.5);
|
| 485 |
-
letter-spacing: -0.02em;
|
| 486 |
-
}
|
| 487 |
-
.unified-overall-section {
|
| 488 |
-
background: linear-gradient(135deg, #f0f9ff, #ecfeff); border-radius: 12px;
|
| 489 |
-
padding: 20px; margin-bottom: 24px; border: 2px solid #06b6d4;
|
| 490 |
-
box-shadow: 0 4px 12px rgba(6, 182, 212, 0.1);
|
| 491 |
-
}
|
| 492 |
-
.unified-dimension-grid {
|
| 493 |
-
display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
| 494 |
-
gap: 16px; margin-bottom: 24px;
|
| 495 |
-
}
|
| 496 |
-
.unified-dimension-item {
|
| 497 |
-
background: white; padding: 16px; border-radius: 10px;
|
| 498 |
-
border: 1px solid #e2e8f0; transition: all 0.2s ease;
|
| 499 |
-
}
|
| 500 |
-
.unified-dimension-item:hover { background: #f8fafc; border-color: #cbd5e1; }
|
| 501 |
-
.unified-breed-info {
|
| 502 |
-
display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
| 503 |
-
gap: 12px; margin: 20px 0;
|
| 504 |
-
}
|
| 505 |
-
.unified-info-item {
|
| 506 |
-
background: #f8fafc; padding: 12px; border-radius: 8px;
|
| 507 |
-
border-left: 4px solid #6366f1;
|
| 508 |
-
}
|
| 509 |
-
.unified-info-label { font-weight: 600; color: #4b5563; font-size: 0.85em; margin-bottom: 4px; }
|
| 510 |
-
.unified-info-value { color: #1f2937; font-weight: 500; }
|
| 511 |
-
|
| 512 |
-
/* Tooltip styles for unified recommendations */
|
| 513 |
-
.tooltip {
|
| 514 |
-
position: relative;
|
| 515 |
-
display: inline-block;
|
| 516 |
-
cursor: help;
|
| 517 |
-
}
|
| 518 |
-
|
| 519 |
-
.tooltip-icon {
|
| 520 |
-
display: inline-block;
|
| 521 |
-
width: 18px;
|
| 522 |
-
height: 18px;
|
| 523 |
-
background: linear-gradient(135deg, #06b6d4, #0891b2);
|
| 524 |
-
color: white;
|
| 525 |
-
border-radius: 50%;
|
| 526 |
-
text-align: center;
|
| 527 |
-
line-height: 18px;
|
| 528 |
-
font-size: 12px;
|
| 529 |
-
font-weight: bold;
|
| 530 |
-
margin-left: 8px;
|
| 531 |
-
cursor: help;
|
| 532 |
-
box-shadow: 0 2px 4px rgba(6, 182, 212, 0.3);
|
| 533 |
-
transition: all 0.2s ease;
|
| 534 |
-
}
|
| 535 |
-
|
| 536 |
-
.tooltip-icon:hover {
|
| 537 |
-
background: linear-gradient(135deg, #0891b2, #0e7490);
|
| 538 |
-
transform: scale(1.1);
|
| 539 |
-
box-shadow: 0 3px 6px rgba(6, 182, 212, 0.4);
|
| 540 |
-
}
|
| 541 |
-
|
| 542 |
-
.tooltip-text {
|
| 543 |
-
visibility: hidden;
|
| 544 |
-
width: 320px;
|
| 545 |
-
background: linear-gradient(145deg, #1e293b, #334155);
|
| 546 |
-
color: #f1f5f9;
|
| 547 |
-
text-align: left;
|
| 548 |
-
border-radius: 12px;
|
| 549 |
-
padding: 16px;
|
| 550 |
-
position: absolute;
|
| 551 |
-
z-index: 1000;
|
| 552 |
-
bottom: 125%;
|
| 553 |
-
left: 50%;
|
| 554 |
-
margin-left: -160px;
|
| 555 |
-
opacity: 0;
|
| 556 |
-
transition: opacity 0.3s ease, transform 0.3s ease;
|
| 557 |
-
transform: translateY(10px);
|
| 558 |
-
font-size: 14px;
|
| 559 |
-
line-height: 1.5;
|
| 560 |
-
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
|
| 561 |
-
border: 1px solid rgba(148, 163, 184, 0.2);
|
| 562 |
-
}
|
| 563 |
-
|
| 564 |
-
.tooltip-text::after {
|
| 565 |
-
content: "";
|
| 566 |
-
position: absolute;
|
| 567 |
-
top: 100%;
|
| 568 |
-
left: 50%;
|
| 569 |
-
margin-left: -8px;
|
| 570 |
-
border-width: 8px;
|
| 571 |
-
border-style: solid;
|
| 572 |
-
border-color: #334155 transparent transparent transparent;
|
| 573 |
-
}
|
| 574 |
-
|
| 575 |
-
.tooltip:hover .tooltip-text {
|
| 576 |
-
visibility: visible;
|
| 577 |
-
opacity: 1;
|
| 578 |
-
transform: translateY(0);
|
| 579 |
-
}
|
| 580 |
-
|
| 581 |
-
.tooltip-text strong {
|
| 582 |
-
color: #06b6d4;
|
| 583 |
-
font-weight: 700;
|
| 584 |
-
display: block;
|
| 585 |
-
margin-bottom: 8px;
|
| 586 |
-
font-size: 15px;
|
| 587 |
-
}
|
| 588 |
-
|
| 589 |
-
.akc-button:hover {
|
| 590 |
-
transform: translateY(-2px) !important;
|
| 591 |
-
box-shadow: 0 6px 16px rgba(59, 130, 246, 0.5) !important;
|
| 592 |
-
}
|
| 593 |
-
</style>
|
| 594 |
-
"""
|
| 595 |
|
| 596 |
def format_unified_percentage(self, score: float) -> str:
|
| 597 |
"""統一格式化百分比顯示,確保數值邏輯一致"""
|
|
|
|
| 1 |
+
# %%writefile recommendation_html_formatter.py
|
| 2 |
+
"""
|
| 3 |
+
HTML 格式化器 - 處理推薦結果的 HTML 生成
|
| 4 |
+
CSS 樣式已分離至 recommendation_css.py
|
| 5 |
+
"""
|
| 6 |
import random
|
| 7 |
from typing import List, Dict
|
| 8 |
from breed_health_info import breed_health_info, default_health_note
|
|
|
|
| 15 |
calculate_breed_bonus_factors,
|
| 16 |
generate_dimension_scores_for_display
|
| 17 |
)
|
| 18 |
+
from recommendation_css import (
|
| 19 |
+
DESCRIPTION_SEARCH_CSS,
|
| 20 |
+
CRITERIA_SEARCH_CSS,
|
| 21 |
+
UNIFIED_CSS,
|
| 22 |
+
get_recommendation_css_styles,
|
| 23 |
+
get_unified_css
|
| 24 |
+
)
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
+
class RecommendationHTMLFormatter:
|
| 28 |
+
"""處理推薦結果的HTML格式化,CSS樣式從recommendation_css.py導入"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
+
def __init__(self):
|
| 31 |
+
# 從 recommendation_css.py 導入 CSS 樣式
|
| 32 |
+
self.description_search_css = DESCRIPTION_SEARCH_CSS
|
| 33 |
+
self.criteria_search_css = CRITERIA_SEARCH_CSS
|
| 34 |
+
self.unified_css = UNIFIED_CSS
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
def format_unified_percentage(self, score: float) -> str:
|
| 37 |
"""統一格式化百分比顯示,確保數值邏輯一致"""
|
scoring_calculation_system.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
from dataclasses import dataclass
|
| 2 |
from typing import Dict, List, Any, Optional
|
| 3 |
import math
|
|
@@ -111,7 +112,9 @@ def calculate_compatibility_score(breed_info: dict, user_prefs: UserPreferences)
|
|
| 111 |
'grooming': _dimension_calculator.calculate_grooming_score(
|
| 112 |
breed_info.get('Grooming Needs', 'Moderate'),
|
| 113 |
user_prefs.grooming_commitment.lower(),
|
| 114 |
-
breed_info['Size']
|
|
|
|
|
|
|
| 115 |
),
|
| 116 |
'experience': _dimension_calculator.calculate_experience_score(
|
| 117 |
breed_info.get('Care Level', 'Moderate'),
|
|
@@ -494,4 +497,4 @@ def calculate_unified_breed_scores(breed_list: List[str], user_prefs: UserPrefer
|
|
| 494 |
# 按總分排序
|
| 495 |
scores.sort(key=lambda x: x.overall_score, reverse=True)
|
| 496 |
|
| 497 |
-
return scores
|
|
|
|
| 1 |
+
# %%writefile scoring_calculation_system.py
|
| 2 |
from dataclasses import dataclass
|
| 3 |
from typing import Dict, List, Any, Optional
|
| 4 |
import math
|
|
|
|
| 112 |
'grooming': _dimension_calculator.calculate_grooming_score(
|
| 113 |
breed_info.get('Grooming Needs', 'Moderate'),
|
| 114 |
user_prefs.grooming_commitment.lower(),
|
| 115 |
+
breed_info['Size'],
|
| 116 |
+
breed_info.get('Breed', ''),
|
| 117 |
+
breed_info.get('Temperament', '')
|
| 118 |
),
|
| 119 |
'experience': _dimension_calculator.calculate_experience_score(
|
| 120 |
breed_info.get('Care Level', 'Moderate'),
|
|
|
|
| 497 |
# 按總分排序
|
| 498 |
scores.sort(key=lambda x: x.overall_score, reverse=True)
|
| 499 |
|
| 500 |
+
return scores
|
semantic_breed_recommender.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
import random
|
| 2 |
import hashlib
|
| 3 |
import numpy as np
|
|
@@ -21,6 +22,7 @@ from config_manager import get_config_manager, get_standardized_breed_data
|
|
| 21 |
from semantic_vector_manager import SemanticVectorManager, BreedDescriptionVector
|
| 22 |
from user_query_analyzer import UserQueryAnalyzer
|
| 23 |
from matching_score_calculator import MatchingScoreCalculator
|
|
|
|
| 24 |
|
| 25 |
class SemanticBreedRecommender:
|
| 26 |
"""
|
|
@@ -49,17 +51,23 @@ class SemanticBreedRecommender:
|
|
| 49 |
# 初始化增強系統組件(如果可用)
|
| 50 |
try:
|
| 51 |
self.query_engine = QueryUnderstandingEngine()
|
|
|
|
| 52 |
self.constraint_manager = ConstraintManager()
|
|
|
|
| 53 |
self.multi_head_scorer = None
|
| 54 |
self.score_calibrator = ScoreCalibrator()
|
|
|
|
| 55 |
self.config_manager = get_config_manager()
|
| 56 |
|
| 57 |
# 如果 SBERT 模型可用,初始化多頭評分器
|
| 58 |
if self.sbert_model:
|
| 59 |
self.multi_head_scorer = MultiHeadScorer(self.sbert_model)
|
| 60 |
print("Multi-head scorer initialized with SBERT model")
|
| 61 |
-
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
| 63 |
self.query_engine = None
|
| 64 |
self.constraint_manager = None
|
| 65 |
self.multi_head_scorer = None
|
|
@@ -107,6 +115,444 @@ class SemanticBreedRecommender:
|
|
| 107 |
"""當增強系統失敗時獲取備用推薦"""
|
| 108 |
return self.score_calculator.get_fallback_recommendations(top_k)
|
| 109 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
def get_enhanced_semantic_recommendations(self, user_input: str, top_k: int = 15) -> List[Dict[str, Any]]:
|
| 111 |
"""
|
| 112 |
增強的多維度語義品種推薦
|
|
@@ -149,9 +595,16 @@ class SemanticBreedRecommender:
|
|
| 149 |
if self.multi_head_scorer:
|
| 150 |
breed_scores = self.multi_head_scorer.score_breeds(filter_result.passed_breeds, dimensions)
|
| 151 |
print(f"Multi-head scoring completed for {len(breed_scores)} breeds")
|
|
|
|
|
|
|
|
|
|
| 152 |
else:
|
| 153 |
-
|
| 154 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
|
| 156 |
# 階段 4: 分數校準
|
| 157 |
if self.score_calibrator:
|
|
@@ -184,6 +637,19 @@ class SemanticBreedRecommender:
|
|
| 184 |
else:
|
| 185 |
breed_info = get_dog_description(breed_name.replace(' ', '_')) or {}
|
| 186 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
recommendation = {
|
| 188 |
'breed': breed_name,
|
| 189 |
'rank': i + 1,
|
|
@@ -194,6 +660,7 @@ class SemanticBreedRecommender:
|
|
| 194 |
'bidirectional_bonus': breed_score.bidirectional_bonus,
|
| 195 |
'confidence_score': breed_score.confidence_score,
|
| 196 |
'dimensional_breakdown': breed_score.dimensional_breakdown,
|
|
|
|
| 197 |
'explanation': breed_score.explanation,
|
| 198 |
'size': breed_info.get('Size', 'Unknown'),
|
| 199 |
'temperament': breed_info.get('Temperament', ''),
|
|
@@ -417,15 +884,45 @@ class SemanticBreedRecommender:
|
|
| 417 |
'lifestyle_bonus': breed_data['lifestyle_bonus']
|
| 418 |
})
|
| 419 |
|
| 420 |
-
#
|
| 421 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 422 |
top_breeds = breed_display_scores[:top_k]
|
| 423 |
|
| 424 |
# 轉換為標準推薦格式
|
| 425 |
recommendations = []
|
| 426 |
for i, breed_data in enumerate(top_breeds):
|
| 427 |
breed = breed_data['breed']
|
| 428 |
-
|
|
|
|
| 429 |
|
| 430 |
# 獲取詳細信息
|
| 431 |
breed_info = get_dog_description(breed)
|
|
@@ -433,8 +930,8 @@ class SemanticBreedRecommender:
|
|
| 433 |
recommendation = {
|
| 434 |
'breed': breed.replace('_', ' '),
|
| 435 |
'rank': i + 1,
|
| 436 |
-
'overall_score':
|
| 437 |
-
'final_score':
|
| 438 |
'semantic_score': breed_data['semantic_score'],
|
| 439 |
'comparative_bonus': breed_data['comparative_bonus'],
|
| 440 |
'lifestyle_bonus': breed_data['lifestyle_bonus'],
|
|
@@ -445,7 +942,8 @@ class SemanticBreedRecommender:
|
|
| 445 |
'good_with_children': breed_info.get('Good with Children', 'Yes') if breed_info else 'Yes',
|
| 446 |
'lifespan': breed_info.get('Lifespan', '10-12 years') if breed_info else '10-12 years',
|
| 447 |
'description': breed_info.get('Description', '') if breed_info else '',
|
| 448 |
-
'search_type': 'description'
|
|
|
|
| 449 |
}
|
| 450 |
|
| 451 |
recommendations.append(recommendation)
|
|
@@ -459,12 +957,20 @@ class SemanticBreedRecommender:
|
|
| 459 |
return []
|
| 460 |
|
| 461 |
def get_enhanced_recommendations_with_unified_scoring(self, user_input: str, top_k: int = 15) -> List[Dict[str, Any]]:
|
| 462 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 463 |
try:
|
| 464 |
-
print(f"Processing enhanced recommendation: {user_input[:50]}...")
|
| 465 |
|
| 466 |
-
#
|
| 467 |
-
return self.
|
| 468 |
|
| 469 |
except Exception as e:
|
| 470 |
error_msg = f"Enhanced recommendation error: {str(e)}. Please check your description."
|
|
@@ -630,65 +1136,61 @@ def get_breed_recommendations_by_description(user_description: str,
|
|
| 630 |
|
| 631 |
|
| 632 |
def get_enhanced_recommendations_with_unified_scoring(user_description: str, top_k: int = 15) -> List[Dict[str, Any]]:
|
| 633 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 634 |
try:
|
| 635 |
-
print(f"Processing description-based recommendation: {user_description[:50]}...")
|
| 636 |
|
| 637 |
-
#
|
| 638 |
recommender = SemanticBreedRecommender()
|
| 639 |
|
|
|
|
| 640 |
if not recommender.vector_manager.is_model_available():
|
| 641 |
print("SBERT model not available, using basic text matching...")
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
recommendations = []
|
| 647 |
-
user_embedding = recommender.vector_manager.encode_text(user_description)
|
| 648 |
-
|
| 649 |
-
# 計算所有品種的增強分數
|
| 650 |
-
all_breed_scores = []
|
| 651 |
-
for breed_name, breed_vector in recommender.breed_vectors.items():
|
| 652 |
-
breed_embedding = breed_vector.embedding
|
| 653 |
-
similarity = cosine_similarity([user_embedding], [breed_embedding])[0][0]
|
| 654 |
-
|
| 655 |
-
# 獲取品種資料
|
| 656 |
-
breed_info = get_dog_description(breed_name) or {}
|
| 657 |
-
|
| 658 |
-
# 計算增強的匹配分數
|
| 659 |
-
enhanced_score = recommender.score_calculator.calculate_enhanced_matching_score(
|
| 660 |
-
breed_name, breed_info, user_description, similarity
|
| 661 |
-
)
|
| 662 |
-
|
| 663 |
-
all_breed_scores.append((breed_name, enhanced_score, breed_info, similarity))
|
| 664 |
|
| 665 |
-
#
|
| 666 |
-
|
| 667 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 668 |
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
'size': breed_info.get('Size', 'Unknown'),
|
| 679 |
-
'temperament': breed_info.get('Temperament', 'Unknown'),
|
| 680 |
-
'exercise_needs': breed_info.get('Exercise Needs', 'Moderate'),
|
| 681 |
-
'grooming_needs': breed_info.get('Grooming Needs', 'Moderate'),
|
| 682 |
-
'good_with_children': breed_info.get('Good with Children', 'Unknown'),
|
| 683 |
-
'lifespan': breed_info.get('Lifespan', '10-12 years'),
|
| 684 |
-
'description': breed_info.get('Description', 'No description available'),
|
| 685 |
-
'search_type': 'description',
|
| 686 |
-
'scores': enhanced_score['dimension_scores']
|
| 687 |
-
}
|
| 688 |
-
recommendations.append(recommendation)
|
| 689 |
|
| 690 |
-
|
| 691 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 692 |
|
| 693 |
except Exception as e:
|
| 694 |
error_msg = f"Error in semantic recommendation system: {str(e)}. Please check your input and try again."
|
|
@@ -730,6 +1232,29 @@ def _get_basic_text_matching_recommendations(user_description: str, top_k: int =
|
|
| 730 |
'Japanese_Spaniel', 'Toy_Terrier', 'Affenpinscher', 'Pekingese', 'Lhasa'
|
| 731 |
]
|
| 732 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 733 |
for breed in basic_breeds:
|
| 734 |
breed_info = get_dog_description(breed) or {}
|
| 735 |
breed_text = f"{breed} {breed_info.get('Temperament', '')} {breed_info.get('Size', '')} {breed_info.get('Description', '')}".lower()
|
|
|
|
| 1 |
+
# %%writefile semantic_breed_recommender.py
|
| 2 |
import random
|
| 3 |
import hashlib
|
| 4 |
import numpy as np
|
|
|
|
| 22 |
from semantic_vector_manager import SemanticVectorManager, BreedDescriptionVector
|
| 23 |
from user_query_analyzer import UserQueryAnalyzer
|
| 24 |
from matching_score_calculator import MatchingScoreCalculator
|
| 25 |
+
from smart_breed_filter import apply_smart_filtering
|
| 26 |
|
| 27 |
class SemanticBreedRecommender:
|
| 28 |
"""
|
|
|
|
| 51 |
# 初始化增強系統組件(如果可用)
|
| 52 |
try:
|
| 53 |
self.query_engine = QueryUnderstandingEngine()
|
| 54 |
+
print("QueryUnderstandingEngine initialized")
|
| 55 |
self.constraint_manager = ConstraintManager()
|
| 56 |
+
print("ConstraintManager initialized")
|
| 57 |
self.multi_head_scorer = None
|
| 58 |
self.score_calibrator = ScoreCalibrator()
|
| 59 |
+
print("ScoreCalibrator initialized")
|
| 60 |
self.config_manager = get_config_manager()
|
| 61 |
|
| 62 |
# 如果 SBERT 模型可用,初始化多頭評分器
|
| 63 |
if self.sbert_model:
|
| 64 |
self.multi_head_scorer = MultiHeadScorer(self.sbert_model)
|
| 65 |
print("Multi-head scorer initialized with SBERT model")
|
| 66 |
+
else:
|
| 67 |
+
print("WARNING: SBERT model not available, multi_head_scorer will be None")
|
| 68 |
+
except Exception as e:
|
| 69 |
+
print(f"Error initializing enhanced system components: {str(e)}")
|
| 70 |
+
print(traceback.format_exc())
|
| 71 |
self.query_engine = None
|
| 72 |
self.constraint_manager = None
|
| 73 |
self.multi_head_scorer = None
|
|
|
|
| 115 |
"""當增強系統失敗時獲取備用推薦"""
|
| 116 |
return self.score_calculator.get_fallback_recommendations(top_k)
|
| 117 |
|
| 118 |
+
def _get_fallback_scoring_with_constraints(self, user_input: str,
|
| 119 |
+
passed_breeds: set,
|
| 120 |
+
dimensions: 'QueryDimensions',
|
| 121 |
+
top_k: int = 15) -> List[Dict[str, Any]]:
|
| 122 |
+
"""
|
| 123 |
+
當 multi_head_scorer 不可用時的回退評分方法
|
| 124 |
+
關鍵:仍然尊重 constraint_manager 的過濾結果,並產生自然分佈的分數
|
| 125 |
+
"""
|
| 126 |
+
print(f"Fallback scoring for {len(passed_breeds)} filtered breeds")
|
| 127 |
+
|
| 128 |
+
recommendations = []
|
| 129 |
+
user_text = user_input.lower()
|
| 130 |
+
|
| 131 |
+
# 提取用戶需求關鍵詞
|
| 132 |
+
lifestyle_keywords = self._extract_lifestyle_keywords(user_input)
|
| 133 |
+
|
| 134 |
+
for breed in passed_breeds:
|
| 135 |
+
breed_info = get_dog_description(breed.replace(' ', '_')) or {}
|
| 136 |
+
if not breed_info:
|
| 137 |
+
continue
|
| 138 |
+
|
| 139 |
+
# 計算多維度匹配分數
|
| 140 |
+
dimension_scores = self._calculate_comprehensive_dimension_scores(
|
| 141 |
+
breed, breed_info, user_text, dimensions, lifestyle_keywords
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
# 基於維度分數計算加權總分
|
| 145 |
+
weights = self._get_dimension_weights_from_query(user_text, dimensions)
|
| 146 |
+
weighted_sum = sum(dimension_scores.get(dim, 0.7) * weights.get(dim, 1.0)
|
| 147 |
+
for dim in dimension_scores)
|
| 148 |
+
total_weight = sum(weights.get(dim, 1.0) for dim in dimension_scores)
|
| 149 |
+
final_score = weighted_sum / total_weight if total_weight > 0 else 0.7
|
| 150 |
+
|
| 151 |
+
# 確保分數在合理範圍內(允許高分,非常契合的品種可超過 90%)
|
| 152 |
+
final_score = max(0.45, min(0.98, final_score))
|
| 153 |
+
dimension_scores['overall'] = final_score
|
| 154 |
+
|
| 155 |
+
recommendation = {
|
| 156 |
+
'breed': breed.replace('_', ' '),
|
| 157 |
+
'rank': 0,
|
| 158 |
+
'overall_score': final_score,
|
| 159 |
+
'final_score': final_score,
|
| 160 |
+
'scores': dimension_scores,
|
| 161 |
+
'size': breed_info.get('Size', 'Unknown'),
|
| 162 |
+
'temperament': breed_info.get('Temperament', ''),
|
| 163 |
+
'exercise_needs': breed_info.get('Exercise Needs', 'Moderate'),
|
| 164 |
+
'grooming_needs': breed_info.get('Grooming Needs', 'Moderate'),
|
| 165 |
+
'good_with_children': breed_info.get('Good with Children', 'Yes'),
|
| 166 |
+
'lifespan': breed_info.get('Lifespan', '10-12 years'),
|
| 167 |
+
'description': breed_info.get('Description', ''),
|
| 168 |
+
'search_type': 'fallback_with_constraints',
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
recommendations.append(recommendation)
|
| 172 |
+
|
| 173 |
+
# 按分數排序
|
| 174 |
+
recommendations.sort(key=lambda x: -x['final_score'])
|
| 175 |
+
|
| 176 |
+
# 更新排名
|
| 177 |
+
for i, rec in enumerate(recommendations[:top_k]):
|
| 178 |
+
rec['rank'] = i + 1
|
| 179 |
+
|
| 180 |
+
print(f"Generated {len(recommendations[:top_k])} fallback recommendations")
|
| 181 |
+
return recommendations[:top_k]
|
| 182 |
+
|
| 183 |
+
def _calculate_comprehensive_dimension_scores(self, breed: str, breed_info: Dict,
|
| 184 |
+
user_text: str, dimensions,
|
| 185 |
+
lifestyle_keywords: Dict) -> Dict[str, float]:
|
| 186 |
+
"""
|
| 187 |
+
計算全面的維度分數,產生自然分佈的評分
|
| 188 |
+
"""
|
| 189 |
+
scores = {}
|
| 190 |
+
temperament = breed_info.get('Temperament', '').lower()
|
| 191 |
+
size = breed_info.get('Size', 'Medium').lower()
|
| 192 |
+
exercise_needs = breed_info.get('Exercise Needs', 'Moderate').lower()
|
| 193 |
+
grooming_needs = breed_info.get('Grooming Needs', 'Moderate').lower()
|
| 194 |
+
good_with_children = breed_info.get('Good with Children', 'Yes')
|
| 195 |
+
care_level = breed_info.get('Care Level', 'Moderate').lower()
|
| 196 |
+
description = breed_info.get('Description', '').lower()
|
| 197 |
+
|
| 198 |
+
# 1. 空間相容性
|
| 199 |
+
space_score = 0.7
|
| 200 |
+
if 'apartment' in user_text or 'small space' in user_text:
|
| 201 |
+
if 'small' in size or 'toy' in size:
|
| 202 |
+
space_score = 0.96
|
| 203 |
+
elif 'medium' in size:
|
| 204 |
+
space_score = 0.78
|
| 205 |
+
elif 'large' in size:
|
| 206 |
+
space_score = 0.52
|
| 207 |
+
else:
|
| 208 |
+
space_score = 0.45
|
| 209 |
+
elif 'house' in user_text or 'yard' in user_text:
|
| 210 |
+
if 'large' in size:
|
| 211 |
+
space_score = 0.92
|
| 212 |
+
elif 'medium' in size:
|
| 213 |
+
space_score = 0.88
|
| 214 |
+
else:
|
| 215 |
+
space_score = 0.82
|
| 216 |
+
scores['space'] = space_score
|
| 217 |
+
|
| 218 |
+
# 2. 運動相容性
|
| 219 |
+
exercise_score = 0.7
|
| 220 |
+
user_wants_high = any(w in user_text for w in ['energetic', 'active', 'running', 'hiking', 'athletic'])
|
| 221 |
+
user_wants_low = any(w in user_text for w in ['low maintenance', 'relaxed', 'calm', 'couch'])
|
| 222 |
+
|
| 223 |
+
if user_wants_high:
|
| 224 |
+
if 'very high' in exercise_needs:
|
| 225 |
+
exercise_score = 0.98
|
| 226 |
+
elif 'high' in exercise_needs:
|
| 227 |
+
exercise_score = 0.92
|
| 228 |
+
elif 'moderate' in exercise_needs:
|
| 229 |
+
exercise_score = 0.68
|
| 230 |
+
else:
|
| 231 |
+
exercise_score = 0.48
|
| 232 |
+
elif user_wants_low:
|
| 233 |
+
if 'low' in exercise_needs:
|
| 234 |
+
exercise_score = 0.96
|
| 235 |
+
elif 'moderate' in exercise_needs:
|
| 236 |
+
exercise_score = 0.78
|
| 237 |
+
elif 'high' in exercise_needs:
|
| 238 |
+
exercise_score = 0.52
|
| 239 |
+
else:
|
| 240 |
+
exercise_score = 0.42
|
| 241 |
+
else:
|
| 242 |
+
# 中等運動需求
|
| 243 |
+
if 'moderate' in exercise_needs:
|
| 244 |
+
exercise_score = 0.88
|
| 245 |
+
elif 'low' in exercise_needs or 'high' in exercise_needs:
|
| 246 |
+
exercise_score = 0.72
|
| 247 |
+
else:
|
| 248 |
+
exercise_score = 0.65
|
| 249 |
+
scores['exercise'] = exercise_score
|
| 250 |
+
|
| 251 |
+
# 3. 美容需求相容性
|
| 252 |
+
grooming_score = 0.7
|
| 253 |
+
user_wants_low_maintenance = any(w in user_text for w in ['low maintenance', 'easy care', 'minimal grooming'])
|
| 254 |
+
|
| 255 |
+
if user_wants_low_maintenance:
|
| 256 |
+
if 'low' in grooming_needs or 'minimal' in grooming_needs:
|
| 257 |
+
grooming_score = 0.96
|
| 258 |
+
elif 'moderate' in grooming_needs:
|
| 259 |
+
grooming_score = 0.75
|
| 260 |
+
else:
|
| 261 |
+
grooming_score = 0.50
|
| 262 |
+
else:
|
| 263 |
+
if 'low' in grooming_needs:
|
| 264 |
+
grooming_score = 0.85
|
| 265 |
+
elif 'moderate' in grooming_needs:
|
| 266 |
+
grooming_score = 0.78
|
| 267 |
+
else:
|
| 268 |
+
grooming_score = 0.70
|
| 269 |
+
scores['grooming'] = grooming_score
|
| 270 |
+
|
| 271 |
+
# 4. 噪音相容性
|
| 272 |
+
noise_score = 0.7
|
| 273 |
+
user_wants_quiet = any(w in user_text for w in ['quiet', 'silent', 'noise', 'bark', 'neighbors'])
|
| 274 |
+
|
| 275 |
+
if user_wants_quiet:
|
| 276 |
+
# 從 breed_noise_info 獲取噪音資訊
|
| 277 |
+
noise_info = breed_noise_info.get(breed.replace(' ', '_'), {})
|
| 278 |
+
noise_level = noise_info.get('noise_level', 'Moderate').lower()
|
| 279 |
+
|
| 280 |
+
if 'low' in noise_level or 'quiet' in noise_level:
|
| 281 |
+
noise_score = 0.97
|
| 282 |
+
elif 'moderate' in noise_level:
|
| 283 |
+
noise_score = 0.72
|
| 284 |
+
elif 'high' in noise_level:
|
| 285 |
+
noise_score = 0.45
|
| 286 |
+
else:
|
| 287 |
+
# 根據性格推斷
|
| 288 |
+
if any(w in temperament for w in ['calm', 'quiet', 'gentle', 'reserved']):
|
| 289 |
+
noise_score = 0.88
|
| 290 |
+
elif any(w in temperament for w in ['alert', 'vocal', 'energetic']):
|
| 291 |
+
noise_score = 0.55
|
| 292 |
+
else:
|
| 293 |
+
noise_score = 0.70
|
| 294 |
+
scores['noise'] = noise_score
|
| 295 |
+
|
| 296 |
+
# 5. 家庭相容性
|
| 297 |
+
family_score = 0.7
|
| 298 |
+
has_family_context = any(w in user_text for w in ['kids', 'children', 'family', 'child'])
|
| 299 |
+
|
| 300 |
+
if has_family_context:
|
| 301 |
+
if good_with_children == 'Yes':
|
| 302 |
+
family_score = 0.94
|
| 303 |
+
# 額外加分:溫和性格
|
| 304 |
+
if any(w in temperament for w in ['gentle', 'friendly', 'patient', 'loving']):
|
| 305 |
+
family_score = min(0.98, family_score + 0.04)
|
| 306 |
+
elif good_with_children == 'No':
|
| 307 |
+
family_score = 0.32
|
| 308 |
+
else:
|
| 309 |
+
family_score = 0.62
|
| 310 |
+
else:
|
| 311 |
+
family_score = 0.76 if good_with_children == 'Yes' else 0.70
|
| 312 |
+
scores['family'] = family_score
|
| 313 |
+
|
| 314 |
+
# 6. 經驗相容性
|
| 315 |
+
experience_score = 0.7
|
| 316 |
+
is_beginner = any(w in user_text for w in ['first dog', 'first time', 'beginner', 'new owner', 'never had'])
|
| 317 |
+
|
| 318 |
+
if is_beginner:
|
| 319 |
+
# 評估品種對新手的友好程度
|
| 320 |
+
if 'low' in care_level or 'easy' in care_level:
|
| 321 |
+
experience_score = 0.94
|
| 322 |
+
elif 'moderate' in care_level:
|
| 323 |
+
experience_score = 0.78
|
| 324 |
+
else:
|
| 325 |
+
experience_score = 0.52
|
| 326 |
+
|
| 327 |
+
# 性格調整
|
| 328 |
+
if any(w in temperament for w in ['eager to please', 'trainable', 'intelligent', 'friendly']):
|
| 329 |
+
experience_score = min(0.98, experience_score + 0.08)
|
| 330 |
+
if any(w in temperament for w in ['stubborn', 'independent', 'strong-willed']):
|
| 331 |
+
experience_score = max(0.38, experience_score - 0.18)
|
| 332 |
+
else:
|
| 333 |
+
experience_score = 0.80
|
| 334 |
+
scores['experience'] = experience_score
|
| 335 |
+
|
| 336 |
+
# 7. 健康分數(基於壽命和品種特性)
|
| 337 |
+
health_score = 0.75
|
| 338 |
+
lifespan = breed_info.get('Lifespan', '10-12 years')
|
| 339 |
+
try:
|
| 340 |
+
# 解析壽命
|
| 341 |
+
years = [int(y) for y in lifespan.replace(' years', '').split('-') if y.strip().isdigit()]
|
| 342 |
+
if years:
|
| 343 |
+
avg_lifespan = sum(years) / len(years)
|
| 344 |
+
if avg_lifespan >= 14:
|
| 345 |
+
health_score = 0.94
|
| 346 |
+
elif avg_lifespan >= 12:
|
| 347 |
+
health_score = 0.85
|
| 348 |
+
elif avg_lifespan >= 10:
|
| 349 |
+
health_score = 0.75
|
| 350 |
+
else:
|
| 351 |
+
health_score = 0.62
|
| 352 |
+
except:
|
| 353 |
+
pass
|
| 354 |
+
scores['health'] = health_score
|
| 355 |
+
|
| 356 |
+
return scores
|
| 357 |
+
|
| 358 |
+
def _get_dimension_weights_from_query(self, user_text: str, dimensions) -> Dict[str, float]:
|
| 359 |
+
"""
|
| 360 |
+
根據用戶查詢動態計算維度權重
|
| 361 |
+
"""
|
| 362 |
+
weights = {
|
| 363 |
+
'space': 1.0,
|
| 364 |
+
'exercise': 1.0,
|
| 365 |
+
'grooming': 1.0,
|
| 366 |
+
'noise': 1.0,
|
| 367 |
+
'family': 1.0,
|
| 368 |
+
'experience': 1.0,
|
| 369 |
+
'health': 0.8
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
# 根據 dimensions 的 priority 調整權重
|
| 373 |
+
if hasattr(dimensions, 'dimension_priorities'):
|
| 374 |
+
priority_map = getattr(dimensions, 'dimension_priorities', {})
|
| 375 |
+
for dim, priority in priority_map.items():
|
| 376 |
+
if dim in weights:
|
| 377 |
+
weights[dim] = priority
|
| 378 |
+
# 映射不同名稱
|
| 379 |
+
if dim == 'size':
|
| 380 |
+
weights['space'] = max(weights['space'], priority)
|
| 381 |
+
if dim == 'family':
|
| 382 |
+
weights['family'] = max(weights['family'], priority)
|
| 383 |
+
|
| 384 |
+
# 根據關鍵詞強化權重
|
| 385 |
+
if any(w in user_text for w in ['quiet', 'noise', 'bark', 'neighbors', 'thin walls']):
|
| 386 |
+
weights['noise'] = max(weights['noise'], 2.2)
|
| 387 |
+
if any(w in user_text for w in ['kids', 'children', 'family', 'child']):
|
| 388 |
+
weights['family'] = max(weights['family'], 2.0)
|
| 389 |
+
if any(w in user_text for w in ['first', 'beginner', 'new owner']):
|
| 390 |
+
weights['experience'] = max(weights['experience'], 2.0)
|
| 391 |
+
if any(w in user_text for w in ['apartment', 'small space', 'studio']):
|
| 392 |
+
weights['space'] = max(weights['space'], 1.8)
|
| 393 |
+
if any(w in user_text for w in ['energetic', 'active', 'running', 'hiking']):
|
| 394 |
+
weights['exercise'] = max(weights['exercise'], 2.0)
|
| 395 |
+
if any(w in user_text for w in ['low maintenance', 'easy care']):
|
| 396 |
+
weights['grooming'] = max(weights['grooming'], 1.8)
|
| 397 |
+
|
| 398 |
+
return weights
|
| 399 |
+
|
| 400 |
+
def _calculate_real_dimension_scores(self, breed: str, breed_info: Dict,
|
| 401 |
+
user_input: str, overall_score: float) -> Dict[str, float]:
|
| 402 |
+
"""
|
| 403 |
+
計算真實的維度分數(基於品種特性和用戶需求)
|
| 404 |
+
這個方法取代了假分數生成器,提供真實的評分
|
| 405 |
+
|
| 406 |
+
Args:
|
| 407 |
+
breed: 品種名稱
|
| 408 |
+
breed_info: 品種資訊字典
|
| 409 |
+
user_input: 用戶輸入文字
|
| 410 |
+
overall_score: 總體分數
|
| 411 |
+
|
| 412 |
+
Returns:
|
| 413 |
+
Dict[str, float]: 維度分數字典
|
| 414 |
+
"""
|
| 415 |
+
if not breed_info:
|
| 416 |
+
breed_info = {}
|
| 417 |
+
|
| 418 |
+
user_text = user_input.lower()
|
| 419 |
+
temperament = breed_info.get('Temperament', '').lower()
|
| 420 |
+
size = breed_info.get('Size', 'Medium').lower()
|
| 421 |
+
exercise_needs = breed_info.get('Exercise Needs', 'Moderate').lower()
|
| 422 |
+
grooming_needs = breed_info.get('Grooming Needs', 'Moderate').lower()
|
| 423 |
+
good_with_children = breed_info.get('Good with Children', 'Yes')
|
| 424 |
+
care_level = breed_info.get('Care Level', 'Moderate').lower()
|
| 425 |
+
|
| 426 |
+
scores = {}
|
| 427 |
+
|
| 428 |
+
# 1. Space Compatibility (空間相容性)
|
| 429 |
+
space_score = 0.7
|
| 430 |
+
if 'apartment' in user_text or 'small' in user_text:
|
| 431 |
+
if 'small' in size:
|
| 432 |
+
space_score = 0.9
|
| 433 |
+
elif 'medium' in size:
|
| 434 |
+
space_score = 0.7
|
| 435 |
+
elif 'large' in size:
|
| 436 |
+
space_score = 0.5
|
| 437 |
+
elif 'giant' in size:
|
| 438 |
+
space_score = 0.3
|
| 439 |
+
elif 'house' in user_text or 'yard' in user_text:
|
| 440 |
+
if 'large' in size or 'giant' in size:
|
| 441 |
+
space_score = 0.85
|
| 442 |
+
else:
|
| 443 |
+
space_score = 0.8
|
| 444 |
+
scores['space'] = space_score
|
| 445 |
+
|
| 446 |
+
# 2. Exercise Compatibility (運動相容性)
|
| 447 |
+
exercise_score = 0.7
|
| 448 |
+
if 'low' in exercise_needs or 'minimal' in exercise_needs:
|
| 449 |
+
if any(term in user_text for term in ['work full time', 'busy', 'low exercise', 'not much exercise']):
|
| 450 |
+
exercise_score = 0.9
|
| 451 |
+
else:
|
| 452 |
+
exercise_score = 0.75
|
| 453 |
+
elif 'high' in exercise_needs or 'very high' in exercise_needs:
|
| 454 |
+
if any(term in user_text for term in ['active', 'running', 'hiking', 'exercise']):
|
| 455 |
+
exercise_score = 0.9
|
| 456 |
+
elif any(term in user_text for term in ['work full time', 'busy']):
|
| 457 |
+
exercise_score = 0.5
|
| 458 |
+
else:
|
| 459 |
+
exercise_score = 0.65
|
| 460 |
+
else: # moderate
|
| 461 |
+
exercise_score = 0.75
|
| 462 |
+
scores['exercise'] = exercise_score
|
| 463 |
+
|
| 464 |
+
# 3. Grooming/Maintenance Compatibility (美容/維護相容性)
|
| 465 |
+
grooming_score = 0.7
|
| 466 |
+
if 'low' in grooming_needs:
|
| 467 |
+
if any(term in user_text for term in ['low maintenance', 'low-maintenance', 'easy care', 'minimal grooming']):
|
| 468 |
+
grooming_score = 0.9
|
| 469 |
+
else:
|
| 470 |
+
grooming_score = 0.8
|
| 471 |
+
elif 'high' in grooming_needs:
|
| 472 |
+
if any(term in user_text for term in ['low maintenance', 'low-maintenance', 'easy care']):
|
| 473 |
+
grooming_score = 0.4
|
| 474 |
+
else:
|
| 475 |
+
grooming_score = 0.6
|
| 476 |
+
|
| 477 |
+
# 敏感品種需要額外照顧
|
| 478 |
+
if 'sensitive' in temperament:
|
| 479 |
+
grooming_score -= 0.1
|
| 480 |
+
# 特殊品種需要額外護理
|
| 481 |
+
breed_lower = breed.lower()
|
| 482 |
+
if any(term in breed_lower for term in ['italian', 'greyhound', 'whippet', 'hairless']):
|
| 483 |
+
if any(term in user_text for term in ['low maintenance', 'low-maintenance', 'easy']):
|
| 484 |
+
grooming_score -= 0.15
|
| 485 |
+
scores['grooming'] = max(0.2, grooming_score)
|
| 486 |
+
|
| 487 |
+
# 4. Experience Compatibility (經驗相容性) - 關鍵維度!
|
| 488 |
+
experience_score = 0.7
|
| 489 |
+
is_beginner = any(term in user_text for term in ['first dog', 'first time', 'beginner', 'new to dogs', 'never owned', 'never had'])
|
| 490 |
+
|
| 491 |
+
if is_beginner:
|
| 492 |
+
# 新手評估
|
| 493 |
+
if 'low' in care_level:
|
| 494 |
+
experience_score = 0.85
|
| 495 |
+
elif 'moderate' in care_level:
|
| 496 |
+
experience_score = 0.65
|
| 497 |
+
elif 'high' in care_level:
|
| 498 |
+
experience_score = 0.45
|
| 499 |
+
|
| 500 |
+
# 性格懲罰 - 對新手很重要
|
| 501 |
+
difficult_traits = ['sensitive', 'stubborn', 'independent', 'dominant', 'aggressive', 'nervous', 'shy', 'timid', 'alert']
|
| 502 |
+
for trait in difficult_traits:
|
| 503 |
+
if trait in temperament:
|
| 504 |
+
if trait == 'sensitive':
|
| 505 |
+
experience_score -= 0.15 # 敏感性格對新手很具挑戰
|
| 506 |
+
elif trait == 'aggressive':
|
| 507 |
+
experience_score -= 0.25
|
| 508 |
+
elif trait in ['stubborn', 'independent', 'dominant']:
|
| 509 |
+
experience_score -= 0.12
|
| 510 |
+
else:
|
| 511 |
+
experience_score -= 0.08
|
| 512 |
+
|
| 513 |
+
# 友善性格獎勵
|
| 514 |
+
easy_traits = ['friendly', 'gentle', 'eager to please', 'patient', 'calm', 'outgoing']
|
| 515 |
+
for trait in easy_traits:
|
| 516 |
+
if trait in temperament:
|
| 517 |
+
experience_score += 0.08
|
| 518 |
+
|
| 519 |
+
# 易於訓練的加分
|
| 520 |
+
if any(term in user_text for term in ['easy to train', 'trainable']):
|
| 521 |
+
if any(term in temperament for term in ['eager to please', 'intelligent', 'trainable']):
|
| 522 |
+
experience_score += 0.1
|
| 523 |
+
elif any(term in temperament for term in ['stubborn', 'independent']):
|
| 524 |
+
experience_score -= 0.1
|
| 525 |
+
else:
|
| 526 |
+
# 有經驗的飼主
|
| 527 |
+
experience_score = 0.8
|
| 528 |
+
|
| 529 |
+
scores['experience'] = max(0.2, min(0.95, experience_score))
|
| 530 |
+
|
| 531 |
+
# 5. Noise Compatibility (噪音相容性)
|
| 532 |
+
noise_score = 0.75
|
| 533 |
+
if any(term in user_text for term in ['quiet', 'apartment', 'neighbors']):
|
| 534 |
+
if any(term in temperament for term in ['quiet', 'calm', 'gentle']):
|
| 535 |
+
noise_score = 0.9
|
| 536 |
+
elif any(term in temperament for term in ['alert', 'vocal', 'barking']):
|
| 537 |
+
noise_score = 0.5
|
| 538 |
+
scores['noise'] = noise_score
|
| 539 |
+
|
| 540 |
+
# 6. Family Compatibility (家庭相容性)
|
| 541 |
+
family_score = 0.7
|
| 542 |
+
if any(term in user_text for term in ['children', 'kids', 'family']):
|
| 543 |
+
if good_with_children == 'Yes' or good_with_children == True:
|
| 544 |
+
family_score = 0.9
|
| 545 |
+
if any(term in temperament for term in ['gentle', 'patient', 'friendly']):
|
| 546 |
+
family_score = 0.95
|
| 547 |
+
else:
|
| 548 |
+
family_score = 0.35
|
| 549 |
+
scores['family'] = family_score
|
| 550 |
+
|
| 551 |
+
# 7. Overall
|
| 552 |
+
scores['overall'] = overall_score
|
| 553 |
+
|
| 554 |
+
return scores
|
| 555 |
+
|
| 556 |
def get_enhanced_semantic_recommendations(self, user_input: str, top_k: int = 15) -> List[Dict[str, Any]]:
|
| 557 |
"""
|
| 558 |
增強的多維度語義品種推薦
|
|
|
|
| 595 |
if self.multi_head_scorer:
|
| 596 |
breed_scores = self.multi_head_scorer.score_breeds(filter_result.passed_breeds, dimensions)
|
| 597 |
print(f"Multi-head scoring completed for {len(breed_scores)} breeds")
|
| 598 |
+
# Debug: 顯示前5名的分數和維度breakdown
|
| 599 |
+
for bs in breed_scores[:5]:
|
| 600 |
+
print(f" {bs.breed_name}: final={bs.final_score:.3f}, breakdown={bs.dimensional_breakdown}")
|
| 601 |
else:
|
| 602 |
+
# 使用回退評分,但仍然尊重 constraint 過濾結果
|
| 603 |
+
print("Multi-head scorer not available, using fallback scoring with constraint filtering")
|
| 604 |
+
fallback_results = self._get_fallback_scoring_with_constraints(
|
| 605 |
+
user_input, filter_result.passed_breeds, dimensions, top_k
|
| 606 |
+
)
|
| 607 |
+
return fallback_results
|
| 608 |
|
| 609 |
# 階段 4: 分數校準
|
| 610 |
if self.score_calibrator:
|
|
|
|
| 637 |
else:
|
| 638 |
breed_info = get_dog_description(breed_name.replace(' ', '_')) or {}
|
| 639 |
|
| 640 |
+
# 將 dimensional_breakdown 轉換為 UI 需要的 scores 格式
|
| 641 |
+
breakdown = breed_score.dimensional_breakdown or {}
|
| 642 |
+
ui_scores = {
|
| 643 |
+
'space': breakdown.get('spatial_compatibility', 0.7),
|
| 644 |
+
'exercise': breakdown.get('activity_compatibility', 0.7),
|
| 645 |
+
'grooming': breakdown.get('maintenance_compatibility', 0.7),
|
| 646 |
+
'experience': breakdown.get('experience_compatibility', 0.7),
|
| 647 |
+
'noise': breakdown.get('noise_compatibility', 0.7),
|
| 648 |
+
'family': breakdown.get('family_compatibility', 0.7),
|
| 649 |
+
'health': breakdown.get('health_compatibility', 0.7),
|
| 650 |
+
'overall': calibrated_score
|
| 651 |
+
}
|
| 652 |
+
|
| 653 |
recommendation = {
|
| 654 |
'breed': breed_name,
|
| 655 |
'rank': i + 1,
|
|
|
|
| 660 |
'bidirectional_bonus': breed_score.bidirectional_bonus,
|
| 661 |
'confidence_score': breed_score.confidence_score,
|
| 662 |
'dimensional_breakdown': breed_score.dimensional_breakdown,
|
| 663 |
+
'scores': ui_scores, # UI 需要的格式
|
| 664 |
'explanation': breed_score.explanation,
|
| 665 |
'size': breed_info.get('Size', 'Unknown'),
|
| 666 |
'temperament': breed_info.get('Temperament', ''),
|
|
|
|
| 884 |
'lifestyle_bonus': breed_data['lifestyle_bonus']
|
| 885 |
})
|
| 886 |
|
| 887 |
+
# 計算真實維度分數並整合到排序中
|
| 888 |
+
for breed_data in breed_display_scores:
|
| 889 |
+
breed = breed_data['breed']
|
| 890 |
+
breed_info = get_dog_description(breed)
|
| 891 |
+
real_scores = self._calculate_real_dimension_scores(
|
| 892 |
+
breed, breed_info, user_input, breed_data['display_score']
|
| 893 |
+
)
|
| 894 |
+
breed_data['real_scores'] = real_scores
|
| 895 |
+
|
| 896 |
+
# 計算加權的最終分數(考慮維度分數)
|
| 897 |
+
# 原始顯示分數權重 50%,維度分數平均權重 50%
|
| 898 |
+
dim_scores = [real_scores.get('space', 0.7), real_scores.get('exercise', 0.7),
|
| 899 |
+
real_scores.get('grooming', 0.7), real_scores.get('experience', 0.7),
|
| 900 |
+
real_scores.get('noise', 0.7)]
|
| 901 |
+
avg_dim_score = sum(dim_scores) / len(dim_scores)
|
| 902 |
+
|
| 903 |
+
# 對低維度分數施加懲罰
|
| 904 |
+
min_dim_score = min(dim_scores)
|
| 905 |
+
penalty = 0
|
| 906 |
+
if min_dim_score < 0.5:
|
| 907 |
+
penalty = (0.5 - min_dim_score) * 0.3 # 最低分數懲罰
|
| 908 |
+
|
| 909 |
+
# 最終排序分數
|
| 910 |
+
breed_data['adjusted_score'] = (
|
| 911 |
+
breed_data['display_score'] * 0.5 +
|
| 912 |
+
avg_dim_score * 0.5 -
|
| 913 |
+
penalty
|
| 914 |
+
)
|
| 915 |
+
|
| 916 |
+
# 按調整後的分數排序
|
| 917 |
+
breed_display_scores.sort(key=lambda x: x['adjusted_score'], reverse=True)
|
| 918 |
top_breeds = breed_display_scores[:top_k]
|
| 919 |
|
| 920 |
# 轉換為標準推薦格式
|
| 921 |
recommendations = []
|
| 922 |
for i, breed_data in enumerate(top_breeds):
|
| 923 |
breed = breed_data['breed']
|
| 924 |
+
adjusted_score = breed_data['adjusted_score']
|
| 925 |
+
real_scores = breed_data['real_scores']
|
| 926 |
|
| 927 |
# 獲取詳細信息
|
| 928 |
breed_info = get_dog_description(breed)
|
|
|
|
| 930 |
recommendation = {
|
| 931 |
'breed': breed.replace('_', ' '),
|
| 932 |
'rank': i + 1,
|
| 933 |
+
'overall_score': adjusted_score, # 使用調整後的分數
|
| 934 |
+
'final_score': adjusted_score, # 確保 final_score 與 overall_score 匹配
|
| 935 |
'semantic_score': breed_data['semantic_score'],
|
| 936 |
'comparative_bonus': breed_data['comparative_bonus'],
|
| 937 |
'lifestyle_bonus': breed_data['lifestyle_bonus'],
|
|
|
|
| 942 |
'good_with_children': breed_info.get('Good with Children', 'Yes') if breed_info else 'Yes',
|
| 943 |
'lifespan': breed_info.get('Lifespan', '10-12 years') if breed_info else '10-12 years',
|
| 944 |
'description': breed_info.get('Description', '') if breed_info else '',
|
| 945 |
+
'search_type': 'description',
|
| 946 |
+
'scores': real_scores # 添加真實的維度分數
|
| 947 |
}
|
| 948 |
|
| 949 |
recommendations.append(recommendation)
|
|
|
|
| 957 |
return []
|
| 958 |
|
| 959 |
def get_enhanced_recommendations_with_unified_scoring(self, user_input: str, top_k: int = 15) -> List[Dict[str, Any]]:
|
| 960 |
+
"""
|
| 961 |
+
增強推薦方法 - 使用完整的多頭評分系統
|
| 962 |
+
|
| 963 |
+
這個方法使用:
|
| 964 |
+
- QueryUnderstandingEngine: 解析用戶意圖
|
| 965 |
+
- PriorityDetector: 檢測維度優先級
|
| 966 |
+
- MultiHeadScorer: 多維度評分
|
| 967 |
+
- DynamicWeightCalculator: 動態權重分配
|
| 968 |
+
"""
|
| 969 |
try:
|
| 970 |
+
print(f"Processing enhanced recommendation with multi-head scoring: {user_input[:50]}...")
|
| 971 |
|
| 972 |
+
# 使用完整的增強語義推薦系統(包含 multi_head_scorer)
|
| 973 |
+
return self.get_enhanced_semantic_recommendations(user_input, top_k)
|
| 974 |
|
| 975 |
except Exception as e:
|
| 976 |
error_msg = f"Enhanced recommendation error: {str(e)}. Please check your description."
|
|
|
|
| 1136 |
|
| 1137 |
|
| 1138 |
def get_enhanced_recommendations_with_unified_scoring(user_description: str, top_k: int = 15) -> List[Dict[str, Any]]:
|
| 1139 |
+
"""
|
| 1140 |
+
模組層級便利函數 - 使用完整的多頭評分系統
|
| 1141 |
+
|
| 1142 |
+
這個函數呼叫 SemanticBreedRecommender 的增強推薦方法,使用:
|
| 1143 |
+
- QueryUnderstandingEngine: 解析用戶意圖
|
| 1144 |
+
- PriorityDetector: 檢測維度優先級
|
| 1145 |
+
- MultiHeadScorer: 多維度評分
|
| 1146 |
+
- DynamicWeightCalculator: 動態權重分配
|
| 1147 |
+
- SmartBreedFilter: 智慧風險過濾(只對真正危害用戶的情況干預)
|
| 1148 |
+
|
| 1149 |
+
如果增強系統失敗,會自動回退到基本語義推薦
|
| 1150 |
+
"""
|
| 1151 |
try:
|
| 1152 |
+
print(f"Processing description-based recommendation with multi-head scoring: {user_description[:50]}...")
|
| 1153 |
|
| 1154 |
+
# 創建推薦器實例
|
| 1155 |
recommender = SemanticBreedRecommender()
|
| 1156 |
|
| 1157 |
+
# 檢查 SBERT 模型是否可用
|
| 1158 |
if not recommender.vector_manager.is_model_available():
|
| 1159 |
print("SBERT model not available, using basic text matching...")
|
| 1160 |
+
results = _get_basic_text_matching_recommendations(user_description, top_k, recommender)
|
| 1161 |
+
# 應用智慧過濾
|
| 1162 |
+
results = apply_smart_filtering(results, user_description)
|
| 1163 |
+
return results
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1164 |
|
| 1165 |
+
# 嘗試使用完整的增強語義推薦系統
|
| 1166 |
+
try:
|
| 1167 |
+
results = recommender.get_enhanced_semantic_recommendations(user_description, top_k)
|
| 1168 |
+
if results:
|
| 1169 |
+
# 應用智慧過濾
|
| 1170 |
+
results = apply_smart_filtering(results, user_description)
|
| 1171 |
+
return results
|
| 1172 |
+
else:
|
| 1173 |
+
print("Enhanced recommendations returned empty, falling back to basic semantic...")
|
| 1174 |
+
except Exception as enhanced_error:
|
| 1175 |
+
print(f"Enhanced recommendation failed: {str(enhanced_error)}, falling back to basic semantic...")
|
| 1176 |
+
print(traceback.format_exc())
|
| 1177 |
|
| 1178 |
+
# 回退到基本語義推薦
|
| 1179 |
+
try:
|
| 1180 |
+
results = recommender.get_semantic_recommendations(user_description, top_k)
|
| 1181 |
+
if results:
|
| 1182 |
+
# 應用智慧過濾
|
| 1183 |
+
results = apply_smart_filtering(results, user_description)
|
| 1184 |
+
return results
|
| 1185 |
+
except Exception as semantic_error:
|
| 1186 |
+
print(f"Basic semantic recommendation also failed: {str(semantic_error)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1187 |
|
| 1188 |
+
# 最後回退到基本文字匹配
|
| 1189 |
+
print("All semantic methods failed, using basic text matching as last resort...")
|
| 1190 |
+
results = _get_basic_text_matching_recommendations(user_description, top_k, recommender)
|
| 1191 |
+
# 應用智慧過濾
|
| 1192 |
+
results = apply_smart_filtering(results, user_description)
|
| 1193 |
+
return results
|
| 1194 |
|
| 1195 |
except Exception as e:
|
| 1196 |
error_msg = f"Error in semantic recommendation system: {str(e)}. Please check your input and try again."
|
|
|
|
| 1232 |
'Japanese_Spaniel', 'Toy_Terrier', 'Affenpinscher', 'Pekingese', 'Lhasa'
|
| 1233 |
]
|
| 1234 |
|
| 1235 |
+
# 應用約束過濾 - 關鍵修復!
|
| 1236 |
+
try:
|
| 1237 |
+
from constraint_manager import ConstraintManager
|
| 1238 |
+
from query_understanding import QueryUnderstandingEngine
|
| 1239 |
+
|
| 1240 |
+
query_engine = QueryUnderstandingEngine()
|
| 1241 |
+
dimensions = query_engine.analyze_query(user_description)
|
| 1242 |
+
constraint_manager = ConstraintManager()
|
| 1243 |
+
filter_result = constraint_manager.apply_constraints(dimensions)
|
| 1244 |
+
|
| 1245 |
+
# 只保留通過約束的品種
|
| 1246 |
+
allowed_breeds = filter_result.passed_breeds
|
| 1247 |
+
filtered_count = len(basic_breeds)
|
| 1248 |
+
basic_breeds = [b for b in basic_breeds if b in allowed_breeds]
|
| 1249 |
+
print(f"Constraint filtering: {filtered_count} -> {len(basic_breeds)} breeds")
|
| 1250 |
+
|
| 1251 |
+
# 記錄被過濾的原因(用於調試)
|
| 1252 |
+
for breed, reason in filter_result.filtered_breeds.items():
|
| 1253 |
+
if breed in ['Italian_Greyhound', 'Rottweiler', 'Malinois']:
|
| 1254 |
+
print(f" Filtered {breed}: {reason}")
|
| 1255 |
+
except Exception as e:
|
| 1256 |
+
print(f"Warning: Could not apply constraints: {str(e)}")
|
| 1257 |
+
|
| 1258 |
for breed in basic_breeds:
|
| 1259 |
breed_info = get_dog_description(breed) or {}
|
| 1260 |
breed_text = f"{breed} {breed_info.get('Temperament', '')} {breed_info.get('Size', '')} {breed_info.get('Description', '')}".lower()
|
smart_breed_filter.py
ADDED
|
@@ -0,0 +1,454 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# %%writefile smart_breed_filter.py
|
| 2 |
+
"""
|
| 3 |
+
Smart Breed Filter - 智慧品種過濾系統
|
| 4 |
+
|
| 5 |
+
設計原則:
|
| 6 |
+
1. 只對「真正危害用戶」的情況進行干預
|
| 7 |
+
2. 無傷大雅的偏好差異維持原有評分邏輯
|
| 8 |
+
3. 所有規則基於通用性設計,不針對特定品種硬編碼
|
| 9 |
+
|
| 10 |
+
危害類型:
|
| 11 |
+
- 安全風險:幼童 + 高風險行為特徵
|
| 12 |
+
- 生活品質嚴重影響:噪音零容忍 + 焦慮/警戒吠叫品種
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
from typing import Dict, List, Tuple, Optional, Set
|
| 16 |
+
from dataclasses import dataclass
|
| 17 |
+
from breed_noise_info import breed_noise_info
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@dataclass
|
| 21 |
+
class UserPriorityContext:
|
| 22 |
+
"""用戶優先級上下文"""
|
| 23 |
+
noise_intolerance: bool = False # 噪音零容忍
|
| 24 |
+
has_young_children: bool = False # 有幼童
|
| 25 |
+
is_beginner: bool = False # 新手
|
| 26 |
+
is_senior: bool = False # 老年人
|
| 27 |
+
priority_dimensions: Dict[str, str] = None # 各維度優先級
|
| 28 |
+
|
| 29 |
+
def __post_init__(self):
|
| 30 |
+
if self.priority_dimensions is None:
|
| 31 |
+
self.priority_dimensions = {}
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class PriorityParser:
|
| 35 |
+
"""
|
| 36 |
+
優先級語意解析器
|
| 37 |
+
|
| 38 |
+
識別用戶是否對某些維度有「絕對需求」vs「一般偏好」
|
| 39 |
+
只在用戶明確強調時才觸發嚴格約束
|
| 40 |
+
"""
|
| 41 |
+
|
| 42 |
+
# 絕對需求信號詞
|
| 43 |
+
ABSOLUTE_SIGNALS = [
|
| 44 |
+
'most importantly', 'absolutely need', 'must have', 'essential',
|
| 45 |
+
'critical', 'cannot', "can't", 'no way', 'zero tolerance',
|
| 46 |
+
'very noise sensitive', 'neighbors complain', 'thin walls'
|
| 47 |
+
]
|
| 48 |
+
|
| 49 |
+
# 主要需求信號詞
|
| 50 |
+
PRIMARY_SIGNALS = [
|
| 51 |
+
'first', 'primarily', 'main priority', 'most important',
|
| 52 |
+
'first priority', 'number one'
|
| 53 |
+
]
|
| 54 |
+
|
| 55 |
+
# 維度關鍵詞
|
| 56 |
+
DIMENSION_KEYWORDS = {
|
| 57 |
+
'noise': ['quiet', 'noise', 'bark', 'silent', 'neighbors',
|
| 58 |
+
'thin walls', 'apartment noise', 'loud', 'vocal'],
|
| 59 |
+
'children': ['kids', 'children', 'child', 'toddler', 'baby',
|
| 60 |
+
'infant', 'young kids', 'aged 1', 'aged 2', 'aged 3',
|
| 61 |
+
'aged 4', 'aged 5', 'preschool'],
|
| 62 |
+
'exercise': ['active', 'exercise', 'running', 'hiking', 'energetic',
|
| 63 |
+
'athletic', 'jogging', 'outdoor activities'],
|
| 64 |
+
'grooming': ['maintenance', 'grooming', 'shedding', 'brush', 'coat',
|
| 65 |
+
'low maintenance', 'easy care'],
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
def parse(self, user_input: str) -> UserPriorityContext:
|
| 69 |
+
"""解析用戶輸入,提取優先級上下文"""
|
| 70 |
+
text = user_input.lower()
|
| 71 |
+
context = UserPriorityContext()
|
| 72 |
+
|
| 73 |
+
# 檢測噪音零容忍
|
| 74 |
+
context.noise_intolerance = self._detect_noise_intolerance(text)
|
| 75 |
+
|
| 76 |
+
# 檢測是否有幼童
|
| 77 |
+
context.has_young_children = self._detect_young_children(text)
|
| 78 |
+
|
| 79 |
+
# 檢測各維度優先級
|
| 80 |
+
context.priority_dimensions = self._detect_dimension_priorities(text)
|
| 81 |
+
|
| 82 |
+
return context
|
| 83 |
+
|
| 84 |
+
def _detect_noise_intolerance(self, text: str) -> bool:
|
| 85 |
+
"""
|
| 86 |
+
檢測噪音零容忍
|
| 87 |
+
|
| 88 |
+
只有當用戶明確表達噪音是嚴重問題時才觸發
|
| 89 |
+
例如:thin walls, neighbors complain, noise sensitive neighbors
|
| 90 |
+
"""
|
| 91 |
+
# 強烈噪音敏感信號
|
| 92 |
+
strong_signals = [
|
| 93 |
+
'thin walls', 'noise sensitive', 'neighbors complain',
|
| 94 |
+
'zero tolerance', 'cannot bark', "can't bark",
|
| 95 |
+
'absolutely quiet', 'must be quiet', 'noise restriction'
|
| 96 |
+
]
|
| 97 |
+
|
| 98 |
+
# 需要同時出現「噪音相關詞」+「強調詞」
|
| 99 |
+
noise_words = ['quiet', 'noise', 'bark', 'silent', 'loud']
|
| 100 |
+
emphasis_words = ['most importantly', 'absolutely', 'must', 'essential',
|
| 101 |
+
'critical', 'very', 'extremely', 'cannot', "can't"]
|
| 102 |
+
|
| 103 |
+
# 檢查強烈信號
|
| 104 |
+
if any(signal in text for signal in strong_signals):
|
| 105 |
+
return True
|
| 106 |
+
|
| 107 |
+
# 檢查組合:噪音詞 + 強調詞
|
| 108 |
+
has_noise_word = any(w in text for w in noise_words)
|
| 109 |
+
has_emphasis = any(w in text for w in emphasis_words)
|
| 110 |
+
|
| 111 |
+
return has_noise_word and has_emphasis
|
| 112 |
+
|
| 113 |
+
def _detect_young_children(self, text: str) -> bool:
|
| 114 |
+
"""
|
| 115 |
+
檢測是否有幼童或一般兒童
|
| 116 |
+
|
| 117 |
+
對於兒童安全,我們採取保守策略:
|
| 118 |
+
- 明確提到 kids/children 就視為有兒童風險需要考慮
|
| 119 |
+
- 因為牧羊本能的 nipping 對任何年齡兒童都有風險
|
| 120 |
+
"""
|
| 121 |
+
# 任何提到兒童的情況都需要考慮安全
|
| 122 |
+
child_signals = [
|
| 123 |
+
'kids', 'children', 'child', 'toddler', 'baby', 'infant',
|
| 124 |
+
'young kids', 'young children',
|
| 125 |
+
'aged 1', 'aged 2', 'aged 3', 'aged 4', 'aged 5',
|
| 126 |
+
'1 year', '2 year', '3 year', '4 year', '5 year',
|
| 127 |
+
'preschool', 'newborn', 'family with'
|
| 128 |
+
]
|
| 129 |
+
return any(signal in text for signal in child_signals)
|
| 130 |
+
|
| 131 |
+
def _detect_dimension_priorities(self, text: str) -> Dict[str, str]:
|
| 132 |
+
"""檢測各維度的優先級"""
|
| 133 |
+
priorities = {}
|
| 134 |
+
|
| 135 |
+
for dimension, keywords in self.DIMENSION_KEYWORDS.items():
|
| 136 |
+
if any(kw in text for kw in keywords):
|
| 137 |
+
# 檢查是否有絕對需求信號
|
| 138 |
+
if any(signal in text for signal in self.ABSOLUTE_SIGNALS):
|
| 139 |
+
# 檢查信號是否與該維度相關(在附近)
|
| 140 |
+
for signal in self.ABSOLUTE_SIGNALS:
|
| 141 |
+
if signal in text:
|
| 142 |
+
signal_pos = text.find(signal)
|
| 143 |
+
for kw in keywords:
|
| 144 |
+
if kw in text:
|
| 145 |
+
kw_pos = text.find(kw)
|
| 146 |
+
# 如果信號詞和維度關鍵詞距離在50字符內
|
| 147 |
+
if abs(signal_pos - kw_pos) < 80:
|
| 148 |
+
priorities[dimension] = 'ABSOLUTE'
|
| 149 |
+
break
|
| 150 |
+
if dimension in priorities:
|
| 151 |
+
break
|
| 152 |
+
|
| 153 |
+
# 檢查是否有主要需求信號
|
| 154 |
+
if dimension not in priorities:
|
| 155 |
+
if any(signal in text for signal in self.PRIMARY_SIGNALS):
|
| 156 |
+
priorities[dimension] = 'PRIMARY'
|
| 157 |
+
else:
|
| 158 |
+
priorities[dimension] = 'PREFERENCE'
|
| 159 |
+
|
| 160 |
+
return priorities
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
class BreedRiskAnalyzer:
|
| 164 |
+
"""
|
| 165 |
+
品種風險分析器
|
| 166 |
+
|
| 167 |
+
只分析「真正的危害風險」,不對一般偏好差異進行干預
|
| 168 |
+
"""
|
| 169 |
+
|
| 170 |
+
# 焦慮相關觸發詞(會導致持續吠叫的真正問題)
|
| 171 |
+
ANXIETY_TRIGGERS = ['anxiety', 'separation anxiety', 'loneliness']
|
| 172 |
+
|
| 173 |
+
# 高警戒觸發詞(會導致頻繁吠叫)
|
| 174 |
+
HIGH_ALERT_TRIGGERS = ['stranger alerts', 'strangers approaching',
|
| 175 |
+
'suspicious activity', 'territorial defense',
|
| 176 |
+
'protecting territory']
|
| 177 |
+
|
| 178 |
+
# 牧羊/追逐本能(對幼童有 nipping 風險)
|
| 179 |
+
HERDING_INDICATORS = ['herding instincts', 'herding', 'nipping']
|
| 180 |
+
|
| 181 |
+
# 獵物驅動(可能追逐小孩)
|
| 182 |
+
PREY_DRIVE_INDICATORS = ['prey drive', 'prey sighting', 'chase']
|
| 183 |
+
|
| 184 |
+
def analyze_noise_risk(self, breed_info: Dict, noise_info: Dict) -> Dict:
|
| 185 |
+
"""
|
| 186 |
+
分析品種的噪音風險
|
| 187 |
+
|
| 188 |
+
只標記「真正會造成問題」的品種:
|
| 189 |
+
- 有焦慮吠叫傾向(持續性問題)
|
| 190 |
+
- 高度警戒吠叫(頻繁問題)
|
| 191 |
+
|
| 192 |
+
不標記:
|
| 193 |
+
- 偶爾興奮吠叫(正常狗行為)
|
| 194 |
+
- 打招呼吠叫(短暫且可控)
|
| 195 |
+
"""
|
| 196 |
+
noise_notes = noise_info.get('noise_notes', '').lower()
|
| 197 |
+
noise_level = noise_info.get('noise_level', 'Moderate').lower()
|
| 198 |
+
temperament = breed_info.get('Temperament', '').lower()
|
| 199 |
+
|
| 200 |
+
risk_factors = []
|
| 201 |
+
|
| 202 |
+
# 1. 焦慮觸發 - 這是真正的問題(持續性吠叫)
|
| 203 |
+
has_anxiety = any(t in noise_notes for t in self.ANXIETY_TRIGGERS)
|
| 204 |
+
if has_anxiety:
|
| 205 |
+
risk_factors.append('anxiety_barking')
|
| 206 |
+
|
| 207 |
+
# 2. 高度警戒 - 頻繁吠叫風險
|
| 208 |
+
has_high_alert = any(t in noise_notes for t in self.HIGH_ALERT_TRIGGERS)
|
| 209 |
+
if has_high_alert:
|
| 210 |
+
risk_factors.append('high_alert_barking')
|
| 211 |
+
|
| 212 |
+
# 3. 敏感性格 + 焦慮觸發的組合(更嚴重)
|
| 213 |
+
is_sensitive = 'sensitive' in temperament
|
| 214 |
+
if is_sensitive and has_anxiety:
|
| 215 |
+
risk_factors.append('sensitive_anxiety_combo')
|
| 216 |
+
|
| 217 |
+
# 4. 基礎噪音等級高
|
| 218 |
+
if noise_level in ['high', 'moderate-high', 'moderate to high']:
|
| 219 |
+
risk_factors.append('high_base_noise')
|
| 220 |
+
|
| 221 |
+
# 計算風險等級
|
| 222 |
+
# 只有真正問題的組合才是 HIGH
|
| 223 |
+
if 'sensitive_anxiety_combo' in risk_factors:
|
| 224 |
+
risk_level = 'HIGH'
|
| 225 |
+
elif 'anxiety_barking' in risk_factors and 'high_alert_barking' in risk_factors:
|
| 226 |
+
risk_level = 'HIGH'
|
| 227 |
+
elif 'anxiety_barking' in risk_factors or len(risk_factors) >= 2:
|
| 228 |
+
risk_level = 'MODERATE'
|
| 229 |
+
elif len(risk_factors) >= 1:
|
| 230 |
+
risk_level = 'LOW'
|
| 231 |
+
else:
|
| 232 |
+
risk_level = 'NONE'
|
| 233 |
+
|
| 234 |
+
return {
|
| 235 |
+
'risk_level': risk_level,
|
| 236 |
+
'risk_factors': risk_factors
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
def analyze_child_safety_risk(self, breed_info: Dict, noise_info: Dict) -> Dict:
|
| 240 |
+
"""
|
| 241 |
+
分析品種對幼童的安全風險
|
| 242 |
+
|
| 243 |
+
只標記「真正的安全風險」:
|
| 244 |
+
- 牧羊本能(nipping 風險)
|
| 245 |
+
- 高獵物驅動 + 大體型(追逐風險)
|
| 246 |
+
- Good with Children = No 且有其他風險因素
|
| 247 |
+
|
| 248 |
+
不標記:
|
| 249 |
+
- 只是體型大但性格溫和
|
| 250 |
+
- 活力高但無追逐/牧羊本能
|
| 251 |
+
"""
|
| 252 |
+
temperament = breed_info.get('Temperament', '').lower()
|
| 253 |
+
description = breed_info.get('Description', '').lower()
|
| 254 |
+
noise_notes = noise_info.get('noise_notes', '').lower()
|
| 255 |
+
size = breed_info.get('Size', '').lower()
|
| 256 |
+
good_with_children = breed_info.get('Good with Children', 'Yes')
|
| 257 |
+
exercise = breed_info.get('Exercise Needs', '').lower()
|
| 258 |
+
|
| 259 |
+
risk_factors = []
|
| 260 |
+
|
| 261 |
+
# 1. 牧羊本能 - 真正的 nipping 風險
|
| 262 |
+
has_herding = any(ind in noise_notes or ind in description
|
| 263 |
+
for ind in self.HERDING_INDICATORS)
|
| 264 |
+
if has_herding:
|
| 265 |
+
risk_factors.append('herding_instinct')
|
| 266 |
+
|
| 267 |
+
# 2. 獵物驅動 - 追逐風險
|
| 268 |
+
has_prey_drive = any(ind in noise_notes or ind in description
|
| 269 |
+
for ind in self.PREY_DRIVE_INDICATORS)
|
| 270 |
+
if has_prey_drive:
|
| 271 |
+
risk_factors.append('prey_drive')
|
| 272 |
+
|
| 273 |
+
# 3. Good with Children = No 是強烈信號
|
| 274 |
+
if good_with_children == 'No':
|
| 275 |
+
risk_factors.append('not_child_friendly')
|
| 276 |
+
|
| 277 |
+
# 4. 大體型 + 高驅動 + 牧羊/獵物本能的組合才是風險
|
| 278 |
+
is_large = size in ['large', 'giant']
|
| 279 |
+
is_very_high_energy = 'very high' in exercise
|
| 280 |
+
|
| 281 |
+
if is_large and (has_herding or has_prey_drive) and is_very_high_energy:
|
| 282 |
+
risk_factors.append('large_high_drive_instinct')
|
| 283 |
+
|
| 284 |
+
# 計算風險等級
|
| 285 |
+
# 只有真正危險的組合才是 HIGH
|
| 286 |
+
if 'not_child_friendly' in risk_factors and len(risk_factors) >= 2:
|
| 287 |
+
risk_level = 'HIGH'
|
| 288 |
+
elif 'large_high_drive_instinct' in risk_factors:
|
| 289 |
+
risk_level = 'HIGH'
|
| 290 |
+
elif 'herding_instinct' in risk_factors and is_very_high_energy:
|
| 291 |
+
# 牧羊本能 + 高能量 = 對兒童的真正風險(nipping + 控制不住)
|
| 292 |
+
risk_level = 'HIGH'
|
| 293 |
+
elif 'herding_instinct' in risk_factors or 'prey_drive' in risk_factors:
|
| 294 |
+
# 單獨的牧羊或獵物本能仍是中等風險
|
| 295 |
+
risk_level = 'MODERATE'
|
| 296 |
+
elif 'not_child_friendly' in risk_factors:
|
| 297 |
+
risk_level = 'MODERATE'
|
| 298 |
+
elif len(risk_factors) >= 1:
|
| 299 |
+
risk_level = 'LOW'
|
| 300 |
+
else:
|
| 301 |
+
risk_level = 'NONE'
|
| 302 |
+
|
| 303 |
+
return {
|
| 304 |
+
'risk_level': risk_level,
|
| 305 |
+
'risk_factors': risk_factors
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
class SmartBreedFilter:
|
| 310 |
+
"""
|
| 311 |
+
智慧品種過濾器
|
| 312 |
+
|
| 313 |
+
整合優先級解析和風險分析,只對真正危害用戶的情況進行干預
|
| 314 |
+
"""
|
| 315 |
+
|
| 316 |
+
def __init__(self):
|
| 317 |
+
self.priority_parser = PriorityParser()
|
| 318 |
+
self.risk_analyzer = BreedRiskAnalyzer()
|
| 319 |
+
|
| 320 |
+
def analyze_user_context(self, user_input: str) -> UserPriorityContext:
|
| 321 |
+
"""分析用戶輸入,提取優先級上下文"""
|
| 322 |
+
return self.priority_parser.parse(user_input)
|
| 323 |
+
|
| 324 |
+
def should_exclude_breed(self, breed_info: Dict, noise_info: Dict,
|
| 325 |
+
user_context: UserPriorityContext) -> Tuple[bool, str]:
|
| 326 |
+
"""
|
| 327 |
+
判斷是否應該排除該品種
|
| 328 |
+
|
| 329 |
+
返回: (是否排除, 排除原因)
|
| 330 |
+
"""
|
| 331 |
+
# 1. 噪音零容忍 + 高噪音風險
|
| 332 |
+
if user_context.noise_intolerance:
|
| 333 |
+
noise_risk = self.risk_analyzer.analyze_noise_risk(breed_info, noise_info)
|
| 334 |
+
if noise_risk['risk_level'] == 'HIGH':
|
| 335 |
+
return True, f"High noise risk ({', '.join(noise_risk['risk_factors'])}) conflicts with noise intolerance"
|
| 336 |
+
|
| 337 |
+
# 2. 有幼童 + 高兒童安全風險
|
| 338 |
+
if user_context.has_young_children:
|
| 339 |
+
child_risk = self.risk_analyzer.analyze_child_safety_risk(breed_info, noise_info)
|
| 340 |
+
if child_risk['risk_level'] == 'HIGH':
|
| 341 |
+
return True, f"Child safety risk ({', '.join(child_risk['risk_factors'])}) with young children"
|
| 342 |
+
|
| 343 |
+
return False, ""
|
| 344 |
+
|
| 345 |
+
def calculate_risk_penalty(self, breed_info: Dict, noise_info: Dict,
|
| 346 |
+
user_context: UserPriorityContext) -> float:
|
| 347 |
+
"""
|
| 348 |
+
計算風險懲罰分數
|
| 349 |
+
|
| 350 |
+
只對中等風險進行輕微降權,不排除
|
| 351 |
+
返回: 懲罰係數 (0.0 - 0.3)
|
| 352 |
+
"""
|
| 353 |
+
penalty = 0.0
|
| 354 |
+
|
| 355 |
+
# 噪音相關懲罰(只在用戶關注噪音時)
|
| 356 |
+
if 'noise' in user_context.priority_dimensions:
|
| 357 |
+
noise_risk = self.risk_analyzer.analyze_noise_risk(breed_info, noise_info)
|
| 358 |
+
if noise_risk['risk_level'] == 'MODERATE':
|
| 359 |
+
penalty += 0.1
|
| 360 |
+
elif noise_risk['risk_level'] == 'HIGH' and not user_context.noise_intolerance:
|
| 361 |
+
penalty += 0.15
|
| 362 |
+
|
| 363 |
+
# 兒童安全相關懲罰(只在用戶有孩子時)
|
| 364 |
+
if 'children' in user_context.priority_dimensions or user_context.has_young_children:
|
| 365 |
+
child_risk = self.risk_analyzer.analyze_child_safety_risk(breed_info, noise_info)
|
| 366 |
+
if child_risk['risk_level'] == 'MODERATE':
|
| 367 |
+
penalty += 0.1
|
| 368 |
+
elif child_risk['risk_level'] == 'HIGH' and not user_context.has_young_children:
|
| 369 |
+
penalty += 0.15
|
| 370 |
+
|
| 371 |
+
return min(penalty, 0.3) # 最大懲罰 30%
|
| 372 |
+
|
| 373 |
+
def filter_and_adjust_recommendations(self, recommendations: List[Dict],
|
| 374 |
+
user_input: str) -> List[Dict]:
|
| 375 |
+
"""
|
| 376 |
+
過濾並調整推薦結果
|
| 377 |
+
|
| 378 |
+
這是主要入口函數,整合所有過濾和��整邏輯
|
| 379 |
+
"""
|
| 380 |
+
user_context = self.analyze_user_context(user_input)
|
| 381 |
+
|
| 382 |
+
filtered_recommendations = []
|
| 383 |
+
|
| 384 |
+
for rec in recommendations:
|
| 385 |
+
breed = rec.get('breed', '')
|
| 386 |
+
|
| 387 |
+
# 智能獲取品種資訊:優先從 info 欄位,否則從 rec 本身,最後從資料庫
|
| 388 |
+
breed_info = rec.get('info')
|
| 389 |
+
if not breed_info:
|
| 390 |
+
# 嘗試從 rec 中構建標準化的 breed_info(處理大小寫差異)
|
| 391 |
+
breed_info = {
|
| 392 |
+
'Temperament': rec.get('Temperament', rec.get('temperament', '')),
|
| 393 |
+
'Description': rec.get('Description', rec.get('description', '')),
|
| 394 |
+
'Size': rec.get('Size', rec.get('size', '')),
|
| 395 |
+
'Exercise Needs': rec.get('Exercise Needs', rec.get('exercise_needs', '')),
|
| 396 |
+
'Good with Children': rec.get('Good with Children', rec.get('good_with_children', 'Yes')),
|
| 397 |
+
'Care Level': rec.get('Care Level', rec.get('care_level', '')),
|
| 398 |
+
}
|
| 399 |
+
# 如果關鍵資訊缺失,從資料庫獲取
|
| 400 |
+
if not breed_info['Temperament'] and not breed_info['Description']:
|
| 401 |
+
from dog_database import get_dog_description
|
| 402 |
+
db_info = get_dog_description(breed.replace(' ', '_'))
|
| 403 |
+
if db_info:
|
| 404 |
+
breed_info = db_info
|
| 405 |
+
|
| 406 |
+
# 獲取噪音資訊(嘗試兩種品種名稱格式)
|
| 407 |
+
noise_info = breed_noise_info.get(breed) or breed_noise_info.get(breed.replace(' ', '_'), {
|
| 408 |
+
'noise_notes': '',
|
| 409 |
+
'noise_level': 'Moderate'
|
| 410 |
+
})
|
| 411 |
+
|
| 412 |
+
# 檢查是否應該排除
|
| 413 |
+
should_exclude, reason = self.should_exclude_breed(
|
| 414 |
+
breed_info, noise_info, user_context
|
| 415 |
+
)
|
| 416 |
+
|
| 417 |
+
if should_exclude:
|
| 418 |
+
print(f" [SmartFilter] Excluded {breed}: {reason}")
|
| 419 |
+
continue
|
| 420 |
+
|
| 421 |
+
# 計算風險懲罰
|
| 422 |
+
penalty = self.calculate_risk_penalty(breed_info, noise_info, user_context)
|
| 423 |
+
|
| 424 |
+
if penalty > 0:
|
| 425 |
+
original_score = rec.get('final_score', rec.get('overall_score', 0.8))
|
| 426 |
+
adjusted_score = original_score * (1 - penalty)
|
| 427 |
+
rec['final_score'] = adjusted_score
|
| 428 |
+
rec['risk_penalty'] = penalty
|
| 429 |
+
|
| 430 |
+
filtered_recommendations.append(rec)
|
| 431 |
+
|
| 432 |
+
# 重新排序
|
| 433 |
+
filtered_recommendations.sort(key=lambda x: -x.get('final_score', 0))
|
| 434 |
+
|
| 435 |
+
# 更新排名
|
| 436 |
+
for i, rec in enumerate(filtered_recommendations):
|
| 437 |
+
rec['rank'] = i + 1
|
| 438 |
+
|
| 439 |
+
return filtered_recommendations
|
| 440 |
+
|
| 441 |
+
|
| 442 |
+
# 模組級便捷函數
|
| 443 |
+
_smart_filter = None
|
| 444 |
+
|
| 445 |
+
def get_smart_filter() -> SmartBreedFilter:
|
| 446 |
+
"""獲取單例過濾器"""
|
| 447 |
+
global _smart_filter
|
| 448 |
+
if _smart_filter is None:
|
| 449 |
+
_smart_filter = SmartBreedFilter()
|
| 450 |
+
return _smart_filter
|
| 451 |
+
|
| 452 |
+
def apply_smart_filtering(recommendations: List[Dict], user_input: str) -> List[Dict]:
|
| 453 |
+
"""便捷函數:應用智慧過濾"""
|
| 454 |
+
return get_smart_filter().filter_and_adjust_recommendations(recommendations, user_input)
|
user_query_analyzer.py
CHANGED
|
@@ -4,7 +4,7 @@ import numpy as np
|
|
| 4 |
import sqlite3
|
| 5 |
import re
|
| 6 |
import traceback
|
| 7 |
-
from typing import List, Dict, Tuple, Optional, Any
|
| 8 |
from dataclasses import dataclass
|
| 9 |
from sentence_transformers import SentenceTransformer
|
| 10 |
import torch
|
|
@@ -18,6 +18,8 @@ from constraint_manager import ConstraintManager, apply_breed_constraints
|
|
| 18 |
from multi_head_scorer import MultiHeadScorer, score_breed_candidates, BreedScore
|
| 19 |
from score_calibrator import ScoreCalibrator, calibrate_breed_scores
|
| 20 |
from config_manager import get_config_manager, get_standardized_breed_data
|
|
|
|
|
|
|
| 21 |
|
| 22 |
class UserQueryAnalyzer:
|
| 23 |
"""
|
|
@@ -28,6 +30,8 @@ class UserQueryAnalyzer:
|
|
| 28 |
def __init__(self, breed_list: List[str]):
|
| 29 |
"""初始化用戶查詢分析器"""
|
| 30 |
self.breed_list = breed_list
|
|
|
|
|
|
|
| 31 |
self.comparative_keywords = {
|
| 32 |
'most': 1.0, 'love': 1.0, 'prefer': 0.9, 'like': 0.8,
|
| 33 |
'then': 0.7, 'second': 0.7, 'followed': 0.6,
|
|
@@ -82,6 +86,34 @@ class UserQueryAnalyzer:
|
|
| 82 |
|
| 83 |
return breed_scores
|
| 84 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
def extract_lifestyle_keywords(self, user_input: str) -> Dict[str, List[str]]:
|
| 86 |
"""增強的生活方式關鍵字提取,具有更好的模式匹配"""
|
| 87 |
keywords = {
|
|
@@ -432,6 +464,35 @@ class UserQueryAnalyzer:
|
|
| 432 |
'experience_level': 'beginner' if any(word in text for word in ['first time', 'beginner', '新手']) else 'intermediate'
|
| 433 |
}
|
| 434 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 435 |
return analysis
|
| 436 |
|
| 437 |
def create_user_preferences_from_analysis_enhanced(self, analysis: Dict[str, Any]) -> 'UserPreferences':
|
|
|
|
| 4 |
import sqlite3
|
| 5 |
import re
|
| 6 |
import traceback
|
| 7 |
+
from typing import List, Dict, Tuple, Optional, Any, Set
|
| 8 |
from dataclasses import dataclass
|
| 9 |
from sentence_transformers import SentenceTransformer
|
| 10 |
import torch
|
|
|
|
| 18 |
from multi_head_scorer import MultiHeadScorer, score_breed_candidates, BreedScore
|
| 19 |
from score_calibrator import ScoreCalibrator, calibrate_breed_scores
|
| 20 |
from config_manager import get_config_manager, get_standardized_breed_data
|
| 21 |
+
from priority_detector import PriorityDetector, PriorityDetectionResult
|
| 22 |
+
from inference_engine import BreedRecommendationInferenceEngine, InferenceResult
|
| 23 |
|
| 24 |
class UserQueryAnalyzer:
|
| 25 |
"""
|
|
|
|
| 30 |
def __init__(self, breed_list: List[str]):
|
| 31 |
"""初始化用戶查詢分析器"""
|
| 32 |
self.breed_list = breed_list
|
| 33 |
+
self.priority_detector = PriorityDetector()
|
| 34 |
+
self.inference_engine = BreedRecommendationInferenceEngine()
|
| 35 |
self.comparative_keywords = {
|
| 36 |
'most': 1.0, 'love': 1.0, 'prefer': 0.9, 'like': 0.8,
|
| 37 |
'then': 0.7, 'second': 0.7, 'followed': 0.6,
|
|
|
|
| 86 |
|
| 87 |
return breed_scores
|
| 88 |
|
| 89 |
+
def _merge_priorities(self,
|
| 90 |
+
explicit_priorities: Dict[str, float],
|
| 91 |
+
implicit_priorities: Dict[str, float]) -> Dict[str, float]:
|
| 92 |
+
"""
|
| 93 |
+
合併顯式和隱式優先級
|
| 94 |
+
|
| 95 |
+
規則:
|
| 96 |
+
1. 明確提及的維度,使用明確優先級
|
| 97 |
+
2. 未明確提及但推斷出的維度,使用隱含優先級
|
| 98 |
+
3. 隱含優先級不會覆蓋明確優先級
|
| 99 |
+
|
| 100 |
+
Args:
|
| 101 |
+
explicit_priorities: 明確優先級
|
| 102 |
+
implicit_priorities: 隱含優先級
|
| 103 |
+
|
| 104 |
+
Returns:
|
| 105 |
+
Dict[str, float]: 合併後的優先級
|
| 106 |
+
"""
|
| 107 |
+
merged = explicit_priorities.copy()
|
| 108 |
+
|
| 109 |
+
for dim, implicit_score in implicit_priorities.items():
|
| 110 |
+
if dim not in merged:
|
| 111 |
+
# 只添加未明確提及的隱含優先級
|
| 112 |
+
merged[dim] = implicit_score
|
| 113 |
+
# 如果已有明確優先級,保持不變
|
| 114 |
+
|
| 115 |
+
return merged
|
| 116 |
+
|
| 117 |
def extract_lifestyle_keywords(self, user_input: str) -> Dict[str, List[str]]:
|
| 118 |
"""增強的生活方式關鍵字提取,具有更好的模式匹配"""
|
| 119 |
keywords = {
|
|
|
|
| 464 |
'experience_level': 'beginner' if any(word in text for word in ['first time', 'beginner', '新手']) else 'intermediate'
|
| 465 |
}
|
| 466 |
|
| 467 |
+
# 優先級檢測與推理
|
| 468 |
+
try:
|
| 469 |
+
# Step 1: 檢測明確優先級
|
| 470 |
+
priority_result = self.priority_detector.detect_priorities(user_description)
|
| 471 |
+
explicit_priorities = priority_result.dimension_priorities
|
| 472 |
+
|
| 473 |
+
# Step 2: 推斷隱含優先級
|
| 474 |
+
inference_result = self.inference_engine.infer_implicit_priorities(
|
| 475 |
+
user_description,
|
| 476 |
+
analysis['user_context']
|
| 477 |
+
)
|
| 478 |
+
implicit_priorities = inference_result.implicit_priorities
|
| 479 |
+
|
| 480 |
+
# Step 3: 合併優先級(明確優先級 > 隱含優先級)
|
| 481 |
+
final_priorities = self._merge_priorities(explicit_priorities, implicit_priorities)
|
| 482 |
+
|
| 483 |
+
# 添加到分析結果
|
| 484 |
+
analysis['dimension_priorities'] = final_priorities
|
| 485 |
+
analysis['explicit_priorities'] = explicit_priorities
|
| 486 |
+
analysis['implicit_priorities'] = implicit_priorities
|
| 487 |
+
analysis['priority_detection_confidence'] = priority_result.detection_confidence
|
| 488 |
+
analysis['inference_confidence'] = inference_result.confidence
|
| 489 |
+
|
| 490 |
+
except Exception as e:
|
| 491 |
+
print(f"Error in priority detection/inference: {str(e)}")
|
| 492 |
+
analysis['dimension_priorities'] = {}
|
| 493 |
+
analysis['explicit_priorities'] = {}
|
| 494 |
+
analysis['implicit_priorities'] = {}
|
| 495 |
+
|
| 496 |
return analysis
|
| 497 |
|
| 498 |
def create_user_preferences_from_analysis_enhanced(self, analysis: Dict[str, Any]) -> 'UserPreferences':
|