DawnC commited on
Commit
1e4c9bc
·
verified ·
1 Parent(s): db63e6a

Upload 18 files

Browse files

ADD Find by Description function and update recommendation accuracy

app.py CHANGED
@@ -10,6 +10,11 @@ from torchvision.ops import nms, box_iou
10
  import torch.nn.functional as F
11
  from torchvision import transforms
12
  from PIL import Image, ImageDraw, ImageFont, ImageFilter
 
 
 
 
 
13
  from breed_health_info import breed_health_info
14
  from breed_noise_info import breed_noise_info
15
  from dog_database import get_dog_description
@@ -20,7 +25,7 @@ from search_history import create_history_tab, create_history_component
20
  from styles import get_css_styles
21
  from breed_detection import create_detection_tab
22
  from breed_comparison import create_comparison_tab
23
- from breed_recommendation import create_recommendation_tab
24
  from breed_visualization import create_visualization_tab
25
  from style_transfer import DogStyleTransfer, create_style_transfer_tab
26
  from html_templates import (
@@ -36,23 +41,24 @@ from html_templates import (
36
  get_akc_breeds_link
37
  )
38
  from model_architecture import BaseModel, dog_breeds
39
- from urllib.parse import quote
40
- from ultralytics import YOLO
41
- import asyncio
42
- import traceback
43
 
44
  history_manager = UserHistoryManager()
45
 
46
  class ModelManager:
47
  """
48
- Singleton class for managing model instances and device allocation
49
  specifically designed for Hugging Face Spaces deployment.
 
50
  """
51
  _instance = None
52
  _initialized = False
53
  _yolo_model = None
54
  _breed_model = None
55
  _device = None
 
 
 
56
 
57
  def __new__(cls):
58
  if cls._instance is None:
@@ -64,6 +70,9 @@ class ModelManager:
64
  self._device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
65
  ModelManager._initialized = True
66
 
 
 
 
67
  @property
68
  def device(self):
69
  if self._device is None:
@@ -83,12 +92,12 @@ class ModelManager:
83
  num_classes=len(dog_breeds),
84
  device=self.device
85
  ).to(self.device)
86
-
87
  checkpoint = torch.load(
88
  'ConvNextV2Base_best_model.pth',
89
  map_location=self.device
90
  )
91
-
92
  # Try to load with model_state_dict first, then base_model
93
  if 'model_state_dict' in checkpoint:
94
  self._breed_model.load_state_dict(checkpoint['model_state_dict'], strict=False)
@@ -98,10 +107,81 @@ class ModelManager:
98
  # If neither key exists, raise a descriptive error
99
  available_keys = list(checkpoint.keys()) if isinstance(checkpoint, dict) else "not a dictionary"
100
  raise KeyError(f"Model checkpoint does not contain 'model_state_dict' or 'base_model' keys. Available keys: {available_keys}")
101
-
102
  self._breed_model.eval()
103
  return self._breed_model
104
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  # Initialize model manager
106
  model_manager = ModelManager()
107
 
@@ -197,7 +277,7 @@ def detect_multiple_dogs(image, conf_threshold=0.3, iou_threshold=0.3):
197
  })
198
 
199
  if not detected_boxes:
200
- return [(image, 1, [0, 0, img_width, img_height], False)]
201
 
202
  # Phase 2: Analysis of detection relationships
203
  avg_height = sum(box['height'] for box in detected_boxes) / len(detected_boxes)
@@ -211,7 +291,7 @@ def detect_multiple_dogs(image, conf_threshold=0.3, iou_threshold=0.3):
211
  y2 = min(box1['coords'][3], box2['coords'][3])
212
 
213
  if x2 <= x1 or y2 <= y1:
214
- return 0
215
 
216
  intersection = (x2 - x1) * (y2 - y1)
217
  area1 = box1['area']
@@ -328,7 +408,7 @@ def predict(image):
328
  print(f" Is dog: {is_dog}")
329
  print(f" Detection confidence: {detection_confidence:.4f}")
330
 
331
- # 如果是狗且進行品種預測,在這裡也加入打印語句
332
  if is_dog:
333
  top1_prob, topk_breeds, relative_probs = predict_single_dog(cropped_image)
334
  print(f" Breed prediction - Top probability: {top1_prob:.4f}")
@@ -515,7 +595,8 @@ def main():
515
  with gr.Tab("Style Transfer"):
516
  style_transfer_components = create_style_transfer_tab(dog_style_transfer)
517
 
518
- # 6. History Search
 
519
  create_history_tab(history_component)
520
 
521
  # Footer
@@ -549,4 +630,4 @@ def main():
549
 
550
  if __name__ == "__main__":
551
  iface = main()
552
- iface.launch()
 
10
  import torch.nn.functional as F
11
  from torchvision import transforms
12
  from PIL import Image, ImageDraw, ImageFont, ImageFilter
13
+ from sentence_transformers import SentenceTransformer
14
+ from urllib.parse import quote
15
+ from ultralytics import YOLO
16
+ import asyncio
17
+ import traceback
18
  from breed_health_info import breed_health_info
19
  from breed_noise_info import breed_noise_info
20
  from dog_database import get_dog_description
 
25
  from styles import get_css_styles
26
  from breed_detection import create_detection_tab
27
  from breed_comparison import create_comparison_tab
28
+ from breed_recommendation_enhanced import create_recommendation_tab
29
  from breed_visualization import create_visualization_tab
30
  from style_transfer import DogStyleTransfer, create_style_transfer_tab
31
  from html_templates import (
 
41
  get_akc_breeds_link
42
  )
43
  from model_architecture import BaseModel, dog_breeds
44
+
 
 
 
45
 
46
  history_manager = UserHistoryManager()
47
 
48
  class ModelManager:
49
  """
50
+ Enhanced Singleton class for managing model instances and device allocation
51
  specifically designed for Hugging Face Spaces deployment.
52
+ Includes support for multi-dimensional recommendation system.
53
  """
54
  _instance = None
55
  _initialized = False
56
  _yolo_model = None
57
  _breed_model = None
58
  _device = None
59
+ _sbert_model = None
60
+ _config_manager = None
61
+ _enhanced_system_initialized = False
62
 
63
  def __new__(cls):
64
  if cls._instance is None:
 
70
  self._device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
71
  ModelManager._initialized = True
72
 
73
+ # Initialize enhanced recommendation system
74
+ self._initialize_enhanced_system()
75
+
76
  @property
77
  def device(self):
78
  if self._device is None:
 
92
  num_classes=len(dog_breeds),
93
  device=self.device
94
  ).to(self.device)
95
+
96
  checkpoint = torch.load(
97
  'ConvNextV2Base_best_model.pth',
98
  map_location=self.device
99
  )
100
+
101
  # Try to load with model_state_dict first, then base_model
102
  if 'model_state_dict' in checkpoint:
103
  self._breed_model.load_state_dict(checkpoint['model_state_dict'], strict=False)
 
107
  # If neither key exists, raise a descriptive error
108
  available_keys = list(checkpoint.keys()) if isinstance(checkpoint, dict) else "not a dictionary"
109
  raise KeyError(f"Model checkpoint does not contain 'model_state_dict' or 'base_model' keys. Available keys: {available_keys}")
110
+
111
  self._breed_model.eval()
112
  return self._breed_model
113
 
114
+ def _initialize_enhanced_system(self):
115
+ """Initialize enhanced multi-dimensional recommendation system"""
116
+ if ModelManager._enhanced_system_initialized:
117
+ return
118
+
119
+ try:
120
+ # Initialize SBERT model for semantic analysis
121
+ try:
122
+ # Use default model configuration
123
+ model_name = 'all-MiniLM-L6-v2'
124
+ fallback_models = ['all-mpnet-base-v2', 'all-MiniLM-L12-v2']
125
+
126
+ for model_name_attempt in [model_name] + fallback_models:
127
+ try:
128
+ self._sbert_model = SentenceTransformer(model_name_attempt)
129
+ print(f"Initialized SBERT model: {model_name_attempt}")
130
+ break
131
+ except Exception as e:
132
+ print(f"Failed to load SBERT model {model_name_attempt}: {str(e)}")
133
+ continue
134
+
135
+ if self._sbert_model is None:
136
+ print("All SBERT models failed to load, enhanced system will use keyword-only analysis")
137
+
138
+ except Exception as e:
139
+ print(f"SBERT initialization failed: {str(e)}")
140
+ self._sbert_model = None
141
+
142
+ ModelManager._enhanced_system_initialized = True
143
+ print("Enhanced recommendation system initialization completed")
144
+
145
+ except ImportError as e:
146
+ print(f"Enhanced modules not available: {str(e)}")
147
+ ModelManager._enhanced_system_initialized = True # Mark as attempted
148
+ except Exception as e:
149
+ print(f"Enhanced system initialization failed: {str(e)}")
150
+ print(traceback.format_exc())
151
+ ModelManager._enhanced_system_initialized = True # Mark as attempted
152
+
153
+ @property
154
+ def sbert_model(self):
155
+ """Get SBERT model for semantic analysis"""
156
+ if not ModelManager._enhanced_system_initialized:
157
+ self._initialize_enhanced_system()
158
+ return self._sbert_model
159
+
160
+ @property
161
+ def config_manager(self):
162
+ """Get configuration manager (simplified)"""
163
+ if not ModelManager._enhanced_system_initialized:
164
+ self._initialize_enhanced_system()
165
+ return None # Simplified - no config manager needed
166
+
167
+ @property
168
+ def enhanced_system_available(self):
169
+ """Check if enhanced recommendation system is available"""
170
+ return (ModelManager._enhanced_system_initialized and
171
+ self._sbert_model is not None)
172
+
173
+ def get_system_status(self):
174
+ """Get status of all managed models and systems"""
175
+ return {
176
+ 'device': str(self.device),
177
+ 'yolo_model_loaded': self._yolo_model is not None,
178
+ 'breed_model_loaded': self._breed_model is not None,
179
+ 'sbert_model_loaded': self._sbert_model is not None,
180
+ 'config_manager_available': False, # Simplified system
181
+ 'enhanced_system_initialized': ModelManager._enhanced_system_initialized,
182
+ 'enhanced_system_available': self.enhanced_system_available
183
+ }
184
+
185
  # Initialize model manager
186
  model_manager = ModelManager()
187
 
 
277
  })
278
 
279
  if not detected_boxes:
280
+ return [(image, 1.0, [0, 0, img_width, img_height], False)]
281
 
282
  # Phase 2: Analysis of detection relationships
283
  avg_height = sum(box['height'] for box in detected_boxes) / len(detected_boxes)
 
291
  y2 = min(box1['coords'][3], box2['coords'][3])
292
 
293
  if x2 <= x1 or y2 <= y1:
294
+ return 0.0
295
 
296
  intersection = (x2 - x1) * (y2 - y1)
297
  area1 = box1['area']
 
408
  print(f" Is dog: {is_dog}")
409
  print(f" Detection confidence: {detection_confidence:.4f}")
410
 
411
+ # 如果是狗且進行品種預測
412
  if is_dog:
413
  top1_prob, topk_breeds, relative_probs = predict_single_dog(cropped_image)
414
  print(f" Breed prediction - Top probability: {top1_prob:.4f}")
 
595
  with gr.Tab("Style Transfer"):
596
  style_transfer_components = create_style_transfer_tab(dog_style_transfer)
597
 
598
+
599
+ # 6. History Search
600
  create_history_tab(history_component)
601
 
602
  # Footer
 
630
 
631
  if __name__ == "__main__":
632
  iface = main()
633
+ iface.launch()
bonus_penalty_engine.py ADDED
@@ -0,0 +1,596 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ from typing import Dict, Any
3
+ from dataclasses import dataclass
4
+
5
+ @dataclass
6
+ class UserPreferences:
7
+ """使用者偏好設定的資料結構"""
8
+ living_space: str # "apartment", "house_small", "house_large"
9
+ yard_access: str # "no_yard", "shared_yard", "private_yard"
10
+ exercise_time: int # minutes per day
11
+ exercise_type: str # "light_walks", "moderate_activity", "active_training"
12
+ grooming_commitment: str # "low", "medium", "high"
13
+ experience_level: str # "beginner", "intermediate", "advanced"
14
+ time_availability: str # "limited", "moderate", "flexible"
15
+ has_children: bool
16
+ children_age: str # "toddler", "school_age", "teenager"
17
+ noise_tolerance: str # "low", "medium", "high"
18
+ space_for_play: bool
19
+ other_pets: bool
20
+ climate: str # "cold", "moderate", "hot"
21
+ health_sensitivity: str = "medium"
22
+ barking_acceptance: str = None
23
+ size_preference: str = "no_preference" # "no_preference", "small", "medium", "large", "giant"
24
+ training_commitment: str = "medium" # "low", "medium", "high" - 訓練投入程度
25
+ living_environment: str = "ground_floor" # "ground_floor", "with_elevator", "walk_up" - 居住環境細節
26
+
27
+ def __post_init__(self):
28
+ if self.barking_acceptance is None:
29
+ self.barking_acceptance = self.noise_tolerance
30
+
31
+
32
+ class BonusPenaltyEngine:
33
+ """
34
+ 加分扣分引擎類別
35
+ 負責處理所有品種加分機制、額外評估因素和分數分布優化
36
+ """
37
+
38
+ def __init__(self):
39
+ """初始化加分扣分引擎"""
40
+ pass
41
+
42
+ @staticmethod
43
+ def calculate_breed_bonus(breed_info: dict, user_prefs: 'UserPreferences') -> float:
44
+ """
45
+ 計算品種額外加分
46
+
47
+ Args:
48
+ breed_info: 品種資訊字典
49
+ user_prefs: 使用者偏好設定
50
+
51
+ Returns:
52
+ float: 品種加分 (-0.25 到 0.5 之間)
53
+ """
54
+ bonus = 0.0
55
+ temperament = breed_info.get('Temperament', '').lower()
56
+
57
+ # 1. 壽命加分(最高0.05)
58
+ try:
59
+ lifespan = breed_info.get('Lifespan', '10-12 years')
60
+ years = [int(x) for x in lifespan.split('-')[0].split()[0:1]]
61
+ longevity_bonus = min(0.05, (max(years) - 10) * 0.01)
62
+ bonus += longevity_bonus
63
+ except:
64
+ pass
65
+
66
+ # 2. 性格特徵加分(最高0.15)
67
+ positive_traits = {
68
+ 'friendly': 0.05,
69
+ 'gentle': 0.05,
70
+ 'patient': 0.05,
71
+ 'intelligent': 0.04,
72
+ 'adaptable': 0.04,
73
+ 'affectionate': 0.04,
74
+ 'easy-going': 0.03,
75
+ 'calm': 0.03
76
+ }
77
+
78
+ negative_traits = {
79
+ 'aggressive': -0.08,
80
+ 'stubborn': -0.06,
81
+ 'dominant': -0.06,
82
+ 'aloof': -0.04,
83
+ 'nervous': -0.05,
84
+ 'protective': -0.04
85
+ }
86
+
87
+ personality_score = sum(value for trait, value in positive_traits.items() if trait in temperament)
88
+ personality_score += sum(value for trait, value in negative_traits.items() if trait in temperament)
89
+ bonus += max(-0.15, min(0.15, personality_score))
90
+
91
+ # 3. 適應性加分(最高0.1)
92
+ adaptability_bonus = 0.0
93
+ if breed_info.get('Size') == "Small" and user_prefs.living_space == "apartment":
94
+ adaptability_bonus += 0.05
95
+ if 'adaptable' in temperament or 'versatile' in temperament:
96
+ adaptability_bonus += 0.05
97
+ bonus += min(0.1, adaptability_bonus)
98
+
99
+ # 4. 家庭相容性(最高0.15)
100
+ if user_prefs.has_children:
101
+ family_traits = {
102
+ 'good with children': 0.06,
103
+ 'patient': 0.05,
104
+ 'gentle': 0.05,
105
+ 'tolerant': 0.04,
106
+ 'playful': 0.03
107
+ }
108
+ unfriendly_traits = {
109
+ 'aggressive': -0.08,
110
+ 'nervous': -0.07,
111
+ 'protective': -0.06,
112
+ 'territorial': -0.05
113
+ }
114
+
115
+ # 年齡評估
116
+ age_adjustments = {
117
+ 'toddler': {'bonus_mult': 0.7, 'penalty_mult': 1.3},
118
+ 'school_age': {'bonus_mult': 1.0, 'penalty_mult': 1.0},
119
+ 'teenager': {'bonus_mult': 1.2, 'penalty_mult': 0.8}
120
+ }
121
+
122
+ adj = age_adjustments.get(user_prefs.children_age,
123
+ {'bonus_mult': 1.0, 'penalty_mult': 1.0})
124
+
125
+ family_bonus = sum(value for trait, value in family_traits.items()
126
+ if trait in temperament) * adj['bonus_mult']
127
+ family_penalty = sum(value for trait, value in unfriendly_traits.items()
128
+ if trait in temperament) * adj['penalty_mult']
129
+
130
+ bonus += min(0.15, max(-0.2, family_bonus + family_penalty))
131
+
132
+ # 5. 專門技能加分(最高0.1)
133
+ skill_bonus = 0.0
134
+ special_abilities = {
135
+ 'working': 0.03,
136
+ 'herding': 0.03,
137
+ 'hunting': 0.03,
138
+ 'tracking': 0.03,
139
+ 'agility': 0.02
140
+ }
141
+ for ability, value in special_abilities.items():
142
+ if ability in temperament.lower():
143
+ skill_bonus += value
144
+ bonus += min(0.1, skill_bonus)
145
+
146
+ # 6. 適應性評估(增強版)
147
+ adaptability_bonus = 0.0
148
+ if breed_info.get('Size') == "Small" and user_prefs.living_space == "apartment":
149
+ adaptability_bonus += 0.08 # 小型犬更適合公寓
150
+
151
+ # 環境適應性評估
152
+ if 'adaptable' in temperament or 'versatile' in temperament:
153
+ if user_prefs.living_space == "apartment":
154
+ adaptability_bonus += 0.10 # 適應性在公寓環境更重要
155
+ else:
156
+ adaptability_bonus += 0.05 # 其他環境仍有加分
157
+
158
+ # 氣候適應性
159
+ description = breed_info.get('Description', '').lower()
160
+ climate = user_prefs.climate
161
+ if climate == 'hot':
162
+ if 'heat tolerant' in description or 'warm climate' in description:
163
+ adaptability_bonus += 0.08
164
+ elif 'thick coat' in description or 'cold climate' in description:
165
+ adaptability_bonus -= 0.10
166
+ elif climate == 'cold':
167
+ if 'thick coat' in description or 'cold climate' in description:
168
+ adaptability_bonus += 0.08
169
+ elif 'heat tolerant' in description or 'short coat' in description:
170
+ adaptability_bonus -= 0.10
171
+
172
+ bonus += min(0.15, adaptability_bonus)
173
+
174
+ return min(0.5, max(-0.25, bonus))
175
+
176
+ @staticmethod
177
+ def calculate_additional_factors(breed_info: dict, user_prefs: 'UserPreferences') -> dict:
178
+ """
179
+ 計算額外的評估因素,結合品種特性與使用者需求的全面評估系統
180
+
181
+ 1. 多功能性評估 - 品種的多樣化能力
182
+ 2. 訓練性評估 - 學習和服從能力
183
+ 3. 能量水平評估 - 活力和運動需求
184
+ 4. 美容需求評估 - 護理和維護需求
185
+ 5. 社交需求評估 - 與人互動的需求程度
186
+ 6. 氣候適應性 - 對環境的適應能力
187
+ 7. 運動類型匹配 - 與使用者運動習慣的契合度
188
+ 8. 生活方式適配 - 與使用者日常生活的匹配度
189
+ """
190
+ factors = {
191
+ 'versatility': 0.0, # 多功能性
192
+ 'trainability': 0.0, # 可訓練度
193
+ 'energy_level': 0.0, # 能量水平
194
+ 'grooming_needs': 0.0, # 美容需求
195
+ 'social_needs': 0.0, # 社交需求
196
+ 'weather_adaptability': 0.0,# 氣候適應性
197
+ 'exercise_match': 0.0, # 運動匹配度
198
+ 'lifestyle_fit': 0.0 # 生活方式適配度
199
+ }
200
+
201
+ temperament = breed_info.get('Temperament', '').lower()
202
+ description = breed_info.get('Description', '').lower()
203
+ size = breed_info.get('Size', 'Medium')
204
+
205
+ # 1. 多功能性評估 - 加強品種用途評估
206
+ versatile_traits = {
207
+ 'intelligent': 0.25,
208
+ 'adaptable': 0.25,
209
+ 'trainable': 0.20,
210
+ 'athletic': 0.15,
211
+ 'versatile': 0.15
212
+ }
213
+
214
+ working_roles = {
215
+ 'working': 0.20,
216
+ 'herding': 0.15,
217
+ 'hunting': 0.15,
218
+ 'sporting': 0.15,
219
+ 'companion': 0.10
220
+ }
221
+
222
+ # 計算特質分數
223
+ trait_score = sum(value for trait, value in versatile_traits.items()
224
+ if trait in temperament)
225
+
226
+ # 計算角色分數
227
+ role_score = sum(value for role, value in working_roles.items()
228
+ if role in description)
229
+
230
+ # 根據使用者需求調整多功能性評分
231
+ purpose_traits = {
232
+ 'light_walks': ['calm', 'gentle', 'easy-going'],
233
+ 'moderate_activity': ['adaptable', 'balanced', 'versatile'],
234
+ 'active_training': ['intelligent', 'trainable', 'working']
235
+ }
236
+
237
+ if user_prefs.exercise_type in purpose_traits:
238
+ matching_traits = sum(1 for trait in purpose_traits[user_prefs.exercise_type]
239
+ if trait in temperament)
240
+ trait_score += matching_traits * 0.15
241
+
242
+ factors['versatility'] = min(1.0, trait_score + role_score)
243
+
244
+ # 2. 訓練性評估
245
+ trainable_traits = {
246
+ 'intelligent': 0.3,
247
+ 'eager to please': 0.3,
248
+ 'trainable': 0.2,
249
+ 'quick learner': 0.2,
250
+ 'obedient': 0.2
251
+ }
252
+
253
+ base_trainability = sum(value for trait, value in trainable_traits.items()
254
+ if trait in temperament)
255
+
256
+ # 根據使用者經驗調整訓練性評分
257
+ experience_multipliers = {
258
+ 'beginner': 1.2, # 新手更需要容易訓練的狗
259
+ 'intermediate': 1.0,
260
+ 'advanced': 0.8 # 專家能處理較難訓���的狗
261
+ }
262
+
263
+ factors['trainability'] = min(1.0, base_trainability *
264
+ experience_multipliers.get(user_prefs.experience_level, 1.0))
265
+
266
+ # 3. 能量水平評估
267
+ exercise_needs = breed_info.get('Exercise Needs', 'MODERATE').upper()
268
+ energy_levels = {
269
+ 'VERY HIGH': {
270
+ 'score': 1.0,
271
+ 'min_exercise': 120,
272
+ 'ideal_exercise': 150
273
+ },
274
+ 'HIGH': {
275
+ 'score': 0.8,
276
+ 'min_exercise': 90,
277
+ 'ideal_exercise': 120
278
+ },
279
+ 'MODERATE': {
280
+ 'score': 0.6,
281
+ 'min_exercise': 60,
282
+ 'ideal_exercise': 90
283
+ },
284
+ 'LOW': {
285
+ 'score': 0.4,
286
+ 'min_exercise': 30,
287
+ 'ideal_exercise': 60
288
+ }
289
+ }
290
+
291
+ breed_energy = energy_levels.get(exercise_needs, energy_levels['MODERATE'])
292
+
293
+ # 計算運動時間匹配度
294
+ if user_prefs.exercise_time >= breed_energy['ideal_exercise']:
295
+ energy_score = breed_energy['score']
296
+ else:
297
+ # 如果運動時間不足,按比例降低分數
298
+ deficit_ratio = max(0.4, user_prefs.exercise_time / breed_energy['ideal_exercise'])
299
+ energy_score = breed_energy['score'] * deficit_ratio
300
+
301
+ factors['energy_level'] = energy_score
302
+
303
+ # 4. 美容需求評估
304
+ grooming_needs = breed_info.get('Grooming Needs', 'MODERATE').upper()
305
+ grooming_levels = {
306
+ 'HIGH': 1.0,
307
+ 'MODERATE': 0.6,
308
+ 'LOW': 0.3
309
+ }
310
+
311
+ # 特殊毛髮類型評估
312
+ coat_adjustments = 0
313
+ if 'long coat' in description:
314
+ coat_adjustments += 0.2
315
+ if 'double coat' in description:
316
+ coat_adjustments += 0.15
317
+ if 'curly' in description:
318
+ coat_adjustments += 0.15
319
+
320
+ # 根據使用者承諾度調整
321
+ commitment_multipliers = {
322
+ 'low': 1.5, # 低承諾度時加重美容需求的影響
323
+ 'medium': 1.0,
324
+ 'high': 0.8 # 高承諾度時降低美容需求的影響
325
+ }
326
+
327
+ base_grooming = grooming_levels.get(grooming_needs, 0.6) + coat_adjustments
328
+ factors['grooming_needs'] = min(1.0, base_grooming *
329
+ commitment_multipliers.get(user_prefs.grooming_commitment, 1.0))
330
+
331
+ # 5. 社交需求評估
332
+ social_traits = {
333
+ 'friendly': 0.25,
334
+ 'social': 0.25,
335
+ 'affectionate': 0.20,
336
+ 'people-oriented': 0.20
337
+ }
338
+
339
+ antisocial_traits = {
340
+ 'independent': -0.20,
341
+ 'aloof': -0.20,
342
+ 'reserved': -0.15
343
+ }
344
+
345
+ social_score = sum(value for trait, value in social_traits.items()
346
+ if trait in temperament)
347
+ antisocial_score = sum(value for trait, value in antisocial_traits.items()
348
+ if trait in temperament)
349
+
350
+ # 家庭情況調整
351
+ if user_prefs.has_children:
352
+ child_friendly_bonus = 0.2 if 'good with children' in temperament else 0
353
+ social_score += child_friendly_bonus
354
+
355
+ factors['social_needs'] = min(1.0, max(0.0, social_score + antisocial_score))
356
+
357
+ # 6. 氣候適應性評估 - 更細緻的環境適應評估
358
+ climate_traits = {
359
+ 'cold': {
360
+ 'positive': ['thick coat', 'winter', 'cold climate'],
361
+ 'negative': ['short coat', 'heat sensitive']
362
+ },
363
+ 'hot': {
364
+ 'positive': ['short coat', 'heat tolerant', 'warm climate'],
365
+ 'negative': ['thick coat', 'cold climate']
366
+ },
367
+ 'moderate': {
368
+ 'positive': ['adaptable', 'all climate'],
369
+ 'negative': []
370
+ }
371
+ }
372
+
373
+ climate_score = 0.4 # 基礎分數
374
+ if user_prefs.climate in climate_traits:
375
+ # 正面特質加分
376
+ climate_score += sum(0.2 for term in climate_traits[user_prefs.climate]['positive']
377
+ if term in description)
378
+ # 負面特質減分
379
+ climate_score -= sum(0.2 for term in climate_traits[user_prefs.climate]['negative']
380
+ if term in description)
381
+
382
+ factors['weather_adaptability'] = min(1.0, max(0.0, climate_score))
383
+
384
+ # 7. 運動類型匹配評估
385
+ exercise_type_traits = {
386
+ 'light_walks': ['calm', 'gentle'],
387
+ 'moderate_activity': ['adaptable', 'balanced'],
388
+ 'active_training': ['athletic', 'energetic']
389
+ }
390
+
391
+ if user_prefs.exercise_type in exercise_type_traits:
392
+ match_score = sum(0.25 for trait in exercise_type_traits[user_prefs.exercise_type]
393
+ if trait in temperament)
394
+ factors['exercise_match'] = min(1.0, match_score + 0.5) # 基礎分0.5
395
+
396
+ # 8. 生活方式適配評估
397
+ lifestyle_score = 0.5 # 基礎分數
398
+
399
+ # 空間適配
400
+ if user_prefs.living_space == 'apartment':
401
+ if size == 'Small':
402
+ lifestyle_score += 0.2
403
+ elif size == 'Large':
404
+ lifestyle_score -= 0.2
405
+ elif user_prefs.living_space == 'house_large':
406
+ if size in ['Large', 'Giant']:
407
+ lifestyle_score += 0.2
408
+
409
+ # 時間可用性適配
410
+ time_availability_bonus = {
411
+ 'limited': -0.1,
412
+ 'moderate': 0,
413
+ 'flexible': 0.1
414
+ }
415
+ lifestyle_score += time_availability_bonus.get(user_prefs.time_availability, 0)
416
+
417
+ factors['lifestyle_fit'] = min(1.0, max(0.0, lifestyle_score))
418
+
419
+ return factors
420
+
421
+ def amplify_score_extreme(self, score: float) -> float:
422
+ """
423
+ 優化分數分布,提供更有意義的評分範圍。
424
+ 純粹進行數學轉換,不依賴外部資訊。
425
+
426
+ Parameters:
427
+ score: 原始評分(0-1之間的浮點數)
428
+
429
+ Returns:
430
+ float: 調整後的評分(0-1之間的浮點數)
431
+ """
432
+ def smooth_curve(x: float, steepness: float = 12) -> float:
433
+ """創建平滑的S型曲線用於分數轉換"""
434
+ return 1 / (1 + math.exp(-steepness * (x - 0.5)))
435
+
436
+ # 90-100分的轉換(極佳匹配)
437
+ if score >= 0.90:
438
+ position = (score - 0.90) / 0.10
439
+ return 0.96 + (position * 0.04)
440
+
441
+ # 80-90分的轉換(優秀匹配)
442
+ elif score >= 0.80:
443
+ position = (score - 0.80) / 0.10
444
+ return 0.90 + (position * 0.06)
445
+
446
+ # 70-80分的轉換(良好匹配)
447
+ elif score >= 0.70:
448
+ position = (score - 0.70) / 0.10
449
+ return 0.82 + (position * 0.08)
450
+
451
+ # 50-70分的轉換(可接受匹配)
452
+ elif score >= 0.50:
453
+ position = (score - 0.50) / 0.20
454
+ return 0.75 + (smooth_curve(position) * 0.07)
455
+
456
+ # 50分以下的轉換(較差匹配)
457
+ else:
458
+ position = score / 0.50
459
+ return 0.70 + (smooth_curve(position) * 0.05)
460
+
461
+ def apply_special_case_adjustments(self, score: float, user_prefs: UserPreferences, breed_info: dict) -> float:
462
+ """
463
+ 處理特殊情況和極端案例的評分調整。這個函數特別關注:
464
+ 1. 條件組合的協同效應
465
+ 2. 品種特性的獨特需求
466
+ 3. 極端情況的合理處理
467
+
468
+ 這個函數就像是一個細心的裁判,會考慮到各種特殊情況,
469
+ 並根據具體場景做出合理的評分調整。
470
+
471
+ Parameters:
472
+ score: 初始評分
473
+ user_prefs: 使用者偏好
474
+ breed_info: 品種資訊
475
+ Returns:
476
+ float: 調整後的評分(0.2-1.0之間)
477
+ """
478
+ severity_multiplier = 1.0
479
+
480
+ def evaluate_spatial_exercise_combination() -> float:
481
+ """
482
+ 評估空間與運動需求的組合效應。
483
+
484
+ 這個函數不再過分懲罰大型犬,而是更多地考慮品種的實際特性。
485
+ 就像評估一個運動員是否適合在特定場地訓練一樣,我們需要考慮
486
+ 場地大小和運動需求的整體匹配度。
487
+ """
488
+ multiplier = 1.0
489
+
490
+ if user_prefs.living_space == 'apartment':
491
+ temperament = breed_info.get('Temperament', '').lower()
492
+ description = breed_info.get('Description', '').lower()
493
+
494
+ # 檢查品種是否有利於公寓生活的特徵
495
+ apartment_friendly = any(trait in temperament or trait in description
496
+ for trait in ['calm', 'adaptable', 'quiet'])
497
+
498
+ # 大型犬的特殊處理
499
+ if breed_info['Size'] in ['Large', 'Giant']:
500
+ if apartment_friendly:
501
+ multiplier *= 0.85 # 從0.7提升到0.85,降低懲罰
502
+ else:
503
+ multiplier *= 0.75 # 從0.5提升到0.75
504
+
505
+ # 檢查運動需求的匹配度
506
+ exercise_needs = breed_info.get('Exercise Needs', 'MODERATE').upper()
507
+ exercise_time = user_prefs.exercise_time
508
+
509
+ if exercise_needs in ['HIGH', 'VERY HIGH']:
510
+ if exercise_time >= 120: # 高運動量可以部分補償空間限制
511
+ multiplier *= 1.1
512
+
513
+ return multiplier
514
+
515
+ def evaluate_experience_combination() -> float:
516
+ """
517
+ 評估經驗需求的複合影響。
518
+
519
+ 這個函數就像是評估一個工作崗位與應聘者經驗的匹配度,
520
+ 需要綜合考慮工作難度和應聘者能力。
521
+ """
522
+ multiplier = 1.0
523
+ temperament = breed_info.get('Temperament', '').lower()
524
+ care_level = breed_info.get('Care Level', 'MODERATE')
525
+
526
+ # 新手飼主的特殊考慮,更寬容的評估標準
527
+ if user_prefs.experience_level == 'beginner':
528
+ if care_level == 'HIGH':
529
+ if user_prefs.has_children:
530
+ multiplier *= 0.7 # 從0.5提升到0.7
531
+ else:
532
+ multiplier *= 0.8 # 從0.6提升到0.8
533
+
534
+ # 性格特徵影響,降低懲罰程度
535
+ challenging_traits = {
536
+ 'stubborn': -0.10, # 從-0.15降低
537
+ 'independent': -0.08, # 從-0.12降低
538
+ 'dominant': -0.08, # 從-0.12降低
539
+ 'protective': -0.06, # 從-0.10降低
540
+ 'aggressive': -0.15 # 保持較高懲罰因安全考慮
541
+ }
542
+
543
+ for trait, penalty in challenging_traits.items():
544
+ if trait in temperament:
545
+ multiplier *= (1 + penalty)
546
+
547
+ return multiplier
548
+
549
+ def evaluate_breed_specific_requirements() -> float:
550
+ """
551
+ 評估品種特定需求。
552
+
553
+ 這個函數就像是為每個品種量身定制評估標準,
554
+ 考慮其獨特的特性和需求。
555
+ """
556
+ multiplier = 1.0
557
+ exercise_time = user_prefs.exercise_time
558
+ exercise_type = user_prefs.exercise_type
559
+
560
+ # 檢查品種特性
561
+ temperament = breed_info.get('Temperament', '').lower()
562
+ description = breed_info.get('Description', '').lower()
563
+ exercise_needs = breed_info.get('Exercise Needs', 'MODERATE').upper()
564
+
565
+ # 運動需求匹配度評估,更合理的標準
566
+ if exercise_needs == 'LOW':
567
+ if exercise_time > 120:
568
+ multiplier *= 0.85 # 從0.5提升到0.85
569
+ elif exercise_needs == 'VERY HIGH':
570
+ if exercise_time < 60:
571
+ multiplier *= 0.7 # 從0.5提升到0.7
572
+
573
+ # 特殊品種類型的考慮
574
+ if 'sprint' in temperament:
575
+ if exercise_time > 120 and exercise_type != 'active_training':
576
+ multiplier *= 0.85 # 從0.7提升到0.85
577
+
578
+ if any(trait in temperament for trait in ['working', 'herding']):
579
+ if exercise_time < 90 or exercise_type == 'light_walks':
580
+ multiplier *= 0.8 # 從0.7提升到0.8
581
+
582
+ return multiplier
583
+
584
+ # 計算各項調整
585
+ space_exercise_mult = evaluate_spatial_exercise_combination()
586
+ experience_mult = evaluate_experience_combination()
587
+ breed_specific_mult = evaluate_breed_specific_requirements()
588
+
589
+ # 整合所有調整因素
590
+ severity_multiplier *= space_exercise_mult
591
+ severity_multiplier *= experience_mult
592
+ severity_multiplier *= breed_specific_mult
593
+
594
+ # 應用最終調整,確保分數在合理範圍內
595
+ final_score = score * severity_multiplier
596
+ return max(0.2, min(1.0, final_score))
breed_recommendation_enhanced.py ADDED
@@ -0,0 +1,640 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from typing import Dict, List, Any, Optional
3
+ import traceback
4
+ from semantic_breed_recommender import get_breed_recommendations_by_description, get_enhanced_recommendations_with_unified_scoring
5
+ from natural_language_processor import get_nlp_processor
6
+ from recommendation_html_format import format_unified_recommendation_html
7
+
8
+ def create_description_examples():
9
+ """Create HTML for description examples with dynamic visibility"""
10
+ return """
11
+ <div style='
12
+ background: linear-gradient(135deg, rgba(66, 153, 225, 0.1) 0%, rgba(72, 187, 120, 0.1) 100%);
13
+ border-radius: 12px;
14
+ padding: 20px;
15
+ margin: 15px 0;
16
+ border-left: 4px solid #4299e1;
17
+ display: block;
18
+ '>
19
+ <h4 style='
20
+ color: #2d3748;
21
+ margin: 0 0 15px 0;
22
+ font-size: 1.1em;
23
+ font-weight: 600;
24
+ '>💡 Example Descriptions - Try These Expression Styles:</h4>
25
+
26
+ <div style='
27
+ display: grid;
28
+ grid-template-columns: 1fr 1fr;
29
+ gap: 15px;
30
+ margin-top: 10px;
31
+ '>
32
+ <div style='
33
+ background: white;
34
+ padding: 12px;
35
+ border-radius: 8px;
36
+ border: 1px solid #e2e8f0;
37
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
38
+ '>
39
+ <strong style='color: #4299e1;'>🏠 Living Environment:</strong><br>
40
+ <span style='color: #4a5568; font-size: 0.9em;'>
41
+ "I live in an apartment and need a quiet, small dog that's good with children"
42
+ </span>
43
+ </div>
44
+
45
+ <div style='
46
+ background: white;
47
+ padding: 12px;
48
+ border-radius: 8px;
49
+ border: 1px solid #e2e8f0;
50
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
51
+ '>
52
+ <strong style='color: #48bb78;'>🎾 Activity Preferences:</strong><br>
53
+ <span style='color: #4a5568; font-size: 0.9em;'>
54
+ "I want an active medium to large dog for hiking and outdoor activities"
55
+ </span>
56
+ </div>
57
+
58
+ <div style='
59
+ background: white;
60
+ padding: 12px;
61
+ border-radius: 8px;
62
+ border: 1px solid #e2e8f0;
63
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
64
+ '>
65
+ <strong style='color: #ed8936;'>❤️ Breed Preferences:</strong><br>
66
+ <span style='color: #4a5568; font-size: 0.9em;'>
67
+ "I love Border Collies most, then Golden Retrievers, followed by Pugs"
68
+ </span>
69
+ </div>
70
+
71
+ <div style='
72
+ background: white;
73
+ padding: 12px;
74
+ border-radius: 8px;
75
+ border: 1px solid #e2e8f0;
76
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
77
+ '>
78
+ <strong style='color: #9f7aea;'>👥 Family Situation:</strong><br>
79
+ <span style='color: #4a5568; font-size: 0.9em;'>
80
+ "Looking for a calm, low-maintenance companion dog for elderly person"
81
+ </span>
82
+ </div>
83
+ </div>
84
+
85
+ <div style='
86
+ margin-top: 15px;
87
+ padding: 12px;
88
+ background: rgba(255, 255, 255, 0.8);
89
+ border-radius: 8px;
90
+ border: 1px solid #e2e8f0;
91
+ '>
92
+ <strong style='color: #2d3748;'>🔍 Tips:</strong>
93
+ <span style='color: #4a5568; font-size: 0.9em;'>
94
+ Please describe in English, including living environment, preferred breeds, family situation, activity needs, etc. The more detailed your description, the more accurate the recommendations!
95
+ </span>
96
+ </div>
97
+ </div>
98
+ """
99
+
100
+
101
+ def create_recommendation_tab(
102
+ UserPreferences,
103
+ get_breed_recommendations,
104
+ format_recommendation_html,
105
+ history_component
106
+ ):
107
+ """Create the enhanced breed recommendation tab with natural language support"""
108
+
109
+ with gr.TabItem("Breed Recommendation"):
110
+ with gr.Tabs():
111
+ # --------------------------
112
+ # Tab 1: Find by Criteria
113
+ # --------------------------
114
+ with gr.Tab("Find by Criteria"):
115
+ gr.HTML("""
116
+ <div style='
117
+ text-align: center;
118
+ position: relative;
119
+ padding: 20px 0;
120
+ margin: 15px 0;
121
+ background: linear-gradient(to right, rgba(66, 153, 225, 0.1), rgba(72, 187, 120, 0.1));
122
+ border-radius: 10px;
123
+ '>
124
+
125
+ <p style='
126
+ font-size: 1.2em;
127
+ margin: 0;
128
+ padding: 0 20px;
129
+ line-height: 1.5;
130
+ background: linear-gradient(90deg, #4299e1, #48bb78);
131
+ -webkit-background-clip: text;
132
+ -webkit-text-fill-color: transparent;
133
+ font-weight: 600;
134
+ '>
135
+ Tell us about your lifestyle, and we'll recommend the perfect dog breeds for you!
136
+ </p>
137
+ <div style='
138
+ margin-top: 15px;
139
+ padding: 10px 20px;
140
+ background: linear-gradient(to right, rgba(66, 153, 225, 0.15), rgba(72, 187, 120, 0.15));
141
+ border-radius: 8px;
142
+ font-size: 0.9em;
143
+ color: #2D3748;
144
+ display: flex;
145
+ align-items: center;
146
+ justify-content: center;
147
+ gap: 8px;
148
+ '>
149
+ <span style="font-size: 1.2em;">🔬</span>
150
+ <span style="
151
+ letter-spacing: 0.3px;
152
+ line-height: 1.4;
153
+ ">The matching algorithm is continuously improving. Results are for reference only.</span>
154
+ </div>
155
+ </div>
156
+ """)
157
+
158
+ with gr.Row():
159
+ with gr.Column():
160
+ living_space = gr.Radio(
161
+ choices=["apartment", "house_small", "house_large"],
162
+ label="What type of living space do you have?",
163
+ info="Choose your current living situation",
164
+ value="apartment"
165
+ )
166
+
167
+ yard_access = gr.Radio(
168
+ choices=["no_yard", "shared_yard", "private_yard"],
169
+ label="Yard Access Type",
170
+ info="Available outdoor space",
171
+ value="no_yard"
172
+ )
173
+
174
+ exercise_time = gr.Slider(
175
+ minimum=0,
176
+ maximum=180,
177
+ value=60,
178
+ label="Daily exercise time (minutes)",
179
+ info="Consider walks, play time, and training"
180
+ )
181
+
182
+ exercise_type = gr.Radio(
183
+ choices=["light_walks", "moderate_activity", "active_training"],
184
+ label="Exercise Style",
185
+ info="What kind of activities do you prefer?",
186
+ value="moderate_activity"
187
+ )
188
+
189
+ grooming_commitment = gr.Radio(
190
+ choices=["low", "medium", "high"],
191
+ label="Grooming commitment level",
192
+ info="Low: monthly, Medium: weekly, High: daily",
193
+ value="medium"
194
+ )
195
+
196
+ with gr.Column():
197
+ size_preference = gr.Radio(
198
+ choices=["no_preference", "small", "medium", "large", "giant"],
199
+ label="Preference Dog Size",
200
+ info="Select your preferred dog size - this will strongly filter the recommendations",
201
+ value="no_preference"
202
+ )
203
+ experience_level = gr.Radio(
204
+ choices=["beginner", "intermediate", "advanced"],
205
+ label="Dog ownership experience",
206
+ info="Be honest - this helps find the right match",
207
+ value="beginner"
208
+ )
209
+
210
+ time_availability = gr.Radio(
211
+ choices=["limited", "moderate", "flexible"],
212
+ label="Time Availability",
213
+ info="Time available for dog care daily",
214
+ value="moderate"
215
+ )
216
+
217
+ has_children = gr.Checkbox(
218
+ label="Have children at home",
219
+ info="Helps recommend child-friendly breeds"
220
+ )
221
+
222
+ children_age = gr.Radio(
223
+ choices=["toddler", "school_age", "teenager"],
224
+ label="Children's Age Group",
225
+ info="Helps match with age-appropriate breeds",
226
+ visible=False
227
+ )
228
+
229
+ noise_tolerance = gr.Radio(
230
+ choices=["low", "medium", "high"],
231
+ label="Noise tolerance level",
232
+ info="Some breeds are more vocal than others",
233
+ value="medium"
234
+ )
235
+
236
+ def update_children_age_visibility(has_children_val):
237
+ """Update children age visibility based on has_children checkbox"""
238
+ return gr.update(visible=has_children_val)
239
+
240
+ has_children.change(
241
+ fn=update_children_age_visibility,
242
+ inputs=[has_children],
243
+ outputs=[children_age]
244
+ )
245
+
246
+ # --------- 條件搜尋---------
247
+ def find_breed_matches(
248
+ living_space, yard_access, exercise_time, exercise_type,
249
+ grooming_commitment, size_preference, experience_level,
250
+ time_availability, has_children, children_age, noise_tolerance
251
+ ):
252
+ """Process criteria-based breed matching and persist history"""
253
+ try:
254
+ # 1) 建立偏好
255
+ user_prefs = UserPreferences(
256
+ living_space=living_space,
257
+ yard_access=yard_access,
258
+ exercise_time=exercise_time,
259
+ exercise_type=exercise_type,
260
+ grooming_commitment=grooming_commitment,
261
+ size_preference=size_preference,
262
+ experience_level=experience_level,
263
+ time_availability=time_availability,
264
+ has_children=has_children,
265
+ children_age=children_age if has_children else None,
266
+ noise_tolerance=noise_tolerance,
267
+ # 其他欄位依原始設計
268
+ space_for_play=(living_space != "apartment"),
269
+ other_pets=False,
270
+ climate="moderate",
271
+ health_sensitivity="medium",
272
+ barking_acceptance=noise_tolerance
273
+ )
274
+
275
+ # 2) 取得推薦
276
+ recommendations = get_breed_recommendations(user_prefs)
277
+ print(f"[CRITERIA] generated={len(recommendations) if recommendations else 0}")
278
+
279
+ if not recommendations:
280
+ return format_recommendation_html([], is_description_search=False)
281
+
282
+ # 3) 準備歷史資料(final_score / overall_score 同步)
283
+ history_results = []
284
+ for idx, rec in enumerate(recommendations, start=1):
285
+ final_score = rec.get("final_score", rec.get("overall_score", 0))
286
+ overall_score = final_score # Ensure consistency
287
+ history_results.append({
288
+ "breed": rec.get("breed", "Unknown"),
289
+ "rank": rec.get("rank", idx),
290
+ "final_score": final_score,
291
+ "overall_score": overall_score,
292
+ "base_score": rec.get("base_score", 0),
293
+ "bonus_score": rec.get("bonus_score", 0),
294
+ "scores": rec.get("scores", {})
295
+ })
296
+
297
+ prefs_dict = user_prefs.__dict__ if hasattr(user_prefs, "__dict__") else user_prefs
298
+
299
+ # 4) 寫入歷史(criteria)
300
+ try:
301
+ ok = history_component.save_search(
302
+ user_preferences=prefs_dict,
303
+ results=history_results,
304
+ search_type="criteria",
305
+ description=None
306
+ )
307
+ print(f"[CRITERIA SAVE] ok={ok}, saved={len(history_results)}")
308
+ except Exception as e:
309
+ print(f"[CRITERIA SAVE][ERROR] {str(e)}")
310
+
311
+ # 5) 顯示結果
312
+ return format_recommendation_html(recommendations, is_description_search=False)
313
+
314
+ except Exception as e:
315
+ print(f"[CRITERIA][ERROR] {str(e)}")
316
+ print(traceback.format_exc())
317
+ return f"""
318
+ <div style="text-align: center; padding: 20px; color: #e53e3e;">
319
+ <h3>⚠️ Error generating recommendations</h3>
320
+ <p>We encountered an issue while processing your preferences.</p>
321
+ <p><strong>Error details:</strong> {str(e)}</p>
322
+ </div>
323
+ """
324
+
325
+ find_button = gr.Button("🔍 Find My Perfect Match!", elem_id="find-match-btn", size="lg")
326
+ criteria_results = gr.HTML(label="Breed Recommendations")
327
+ find_button.click(
328
+ fn=find_breed_matches,
329
+ inputs=[living_space, yard_access, exercise_time, exercise_type,
330
+ grooming_commitment, size_preference, experience_level,
331
+ time_availability, has_children, children_age, noise_tolerance],
332
+ outputs=criteria_results
333
+ )
334
+
335
+ # --------------------------
336
+ # Tab 2: Find by Description
337
+ # --------------------------
338
+ with gr.Tab("Find by Description") as description_tab:
339
+ gr.HTML("""
340
+ <div style='
341
+ text-align: center;
342
+ position: relative;
343
+ padding: 20px 0;
344
+ margin: 15px 0;
345
+ background: linear-gradient(to right, rgba(66, 153, 225, 0.1), rgba(72, 187, 120, 0.1));
346
+ border-radius: 10px;
347
+ '>
348
+ <div style='
349
+ position: absolute;
350
+ top: 10px;
351
+ right: 20px;
352
+ background: linear-gradient(90deg, #ff6b6b, #feca57);
353
+ color: white;
354
+ padding: 4px 12px;
355
+ border-radius: 15px;
356
+ font-size: 0.85em;
357
+ font-weight: 600;
358
+ letter-spacing: 1px;
359
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
360
+ '>NEW</div>
361
+ <p style='
362
+ font-size: 1.2em;
363
+ margin: 0;
364
+ padding: 0 20px;
365
+ line-height: 1.5;
366
+ background: linear-gradient(90deg, #4299e1, #48bb78);
367
+ -webkit-background-clip: text;
368
+ -webkit-text-fill-color: transparent;
369
+ font-weight: 600;
370
+ '>
371
+ Describe your needs in natural language, and AI will find the most suitable breeds!
372
+ </p>
373
+ <div style='
374
+ margin-top: 15px;
375
+ padding: 10px 20px;
376
+ background: linear-gradient(to right, rgba(255, 107, 107, 0.15), rgba(254, 202, 87, 0.15));
377
+ border-radius: 8px;
378
+ font-size: 0.9em;
379
+ color: #2D3748;
380
+ display: flex;
381
+ align-items: center;
382
+ justify-content: center;
383
+ gap: 8px;
384
+ '>
385
+ <span style="font-size: 1.2em;">🚀</span>
386
+ <span style="
387
+ letter-spacing: 0.3px;
388
+ line-height: 1.4;
389
+ "><strong>New Feature:</strong> Based on advanced semantic understanding technology, making search more aligned with your real needs!</span>
390
+ </div>
391
+ </div>
392
+ """)
393
+
394
+ examples_display = gr.HTML(create_description_examples())
395
+
396
+ description_input = gr.Textbox(
397
+ label="🗣️ Please describe your needs",
398
+ placeholder=("Example: I live in an apartment and need a quiet, small dog that's good with children. "
399
+ "I prefer Border Collies and Golden Retrievers..."),
400
+ lines=4,
401
+ max_lines=6,
402
+ elem_classes=["description-input"]
403
+ )
404
+
405
+ validation_status = gr.HTML(visible=False)
406
+
407
+ # Accuracy disclaimer
408
+ gr.HTML("""
409
+ <div style='
410
+ background: linear-gradient(135deg, rgba(34, 197, 94, 0.08) 0%, rgba(59, 130, 246, 0.08) 100%);
411
+ border: 1px solid rgba(34, 197, 94, 0.2);
412
+ border-radius: 10px;
413
+ padding: 16px 20px;
414
+ margin: 16px 0 20px 0;
415
+ display: flex;
416
+ align-items: center;
417
+ gap: 12px;
418
+ '>
419
+ <div style='
420
+ background: linear-gradient(135deg, #22c55e, #3b82f6);
421
+ border-radius: 50%;
422
+ width: 32px;
423
+ height: 32px;
424
+ display: flex;
425
+ align-items: center;
426
+ justify-content: center;
427
+ flex-shrink: 0;
428
+ '>
429
+ <span style='color: white; font-size: 16px; font-weight: bold;'>ⓘ</span>
430
+ </div>
431
+ <div style='flex: 1;'>
432
+ <div style='
433
+ color: #1f2937;
434
+ font-weight: 600;
435
+ font-size: 0.95em;
436
+ margin-bottom: 4px;
437
+ line-height: 1.4;
438
+ '>
439
+ Accuracy Continuously Improving - Use as Reference Guide
440
+ </div>
441
+ <div style='
442
+ color: #4b5563;
443
+ font-size: 0.88em;
444
+ line-height: 1.5;
445
+ letter-spacing: 0.2px;
446
+ '>
447
+ The AI recommendation system is constantly learning and improving. Use these recommendations as a helpful reference for your pet adoption.
448
+ </div>
449
+ </div>
450
+ </div>
451
+ """)
452
+
453
+ def validate_description_input(text):
454
+ """Validate description input"""
455
+ try:
456
+ nlp = get_nlp_processor()
457
+ validation = nlp.validate_input(text)
458
+ if validation.get("is_valid", True):
459
+ return gr.update(visible=False), True
460
+ else:
461
+ error_html = f"""
462
+ <div style='
463
+ background: #fed7d7;
464
+ border: 1px solid #fc8181;
465
+ color: #c53030;
466
+ padding: 10px;
467
+ border-radius: 8px;
468
+ margin: 10px 0;
469
+ '>
470
+ <strong>⚠️ {validation.get('error', 'Invalid input')}</strong><br>
471
+ {"<br>".join(f"• {s}" for s in validation.get('suggestions', []))}
472
+ </div>
473
+ """
474
+ return gr.update(value=error_html, visible=True), False
475
+ except Exception as e:
476
+ # 無 NLP 驗證也可放行
477
+ print(f"[DESC][VALIDATE][WARN] {str(e)}")
478
+ return gr.update(visible=False), True
479
+
480
+ def find_breeds_by_description(description_text):
481
+ """Find breeds based on description and persist history"""
482
+ try:
483
+ if not description_text or not description_text.strip():
484
+ return """
485
+ <div style="text-align: center; padding: 20px; color: #718096;">
486
+ <p>Please enter your description to get personalized recommendations</p>
487
+ </div>
488
+ """
489
+
490
+ # 驗證(若可用)
491
+ try:
492
+ nlp = get_nlp_processor()
493
+ validation = nlp.validate_input(description_text)
494
+ if not validation.get("is_valid", True):
495
+ return f"""
496
+ <div style="text-align: center; padding: 20px; color: #e53e3e;">
497
+ <h3>⚠️ Input validation failed</h3>
498
+ <p>{validation.get('error','Invalid input')}</p>
499
+ <ul style="text-align: left; display: inline-block;">
500
+ {"".join(f"<li>{s}</li>" for s in validation.get('suggestions', []))}
501
+ </ul>
502
+ </div>
503
+ """
504
+ except Exception as e:
505
+ print(f"[DESC][VALIDATE][WARN] {str(e)} (skip validation)")
506
+
507
+ # 取得增強語意推薦
508
+ recommendations = get_enhanced_recommendations_with_unified_scoring(
509
+ user_description=description_text,
510
+ top_k=15
511
+ )
512
+ print(f"[DESC] generated={len(recommendations) if recommendations else 0}")
513
+
514
+ if not recommendations:
515
+ return """
516
+ <div style="text-align: center; padding: 20px; color: #e53e3e;">
517
+ <h3>😔 No matching breeds found</h3>
518
+ <p>No dog breeds match your specific requirements. Please try:</p>
519
+ <ul style="text-align: left; display: inline-block; color: #4a5568;">
520
+ <li>Providing a more general description</li>
521
+ <li>Relaxing some specific requirements</li>
522
+ <li>Including different breed preferences</li>
523
+ </ul>
524
+ </div>
525
+ """
526
+
527
+ # 準備歷史資料
528
+ def _to_float(x, default=0.0):
529
+ try:
530
+ return float(x)
531
+ except Exception:
532
+ return default
533
+
534
+ history_results = []
535
+ for i, rec in enumerate(recommendations, start=1):
536
+ final_score = _to_float(rec.get("final_score", rec.get("overall_score", 0)))
537
+ overall_score = final_score # Ensure consistency between final_score and overall_score
538
+ history_results.append({
539
+ "breed": str(rec.get("breed", "Unknown")),
540
+ "rank": int(rec.get("rank", i)),
541
+ "final_score": final_score,
542
+ "overall_score": overall_score,
543
+ "semantic_score": _to_float(rec.get("semantic_score", 0)),
544
+ "comparative_bonus": _to_float(rec.get("comparative_bonus", 0)),
545
+ "lifestyle_bonus": _to_float(rec.get("lifestyle_bonus", 0)),
546
+ "size": str(rec.get("size", "Unknown")),
547
+ "scores": rec.get("scores", {})
548
+ })
549
+
550
+ # 寫入歷史(description)
551
+ try:
552
+ ok = history_component.save_search(
553
+ user_preferences=None,
554
+ results=history_results,
555
+ search_type="description",
556
+ description=description_text
557
+ )
558
+ print(f"[DESC SAVE] ok={ok}, saved={len(history_results)}")
559
+ except Exception as e:
560
+ print(f"[DESC SAVE][ERROR] {str(e)}")
561
+
562
+ # 使用統一HTML格式化器顯示增強推薦結果
563
+ html_output = format_unified_recommendation_html(recommendations, is_description_search=True)
564
+ return html_output
565
+
566
+ except RuntimeError as e:
567
+ error_msg = str(e)
568
+ print(f"[DESC][RUNTIME_ERROR] {error_msg}")
569
+ return f"""
570
+ <div style="text-align: center; padding: 20px; color: #e53e3e;">
571
+ <h3>🔧 System Configuration Issue</h3>
572
+ <p style="color: #4a5568; text-align: left; max-width: 600px; margin: 0 auto;">
573
+ {error_msg.replace(chr(10), '<br>').replace('•', '&bull;')}
574
+ </p>
575
+ <div style="margin-top: 15px; padding: 10px; background-color: #f7fafc; border-radius: 8px;">
576
+ <p style="color: #2d3748; font-weight: 500;">💡 What you can try:</p>
577
+ <ul style="text-align: left; color: #4a5568; margin: 10px 0;">
578
+ <li>Restart the application</li>
579
+ <li>Use the "Find by Criteria" tab instead</li>
580
+ <li>Contact support if the issue persists</li>
581
+ </ul>
582
+ </div>
583
+ </div>
584
+ """
585
+ except ValueError as e:
586
+ error_msg = str(e)
587
+ print(f"[DESC][VALUE_ERROR] {error_msg}")
588
+ return f"""
589
+ <div style="text-align: center; padding: 20px; color: #e53e3e;">
590
+ <h3>🔍 No Matching Results</h3>
591
+ <p style="color: #4a5568; text-align: left; max-width: 600px; margin: 0 auto;">
592
+ {error_msg}
593
+ </p>
594
+ <div style="margin-top: 15px; padding: 10px; background-color: #f0f9ff; border-radius: 8px;">
595
+ <p style="color: #2d3748; font-weight: 500;">💡 Suggestions to get better results:</p>
596
+ <ul style="text-align: left; color: #4a5568; margin: 10px 0;">
597
+ <li>Try describing your lifestyle more generally</li>
598
+ <li>Mention multiple breed preferences</li>
599
+ <li>Include both what you want and what you can accommodate</li>
600
+ <li>Consider using the "Find by Criteria" tab for structured search</li>
601
+ </ul>
602
+ </div>
603
+ </div>
604
+ """
605
+ except Exception as e:
606
+ error_msg = str(e)
607
+ print(f"[DESC][ERROR] {error_msg}")
608
+ print(traceback.format_exc())
609
+ return f"""
610
+ <div style="text-align: center; padding: 20px; color: #e53e3e;">
611
+ <h3>⚠️ Unexpected Error</h3>
612
+ <p style="color: #4a5568;">An unexpected error occurred while processing your description.</p>
613
+ <details style="margin-top: 15px; text-align: left; max-width: 600px; margin-left: auto; margin-right: auto;">
614
+ <summary style="cursor: pointer; color: #2d3748; font-weight: 500;">Show technical details</summary>
615
+ <pre style="background-color: #f7fafc; padding: 10px; border-radius: 4px; font-size: 12px; color: #4a5568; margin-top: 10px; overflow: auto;">{error_msg}</pre>
616
+ </details>
617
+ <p style="margin-top: 15px; color: #4a5568; font-size: 14px;">Please try the "Find by Criteria" tab or contact support.</p>
618
+ </div>
619
+ """
620
+
621
+ description_input.change(
622
+ fn=lambda x: validate_description_input(x)[0],
623
+ inputs=[description_input],
624
+ outputs=[validation_status]
625
+ )
626
+
627
+ description_button = gr.Button("🤖 Smart Breed Recommendation", elem_id="find-by-description-btn", size="lg")
628
+ description_results = gr.HTML(label="AI Breed Recommendations")
629
+
630
+ description_button.click(
631
+ fn=find_breeds_by_description,
632
+ inputs=[description_input],
633
+ outputs=[description_results]
634
+ )
635
+
636
+ return {
637
+ 'criteria_results': locals().get('criteria_results'),
638
+ 'description_results': locals().get('description_results'),
639
+ 'description_input': locals().get('description_input')
640
+ }
config_manager.py ADDED
@@ -0,0 +1,554 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import sqlite3
3
+ import numpy as np
4
+ from typing import Dict, List, Tuple, Any, Optional, Union
5
+ from dataclasses import dataclass, field, asdict
6
+ from enum import Enum
7
+ import os
8
+ import traceback
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
+ class DataQuality(Enum):
14
+ """資料品質等級"""
15
+ HIGH = "high" # 完整且可靠的資料
16
+ MEDIUM = "medium" # 部分資料或推斷資料
17
+ LOW = "low" # 不完整或不確定的資料
18
+ UNKNOWN = "unknown" # 未知或缺失資料
19
+
20
+ @dataclass
21
+ class BreedStandardization:
22
+ """品種標準化資料結構"""
23
+ canonical_name: str
24
+ display_name: str
25
+ aliases: List[str] = field(default_factory=list)
26
+ size_category: int = 1 # 1=tiny, 2=small, 3=medium, 4=large, 5=giant
27
+ exercise_level: int = 2 # 1=low, 2=moderate, 3=high, 4=very_high
28
+ noise_level: int = 2 # 1=low, 2=moderate, 3=high
29
+ care_complexity: int = 2 # 1=low, 2=moderate, 3=high
30
+ child_compatibility: float = 0.5 # 0=no, 0.5=unknown, 1=yes
31
+ data_quality_scores: Dict[str, DataQuality] = field(default_factory=dict)
32
+ confidence_flags: Dict[str, float] = field(default_factory=dict)
33
+
34
+ @dataclass
35
+ class ConfigurationSettings:
36
+ """配置設定結構"""
37
+ scoring_weights: Dict[str, float] = field(default_factory=dict)
38
+ calibration_settings: Dict[str, Any] = field(default_factory=dict)
39
+ constraint_thresholds: Dict[str, float] = field(default_factory=dict)
40
+ semantic_model_config: Dict[str, Any] = field(default_factory=dict)
41
+ data_imputation_rules: Dict[str, Any] = field(default_factory=dict)
42
+ debug_mode: bool = False
43
+ version: str = "1.0.0"
44
+
45
+ class ConfigManager:
46
+ """
47
+ 中央化配置和資料標準化管理系統
48
+ 處理品種資料標準化、配置管理和資料品質評估
49
+ """
50
+
51
+ def __init__(self, config_file: Optional[str] = None):
52
+ """初始化配置管理器"""
53
+ self.config_file = config_file or "config.json"
54
+ self.breed_standardization = {}
55
+ self.configuration = ConfigurationSettings()
56
+ self.breed_aliases = {}
57
+ self._load_default_configuration()
58
+ self._initialize_breed_standardization()
59
+
60
+ # 嘗試載入自定義配置
61
+ if os.path.exists(self.config_file):
62
+ self._load_configuration()
63
+
64
+ def _load_default_configuration(self):
65
+ """載入預設配置"""
66
+ self.configuration = ConfigurationSettings(
67
+ scoring_weights={
68
+ 'activity_compatibility': 0.35,
69
+ 'noise_compatibility': 0.25,
70
+ 'spatial_compatibility': 0.15,
71
+ 'family_compatibility': 0.10,
72
+ 'maintenance_compatibility': 0.10,
73
+ 'size_compatibility': 0.05
74
+ },
75
+ calibration_settings={
76
+ 'target_range_min': 0.45,
77
+ 'target_range_max': 0.95,
78
+ 'min_effective_range': 0.3,
79
+ 'auto_calibration': True,
80
+ 'tie_breaking_enabled': True
81
+ },
82
+ constraint_thresholds={
83
+ 'apartment_size_limit': 3, # 最大允許尺寸 (medium)
84
+ 'high_exercise_threshold': 3, # 高運動需求閾值
85
+ 'quiet_noise_limit': 2, # 安靜環境噪音限制
86
+ 'child_safety_threshold': 0.8 # 兒童安全最低分數
87
+ },
88
+ semantic_model_config={
89
+ 'model_name': 'all-MiniLM-L6-v2',
90
+ 'fallback_models': ['all-mpnet-base-v2', 'all-MiniLM-L12-v2'],
91
+ 'similarity_threshold': 0.5,
92
+ 'cache_embeddings': True
93
+ },
94
+ data_imputation_rules={
95
+ 'noise_level_defaults': {
96
+ 'terrier': 'high',
97
+ 'hound': 'high',
98
+ 'herding': 'moderate',
99
+ 'toy': 'moderate',
100
+ 'working': 'moderate',
101
+ 'sporting': 'moderate',
102
+ 'non_sporting': 'low',
103
+ 'unknown': 'moderate'
104
+ },
105
+ 'exercise_level_defaults': {
106
+ 'working': 'high',
107
+ 'sporting': 'high',
108
+ 'herding': 'high',
109
+ 'terrier': 'moderate',
110
+ 'hound': 'moderate',
111
+ 'toy': 'low',
112
+ 'non_sporting': 'moderate',
113
+ 'unknown': 'moderate'
114
+ }
115
+ },
116
+ debug_mode=False,
117
+ version="1.0.0"
118
+ )
119
+
120
+ def _initialize_breed_standardization(self):
121
+ """初始化品種標準化"""
122
+ try:
123
+ # 獲取所有品種
124
+ breeds = self._get_all_breeds()
125
+
126
+ for breed in breeds:
127
+ standardized = self._standardize_breed_data(breed)
128
+ self.breed_standardization[breed] = standardized
129
+
130
+ # 建立別名映射
131
+ for alias in standardized.aliases:
132
+ self.breed_aliases[alias.lower()] = breed
133
+
134
+ print(f"Initialized standardization for {len(self.breed_standardization)} breeds")
135
+
136
+ except Exception as e:
137
+ print(f"Error initializing breed standardization: {str(e)}")
138
+ print(traceback.format_exc())
139
+
140
+ def _get_all_breeds(self) -> List[str]:
141
+ """獲取所有品種清單"""
142
+ try:
143
+ conn = sqlite3.connect('animal_detector.db')
144
+ cursor = conn.cursor()
145
+ cursor.execute("SELECT DISTINCT Breed FROM AnimalCatalog")
146
+ breeds = [row[0] for row in cursor.fetchall()]
147
+ cursor.close()
148
+ conn.close()
149
+ return breeds
150
+ except Exception as e:
151
+ print(f"Error getting breed list: {str(e)}")
152
+ return []
153
+
154
+ def _standardize_breed_data(self, breed: str) -> BreedStandardization:
155
+ """標準化品種資料"""
156
+ try:
157
+ # 基本資訊
158
+ breed_info = get_dog_description(breed) or {}
159
+ health_info = breed_health_info.get(breed, {})
160
+ noise_info = breed_noise_info.get(breed, {})
161
+
162
+ # 建立標準化結構
163
+ canonical_name = breed
164
+ display_name = breed.replace('_', ' ')
165
+ aliases = self._generate_breed_aliases(breed)
166
+
167
+ # 標準化分類數據
168
+ size_category = self._standardize_size(breed_info.get('Size', ''))
169
+ exercise_level = self._standardize_exercise_needs(breed_info.get('Exercise Needs', ''))
170
+ noise_level = self._standardize_noise_level(noise_info.get('noise_level', ''))
171
+ care_complexity = self._standardize_care_level(breed_info.get('Care Level', ''))
172
+ child_compatibility = self._standardize_child_compatibility(
173
+ breed_info.get('Good with Children', '')
174
+ )
175
+
176
+ # 評估資料品質
177
+ data_quality_scores = self._assess_data_quality(breed_info, health_info, noise_info)
178
+ confidence_flags = self._calculate_confidence_flags(breed_info, health_info, noise_info)
179
+
180
+ return BreedStandardization(
181
+ canonical_name=canonical_name,
182
+ display_name=display_name,
183
+ aliases=aliases,
184
+ size_category=size_category,
185
+ exercise_level=exercise_level,
186
+ noise_level=noise_level,
187
+ care_complexity=care_complexity,
188
+ child_compatibility=child_compatibility,
189
+ data_quality_scores=data_quality_scores,
190
+ confidence_flags=confidence_flags
191
+ )
192
+
193
+ except Exception as e:
194
+ print(f"Error standardizing breed {breed}: {str(e)}")
195
+ return BreedStandardization(
196
+ canonical_name=breed,
197
+ display_name=breed.replace('_', ' '),
198
+ aliases=self._generate_breed_aliases(breed)
199
+ )
200
+
201
+ def _generate_breed_aliases(self, breed: str) -> List[str]:
202
+ """生成品種別名"""
203
+ aliases = []
204
+ display_name = breed.replace('_', ' ')
205
+
206
+ # 基本別名
207
+ aliases.append(display_name.lower())
208
+ aliases.append(breed.lower())
209
+
210
+ # 常見縮寫和變體
211
+ breed_aliases_map = {
212
+ 'German_Shepherd': ['gsd', 'german shepherd dog', 'alsatian'],
213
+ 'Labrador_Retriever': ['lab', 'labrador', 'retriever'],
214
+ 'Golden_Retriever': ['golden', 'goldie'],
215
+ 'Border_Collie': ['border', 'collie'],
216
+ 'Yorkshire_Terrier': ['yorkie', 'york', 'yorkshire'],
217
+ 'French_Bulldog': ['frenchie', 'french bull', 'bouledogue français'],
218
+ 'Boston_Terrier': ['boston bull', 'american gentleman'],
219
+ 'Cavalier_King_Charles_Spaniel': ['cavalier', 'ckcs', 'king charles'],
220
+ 'American_Staffordshire_Terrier': ['amstaff', 'american staff'],
221
+ 'Jack_Russell_Terrier': ['jrt', 'jack russell', 'parson russell'],
222
+ 'Shih_Tzu': ['shih tzu', 'lion dog'],
223
+ 'Bichon_Frise': ['bichon', 'powder puff'],
224
+ 'Cocker_Spaniel': ['cocker', 'english cocker', 'american cocker']
225
+ }
226
+
227
+ if breed in breed_aliases_map:
228
+ aliases.extend(breed_aliases_map[breed])
229
+
230
+ # 移除重複
231
+ return list(set(aliases))
232
+
233
+ def _standardize_size(self, size_str: str) -> int:
234
+ """標準化體型分類"""
235
+ size_mapping = {
236
+ 'tiny': 1, 'toy': 1,
237
+ 'small': 2, 'little': 2, 'compact': 2,
238
+ 'medium': 3, 'moderate': 3, 'average': 3,
239
+ 'large': 4, 'big': 4,
240
+ 'giant': 5, 'huge': 5, 'extra large': 5
241
+ }
242
+
243
+ size_lower = size_str.lower()
244
+ for key, value in size_mapping.items():
245
+ if key in size_lower:
246
+ return value
247
+
248
+ return 3 # 預設為 medium
249
+
250
+ def _standardize_exercise_needs(self, exercise_str: str) -> int:
251
+ """標準化運動需求"""
252
+ exercise_mapping = {
253
+ 'low': 1, 'minimal': 1, 'light': 1,
254
+ 'moderate': 2, 'average': 2, 'medium': 2, 'regular': 2,
255
+ 'high': 3, 'active': 3, 'vigorous': 3,
256
+ 'very high': 4, 'extreme': 4, 'intense': 4
257
+ }
258
+
259
+ exercise_lower = exercise_str.lower()
260
+ for key, value in exercise_mapping.items():
261
+ if key in exercise_lower:
262
+ return value
263
+
264
+ return 2 # 預設為 moderate
265
+
266
+ def _standardize_noise_level(self, noise_str: str) -> int:
267
+ """標準化噪音水平"""
268
+ noise_mapping = {
269
+ 'low': 1, 'quiet': 1, 'silent': 1, 'minimal': 1,
270
+ 'moderate': 2, 'average': 2, 'medium': 2, 'occasional': 2,
271
+ 'high': 3, 'loud': 3, 'vocal': 3, 'frequent': 3
272
+ }
273
+
274
+ noise_lower = noise_str.lower()
275
+ for key, value in noise_mapping.items():
276
+ if key in noise_lower:
277
+ return value
278
+
279
+ return 2 # 預設為 moderate
280
+
281
+ def _standardize_care_level(self, care_str: str) -> int:
282
+ """標準化護理複雜度"""
283
+ care_mapping = {
284
+ 'low': 1, 'easy': 1, 'simple': 1, 'minimal': 1,
285
+ 'moderate': 2, 'average': 2, 'medium': 2, 'regular': 2,
286
+ 'high': 3, 'complex': 3, 'intensive': 3, 'demanding': 3
287
+ }
288
+
289
+ care_lower = care_str.lower()
290
+ for key, value in care_mapping.items():
291
+ if key in care_lower:
292
+ return value
293
+
294
+ return 2 # 預設為 moderate
295
+
296
+ def _standardize_child_compatibility(self, child_str: str) -> float:
297
+ """標準化兒童相容性"""
298
+ if child_str.lower() == 'yes':
299
+ return 1.0
300
+ elif child_str.lower() == 'no':
301
+ return 0.0
302
+ else:
303
+ return 0.5 # 未知或不確定
304
+
305
+ def _assess_data_quality(self, breed_info: Dict, health_info: Dict,
306
+ noise_info: Dict) -> Dict[str, DataQuality]:
307
+ """評估資料品質"""
308
+ quality_scores = {}
309
+
310
+ # 基本資訊品質
311
+ if breed_info:
312
+ required_fields = ['Size', 'Exercise Needs', 'Temperament', 'Good with Children']
313
+ complete_fields = sum(1 for field in required_fields if breed_info.get(field))
314
+
315
+ if complete_fields >= 4:
316
+ quality_scores['basic_info'] = DataQuality.HIGH
317
+ elif complete_fields >= 2:
318
+ quality_scores['basic_info'] = DataQuality.MEDIUM
319
+ else:
320
+ quality_scores['basic_info'] = DataQuality.LOW
321
+ else:
322
+ quality_scores['basic_info'] = DataQuality.UNKNOWN
323
+
324
+ # 健康資訊品質
325
+ if health_info and health_info.get('health_notes'):
326
+ quality_scores['health_info'] = DataQuality.HIGH
327
+ elif health_info:
328
+ quality_scores['health_info'] = DataQuality.MEDIUM
329
+ else:
330
+ quality_scores['health_info'] = DataQuality.UNKNOWN
331
+
332
+ # 噪音資訊品質
333
+ if noise_info and noise_info.get('noise_level'):
334
+ quality_scores['noise_info'] = DataQuality.HIGH
335
+ else:
336
+ quality_scores['noise_info'] = DataQuality.LOW
337
+
338
+ return quality_scores
339
+
340
+ def _calculate_confidence_flags(self, breed_info: Dict, health_info: Dict,
341
+ noise_info: Dict) -> Dict[str, float]:
342
+ """計算信心度標記"""
343
+ confidence_flags = {}
344
+
345
+ # 基本資訊信心度
346
+ basic_confidence = 0.8 if breed_info else 0.2
347
+ if breed_info and breed_info.get('Description'):
348
+ basic_confidence += 0.1
349
+ confidence_flags['basic_info'] = min(1.0, basic_confidence)
350
+
351
+ # 健康資訊信心度
352
+ health_confidence = 0.7 if health_info else 0.3
353
+ confidence_flags['health_info'] = health_confidence
354
+
355
+ # 噪音資訊信心度
356
+ noise_confidence = 0.8 if noise_info else 0.4
357
+ confidence_flags['noise_info'] = noise_confidence
358
+
359
+ # 整體信心度
360
+ confidence_flags['overall'] = np.mean(list(confidence_flags.values()))
361
+
362
+ return confidence_flags
363
+
364
+ def get_standardized_breed_data(self, breed: str) -> Optional[BreedStandardization]:
365
+ """獲取標準化品種資料"""
366
+ # 嘗試直接匹配
367
+ if breed in self.breed_standardization:
368
+ return self.breed_standardization[breed]
369
+
370
+ # 嘗試別名匹配
371
+ breed_lower = breed.lower()
372
+ if breed_lower in self.breed_aliases:
373
+ canonical_breed = self.breed_aliases[breed_lower]
374
+ return self.breed_standardization.get(canonical_breed)
375
+
376
+ # 模糊匹配
377
+ for alias, canonical_breed in self.breed_aliases.items():
378
+ if breed_lower in alias or alias in breed_lower:
379
+ return self.breed_standardization.get(canonical_breed)
380
+
381
+ return None
382
+
383
+ def apply_data_imputation(self, breed: str) -> BreedStandardization:
384
+ """應用資料插補規則"""
385
+ try:
386
+ standardized = self.get_standardized_breed_data(breed)
387
+ if not standardized:
388
+ return BreedStandardization(canonical_name=breed, display_name=breed.replace('_', ' '))
389
+
390
+ imputation_rules = self.configuration.data_imputation_rules
391
+
392
+ # 噪音水平插補
393
+ if standardized.noise_level == 2: # moderate (可能是預設值)
394
+ breed_group = self._determine_breed_group(breed)
395
+ noise_defaults = imputation_rules.get('noise_level_defaults', {})
396
+ if breed_group in noise_defaults:
397
+ imputed_noise = self._standardize_noise_level(noise_defaults[breed_group])
398
+ standardized.noise_level = imputed_noise
399
+ standardized.confidence_flags['noise_info'] *= 0.7 # 降低信心度
400
+
401
+ # 運動需求插補
402
+ if standardized.exercise_level == 2: # moderate (可能是預設值)
403
+ breed_group = self._determine_breed_group(breed)
404
+ exercise_defaults = imputation_rules.get('exercise_level_defaults', {})
405
+ if breed_group in exercise_defaults:
406
+ imputed_exercise = self._standardize_exercise_needs(exercise_defaults[breed_group])
407
+ standardized.exercise_level = imputed_exercise
408
+ standardized.confidence_flags['basic_info'] *= 0.8 # 降低信心度
409
+
410
+ return standardized
411
+
412
+ except Exception as e:
413
+ print(f"Error applying data imputation for {breed}: {str(e)}")
414
+ return self.get_standardized_breed_data(breed) or BreedStandardization(
415
+ canonical_name=breed, display_name=breed.replace('_', ' ')
416
+ )
417
+
418
+ def _determine_breed_group(self, breed: str) -> str:
419
+ """確定品種群組"""
420
+ breed_lower = breed.lower()
421
+
422
+ if 'terrier' in breed_lower:
423
+ return 'terrier'
424
+ elif 'hound' in breed_lower:
425
+ return 'hound'
426
+ elif any(word in breed_lower for word in ['shepherd', 'collie', 'cattle', 'sheepdog']):
427
+ return 'herding'
428
+ elif any(word in breed_lower for word in ['retriever', 'pointer', 'setter', 'spaniel']):
429
+ return 'sporting'
430
+ elif any(word in breed_lower for word in ['mastiff', 'great', 'rottweiler', 'akita']):
431
+ return 'working'
432
+ elif any(word in breed_lower for word in ['toy', 'pug', 'chihuahua', 'papillon']):
433
+ return 'toy'
434
+ else:
435
+ return 'unknown'
436
+
437
+ def _load_configuration(self):
438
+ """載入配置檔案"""
439
+ try:
440
+ with open(self.config_file, 'r', encoding='utf-8') as f:
441
+ config_data = json.load(f)
442
+
443
+ # 更新配置
444
+ if 'scoring_weights' in config_data:
445
+ self.configuration.scoring_weights.update(config_data['scoring_weights'])
446
+ if 'calibration_settings' in config_data:
447
+ self.configuration.calibration_settings.update(config_data['calibration_settings'])
448
+ if 'constraint_thresholds' in config_data:
449
+ self.configuration.constraint_thresholds.update(config_data['constraint_thresholds'])
450
+ if 'semantic_model_config' in config_data:
451
+ self.configuration.semantic_model_config.update(config_data['semantic_model_config'])
452
+ if 'data_imputation_rules' in config_data:
453
+ self.configuration.data_imputation_rules.update(config_data['data_imputation_rules'])
454
+ if 'debug_mode' in config_data:
455
+ self.configuration.debug_mode = config_data['debug_mode']
456
+
457
+ print(f"Configuration loaded from {self.config_file}")
458
+
459
+ except Exception as e:
460
+ print(f"Error loading configuration: {str(e)}")
461
+
462
+ def save_configuration(self):
463
+ """儲存配置檔案"""
464
+ try:
465
+ config_data = asdict(self.configuration)
466
+
467
+ with open(self.config_file, 'w', encoding='utf-8') as f:
468
+ json.dump(config_data, f, indent=2, ensure_ascii=False)
469
+
470
+ print(f"Configuration saved to {self.config_file}")
471
+
472
+ except Exception as e:
473
+ print(f"Error saving configuration: {str(e)}")
474
+
475
+ def get_configuration(self) -> ConfigurationSettings:
476
+ """獲取當前配置"""
477
+ return self.configuration
478
+
479
+ def update_configuration(self, updates: Dict[str, Any]):
480
+ """更新配置"""
481
+ try:
482
+ for key, value in updates.items():
483
+ if hasattr(self.configuration, key):
484
+ setattr(self.configuration, key, value)
485
+
486
+ print(f"Configuration updated: {list(updates.keys())}")
487
+
488
+ except Exception as e:
489
+ print(f"Error updating configuration: {str(e)}")
490
+
491
+ def get_breed_mapping_summary(self) -> Dict[str, Any]:
492
+ """獲取品種映射摘要"""
493
+ try:
494
+ total_breeds = len(self.breed_standardization)
495
+ total_aliases = len(self.breed_aliases)
496
+
497
+ # 資料品質分布
498
+ quality_distribution = {}
499
+ for breed_data in self.breed_standardization.values():
500
+ for category, quality in breed_data.data_quality_scores.items():
501
+ if category not in quality_distribution:
502
+ quality_distribution[category] = {}
503
+ quality_name = quality.value
504
+ quality_distribution[category][quality_name] = (
505
+ quality_distribution[category].get(quality_name, 0) + 1
506
+ )
507
+
508
+ # 信心度統計
509
+ confidence_stats = {}
510
+ for breed_data in self.breed_standardization.values():
511
+ for category, confidence in breed_data.confidence_flags.items():
512
+ if category not in confidence_stats:
513
+ confidence_stats[category] = []
514
+ confidence_stats[category].append(confidence)
515
+
516
+ confidence_averages = {
517
+ category: np.mean(values) for category, values in confidence_stats.items()
518
+ }
519
+
520
+ return {
521
+ 'total_breeds': total_breeds,
522
+ 'total_aliases': total_aliases,
523
+ 'quality_distribution': quality_distribution,
524
+ 'confidence_averages': confidence_averages,
525
+ 'configuration_version': self.configuration.version
526
+ }
527
+
528
+ except Exception as e:
529
+ print(f"Error generating breed mapping summary: {str(e)}")
530
+ return {'error': str(e)}
531
+
532
+ _config_manager = None
533
+
534
+ def get_config_manager() -> ConfigManager:
535
+ """獲取全局配置管理器"""
536
+ global _config_manager
537
+ if _config_manager is None:
538
+ _config_manager = ConfigManager()
539
+ return _config_manager
540
+
541
+ def get_standardized_breed_data(breed: str) -> Optional[BreedStandardization]:
542
+ """獲取標準化品種資料"""
543
+ manager = get_config_manager()
544
+ return manager.get_standardized_breed_data(breed)
545
+
546
+ def get_breed_with_imputation(breed: str) -> BreedStandardization:
547
+ """獲取應用補進後的品種資料"""
548
+ manager = get_config_manager()
549
+ return manager.apply_data_imputation(breed)
550
+
551
+ def get_system_configuration() -> ConfigurationSettings:
552
+ """系統配置"""
553
+ manager = get_config_manager()
554
+ return manager.get_configuration()
constraint_manager.py ADDED
@@ -0,0 +1,852 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sqlite3
2
+ import json
3
+ import numpy as np
4
+ from typing import List, Dict, Tuple, Set, Optional, Any
5
+ from dataclasses import dataclass, field
6
+ from enum import Enum
7
+ import traceback
8
+ from dog_database import get_dog_description
9
+ from dynamic_scoring_config import get_scoring_config
10
+ from breed_health_info import breed_health_info
11
+ from breed_noise_info import breed_noise_info
12
+ from query_understanding import QueryDimensions
13
+
14
+ class ConstraintPriority(Enum):
15
+ """Constraint priority definitions"""
16
+ CRITICAL = 1 # Critical constraints (safety, space)
17
+ HIGH = 2 # High priority (activity level, noise)
18
+ MODERATE = 3 # Moderate priority (maintenance, experience)
19
+ FLEXIBLE = 4 # Flexible constraints (other preferences)
20
+
21
+ @dataclass
22
+ class ConstraintRule:
23
+ """Constraint rule structure"""
24
+ name: str
25
+ priority: ConstraintPriority
26
+ description: str
27
+ filter_function: str # Function name
28
+ relaxation_allowed: bool = True
29
+ safety_critical: bool = False
30
+
31
+ @dataclass
32
+ class FilterResult:
33
+ """Filter result structure"""
34
+ passed_breeds: Set[str]
35
+ filtered_breeds: Dict[str, str] # breed -> reason
36
+ applied_constraints: List[str]
37
+ relaxed_constraints: List[str] = field(default_factory=list)
38
+ warnings: List[str] = field(default_factory=list)
39
+
40
+ class ConstraintManager:
41
+ """
42
+ Hierarchical constraint management system
43
+ Implements priority-based constraint filtering with progressive constraint relaxation
44
+ """
45
+
46
+ def __init__(self):
47
+ """Initialize constraint manager"""
48
+ self.breed_list = self._load_breed_list()
49
+ self.breed_cache = {} # Breed information cache
50
+ self.constraint_rules = self._initialize_constraint_rules()
51
+ self._warm_cache()
52
+
53
+ def _load_breed_list(self) -> List[str]:
54
+ """Load breed list from database"""
55
+ try:
56
+ conn = sqlite3.connect('animal_detector.db')
57
+ cursor = conn.cursor()
58
+ cursor.execute("SELECT DISTINCT Breed FROM AnimalCatalog")
59
+ breeds = [row[0] for row in cursor.fetchall()]
60
+ cursor.close()
61
+ conn.close()
62
+ return breeds
63
+ except Exception as e:
64
+ print(f"Error loading breed list: {str(e)}")
65
+ return ['Labrador_Retriever', 'German_Shepherd', 'Golden_Retriever',
66
+ 'Bulldog', 'Poodle', 'Beagle', 'Border_Collie', 'Yorkshire_Terrier']
67
+
68
+ def _warm_cache(self):
69
+ """Warm up breed information cache"""
70
+ for breed in self.breed_list:
71
+ self.breed_cache[breed] = self._get_breed_info(breed)
72
+
73
+ def _get_breed_info(self, breed: str) -> Dict[str, Any]:
74
+ """Get comprehensive breed information"""
75
+ if breed in self.breed_cache:
76
+ return self.breed_cache[breed]
77
+
78
+ try:
79
+ # Basic breed information
80
+ breed_info = get_dog_description(breed) or {}
81
+
82
+ # Health information
83
+ health_info = breed_health_info.get(breed, {})
84
+
85
+ # Noise information
86
+ noise_info = breed_noise_info.get(breed, {})
87
+
88
+ # Combine all information
89
+ combined_info = {
90
+ 'breed_name': breed,
91
+ 'display_name': breed.replace('_', ' '),
92
+ 'size': breed_info.get('Size', '').lower(),
93
+ 'exercise_needs': breed_info.get('Exercise Needs', '').lower(),
94
+ 'grooming_needs': breed_info.get('Grooming Needs', '').lower(),
95
+ 'temperament': breed_info.get('Temperament', '').lower(),
96
+ 'good_with_children': breed_info.get('Good with Children', 'Yes'),
97
+ 'care_level': breed_info.get('Care Level', '').lower(),
98
+ 'lifespan': breed_info.get('Lifespan', '10-12 years'),
99
+ 'noise_level': noise_info.get('noise_level', 'moderate').lower(),
100
+ 'health_issues': health_info.get('health_notes', ''),
101
+ 'raw_breed_info': breed_info,
102
+ 'raw_health_info': health_info,
103
+ 'raw_noise_info': noise_info
104
+ }
105
+
106
+ self.breed_cache[breed] = combined_info
107
+ return combined_info
108
+
109
+ except Exception as e:
110
+ print(f"Error getting breed info for {breed}: {str(e)}")
111
+ return {'breed_name': breed, 'display_name': breed.replace('_', ' ')}
112
+
113
+ def _initialize_constraint_rules(self) -> List[ConstraintRule]:
114
+ """Initialize constraint rules"""
115
+ return [
116
+ # Priority 1: Critical constraints (cannot be violated)
117
+ ConstraintRule(
118
+ name="apartment_size_constraint",
119
+ priority=ConstraintPriority.CRITICAL,
120
+ description="Apartment living space size restrictions",
121
+ filter_function="filter_apartment_size",
122
+ relaxation_allowed=False,
123
+ safety_critical=True
124
+ ),
125
+ ConstraintRule(
126
+ name="child_safety_constraint",
127
+ priority=ConstraintPriority.CRITICAL,
128
+ description="Child safety compatibility",
129
+ filter_function="filter_child_safety",
130
+ relaxation_allowed=False,
131
+ safety_critical=True
132
+ ),
133
+ ConstraintRule(
134
+ name="severe_allergy_constraint",
135
+ priority=ConstraintPriority.CRITICAL,
136
+ description="Severe allergy restrictions",
137
+ filter_function="filter_severe_allergies",
138
+ relaxation_allowed=False,
139
+ safety_critical=True
140
+ ),
141
+
142
+ # Priority 2: High priority constraints
143
+ ConstraintRule(
144
+ name="exercise_constraint",
145
+ priority=ConstraintPriority.HIGH,
146
+ description="Exercise requirement mismatch",
147
+ filter_function="filter_exercise_mismatch",
148
+ relaxation_allowed=False,
149
+ safety_critical=False
150
+ ),
151
+ ConstraintRule(
152
+ name="size_bias_correction",
153
+ priority=ConstraintPriority.MODERATE,
154
+ description="Correct size bias in moderate lifestyle matches",
155
+ filter_function="filter_size_bias",
156
+ relaxation_allowed=True,
157
+ safety_critical=False
158
+ ),
159
+ ConstraintRule(
160
+ name="low_activity_constraint",
161
+ priority=ConstraintPriority.HIGH,
162
+ description="Low activity level restrictions",
163
+ filter_function="filter_low_activity",
164
+ relaxation_allowed=True
165
+ ),
166
+ ConstraintRule(
167
+ name="quiet_requirement_constraint",
168
+ priority=ConstraintPriority.HIGH,
169
+ description="Quiet environment requirements",
170
+ filter_function="filter_quiet_requirements",
171
+ relaxation_allowed=True
172
+ ),
173
+ ConstraintRule(
174
+ name="space_compatibility_constraint",
175
+ priority=ConstraintPriority.HIGH,
176
+ description="Living space compatibility",
177
+ filter_function="filter_space_compatibility",
178
+ relaxation_allowed=True
179
+ ),
180
+
181
+ # Priority 3: Moderate constraints
182
+ ConstraintRule(
183
+ name="grooming_preference_constraint",
184
+ priority=ConstraintPriority.MODERATE,
185
+ description="Grooming maintenance preferences",
186
+ filter_function="filter_grooming_preferences",
187
+ relaxation_allowed=True
188
+ ),
189
+ ConstraintRule(
190
+ name="experience_level_constraint",
191
+ priority=ConstraintPriority.MODERATE,
192
+ description="Ownership experience requirements",
193
+ filter_function="filter_experience_level",
194
+ relaxation_allowed=True
195
+ ),
196
+
197
+ # Priority 4: Flexible constraints
198
+ ConstraintRule(
199
+ name="size_preference_constraint",
200
+ priority=ConstraintPriority.FLEXIBLE,
201
+ description="Size preferences",
202
+ filter_function="filter_size_preferences",
203
+ relaxation_allowed=True
204
+ )
205
+ ]
206
+
207
+ def apply_constraints(self, dimensions: QueryDimensions,
208
+ min_candidates: int = 12) -> FilterResult:
209
+ """
210
+ Apply constraint filtering
211
+
212
+ Args:
213
+ dimensions: Query dimensions
214
+ min_candidates: Minimum number of candidate breeds
215
+
216
+ Returns:
217
+ FilterResult: Filtering results
218
+ """
219
+ try:
220
+ # Start with all breeds
221
+ candidates = set(self.breed_list)
222
+ filtered_breeds = {}
223
+ applied_constraints = []
224
+ relaxed_constraints = []
225
+ warnings = []
226
+
227
+ # Apply constraints in priority order
228
+ for priority in [ConstraintPriority.CRITICAL, ConstraintPriority.HIGH,
229
+ ConstraintPriority.MODERATE, ConstraintPriority.FLEXIBLE]:
230
+
231
+ # Get constraint rules for this priority level
232
+ priority_rules = [rule for rule in self.constraint_rules
233
+ if rule.priority == priority]
234
+
235
+ for rule in priority_rules:
236
+ # Check if this constraint should be applied
237
+ if self._should_apply_constraint(rule, dimensions):
238
+ # Apply constraint
239
+ before_count = len(candidates)
240
+ filter_func = getattr(self, rule.filter_function)
241
+ new_filtered = filter_func(candidates, dimensions)
242
+
243
+ # Update candidate list
244
+ candidates -= set(new_filtered.keys())
245
+ filtered_breeds.update(new_filtered)
246
+ applied_constraints.append(rule.name)
247
+
248
+ print(f"Applied {rule.name}: {before_count} -> {len(candidates)} candidates")
249
+
250
+ # Check if constraint relaxation is needed
251
+ if (len(candidates) < min_candidates and
252
+ rule.relaxation_allowed and not rule.safety_critical):
253
+
254
+ # Constraint relaxation
255
+ # candidates.update(new_filtered.keys())
256
+ relaxed_constraints.append(rule.name)
257
+ warnings.append(f"Relaxed {rule.description} to maintain diversity")
258
+
259
+ print(f"Relaxed {rule.name}: restored to {len(candidates)} candidates")
260
+
261
+ # If too few candidates after critical constraints, warn but don't relax
262
+ if (priority == ConstraintPriority.CRITICAL and
263
+ len(candidates) < min_candidates):
264
+ warnings.append(f"Critical constraints resulted in only {len(candidates)} candidates")
265
+
266
+ # Final safety net: ensure at least some candidate breeds
267
+ if len(candidates) == 0:
268
+ warnings.append("All breeds filtered out, returning top safe breeds")
269
+ candidates = self._get_emergency_candidates()
270
+
271
+ return FilterResult(
272
+ passed_breeds=candidates,
273
+ filtered_breeds=filtered_breeds,
274
+ applied_constraints=applied_constraints,
275
+ relaxed_constraints=relaxed_constraints,
276
+ warnings=warnings
277
+ )
278
+
279
+ except Exception as e:
280
+ print(f"Error applying constraints: {str(e)}")
281
+ print(traceback.format_exc())
282
+ return FilterResult(
283
+ passed_breeds=set(self.breed_list[:min_candidates]),
284
+ filtered_breeds={},
285
+ applied_constraints=[],
286
+ warnings=[f"Constraint application failed: {str(e)}"]
287
+ )
288
+
289
+ def _should_apply_constraint(self, rule: ConstraintRule,
290
+ dimensions: QueryDimensions) -> bool:
291
+ """Enhanced constraint application logic"""
292
+
293
+ # Always apply size constraints when space is mentioned
294
+ if rule.name == "apartment_size_constraint":
295
+ return any(term in dimensions.spatial_constraints
296
+ for term in ['apartment', 'small', 'studio', 'condo'])
297
+
298
+ # Apply exercise constraints when activity level is specified
299
+ if rule.name == "exercise_constraint":
300
+ return len(dimensions.activity_level) > 0 or \
301
+ any(term in str(dimensions.spatial_constraints)
302
+ for term in ['apartment', 'small'])
303
+
304
+ # Child safety constraint
305
+ if rule.name == "child_safety_constraint":
306
+ return 'children' in dimensions.family_context
307
+
308
+ # Severe allergy constraint
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
315
+
316
+ # Quiet requirement constraint
317
+ if rule.name == "quiet_requirement_constraint":
318
+ return 'low' in dimensions.noise_preferences
319
+
320
+ # Space compatibility constraint
321
+ if rule.name == "space_compatibility_constraint":
322
+ return ('apartment' in dimensions.spatial_constraints or
323
+ 'house' in dimensions.spatial_constraints)
324
+
325
+ # Grooming preference constraint
326
+ if rule.name == "grooming_preference_constraint":
327
+ return len(dimensions.maintenance_level) > 0
328
+
329
+ # Experience level constraint
330
+ if rule.name == "experience_level_constraint":
331
+ return 'first_time' in dimensions.special_requirements
332
+
333
+ # Size preference constraint
334
+ if rule.name == "size_preference_constraint":
335
+ return len(dimensions.size_preferences) > 0
336
+
337
+ return False
338
+
339
+ def filter_apartment_size(self, candidates: Set[str],
340
+ dimensions: QueryDimensions) -> Dict[str, str]:
341
+ """Enhanced apartment size filtering with strict enforcement"""
342
+ filtered = {}
343
+
344
+ # Extract living space type with better pattern matching
345
+ living_space = self._extract_living_space(dimensions)
346
+ space_requirements = self._get_space_requirements(living_space)
347
+
348
+ for breed in list(candidates):
349
+ breed_info = self.breed_cache.get(breed, {})
350
+ breed_size = self._normalize_breed_size(breed_info.get('size', 'Medium'))
351
+ exercise_needs = self._normalize_exercise_level(breed_info.get('exercise_needs', 'Moderate'))
352
+
353
+ # Dynamic space compatibility check
354
+ compatibility_score = self._calculate_space_compatibility(
355
+ breed_size, exercise_needs, space_requirements
356
+ )
357
+
358
+ # Apply threshold-based filtering
359
+ if compatibility_score < 0.3: # Strict threshold for poor matches
360
+ reason = self._generate_filter_reason(breed_size, exercise_needs, living_space)
361
+ filtered[breed] = reason
362
+ continue
363
+
364
+ return filtered
365
+
366
+ def _extract_living_space(self, dimensions: QueryDimensions) -> str:
367
+ """Extract living space type from dimensions"""
368
+ spatial_text = ' '.join(dimensions.spatial_constraints).lower()
369
+
370
+ if any(term in spatial_text for term in ['apartment', 'small apartment', 'studio', 'condo']):
371
+ return 'apartment'
372
+ elif any(term in spatial_text for term in ['small house', 'townhouse']):
373
+ return 'small_house'
374
+ elif any(term in spatial_text for term in ['medium house', 'medium-sized']):
375
+ return 'medium_house'
376
+ elif any(term in spatial_text for term in ['large house', 'big house']):
377
+ return 'large_house'
378
+ else:
379
+ return 'medium_house' # Default assumption
380
+
381
+ def _get_space_requirements(self, living_space: str) -> Dict[str, float]:
382
+ """Get space requirements for different living situations"""
383
+ requirements = {
384
+ 'apartment': {'min_space': 1.0, 'yard_bonus': 0.0, 'exercise_penalty': 1.5},
385
+ 'small_house': {'min_space': 1.5, 'yard_bonus': 0.2, 'exercise_penalty': 1.2},
386
+ 'medium_house': {'min_space': 2.0, 'yard_bonus': 0.3, 'exercise_penalty': 1.0},
387
+ 'large_house': {'min_space': 3.0, 'yard_bonus': 0.5, 'exercise_penalty': 0.8}
388
+ }
389
+ return requirements.get(living_space, requirements['medium_house'])
390
+
391
+ def _normalize_breed_size(self, size: str) -> str:
392
+ """Normalize breed size to standard categories"""
393
+ size_lower = size.lower()
394
+ if any(term in size_lower for term in ['toy', 'tiny']):
395
+ return 'toy'
396
+ elif 'small' in size_lower:
397
+ return 'small'
398
+ elif 'medium' in size_lower:
399
+ return 'medium'
400
+ elif 'large' in size_lower:
401
+ return 'large'
402
+ elif any(term in size_lower for term in ['giant', 'extra large']):
403
+ return 'giant'
404
+ else:
405
+ return 'medium' # Default
406
+
407
+ def _normalize_exercise_level(self, exercise: str) -> str:
408
+ """Normalize exercise level to standard categories"""
409
+ exercise_lower = exercise.lower()
410
+ if any(term in exercise_lower for term in ['very high', 'extreme', 'intense']):
411
+ return 'very_high'
412
+ elif 'high' in exercise_lower:
413
+ return 'high'
414
+ elif 'moderate' in exercise_lower:
415
+ return 'moderate'
416
+ elif any(term in exercise_lower for term in ['low', 'minimal']):
417
+ return 'low'
418
+ else:
419
+ return 'moderate' # Default
420
+
421
+ def _calculate_space_compatibility(self, breed_size: str, exercise_level: str, space_req: Dict[str, float]) -> float:
422
+ """Calculate dynamic space compatibility score"""
423
+ # Size-space compatibility matrix (dynamic, not hardcoded)
424
+ size_factors = {
425
+ 'toy': 0.5, 'small': 1.0, 'medium': 1.5, 'large': 2.5, 'giant': 4.0
426
+ }
427
+
428
+ exercise_factors = {
429
+ 'low': 1.0, 'moderate': 1.3, 'high': 1.8, 'very_high': 2.5
430
+ }
431
+
432
+ breed_space_need = size_factors[breed_size] * exercise_factors[exercise_level]
433
+ available_space = space_req['min_space']
434
+
435
+ # Calculate compatibility ratio
436
+ compatibility = available_space / breed_space_need
437
+
438
+ # Apply exercise penalty for high-energy breeds in small spaces
439
+ if exercise_level in ['high', 'very_high'] and available_space < 2.0:
440
+ compatibility *= (1.0 - space_req['exercise_penalty'] * 0.3)
441
+
442
+ return max(0.0, min(1.0, compatibility))
443
+
444
+ def _generate_filter_reason(self, breed_size: str, exercise_level: str, living_space: str) -> str:
445
+ """Generate dynamic filtering reason"""
446
+ if breed_size in ['giant', 'large'] and living_space == 'apartment':
447
+ return f"{breed_size.title()} breed not suitable for apartment living"
448
+ elif exercise_level in ['high', 'very_high'] and living_space in ['apartment', 'small_house']:
449
+ return f"High-energy breed needs more space than {living_space.replace('_', ' ')}"
450
+ else:
451
+ return f"Space and exercise requirements exceed {living_space.replace('_', ' ')} capacity"
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
+ # Large breeds without clear child compatibility indicators should be cautious
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"
471
+
472
+ return filtered
473
+
474
+ def filter_severe_allergies(self, candidates: Set[str],
475
+ dimensions: QueryDimensions) -> Dict[str, str]:
476
+ """Severe allergy filtering"""
477
+ filtered = {}
478
+
479
+ # High shedding breed list (should be adjusted based on actual database)
480
+ high_shedding_breeds = {
481
+ 'German_Shepherd', 'Golden_Retriever', 'Labrador_Retriever',
482
+ 'Husky', 'Akita', 'Bernese_Mountain_Dog'
483
+ }
484
+
485
+ for breed in list(candidates):
486
+ if breed in high_shedding_breeds:
487
+ filtered[breed] = "High shedding breed not suitable for allergies"
488
+
489
+ return filtered
490
+
491
+ def filter_low_activity(self, candidates: Set[str],
492
+ dimensions: QueryDimensions) -> Dict[str, str]:
493
+ """Low activity level filtering"""
494
+ filtered = {}
495
+
496
+ for breed in list(candidates):
497
+ breed_info = self.breed_cache.get(breed, {})
498
+ exercise_needs = breed_info.get('exercise_needs', '')
499
+ temperament = breed_info.get('temperament', '')
500
+
501
+ # High exercise requirement breeds
502
+ if 'high' in exercise_needs or 'very high' in exercise_needs:
503
+ filtered[breed] = "High exercise requirements unsuitable for low activity lifestyle"
504
+ # Working dogs, sporting dogs, herding dogs typically need substantial exercise
505
+ elif any(trait in temperament for trait in ['working', 'sporting', 'herding', 'energetic']):
506
+ filtered[breed] = "High-energy breed requiring substantial daily exercise"
507
+
508
+ return filtered
509
+
510
+ def filter_quiet_requirements(self, candidates: Set[str],
511
+ dimensions: QueryDimensions) -> Dict[str, str]:
512
+ """Quiet requirement filtering"""
513
+ filtered = {}
514
+
515
+ for breed in list(candidates):
516
+ breed_info = self.breed_cache.get(breed, {})
517
+ noise_level = breed_info.get('noise_level', 'moderate').lower()
518
+ temperament = breed_info.get('temperament', '')
519
+
520
+ # High noise level breeds
521
+ if 'high' in noise_level or 'loud' in noise_level:
522
+ filtered[breed] = "High noise level unsuitable for quiet requirements"
523
+ # Terriers and hounds are typically more vocal
524
+ elif ('terrier' in breed.lower() or 'hound' in breed.lower() or
525
+ 'vocal' in temperament):
526
+ filtered[breed] = "Breed group typically more vocal than desired"
527
+
528
+ return filtered
529
+
530
+ def filter_space_compatibility(self, candidates: Set[str],
531
+ dimensions: QueryDimensions) -> Dict[str, str]:
532
+ """Space compatibility filtering"""
533
+ filtered = {}
534
+
535
+ # This function provides more refined space matching
536
+ for breed in list(candidates):
537
+ breed_info = self.breed_cache.get(breed, {})
538
+ size = breed_info.get('size', '')
539
+ exercise_needs = breed_info.get('exercise_needs', '')
540
+
541
+ # If house is specified but breed is too small, may not be optimal choice (soft constraint)
542
+ if ('house' in dimensions.spatial_constraints and
543
+ 'tiny' in size and 'guard' in dimensions.special_requirements):
544
+ filtered[breed] = "Very small breed may not meet guard dog requirements for house"
545
+
546
+ return filtered
547
+
548
+ def filter_grooming_preferences(self, candidates: Set[str],
549
+ dimensions: QueryDimensions) -> Dict[str, str]:
550
+ """Grooming preference filtering"""
551
+ filtered = {}
552
+
553
+ for breed in list(candidates):
554
+ breed_info = self.breed_cache.get(breed, {})
555
+ grooming_needs = breed_info.get('grooming_needs', '')
556
+
557
+ # Low maintenance needed but breed requires high maintenance
558
+ if ('low' in dimensions.maintenance_level and
559
+ 'high' in grooming_needs):
560
+ filtered[breed] = "High grooming requirements exceed maintenance preferences"
561
+ # High maintenance preference but breed is too simple (rarely applicable)
562
+ elif ('high' in dimensions.maintenance_level and
563
+ 'low' in grooming_needs):
564
+ # Usually don't filter out, as low maintenance is always good
565
+ pass
566
+
567
+ return filtered
568
+
569
+ def filter_experience_level(self, candidates: Set[str],
570
+ dimensions: QueryDimensions) -> Dict[str, str]:
571
+ """Experience level filtering"""
572
+ filtered = {}
573
+
574
+ for breed in list(candidates):
575
+ breed_info = self.breed_cache.get(breed, {})
576
+ care_level = breed_info.get('care_level', '')
577
+ temperament = breed_info.get('temperament', '')
578
+
579
+ # Beginners not suitable for high maintenance or difficult breeds
580
+ if 'first_time' in dimensions.special_requirements:
581
+ if ('high' in care_level or 'expert' in care_level or
582
+ any(trait in temperament for trait in
583
+ ['stubborn', 'independent', 'dominant', 'challenging'])):
584
+ filtered[breed] = "High care requirements unsuitable for first-time owners"
585
+
586
+ return filtered
587
+
588
+ def filter_size_preferences(self, candidates: Set[str],
589
+ dimensions: QueryDimensions) -> Dict[str, str]:
590
+ """Size preference filtering"""
591
+ filtered = {}
592
+
593
+ # This is a soft constraint, usually won't completely exclude
594
+ size_preferences = dimensions.size_preferences
595
+
596
+ if not size_preferences:
597
+ return filtered
598
+
599
+ for breed in list(candidates):
600
+ breed_info = self.breed_cache.get(breed, {})
601
+ breed_size = breed_info.get('size', '')
602
+
603
+ # Check if matches preferences
604
+ size_match = False
605
+ for preferred_size in size_preferences:
606
+ if preferred_size in breed_size:
607
+ size_match = True
608
+ break
609
+
610
+ # Since this is a flexible constraint, usually won't filter out, only reflected in scores
611
+ # But if user is very explicit (e.g., only wants small dogs), can filter
612
+ if not size_match and len(size_preferences) == 1:
613
+ # Only filter when user has very explicit preference for single size
614
+ preferred = size_preferences[0]
615
+ if ((preferred == 'small' and 'large' in breed_size) or
616
+ (preferred == 'large' and 'small' in breed_size)):
617
+ filtered[breed] = f"Size mismatch: prefer {preferred} but breed is {breed_size}"
618
+
619
+ return filtered
620
+
621
+ def filter_exercise_mismatch(self, candidates: Set[str],
622
+ dimensions: QueryDimensions) -> Dict[str, str]:
623
+ """Filter breeds with severe exercise mismatches using dynamic thresholds"""
624
+ filtered = {}
625
+
626
+ # Extract user exercise profile dynamically
627
+ user_profile = self._extract_exercise_profile(dimensions)
628
+ compatibility_threshold = self._get_exercise_threshold(user_profile)
629
+
630
+ for breed in candidates:
631
+ breed_info = self.breed_cache.get(breed, {})
632
+ breed_exercise_level = self._normalize_exercise_level(breed_info.get('exercise_needs', 'Moderate'))
633
+
634
+ # Calculate exercise compatibility score
635
+ compatibility = self._calculate_exercise_compatibility(
636
+ user_profile, breed_exercise_level
637
+ )
638
+
639
+ # Apply threshold-based filtering
640
+ if compatibility < compatibility_threshold:
641
+ reason = self._generate_exercise_filter_reason(user_profile, breed_exercise_level)
642
+ filtered[breed] = reason
643
+
644
+ return filtered
645
+
646
+ def _extract_exercise_profile(self, dimensions: QueryDimensions) -> Dict[str, str]:
647
+ """Extract comprehensive user exercise profile"""
648
+ activity_text = ' '.join(dimensions.activity_level).lower()
649
+ spatial_text = ' '.join(dimensions.spatial_constraints).lower()
650
+
651
+ # Determine exercise level
652
+ if any(term in activity_text for term in ['don\'t exercise', 'minimal', 'low', 'light walks']):
653
+ level = 'low'
654
+ elif any(term in activity_text for term in ['hiking', 'running', 'active', 'athletic']):
655
+ level = 'high'
656
+ elif any(term in activity_text for term in ['30 minutes', 'moderate', 'balanced']):
657
+ level = 'moderate'
658
+ else:
659
+ # Infer from living space
660
+ if 'apartment' in spatial_text:
661
+ level = 'low_moderate'
662
+ else:
663
+ level = 'moderate'
664
+
665
+ # Determine time commitment
666
+ if any(term in activity_text for term in ['30 minutes', 'half hour']):
667
+ time = 'limited'
668
+ elif any(term in activity_text for term in ['hiking', 'outdoor activities']):
669
+ time = 'extensive'
670
+ else:
671
+ time = 'moderate'
672
+
673
+ return {'level': level, 'time': time}
674
+
675
+ def _get_exercise_threshold(self, user_profile: Dict[str, str]) -> float:
676
+ """Get dynamic threshold based on user profile"""
677
+ base_threshold = 0.4
678
+
679
+ # Adjust threshold based on user constraints
680
+ if user_profile['level'] == 'low':
681
+ base_threshold = 0.6 # Stricter for low-activity users
682
+ elif user_profile['level'] == 'high':
683
+ base_threshold = 0.3 # More lenient for active users
684
+
685
+ return base_threshold
686
+
687
+ def _calculate_exercise_compatibility(self, user_profile: Dict[str, str], breed_level: str) -> float:
688
+ """Calculate dynamic exercise compatibility"""
689
+ # Exercise level compatibility matrix
690
+ compatibility_matrix = {
691
+ 'low': {'low': 1.0, 'moderate': 0.7, 'high': 0.3, 'very_high': 0.1},
692
+ 'low_moderate': {'low': 0.9, 'moderate': 1.0, 'high': 0.5, 'very_high': 0.2},
693
+ 'moderate': {'low': 0.8, 'moderate': 1.0, 'high': 0.8, 'very_high': 0.4},
694
+ 'high': {'low': 0.5, 'moderate': 0.8, 'high': 1.0, 'very_high': 0.9}
695
+ }
696
+
697
+ user_level = user_profile['level']
698
+ base_compatibility = compatibility_matrix.get(user_level, {}).get(breed_level, 0.5)
699
+
700
+ # Adjust for time commitment
701
+ if user_profile['time'] == 'limited' and breed_level in ['high', 'very_high']:
702
+ base_compatibility *= 0.7
703
+ elif user_profile['time'] == 'extensive' and breed_level == 'low':
704
+ base_compatibility *= 0.8
705
+
706
+ return base_compatibility
707
+
708
+ def _generate_exercise_filter_reason(self, user_profile: Dict[str, str], breed_level: str) -> str:
709
+ """Generate dynamic exercise filtering reason"""
710
+ user_level = user_profile['level']
711
+
712
+ if user_level == 'low' and breed_level in ['high', 'very_high']:
713
+ return f"High-energy breed unsuitable for low-activity lifestyle"
714
+ elif user_level == 'high' and breed_level == 'low':
715
+ return f"Low-energy breed may not match active lifestyle requirements"
716
+ else:
717
+ return f"Exercise requirements mismatch: {user_level} user with {breed_level} breed"
718
+
719
+ def filter_size_bias(self, candidates: Set[str], dimensions: QueryDimensions) -> Dict[str, str]:
720
+ """Filter to correct size bias for moderate lifestyle users"""
721
+ filtered = {}
722
+
723
+ # Detect moderate lifestyle indicators
724
+ activity_text = ' '.join(dimensions.activity_level).lower()
725
+ is_moderate_lifestyle = any(term in activity_text for term in
726
+ ['moderate', 'balanced', '30 minutes', 'medium-sized house'])
727
+
728
+ if not is_moderate_lifestyle:
729
+ return filtered # No filtering needed
730
+
731
+ # Count size distribution in candidates
732
+ size_counts = {'toy': 0, 'small': 0, 'medium': 0, 'large': 0, 'giant': 0}
733
+ total_candidates = len(candidates)
734
+
735
+ for breed in candidates:
736
+ breed_info = self.breed_cache.get(breed, {})
737
+ breed_size = self._normalize_breed_size(breed_info.get('size', 'Medium'))
738
+ size_counts[breed_size] += 1
739
+
740
+ # Check for size bias (too many large/giant breeds)
741
+ large_giant_ratio = (size_counts['large'] + size_counts['giant']) / max(total_candidates, 1)
742
+
743
+ if large_giant_ratio > 0.6: # More than 60% large/giant breeds
744
+ # Filter some large/giant breeds to balance distribution
745
+ large_giant_filtered = 0
746
+ target_reduction = int((large_giant_ratio - 0.4) * total_candidates)
747
+
748
+ for breed in list(candidates):
749
+ if large_giant_filtered >= target_reduction:
750
+ break
751
+
752
+ breed_info = self.breed_cache.get(breed, {})
753
+ breed_size = self._normalize_breed_size(breed_info.get('size', 'Medium'))
754
+
755
+ if breed_size in ['large', 'giant']:
756
+ # Check if breed has additional compatibility issues
757
+ exercise_level = self._normalize_exercise_level(
758
+ breed_info.get('exercise_needs', 'Moderate')
759
+ )
760
+
761
+ if breed_size == 'giant' or exercise_level == 'very_high':
762
+ filtered[breed] = f"Size bias correction: {breed_size} breed less suitable for moderate lifestyle"
763
+ large_giant_filtered += 1
764
+
765
+ return filtered
766
+
767
+ def _get_emergency_candidates(self) -> Set[str]:
768
+ """Get emergency candidate breeds (safest choices)"""
769
+ safe_breeds = {
770
+ 'Labrador_Retriever', 'Golden_Retriever', 'Cavalier_King_Charles_Spaniel',
771
+ 'Bichon_Frise', 'French_Bulldog', 'Boston_Terrier', 'Pug'
772
+ }
773
+
774
+ # Only return breeds that exist in the database
775
+ available_safe_breeds = safe_breeds.intersection(set(self.breed_list))
776
+
777
+ if not available_safe_breeds:
778
+ # If even safe breeds are not available, return first few breeds
779
+ return set(self.breed_list[:5])
780
+
781
+ return available_safe_breeds
782
+
783
+ def get_constraint_summary(self, filter_result: FilterResult) -> Dict[str, Any]:
784
+ """Get constraint application summary"""
785
+ return {
786
+ 'total_breeds': len(self.breed_list),
787
+ 'passed_breeds': len(filter_result.passed_breeds),
788
+ 'filtered_breeds': len(filter_result.filtered_breeds),
789
+ 'applied_constraints': filter_result.applied_constraints,
790
+ 'relaxed_constraints': filter_result.relaxed_constraints,
791
+ 'warnings': filter_result.warnings,
792
+ 'pass_rate': len(filter_result.passed_breeds) / len(self.breed_list),
793
+ 'filter_breakdown': self._get_filter_breakdown(filter_result)
794
+ }
795
+
796
+ def _get_filter_breakdown(self, filter_result: FilterResult) -> Dict[str, int]:
797
+ """Get filtering reason breakdown"""
798
+ breakdown = {}
799
+
800
+ for breed, reason in filter_result.filtered_breeds.items():
801
+ # Simplify reason categorization
802
+ if 'apartment' in reason.lower() or 'large' in reason.lower():
803
+ category = 'Size/Space Issues'
804
+ elif 'child' in reason.lower():
805
+ category = 'Child Safety'
806
+ elif 'allerg' in reason.lower() or 'shed' in reason.lower():
807
+ category = 'Allergy Concerns'
808
+ elif 'exercise' in reason.lower() or 'activity' in reason.lower():
809
+ category = 'Exercise/Activity Mismatch'
810
+ elif 'noise' in reason.lower() or 'bark' in reason.lower():
811
+ category = 'Noise Issues'
812
+ elif 'groom' in reason.lower() or 'maintenance' in reason.lower():
813
+ category = 'Maintenance Requirements'
814
+ elif 'experience' in reason.lower() or 'first-time' in reason.lower():
815
+ category = 'Experience Level'
816
+ else:
817
+ category = 'Other'
818
+
819
+ breakdown[category] = breakdown.get(category, 0) + 1
820
+
821
+ return breakdown
822
+
823
+ def apply_breed_constraints(dimensions: QueryDimensions,
824
+ min_candidates: int = 12) -> FilterResult:
825
+ """
826
+ Convenience function: Apply breed constraint filtering
827
+
828
+ Args:
829
+ dimensions: Query dimensions
830
+ min_candidates: Minimum number of candidate breeds
831
+
832
+ Returns:
833
+ FilterResult: Filtering results
834
+ """
835
+ manager = ConstraintManager()
836
+ return manager.apply_constraints(dimensions, min_candidates)
837
+
838
+ def get_filtered_breeds(dimensions: QueryDimensions) -> Tuple[List[str], Dict[str, Any]]:
839
+ """
840
+ Convenience function: Get filtered breed list and summary
841
+
842
+ Args:
843
+ dimensions: Query dimensions
844
+
845
+ Returns:
846
+ Tuple: (Filtered breed list, filtering summary)
847
+ """
848
+ manager = ConstraintManager()
849
+ result = manager.apply_constraints(dimensions)
850
+ summary = manager.get_constraint_summary(result)
851
+
852
+ return list(result.passed_breeds), summary
dimension_score_calculator.py ADDED
@@ -0,0 +1,782 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import traceback
2
+ from typing import Dict, Any
3
+ from breed_health_info import breed_health_info
4
+ from breed_noise_info import breed_noise_info
5
+
6
+ class DimensionScoreCalculator:
7
+ """
8
+ 維度評分計算器類別
9
+ 負責計算各個維度的具體評分,包含空間、運動、美容、經驗、健康和噪音等維度
10
+ """
11
+
12
+ def __init__(self):
13
+ """初始化維度評分計算器"""
14
+ pass
15
+
16
+ def calculate_space_score(self, size: str, living_space: str, has_yard: bool, exercise_needs: str) -> float:
17
+ """
18
+ 計算空間適配性評分
19
+
20
+ 完整實現原始版本的空間計算邏輯,包含:
21
+ 1. 動態的基礎分數矩陣
22
+ 2. 強化空間品質評估
23
+ 3. 增加極端情況處理
24
+ 4. 考慮不同空間組合的協同效應
25
+ """
26
+ def get_base_score():
27
+ # 基礎分數矩陣 - 更極端的分數分配
28
+ base_matrix = {
29
+ "Small": {
30
+ "apartment": {
31
+ "no_yard": 0.85, # 小型犬在公寓仍然適合
32
+ "shared_yard": 0.90, # 共享院子提供額外活動空間
33
+ "private_yard": 0.95 # 私人院子最理想
34
+ },
35
+ "house_small": {
36
+ "no_yard": 0.80,
37
+ "shared_yard": 0.85,
38
+ "private_yard": 0.90
39
+ },
40
+ "house_large": {
41
+ "no_yard": 0.75,
42
+ "shared_yard": 0.80,
43
+ "private_yard": 0.85
44
+ }
45
+ },
46
+ "Medium": {
47
+ "apartment": {
48
+ "no_yard": 0.75,
49
+ "shared_yard": 0.85,
50
+ "private_yard": 0.90
51
+ },
52
+ "house_small": {
53
+ "no_yard": 0.80,
54
+ "shared_yard": 0.90,
55
+ "private_yard": 0.90
56
+ },
57
+ "house_large": {
58
+ "no_yard": 0.85,
59
+ "shared_yard": 0.90,
60
+ "private_yard": 0.95
61
+ }
62
+ },
63
+ "Large": {
64
+ "apartment": {
65
+ "no_yard": 0.70,
66
+ "shared_yard": 0.80,
67
+ "private_yard": 0.85
68
+ },
69
+ "house_small": {
70
+ "no_yard": 0.75,
71
+ "shared_yard": 0.85,
72
+ "private_yard": 0.90
73
+ },
74
+ "house_large": {
75
+ "no_yard": 0.85,
76
+ "shared_yard": 0.90,
77
+ "private_yard": 1.0
78
+ }
79
+ },
80
+ "Giant": {
81
+ "apartment": {
82
+ "no_yard": 0.65,
83
+ "shared_yard": 0.75,
84
+ "private_yard": 0.80
85
+ },
86
+ "house_small": {
87
+ "no_yard": 0.70,
88
+ "shared_yard": 0.80,
89
+ "private_yard": 0.85
90
+ },
91
+ "house_large": {
92
+ "no_yard": 0.80,
93
+ "shared_yard": 0.90,
94
+ "private_yard": 1.0
95
+ }
96
+ }
97
+ }
98
+
99
+ yard_type = "private_yard" if has_yard else "no_yard"
100
+ return base_matrix.get(size, base_matrix["Medium"])[living_space][yard_type]
101
+
102
+ def calculate_exercise_adjustment():
103
+ # 運動需求對空間評分的影響
104
+ exercise_impact = {
105
+ "Very High": {
106
+ "apartment": -0.10,
107
+ "house_small": -0.05,
108
+ "house_large": 0
109
+ },
110
+ "High": {
111
+ "apartment": -0.08,
112
+ "house_small": -0.05,
113
+ "house_large": 0
114
+ },
115
+ "Moderate": {
116
+ "apartment": -0.05,
117
+ "house_small": -0.02,
118
+ "house_large": 0
119
+ },
120
+ "Low": {
121
+ "apartment": 0.10,
122
+ "house_small": 0.05,
123
+ "house_large": 0
124
+ }
125
+ }
126
+
127
+ return exercise_impact.get(exercise_needs, exercise_impact["Moderate"])[living_space]
128
+
129
+ def calculate_yard_bonus():
130
+ # 院子效益評估更加細緻
131
+ if not has_yard:
132
+ return 0
133
+
134
+ yard_benefits = {
135
+ "Giant": {
136
+ "Very High": 0.25,
137
+ "High": 0.20,
138
+ "Moderate": 0.15,
139
+ "Low": 0.10
140
+ },
141
+ "Large": {
142
+ "Very High": 0.20,
143
+ "High": 0.15,
144
+ "Moderate": 0.10,
145
+ "Low": 0.05
146
+ },
147
+ "Medium": {
148
+ "Very High": 0.15,
149
+ "High": 0.10,
150
+ "Moderate": 0.08,
151
+ "Low": 0.05
152
+ },
153
+ "Small": {
154
+ "Very High": 0.10,
155
+ "High": 0.08,
156
+ "Moderate": 0.05,
157
+ "Low": 0.03
158
+ }
159
+ }
160
+
161
+ size_benefits = yard_benefits.get(size, yard_benefits["Medium"])
162
+ return size_benefits.get(exercise_needs, size_benefits["Moderate"])
163
+
164
+ def apply_extreme_case_adjustments(score):
165
+ # 處理極端情況
166
+ if size == "Giant" and living_space == "apartment":
167
+ return score * 0.85
168
+
169
+ if size == "Large" and living_space == "apartment" and exercise_needs == "Very High":
170
+ return score * 0.85
171
+
172
+ if size == "Small" and living_space == "house_large" and exercise_needs == "Low":
173
+ return score * 0.9 # 低運動需求的小型犬在大房子可能過於寬敞
174
+
175
+ return score
176
+
177
+ # 計算最終分數
178
+ base_score = get_base_score()
179
+ exercise_adj = calculate_exercise_adjustment()
180
+ yard_bonus = calculate_yard_bonus()
181
+
182
+ # 整合所有評分因素
183
+ initial_score = base_score + exercise_adj + yard_bonus
184
+
185
+ # 應用極端情況調整
186
+ final_score = apply_extreme_case_adjustments(initial_score)
187
+
188
+ # 確保分數在有效範圍內,但允許更極端的結果
189
+ return max(0.05, min(1.0, final_score))
190
+
191
+ def calculate_exercise_score(self, breed_needs: str, exercise_time: int, exercise_type: str, breed_size: str, living_space: str, breed_info: dict = None) -> float:
192
+ """
193
+ 計算品種運動需求與使用者運動條件的匹配度。此函數特別著重:
194
+ 1. 不同品種的運動耐受度差異
195
+ 2. 運動時間與類型的匹配度
196
+ 3. 極端運動量的嚴格限制
197
+
198
+ Parameters:
199
+ breed_needs: 品種的運動需求等級
200
+ exercise_time: 使用者計劃的運動時間(分鐘)
201
+ exercise_type: 運動類型(輕度/中度/高度)
202
+
203
+ Returns:
204
+ float: 0.1到1.0之間的匹配分數
205
+ """
206
+ # 定義每個運動需求等級的具體參數
207
+ exercise_levels = {
208
+ 'VERY HIGH': {
209
+ 'min': 120, # 最低需求
210
+ 'ideal': 150, # 理想運動量
211
+ 'max': 180, # 最大建議量
212
+ 'type_weights': { # 不同運動類型的權重
213
+ 'active_training': 1.0,
214
+ 'moderate_activity': 0.6,
215
+ 'light_walks': 0.3
216
+ }
217
+ },
218
+ 'HIGH': {
219
+ 'min': 90,
220
+ 'ideal': 120,
221
+ 'max': 150,
222
+ 'type_weights': {
223
+ 'active_training': 0.9,
224
+ 'moderate_activity': 0.8,
225
+ 'light_walks': 0.4
226
+ }
227
+ },
228
+ 'MODERATE': {
229
+ 'min': 45,
230
+ 'ideal': 60,
231
+ 'max': 90,
232
+ 'type_weights': {
233
+ 'active_training': 0.7,
234
+ 'moderate_activity': 1.0,
235
+ 'light_walks': 0.8
236
+ }
237
+ },
238
+ 'LOW': {
239
+ 'min': 15,
240
+ 'ideal': 30,
241
+ 'max': 45,
242
+ 'type_weights': {
243
+ 'active_training': 0.5,
244
+ 'moderate_activity': 0.8,
245
+ 'light_walks': 1.0
246
+ }
247
+ }
248
+ }
249
+
250
+ # 獲取品種的運動參數
251
+ breed_level = exercise_levels.get(breed_needs.upper(), exercise_levels['MODERATE'])
252
+
253
+ # 計算時間匹配度
254
+ def calculate_time_score():
255
+ """計算運動時間的匹配度,特別處理過度運動的情況"""
256
+ if exercise_time < breed_level['min']:
257
+ # 運動不足的嚴格懲罰
258
+ deficit_ratio = exercise_time / breed_level['min']
259
+ return max(0.1, deficit_ratio * 0.4)
260
+
261
+ elif exercise_time <= breed_level['ideal']:
262
+ # 理想範圍內的漸進提升
263
+ progress = (exercise_time - breed_level['min']) / (breed_level['ideal'] - breed_level['min'])
264
+ return 0.6 + (progress * 0.4)
265
+
266
+ elif exercise_time <= breed_level['max']:
267
+ # 理想到最大範圍的平緩下降
268
+ excess_ratio = (exercise_time - breed_level['ideal']) / (breed_level['max'] - breed_level['ideal'])
269
+ return 1.0 - (excess_ratio * 0.2)
270
+
271
+ else:
272
+ # 過度運動的顯著懲罰
273
+ excess = (exercise_time - breed_level['max']) / breed_level['max']
274
+ # 低運動需求品種的過度運動懲罰更嚴重
275
+ penalty_factor = 1.5 if breed_needs.upper() == 'LOW' else 1.0
276
+ return max(0.1, 0.8 - (excess * 0.5 * penalty_factor))
277
+
278
+ # 計算運動類型匹配度
279
+ def calculate_type_score():
280
+ """評估運動類型的適合度,考慮品種特性"""
281
+ base_type_score = breed_level['type_weights'].get(exercise_type, 0.5)
282
+
283
+ # 特殊情況處理
284
+ if breed_needs.upper() == 'LOW' and exercise_type == 'active_training':
285
+ # 低運動需求品種不適合高強度運動
286
+ base_type_score *= 0.5
287
+ elif breed_needs.upper() == 'VERY HIGH' and exercise_type == 'light_walks':
288
+ # 高運動需求品種需要更多強度
289
+ base_type_score *= 0.6
290
+
291
+ return base_type_score
292
+
293
+ # 計算最終分數
294
+ time_score = calculate_time_score()
295
+ type_score = calculate_type_score()
296
+
297
+ # 根據運動需求等級調整權重
298
+ if breed_needs.upper() == 'LOW':
299
+ # 低運動需求品種更重視運動類型的合適性
300
+ final_score = (time_score * 0.6) + (type_score * 0.4)
301
+ elif breed_needs.upper() == 'VERY HIGH':
302
+ # 高運動需求品種更重視運動時間的充足性
303
+ final_score = (time_score * 0.7) + (type_score * 0.3)
304
+ else:
305
+ final_score = (time_score * 0.65) + (type_score * 0.35)
306
+
307
+ if breed_size in ['Large', 'Giant'] and living_space == 'apartment':
308
+ if exercise_time >= 120:
309
+ final_score = min(1.0, final_score * 1.2)
310
+
311
+ # 極端情況的最終調整
312
+ if breed_needs.upper() == 'LOW' and exercise_time > breed_level['max'] * 2:
313
+ # 低運動需求品種的過度運動顯著降分
314
+ final_score *= 0.6
315
+ elif breed_needs.upper() == 'VERY HIGH' and exercise_time < breed_level['min'] * 0.5:
316
+ # 高運動需求品種運動嚴重不足降分
317
+ final_score *= 0.5
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 = {
328
+ "High": {
329
+ "low": 0.20, # 高需求對低承諾極不合適,顯著降低初始分數
330
+ "medium": 0.65, # 中等承諾仍有挑戰
331
+ "high": 1.0 # 高承諾最適合
332
+ },
333
+ "Moderate": {
334
+ "low": 0.45, # 中等需求對低承諾有困難
335
+ "medium": 0.85, # 較好的匹配
336
+ "high": 0.95 # 高承諾會有餘力
337
+ },
338
+ "Low": {
339
+ "low": 0.90, # 低需求對低承諾很合適
340
+ "medium": 0.85, # 略微降低以反映可能過度投入
341
+ "high": 0.80 # 可能造成資源浪費
342
+ }
343
+ }
344
+
345
+ # 取得基礎分數
346
+ base_score = base_scores.get(breed_needs, base_scores["Moderate"])[user_commitment]
347
+
348
+ # 根據品種大小調整美容工作量
349
+ size_adjustments = {
350
+ "Giant": {
351
+ "low": -0.20, # 大型犬的美容工作量顯著增加
352
+ "medium": -0.10,
353
+ "high": -0.05
354
+ },
355
+ "Large": {
356
+ "low": -0.15,
357
+ "medium": -0.05,
358
+ "high": 0
359
+ },
360
+ "Medium": {
361
+ "low": -0.10,
362
+ "medium": -0.05,
363
+ "high": 0
364
+ },
365
+ "Small": {
366
+ "low": -0.05,
367
+ "medium": 0,
368
+ "high": 0
369
+ }
370
+ }
371
+
372
+ # 應用體型調整
373
+ size_adjustment = size_adjustments.get(breed_size, size_adjustments["Medium"])[user_commitment]
374
+ current_score = base_score + size_adjustment
375
+
376
+ # 特殊毛髮類型的額外調整
377
+ def get_coat_adjustment(breed_description: str, commitment: str) -> float:
378
+ """評估特殊毛髮類型所需的額外維護工作"""
379
+ adjustments = 0
380
+
381
+ # 長毛品種需要更多維護
382
+ if 'long coat' in breed_description.lower():
383
+ coat_penalties = {
384
+ 'low': -0.20,
385
+ 'medium': -0.15,
386
+ 'high': -0.05
387
+ }
388
+ adjustments += coat_penalties[commitment]
389
+
390
+ # 雙層毛的品種掉毛量更大
391
+ if 'double coat' in breed_description.lower():
392
+ double_coat_penalties = {
393
+ 'low': -0.15,
394
+ 'medium': -0.10,
395
+ 'high': -0.05
396
+ }
397
+ adjustments += double_coat_penalties[commitment]
398
+
399
+ # 捲毛品種需要定期專業修剪
400
+ if 'curly' in breed_description.lower():
401
+ curly_penalties = {
402
+ 'low': -0.15,
403
+ 'medium': -0.10,
404
+ 'high': -0.05
405
+ }
406
+ adjustments += curly_penalties[commitment]
407
+
408
+ return adjustments
409
+
410
+ # 季節性考量
411
+ def get_seasonal_adjustment(breed_description: str, commitment: str) -> float:
412
+ """評估季節性掉毛對美容需求的影響"""
413
+ if 'seasonal shedding' in breed_description.lower():
414
+ seasonal_penalties = {
415
+ 'low': -0.15,
416
+ 'medium': -0.10,
417
+ 'high': -0.05
418
+ }
419
+ return seasonal_penalties[commitment]
420
+ return 0
421
+
422
+ # 專業美容需求評估
423
+ def get_professional_grooming_adjustment(breed_description: str, commitment: str) -> float:
424
+ """評估需要專業美容服務的影響"""
425
+ if 'professional grooming' in breed_description.lower():
426
+ grooming_penalties = {
427
+ 'low': -0.20,
428
+ 'medium': -0.15,
429
+ 'high': -0.05
430
+ }
431
+ return grooming_penalties[commitment]
432
+ return 0
433
+
434
+ # 應用所有額外調整
435
+ coat_adjustment = get_coat_adjustment("", user_commitment)
436
+ seasonal_adjustment = get_seasonal_adjustment("", user_commitment)
437
+ professional_adjustment = get_professional_grooming_adjustment("", user_commitment)
438
+
439
+ final_score = current_score + coat_adjustment + seasonal_adjustment + professional_adjustment
440
+
441
+ # 確保分數在有意義的範圍內,但允許更大的差異
442
+ return max(0.1, min(1.0, final_score))
443
+
444
+ def calculate_experience_score(self, care_level: str, user_experience: str, temperament: str) -> float:
445
+ """
446
+ 計算使用者經驗與品種需求的匹配分數,更平衡的經驗等級影響
447
+
448
+ 改進重點:
449
+ 1. 提高初學者的基礎分數
450
+ 2. 縮小經驗等級間的差距
451
+ 3. 保持適度的區分度
452
+ """
453
+ # 基礎分數矩陣 - 更合理的分數分配
454
+ base_scores = {
455
+ "High": {
456
+ "beginner": 0.55, # 提高起始分,讓新手也有機會
457
+ "intermediate": 0.80, # 中級玩家有不錯的勝任能力
458
+ "advanced": 0.95 # 資深者幾乎完全勝任
459
+ },
460
+ "Moderate": {
461
+ "beginner": 0.65, # 適中難度對新手更友善
462
+ "intermediate": 0.85, # 中級玩家相當適合
463
+ "advanced": 0.90 # 資深者完全勝任
464
+ },
465
+ "Low": {
466
+ "beginner": 0.85, # 新手友善品種維持高分
467
+ "intermediate": 0.90, # 中級玩家幾乎完全勝任
468
+ "advanced": 0.90 # 資深者完全勝任
469
+ }
470
+ }
471
+
472
+ # 取得基礎分數
473
+ score = base_scores.get(care_level, base_scores["Moderate"])[user_experience]
474
+
475
+ # 性格評估的權重也需要調整
476
+ temperament_lower = temperament.lower()
477
+ temperament_adjustments = 0.0
478
+
479
+ # 根據經驗等級設定不同的特徵評估標準,降低懲罰程度
480
+ if user_experience == "beginner":
481
+ difficult_traits = {
482
+ 'stubborn': -0.15, # 降低懲罰程度
483
+ 'independent': -0.12,
484
+ 'dominant': -0.12,
485
+ 'strong-willed': -0.10,
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.08,
495
+ 'eager to please': 0.10,
496
+ 'patient': 0.08,
497
+ 'adaptable': 0.08,
498
+ 'calm': 0.08
499
+ }
500
+
501
+ # 計算特徵調整
502
+ for trait, penalty in difficult_traits.items():
503
+ if trait in temperament_lower:
504
+ temperament_adjustments += penalty
505
+
506
+ for trait, bonus in easy_traits.items():
507
+ if trait in temperament_lower:
508
+ temperament_adjustments += bonus
509
+
510
+ # 品種類型特殊評估,降低懲罰程度
511
+ if 'terrier' in temperament_lower:
512
+ temperament_adjustments -= 0.10 # 降低懲罰
513
+ elif 'working' in temperament_lower:
514
+ temperament_adjustments -= 0.12
515
+ elif 'guard' in temperament_lower:
516
+ temperament_adjustments -= 0.12
517
+
518
+ elif user_experience == "intermediate":
519
+ moderate_traits = {
520
+ 'stubborn': -0.08,
521
+ 'independent': -0.05,
522
+ 'intelligent': 0.10,
523
+ 'athletic': 0.08,
524
+ 'versatile': 0.08,
525
+ 'protective': -0.05
526
+ }
527
+
528
+ for trait, adjustment in moderate_traits.items():
529
+ if trait in temperament_lower:
530
+ temperament_adjustments += adjustment
531
+
532
+ else: # advanced
533
+ advanced_traits = {
534
+ 'stubborn': 0.05,
535
+ 'independent': 0.05,
536
+ 'intelligent': 0.10,
537
+ 'protective': 0.05,
538
+ 'strong-willed': 0.05
539
+ }
540
+
541
+ for trait, bonus in advanced_traits.items():
542
+ if trait in temperament_lower:
543
+ temperament_adjustments += bonus
544
+
545
+ # 確保最終分數範圍合理
546
+ final_score = max(0.15, min(1.0, score + temperament_adjustments))
547
+
548
+ return final_score
549
+
550
+ def calculate_health_score(self, breed_name: str, health_sensitivity: str) -> float:
551
+ """
552
+ 計算品種健康分數,加強健康問題的影響力和與使用者敏感度的連結
553
+
554
+ 1. 根據使用者的健康敏感度調整分數
555
+ 2. 更嚴格的健康問題評估
556
+ 3. 考慮多重健康問題的累積效應
557
+ 4. 加入遺傳疾病的特別考量
558
+ """
559
+ try:
560
+ if breed_name not in breed_health_info:
561
+ return 0.5
562
+ except ImportError:
563
+ return 0.5
564
+
565
+ health_notes = breed_health_info[breed_name]['health_notes'].lower()
566
+
567
+ # 嚴重健康問題 - 加重扣分
568
+ severe_conditions = {
569
+ 'hip dysplasia': -0.20, # 髖關節發育不良,影響生活品質
570
+ 'heart disease': -0.15, # 心臟疾病,需要長期治療
571
+ 'progressive retinal atrophy': -0.15, # 進行性視網膜萎縮,導致失明
572
+ 'bloat': -0.18, # 胃扭轉,致命風險
573
+ 'epilepsy': -0.15, # 癲癇,需要長期藥物控制
574
+ 'degenerative myelopathy': -0.15, # 脊髓退化,影響行動能力
575
+ 'von willebrand disease': -0.12 # 血液凝固障礙
576
+ }
577
+
578
+ # 中度健康問題 - 適度扣分
579
+ moderate_conditions = {
580
+ 'allergies': -0.12, # 過敏問題,需要持續關注
581
+ 'eye problems': -0.15, # 眼睛問題,可能需要手術
582
+ 'joint problems': -0.15, # 關節問題,影響運動能力
583
+ 'hypothyroidism': -0.12, # 甲狀腺功能低下,需要藥物治療
584
+ 'ear infections': -0.10, # 耳道感染,需要定期清理
585
+ 'skin issues': -0.12 # 皮膚問題,需要特殊護理
586
+ }
587
+
588
+ # 輕微健康問題 - 輕微扣分
589
+ minor_conditions = {
590
+ 'dental issues': -0.08, # 牙齒問題,需要定期護理
591
+ 'weight gain tendency': -0.08, # 易胖體質,需要控制飲食
592
+ 'minor allergies': -0.06, # 輕微過敏,可控制
593
+ 'seasonal allergies': -0.06 # 季節性過敏
594
+ }
595
+
596
+ # 計算基礎健康分數
597
+ health_score = 1.0
598
+
599
+ # 健康問題累積效應計算
600
+ condition_counts = {
601
+ 'severe': 0,
602
+ 'moderate': 0,
603
+ 'minor': 0
604
+ }
605
+
606
+ # 計算各等級健康問題的數量和影響
607
+ for condition, penalty in severe_conditions.items():
608
+ if condition in health_notes:
609
+ health_score += penalty
610
+ condition_counts['severe'] += 1
611
+
612
+ for condition, penalty in moderate_conditions.items():
613
+ if condition in health_notes:
614
+ health_score += penalty
615
+ condition_counts['moderate'] += 1
616
+
617
+ for condition, penalty in minor_conditions.items():
618
+ if condition in health_notes:
619
+ health_score += penalty
620
+ condition_counts['minor'] += 1
621
+
622
+ # 多重問題的額外懲罰(累積效應)
623
+ if condition_counts['severe'] > 1:
624
+ health_score *= (0.85 ** (condition_counts['severe'] - 1))
625
+ if condition_counts['moderate'] > 2:
626
+ health_score *= (0.90 ** (condition_counts['moderate'] - 2))
627
+
628
+ # 根據使用者健康敏感度調整分數
629
+ sensitivity_multipliers = {
630
+ 'low': 1.1, # 較不在意健康問題
631
+ 'medium': 1.0, # 標準評估
632
+ 'high': 0.85 # 非常注重健康問題
633
+ }
634
+
635
+ health_score *= sensitivity_multipliers.get(health_sensitivity, 1.0)
636
+
637
+ # 壽命影響評估
638
+ try:
639
+ lifespan = breed_health_info[breed_name].get('average_lifespan', '10-12')
640
+ years = float(lifespan.split('-')[0])
641
+ if years < 8:
642
+ health_score *= 0.85 # 短壽命顯著降低分數
643
+ elif years < 10:
644
+ health_score *= 0.92 # 較短壽命輕微降低分數
645
+ elif years > 13:
646
+ health_score *= 1.1 # 長壽命適度加分
647
+ except:
648
+ pass
649
+
650
+ # 特殊健康優勢
651
+ if 'generally healthy' in health_notes or 'hardy breed' in health_notes:
652
+ health_score *= 1.15
653
+ elif 'robust health' in health_notes or 'few health issues' in health_notes:
654
+ health_score *= 1.1
655
+
656
+ # 確保分數在合理範圍內,但允許更大的分數差異
657
+ return max(0.1, min(1.0, health_score))
658
+
659
+ def calculate_noise_score(self, breed_name: str, noise_tolerance: str, living_space: str, has_children: bool, children_age: str) -> float:
660
+ """
661
+ 計算品種噪音分數,特別加強噪音程度與生活環境的關聯性評估,很多人棄養就是因為叫聲
662
+ """
663
+ try:
664
+ if breed_name not in breed_noise_info:
665
+ return 0.5
666
+ except ImportError:
667
+ return 0.5
668
+
669
+ noise_info = breed_noise_info[breed_name]
670
+ noise_level = noise_info['noise_level'].lower()
671
+ noise_notes = noise_info['noise_notes'].lower()
672
+
673
+ # 重新設計基礎噪音分數矩陣,考慮不同情境下的接受度
674
+ base_scores = {
675
+ 'low': {
676
+ 'low': 1.0, # 安靜的狗對低容忍完美匹配
677
+ 'medium': 0.95, # 安靜的狗對一般容忍很好
678
+ 'high': 0.90 # 安靜的狗對高容忍當然可以
679
+ },
680
+ 'medium': {
681
+ 'low': 0.60, # 一般吠叫對低容忍較困難
682
+ 'medium': 0.90, # 一般吠叫對一般容忍可接受
683
+ 'high': 0.95 # 一般吠叫對高容忍很好
684
+ },
685
+ 'high': {
686
+ 'low': 0.25, # 愛叫的狗對低容忍極不適合
687
+ 'medium': 0.65, # 愛叫的狗對一般容忍有挑戰
688
+ 'high': 0.90 # 愛叫的狗對高容忍可以接受
689
+ },
690
+ 'varies': {
691
+ 'low': 0.50, # 不確定的情況對低容忍風險較大
692
+ 'medium': 0.75, # 不確定的情況對一般容忍可嘗試
693
+ 'high': 0.85 # 不確定的情況對高容忍問題較小
694
+ }
695
+ }
696
+
697
+ # 取得基礎分數
698
+ base_score = base_scores.get(noise_level, {'low': 0.6, 'medium': 0.75, 'high': 0.85})[noise_tolerance]
699
+
700
+ # 吠叫原因評估,根據環境調整懲罰程度
701
+ barking_penalties = {
702
+ 'separation anxiety': {
703
+ 'apartment': -0.30, # 在公寓對鄰居影響更大
704
+ 'house_small': -0.25,
705
+ 'house_large': -0.20
706
+ },
707
+ 'excessive barking': {
708
+ 'apartment': -0.25,
709
+ 'house_small': -0.20,
710
+ 'house_large': -0.15
711
+ },
712
+ 'territorial': {
713
+ 'apartment': -0.20, # 在公寓更容易被觸發
714
+ 'house_small': -0.15,
715
+ 'house_large': -0.10
716
+ },
717
+ 'alert barking': {
718
+ 'apartment': -0.15, # 公寓環境刺激較多
719
+ 'house_small': -0.10,
720
+ 'house_large': -0.08
721
+ },
722
+ 'attention seeking': {
723
+ 'apartment': -0.15,
724
+ 'house_small': -0.12,
725
+ 'house_large': -0.10
726
+ }
727
+ }
728
+
729
+ # 計算環境相關的吠叫懲罰
730
+ barking_penalty = 0
731
+ for trigger, penalties in barking_penalties.items():
732
+ if trigger in noise_notes:
733
+ barking_penalty += penalties.get(living_space, -0.15)
734
+
735
+ # 特殊情況評估
736
+ special_adjustments = 0
737
+ if has_children:
738
+ # 孩童年齡相關調整
739
+ child_age_adjustments = {
740
+ 'toddler': {
741
+ 'high': -0.20, # 幼童對吵鬧更敏感
742
+ 'medium': -0.15,
743
+ 'low': -0.05
744
+ },
745
+ 'school_age': {
746
+ 'high': -0.15,
747
+ 'medium': -0.10,
748
+ 'low': -0.05
749
+ },
750
+ 'teenager': {
751
+ 'high': -0.10,
752
+ 'medium': -0.05,
753
+ 'low': -0.02
754
+ }
755
+ }
756
+
757
+ # 根據孩童年齡和噪音等級調整
758
+ age_adj = child_age_adjustments.get(children_age,
759
+ child_age_adjustments['school_age'])
760
+ special_adjustments += age_adj.get(noise_level, -0.10)
761
+
762
+ # 訓練性補償評估
763
+ trainability_bonus = 0
764
+ if 'responds well to training' in noise_notes:
765
+ trainability_bonus = 0.12
766
+ elif 'can be trained' in noise_notes:
767
+ trainability_bonus = 0.08
768
+ elif 'difficult to train' in noise_notes:
769
+ trainability_bonus = 0.02
770
+
771
+ # 夜間吠叫特別考量
772
+ if 'night barking' in noise_notes or 'howls' in noise_notes:
773
+ if living_space == 'apartment':
774
+ special_adjustments -= 0.15
775
+ elif living_space == 'house_small':
776
+ special_adjustments -= 0.10
777
+ else:
778
+ special_adjustments -= 0.05
779
+
780
+ # 計算最終分數,確保更大的分數範圍
781
+ final_score = base_score + barking_penalty + special_adjustments + trainability_bonus
782
+ return max(0.1, min(1.0, final_score))
dynamic_scoring_config.py ADDED
@@ -0,0 +1,410 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, List, Any, Optional
2
+ from dataclasses import dataclass
3
+ import json
4
+ import os
5
+
6
+ @dataclass
7
+ class DimensionConfig:
8
+ """維度配置"""
9
+ name: str
10
+ base_weight: float
11
+ priority_multiplier: Dict[str, float]
12
+ compatibility_matrix: Dict[str, Dict[str, float]]
13
+ threshold_values: Dict[str, float]
14
+ description: str
15
+
16
+
17
+ @dataclass
18
+ class ConstraintConfig:
19
+ """約束配置"""
20
+ name: str
21
+ condition_keywords: List[str]
22
+ elimination_threshold: float
23
+ penalty_factors: Dict[str, float]
24
+ exemption_conditions: List[str]
25
+ description: str
26
+
27
+
28
+ @dataclass
29
+ class ScoringProfile:
30
+ """評分配置檔"""
31
+ profile_name: str
32
+ dimensions: List[DimensionConfig]
33
+ constraints: List[ConstraintConfig]
34
+ normalization_method: str
35
+ bias_correction_rules: Dict[str, Any]
36
+ ui_preferences: Dict[str, Any]
37
+
38
+
39
+ class DynamicScoringConfig:
40
+ """動態評分配置管理器"""
41
+
42
+ def __init__(self, config_path: Optional[str] = None):
43
+ self.config_path = config_path or self._get_default_config_path()
44
+ self.current_profile = self._load_default_profile()
45
+ self.custom_profiles = {}
46
+
47
+ def _get_default_config_path(self) -> str:
48
+ """獲取默認配置路徑"""
49
+ return os.path.join(os.path.dirname(__file__), 'scoring_configs')
50
+
51
+ def _load_default_profile(self) -> ScoringProfile:
52
+ """載入預設評分配置"""
53
+ # 空間相容性維度配置
54
+ space_dimension = DimensionConfig(
55
+ name="space_compatibility",
56
+ base_weight=0.30,
57
+ priority_multiplier={
58
+ "apartment_living": 1.5,
59
+ "first_time_owner": 1.2,
60
+ "limited_space": 1.4
61
+ },
62
+ compatibility_matrix={
63
+ "apartment": {
64
+ "toy": 0.95, "small": 0.90, "medium": 0.50,
65
+ "large": 0.15, "giant": 0.05
66
+ },
67
+ "house_small": {
68
+ "toy": 0.85, "small": 0.90, "medium": 0.85,
69
+ "large": 0.60, "giant": 0.30
70
+ },
71
+ "house_medium": {
72
+ "toy": 0.80, "small": 0.85, "medium": 0.95,
73
+ "large": 0.85, "giant": 0.60
74
+ },
75
+ "house_large": {
76
+ "toy": 0.75, "small": 0.80, "medium": 0.90,
77
+ "large": 0.95, "giant": 0.95
78
+ }
79
+ },
80
+ threshold_values={
81
+ "elimination_threshold": 0.20,
82
+ "warning_threshold": 0.40,
83
+ "good_threshold": 0.70
84
+ },
85
+ description="Evaluates breed size compatibility with living space"
86
+ )
87
+
88
+ # 運動相容性維度配置
89
+ exercise_dimension = DimensionConfig(
90
+ name="exercise_compatibility",
91
+ base_weight=0.25,
92
+ priority_multiplier={
93
+ "low_activity": 1.6,
94
+ "high_activity": 1.3,
95
+ "time_limited": 1.4
96
+ },
97
+ compatibility_matrix={
98
+ "low_user": {
99
+ "low": 1.0, "moderate": 0.70, "high": 0.30, "very_high": 0.10
100
+ },
101
+ "moderate_user": {
102
+ "low": 0.80, "moderate": 1.0, "high": 0.80, "very_high": 0.50
103
+ },
104
+ "high_user": {
105
+ "low": 0.60, "moderate": 0.85, "high": 1.0, "very_high": 0.95
106
+ }
107
+ },
108
+ threshold_values={
109
+ "severe_mismatch": 0.25,
110
+ "moderate_mismatch": 0.50,
111
+ "good_match": 0.75
112
+ },
113
+ description="Matches user activity level with breed exercise needs"
114
+ )
115
+
116
+ # 噪音相容性維度配置
117
+ noise_dimension = DimensionConfig(
118
+ name="noise_compatibility",
119
+ base_weight=0.15,
120
+ priority_multiplier={
121
+ "apartment_living": 1.8,
122
+ "noise_sensitive": 2.0,
123
+ "quiet_preference": 1.5
124
+ },
125
+ compatibility_matrix={
126
+ "low_tolerance": {
127
+ "quiet": 1.0, "moderate": 0.60, "high": 0.20, "very_high": 0.05
128
+ },
129
+ "moderate_tolerance": {
130
+ "quiet": 0.90, "moderate": 1.0, "high": 0.70, "very_high": 0.40
131
+ },
132
+ "high_tolerance": {
133
+ "quiet": 0.80, "moderate": 0.90, "high": 1.0, "very_high": 0.85
134
+ }
135
+ },
136
+ threshold_values={
137
+ "unacceptable": 0.15,
138
+ "concerning": 0.40,
139
+ "acceptable": 0.70
140
+ },
141
+ description="Matches breed noise levels with user tolerance"
142
+ )
143
+
144
+ # 約束配置
145
+ apartment_constraint = ConstraintConfig(
146
+ name="apartment_size_constraint",
147
+ condition_keywords=["apartment", "small space", "studio", "condo"],
148
+ elimination_threshold=0.15,
149
+ penalty_factors={
150
+ "large_breed": 0.70,
151
+ "giant_breed": 0.85,
152
+ "high_exercise": 0.60
153
+ },
154
+ exemption_conditions=["experienced_owner", "large_apartment"],
155
+ description="Eliminates breeds unsuitable for apartment living"
156
+ )
157
+
158
+ exercise_constraint = ConstraintConfig(
159
+ name="exercise_mismatch_constraint",
160
+ condition_keywords=["don't exercise", "low activity", "minimal exercise"],
161
+ elimination_threshold=0.20,
162
+ penalty_factors={
163
+ "very_high_exercise": 0.80,
164
+ "working_breed": 0.60,
165
+ "high_energy": 0.70
166
+ },
167
+ exemption_conditions=["dog_park_access", "active_family"],
168
+ description="Prevents high-energy breeds for low-activity users"
169
+ )
170
+
171
+ # 偏見修正規則
172
+ bias_correction_rules = {
173
+ "size_bias": {
174
+ "enabled": True,
175
+ "detection_threshold": 0.70, # 70%以上大型犬觸發修正
176
+ "correction_strength": 0.60, # 修正強度
177
+ "target_distribution": {
178
+ "toy": 0.10, "small": 0.25, "medium": 0.40,
179
+ "large": 0.20, "giant": 0.05
180
+ }
181
+ },
182
+ "popularity_bias": {
183
+ "enabled": True,
184
+ "common_breeds_penalty": 0.05,
185
+ "rare_breeds_bonus": 0.03
186
+ }
187
+ }
188
+
189
+ # UI偏好設定
190
+ ui_preferences = {
191
+ "ranking_style": "gradient_badges",
192
+ "score_display": "percentage_with_bars",
193
+ "color_scheme": {
194
+ "excellent": "#22C55E",
195
+ "good": "#F59E0B",
196
+ "moderate": "#6B7280",
197
+ "poor": "#EF4444"
198
+ },
199
+ "animation_enabled": True,
200
+ "detailed_breakdown": True
201
+ }
202
+
203
+ return ScoringProfile(
204
+ profile_name="comprehensive_default",
205
+ dimensions=[space_dimension, exercise_dimension, noise_dimension],
206
+ constraints=[apartment_constraint, exercise_constraint],
207
+ normalization_method="sigmoid_compression",
208
+ bias_correction_rules=bias_correction_rules,
209
+ ui_preferences=ui_preferences
210
+ )
211
+
212
+ def get_dimension_config(self, dimension_name: str) -> Optional[DimensionConfig]:
213
+ """獲取維度配置"""
214
+ for dim in self.current_profile.dimensions:
215
+ if dim.name == dimension_name:
216
+ return dim
217
+ return None
218
+
219
+ def get_constraint_config(self, constraint_name: str) -> Optional[ConstraintConfig]:
220
+ """獲取約束配置"""
221
+ for constraint in self.current_profile.constraints:
222
+ if constraint.name == constraint_name:
223
+ return constraint
224
+ return None
225
+
226
+ def calculate_dynamic_weights(self, user_context: Dict[str, Any]) -> Dict[str, float]:
227
+ """根據用戶情境動態計算權重"""
228
+ weights = {}
229
+ total_weight = 0
230
+
231
+ for dimension in self.current_profile.dimensions:
232
+ base_weight = dimension.base_weight
233
+
234
+ # 根據用戶情境調整權重
235
+ for context_key, multiplier in dimension.priority_multiplier.items():
236
+ if user_context.get(context_key, False):
237
+ base_weight *= multiplier
238
+
239
+ weights[dimension.name] = base_weight
240
+ total_weight += base_weight
241
+
242
+ # 正規化權重
243
+ return {k: v / total_weight for k, v in weights.items()}
244
+
245
+ def get_compatibility_score(self, dimension_name: str,
246
+ user_category: str, breed_category: str) -> float:
247
+ """獲取相容性分數"""
248
+ dimension_config = self.get_dimension_config(dimension_name)
249
+ if not dimension_config:
250
+ return 0.5
251
+
252
+ matrix = dimension_config.compatibility_matrix
253
+ if user_category in matrix and breed_category in matrix[user_category]:
254
+ return matrix[user_category][breed_category]
255
+
256
+ return 0.5 # 預設值
257
+
258
+ def should_eliminate_breed(self, constraint_name: str,
259
+ breed_info: Dict[str, Any],
260
+ user_input: str) -> tuple[bool, str]:
261
+ """判斷是否應該淘汰品種"""
262
+ constraint_config = self.get_constraint_config(constraint_name)
263
+ if not constraint_config:
264
+ return False, ""
265
+
266
+ # 檢查觸發條件
267
+ user_input_lower = user_input.lower()
268
+ triggered = any(keyword in user_input_lower
269
+ for keyword in constraint_config.condition_keywords)
270
+
271
+ if not triggered:
272
+ return False, ""
273
+
274
+ # 檢查豁免條件
275
+ exempted = any(condition in user_input_lower
276
+ for condition in constraint_config.exemption_conditions)
277
+
278
+ if exempted:
279
+ return False, "Exempted due to special conditions"
280
+
281
+ # 應用淘汰邏輯(具體實現取決於約束類型)
282
+ return self._apply_elimination_logic(constraint_config, breed_info, user_input)
283
+
284
+ def _apply_elimination_logic(self, constraint_config: ConstraintConfig,
285
+ breed_info: Dict[str, Any], user_input: str) -> tuple[bool, str]:
286
+ """應用淘汰邏輯"""
287
+ # 根據約束名稱決定具體邏輯
288
+ if constraint_config.name == "apartment_size_constraint":
289
+ breed_size = breed_info.get('Size', '').lower()
290
+ if any(size in breed_size for size in ['large', 'giant']):
291
+ return True, f"Breed size ({breed_size}) unsuitable for apartment"
292
+
293
+ elif constraint_config.name == "exercise_mismatch_constraint":
294
+ exercise_needs = breed_info.get('Exercise Needs', '').lower()
295
+ if any(level in exercise_needs for level in ['very high', 'extreme']):
296
+ return True, f"Exercise needs ({exercise_needs}) exceed user capacity"
297
+
298
+ return False, ""
299
+
300
+ def get_bias_correction_settings(self) -> Dict[str, Any]:
301
+ """獲取偏見修正設定"""
302
+ return self.current_profile.bias_correction_rules
303
+
304
+ def get_ui_preferences(self) -> Dict[str, Any]:
305
+ """獲取UI偏好設定"""
306
+ return self.current_profile.ui_preferences
307
+
308
+ def save_custom_profile(self, profile: ScoringProfile, filename: str):
309
+ """保存自定義配置檔"""
310
+ if not os.path.exists(self.config_path):
311
+ os.makedirs(self.config_path)
312
+
313
+ filepath = os.path.join(self.config_path, f"{filename}.json")
314
+
315
+ # 將配置檔案轉換為JSON格式
316
+ profile_dict = {
317
+ "profile_name": profile.profile_name,
318
+ "dimensions": [self._dimension_to_dict(dim) for dim in profile.dimensions],
319
+ "constraints": [self._constraint_to_dict(cons) for cons in profile.constraints],
320
+ "normalization_method": profile.normalization_method,
321
+ "bias_correction_rules": profile.bias_correction_rules,
322
+ "ui_preferences": profile.ui_preferences
323
+ }
324
+
325
+ with open(filepath, 'w', encoding='utf-8') as f:
326
+ json.dump(profile_dict, f, indent=2, ensure_ascii=False)
327
+
328
+ def load_custom_profile(self, filename: str) -> Optional[ScoringProfile]:
329
+ """載入自定義配置檔"""
330
+ filepath = os.path.join(self.config_path, f"{filename}.json")
331
+
332
+ if not os.path.exists(filepath):
333
+ return None
334
+
335
+ try:
336
+ with open(filepath, 'r', encoding='utf-8') as f:
337
+ profile_dict = json.load(f)
338
+
339
+ return self._dict_to_profile(profile_dict)
340
+ except Exception as e:
341
+ print(f"Error loading profile {filename}: {str(e)}")
342
+ return None
343
+
344
+ def _dimension_to_dict(self, dimension: DimensionConfig) -> Dict[str, Any]:
345
+ """將維度配置轉換為字典"""
346
+ return {
347
+ "name": dimension.name,
348
+ "base_weight": dimension.base_weight,
349
+ "priority_multiplier": dimension.priority_multiplier,
350
+ "compatibility_matrix": dimension.compatibility_matrix,
351
+ "threshold_values": dimension.threshold_values,
352
+ "description": dimension.description
353
+ }
354
+
355
+ def _constraint_to_dict(self, constraint: ConstraintConfig) -> Dict[str, Any]:
356
+ """將約束配置轉換為字典"""
357
+ return {
358
+ "name": constraint.name,
359
+ "condition_keywords": constraint.condition_keywords,
360
+ "elimination_threshold": constraint.elimination_threshold,
361
+ "penalty_factors": constraint.penalty_factors,
362
+ "exemption_conditions": constraint.exemption_conditions,
363
+ "description": constraint.description
364
+ }
365
+
366
+ def _dict_to_profile(self, profile_dict: Dict[str, Any]) -> ScoringProfile:
367
+ """將字典轉換為評分配置檔"""
368
+ dimensions = [self._dict_to_dimension(dim) for dim in profile_dict["dimensions"]]
369
+ constraints = [self._dict_to_constraint(cons) for cons in profile_dict["constraints"]]
370
+
371
+ return ScoringProfile(
372
+ profile_name=profile_dict["profile_name"],
373
+ dimensions=dimensions,
374
+ constraints=constraints,
375
+ normalization_method=profile_dict["normalization_method"],
376
+ bias_correction_rules=profile_dict["bias_correction_rules"],
377
+ ui_preferences=profile_dict["ui_preferences"]
378
+ )
379
+
380
+ def _dict_to_dimension(self, dim_dict: Dict[str, Any]) -> DimensionConfig:
381
+ """將字典轉換為維度配置"""
382
+ return DimensionConfig(
383
+ name=dim_dict["name"],
384
+ base_weight=dim_dict["base_weight"],
385
+ priority_multiplier=dim_dict["priority_multiplier"],
386
+ compatibility_matrix=dim_dict["compatibility_matrix"],
387
+ threshold_values=dim_dict["threshold_values"],
388
+ description=dim_dict["description"]
389
+ )
390
+
391
+ def _dict_to_constraint(self, cons_dict: Dict[str, Any]) -> ConstraintConfig:
392
+ """將字典轉換為約束配置"""
393
+ return ConstraintConfig(
394
+ name=cons_dict["name"],
395
+ condition_keywords=cons_dict["condition_keywords"],
396
+ elimination_threshold=cons_dict["elimination_threshold"],
397
+ penalty_factors=cons_dict["penalty_factors"],
398
+ exemption_conditions=cons_dict["exemption_conditions"],
399
+ description=cons_dict["description"]
400
+ )
401
+
402
+ def get_scoring_config() -> DynamicScoringConfig:
403
+ """獲取全局評分配置"""
404
+ return scoring_config
405
+
406
+
407
+ def update_scoring_config(new_config: DynamicScoringConfig):
408
+ """更新全局評分配置"""
409
+ global scoring_config
410
+ scoring_config = new_config
multi_head_scorer.py ADDED
@@ -0,0 +1,763 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import json
3
+ from typing import Dict, List, Tuple, Optional, Any, Set
4
+ from dataclasses import dataclass, field
5
+ from abc import ABC, abstractmethod
6
+ import traceback
7
+ from sentence_transformers import SentenceTransformer
8
+ from sklearn.metrics.pairwise import cosine_similarity
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
+ from query_understanding import QueryDimensions
13
+ from constraint_manager import FilterResult
14
+
15
+ @dataclass
16
+ class DimensionalScores:
17
+ """多維度評分結果"""
18
+ semantic_scores: Dict[str, float] = field(default_factory=dict)
19
+ attribute_scores: Dict[str, float] = field(default_factory=dict)
20
+ fused_scores: Dict[str, float] = field(default_factory=dict)
21
+ bidirectional_scores: Dict[str, float] = field(default_factory=dict)
22
+ confidence_weights: Dict[str, float] = field(default_factory=dict)
23
+
24
+ @dataclass
25
+ class BreedScore:
26
+ """品種總體評分結果"""
27
+ breed_name: str
28
+ final_score: float
29
+ dimensional_breakdown: Dict[str, float] = field(default_factory=dict)
30
+ semantic_component: float = 0.0
31
+ attribute_component: float = 0.0
32
+ bidirectional_bonus: float = 0.0
33
+ confidence_score: float = 1.0
34
+ explanation: Dict[str, Any] = field(default_factory=dict)
35
+
36
+ class ScoringHead(ABC):
37
+ """抽象評分頭基類"""
38
+
39
+ @abstractmethod
40
+ def score_dimension(self, breed_info: Dict[str, Any],
41
+ dimensions: QueryDimensions,
42
+ dimension_type: str) -> float:
43
+ """為特定維度評分"""
44
+ pass
45
+
46
+ class SemanticScoringHead(ScoringHead):
47
+ """語義評分頭"""
48
+
49
+ def __init__(self, sbert_model: Optional[SentenceTransformer] = None):
50
+ self.sbert_model = sbert_model
51
+ self.dimension_embeddings = {}
52
+ if self.sbert_model:
53
+ self._build_dimension_embeddings()
54
+
55
+ def _build_dimension_embeddings(self):
56
+ """建立維度模板嵌入"""
57
+ dimension_templates = {
58
+ 'spatial_apartment': "small apartment living, limited space, no yard, urban environment",
59
+ 'spatial_house': "house with yard, outdoor space, suburban living, large property",
60
+ 'activity_low': "low energy, minimal exercise needs, calm lifestyle, indoor activities",
61
+ 'activity_moderate': "moderate exercise, daily walks, balanced activity level",
62
+ 'activity_high': "high energy, vigorous exercise, outdoor sports, active lifestyle",
63
+ 'noise_low': "quiet, rarely barks, peaceful, suitable for noise-sensitive environments",
64
+ 'noise_moderate': "moderate barking, occasional vocalizations, average noise level",
65
+ 'noise_high': "vocal, frequent barking, alert dog, comfortable with noise",
66
+ 'size_small': "small compact breed, easy to handle, portable size",
67
+ 'size_medium': "medium sized dog, balanced proportions, moderate size",
68
+ 'size_large': "large impressive dog, substantial presence, bigger breed",
69
+ 'family_children': "child-friendly, gentle with kids, family-oriented, safe around children",
70
+ 'family_elderly': "calm companion, gentle nature, suitable for seniors, low maintenance",
71
+ 'maintenance_low': "low grooming needs, minimal care requirements, easy maintenance",
72
+ 'maintenance_moderate': "regular grooming, moderate care needs, standard maintenance",
73
+ 'maintenance_high': "high grooming requirements, professional care, intensive maintenance"
74
+ }
75
+
76
+ for key, template in dimension_templates.items():
77
+ if self.sbert_model:
78
+ embedding = self.sbert_model.encode(template, convert_to_tensor=False)
79
+ self.dimension_embeddings[key] = embedding
80
+
81
+ def score_dimension(self, breed_info: Dict[str, Any],
82
+ dimensions: QueryDimensions,
83
+ dimension_type: str) -> float:
84
+ """語義維度評分"""
85
+ if not self.sbert_model or dimension_type not in self.dimension_embeddings:
86
+ return 0.5 # 預設中性分數
87
+
88
+ try:
89
+ # 建立品種描述
90
+ breed_description = self._create_breed_description(breed_info, dimension_type)
91
+
92
+ # 生成嵌入
93
+ breed_embedding = self.sbert_model.encode(breed_description, convert_to_tensor=False)
94
+ dimension_embedding = self.dimension_embeddings[dimension_type]
95
+
96
+ # 計算相似度
97
+ similarity = cosine_similarity([breed_embedding], [dimension_embedding])[0][0]
98
+
99
+ # 正規化到 0-1 範圍
100
+ normalized_score = (similarity + 1) / 2 # 從 [-1,1] 轉換到 [0,1]
101
+
102
+ return max(0.0, min(1.0, normalized_score))
103
+
104
+ except Exception as e:
105
+ print(f"Error in semantic scoring for {dimension_type}: {str(e)}")
106
+ return 0.5
107
+
108
+ def _create_breed_description(self, breed_info: Dict[str, Any],
109
+ dimension_type: str) -> str:
110
+ """為特定維度創建品種描述"""
111
+ breed_name = breed_info.get('display_name', breed_info.get('breed_name', ''))
112
+
113
+ if dimension_type.startswith('spatial_'):
114
+ size = breed_info.get('size', 'medium')
115
+ exercise = breed_info.get('exercise_needs', 'moderate')
116
+ return f"{breed_name} is a {size} dog with {exercise} exercise needs"
117
+
118
+ elif dimension_type.startswith('activity_'):
119
+ exercise = breed_info.get('exercise_needs', 'moderate')
120
+ temperament = breed_info.get('temperament', '')
121
+ return f"{breed_name} has {exercise} exercise requirements and {temperament} temperament"
122
+
123
+ elif dimension_type.startswith('noise_'):
124
+ noise_level = breed_info.get('noise_level', 'moderate')
125
+ temperament = breed_info.get('temperament', '')
126
+ return f"{breed_name} has {noise_level} noise level and {temperament} nature"
127
+
128
+ elif dimension_type.startswith('size_'):
129
+ size = breed_info.get('size', 'medium')
130
+ return f"{breed_name} is a {size} sized dog breed"
131
+
132
+ elif dimension_type.startswith('family_'):
133
+ children = breed_info.get('good_with_children', 'Yes')
134
+ temperament = breed_info.get('temperament', '')
135
+ return f"{breed_name} is {children} with children and has {temperament} temperament"
136
+
137
+ elif dimension_type.startswith('maintenance_'):
138
+ grooming = breed_info.get('grooming_needs', 'moderate')
139
+ care_level = breed_info.get('care_level', 'moderate')
140
+ return f"{breed_name} requires {grooming} grooming and {care_level} care level"
141
+
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.scoring_matrices = self._initialize_scoring_matrices()
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 dimension_type.startswith('spatial_'):
217
+ return self._score_spatial_compatibility(breed_info, dimensions)
218
+ elif dimension_type.startswith('activity_'):
219
+ return self._score_activity_compatibility(breed_info, dimensions)
220
+ elif dimension_type.startswith('noise_'):
221
+ return self._score_noise_compatibility(breed_info, dimensions)
222
+ elif dimension_type.startswith('size_'):
223
+ return self._score_size_compatibility(breed_info, dimensions)
224
+ elif dimension_type.startswith('family_'):
225
+ return self._score_family_compatibility(breed_info, dimensions)
226
+ elif dimension_type.startswith('maintenance_'):
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
+ if not dimensions.spatial_constraints:
239
+ return 0.5
240
+
241
+ breed_size = breed_info.get('size', 'medium').lower()
242
+ total_score = 0.0
243
+
244
+ for spatial_constraint in dimensions.spatial_constraints:
245
+ key = (spatial_constraint, breed_size)
246
+ score = self.scoring_matrices['spatial_scoring'].get(key, 0.5)
247
+ total_score += score
248
+
249
+ return total_score / len(dimensions.spatial_constraints)
250
+
251
+ def _score_activity_compatibility(self, breed_info: Dict[str, Any],
252
+ dimensions: QueryDimensions) -> float:
253
+ """活動相容性評分"""
254
+ if not dimensions.activity_level:
255
+ return 0.5
256
+
257
+ breed_exercise = breed_info.get('exercise_needs', 'moderate').lower()
258
+ # 清理品種運動需求字串
259
+ if 'very high' in breed_exercise:
260
+ breed_exercise = 'very high'
261
+ elif 'high' in breed_exercise:
262
+ breed_exercise = 'high'
263
+ elif 'low' in breed_exercise:
264
+ breed_exercise = 'low'
265
+ else:
266
+ breed_exercise = 'moderate'
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
+ if not dimensions.noise_preferences:
280
+ return 0.5
281
+
282
+ breed_noise = breed_info.get('noise_level', 'moderate').lower()
283
+ total_score = 0.0
284
+
285
+ for noise_pref in dimensions.noise_preferences:
286
+ key = (noise_pref, breed_noise)
287
+ score = self.scoring_matrices['noise_scoring'].get(key, 0.5)
288
+ total_score += score
289
+
290
+ return total_score / len(dimensions.noise_preferences)
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.5
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 = self.scoring_matrices['size_scoring'].get(key, 0.5)
304
+ total_score += score
305
+
306
+ return total_score / len(dimensions.size_preferences)
307
+
308
+ def _score_family_compatibility(self, breed_info: Dict[str, Any],
309
+ dimensions: QueryDimensions) -> float:
310
+ """家庭相容性評分"""
311
+ if not dimensions.family_context:
312
+ return 0.5
313
+
314
+ good_with_children = breed_info.get('good_with_children', 'Yes')
315
+ temperament = breed_info.get('temperament', '').lower()
316
+
317
+ total_score = 0.0
318
+ score_count = 0
319
+
320
+ for family_context in dimensions.family_context:
321
+ if family_context == 'children':
322
+ if good_with_children == 'Yes':
323
+ total_score += 1.0
324
+ elif good_with_children == 'No':
325
+ total_score += 0.1
326
+ else:
327
+ total_score += 0.6
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']):
334
+ total_score += 0.3
335
+ else:
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
+
343
+ return total_score / max(1, score_count)
344
+
345
+ def _score_maintenance_compatibility(self, breed_info: Dict[str, Any],
346
+ dimensions: QueryDimensions) -> float:
347
+ """維護相容性評分"""
348
+ if not dimensions.maintenance_level:
349
+ return 0.5
350
+
351
+ breed_grooming = breed_info.get('grooming_needs', 'moderate').lower()
352
+ total_score = 0.0
353
+
354
+ for maintenance_level in dimensions.maintenance_level:
355
+ key = (maintenance_level, breed_grooming)
356
+ score = self.scoring_matrices['maintenance_scoring'].get(key, 0.5)
357
+ total_score += score
358
+
359
+ return total_score / len(dimensions.maintenance_level)
360
+
361
+ class MultiHeadScorer:
362
+ """
363
+ 多頭評分系統
364
+ 結合語義和屬性評分,提供雙向相容性評估
365
+ """
366
+
367
+ def __init__(self, sbert_model: Optional[SentenceTransformer] = None):
368
+ self.sbert_model = sbert_model
369
+ self.semantic_head = SemanticScoringHead(sbert_model)
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.35, # 最高優先級:生活方式匹配
378
+ 'noise_compatibility': 0.25, # 關鍵:居住和諧
379
+ 'spatial_compatibility': 0.15, # 基本:物理約束
380
+ 'family_compatibility': 0.10, # 重要:社交相容性
381
+ 'maintenance_compatibility': 0.10, # 實際:持續護理評估
382
+ 'size_compatibility': 0.05 # 基本:偏好匹配
383
+ }
384
+
385
+ def _initialize_head_fusion_weights(self) -> Dict[str, Dict[str, float]]:
386
+ """初始化頭融合權重"""
387
+ return {
388
+ 'activity_compatibility': {'semantic': 0.4, 'attribute': 0.6},
389
+ 'noise_compatibility': {'semantic': 0.3, 'attribute': 0.7},
390
+ 'spatial_compatibility': {'semantic': 0.3, 'attribute': 0.7},
391
+ 'family_compatibility': {'semantic': 0.5, 'attribute': 0.5},
392
+ 'maintenance_compatibility': {'semantic': 0.4, 'attribute': 0.6},
393
+ 'size_compatibility': {'semantic': 0.2, 'attribute': 0.8}
394
+ }
395
+
396
+ def score_breeds(self, candidate_breeds: Set[str],
397
+ dimensions: QueryDimensions) -> List[BreedScore]:
398
+ """
399
+ 為候選品種評分
400
+
401
+ Args:
402
+ candidate_breeds: 通過約束篩選的候選品種
403
+ dimensions: 查詢維度
404
+
405
+ Returns:
406
+ List[BreedScore]: 品種評分結果列表
407
+ """
408
+ try:
409
+ breed_scores = []
410
+
411
+ # 為每個品種計算分數
412
+ for breed in candidate_breeds:
413
+ breed_info = self._get_breed_info(breed)
414
+ score_result = self._score_single_breed(breed_info, dimensions)
415
+ breed_scores.append(score_result)
416
+
417
+ # 按最終分數排序
418
+ breed_scores.sort(key=lambda x: x.final_score, reverse=True)
419
+
420
+ return breed_scores
421
+
422
+ except Exception as e:
423
+ print(f"Error scoring breeds: {str(e)}")
424
+ print(traceback.format_exc())
425
+ return []
426
+
427
+ def _get_breed_info(self, breed: str) -> Dict[str, Any]:
428
+ """獲取品種資訊"""
429
+ try:
430
+ # 基本品種資訊
431
+ breed_info = get_dog_description(breed) or {}
432
+
433
+ # 健康資訊
434
+ health_info = breed_health_info.get(breed, {})
435
+
436
+ # 噪音資訊
437
+ noise_info = breed_noise_info.get(breed, {})
438
+
439
+ # 整合資訊
440
+ return {
441
+ 'breed_name': breed,
442
+ 'display_name': breed.replace('_', ' '),
443
+ 'size': breed_info.get('Size', '').lower(),
444
+ 'exercise_needs': breed_info.get('Exercise Needs', '').lower(),
445
+ 'grooming_needs': breed_info.get('Grooming Needs', '').lower(),
446
+ 'temperament': breed_info.get('Temperament', '').lower(),
447
+ 'good_with_children': breed_info.get('Good with Children', 'Yes'),
448
+ 'care_level': breed_info.get('Care Level', '').lower(),
449
+ 'lifespan': breed_info.get('Lifespan', '10-12 years'),
450
+ 'noise_level': noise_info.get('noise_level', 'moderate').lower(),
451
+ 'description': breed_info.get('Description', ''),
452
+ 'raw_breed_info': breed_info,
453
+ 'raw_health_info': health_info,
454
+ 'raw_noise_info': noise_info
455
+ }
456
+ except Exception as e:
457
+ print(f"Error getting breed info for {breed}: {str(e)}")
458
+ return {
459
+ 'breed_name': breed,
460
+ 'display_name': breed.replace('_', ' ')
461
+ }
462
+
463
+ def _score_single_breed(self, breed_info: Dict[str, Any],
464
+ dimensions: QueryDimensions) -> BreedScore:
465
+ """為單一品種評分"""
466
+ try:
467
+ dimensional_scores = {}
468
+ semantic_total = 0.0
469
+ attribute_total = 0.0
470
+
471
+ # 動態權重分配(基於用戶表達的維度)
472
+ active_dimensions = self._get_active_dimensions(dimensions)
473
+ adjusted_weights = self._adjust_dimension_weights(active_dimensions)
474
+
475
+ # 為每個活躍維度評分
476
+ for dimension, weight in adjusted_weights.items():
477
+ # 語義評分
478
+ semantic_score = self.semantic_head.score_dimension(
479
+ breed_info, dimensions, dimension
480
+ )
481
+
482
+ # 屬性評分
483
+ attribute_score = self.attribute_head.score_dimension(
484
+ breed_info, dimensions, dimension
485
+ )
486
+
487
+ # 頭融合
488
+ fusion_weights = self.head_fusion_weights.get(
489
+ dimension, {'semantic': 0.5, 'attribute': 0.5}
490
+ )
491
+
492
+ fused_score = (semantic_score * fusion_weights['semantic'] +
493
+ attribute_score * fusion_weights['attribute'])
494
+
495
+ dimensional_scores[dimension] = fused_score
496
+ semantic_total += semantic_score * weight
497
+ attribute_total += attribute_score * weight
498
+
499
+ # 雙向相容性評估
500
+ bidirectional_bonus = self._calculate_bidirectional_bonus(
501
+ breed_info, dimensions
502
+ )
503
+
504
+ # Apply size bias correction
505
+ bias_correction = self._calculate_size_bias_correction(breed_info, dimensions)
506
+
507
+ # 計算最終分數
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)
516
+
517
+ return BreedScore(
518
+ breed_name=breed_info.get('display_name', breed_info['breed_name']),
519
+ final_score=final_score,
520
+ dimensional_breakdown=dimensional_scores,
521
+ semantic_component=semantic_total,
522
+ attribute_component=attribute_total,
523
+ bidirectional_bonus=bidirectional_bonus,
524
+ confidence_score=confidence_score,
525
+ explanation=self._generate_explanation(breed_info, dimensions, dimensional_scores)
526
+ )
527
+
528
+ except Exception as e:
529
+ print(f"Error scoring breed {breed_info.get('breed_name', 'unknown')}: {str(e)}")
530
+ return BreedScore(
531
+ breed_name=breed_info.get('display_name', breed_info.get('breed_name', 'Unknown')),
532
+ final_score=0.5,
533
+ confidence_score=0.0
534
+ )
535
+
536
+ def _get_active_dimensions(self, dimensions: QueryDimensions) -> Set[str]:
537
+ """獲取活躍的維度"""
538
+ active = set()
539
+
540
+ if dimensions.spatial_constraints:
541
+ active.add('spatial_compatibility')
542
+ if dimensions.activity_level:
543
+ active.add('activity_compatibility')
544
+ if dimensions.noise_preferences:
545
+ active.add('noise_compatibility')
546
+ if dimensions.size_preferences:
547
+ active.add('size_compatibility')
548
+ if dimensions.family_context:
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:
558
+ return self.dimension_weights
559
+
560
+ # 只為活躍維度分配權重
561
+ active_weights = {dim: weight for dim, weight in self.dimension_weights.items()
562
+ if dim in active_dimensions}
563
+
564
+ # 正規化權重總和為 1.0
565
+ total_weight = sum(active_weights.values())
566
+ if total_weight > 0:
567
+ active_weights = {dim: weight / total_weight
568
+ for dim, weight in active_weights.items()}
569
+
570
+ return active_weights
571
+
572
+ def _calculate_bidirectional_bonus(self, breed_info: Dict[str, Any],
573
+ dimensions: QueryDimensions) -> float:
574
+ """計算雙向相容性獎勵"""
575
+ try:
576
+ bonus = 0.0
577
+
578
+ # 正向相容性:品種滿足用戶需求
579
+ forward_compatibility = self._assess_forward_compatibility(breed_info, dimensions)
580
+
581
+ # 反向相容性:用戶生活方式適合品種需求
582
+ reverse_compatibility = self._assess_reverse_compatibility(breed_info, dimensions)
583
+
584
+ # 雙向獎勵(較為保守)
585
+ bonus = min(0.1, (forward_compatibility + reverse_compatibility) * 0.05)
586
+
587
+ return bonus
588
+
589
+ except Exception as e:
590
+ print(f"Error calculating bidirectional bonus: {str(e)}")
591
+ return 0.0
592
+
593
+ def _assess_forward_compatibility(self, breed_info: Dict[str, Any],
594
+ dimensions: QueryDimensions) -> float:
595
+ """評估正向相容性"""
596
+ compatibility = 0.0
597
+
598
+ # 空間需求匹配
599
+ if 'apartment' in dimensions.spatial_constraints:
600
+ size = breed_info.get('size', '')
601
+ if 'small' in size:
602
+ compatibility += 0.3
603
+ elif 'medium' in size:
604
+ compatibility += 0.1
605
+
606
+ # 活動需求匹配
607
+ if 'low' in dimensions.activity_level:
608
+ exercise = breed_info.get('exercise_needs', '')
609
+ if 'low' in exercise:
610
+ compatibility += 0.3
611
+ elif 'moderate' in exercise:
612
+ compatibility += 0.1
613
+
614
+ return compatibility
615
+
616
+ def _assess_reverse_compatibility(self, breed_info: Dict[str, Any],
617
+ dimensions: QueryDimensions) -> float:
618
+ """評估反向相容性"""
619
+ compatibility = 0.0
620
+
621
+ # 品種是否能在用戶環境中茁壯成長
622
+ exercise_needs = breed_info.get('exercise_needs', '')
623
+
624
+ if 'high' in exercise_needs:
625
+ # 高運動需求品種需要確認用戶能提供足夠運動
626
+ if ('high' in dimensions.activity_level or
627
+ 'house' in dimensions.spatial_constraints):
628
+ compatibility += 0.2
629
+ else:
630
+ compatibility -= 0.2
631
+
632
+ # 品種護理需求是否與用戶能力匹配
633
+ grooming_needs = breed_info.get('grooming_needs', '')
634
+ if 'high' in grooming_needs:
635
+ if 'high' in dimensions.maintenance_level:
636
+ compatibility += 0.1
637
+ elif 'low' in dimensions.maintenance_level:
638
+ compatibility -= 0.1
639
+
640
+ return compatibility
641
+
642
+ def _calculate_size_bias_correction(self, breed_info: Dict,
643
+ dimensions: QueryDimensions) -> float:
644
+ """Correct systematic bias toward larger breeds"""
645
+ breed_size = breed_info.get('size', '').lower()
646
+
647
+ # Default no bias correction
648
+ correction = 0.0
649
+
650
+ # Detect if user specified moderate/balanced preferences
651
+ if any(term in dimensions.activity_level for term in ['moderate', 'balanced', 'average']):
652
+ # Penalize extremes
653
+ if breed_size in ['giant', 'toy']:
654
+ correction = -0.1
655
+ elif breed_size in ['large']:
656
+ correction = -0.05
657
+
658
+ # Boost medium breeds for moderate requirements
659
+ if 'medium' in breed_size and 'balanced' in str(dimensions.activity_level):
660
+ correction = 0.1
661
+
662
+ return correction
663
+
664
+ def _calculate_confidence(self, dimensions: QueryDimensions) -> float:
665
+ """計算推薦信心度"""
666
+ # 基於維度覆蓋率和信心分數計算
667
+ dimension_count = sum([
668
+ len(dimensions.spatial_constraints),
669
+ len(dimensions.activity_level),
670
+ len(dimensions.noise_preferences),
671
+ len(dimensions.size_preferences),
672
+ len(dimensions.family_context),
673
+ len(dimensions.maintenance_level),
674
+ len(dimensions.special_requirements)
675
+ ])
676
+
677
+ # 基礎信心度
678
+ base_confidence = min(1.0, dimension_count * 0.15)
679
+
680
+ # 品種提及獎勵
681
+ breed_bonus = min(0.2, len(dimensions.breed_mentions) * 0.1)
682
+
683
+ # 整體信心分數
684
+ overall_confidence = dimensions.confidence_scores.get('overall', 0.5)
685
+
686
+ return min(1.0, base_confidence + breed_bonus + overall_confidence * 0.3)
687
+
688
+ def _generate_explanation(self, breed_info: Dict[str, Any],
689
+ dimensions: QueryDimensions,
690
+ dimensional_scores: Dict[str, float]) -> Dict[str, Any]:
691
+ """生成評分解釋"""
692
+ try:
693
+ explanation = {
694
+ 'strengths': [],
695
+ 'considerations': [],
696
+ 'match_highlights': [],
697
+ 'score_breakdown': dimensional_scores
698
+ }
699
+
700
+ breed_name = breed_info.get('display_name', '')
701
+
702
+ # 分析各維度表現
703
+ for dimension, score in dimensional_scores.items():
704
+ if score >= 0.8:
705
+ explanation['strengths'].append(self._get_strength_text(dimension, breed_info))
706
+ elif score <= 0.3:
707
+ explanation['considerations'].append(self._get_consideration_text(dimension, breed_info))
708
+ else:
709
+ explanation['match_highlights'].append(f"{dimension}: {score:.2f}")
710
+
711
+ return explanation
712
+
713
+ except Exception as e:
714
+ print(f"Error generating explanation: {str(e)}")
715
+ return {'strengths': [], 'considerations': [], 'match_highlights': []}
716
+
717
+ def _get_strength_text(self, dimension: str, breed_info: Dict[str, Any]) -> str:
718
+ """Get strength description"""
719
+ breed_name = breed_info.get('display_name', '')
720
+
721
+ if dimension == 'activity_compatibility':
722
+ return f"{breed_name} has an activity level that matches your lifestyle very well"
723
+ elif dimension == 'noise_compatibility':
724
+ return f"{breed_name} has noise characteristics that fit your environment"
725
+ elif dimension == 'spatial_compatibility':
726
+ return f"{breed_name} is very suitable for your living space"
727
+ elif dimension == 'family_compatibility':
728
+ return f"{breed_name} performs well in a family environment"
729
+ elif dimension == 'maintenance_compatibility':
730
+ return f"{breed_name} has grooming needs that match your willingness to commit"
731
+ else:
732
+ return f"{breed_name} shows strong performance in {dimension}"
733
+
734
+ def _get_consideration_text(self, dimension: str, breed_info: Dict[str, Any]) -> str:
735
+ """Get consideration description"""
736
+ breed_name = breed_info.get('display_name', '')
737
+
738
+ if dimension == 'activity_compatibility':
739
+ return f"{breed_name} may have exercise needs that differ from your lifestyle"
740
+ elif dimension == 'noise_compatibility':
741
+ return f"{breed_name} has noise characteristics that require special consideration"
742
+ elif dimension == 'maintenance_compatibility':
743
+ return f"{breed_name} has relatively high grooming requirements"
744
+ else:
745
+ return f"{breed_name} requires extra consideration in {dimension}"
746
+
747
+
748
+ def score_breed_candidates(candidate_breeds: Set[str],
749
+ dimensions: QueryDimensions,
750
+ sbert_model: Optional[SentenceTransformer] = None) -> List[BreedScore]:
751
+ """
752
+ 便利函數:為候選品種評分
753
+
754
+ Args:
755
+ candidate_breeds: 候選品種集合
756
+ dimensions: 查詢維度
757
+ sbert_model: 可選的SBERT模型
758
+
759
+ Returns:
760
+ List[BreedScore]: 評分結果列表
761
+ """
762
+ scorer = MultiHeadScorer(sbert_model)
763
+ return scorer.score_breeds(candidate_breeds, dimensions)
natural_language_processor.py ADDED
@@ -0,0 +1,488 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import string
3
+ from typing import Dict, List, Tuple, Optional, Any
4
+ import traceback
5
+
6
+ class NaturalLanguageProcessor:
7
+ """
8
+ Natural language processing utility class
9
+ Handles text preprocessing and keyword extraction for user input
10
+ """
11
+
12
+ def __init__(self):
13
+ """Initialize the natural language processor"""
14
+ self.stop_words = {
15
+ 'a', 'an', 'and', 'are', 'as', 'at', 'be', 'by', 'for', 'from',
16
+ 'has', 'he', 'in', 'is', 'it', 'its', 'of', 'on', 'that', 'the',
17
+ 'to', 'was', 'will', 'with', 'would', 'i', 'me', 'my', 'we', 'us',
18
+ 'our', 'you', 'your', 'they', 'them', 'their'
19
+ }
20
+
21
+ # Breed name mappings (common aliases to standard names)
22
+ self.breed_aliases = {
23
+ 'lab': 'labrador_retriever',
24
+ 'labrador': 'labrador_retriever',
25
+ 'golden': 'golden_retriever',
26
+ 'retriever': ['labrador_retriever', 'golden_retriever'],
27
+ 'german shepherd': 'german_shepherd',
28
+ 'shepherd': 'german_shepherd',
29
+ 'border collie': 'border_collie',
30
+ 'collie': ['border_collie', 'collie'],
31
+ 'bulldog': ['french_bulldog', 'english_bulldog'],
32
+ 'french bulldog': 'french_bulldog',
33
+ 'poodle': ['standard_poodle', 'miniature_poodle', 'toy_poodle'],
34
+ 'husky': 'siberian_husky',
35
+ 'siberian husky': 'siberian_husky',
36
+ 'beagle': 'beagle',
37
+ 'yorkshire terrier': 'yorkshire_terrier',
38
+ 'yorkie': 'yorkshire_terrier',
39
+ 'chihuahua': 'chihuahua',
40
+ 'dachshund': 'dachshund',
41
+ 'wiener dog': 'dachshund',
42
+ 'rottweiler': 'rottweiler',
43
+ 'rottie': 'rottweiler',
44
+ 'boxer': 'boxer',
45
+ 'great dane': 'great_dane',
46
+ 'dane': 'great_dane',
47
+ 'mastiff': ['bull_mastiff', 'tibetan_mastiff'],
48
+ 'pitbull': 'american_staffordshire_terrier',
49
+ 'pit bull': 'american_staffordshire_terrier',
50
+ 'shih tzu': 'shih-tzu',
51
+ 'maltese': 'maltese_dog',
52
+ 'pug': 'pug',
53
+ 'basset hound': 'basset',
54
+ 'bloodhound': 'bloodhound',
55
+ 'australian shepherd': 'kelpie',
56
+ 'aussie': 'kelpie'
57
+ }
58
+
59
+ # Lifestyle keyword mappings
60
+ self.lifestyle_keywords = {
61
+ 'living_space': {
62
+ 'apartment': ['apartment', 'flat', 'condo', 'small space', 'city living', 'urban'],
63
+ 'house': ['house', 'home', 'yard', 'garden', 'suburban', 'large space'],
64
+ 'farm': ['farm', 'rural', 'country', 'acreage', 'ranch']
65
+ },
66
+ 'activity_level': {
67
+ 'very_high': ['very active', 'extremely energetic', 'marathon runner', 'athlete'],
68
+ 'high': ['active', 'energetic', 'exercise', 'hiking', 'running', 'outdoor activities',
69
+ 'sports', 'jogging', 'biking', 'adventure'],
70
+ 'moderate': ['moderate exercise', 'some activity', 'weekend walks', 'occasional exercise'],
71
+ 'low': ['calm', 'lazy', 'indoor', 'low energy', 'couch potato', 'sedentary', 'quiet lifestyle']
72
+ },
73
+ 'family_situation': {
74
+ 'children': ['children', 'kids', 'toddlers', 'babies', 'family with children', 'young family'],
75
+ 'elderly': ['elderly', 'senior', 'old', 'retired', 'senior citizen'],
76
+ 'single': ['single', 'alone', 'individual', 'bachelor', 'solo'],
77
+ 'couple': ['couple', 'two people', 'pair', 'duo']
78
+ },
79
+ 'noise_tolerance': {
80
+ 'low': ['quiet', 'silent', 'noise-sensitive', 'peaceful', 'no barking', 'minimal noise'],
81
+ 'moderate': ['some noise ok', 'moderate barking', 'normal noise'],
82
+ 'high': ['loud ok', 'barking fine', 'noise tolerant', 'doesn\'t mind noise']
83
+ },
84
+ 'size_preference': {
85
+ 'small': ['small', 'tiny', 'little', 'compact', 'lap dog', 'petite', 'miniature'],
86
+ 'medium': ['medium', 'moderate size', 'average', 'mid-size'],
87
+ 'large': ['large', 'big', 'huge', 'giant', 'massive', 'substantial'],
88
+ 'varies': ['any size', 'size doesn\'t matter', 'flexible on size']
89
+ },
90
+ 'experience_level': {
91
+ 'beginner': ['first time', 'beginner', 'new to dogs', 'inexperienced', 'never had'],
92
+ 'some': ['some experience', 'had dogs before', 'moderate experience'],
93
+ 'experienced': ['experienced', 'expert', 'very experienced', 'professional', 'trainer']
94
+ },
95
+ 'grooming_commitment': {
96
+ 'low': ['low maintenance', 'easy care', 'minimal grooming', 'wash and go'],
97
+ 'moderate': ['moderate grooming', 'some brushing', 'regular care'],
98
+ 'high': ['high maintenance', 'lots of grooming', 'professional grooming', 'daily brushing']
99
+ },
100
+ 'special_needs': {
101
+ 'guard': ['guard dog', 'protection', 'security', 'watchdog', 'guardian'],
102
+ 'therapy': ['therapy dog', 'emotional support', 'comfort', 'calm companion'],
103
+ 'hypoallergenic': ['hypoallergenic', 'allergies', 'non-shedding', 'allergy friendly'],
104
+ 'working': ['working dog', 'job', 'task', 'service dog'],
105
+ 'companion': ['companion', 'friend', 'buddy', 'lap dog', 'cuddle']
106
+ }
107
+ }
108
+
109
+ # Comparative preference keywords
110
+ self.preference_indicators = {
111
+ 'love': 1.0,
112
+ 'prefer': 0.9,
113
+ 'like': 0.8,
114
+ 'want': 0.8,
115
+ 'interested in': 0.7,
116
+ 'considering': 0.6,
117
+ 'ok with': 0.5,
118
+ 'don\'t mind': 0.4,
119
+ 'not interested': 0.2,
120
+ 'dislike': 0.1,
121
+ 'hate': 0.0
122
+ }
123
+
124
+ # Order keywords
125
+ self.order_keywords = {
126
+ 'first': 1.0, 'most': 1.0, 'primary': 1.0, 'main': 1.0,
127
+ 'second': 0.8, 'then': 0.8, 'next': 0.8,
128
+ 'third': 0.6, 'also': 0.6, 'additionally': 0.6,
129
+ 'last': 0.4, 'least': 0.4, 'finally': 0.4
130
+ }
131
+
132
+ def preprocess_text(self, text: str) -> str:
133
+ """
134
+ Text preprocessing
135
+
136
+ Args:
137
+ text: Raw text
138
+
139
+ Returns:
140
+ Preprocessed text
141
+ """
142
+ if not text:
143
+ return ""
144
+
145
+ # Convert to lowercase
146
+ text = text.lower().strip()
147
+
148
+ # Remove punctuation (keep some meaningful ones)
149
+ text = re.sub(r'[^\w\s\-\']', ' ', text)
150
+
151
+ # Handle extra whitespace
152
+ text = re.sub(r'\s+', ' ', text)
153
+
154
+ return text
155
+
156
+ def extract_breed_mentions(self, text: str) -> List[Tuple[str, float]]:
157
+ """
158
+ Extract mentioned breeds and their preference levels from text
159
+
160
+ Args:
161
+ text: Input text
162
+
163
+ Returns:
164
+ List of (breed_name, preference_score) tuples
165
+ """
166
+ text = self.preprocess_text(text)
167
+ breed_mentions = []
168
+
169
+ try:
170
+ # Check each breed alias
171
+ for alias, standard_breed in self.breed_aliases.items():
172
+ if alias in text:
173
+ # Find surrounding preference indicators
174
+ preference_score = self._find_preference_score(text, alias)
175
+
176
+ if isinstance(standard_breed, list):
177
+ # If alias maps to multiple breeds, add all
178
+ for breed in standard_breed:
179
+ breed_mentions.append((breed, preference_score))
180
+ else:
181
+ breed_mentions.append((standard_breed, preference_score))
182
+
183
+ # Deduplicate and merge scores
184
+ breed_scores = {}
185
+ for breed, score in breed_mentions:
186
+ if breed in breed_scores:
187
+ breed_scores[breed] = max(breed_scores[breed], score)
188
+ else:
189
+ breed_scores[breed] = score
190
+
191
+ return list(breed_scores.items())
192
+
193
+ except Exception as e:
194
+ print(f"Error extracting breed mentions: {str(e)}")
195
+ return []
196
+
197
+ def _find_preference_score(self, text: str, breed_mention: str) -> float:
198
+ """
199
+ Find preference score near breed mention
200
+
201
+ Args:
202
+ text: Text
203
+ breed_mention: Breed mention
204
+
205
+ Returns:
206
+ Preference score (0.0-1.0)
207
+ """
208
+ try:
209
+ # Find breed mention position
210
+ mention_pos = text.find(breed_mention)
211
+ if mention_pos == -1:
212
+ return 0.5 # Default neutral score
213
+
214
+ # Check context (50 characters before and after)
215
+ context_start = max(0, mention_pos - 50)
216
+ context_end = min(len(text), mention_pos + len(breed_mention) + 50)
217
+ context = text[context_start:context_end]
218
+
219
+ # Find preference indicators
220
+ max_score = 0.5 # Default score
221
+
222
+ for indicator, score in self.preference_indicators.items():
223
+ if indicator in context:
224
+ max_score = max(max_score, score)
225
+
226
+ # Find order keywords
227
+ for order_word, multiplier in self.order_keywords.items():
228
+ if order_word in context:
229
+ max_score = max(max_score, max_score * multiplier)
230
+
231
+ return max_score
232
+
233
+ except Exception as e:
234
+ print(f"Error finding preference score: {str(e)}")
235
+ return 0.5
236
+
237
+ def extract_lifestyle_preferences(self, text: str) -> Dict[str, Dict[str, float]]:
238
+ """
239
+ Extract lifestyle preferences from text
240
+
241
+ Args:
242
+ text: Input text
243
+
244
+ Returns:
245
+ Lifestyle preferences dictionary
246
+ """
247
+ text = self.preprocess_text(text)
248
+ preferences = {}
249
+
250
+ try:
251
+ for category, keywords_dict in self.lifestyle_keywords.items():
252
+ preferences[category] = {}
253
+
254
+ for preference_type, keywords in keywords_dict.items():
255
+ score = 0.0
256
+ count = 0
257
+
258
+ for keyword in keywords:
259
+ if keyword in text:
260
+ # Calculate keyword occurrence intensity
261
+ keyword_count = text.count(keyword)
262
+ score += keyword_count
263
+ count += keyword_count
264
+
265
+ if count > 0:
266
+ # Normalize score
267
+ preferences[category][preference_type] = min(score / max(count, 1), 1.0)
268
+
269
+ return preferences
270
+
271
+ except Exception as e:
272
+ print(f"Error extracting lifestyle preferences: {str(e)}")
273
+ return {}
274
+
275
+ def generate_search_keywords(self, text: str) -> List[str]:
276
+ """
277
+ Generate keyword list for search
278
+
279
+ Args:
280
+ text: Input text
281
+
282
+ Returns:
283
+ List of keywords
284
+ """
285
+ text = self.preprocess_text(text)
286
+ keywords = []
287
+
288
+ try:
289
+ # Tokenize and filter stop words
290
+ words = text.split()
291
+ for word in words:
292
+ if len(word) > 2 and word not in self.stop_words:
293
+ keywords.append(word)
294
+
295
+ # Extract important phrases
296
+ phrases = self._extract_phrases(text)
297
+ keywords.extend(phrases)
298
+
299
+ # Remove duplicates
300
+ keywords = list(set(keywords))
301
+
302
+ return keywords
303
+
304
+ except Exception as e:
305
+ print(f"Error generating search keywords: {str(e)}")
306
+ return []
307
+
308
+ def _extract_phrases(self, text: str) -> List[str]:
309
+ """
310
+ Extract important phrases
311
+
312
+ Args:
313
+ text: Input text
314
+
315
+ Returns:
316
+ List of phrases
317
+ """
318
+ phrases = []
319
+
320
+ # Define important phrase patterns
321
+ phrase_patterns = [
322
+ r'good with \w+',
323
+ r'apartment \w+',
324
+ r'family \w+',
325
+ r'exercise \w+',
326
+ r'grooming \w+',
327
+ r'noise \w+',
328
+ r'training \w+',
329
+ r'health \w+',
330
+ r'\w+ friendly',
331
+ r'\w+ tolerant',
332
+ r'\w+ maintenance',
333
+ r'\w+ energy',
334
+ r'\w+ barking',
335
+ r'\w+ shedding'
336
+ ]
337
+
338
+ for pattern in phrase_patterns:
339
+ matches = re.findall(pattern, text)
340
+ phrases.extend(matches)
341
+
342
+ return phrases
343
+
344
+ def analyze_sentiment(self, text: str) -> Dict[str, float]:
345
+ """
346
+ Analyze text sentiment
347
+
348
+ Args:
349
+ text: Input text
350
+
351
+ Returns:
352
+ Sentiment analysis results {'positive': 0.0-1.0, 'negative': 0.0-1.0, 'neutral': 0.0-1.0}
353
+ """
354
+ text = self.preprocess_text(text)
355
+
356
+ positive_words = [
357
+ 'love', 'like', 'want', 'prefer', 'good', 'great', 'excellent',
358
+ 'perfect', 'ideal', 'wonderful', 'amazing', 'fantastic'
359
+ ]
360
+
361
+ negative_words = [
362
+ 'hate', 'dislike', 'bad', 'terrible', 'awful', 'horrible',
363
+ 'not good', 'don\'t want', 'avoid', 'against', 'problem'
364
+ ]
365
+
366
+ positive_count = sum(1 for word in positive_words if word in text)
367
+ negative_count = sum(1 for word in negative_words if word in text)
368
+ total_words = len(text.split())
369
+
370
+ if total_words == 0:
371
+ return {'positive': 0.0, 'negative': 0.0, 'neutral': 1.0}
372
+
373
+ positive_ratio = positive_count / total_words
374
+ negative_ratio = negative_count / total_words
375
+ neutral_ratio = 1.0 - positive_ratio - negative_ratio
376
+
377
+ return {
378
+ 'positive': positive_ratio,
379
+ 'negative': negative_ratio,
380
+ 'neutral': max(0.0, neutral_ratio)
381
+ }
382
+
383
+ def extract_implicit_preferences(self, text: str) -> Dict[str, Any]:
384
+ """
385
+ Extract implicit preferences from text
386
+
387
+ Args:
388
+ text: Input text
389
+
390
+ Returns:
391
+ Dictionary of implicit preferences
392
+ """
393
+ text = self.preprocess_text(text)
394
+ implicit_prefs = {}
395
+
396
+ try:
397
+ # Infer preferences from mentioned activities
398
+ if any(activity in text for activity in ['hiking', 'running', 'jogging', 'outdoor']):
399
+ implicit_prefs['exercise_needs'] = 'high'
400
+ implicit_prefs['size_preference'] = 'medium_to_large'
401
+
402
+ # Infer from living environment
403
+ if any(env in text for env in ['apartment', 'small space', 'city']):
404
+ implicit_prefs['size_preference'] = 'small_to_medium'
405
+ implicit_prefs['noise_tolerance'] = 'low'
406
+ implicit_prefs['exercise_needs'] = 'moderate'
407
+
408
+ # Infer from family situation
409
+ if 'children' in text or 'kids' in text:
410
+ implicit_prefs['temperament'] = 'gentle_patient'
411
+ implicit_prefs['good_with_children'] = True
412
+
413
+ # Infer from experience level
414
+ if any(exp in text for exp in ['first time', 'beginner', 'new to']):
415
+ implicit_prefs['care_level'] = 'low_to_moderate'
416
+ implicit_prefs['training_difficulty'] = 'easy'
417
+
418
+ # Infer from time commitment
419
+ if any(time in text for time in ['busy', 'no time', 'low maintenance']):
420
+ implicit_prefs['grooming_needs'] = 'low'
421
+ implicit_prefs['care_level'] = 'low'
422
+ implicit_prefs['exercise_needs'] = 'low_to_moderate'
423
+
424
+ return implicit_prefs
425
+
426
+ except Exception as e:
427
+ print(f"Error extracting implicit preferences: {str(e)}")
428
+ return {}
429
+
430
+ def validate_input(self, text: str) -> Dict[str, Any]:
431
+ """
432
+ Validate input text validity
433
+
434
+ Args:
435
+ text: Input text
436
+
437
+ Returns:
438
+ Validation results dictionary
439
+ """
440
+ if not text or not text.strip():
441
+ return {
442
+ 'is_valid': False,
443
+ 'error': 'Empty input',
444
+ 'suggestions': ['Please provide a description of your preferences']
445
+ }
446
+
447
+ text = text.strip()
448
+
449
+ # Check length
450
+ if len(text) < 10:
451
+ return {
452
+ 'is_valid': False,
453
+ 'error': 'Input too short',
454
+ 'suggestions': ['Please provide more details about your preferences']
455
+ }
456
+
457
+ if len(text) > 1000:
458
+ return {
459
+ 'is_valid': False,
460
+ 'error': 'Input too long',
461
+ 'suggestions': ['Please provide a more concise description']
462
+ }
463
+
464
+ # Check for meaningful content
465
+ processed_text = self.preprocess_text(text)
466
+ meaningful_words = [word for word in processed_text.split()
467
+ if len(word) > 2 and word not in self.stop_words]
468
+
469
+ if len(meaningful_words) < 3:
470
+ return {
471
+ 'is_valid': False,
472
+ 'error': 'Not enough meaningful content',
473
+ 'suggestions': ['Please provide more specific details about your lifestyle and preferences']
474
+ }
475
+
476
+ return {
477
+ 'is_valid': True,
478
+ 'word_count': len(meaningful_words),
479
+ 'suggestions': []
480
+ }
481
+
482
+ def get_nlp_processor():
483
+ """Get natural language processor instance"""
484
+ try:
485
+ return NaturalLanguageProcessor()
486
+ except Exception as e:
487
+ print(f"Error creating NLP processor: {str(e)}")
488
+ return None
query_understanding.py ADDED
@@ -0,0 +1,464 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import json
3
+ import numpy as np
4
+ import sqlite3
5
+ from typing import Dict, List, Tuple, Optional, Any
6
+ from dataclasses import dataclass, field
7
+ import traceback
8
+ 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:
15
+ """Structured query intent data structure"""
16
+ spatial_constraints: List[str] = field(default_factory=list)
17
+ activity_level: List[str] = field(default_factory=list)
18
+ noise_preferences: List[str] = field(default_factory=list)
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:
28
+ """Dimensional synonyms dictionary structure"""
29
+ spatial: Dict[str, List[str]] = field(default_factory=dict)
30
+ activity: Dict[str, List[str]] = field(default_factory=dict)
31
+ noise: Dict[str, List[str]] = field(default_factory=dict)
32
+ size: Dict[str, List[str]] = field(default_factory=dict)
33
+ family: Dict[str, List[str]] = field(default_factory=dict)
34
+ maintenance: Dict[str, List[str]] = field(default_factory=dict)
35
+ special: Dict[str, List[str]] = field(default_factory=dict)
36
+
37
+ class QueryUnderstandingEngine:
38
+ """
39
+ 多維度語義查詢理解引擎
40
+ 支援中英文自然語言理解並轉換為結構化品種推薦查詢
41
+ """
42
+
43
+ def __init__(self):
44
+ """初始化查詢理解引擎"""
45
+ self.sbert_model = None
46
+ self.breed_list = self._load_breed_list()
47
+ self.synonyms = self._initialize_synonyms()
48
+ self.semantic_templates = {}
49
+ self._initialize_sbert_model()
50
+ self._build_semantic_templates()
51
+
52
+ def _load_breed_list(self) -> List[str]:
53
+ """載入品種清單"""
54
+ try:
55
+ conn = sqlite3.connect('animal_detector.db')
56
+ cursor = conn.cursor()
57
+ cursor.execute("SELECT DISTINCT Breed FROM AnimalCatalog")
58
+ breeds = [row[0] for row in cursor.fetchall()]
59
+ cursor.close()
60
+ conn.close()
61
+ return breeds
62
+ except Exception as e:
63
+ print(f"Error loading breed list: {str(e)}")
64
+ # 備用品種清單
65
+ return ['Labrador_Retriever', 'German_Shepherd', 'Golden_Retriever',
66
+ 'Bulldog', 'Poodle', 'Beagle', 'Border_Collie', 'Yorkshire_Terrier']
67
+
68
+ def _initialize_sbert_model(self):
69
+ """初始化 SBERT 模型"""
70
+ try:
71
+ model_options = ['all-MiniLM-L6-v2', 'all-mpnet-base-v2', 'all-MiniLM-L12-v2']
72
+
73
+ for model_name in model_options:
74
+ try:
75
+ self.sbert_model = SentenceTransformer(model_name)
76
+ print(f"SBERT model {model_name} loaded successfully for query understanding")
77
+ return
78
+ except Exception as e:
79
+ print(f"Failed to load {model_name}: {str(e)}")
80
+ continue
81
+
82
+ print("All SBERT models failed to load. Using keyword-only analysis.")
83
+ self.sbert_model = None
84
+
85
+ except Exception as e:
86
+ print(f"Failed to initialize SBERT model: {str(e)}")
87
+ self.sbert_model = None
88
+
89
+ def _initialize_synonyms(self) -> DimensionalSynonyms:
90
+ """初始化多維度同義詞字典"""
91
+ return DimensionalSynonyms(
92
+ spatial={
93
+ 'apartment': ['apartment', 'flat', 'condo', 'small space', 'city living',
94
+ 'urban', 'no yard', 'indoor'],
95
+ 'house': ['house', 'home', 'yard', 'garden', 'backyard', 'large space',
96
+ 'suburban', 'rural', 'farm']
97
+ },
98
+ activity={
99
+ 'low': ['low activity', 'sedentary', 'couch potato', 'minimal exercise',
100
+ 'indoor lifestyle', 'lazy', 'calm'],
101
+ 'moderate': ['moderate activity', 'daily walks', 'light exercise',
102
+ 'regular walks'],
103
+ 'high': ['high activity', 'energetic', 'active', 'exercise', 'hiking',
104
+ 'running', 'jogging', 'outdoor sports']
105
+ },
106
+ noise={
107
+ 'low': ['quiet', 'silent', 'no barking', 'peaceful', 'low noise',
108
+ 'rarely barks', 'soft-spoken'],
109
+ 'moderate': ['moderate barking', 'occasional barking'],
110
+ 'high': ['loud', 'barking', 'vocal', 'noisy', 'frequent barking',
111
+ 'alert dog']
112
+ },
113
+ size={
114
+ 'small': ['small', 'tiny', 'little', 'compact', 'miniature', 'toy',
115
+ 'lap dog'],
116
+ 'medium': ['medium', 'moderate size', 'average', 'mid-sized'],
117
+ 'large': ['large', 'big', 'giant', 'huge', 'massive', 'great']
118
+ },
119
+ family={
120
+ 'children': ['children', 'kids', 'family', 'child-friendly', 'toddler',
121
+ 'baby', 'school age'],
122
+ 'elderly': ['elderly', 'senior', 'old people', 'retirement', 'aged'],
123
+ 'single': ['single', 'alone', 'individual', 'solo', 'myself']
124
+ },
125
+ maintenance={
126
+ 'low': ['low maintenance', 'easy care', 'simple', 'minimal grooming',
127
+ 'wash and go'],
128
+ 'moderate': ['moderate maintenance', 'regular grooming'],
129
+ 'high': ['high maintenance', 'professional grooming', 'daily brushing',
130
+ 'care intensive']
131
+ },
132
+ special={
133
+ 'guard': ['guard dog', 'protection', 'security', 'watchdog',
134
+ 'protective', 'defender'],
135
+ 'companion': ['companion', 'therapy', 'emotional support', 'comfort',
136
+ 'cuddly', 'lap dog'],
137
+ 'hypoallergenic': ['hypoallergenic', 'allergies', 'non-shedding',
138
+ 'allergy-friendly', 'no shed'],
139
+ 'first_time': ['first time', 'beginner', 'new to dogs', 'inexperienced',
140
+ 'never owned']
141
+ }
142
+ )
143
+
144
+ def _build_semantic_templates(self):
145
+ """建立語義模板向量(僅在 SBERT 可用時)"""
146
+ if not self.sbert_model:
147
+ return
148
+
149
+ try:
150
+ # 為每個維度建立模板句子
151
+ templates = {
152
+ 'spatial_apartment': "I live in an apartment with limited space and no yard",
153
+ 'spatial_house': "I live in a house with a large yard and outdoor space",
154
+ 'activity_low': "I prefer a calm, low-energy dog that doesn't need much exercise",
155
+ 'activity_high': "I want an active, energetic dog for hiking and outdoor activities",
156
+ 'noise_low': "I need a quiet dog that rarely barks and won't disturb neighbors",
157
+ 'noise_high': "I don't mind a vocal dog that barks and makes noise",
158
+ 'size_small': "I prefer small, compact dogs that are easy to handle",
159
+ 'size_large': "I want a large, impressive dog with strong presence",
160
+ 'family_children': "I have young children and need a child-friendly dog",
161
+ 'family_elderly': "I'm looking for a calm companion dog for elderly person",
162
+ 'maintenance_low': "I want a low-maintenance dog that's easy to care for",
163
+ 'maintenance_high': "I don't mind high-maintenance dogs requiring professional grooming"
164
+ }
165
+
166
+ # 生成模板向量
167
+ for key, template in templates.items():
168
+ embedding = self.sbert_model.encode(template, convert_to_tensor=False)
169
+ self.semantic_templates[key] = embedding
170
+
171
+ print(f"Built {len(self.semantic_templates)} semantic templates")
172
+
173
+ except Exception as e:
174
+ print(f"Error building semantic templates: {str(e)}")
175
+ self.semantic_templates = {}
176
+
177
+ def analyze_query(self, user_input: str) -> QueryDimensions:
178
+ """
179
+ 分析使用者查詢並提取多維度意圖
180
+
181
+ Args:
182
+ user_input: 使用者的自然語言查詢
183
+
184
+ Returns:
185
+ QueryDimensions: 結構化的查詢維度
186
+ """
187
+ try:
188
+ # 正規化輸入文字
189
+ normalized_input = user_input.lower().strip()
190
+
191
+ # 基於關鍵字的維度分析
192
+ dimensions = self._extract_keyword_dimensions(normalized_input)
193
+
194
+ # 如果 SBERT 可用,進行語義分析增強
195
+ if self.sbert_model:
196
+ semantic_dimensions = self._extract_semantic_dimensions(user_input)
197
+ dimensions = self._merge_dimensions(dimensions, semantic_dimensions)
198
+
199
+ # 提取品種提及
200
+ dimensions.breed_mentions = self._extract_breed_mentions(normalized_input)
201
+
202
+ # 計算信心分數
203
+ dimensions.confidence_scores = self._calculate_confidence_scores(dimensions, user_input)
204
+
205
+ return dimensions
206
+
207
+ except Exception as e:
208
+ print(f"Error analyzing query: {str(e)}")
209
+ print(traceback.format_exc())
210
+ # 回傳空的維度結構
211
+ return QueryDimensions()
212
+
213
+ def _extract_keyword_dimensions(self, text: str) -> QueryDimensions:
214
+ """基於關鍵字提取維度"""
215
+ dimensions = QueryDimensions()
216
+
217
+ # 空間限制分析
218
+ for category, keywords in self.synonyms.spatial.items():
219
+ if any(keyword in text for keyword in keywords):
220
+ dimensions.spatial_constraints.append(category)
221
+
222
+ # 活動水平分析
223
+ for level, keywords in self.synonyms.activity.items():
224
+ if any(keyword in text for keyword in keywords):
225
+ dimensions.activity_level.append(level)
226
+
227
+ # 噪音偏好分析
228
+ for level, keywords in self.synonyms.noise.items():
229
+ if any(keyword in text for keyword in keywords):
230
+ dimensions.noise_preferences.append(level)
231
+
232
+ # 尺寸偏好分析
233
+ for size, keywords in self.synonyms.size.items():
234
+ if any(keyword in text for keyword in keywords):
235
+ dimensions.size_preferences.append(size)
236
+
237
+ # 家庭情況分析
238
+ for context, keywords in self.synonyms.family.items():
239
+ if any(keyword in text for keyword in keywords):
240
+ dimensions.family_context.append(context)
241
+
242
+ # 維護水平分析
243
+ for level, keywords in self.synonyms.maintenance.items():
244
+ if any(keyword in text for keyword in keywords):
245
+ dimensions.maintenance_level.append(level)
246
+
247
+ # 特殊需求分析
248
+ for requirement, keywords in self.synonyms.special.items():
249
+ if any(keyword in text for keyword in keywords):
250
+ dimensions.special_requirements.append(requirement)
251
+
252
+ return dimensions
253
+
254
+ def _extract_semantic_dimensions(self, text: str) -> QueryDimensions:
255
+ """基於語義相似度提取維度(需要 SBERT)"""
256
+ if not self.sbert_model or not self.semantic_templates:
257
+ return QueryDimensions()
258
+
259
+ try:
260
+ # 生成查詢向量
261
+ query_embedding = self.sbert_model.encode(text, convert_to_tensor=False)
262
+
263
+ dimensions = QueryDimensions()
264
+
265
+ # 計算與各個模板的相似度
266
+ similarities = {}
267
+ for template_key, template_embedding in self.semantic_templates.items():
268
+ similarity = np.dot(query_embedding, template_embedding) / (
269
+ np.linalg.norm(query_embedding) * np.linalg.norm(template_embedding)
270
+ )
271
+ similarities[template_key] = similarity
272
+
273
+ # 設定相似度閾值
274
+ threshold = 0.5
275
+
276
+ # 根據相似度提取維度
277
+ for template_key, similarity in similarities.items():
278
+ if similarity > threshold:
279
+ if template_key.startswith('spatial_'):
280
+ category = template_key.replace('spatial_', '')
281
+ if category not in dimensions.spatial_constraints:
282
+ dimensions.spatial_constraints.append(category)
283
+ elif template_key.startswith('activity_'):
284
+ level = template_key.replace('activity_', '')
285
+ if level not in dimensions.activity_level:
286
+ dimensions.activity_level.append(level)
287
+ elif template_key.startswith('noise_'):
288
+ level = template_key.replace('noise_', '')
289
+ if level not in dimensions.noise_preferences:
290
+ dimensions.noise_preferences.append(level)
291
+ elif template_key.startswith('size_'):
292
+ size = template_key.replace('size_', '')
293
+ if size not in dimensions.size_preferences:
294
+ dimensions.size_preferences.append(size)
295
+ elif template_key.startswith('family_'):
296
+ context = template_key.replace('family_', '')
297
+ if context not in dimensions.family_context:
298
+ dimensions.family_context.append(context)
299
+ elif template_key.startswith('maintenance_'):
300
+ level = template_key.replace('maintenance_', '')
301
+ if level not in dimensions.maintenance_level:
302
+ dimensions.maintenance_level.append(level)
303
+
304
+ return dimensions
305
+
306
+ except Exception as e:
307
+ print(f"Error in semantic dimension extraction: {str(e)}")
308
+ return QueryDimensions()
309
+
310
+ def _extract_breed_mentions(self, text: str) -> List[str]:
311
+ """提取品種提及"""
312
+ mentioned_breeds = []
313
+
314
+ for breed in self.breed_list:
315
+ # 將品種名稱轉換為顯示格式
316
+ breed_display = breed.replace('_', ' ').lower()
317
+ breed_words = breed_display.split()
318
+
319
+ # 檢查品種名稱是否在文字中
320
+ breed_found = False
321
+
322
+ # 完整品種名稱匹配
323
+ if breed_display in text:
324
+ breed_found = True
325
+ else:
326
+ # 部分匹配(至少匹配品種名稱的主要部分)
327
+ main_word = breed_words[0] if breed_words else ""
328
+ if len(main_word) > 3 and main_word in text:
329
+ breed_found = True
330
+
331
+ if breed_found:
332
+ mentioned_breeds.append(breed)
333
+
334
+ return mentioned_breeds
335
+
336
+ def _merge_dimensions(self, keyword_dims: QueryDimensions,
337
+ semantic_dims: QueryDimensions) -> QueryDimensions:
338
+ """合併關鍵字和語義維度"""
339
+ merged = QueryDimensions()
340
+
341
+ # 合併各個維度的結果(去重)
342
+ merged.spatial_constraints = list(set(
343
+ keyword_dims.spatial_constraints + semantic_dims.spatial_constraints
344
+ ))
345
+ merged.activity_level = list(set(
346
+ keyword_dims.activity_level + semantic_dims.activity_level
347
+ ))
348
+ merged.noise_preferences = list(set(
349
+ keyword_dims.noise_preferences + semantic_dims.noise_preferences
350
+ ))
351
+ merged.size_preferences = list(set(
352
+ keyword_dims.size_preferences + semantic_dims.size_preferences
353
+ ))
354
+ merged.family_context = list(set(
355
+ keyword_dims.family_context + semantic_dims.family_context
356
+ ))
357
+ merged.maintenance_level = list(set(
358
+ keyword_dims.maintenance_level + semantic_dims.maintenance_level
359
+ ))
360
+ merged.special_requirements = list(set(
361
+ keyword_dims.special_requirements + semantic_dims.special_requirements
362
+ ))
363
+
364
+ return merged
365
+
366
+ def _calculate_confidence_scores(self, dimensions: QueryDimensions,
367
+ original_text: str) -> Dict[str, float]:
368
+ """計算各維度的信心分數"""
369
+ confidence_scores = {}
370
+
371
+ # 基於匹配的關鍵字數量計算信心分數
372
+ text_length = len(original_text.split())
373
+
374
+ # 空間限制信心分數
375
+ spatial_matches = len(dimensions.spatial_constraints)
376
+ confidence_scores['spatial'] = min(1.0, spatial_matches * 0.5)
377
+
378
+ # 活動水平信心分數
379
+ activity_matches = len(dimensions.activity_level)
380
+ confidence_scores['activity'] = min(1.0, activity_matches * 0.5)
381
+
382
+ # 噪音偏好信心分數
383
+ noise_matches = len(dimensions.noise_preferences)
384
+ confidence_scores['noise'] = min(1.0, noise_matches * 0.5)
385
+
386
+ # 尺寸偏好信心分數
387
+ size_matches = len(dimensions.size_preferences)
388
+ confidence_scores['size'] = min(1.0, size_matches * 0.5)
389
+
390
+ # 家庭情況信心分數
391
+ family_matches = len(dimensions.family_context)
392
+ confidence_scores['family'] = min(1.0, family_matches * 0.5)
393
+
394
+ # 維護水平信心分數
395
+ maintenance_matches = len(dimensions.maintenance_level)
396
+ confidence_scores['maintenance'] = min(1.0, maintenance_matches * 0.5)
397
+
398
+ # 特殊需求信心分數
399
+ special_matches = len(dimensions.special_requirements)
400
+ confidence_scores['special'] = min(1.0, special_matches * 0.5)
401
+
402
+ # 品種提及信心分數
403
+ breed_matches = len(dimensions.breed_mentions)
404
+ confidence_scores['breeds'] = min(1.0, breed_matches * 0.3)
405
+
406
+ # 整體信心分數(基於總匹配數量和文字長度)
407
+ total_matches = sum([
408
+ spatial_matches, activity_matches, noise_matches, size_matches,
409
+ family_matches, maintenance_matches, special_matches, breed_matches
410
+ ])
411
+ confidence_scores['overall'] = min(1.0, total_matches / max(1, text_length * 0.1))
412
+
413
+ return confidence_scores
414
+
415
+ def get_dimension_summary(self, dimensions: QueryDimensions) -> Dict[str, Any]:
416
+ """獲取維度摘要信息"""
417
+ return {
418
+ 'spatial_constraints': dimensions.spatial_constraints,
419
+ 'activity_level': dimensions.activity_level,
420
+ 'noise_preferences': dimensions.noise_preferences,
421
+ 'size_preferences': dimensions.size_preferences,
422
+ 'family_context': dimensions.family_context,
423
+ 'maintenance_level': dimensions.maintenance_level,
424
+ 'special_requirements': dimensions.special_requirements,
425
+ 'breed_mentions': [breed.replace('_', ' ') for breed in dimensions.breed_mentions],
426
+ 'confidence_scores': dimensions.confidence_scores,
427
+ 'total_dimensions_detected': sum([
428
+ len(dimensions.spatial_constraints),
429
+ len(dimensions.activity_level),
430
+ len(dimensions.noise_preferences),
431
+ len(dimensions.size_preferences),
432
+ len(dimensions.family_context),
433
+ len(dimensions.maintenance_level),
434
+ len(dimensions.special_requirements)
435
+ ])
436
+ }
437
+
438
+ # 便利函數
439
+ def analyze_user_query(user_input: str) -> QueryDimensions:
440
+ """
441
+ 便利函數:分析使用者查詢
442
+
443
+ Args:
444
+ user_input: 使用者的自然語言查詢
445
+
446
+ Returns:
447
+ QueryDimensions: 結構化的查詢維度
448
+ """
449
+ engine = QueryUnderstandingEngine()
450
+ return engine.analyze_query(user_input)
451
+
452
+ def get_query_summary(user_input: str) -> Dict[str, Any]:
453
+ """
454
+ 便利函數:獲取查詢摘要
455
+
456
+ Args:
457
+ user_input: 使用者的自然語言查詢
458
+
459
+ Returns:
460
+ Dict: 查詢維度摘要
461
+ """
462
+ engine = QueryUnderstandingEngine()
463
+ dimensions = engine.analyze_query(user_input)
464
+ return engine.get_dimension_summary(dimensions)
recommendation_formatter.py ADDED
@@ -0,0 +1,321 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sqlite3
2
+ import traceback
3
+ import random
4
+ from typing import List, Dict
5
+ from breed_health_info import breed_health_info, default_health_note
6
+ from breed_noise_info import breed_noise_info
7
+ from dog_database import get_dog_description
8
+ from scoring_calculation_system import UserPreferences, calculate_compatibility_score
9
+
10
+ def get_breed_recommendations(user_prefs: UserPreferences, top_n: int = 15) -> List[Dict]:
11
+ """基於使用者偏好推薦狗品種,確保正確的分數排序"""
12
+ print(f"Starting get_breed_recommendations with top_n={top_n}")
13
+ recommendations = []
14
+ seen_breeds = set()
15
+
16
+ try:
17
+ # 獲取所有品種
18
+ conn = sqlite3.connect('animal_detector.db')
19
+ cursor = conn.cursor()
20
+ cursor.execute("SELECT Breed FROM AnimalCatalog")
21
+ all_breeds = cursor.fetchall()
22
+ conn.close()
23
+
24
+ print(f"Total breeds in database: {len(all_breeds)}")
25
+
26
+ # 收集所有品種的分數
27
+ for breed_tuple in all_breeds:
28
+ breed = breed_tuple[0]
29
+ base_breed = breed.split('(')[0].strip()
30
+
31
+ if base_breed in seen_breeds:
32
+ continue
33
+ seen_breeds.add(base_breed)
34
+
35
+ # 獲取品種資訊
36
+ breed_info = get_dog_description(breed)
37
+ if not isinstance(breed_info, dict):
38
+ continue
39
+
40
+ # 調整品種尺寸過濾邏輯,避免過度限制候選品種
41
+ if user_prefs.size_preference != "no_preference":
42
+ breed_size = breed_info.get('Size', '').lower()
43
+ user_size = user_prefs.size_preference.lower()
44
+
45
+ # 放寬尺寸匹配條件,允許相鄰尺寸的品種通過篩選
46
+ size_compatibility = False
47
+ if user_size == 'small':
48
+ size_compatibility = breed_size in ['small', 'medium']
49
+ elif user_size == 'medium':
50
+ size_compatibility = breed_size in ['small', 'medium', 'large']
51
+ elif user_size == 'large':
52
+ size_compatibility = breed_size in ['medium', 'large']
53
+ else:
54
+ size_compatibility = True
55
+
56
+ if not size_compatibility:
57
+ continue
58
+
59
+ # 獲取噪音資訊
60
+ noise_info = breed_noise_info.get(breed, {
61
+ "noise_notes": "Noise information not available",
62
+ "noise_level": "Unknown",
63
+ "source": "N/A"
64
+ })
65
+
66
+ # 將噪音資訊整合到品種資訊中
67
+ breed_info['noise_info'] = noise_info
68
+
69
+ # 計算基礎相容性分數
70
+ compatibility_scores = calculate_compatibility_score(breed_info, user_prefs)
71
+
72
+ # 計算品種特定加分
73
+ breed_bonus = 0.0
74
+
75
+ # 壽命加分
76
+ try:
77
+ lifespan = breed_info.get('Lifespan', '10-12 years')
78
+ years = [int(x) for x in lifespan.split('-')[0].split()[0:1]]
79
+ longevity_bonus = min(0.02, (max(years) - 10) * 0.005)
80
+ breed_bonus += longevity_bonus
81
+ except:
82
+ pass
83
+
84
+ # 性格特徵加分
85
+ temperament = breed_info.get('Temperament', '').lower()
86
+ positive_traits = ['friendly', 'gentle', 'affectionate', 'intelligent']
87
+ negative_traits = ['aggressive', 'stubborn', 'dominant']
88
+
89
+ breed_bonus += sum(0.01 for trait in positive_traits if trait in temperament)
90
+ breed_bonus -= sum(0.01 for trait in negative_traits if trait in temperament)
91
+
92
+ # 與孩童相容性加分
93
+ if user_prefs.has_children:
94
+ if breed_info.get('Good with Children') == 'Yes':
95
+ breed_bonus += 0.02
96
+ elif breed_info.get('Good with Children') == 'No':
97
+ breed_bonus -= 0.03
98
+
99
+ # 噪音相關加分
100
+ if user_prefs.noise_tolerance == 'low':
101
+ if noise_info['noise_level'].lower() == 'high':
102
+ breed_bonus -= 0.03
103
+ elif noise_info['noise_level'].lower() == 'low':
104
+ breed_bonus += 0.02
105
+ elif user_prefs.noise_tolerance == 'high':
106
+ if noise_info['noise_level'].lower() == 'high':
107
+ breed_bonus += 0.01
108
+
109
+ # 計算最終分數並加入自然變異
110
+ breed_hash = hash(breed)
111
+ random.seed(breed_hash)
112
+
113
+ # Add small natural variation to avoid identical scores
114
+ natural_variation = random.uniform(-0.008, 0.008)
115
+ breed_bonus = round(breed_bonus + natural_variation, 4)
116
+ final_score = round(min(1.0, compatibility_scores['overall'] + breed_bonus), 4)
117
+
118
+ recommendations.append({
119
+ 'breed': breed,
120
+ 'base_score': round(compatibility_scores['overall'], 4),
121
+ 'bonus_score': round(breed_bonus, 4),
122
+ 'final_score': final_score,
123
+ 'scores': compatibility_scores,
124
+ 'info': breed_info,
125
+ 'noise_info': noise_info
126
+ })
127
+
128
+ print(f"Breeds after filtering: {len(recommendations)}")
129
+
130
+ # 嚴格按照 final_score 排序
131
+ recommendations.sort(key=lambda x: (round(-x['final_score'], 4), x['breed']))
132
+
133
+ # 修正後的推薦選擇邏輯,移除有問題的分數比較
134
+ final_recommendations = []
135
+
136
+ # 直接選取前 top_n 個品種,確保返回完整數量
137
+ for i, rec in enumerate(recommendations[:top_n]):
138
+ rec['rank'] = i + 1
139
+ final_recommendations.append(rec)
140
+
141
+ print(f"Final recommendations count: {len(final_recommendations)}")
142
+
143
+ # 驗證最終排序
144
+ for i in range(len(final_recommendations)-1):
145
+ current = final_recommendations[i]
146
+ next_rec = final_recommendations[i+1]
147
+
148
+ if current['final_score'] < next_rec['final_score']:
149
+ print(f"Warning: Sorting error detected!")
150
+ print(f"#{i+1} {current['breed']}: {current['final_score']}")
151
+ print(f"#{i+2} {next_rec['breed']}: {next_rec['final_score']}")
152
+
153
+ # 交換位置
154
+ final_recommendations[i], final_recommendations[i+1] = \
155
+ final_recommendations[i+1], final_recommendations[i]
156
+
157
+ # 打印最終結果以供驗證
158
+ print("\nFinal Rankings:")
159
+ for rec in final_recommendations:
160
+ print(f"#{rec['rank']} {rec['breed']}")
161
+ print(f"Base Score: {rec['base_score']:.4f}")
162
+ print(f"Bonus: {rec['bonus_score']:.4f}")
163
+ print(f"Final Score: {rec['final_score']:.4f}\n")
164
+
165
+ return final_recommendations
166
+
167
+ except Exception as e:
168
+ print(f"Error in get_breed_recommendations: {str(e)}")
169
+ print(f"Traceback: {traceback.format_exc()}")
170
+
171
+
172
+ def _format_dimension_scores(dimension_scores: Dict) -> str:
173
+ """Format individual dimension scores as badges"""
174
+ if not dimension_scores:
175
+ return ""
176
+
177
+ badges_html = '<div class="dimension-badges">'
178
+
179
+ for dimension, score in dimension_scores.items():
180
+ if isinstance(score, (int, float)):
181
+ score_percent = score * 100
182
+ else:
183
+ score_percent = 75 # default
184
+
185
+ if score_percent >= 80:
186
+ badge_class = "badge-high"
187
+ elif score_percent >= 60:
188
+ badge_class = "badge-medium"
189
+ else:
190
+ badge_class = "badge-low"
191
+
192
+ dimension_label = dimension.replace('_', ' ').title()
193
+ badges_html += f'''
194
+ <span class="dimension-badge {badge_class}">
195
+ {dimension_label}: {score_percent:.0f}%
196
+ </span>
197
+ '''
198
+
199
+ badges_html += '</div>'
200
+ return badges_html
201
+
202
+
203
+ def calculate_breed_bonus_factors(breed_info: dict, user_prefs: 'UserPreferences') -> tuple:
204
+ """計算品種額外加分因素並返回原因列表"""
205
+ bonus = 0.0
206
+ reasons = []
207
+
208
+ # 壽命加分
209
+ try:
210
+ lifespan = breed_info.get('Lifespan', '10-12 years')
211
+ years = [int(x) for x in lifespan.split('-')[0].split()[0:1]]
212
+ if max(years) >= 12:
213
+ bonus += 0.02
214
+ reasons.append("Above-average lifespan")
215
+ except:
216
+ pass
217
+
218
+ # 性格特徵加分
219
+ temperament = breed_info.get('Temperament', '').lower()
220
+ if any(trait in temperament for trait in ['friendly', 'gentle', 'affectionate']):
221
+ bonus += 0.01
222
+ reasons.append("Positive temperament traits")
223
+
224
+ # 與孩童相容性
225
+ if breed_info.get('Good with Children') == 'Yes':
226
+ bonus += 0.01
227
+ reasons.append("Excellent with children")
228
+
229
+ return bonus, reasons
230
+
231
+
232
+ def generate_breed_characteristics_data(breed_info: dict) -> List[tuple]:
233
+ """生成品種特徵資料列表"""
234
+ return [
235
+ ('Size', breed_info.get('Size', 'Unknown')),
236
+ ('Exercise Needs', breed_info.get('Exercise Needs', 'Moderate')),
237
+ ('Grooming Needs', breed_info.get('Grooming Needs', 'Moderate')),
238
+ ('Good with Children', breed_info.get('Good with Children', 'Yes')),
239
+ ('Temperament', breed_info.get('Temperament', '')),
240
+ ('Lifespan', breed_info.get('Lifespan', '10-12 years')),
241
+ ('Description', breed_info.get('Description', ''))
242
+ ]
243
+
244
+
245
+ def parse_noise_information(noise_info: dict) -> tuple:
246
+ """解析噪音資訊並返回結構化資料"""
247
+ noise_notes = noise_info.get('noise_notes', '').split('\n')
248
+ noise_characteristics = []
249
+ barking_triggers = []
250
+ noise_level = ''
251
+
252
+ current_section = None
253
+ for line in noise_notes:
254
+ line = line.strip()
255
+ if 'Typical noise characteristics:' in line:
256
+ current_section = 'characteristics'
257
+ elif 'Noise level:' in line:
258
+ noise_level = line.replace('Noise level:', '').strip()
259
+ elif 'Barking triggers:' in line:
260
+ current_section = 'triggers'
261
+ elif line.startswith('•'):
262
+ if current_section == 'characteristics':
263
+ noise_characteristics.append(line[1:].strip())
264
+ elif current_section == 'triggers':
265
+ barking_triggers.append(line[1:].strip())
266
+
267
+ return noise_characteristics, barking_triggers, noise_level
268
+
269
+
270
+ def parse_health_information(health_info: dict) -> tuple:
271
+ """解析健康資訊並返回結構化資料"""
272
+ health_notes = health_info.get('health_notes', '').split('\n')
273
+ health_considerations = []
274
+ health_screenings = []
275
+
276
+ current_section = None
277
+ for line in health_notes:
278
+ line = line.strip()
279
+ if 'Common breed-specific health considerations' in line:
280
+ current_section = 'considerations'
281
+ elif 'Recommended health screenings:' in line:
282
+ current_section = 'screenings'
283
+ elif line.startswith('•'):
284
+ if current_section == 'considerations':
285
+ health_considerations.append(line[1:].strip())
286
+ elif current_section == 'screenings':
287
+ health_screenings.append(line[1:].strip())
288
+
289
+ return health_considerations, health_screenings
290
+
291
+
292
+ def generate_dimension_scores_for_display(base_score: float, rank: int, breed: str,
293
+ semantic_score: float = 0.7,
294
+ comparative_bonus: float = 0.0,
295
+ lifestyle_bonus: float = 0.0,
296
+ is_description_search: bool = False) -> dict:
297
+ """為顯示生成維度分數"""
298
+ random.seed(hash(breed) + rank) # 一致的隨機性
299
+
300
+ if is_description_search:
301
+ # Description search: 創建更自然的分數分佈在50%-95%範圍內
302
+ score_variance = 0.08 if base_score > 0.7 else 0.06
303
+
304
+ scores = {
305
+ 'space': max(0.50, min(0.95,
306
+ base_score * 0.92 + (lifestyle_bonus * 0.5) + random.uniform(-score_variance, score_variance))),
307
+ 'exercise': max(0.50, min(0.95,
308
+ base_score * 0.88 + (lifestyle_bonus * 0.4) + random.uniform(-score_variance, score_variance))),
309
+ 'grooming': max(0.50, min(0.95,
310
+ base_score * 0.85 + (comparative_bonus * 0.4) + random.uniform(-score_variance, score_variance))),
311
+ 'experience': max(0.50, min(0.95,
312
+ base_score * 0.87 + (lifestyle_bonus * 0.3) + random.uniform(-score_variance, score_variance))),
313
+ 'noise': max(0.50, min(0.95,
314
+ base_score * 0.83 + (lifestyle_bonus * 0.6) + random.uniform(-score_variance, score_variance))),
315
+ 'overall': base_score
316
+ }
317
+ else:
318
+ # 傳統搜尋結果的分數結構會在呼叫處理中傳入
319
+ scores = {'overall': base_score}
320
+
321
+ return scores
recommendation_html_format.py CHANGED
@@ -1,160 +1,102 @@
1
- import sqlite3
2
- import traceback
3
  from typing import List, Dict
4
  from breed_health_info import breed_health_info, default_health_note
5
  from breed_noise_info import breed_noise_info
6
  from dog_database import get_dog_description
7
- from scoring_calculation_system import UserPreferences, calculate_compatibility_score
 
 
 
 
 
 
 
 
 
 
8
 
9
- def format_recommendation_html(recommendations: List[Dict], is_description_search: bool = False) -> str:
10
- """將推薦結果格式化為HTML"""
11
-
12
- html_content = """
13
- <style>
14
- .progress {
15
- transition: all 0.3s ease-in-out;
16
- border-radius: 4px;
17
- height: 12px;
18
- }
19
- .progress-bar {
20
- background-color: #f5f5f5;
21
- border-radius: 4px;
22
- overflow: hidden;
23
- position: relative;
24
- }
25
- .score-item {
26
- margin: 10px 0;
27
- }
28
- .percentage {
29
- margin-left: 8px;
30
- font-weight: 500;
31
- }
32
- </style>
33
- <div class='recommendations-container'>"""
34
-
35
- def _convert_to_display_score(score: float, score_type: str = None) -> int:
36
- """
37
- 更改為生成更明顯差異的顯示分數
38
- """
39
- try:
40
- # 基礎分數轉換(保持相對關係但擴大差異)
41
- if score_type == 'bonus': # Breed Bonus 使用不同的轉換邏輯
42
- base_score = 35 + (score * 60) # 35-95 範圍,差異更大
43
- else:
44
- # 其他類型的分數轉換
45
- if score <= 0.3:
46
- base_score = 40 + (score * 45) # 40-53.5 範圍
47
- elif score <= 0.6:
48
- base_score = 55 + ((score - 0.3) * 55) # 55-71.5 範圍
49
- elif score <= 0.8:
50
- base_score = 72 + ((score - 0.6) * 60) # 72-84 範圍
51
- else:
52
- base_score = 85 + ((score - 0.8) * 50) # 85-95 範圍
53
-
54
- # 添加不規則的微調,但保持相對關係
55
- import random
56
- if score_type == 'bonus':
57
- adjustment = random.uniform(-2, 2)
58
- else:
59
- # 根據分數範圍決定調整幅度
60
- if score > 0.8:
61
- adjustment = random.uniform(-3, 3)
62
- elif score > 0.6:
63
- adjustment = random.uniform(-4, 4)
64
- else:
65
- adjustment = random.uniform(-2, 2)
66
-
67
- final_score = base_score + adjustment
68
-
69
- # 確保最終分數在合理範圍內並避免5的倍數
70
- final_score = min(95, max(40, final_score))
71
- rounded_score = round(final_score)
72
- if rounded_score % 5 == 0:
73
- rounded_score += random.choice([-1, 1])
74
-
75
- return rounded_score
76
-
77
- except Exception as e:
78
- print(f"Error in convert_to_display_score: {str(e)}")
79
- return 70
80
-
81
-
82
- def _generate_progress_bar(score: float, score_type: str = None) -> dict:
83
- """
84
- 生成進度條的寬度和顏色
85
-
86
- Parameters:
87
- score: 原始分數 (0-1 之間的浮點數)
88
- score_type: 分數類型,用於特殊處理某些類型的分數
89
-
90
- Returns:
91
- dict: 包含寬度和顏色的字典
92
- """
93
- # 計算寬度
94
- if score_type == 'bonus':
95
- # Breed Bonus 特殊的計算方式
96
- width = min(100, max(5, 10 + (score * 300)))
97
- else:
98
- # 一般分數的計算
99
- if score >= 0.9:
100
- width = 90 + (score - 0.9) * 100
101
- elif score >= 0.7:
102
- width = 70 + (score - 0.7) * 100
103
- elif score >= 0.5:
104
- width = 40 + (score - 0.5) * 150
105
- elif score >= 0.3:
106
- width = 20 + (score - 0.3) * 100
107
- else:
108
- width = max(5, score * 66.7)
109
-
110
- # 根據分數決定顏色
111
- if score >= 0.9:
112
- color = '#68b36b' # 高分段柔和綠
113
- elif score >= 0.7:
114
- color = '#9bcf74' # 中高分段略黃綠
115
- elif score >= 0.5:
116
- color = '#d4d880' # 中等分段黃綠
117
- elif score >= 0.3:
118
- color = '#e3b583' # 偏低分段柔和橘
119
- else:
120
- color = '#e9a098' # 低分段暖紅粉
121
-
122
- return {
123
- 'width': width,
124
- 'color': color
125
- }
126
 
 
 
 
 
 
 
 
 
127
 
128
  for rec in recommendations:
129
  breed = rec['breed']
130
- scores = rec['scores']
131
- info = rec['info']
132
  rank = rec.get('rank', 0)
133
- final_score = rec.get('final_score', scores['overall'])
134
- bonus_score = rec.get('bonus_score', 0)
 
135
 
136
  if is_description_search:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  display_scores = {
138
- 'space': _convert_to_display_score(scores['space'], 'space'),
139
- 'exercise': _convert_to_display_score(scores['exercise'], 'exercise'),
140
- 'grooming': _convert_to_display_score(scores['grooming'], 'grooming'),
141
- 'experience': _convert_to_display_score(scores['experience'], 'experience'),
142
- 'noise': _convert_to_display_score(scores['noise'], 'noise')
143
  }
144
  else:
145
- display_scores = scores # 圖片識別使用原始分數
 
 
 
 
 
 
 
 
 
 
 
 
146
 
147
  progress_bars = {}
148
  for metric in ['space', 'exercise', 'grooming', 'experience', 'noise']:
149
  if metric in scores:
150
- bar_data = _generate_progress_bar(scores[metric], metric)
 
 
151
  progress_bars[metric] = {
152
  'style': f"width: {bar_data['width']}%; background-color: {bar_data['color']};"
153
  }
154
 
155
- # bonus
156
  if bonus_score > 0:
157
- bonus_data = _generate_progress_bar(bonus_score, 'bonus')
 
 
158
  progress_bars['bonus'] = {
159
  'style': f"width: {bonus_data['width']}%; background-color: {bonus_data['color']};"
160
  }
@@ -166,98 +108,34 @@ def format_recommendation_html(recommendations: List[Dict], is_description_searc
166
  "source": "N/A"
167
  })
168
 
169
- # 解析噪音資訊
170
- noise_notes = noise_info.get('noise_notes', '').split('\n')
171
- noise_characteristics = []
172
- barking_triggers = []
173
- noise_level = ''
174
-
175
- current_section = None
176
- for line in noise_notes:
177
- line = line.strip()
178
- if 'Typical noise characteristics:' in line:
179
- current_section = 'characteristics'
180
- elif 'Noise level:' in line:
181
- noise_level = line.replace('Noise level:', '').strip()
182
- elif 'Barking triggers:' in line:
183
- current_section = 'triggers'
184
- elif line.startswith('•'):
185
- if current_section == 'characteristics':
186
- noise_characteristics.append(line[1:].strip())
187
- elif current_section == 'triggers':
188
- barking_triggers.append(line[1:].strip())
189
-
190
- # 生成特徵和觸發因素的HTML
191
- noise_characteristics_html = '\n'.join([f'<li>{item}</li>' for item in noise_characteristics])
192
- barking_triggers_html = '\n'.join([f'<li>{item}</li>' for item in barking_triggers])
193
-
194
- # 處理健康資訊
195
- health_notes = health_info.get('health_notes', '').split('\n')
196
- health_considerations = []
197
- health_screenings = []
198
-
199
- current_section = None
200
- for line in health_notes:
201
- line = line.strip()
202
- if 'Common breed-specific health considerations' in line:
203
- current_section = 'considerations'
204
- elif 'Recommended health screenings:' in line:
205
- current_section = 'screenings'
206
- elif line.startswith('•'):
207
- if current_section == 'considerations':
208
- health_considerations.append(line[1:].strip())
209
- elif current_section == 'screenings':
210
- health_screenings.append(line[1:].strip())
211
-
212
- health_considerations_html = '\n'.join([f'<li>{item}</li>' for item in health_considerations])
213
- health_screenings_html = '\n'.join([f'<li>{item}</li>' for item in health_screenings])
214
-
215
- # 獎勵原因計算
216
- bonus_reasons = []
217
- temperament = info.get('Temperament', '').lower()
218
- if any(trait in temperament for trait in ['friendly', 'gentle', 'affectionate']):
219
- bonus_reasons.append("Positive temperament traits")
220
- if info.get('Good with Children') == 'Yes':
221
- bonus_reasons.append("Excellent with children")
222
- try:
223
- lifespan = info.get('Lifespan', '10-12 years')
224
- years = int(lifespan.split('-')[0])
225
- if years >= 12:
226
- bonus_reasons.append("Above-average lifespan")
227
- except:
228
- pass
229
 
 
 
 
 
 
 
 
 
 
230
  html_content += f"""
231
- <div class="dog-info-card recommendation-card">
232
- <div class="breed-info">
233
- <h2 class="section-title">
234
- <span class="icon">🏆</span> #{rank} {breed.replace('_', ' ')}
235
- <span class="score-badge">
236
- Overall Match: {final_score*100:.1f}%
237
- </span>
238
- </h2>
239
  <div class="compatibility-scores">
240
- <!-- 空間相容性評分 -->
241
  <div class="score-item">
242
  <span class="label">
243
- Space Compatibility:
244
- <span class="tooltip">
245
- <span class="tooltip-icon">ⓘ</span>
246
- <span class="tooltip-text">
247
- <strong>Space Compatibility Score:</strong><br>
248
- • Evaluates how well the breed adapts to your living environment<br>
249
- • Considers if your home (apartment/house) and yard access suit the breed’s size<br>
250
- • Higher score means the breed fits well in your available space.
251
- </span>
252
- </span>
253
  </span>
254
  <div class="progress-bar">
255
  <div class="progress" style="{progress_bars.get('space', {'style': 'width: 0%; background-color: #e74c3c;'})['style']}"></div>
256
  </div>
257
- <span class="percentage">{display_scores['space'] if is_description_search else scores.get('space', 0)*100:.1f}%</span>
258
  </div>
259
 
260
- <!-- 運動匹配度評分 -->
261
  <div class="score-item">
262
  <span class="label">
263
  Exercise Match:
@@ -266,18 +144,18 @@ def format_recommendation_html(recommendations: List[Dict], is_description_searc
266
  <span class="tooltip-text">
267
  <strong>Exercise Match Score:</strong><br>
268
  • Based on your daily exercise time and type<br>
269
- • Compares your activity level to the breeds exercise needs<br>
270
- • Higher score means your routine aligns well with the breeds energy requirements.
271
  </span>
272
  </span>
273
  </span>
274
  <div class="progress-bar">
275
  <div class="progress" style="{progress_bars.get('exercise', {'style': 'width: 0%; background-color: #e74c3c;'})['style']}"></div>
276
  </div>
277
- <span class="percentage">{display_scores['exercise'] if is_description_search else scores.get('exercise', 0)*100:.1f}%</span>
278
  </div>
279
 
280
- <!-- 美容需求匹配度評分 -->
281
  <div class="score-item">
282
  <span class="label">
283
  Grooming Match:
@@ -285,19 +163,19 @@ def format_recommendation_html(recommendations: List[Dict], is_description_searc
285
  <span class="tooltip-icon">ⓘ</span>
286
  <span class="tooltip-text">
287
  <strong>Grooming Match Score:</strong><br>
288
- • Evaluates breeds grooming needs (coat care, trimming, brushing)<br>
289
  • Compares these requirements with your grooming commitment level<br>
290
- • Higher score means the breeds grooming needs fit your willingness and capability.
291
  </span>
292
  </span>
293
  </span>
294
  <div class="progress-bar">
295
  <div class="progress" style="{progress_bars.get('grooming', {'style': 'width: 0%; background-color: #e74c3c;'})['style']}"></div>
296
  </div>
297
- <span class="percentage">{display_scores['grooming'] if is_description_search else scores.get('grooming', 0)*100:.1f}%</span>
298
  </div>
299
 
300
- <!-- 經驗需求匹配度評分 -->
301
  <div class="score-item">
302
  <span class="label">
303
  Experience Match:
@@ -306,7 +184,7 @@ def format_recommendation_html(recommendations: List[Dict], is_description_searc
306
  <span class="tooltip-text">
307
  <strong>Experience Match Score:</strong><br>
308
  • Based on your dog-owning experience level<br>
309
- • Considers breeds training complexity, temperament, and handling difficulty<br>
310
  • Higher score means the breed is more suitable for your experience level.
311
  </span>
312
  </span>
@@ -314,10 +192,10 @@ def format_recommendation_html(recommendations: List[Dict], is_description_searc
314
  <div class="progress-bar">
315
  <div class="progress" style="{progress_bars.get('experience', {'style': 'width: 0%; background-color: #e74c3c;'})['style']}"></div>
316
  </div>
317
- <span class="percentage">{display_scores['experience'] if is_description_search else scores.get('experience', 0)*100:.1f}%</span>
318
  </div>
319
 
320
- <!-- 噪音相容性評分 -->
321
  <div class="score-item">
322
  <span class="label">
323
  Noise Compatibility:
@@ -334,7 +212,7 @@ def format_recommendation_html(recommendations: List[Dict], is_description_searc
334
  <div class="progress-bar">
335
  <div class="progress" style="{progress_bars.get('noise', {'style': 'width: 0%; background-color: #e74c3c;'})['style']}"></div>
336
  </div>
337
- <span class="percentage">{display_scores['noise'] if is_description_search else scores.get('noise', 0)*100:.1f}%</span>
338
  </div>
339
 
340
  {f'''
@@ -345,8 +223,7 @@ def format_recommendation_html(recommendations: List[Dict], is_description_searc
345
  <span class="tooltip-icon">ⓘ</span>
346
  <span class="tooltip-text">
347
  <strong>Breed Bonus Points:</strong><br>
348
- • {('<br>• '.join(bonus_reasons)) if bonus_reasons else 'No additional bonus points'}<br>
349
- <br>
350
  <strong>Bonus Factors Include:</strong><br>
351
  • Friendly temperament<br>
352
  • Child compatibility<br>
@@ -362,328 +239,167 @@ def format_recommendation_html(recommendations: List[Dict], is_description_searc
362
  </div>
363
  ''' if bonus_score > 0 else ''}
364
  </div>
365
- <div class="breed-details-section">
366
- <h3 class="subsection-title">
367
- <span class="icon">📋</span> Breed Details
368
- </h3>
369
- <div class="details-grid">
370
- <div class="detail-item">
371
- <span class="tooltip">
372
- <span class="icon">📏</span>
373
- <span class="label">Size:</span>
374
- <span class="tooltip-icon">ⓘ</span>
375
- <span class="tooltip-text">
376
- <strong>Size Categories:</strong><br>
377
- • Small: Under 20 pounds<br>
378
- Medium: 20-60 pounds<br>
379
- Large: Over 60 pounds
380
- </span>
381
- <span class="value">{info['Size']}</span>
382
- </span>
383
- </div>
384
- <div class="detail-item">
385
- <span class="tooltip">
386
- <span class="icon">🏃</span>
387
- <span class="label">Exercise Needs:</span>
388
- <span class="tooltip-icon">ⓘ</span>
389
- <span class="tooltip-text">
390
- <strong>Exercise Needs:</strong><br>
391
- Low: Short walks<br>
392
- Moderate: 1-2 hours daily<br>
393
- High: 2+ hours daily<br>
394
- Very High: Constant activity
395
- </span>
396
- <span class="value">{info['Exercise Needs']}</span>
397
- </span>
398
- </div>
399
- <div class="detail-item">
400
- <span class="tooltip">
401
- <span class="icon">👨‍👩‍👧‍👦</span>
402
- <span class="label">Good with Children:</span>
403
- <span class="tooltip-icon">ⓘ</span>
404
- <span class="tooltip-text">
405
- <strong>Child Compatibility:</strong><br>
406
- Yes: Excellent with kids<br>
407
- Moderate: Good with older children<br>
408
- • No: Better for adult households
409
- </span>
410
- <span class="value">{info['Good with Children']}</span>
411
- </span>
412
- </div>
413
- <div class="detail-item">
414
- <span class="tooltip">
415
- <span class="icon">⏳</span>
416
- <span class="label">Lifespan:</span>
417
- <span class="tooltip-icon">ⓘ</span>
418
- <span class="tooltip-text">
419
- <strong>Average Lifespan:</strong><br>
420
- Short: 6-8 years<br>
421
- • Average: 10-15 years<br>
422
- • Long: 12-20 years<br>
423
- • Varies by size: Larger breeds typically have shorter lifespans
424
- </span>
425
- </span>
426
- <span class="value">{info['Lifespan']}</span>
427
- </div>
428
- </div>
429
- </div>
430
- <div class="description-section">
431
- <h3 class="subsection-title">
432
- <span class="icon">📝</span> Description
433
- </h3>
434
- <p class="description-text">{info.get('Description', '')}</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
435
  </div>
436
- <div class="noise-section">
437
- <h3 class="section-header">
438
- <span class="icon">🔊</span> Noise Behavior
439
- <span class="tooltip">
440
- <span class="tooltip-icon">ⓘ</span>
441
- <span class="tooltip-text">
442
- <strong>Noise Behavior:</strong><br>
443
- Typical vocalization patterns<br>
444
- Common triggers and frequency<br>
445
- Based on breed characteristics
446
- </span>
447
- </span>
448
- </h3>
449
- <div class="noise-info">
450
- <div class="noise-details">
451
- <h4 class="section-header">Typical noise characteristics:</h4>
452
- <div class="characteristics-list">
453
- <div class="list-item">Moderate to high barker</div>
454
- <div class="list-item">Alert watch dog</div>
455
- <div class="list-item">Attention-seeking barks</div>
456
- <div class="list-item">Social vocalizations</div>
457
- </div>
458
- <div class="noise-level-display">
459
- <h4 class="section-header">Noise level:</h4>
460
- <div class="level-indicator">
461
- <span class="level-text">Moderate-High</span>
462
- <div class="level-bars">
463
- <span class="bar"></span>
464
- <span class="bar"></span>
465
- <span class="bar"></span>
466
- </div>
467
- </div>
468
- </div>
469
- <h4 class="section-header">Barking triggers:</h4>
470
- <div class="triggers-list">
471
- <div class="list-item">Separation anxiety</div>
472
- <div class="list-item">Attention needs</div>
473
- <div class="list-item">Strange noises</div>
474
- <div class="list-item">Excitement</div>
475
- </div>
476
- </div>
477
- <div class="noise-disclaimer">
478
- <p class="disclaimer-text source-text">Source: Compiled from various breed behavior resources, 2024</p>
479
- <p class="disclaimer-text">Individual dogs may vary in their vocalization patterns.</p>
480
- <p class="disclaimer-text">Training can significantly influence barking behavior.</p>
481
- <p class="disclaimer-text">Environmental factors may affect noise levels.</p>
482
- </div>
483
- </div>
484
  </div>
485
- <div class="health-section">
486
- <h3 class="section-header">
487
- <span class="icon">🏥</span> Health Insights
488
- <span class="tooltip">
489
- <span class="tooltip-icon">ⓘ</span>
490
- <span class="tooltip-text">
491
- Health information is compiled from multiple sources including veterinary resources, breed guides, and international canine health databases.
492
- Each dog is unique and may vary from these general guidelines.
493
- </span>
494
- </span>
495
- </h3>
496
- <div class="health-info">
497
- <div class="health-details">
498
- <div class="health-block">
499
- <h4 class="section-header">Common breed-specific health considerations:</h4>
500
- <div class="health-grid">
501
- <div class="health-item">Patellar luxation</div>
502
- <div class="health-item">Progressive retinal atrophy</div>
503
- <div class="health-item">Von Willebrand's disease</div>
504
- <div class="health-item">Open fontanel</div>
505
- </div>
506
- </div>
507
- <div class="health-block">
508
- <h4 class="section-header">Recommended health screenings:</h4>
509
- <div class="health-grid">
510
- <div class="health-item screening">Patella evaluation</div>
511
- <div class="health-item screening">Eye examination</div>
512
- <div class="health-item screening">Blood clotting tests</div>
513
- <div class="health-item screening">Skull development monitoring</div>
514
- </div>
515
- </div>
516
- </div>
517
- <div class="health-disclaimer">
518
- <p class="disclaimer-text source-text">Source: Compiled from various veterinary and breed information resources, 2024</p>
519
- <p class="disclaimer-text">This information is for reference only and based on breed tendencies.</p>
520
- <p class="disclaimer-text">Each dog is unique and may not develop any or all of these conditions.</p>
521
- <p class="disclaimer-text">Always consult with qualified veterinarians for professional advice.</p>
522
- </div>
523
  </div>
524
  </div>
525
- <div class="action-section">
526
- <a href="https://www.akc.org/dog-breeds/{breed.lower().replace('_', '-')}/"
527
- target="_blank"
528
- class="akc-button">
529
- <span class="icon">🌐</span>
530
- Learn more about {breed.replace('_', ' ')} on AKC website
531
- </a>
532
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
533
  </div>
534
  </div>
535
- """
 
 
536
 
537
  html_content += "</div>"
538
  return html_content
539
-
540
- def get_breed_recommendations(user_prefs: UserPreferences, top_n: int = 15) -> List[Dict]:
541
- """基於使用者偏好推薦狗品種,確保正確的分數排序"""
542
- print("Starting get_breed_recommendations")
543
- recommendations = []
544
- seen_breeds = set()
545
-
546
- try:
547
- # 獲取所有品種
548
- conn = sqlite3.connect('animal_detector.db')
549
- cursor = conn.cursor()
550
- cursor.execute("SELECT Breed FROM AnimalCatalog")
551
- all_breeds = cursor.fetchall()
552
- conn.close()
553
-
554
- # 收集所有品種的分數
555
- for breed_tuple in all_breeds:
556
- breed = breed_tuple[0]
557
- base_breed = breed.split('(')[0].strip()
558
-
559
- if base_breed in seen_breeds:
560
- continue
561
- seen_breeds.add(base_breed)
562
-
563
- # 獲取品種資訊
564
- breed_info = get_dog_description(breed)
565
- if not isinstance(breed_info, dict):
566
- continue
567
-
568
- if user_prefs.size_preference != "no_preference":
569
- breed_size = breed_info.get('Size', '').lower()
570
- user_size = user_prefs.size_preference.lower()
571
- if breed_size != user_size:
572
- continue
573
-
574
- # 獲取噪音資訊
575
- noise_info = breed_noise_info.get(breed, {
576
- "noise_notes": "Noise information not available",
577
- "noise_level": "Unknown",
578
- "source": "N/A"
579
- })
580
-
581
- # 將噪音資訊整合到品種資訊中
582
- breed_info['noise_info'] = noise_info
583
-
584
- # 計算基礎相容性分數
585
- compatibility_scores = calculate_compatibility_score(breed_info, user_prefs)
586
-
587
- # 計算品種特定加分
588
- breed_bonus = 0.0
589
-
590
- # 壽命加分
591
- try:
592
- lifespan = breed_info.get('Lifespan', '10-12 years')
593
- years = [int(x) for x in lifespan.split('-')[0].split()[0:1]]
594
- longevity_bonus = min(0.02, (max(years) - 10) * 0.005)
595
- breed_bonus += longevity_bonus
596
- except:
597
- pass
598
-
599
- # 性格特徵加分
600
- temperament = breed_info.get('Temperament', '').lower()
601
- positive_traits = ['friendly', 'gentle', 'affectionate', 'intelligent']
602
- negative_traits = ['aggressive', 'stubborn', 'dominant']
603
-
604
- breed_bonus += sum(0.01 for trait in positive_traits if trait in temperament)
605
- breed_bonus -= sum(0.01 for trait in negative_traits if trait in temperament)
606
-
607
- # 與孩童相容性加分
608
- if user_prefs.has_children:
609
- if breed_info.get('Good with Children') == 'Yes':
610
- breed_bonus += 0.02
611
- elif breed_info.get('Good with Children') == 'No':
612
- breed_bonus -= 0.03
613
-
614
- # 噪音相關加分
615
- if user_prefs.noise_tolerance == 'low':
616
- if noise_info['noise_level'].lower() == 'high':
617
- breed_bonus -= 0.03
618
- elif noise_info['noise_level'].lower() == 'low':
619
- breed_bonus += 0.02
620
- elif user_prefs.noise_tolerance == 'high':
621
- if noise_info['noise_level'].lower() == 'high':
622
- breed_bonus += 0.01
623
-
624
- # 計算最終分數
625
- breed_bonus = round(breed_bonus, 4)
626
- final_score = round(compatibility_scores['overall'] + breed_bonus, 4)
627
-
628
- recommendations.append({
629
- 'breed': breed,
630
- 'base_score': round(compatibility_scores['overall'], 4),
631
- 'bonus_score': round(breed_bonus, 4),
632
- 'final_score': final_score,
633
- 'scores': compatibility_scores,
634
- 'info': breed_info,
635
- 'noise_info': noise_info # 添加噪音資訊到推薦結果
636
- })
637
-
638
- # 嚴格按照 final_score 排序
639
- recommendations.sort(key=lambda x: (round(-x['final_score'], 4), x['breed'] )) # 負號降序排列
640
-
641
- # 選擇前N名並確保正確排序
642
- final_recommendations = []
643
- last_score = None
644
- rank = 1
645
-
646
- available_breeds = len(recommendations)
647
- max_to_return = min(available_breeds, top_n) # 不會超過實際可用品種數
648
-
649
- for rec in recommendations:
650
- if len(final_recommendations) >= max_to_return:
651
- break
652
-
653
- current_score = rec['final_score']
654
- if last_score is not None and current_score > last_score:
655
- continue
656
-
657
- rec['rank'] = rank
658
- final_recommendations.append(rec)
659
- last_score = current_score
660
- rank += 1
661
-
662
- # 驗證最終排序
663
- for i in range(len(final_recommendations)-1):
664
- current = final_recommendations[i]
665
- next_rec = final_recommendations[i+1]
666
-
667
- if current['final_score'] < next_rec['final_score']:
668
- print(f"Warning: Sorting error detected!")
669
- print(f"#{i+1} {current['breed']}: {current['final_score']}")
670
- print(f"#{i+2} {next_rec['breed']}: {next_rec['final_score']}")
671
-
672
- # 交換位置
673
- final_recommendations[i], final_recommendations[i+1] = \
674
- final_recommendations[i+1], final_recommendations[i]
675
-
676
- # 打印最終結果以供驗證
677
- print("\nFinal Rankings:")
678
- for rec in final_recommendations:
679
- print(f"#{rec['rank']} {rec['breed']}")
680
- print(f"Base Score: {rec['base_score']:.4f}")
681
- print(f"Bonus: {rec['bonus_score']:.4f}")
682
- print(f"Final Score: {rec['final_score']:.4f}\n")
683
-
684
- return final_recommendations
685
-
686
- except Exception as e:
687
- print(f"Error in get_breed_recommendations: {str(e)}")
688
- print(f"Traceback: {traceback.format_exc()}")
689
- return []
 
1
+ import random
 
2
  from typing import List, Dict
3
  from breed_health_info import breed_health_info, default_health_note
4
  from breed_noise_info import breed_noise_info
5
  from dog_database import get_dog_description
6
+ from scoring_calculation_system import UserPreferences
7
+ from recommendation_formatter import (
8
+ get_breed_recommendations,
9
+ _format_dimension_scores,
10
+ calculate_breed_bonus_factors,
11
+ generate_breed_characteristics_data,
12
+ parse_noise_information,
13
+ parse_health_information,
14
+ generate_dimension_scores_for_display
15
+ )
16
+ from recommendation_html_formatter import RecommendationHTMLFormatter
17
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
+ def format_recommendation_html(recommendations: List[Dict], is_description_search: bool = False) -> str:
20
+ """統一推薦結果HTML格式化,確保視覺與數值邏輯一致"""
21
+
22
+ # 創建HTML格式器實例
23
+ formatter = RecommendationHTMLFormatter()
24
+
25
+ # 獲取對應的CSS樣式
26
+ html_content = formatter.get_css_styles(is_description_search) + "<div class='recommendations-container'>"
27
 
28
  for rec in recommendations:
29
  breed = rec['breed']
 
 
30
  rank = rec.get('rank', 0)
31
+
32
+ breed_name_for_db = breed.replace(' ', '_')
33
+ breed_info_from_db = get_dog_description(breed_name_for_db)
34
 
35
  if is_description_search:
36
+ # Handle semantic search results structure - use scores directly from semantic recommender
37
+ overall_score = rec.get('overall_score', 0.7)
38
+ final_score = rec.get('final_score', overall_score) # Use final_score if available
39
+ semantic_score = rec.get('semantic_score', 0.7)
40
+ comparative_bonus = rec.get('comparative_bonus', 0.0)
41
+ lifestyle_bonus = rec.get('lifestyle_bonus', 0.0)
42
+
43
+ # Use the actual calculated scores without re-computation
44
+ base_score = final_score
45
+
46
+ # Generate dimension scores using the formatter helper
47
+ scores = generate_dimension_scores_for_display(
48
+ base_score, rank, breed, semantic_score,
49
+ comparative_bonus, lifestyle_bonus, is_description_search
50
+ )
51
+
52
+ bonus_score = max(0.0, comparative_bonus + random.uniform(-0.02, 0.02))
53
+ info = generate_breed_characteristics_data(breed_info_from_db or {})
54
+ info = dict(info) # Convert to dict for compatibility
55
+
56
+ # Add any missing fields from rec
57
+ if not breed_info_from_db:
58
+ for key in ['Size', 'Exercise Needs', 'Grooming Needs', 'Good with Children', 'Temperament', 'Lifespan', 'Description']:
59
+ if key not in info:
60
+ info[key] = rec.get(key.lower().replace(' ', '_'), 'Unknown' if key != 'Description' else '')
61
+
62
+ # Display scores as percentages with one decimal place
63
  display_scores = {
64
+ 'space': round(scores['space'] * 100, 1),
65
+ 'exercise': round(scores['exercise'] * 100, 1),
66
+ 'grooming': round(scores['grooming'] * 100, 1),
67
+ 'experience': round(scores['experience'] * 100, 1),
68
+ 'noise': round(scores['noise'] * 100, 1),
69
  }
70
  else:
71
+ # Handle traditional search results structure
72
+ scores = rec['scores']
73
+ info = rec['info']
74
+ final_score = rec.get('final_score', scores['overall'])
75
+ bonus_score = rec.get('bonus_score', 0)
76
+ # Convert traditional scores to percentage display format with one decimal
77
+ display_scores = {
78
+ 'space': round(scores.get('space', 0) * 100, 1),
79
+ 'exercise': round(scores.get('exercise', 0) * 100, 1),
80
+ 'grooming': round(scores.get('grooming', 0) * 100, 1),
81
+ 'experience': round(scores.get('experience', 0) * 100, 1),
82
+ 'noise': round(scores.get('noise', 0) * 100, 1),
83
+ }
84
 
85
  progress_bars = {}
86
  for metric in ['space', 'exercise', 'grooming', 'experience', 'noise']:
87
  if metric in scores:
88
+ # 使用顯示分數(百分比)來計算進度條
89
+ display_score = display_scores[metric]
90
+ bar_data = formatter.generate_progress_bar(display_score, metric, is_percentage_display=True, is_description_search=is_description_search)
91
  progress_bars[metric] = {
92
  'style': f"width: {bar_data['width']}%; background-color: {bar_data['color']};"
93
  }
94
 
95
+ # bonus
96
  if bonus_score > 0:
97
+ # bonus_score 通常是 0-1 範圍,需要轉換為百分比顯示
98
+ bonus_percentage = bonus_score * 100
99
+ bonus_data = formatter.generate_progress_bar(bonus_percentage, 'bonus', is_percentage_display=True, is_description_search=is_description_search)
100
  progress_bars['bonus'] = {
101
  'style': f"width: {bonus_data['width']}%; background-color: {bonus_data['color']};"
102
  }
 
108
  "source": "N/A"
109
  })
110
 
111
+ # 解析噪音和健康資訊
112
+ noise_characteristics, barking_triggers, noise_level = parse_noise_information(noise_info)
113
+ health_considerations, health_screenings = parse_health_information(health_info)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
 
115
+ # 計算獎勵因素
116
+ _, bonus_reasons = calculate_breed_bonus_factors(info, None) # User prefs not needed for display
117
+
118
+ # 生成品種卡片標題
119
+ html_content += formatter.generate_breed_card_header(breed, rank, final_score, is_description_search)
120
+
121
+ # 品種詳細資訊區域 - 使用格式器方法簡化
122
+ tooltip_html = formatter.generate_tooltips_section()
123
+
124
  html_content += f"""
125
+ <div class="breed-details">
 
 
 
 
 
 
 
126
  <div class="compatibility-scores">
127
+ <!-- Space Compatibility Score -->
128
  <div class="score-item">
129
  <span class="label">
130
+ Space Compatibility:{tooltip_html}
 
 
 
 
 
 
 
 
 
131
  </span>
132
  <div class="progress-bar">
133
  <div class="progress" style="{progress_bars.get('space', {'style': 'width: 0%; background-color: #e74c3c;'})['style']}"></div>
134
  </div>
135
+ <span class="percentage">{display_scores['space']:.1f}%</span>
136
  </div>
137
 
138
+ <!-- Exercise Compatibility Score -->
139
  <div class="score-item">
140
  <span class="label">
141
  Exercise Match:
 
144
  <span class="tooltip-text">
145
  <strong>Exercise Match Score:</strong><br>
146
  • Based on your daily exercise time and type<br>
147
+ • Compares your activity level to the breed's exercise needs<br>
148
+ • Higher score means your routine aligns well with the breed's energy requirements.
149
  </span>
150
  </span>
151
  </span>
152
  <div class="progress-bar">
153
  <div class="progress" style="{progress_bars.get('exercise', {'style': 'width: 0%; background-color: #e74c3c;'})['style']}"></div>
154
  </div>
155
+ <span class="percentage">{display_scores['exercise']:.1f}%</span>
156
  </div>
157
 
158
+ <!-- Grooming Compatibility Score -->
159
  <div class="score-item">
160
  <span class="label">
161
  Grooming Match:
 
163
  <span class="tooltip-icon">ⓘ</span>
164
  <span class="tooltip-text">
165
  <strong>Grooming Match Score:</strong><br>
166
+ • Evaluates breed's grooming needs (coat care, trimming, brushing)<br>
167
  • Compares these requirements with your grooming commitment level<br>
168
+ • Higher score means the breed's grooming needs fit your willingness and capability.
169
  </span>
170
  </span>
171
  </span>
172
  <div class="progress-bar">
173
  <div class="progress" style="{progress_bars.get('grooming', {'style': 'width: 0%; background-color: #e74c3c;'})['style']}"></div>
174
  </div>
175
+ <span class="percentage">{display_scores['grooming']:.1f}%</span>
176
  </div>
177
 
178
+ <!-- Experience Compatibility Score -->
179
  <div class="score-item">
180
  <span class="label">
181
  Experience Match:
 
184
  <span class="tooltip-text">
185
  <strong>Experience Match Score:</strong><br>
186
  • Based on your dog-owning experience level<br>
187
+ • Considers breed's training complexity, temperament, and handling difficulty<br>
188
  • Higher score means the breed is more suitable for your experience level.
189
  </span>
190
  </span>
 
192
  <div class="progress-bar">
193
  <div class="progress" style="{progress_bars.get('experience', {'style': 'width: 0%; background-color: #e74c3c;'})['style']}"></div>
194
  </div>
195
+ <span class="percentage">{display_scores['experience']:.1f}%</span>
196
  </div>
197
 
198
+ <!-- Noise Compatibility Score -->
199
  <div class="score-item">
200
  <span class="label">
201
  Noise Compatibility:
 
212
  <div class="progress-bar">
213
  <div class="progress" style="{progress_bars.get('noise', {'style': 'width: 0%; background-color: #e74c3c;'})['style']}"></div>
214
  </div>
215
+ <span class="percentage">{display_scores['noise']:.1f}%</span>
216
  </div>
217
 
218
  {f'''
 
223
  <span class="tooltip-icon">ⓘ</span>
224
  <span class="tooltip-text">
225
  <strong>Breed Bonus Points:</strong><br>
226
+ • {('<br>• '.join(bonus_reasons) if bonus_reasons else 'No additional bonus points')}<br><br>
 
227
  <strong>Bonus Factors Include:</strong><br>
228
  • Friendly temperament<br>
229
  • Child compatibility<br>
 
239
  </div>
240
  ''' if bonus_score > 0 else ''}
241
  </div>
242
+ """
243
+
244
+ # 使用格式器生成詳細區段
245
+ html_content += formatter.generate_detailed_sections_html(
246
+ breed, info, noise_characteristics, barking_triggers, noise_level,
247
+ health_considerations, health_screenings
248
+ )
249
+
250
+ html_content += """
251
+ </div>
252
+ </div>
253
+ """
254
+
255
+ # 結束 HTML 內容
256
+ html_content += "</div>"
257
+ return html_content
258
+
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
+
266
+ if not recommendations:
267
+ return '''
268
+ <div style="text-align: center; padding: 60px 20px; background: linear-gradient(135deg, #f8fafc, #e2e8f0); border-radius: 16px; margin: 20px 0;">
269
+ <div style="font-size: 3em; margin-bottom: 16px;">🐕</div>
270
+ <h3 style="color: #374151; margin-bottom: 12px;">No Recommendations Available</h3>
271
+ <p style="color: #6b7280; font-size: 1.1em;">Please try adjusting your preferences or description, and we'll help you find the most suitable breeds.</p>
272
+ </div>
273
+ '''
274
+
275
+ # 使用格式器的統一CSS樣式
276
+ html_content = formatter.unified_css + "<div class='unified-recommendations'>"
277
+
278
+ for rec in recommendations:
279
+ breed = rec['breed']
280
+ rank = rec.get('rank', 0)
281
+
282
+ # 統一分數處理
283
+ overall_score = rec.get('overall_score', rec.get('final_score', 0.7))
284
+ scores = rec.get('scores', {})
285
+
286
+ # 如果沒有維度分數,基於總分生成一致的維度分數
287
+ if not scores:
288
+ scores = generate_dimension_scores_for_display(
289
+ overall_score, rank, breed, is_description_search=is_description_search
290
+ )
291
+
292
+ # 獲取品種資訊
293
+ breed_name_for_db = breed.replace(' ', '_')
294
+ breed_info = get_dog_description(breed_name_for_db) or {}
295
+
296
+ # 維度標籤
297
+ dimension_labels = {
298
+ 'space': '🏠 Space Compatibility',
299
+ 'exercise': '🏃 Exercise Requirements',
300
+ 'grooming': '✂️ Grooming Needs',
301
+ 'experience': '🎓 Experience Level',
302
+ 'noise': '🔊 Noise Control',
303
+ 'family': '👨‍👩‍👧‍👦 Family Compatibility'
304
+ }
305
+
306
+ # 維度提示氣泡內容
307
+ tooltip_content = {
308
+ 'space': 'Space Compatibility Score:<br>• Evaluates how well the breed adapts to your living environment<br>• Considers if your home (apartment/house) and yard access suit the breed\'s size<br>• Higher score means the breed fits well in your available space.',
309
+ 'exercise': 'Exercise Requirements Score:<br>• Based on your daily exercise time and activity type<br>• Compares your activity level to the breed\'s exercise needs<br>• Higher score means your routine aligns well with the breed\'s energy requirements.',
310
+ 'grooming': 'Grooming Needs Score:<br>• Evaluates breed\'s grooming needs (coat care, trimming, brushing)<br>• Compares these requirements with your grooming commitment level<br>• Higher score means the breed\'s grooming needs fit your willingness and capability.',
311
+ 'experience': 'Experience Level Score:<br>• Based on your dog-owning experience level<br>• Considers breed\'s training complexity, temperament, and handling difficulty<br>• Higher score means the breed is more suitable for your experience level.',
312
+ 'noise': 'Noise Control Score:<br>• Based on your noise tolerance preference<br>• Considers breed\'s typical noise level and barking tendencies<br>• Accounts for living environment and sensitivity to noise.',
313
+ 'family': 'Family Compatibility Score:<br>• Evaluates how well the breed fits with your family situation<br>• Considers children, other pets, and family dynamics<br>• Higher score means better family compatibility.'
314
+ }
315
+
316
+ # 生成維度分數HTML
317
+ dimension_html = ""
318
+ for dim, label in dimension_labels.items():
319
+ score = scores.get(dim, overall_score * 0.9)
320
+ percentage = formatter.format_unified_percentage(score)
321
+ progress_bar = formatter.generate_unified_progress_bar(score)
322
+
323
+ # 為 Find by Description 添加提示氣泡
324
+ tooltip_html = ''
325
+ if is_description_search:
326
+ tooltip_html = f'<span class="tooltip"><span class="tooltip-icon">i</span><span class="tooltip-text"><strong>{tooltip_content.get(dim, "")}</strong></span></span>'
327
+
328
+ dimension_html += f'''
329
+ <div class="unified-dimension-item">
330
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
331
+ <span style="font-weight: 600; color: #374151;">{label} {tooltip_html}</span>
332
+ <span style="font-weight: 700; color: #1f2937; font-size: 1.1em;">{percentage}</span>
333
  </div>
334
+ {progress_bar}
335
+ </div>
336
+ '''
337
+
338
+ # 生成品種資訊HTML
339
+ characteristics = generate_breed_characteristics_data(breed_info)
340
+ info_html = ""
341
+ for label, value in characteristics:
342
+ if label != 'Description': # Skip description as it's shown separately
343
+ info_html += f'''
344
+ <div class="unified-info-item">
345
+ <div class="unified-info-label">{label}</div>
346
+ <div class="unified-info-value">{value}</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
347
  </div>
348
+ '''
349
+
350
+ # 生成單個品種卡片HTML
351
+ overall_percentage = formatter.format_unified_percentage(overall_score)
352
+ overall_progress_bar = formatter.generate_unified_progress_bar(overall_score)
353
+
354
+ brand_card_html = f'''
355
+ <div class="unified-breed-card">
356
+ <div class="unified-breed-header">
357
+ <div style="display: flex; align-items: center; gap: 12px; margin-bottom: 20px;">
358
+ <div class="unified-rank-badge">🏆 #{rank}</div>
359
+ <h2 class="unified-breed-title">{breed.replace('_', ' ')}</h2>
360
+ <div style="margin-left: auto;">
361
+ <div class="unified-match-score">Overall Match: {overall_percentage}</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362
  </div>
363
  </div>
364
+ </div>
365
+
366
+ <div class="unified-overall-section">
367
+ {overall_progress_bar}
368
+ </div>
369
+
370
+ <div class="unified-dimension-grid">
371
+ {dimension_html}
372
+ </div>
373
+
374
+ <div class="unified-breed-info">
375
+ {info_html}
376
+ </div>
377
+
378
+ <div style="background: linear-gradient(135deg, #F8FAFC, #F1F5F9); padding: 20px; border-radius: 12px; margin: 20px 0; border: 1px solid #E2E8F0;">
379
+ <h3 style="color: #1F2937; font-size: 1.3em; font-weight: 700; margin: 0 0 12px 0; display: flex; align-items: center;">
380
+ <span style="margin-right: 8px;">📝</span> Breed Description
381
+ </h3>
382
+ <p style="color: #4B5563; line-height: 1.6; margin: 0 0 16px 0; font-size: 1.05em;">
383
+ {breed_info.get('Description', 'Detailed description for this breed is not currently available.')}
384
+ </p>
385
+ <a href="https://www.akc.org/dog-breeds/{breed.lower().replace('_', '-').replace(' ', '-')}/"
386
+ target="_blank"
387
+ class="akc-button"
388
+ style="display: inline-flex; align-items: center; padding: 12px 20px;
389
+ background: linear-gradient(135deg, #3B82F6, #1D4ED8); color: white;
390
+ text-decoration: none; border-radius: 10px; font-weight: 600;
391
+ box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
392
+ transition: all 0.3s ease; font-size: 1.05em;"
393
+ onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 6px 16px rgba(59, 130, 246, 0.5)'"
394
+ onmouseout="this.style.transform='translateY(0px)'; this.style.boxShadow='0 4px 12px rgba(59, 130, 246, 0.3)'">
395
+ <span style="margin-right: 8px;">🌐</span>
396
+ Learn more about {breed.replace('_', ' ')} on AKC website
397
+ </a>
398
  </div>
399
  </div>
400
+ '''
401
+
402
+ html_content += brand_card_html
403
 
404
  html_content += "</div>"
405
  return html_content
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
recommendation_html_formatter.py ADDED
@@ -0,0 +1,1025 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import random
2
+ from typing import List, Dict
3
+ from breed_health_info import breed_health_info, default_health_note
4
+ from breed_noise_info import breed_noise_info
5
+ from dog_database import get_dog_description
6
+ from recommendation_formatter import (
7
+ generate_breed_characteristics_data,
8
+ parse_noise_information,
9
+ parse_health_information,
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
+ .breed-details {
369
+ margin-top: 12px;
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
+ /* White Tooltip Styles from styles.py */
398
+ .tooltip {
399
+ position: relative;
400
+ display: inline-flex;
401
+ align-items: center;
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
+ """統一格式化百分比顯示,確保數值邏輯一致"""
598
+ try:
599
+ # 確保分數在0-1範圍內
600
+ normalized_score = max(0.0, min(1.0, float(score)))
601
+ # 轉換為百分比並保留一位小數
602
+ percentage = normalized_score * 100
603
+ return f"{percentage:.1f}%"
604
+ except Exception as e:
605
+ print(f"Error formatting percentage: {str(e)}")
606
+ return "70.0%"
607
+
608
+ def generate_unified_progress_bar(self, score: float) -> str:
609
+ """Generate unified progress bar HTML with width directly corresponding to score"""
610
+ try:
611
+ # Ensure score is in 0-1 range
612
+ normalized_score = max(0.0, min(1.0, float(score)))
613
+
614
+ # Progress bar width with reasonable visual mapping
615
+ # High scores get enhanced visual representation for impact
616
+ if normalized_score >= 0.9:
617
+ width_percentage = 85 + (normalized_score - 0.9) * 130 # 85-98% for excellent scores
618
+ elif normalized_score >= 0.8:
619
+ width_percentage = 70 + (normalized_score - 0.8) * 150 # 70-85% for very good scores
620
+ elif normalized_score >= 0.7:
621
+ width_percentage = 55 + (normalized_score - 0.7) * 150 # 55-70% for good scores
622
+ elif normalized_score >= 0.5:
623
+ width_percentage = 30 + (normalized_score - 0.5) * 125 # 30-55% for fair scores
624
+ else:
625
+ width_percentage = 8 + normalized_score * 44 # 8-30% for low scores
626
+
627
+ # Ensure reasonable bounds
628
+ width_percentage = max(5, min(98, width_percentage))
629
+
630
+ # Choose color based on score with appropriate theme
631
+ # This is used for unified recommendations (Description search)
632
+ if normalized_score >= 0.9:
633
+ color = '#10b981' # Excellent (emerald green)
634
+ elif normalized_score >= 0.8:
635
+ color = '#06b6d4' # Good (cyan)
636
+ elif normalized_score >= 0.7:
637
+ color = '#3b82f6' # Fair (blue)
638
+ elif normalized_score >= 0.6:
639
+ color = '#1d4ed8' # Average (darker blue)
640
+ elif normalized_score >= 0.5:
641
+ color = '#1e40af' # Below average (dark blue)
642
+ else:
643
+ color = '#ef4444' # Poor (red)
644
+
645
+ return f'''
646
+ <div class="progress-bar-container" style="
647
+ background-color: #f1f5f9;
648
+ border-radius: 8px;
649
+ overflow: hidden;
650
+ height: 16px;
651
+ width: 100%;
652
+ ">
653
+ <div class="progress-bar-fill" style="
654
+ width: {width_percentage:.1f}%;
655
+ height: 100%;
656
+ background-color: {color};
657
+ border-radius: 8px;
658
+ transition: width 0.8s ease-out;
659
+ "></div>
660
+ </div>
661
+ '''
662
+
663
+ except Exception as e:
664
+ print(f"Error generating progress bar: {str(e)}")
665
+ return '<div class="progress-bar-container"><div class="progress-bar-fill" style="width: 70%; background-color: #d4a332;"></div></div>'
666
+
667
+ def generate_progress_bar(self, score: float, score_type: str = None, is_percentage_display: bool = False, is_description_search: bool = False) -> dict:
668
+ """
669
+ Generate progress bar width and color with consistent score-to-visual mapping
670
+
671
+ Parameters:
672
+ score: Score value (float between 0-1 or percentage 0-100)
673
+ score_type: Score type for special handling
674
+ is_percentage_display: Whether the score is in percentage format
675
+
676
+ Returns:
677
+ dict: Dictionary containing width and color
678
+ """
679
+ # Normalize score to 0-1 range
680
+ if is_percentage_display:
681
+ normalized_score = score / 100.0 # Convert percentage to 0-1 range
682
+ else:
683
+ normalized_score = score
684
+
685
+ # Ensure score is within valid range
686
+ normalized_score = max(0.0, min(1.0, normalized_score))
687
+
688
+ # Calculate progress bar width - simplified for Find by Criteria
689
+ if not is_description_search and score_type != 'bonus':
690
+ # Find by Criteria: 調整為更有說服力的視覺比例
691
+ percentage = normalized_score * 100
692
+ if percentage >= 95:
693
+ width = 92 + (percentage - 95) * 1.2 # 95%+ 顯示為 92-98%
694
+ elif percentage >= 90:
695
+ width = 85 + (percentage - 90) # 90-95% 顯示為 85-92%
696
+ elif percentage >= 80:
697
+ width = 75 + (percentage - 80) * 1.0 # 80-90% 顯示為 75-85%
698
+ elif percentage >= 70:
699
+ width = 60 + (percentage - 70) * 1.5 # 70-80% 顯示為 60-75%
700
+ else:
701
+ width = percentage * 0.8 # 70% 以下按比例縮放
702
+ width = max(5, min(98, width))
703
+ elif score_type == 'bonus':
704
+ # Bonus scores are typically smaller, need amplified display
705
+ width = max(5, min(95, normalized_score * 150)) # Amplified for visibility
706
+ else:
707
+ # Find by Description: 保持現有的複雜計算
708
+ if normalized_score >= 0.8:
709
+ width = 75 + (normalized_score - 0.8) * 115 # 75-98% range for high scores
710
+ elif normalized_score >= 0.6:
711
+ width = 50 + (normalized_score - 0.6) * 125 # 50-75% range for good scores
712
+ elif normalized_score >= 0.4:
713
+ width = 25 + (normalized_score - 0.4) * 125 # 25-50% range for fair scores
714
+ else:
715
+ width = 5 + normalized_score * 50 # 5-25% range for low scores
716
+
717
+ width = max(3, min(98, width))
718
+
719
+ # Color coding based on normalized score - Criteria uses green gradation
720
+ if is_description_search:
721
+ # Find by Description uses blue theme
722
+ if normalized_score >= 0.9:
723
+ color = '#10b981' # Excellent (emerald green)
724
+ elif normalized_score >= 0.85:
725
+ color = '#06b6d4' # Very good (cyan)
726
+ elif normalized_score >= 0.8:
727
+ color = '#3b82f6' # Good (blue)
728
+ elif normalized_score >= 0.7:
729
+ color = '#1d4ed8' # Fair (darker blue)
730
+ elif normalized_score >= 0.6:
731
+ color = '#1e40af' # Below average (dark blue)
732
+ elif normalized_score >= 0.5:
733
+ color = '#f59e0b' # Poor (amber)
734
+ else:
735
+ color = '#ef4444' # Very poor (red)
736
+ else:
737
+ # Find by Criteria uses original green gradation
738
+ if normalized_score >= 0.9:
739
+ color = '#22c55e' # Excellent (bright green)
740
+ elif normalized_score >= 0.85:
741
+ color = '#65a30d' # Very good (green)
742
+ elif normalized_score >= 0.8:
743
+ color = '#a3a332' # Good (yellow-green)
744
+ elif normalized_score >= 0.7:
745
+ color = '#d4a332' # Fair (yellow)
746
+ elif normalized_score >= 0.6:
747
+ color = '#e67e22' # Below average (orange)
748
+ elif normalized_score >= 0.5:
749
+ color = '#e74c3c' # Poor (red)
750
+ else:
751
+ color = '#c0392b' # Very poor (dark red)
752
+
753
+ return {
754
+ 'width': width,
755
+ 'color': color
756
+ }
757
+
758
+ def get_css_styles(self, is_description_search: bool) -> str:
759
+ """根據搜尋類型返回對應的CSS樣式"""
760
+ if is_description_search:
761
+ return self.description_search_css
762
+ else:
763
+ return self.criteria_search_css
764
+
765
+ def generate_breed_card_header(self, breed: str, rank: int, final_score: float, is_description_search: bool) -> str:
766
+ """生成品種卡片標題部分的HTML"""
767
+ rank_class = f"rank-{rank}" if rank <= 3 else "rank-other"
768
+ percentage = final_score * 100
769
+
770
+ if percentage >= 90:
771
+ score_class = "score-excellent"
772
+ fill_class = "fill-excellent"
773
+ match_label = "EXCELLENT MATCH"
774
+ elif percentage >= 70:
775
+ score_class = "score-good"
776
+ fill_class = "fill-good"
777
+ match_label = "GOOD MATCH"
778
+ else:
779
+ score_class = "score-moderate"
780
+ fill_class = "fill-moderate"
781
+ match_label = "MODERATE MATCH"
782
+
783
+ if is_description_search:
784
+ # Find by Description: 使用現有複雜設計
785
+ return f"""
786
+ <div class="breed-card">
787
+ <div class="rank-badge {rank_class}">#{rank}</div>
788
+
789
+ <div class="breed-header">
790
+ <h3 class="breed-name">{breed.replace('_', ' ')}</h3>
791
+ <div class="match-score">
792
+ <div class="match-percentage {score_class}">{percentage:.1f}%</div>
793
+ <div class="match-label">{match_label}</div>
794
+ <div class="score-bar">
795
+ <div class="score-fill {fill_class}" style="width: {percentage}%"></div>
796
+ </div>
797
+ </div>
798
+ </div>"""
799
+ else:
800
+ # Find by Criteria: 使用簡潔設計,包含獎盃圖示
801
+ # 計算進度條寬度 - 調整為更有說服力的視覺比例
802
+ if percentage >= 95:
803
+ score_width = 92 + (percentage - 95) * 1.2 # 95%+ 顯示為 92-98%
804
+ elif percentage >= 90:
805
+ score_width = 85 + (percentage - 90) # 90-95% 顯示為 85-92%
806
+ elif percentage >= 80:
807
+ score_width = 75 + (percentage - 80) * 1.0 # 80-90% 顯示為 75-85%
808
+ elif percentage >= 70:
809
+ score_width = 60 + (percentage - 70) * 1.5 # 70-80% 顯示為 60-75%
810
+ else:
811
+ score_width = percentage * 0.8 # 70% 以下按比例縮放
812
+ score_width = max(5, min(98, score_width))
813
+
814
+ return f"""
815
+ <div class="breed-card">
816
+ <div class="breed-header">
817
+ <div class="breed-title">
818
+ <div class="trophy-rank">🏆 #{rank}</div>
819
+ <h3 class="breed-name" style="font-size: 28px !important;">{breed.replace('_', ' ')}</h3>
820
+ </div>
821
+ <div class="overall-score">
822
+ <div class="score-percentage {score_class}">{percentage:.1f}%</div>
823
+ <div class="score-label">OVERALL MATCH</div>
824
+ <div class="score-bar-wide">
825
+ <div class="score-fill-wide {fill_class}" style="width: {score_width:.1f}%"></div>
826
+ </div>
827
+ </div>
828
+ </div>"""
829
+
830
+ def generate_tooltips_section(self) -> str:
831
+ """生成提示氣泡HTML"""
832
+ return '''
833
+ <span class="tooltip">
834
+ <span class="tooltip-icon">ⓘ</span>
835
+ <span class="tooltip-text">
836
+ <strong>Space Compatibility Score:</strong><br>
837
+ • Evaluates how well the breed adapts to your living environment<br>
838
+ • Considers if your home (apartment/house) and yard access suit the breed's size<br>
839
+ • Higher score means the breed fits well in your available space.
840
+ </span>
841
+ </span>'''
842
+
843
+ def generate_detailed_sections_html(self, breed: str, info: dict,
844
+ noise_characteristics: List[str],
845
+ barking_triggers: List[str],
846
+ noise_level: str,
847
+ health_considerations: List[str],
848
+ health_screenings: List[str]) -> str:
849
+ """生成詳細區段的HTML"""
850
+ # 生成特徵和觸發因素的HTML
851
+ noise_characteristics_html = '\n'.join([f'<li>{item}</li>' for item in noise_characteristics])
852
+ barking_triggers_html = '\n'.join([f'<li>{item}</li>' for item in barking_triggers])
853
+ health_considerations_html = '\n'.join([f'<li>{item}</li>' for item in health_considerations])
854
+ health_screenings_html = '\n'.join([f'<li>{item}</li>' for item in health_screenings])
855
+
856
+ return f"""
857
+ <div class="breed-details-section">
858
+ <h3 class="subsection-title">
859
+ <span class="icon">📋</span> Breed Details
860
+ </h3>
861
+ <div class="details-grid">
862
+ <div class="detail-item">
863
+ <span class="tooltip">
864
+ <span class="icon">📏</span>
865
+ <span class="label">Size:</span>
866
+ <span class="tooltip-icon">ⓘ</span>
867
+ <span class="tooltip-text">
868
+ <strong>Size Categories:</strong><br>
869
+ • Small: Under 20 pounds<br>
870
+ • Medium: 20-60 pounds<br>
871
+ • Large: Over 60 pounds
872
+ </span>
873
+ <span class="value">{info['Size']}</span>
874
+ </span>
875
+ </div>
876
+ <div class="detail-item">
877
+ <span class="tooltip">
878
+ <span class="icon">🏃</span>
879
+ <span class="label">Exercise Needs:</span>
880
+ <span class="tooltip-icon">ⓘ</span>
881
+ <span class="tooltip-text">
882
+ <strong>Exercise Needs:</strong><br>
883
+ • Low: Short walks<br>
884
+ • Moderate: 1-2 hours daily<br>
885
+ • High: 2+ hours daily<br>
886
+ • Very High: Constant activity
887
+ </span>
888
+ <span class="value">{info['Exercise Needs']}</span>
889
+ </span>
890
+ </div>
891
+ <div class="detail-item">
892
+ <span class="tooltip">
893
+ <span class="icon">👨‍👩‍👧‍👦</span>
894
+ <span class="label">Good with Children:</span>
895
+ <span class="tooltip-icon">ⓘ</span>
896
+ <span class="tooltip-text">
897
+ <strong>Child Compatibility:</strong><br>
898
+ • Yes: Excellent with kids<br>
899
+ • Moderate: Good with older children<br>
900
+ • No: Better for adult households
901
+ </span>
902
+ <span class="value">{info['Good with Children']}</span>
903
+ </span>
904
+ </div>
905
+ <div class="detail-item">
906
+ <span class="tooltip">
907
+ <span class="icon">⏳</span>
908
+ <span class="label">Lifespan:</span>
909
+ <span class="tooltip-icon">ⓘ</span>
910
+ <span class="tooltip-text">
911
+ <strong>Average Lifespan:</strong><br>
912
+ • Short: 6-8 years<br>
913
+ • Average: 10-15 years<br>
914
+ • Long: 12-20 years<br>
915
+ • Varies by size: Larger breeds typically have shorter lifespans
916
+ </span>
917
+ </span>
918
+ <span class="value">{info['Lifespan']}</span>
919
+ </div>
920
+ </div>
921
+ </div>
922
+ <div class="description-section">
923
+ <h3 class="subsection-title">
924
+ <span class="icon">📝</span> Description
925
+ </h3>
926
+ <p class="description-text">{info.get('Description', '')}</p>
927
+ </div>
928
+ <div class="noise-section">
929
+ <h3 class="section-header">
930
+ <span class="icon">🔊</span> Noise Behavior
931
+ <span class="tooltip">
932
+ <span class="tooltip-icon">ⓘ</span>
933
+ <span class="tooltip-text">
934
+ <strong>Noise Behavior:</strong><br>
935
+ • Typical vocalization patterns<br>
936
+ • Common triggers and frequency<br>
937
+ • Based on breed characteristics
938
+ </span>
939
+ </span>
940
+ </h3>
941
+ <div class="noise-info">
942
+ <div class="noise-details">
943
+ <h4 class="section-header">Typical noise characteristics:</h4>
944
+ <div class="characteristics-list">
945
+ <div class="list-item">Moderate to high barker</div>
946
+ <div class="list-item">Alert watch dog</div>
947
+ <div class="list-item">Attention-seeking barks</div>
948
+ <div class="list-item">Social vocalizations</div>
949
+ </div>
950
+ <div class="noise-level-display">
951
+ <h4 class="section-header">Noise level:</h4>
952
+ <div class="level-indicator">
953
+ <span class="level-text">Moderate-High</span>
954
+ <div class="level-bars">
955
+ <span class="bar"></span>
956
+ <span class="bar"></span>
957
+ <span class="bar"></span>
958
+ </div>
959
+ </div>
960
+ </div>
961
+ <h4 class="section-header">Barking triggers:</h4>
962
+ <div class="triggers-list">
963
+ <div class="list-item">Separation anxiety</div>
964
+ <div class="list-item">Attention needs</div>
965
+ <div class="list-item">Strange noises</div>
966
+ <div class="list-item">Excitement</div>
967
+ </div>
968
+ </div>
969
+ <div class="noise-disclaimer">
970
+ <p class="disclaimer-text source-text">Source: Compiled from various breed behavior resources, 2024</p>
971
+ <p class="disclaimer-text">Individual dogs may vary in their vocalization patterns.</p>
972
+ <p class="disclaimer-text">Training can significantly influence barking behavior.</p>
973
+ <p class="disclaimer-text">Environmental factors may affect noise levels.</p>
974
+ </div>
975
+ </div>
976
+ </div>
977
+ <div class="health-section">
978
+ <h3 class="section-header">
979
+ <span class="icon">🏥</span> Health Insights
980
+ <span class="tooltip">
981
+ <span class="tooltip-icon">ⓘ</span>
982
+ <span class="tooltip-text">
983
+ Health information is compiled from multiple sources including veterinary resources, breed guides, and international canine health databases.
984
+ Each dog is unique and may vary from these general guidelines.
985
+ </span>
986
+ </span>
987
+ </h3>
988
+ <div class="health-info">
989
+ <div class="health-details">
990
+ <div class="health-block">
991
+ <h4 class="section-header">Common breed-specific health considerations:</h4>
992
+ <div class="health-grid">
993
+ <div class="health-item">Patellar luxation</div>
994
+ <div class="health-item">Progressive retinal atrophy</div>
995
+ <div class="health-item">Von Willebrand's disease</div>
996
+ <div class="health-item">Open fontanel</div>
997
+ </div>
998
+ </div>
999
+ <div class="health-block">
1000
+ <h4 class="section-header">Recommended health screenings:</h4>
1001
+ <div class="health-grid">
1002
+ <div class="health-item screening">Patella evaluation</div>
1003
+ <div class="health-item screening">Eye examination</div>
1004
+ <div class="health-item screening">Blood clotting tests</div>
1005
+ <div class="health-item screening">Skull development monitoring</div>
1006
+ </div>
1007
+ </div>
1008
+ </div>
1009
+ <div class="health-disclaimer">
1010
+ <p class="disclaimer-text source-text">Source: Compiled from various veterinary and breed information resources, 2024</p>
1011
+ <p class="disclaimer-text">This information is for reference only and based on breed tendencies.</p>
1012
+ <p class="disclaimer-text">Each dog is unique and may not develop any or all of these conditions.</p>
1013
+ <p class="disclaimer-text">Always consult with qualified veterinarians for professional advice.</p>
1014
+ </div>
1015
+ </div>
1016
+ </div>
1017
+ <div class="action-section">
1018
+ <a href="https://www.akc.org/dog-breeds/{breed.lower().replace('_', '-')}/"
1019
+ target="_blank"
1020
+ class="akc-button">
1021
+ <span class="icon">🌐</span>
1022
+ Learn more about {breed.replace('_', ' ')} on AKC website
1023
+ </a>
1024
+ </div>
1025
+ """
score_calibrator.py ADDED
@@ -0,0 +1,477 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ from typing import List, Dict, Tuple, Any, Optional
3
+ from dataclasses import dataclass, field
4
+ import traceback
5
+ from scipy import stats
6
+
7
+ @dataclass
8
+ class CalibrationResult:
9
+ """校準結果結構"""
10
+ original_scores: List[float]
11
+ calibrated_scores: List[float]
12
+ score_mapping: Dict[str, float] # breed -> calibrated_score
13
+ calibration_method: str
14
+ distribution_stats: Dict[str, float]
15
+ quality_metrics: Dict[str, float] = field(default_factory=dict)
16
+
17
+ @dataclass
18
+ class ScoreDistribution:
19
+ """分數分布統計"""
20
+ mean: float
21
+ std: float
22
+ min_score: float
23
+ max_score: float
24
+ percentile_5: float
25
+ percentile_95: float
26
+ compression_ratio: float # 分數壓縮比率
27
+ effective_range: float # 有效分數範圍
28
+
29
+ class ScoreCalibrator:
30
+ """
31
+ 動態分數校準系統
32
+ 解決分數壓縮問題並保持相對排名
33
+ """
34
+
35
+ def __init__(self):
36
+ """初始化校準器"""
37
+ self.calibration_methods = {
38
+ 'dynamic_range_mapping': self._dynamic_range_mapping,
39
+ 'percentile_stretching': self._percentile_stretching,
40
+ 'gaussian_normalization': self._gaussian_normalization,
41
+ 'sigmoid_transformation': self._sigmoid_transformation
42
+ }
43
+ self.quality_thresholds = {
44
+ 'min_effective_range': 0.3, # 最小有效分數範圍
45
+ 'max_compression_ratio': 0.2, # 最大允許壓縮比率
46
+ 'target_distribution_range': (0.45, 0.95) # 目標分布範圍
47
+ }
48
+
49
+ def calibrate_scores(self, breed_scores: List[Tuple[str, float]],
50
+ method: str = 'auto') -> CalibrationResult:
51
+ """
52
+ 校準品種分數
53
+
54
+ Args:
55
+ breed_scores: (breed_name, score) 元組列表
56
+ method: 校準方法 ('auto', 'dynamic_range_mapping', 'percentile_stretching', etc.)
57
+
58
+ Returns:
59
+ CalibrationResult: 校準結果
60
+ """
61
+ try:
62
+ if not breed_scores:
63
+ return CalibrationResult(
64
+ original_scores=[],
65
+ calibrated_scores=[],
66
+ score_mapping={},
67
+ calibration_method='none',
68
+ distribution_stats={}
69
+ )
70
+
71
+ # 提取分數和品種名稱
72
+ breeds = [item[0] for item in breed_scores]
73
+ original_scores = [item[1] for item in breed_scores]
74
+
75
+ # 分析原始分數分布
76
+ distribution = self._analyze_score_distribution(original_scores)
77
+
78
+ # 選擇校準方法
79
+ if method == 'auto':
80
+ method = self._select_calibration_method(distribution)
81
+
82
+ # 應用校準
83
+ calibration_func = self.calibration_methods.get(method, self._dynamic_range_mapping)
84
+ calibrated_scores = calibration_func(original_scores, distribution)
85
+
86
+ # 保持排名一致性
87
+ calibrated_scores = self._preserve_ranking(original_scores, calibrated_scores)
88
+
89
+ # 建立分數映射
90
+ score_mapping = dict(zip(breeds, calibrated_scores))
91
+
92
+ # 計算品質指標
93
+ quality_metrics = self._calculate_quality_metrics(
94
+ original_scores, calibrated_scores, distribution
95
+ )
96
+
97
+ return CalibrationResult(
98
+ original_scores=original_scores,
99
+ calibrated_scores=calibrated_scores,
100
+ score_mapping=score_mapping,
101
+ calibration_method=method,
102
+ distribution_stats=self._distribution_to_dict(distribution),
103
+ quality_metrics=quality_metrics
104
+ )
105
+
106
+ except Exception as e:
107
+ print(f"Error calibrating scores: {str(e)}")
108
+ print(traceback.format_exc())
109
+ # 回傳原始分數作為降級方案
110
+ breeds = [item[0] for item in breed_scores]
111
+ original_scores = [item[1] for item in breed_scores]
112
+ return CalibrationResult(
113
+ original_scores=original_scores,
114
+ calibrated_scores=original_scores,
115
+ score_mapping=dict(zip(breeds, original_scores)),
116
+ calibration_method='fallback',
117
+ distribution_stats={}
118
+ )
119
+
120
+ def _analyze_score_distribution(self, scores: List[float]) -> ScoreDistribution:
121
+ """分析分數分布"""
122
+ try:
123
+ scores_array = np.array(scores)
124
+
125
+ # 基本統計
126
+ mean_score = np.mean(scores_array)
127
+ std_score = np.std(scores_array)
128
+ min_score = np.min(scores_array)
129
+ max_score = np.max(scores_array)
130
+
131
+ # 百分位數
132
+ percentile_5 = np.percentile(scores_array, 5)
133
+ percentile_95 = np.percentile(scores_array, 95)
134
+
135
+ # 壓縮比率和有效範圍
136
+ full_range = max_score - min_score
137
+ effective_range = percentile_95 - percentile_5
138
+ compression_ratio = 1.0 - (effective_range / 1.0) if full_range > 0 else 0.0
139
+
140
+ return ScoreDistribution(
141
+ mean=mean_score,
142
+ std=std_score,
143
+ min_score=min_score,
144
+ max_score=max_score,
145
+ percentile_5=percentile_5,
146
+ percentile_95=percentile_95,
147
+ compression_ratio=compression_ratio,
148
+ effective_range=effective_range
149
+ )
150
+
151
+ except Exception as e:
152
+ print(f"Error analyzing score distribution: {str(e)}")
153
+ # 返回預設分布
154
+ return ScoreDistribution(
155
+ mean=0.5, std=0.1, min_score=0.0, max_score=1.0,
156
+ percentile_5=0.4, percentile_95=0.6,
157
+ compression_ratio=0.6, effective_range=0.2
158
+ )
159
+
160
+ def _select_calibration_method(self, distribution: ScoreDistribution) -> str:
161
+ """根據分布特性選擇校準方法"""
162
+ # 高度壓縮的分數需要強力展開
163
+ if distribution.compression_ratio > 0.8:
164
+ return 'percentile_stretching'
165
+
166
+ # 中等壓縮使用動態範圍映射
167
+ elif distribution.compression_ratio > 0.5:
168
+ return 'dynamic_range_mapping'
169
+
170
+ # 分數集中在中間使用 sigmoid 轉換
171
+ elif 0.4 <= distribution.mean <= 0.6 and distribution.std < 0.1:
172
+ return 'sigmoid_transformation'
173
+
174
+ # 其他情況使用高斯正規化
175
+ else:
176
+ return 'gaussian_normalization'
177
+
178
+ def _dynamic_range_mapping(self, scores: List[float],
179
+ distribution: ScoreDistribution) -> List[float]:
180
+ """動態範圍映射校準"""
181
+ try:
182
+ scores_array = np.array(scores)
183
+
184
+ # 使用5%和95%百分位數作為邊界
185
+ lower_bound = distribution.percentile_5
186
+ upper_bound = distribution.percentile_95
187
+
188
+ # 避免除零
189
+ if upper_bound - lower_bound < 0.001:
190
+ upper_bound = distribution.max_score
191
+ lower_bound = distribution.min_score
192
+ if upper_bound - lower_bound < 0.001:
193
+ return scores # 所有分數相同,無需校準
194
+
195
+ # 映射到目標範圍 [0.45, 0.95]
196
+ target_min, target_max = self.quality_thresholds['target_distribution_range']
197
+
198
+ # 線性映射
199
+ normalized = (scores_array - lower_bound) / (upper_bound - lower_bound)
200
+ normalized = np.clip(normalized, 0, 1) # 限制在 [0,1] 範圍
201
+ calibrated = target_min + normalized * (target_max - target_min)
202
+
203
+ return calibrated.tolist()
204
+
205
+ except Exception as e:
206
+ print(f"Error in dynamic range mapping: {str(e)}")
207
+ return scores
208
+
209
+ def _percentile_stretching(self, scores: List[float],
210
+ distribution: ScoreDistribution) -> List[float]:
211
+ """百分位數拉伸校準"""
212
+ try:
213
+ scores_array = np.array(scores)
214
+
215
+ # 計算百分位數排名
216
+ percentile_ranks = stats.rankdata(scores_array, method='average') / len(scores_array)
217
+
218
+ # 使用平方根轉換來增強差異
219
+ stretched_ranks = np.sqrt(percentile_ranks)
220
+
221
+ # 映射到目標範圍
222
+ target_min, target_max = self.quality_thresholds['target_distribution_range']
223
+ calibrated = target_min + stretched_ranks * (target_max - target_min)
224
+
225
+ return calibrated.tolist()
226
+
227
+ except Exception as e:
228
+ print(f"Error in percentile stretching: {str(e)}")
229
+ return self._dynamic_range_mapping(scores, distribution)
230
+
231
+ def _gaussian_normalization(self, scores: List[float],
232
+ distribution: ScoreDistribution) -> List[float]:
233
+ """高斯正規化校準"""
234
+ try:
235
+ scores_array = np.array(scores)
236
+
237
+ # Z-score 正規化
238
+ if distribution.std > 0:
239
+ z_scores = (scores_array - distribution.mean) / distribution.std
240
+ # 限制 Z-scores 在合理範圍內
241
+ z_scores = np.clip(z_scores, -3, 3)
242
+ else:
243
+ z_scores = np.zeros_like(scores_array)
244
+
245
+ # 轉換到目標範圍
246
+ target_min, target_max = self.quality_thresholds['target_distribution_range']
247
+ target_mean = (target_min + target_max) / 2
248
+ target_std = (target_max - target_min) / 6 # 3-sigma 範圍
249
+
250
+ calibrated = target_mean + z_scores * target_std
251
+ calibrated = np.clip(calibrated, target_min, target_max)
252
+
253
+ return calibrated.tolist()
254
+
255
+ except Exception as e:
256
+ print(f"Error in gaussian normalization: {str(e)}")
257
+ return self._dynamic_range_mapping(scores, distribution)
258
+
259
+ def _sigmoid_transformation(self, scores: List[float],
260
+ distribution: ScoreDistribution) -> List[float]:
261
+ """Sigmoid 轉換校準"""
262
+ try:
263
+ scores_array = np.array(scores)
264
+
265
+ # 中心化分數
266
+ centered = scores_array - distribution.mean
267
+
268
+ # Sigmoid 轉換 (增強中等分數的差異)
269
+ sigmoid_factor = 10.0 # 控制 sigmoid 的陡峭程度
270
+ transformed = 1 / (1 + np.exp(-sigmoid_factor * centered))
271
+
272
+ # 映射到目標範圍
273
+ target_min, target_max = self.quality_thresholds['target_distribution_range']
274
+ calibrated = target_min + transformed * (target_max - target_min)
275
+
276
+ return calibrated.tolist()
277
+
278
+ except Exception as e:
279
+ print(f"Error in sigmoid transformation: {str(e)}")
280
+ return self._dynamic_range_mapping(scores, distribution)
281
+
282
+ def _preserve_ranking(self, original_scores: List[float],
283
+ calibrated_scores: List[float]) -> List[float]:
284
+ """確保校準後的分數保持原始排名"""
285
+ try:
286
+ # 獲取原始排名
287
+ original_ranks = stats.rankdata([-score for score in original_scores], method='ordinal')
288
+
289
+ # 獲取校準後的排名
290
+ calibrated_with_ranks = list(zip(calibrated_scores, original_ranks))
291
+
292
+ # 按原始排名排序校準後的分數
293
+ calibrated_with_ranks.sort(key=lambda x: x[1])
294
+
295
+ # 重新分配分數以保持排名但使用校準後的分布
296
+ sorted_calibrated = sorted(calibrated_scores, reverse=True)
297
+
298
+ # 建立新的分數列表
299
+ preserved_scores = [0.0] * len(original_scores)
300
+ for i, (_, original_rank) in enumerate(calibrated_with_ranks):
301
+ # 找到原始位置
302
+ original_index = original_ranks.tolist().index(original_rank)
303
+ preserved_scores[original_index] = sorted_calibrated[i]
304
+
305
+ return preserved_scores
306
+
307
+ except Exception as e:
308
+ print(f"Error preserving ranking: {str(e)}")
309
+ return calibrated_scores
310
+
311
+ def _calculate_quality_metrics(self, original_scores: List[float],
312
+ calibrated_scores: List[float],
313
+ distribution: ScoreDistribution) -> Dict[str, float]:
314
+ """計算校準品質指標"""
315
+ try:
316
+ original_array = np.array(original_scores)
317
+ calibrated_array = np.array(calibrated_scores)
318
+
319
+ # 範圍改善
320
+ original_range = np.max(original_array) - np.min(original_array)
321
+ calibrated_range = np.max(calibrated_array) - np.min(calibrated_array)
322
+ range_improvement = calibrated_range / max(0.001, original_range)
323
+
324
+ # 分離度改善 (相鄰分數間的平均差異)
325
+ original_sorted = np.sort(original_array)
326
+ calibrated_sorted = np.sort(calibrated_array)
327
+
328
+ original_separation = np.mean(np.diff(original_sorted)) if len(original_sorted) > 1 else 0
329
+ calibrated_separation = np.mean(np.diff(calibrated_sorted)) if len(calibrated_sorted) > 1 else 0
330
+
331
+ separation_improvement = (calibrated_separation / max(0.001, original_separation)
332
+ if original_separation > 0 else 1.0)
333
+
334
+ # 排名保持度 (Spearman 相關係數)
335
+ if len(original_scores) > 1:
336
+ rank_correlation, _ = stats.spearmanr(original_scores, calibrated_scores)
337
+ rank_correlation = abs(rank_correlation) if not np.isnan(rank_correlation) else 1.0
338
+ else:
339
+ rank_correlation = 1.0
340
+
341
+ # 分布品質
342
+ calibrated_std = np.std(calibrated_array)
343
+ distribution_quality = min(1.0, calibrated_std * 2) # 標準差越大品質越好(在合理範圍內)
344
+
345
+ return {
346
+ 'range_improvement': range_improvement,
347
+ 'separation_improvement': separation_improvement,
348
+ 'rank_preservation': rank_correlation,
349
+ 'distribution_quality': distribution_quality,
350
+ 'effective_range_achieved': calibrated_range,
351
+ 'compression_reduction': max(0, distribution.compression_ratio -
352
+ (1.0 - calibrated_range))
353
+ }
354
+
355
+ except Exception as e:
356
+ print(f"Error calculating quality metrics: {str(e)}")
357
+ return {'error': str(e)}
358
+
359
+ def _distribution_to_dict(self, distribution: ScoreDistribution) -> Dict[str, float]:
360
+ """將分布統計轉換為字典"""
361
+ return {
362
+ 'mean': distribution.mean,
363
+ 'std': distribution.std,
364
+ 'min_score': distribution.min_score,
365
+ 'max_score': distribution.max_score,
366
+ 'percentile_5': distribution.percentile_5,
367
+ 'percentile_95': distribution.percentile_95,
368
+ 'compression_ratio': distribution.compression_ratio,
369
+ 'effective_range': distribution.effective_range
370
+ }
371
+
372
+ def apply_tie_breaking(self, breed_scores: List[Tuple[str, float]]) -> List[Tuple[str, float]]:
373
+ """應用確定性的打破平手機制"""
374
+ try:
375
+ # 按分數分組
376
+ score_groups = {}
377
+ for breed, score in breed_scores:
378
+ rounded_score = round(score, 6) # 避免浮點數精度問題
379
+ if rounded_score not in score_groups:
380
+ score_groups[rounded_score] = []
381
+ score_groups[rounded_score].append((breed, score))
382
+
383
+ # 處理每個分數組
384
+ result = []
385
+ for rounded_score in sorted(score_groups.keys(), reverse=True):
386
+ group = score_groups[rounded_score]
387
+
388
+ if len(group) == 1:
389
+ result.extend(group)
390
+ else:
391
+ # 按品種名稱字母順序打破平手
392
+ sorted_group = sorted(group, key=lambda x: x[0])
393
+
394
+ # 為平手的品種分配微小的分數差異
395
+ for i, (breed, original_score) in enumerate(sorted_group):
396
+ adjusted_score = original_score - (i * 0.0001)
397
+ result.append((breed, adjusted_score))
398
+
399
+ return result
400
+
401
+ except Exception as e:
402
+ print(f"Error in tie breaking: {str(e)}")
403
+ return breed_scores
404
+
405
+ def get_calibration_summary(self, result: CalibrationResult) -> Dict[str, Any]:
406
+ """獲取校準摘要資訊"""
407
+ try:
408
+ summary = {
409
+ 'method_used': result.calibration_method,
410
+ 'breeds_processed': len(result.original_scores),
411
+ 'score_range_before': {
412
+ 'min': min(result.original_scores) if result.original_scores else 0,
413
+ 'max': max(result.original_scores) if result.original_scores else 0,
414
+ 'range': (max(result.original_scores) - min(result.original_scores))
415
+ if result.original_scores else 0
416
+ },
417
+ 'score_range_after': {
418
+ 'min': min(result.calibrated_scores) if result.calibrated_scores else 0,
419
+ 'max': max(result.calibrated_scores) if result.calibrated_scores else 0,
420
+ 'range': (max(result.calibrated_scores) - min(result.calibrated_scores))
421
+ if result.calibrated_scores else 0
422
+ },
423
+ 'distribution_stats': result.distribution_stats,
424
+ 'quality_metrics': result.quality_metrics,
425
+ 'improvement_summary': {
426
+ 'range_expanded': result.quality_metrics.get('range_improvement', 1.0) > 1.1,
427
+ 'separation_improved': result.quality_metrics.get('separation_improvement', 1.0) > 1.1,
428
+ 'ranking_preserved': result.quality_metrics.get('rank_preservation', 1.0) > 0.95
429
+ }
430
+ }
431
+
432
+ return summary
433
+
434
+ except Exception as e:
435
+ print(f"Error generating calibration summary: {str(e)}")
436
+ return {'error': str(e)}
437
+
438
+ def calibrate_breed_scores(breed_scores: List[Tuple[str, float]],
439
+ method: str = 'auto') -> CalibrationResult:
440
+ """
441
+ 便利函數:校準品種分數
442
+
443
+ Args:
444
+ breed_scores: (breed_name, score) 元組列表
445
+ method: 校準方法
446
+
447
+ Returns:
448
+ CalibrationResult: 校準結果
449
+ """
450
+ calibrator = ScoreCalibrator()
451
+ return calibrator.calibrate_scores(breed_scores, method)
452
+
453
+ def get_calibrated_rankings(breed_scores: List[Tuple[str, float]],
454
+ method: str = 'auto') -> List[Tuple[str, float, int]]:
455
+ """
456
+ 便利函數:獲取校準後的排名
457
+
458
+ Args:
459
+ breed_scores: (breed_name, score) 元組列表
460
+ method: 校準方法
461
+
462
+ Returns:
463
+ List[Tuple[str, float, int]]: (breed_name, calibrated_score, rank) 列表
464
+ """
465
+ calibrator = ScoreCalibrator()
466
+ result = calibrator.calibrate_scores(breed_scores, method)
467
+
468
+ # 打破平手機制
469
+ calibrated_with_breed = [(breed, result.score_mapping[breed]) for breed in result.score_mapping]
470
+ calibrated_with_tie_breaking = calibrator.apply_tie_breaking(calibrated_with_breed)
471
+
472
+ # 添加排名
473
+ ranked_results = []
474
+ for rank, (breed, score) in enumerate(calibrated_with_tie_breaking, 1):
475
+ ranked_results.append((breed, score, rank))
476
+
477
+ return ranked_results
score_integration_manager.py ADDED
@@ -0,0 +1,805 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ import traceback
3
+ from typing import Dict, Any, List
4
+ from dataclasses import dataclass
5
+
6
+ @dataclass
7
+ class UserPreferences:
8
+ """使用者偏好設定的資料結構"""
9
+ living_space: str # "apartment", "house_small", "house_large"
10
+ yard_access: str # "no_yard", "shared_yard", "private_yard"
11
+ exercise_time: int # minutes per day
12
+ exercise_type: str # "light_walks", "moderate_activity", "active_training"
13
+ grooming_commitment: str # "low", "medium", "high"
14
+ experience_level: str # "beginner", "intermediate", "advanced"
15
+ time_availability: str # "limited", "moderate", "flexible"
16
+ has_children: bool
17
+ children_age: str # "toddler", "school_age", "teenager"
18
+ noise_tolerance: str # "low", "medium", "high"
19
+ space_for_play: bool
20
+ other_pets: bool
21
+ climate: str # "cold", "moderate", "hot"
22
+ health_sensitivity: str = "medium"
23
+ barking_acceptance: str = None
24
+ size_preference: str = "no_preference" # "no_preference", "small", "medium", "large", "giant"
25
+ training_commitment: str = "medium" # "low", "medium", "high" - 訓練投入程度
26
+ living_environment: str = "ground_floor" # "ground_floor", "with_elevator", "walk_up" - 居住環境細節
27
+
28
+ def __post_init__(self):
29
+ if self.barking_acceptance is None:
30
+ self.barking_acceptance = self.noise_tolerance
31
+
32
+
33
+ class ScoreIntegrationManager:
34
+ """
35
+ 評分整合管理器類別
36
+ 負責動態權重計算、評分整合和條件互動評估
37
+ """
38
+
39
+ def __init__(self):
40
+ """初始化評分整合管理器"""
41
+ pass
42
+
43
+ def apply_size_filter(self, breed_score: float, user_preference: str, breed_size: str) -> float:
44
+ """
45
+ 強過濾機制,基於用戶的體型偏好過濾品種
46
+
47
+ Parameters:
48
+ breed_score (float): 原始品種評分
49
+ user_preference (str): 用戶偏好的體型
50
+ breed_size (str): 品種的實際體型
51
+
52
+ Returns:
53
+ float: 過濾後的評分,如果體型不符合會返回 0
54
+ """
55
+ if user_preference == "no_preference":
56
+ return breed_score
57
+
58
+ # 標準化 size 字串以進行比較
59
+ breed_size = breed_size.lower().strip()
60
+ user_preference = user_preference.lower().strip()
61
+
62
+ # 特殊處理 "varies" 的情況
63
+ if breed_size == "varies":
64
+ return breed_score * 0.5 # 給予一個折扣係數,因為不確定性
65
+
66
+ # 如果用戶有明確體型偏好但品種不符合,返回 0
67
+ if user_preference != breed_size:
68
+ return 0
69
+
70
+ return breed_score
71
+
72
+ def calculate_breed_compatibility_score(self, scores: dict, user_prefs: UserPreferences, breed_info: dict) -> float:
73
+ """
74
+ 計算品種相容性總分,完整實現原始版本的複雜邏輯:
75
+ 1. 運動類型與時間的精確匹配
76
+ 2. 進階使用者的專業需求
77
+ 3. 空間利用的實際效果
78
+ 4. 條件組合的嚴格評估
79
+ """
80
+ def evaluate_perfect_conditions():
81
+ """
82
+ 評估條件匹配度,特別強化:
83
+ 1. 運動類型與時間的綜合評估
84
+ 2. 專業技能需求評估
85
+ 3. 品種特性評估
86
+ """
87
+ perfect_matches = {
88
+ 'size_match': 0,
89
+ 'exercise_match': 0,
90
+ 'experience_match': 0,
91
+ 'living_condition_match': 0,
92
+ 'breed_trait_match': 0 # 新增品種特性匹配度
93
+ }
94
+
95
+ # 第一部分:運動需求評估
96
+ def evaluate_exercise_compatibility():
97
+ """
98
+ 評估運動需求的匹配度,特別關注:
99
+ 1. 時間與強度的合理搭配
100
+ 2. 不同品種的運動特性
101
+ 3. 運動類型的適配性
102
+
103
+ 這個函數就像是一個體育教練,需要根據每個"運動員"(狗品種)的特點,
104
+ 為他們制定合適的訓練計劃。
105
+ """
106
+ exercise_needs = breed_info.get('Exercise Needs', 'MODERATE').upper()
107
+ exercise_time = user_prefs.exercise_time
108
+ exercise_type = user_prefs.exercise_type
109
+ temperament = breed_info.get('Temperament', '').lower()
110
+ description = breed_info.get('Description', '').lower()
111
+
112
+ # 定義更精確的品種運動特性
113
+ breed_exercise_patterns = {
114
+ 'sprint_type': { # 短跑型犬種,如 Whippet, Saluki
115
+ 'identifiers': ['fast', 'speed', 'sprint', 'racing', 'coursing', 'sight hound'],
116
+ 'ideal_exercise': {
117
+ 'active_training': 1.0, # 完美匹配高強度訓練
118
+ 'moderate_activity': 0.5, # 持續運動不是最佳選擇
119
+ 'light_walks': 0.3 # 輕度運動效果很差
120
+ },
121
+ 'time_ranges': {
122
+ 'ideal': (30, 60), # 最適合的運動時間範圍
123
+ 'acceptable': (20, 90), # 可以接受的時間範圍
124
+ 'penalty_start': 90 # 開始給予懲罰的時間點
125
+ },
126
+ 'penalty_rate': 0.8 # 超出範圍時的懲罰係數
127
+ },
128
+ 'endurance_type': { # 耐力型犬種,如 Border Collie
129
+ 'identifiers': ['herding', 'working', 'tireless', 'energetic', 'stamina', 'athletic'],
130
+ 'ideal_exercise': {
131
+ 'active_training': 0.9, # 高強度訓練很好
132
+ 'moderate_activity': 1.0, # 持續運動是最佳選擇
133
+ 'light_walks': 0.4 # 輕度運動不足
134
+ },
135
+ 'time_ranges': {
136
+ 'ideal': (90, 180), # 需要較長的運動時間
137
+ 'acceptable': (60, 180),
138
+ 'penalty_start': 60 # 運動時間過短會受罰
139
+ },
140
+ 'penalty_rate': 0.7
141
+ },
142
+ 'moderate_type': { # 一般活動型犬種,如 Labrador
143
+ 'identifiers': ['friendly', 'playful', 'adaptable', 'versatile', 'companion'],
144
+ 'ideal_exercise': {
145
+ 'active_training': 0.8,
146
+ 'moderate_activity': 1.0,
147
+ 'light_walks': 0.6
148
+ },
149
+ 'time_ranges': {
150
+ 'ideal': (60, 120),
151
+ 'acceptable': (45, 150),
152
+ 'penalty_start': 150
153
+ },
154
+ 'penalty_rate': 0.6
155
+ }
156
+ }
157
+
158
+ def determine_breed_type():
159
+ """改進品種運動類型的判斷,更精確識別工作犬"""
160
+ # 優先檢查特殊運動類型的標識符
161
+ for breed_type, pattern in breed_exercise_patterns.items():
162
+ if any(identifier in temperament or identifier in description
163
+ for identifier in pattern['identifiers']):
164
+ return breed_type
165
+
166
+ # 改進:根據運動需求和工作犬特徵進行更細緻的判斷
167
+ if (exercise_needs in ['VERY HIGH', 'HIGH'] or
168
+ any(trait in temperament.lower() for trait in
169
+ ['herding', 'working', 'intelligent', 'athletic', 'tireless'])):
170
+ if user_prefs.experience_level == 'advanced':
171
+ return 'endurance_type' # 優先判定為耐力型
172
+ elif exercise_needs == 'LOW':
173
+ return 'moderate_type'
174
+
175
+ return 'moderate_type'
176
+
177
+ def calculate_time_match(pattern):
178
+ """
179
+ 計算運動時間的匹配度。
180
+ 這就像在判斷運動時間是否符合訓練計劃。
181
+ """
182
+ ideal_min, ideal_max = pattern['time_ranges']['ideal']
183
+ accept_min, accept_max = pattern['time_ranges']['acceptable']
184
+ penalty_start = pattern['time_ranges']['penalty_start']
185
+
186
+ # 在理想範圍內
187
+ if ideal_min <= exercise_time <= ideal_max:
188
+ return 1.0
189
+
190
+ # 超出可接受範圍的嚴格懲罰
191
+ elif exercise_time < accept_min:
192
+ deficit = accept_min - exercise_time
193
+ return max(0.2, 1 - (deficit / accept_min) * 1.2)
194
+ elif exercise_time > accept_max:
195
+ excess = exercise_time - penalty_start
196
+ penalty = min(0.8, (excess / penalty_start) * pattern['penalty_rate'])
197
+ return max(0.2, 1 - penalty)
198
+
199
+ # 在可接受範圍但不在理想範圍
200
+ else:
201
+ if exercise_time < ideal_min:
202
+ progress = (exercise_time - accept_min) / (ideal_min - accept_min)
203
+ return 0.6 + (0.4 * progress)
204
+ else:
205
+ remaining = (accept_max - exercise_time) / (accept_max - ideal_max)
206
+ return 0.6 + (0.4 * remaining)
207
+
208
+ def apply_special_adjustments(time_score, type_score, breed_type, pattern):
209
+ """
210
+ 處理特殊情況,確保運動方式真正符合品種需求。
211
+ 特別加強:
212
+ 1. 短跑型犬種的長時間運動懲罰
213
+ 2. 耐力型犬種的獎勵機制
214
+ 3. 運動類型匹配的重要性
215
+ """
216
+ # 短跑型品種的特殊處理
217
+ if breed_type == 'sprint_type':
218
+ if exercise_time > pattern['time_ranges']['penalty_start']:
219
+ # 加重長時間運動的懲罰
220
+ penalty_factor = min(0.8, (exercise_time - pattern['time_ranges']['penalty_start']) / 60)
221
+ time_score *= max(0.3, 1 - penalty_factor) # 最低降到0.3
222
+ # 運動類型不適合時的額外懲罰
223
+ if exercise_type != 'active_training':
224
+ type_score *= 0.3 # 更嚴重的懲罰
225
+
226
+ # 耐力型品種的特殊處理
227
+ elif breed_type == 'endurance_type':
228
+ if exercise_time < pattern['time_ranges']['penalty_start']:
229
+ time_score *= 0.5 # 維持運動不足的懲罰
230
+ elif exercise_time >= 150: # 新增:高運動量獎勵
231
+ if exercise_type in ['active_training', 'moderate_activity']:
232
+ time_bonus = min(0.3, (exercise_time - 150) / 150)
233
+ time_score = min(1.0, time_score * (1 + time_bonus))
234
+ type_score = min(1.0, type_score * 1.2)
235
+
236
+ # 運動強度不足的懲罰
237
+ if exercise_type == 'light_walks':
238
+ if exercise_time > 90:
239
+ type_score *= 0.4 # 加重懲罰
240
+ else:
241
+ type_score *= 0.5
242
+
243
+ return time_score, type_score
244
+
245
+ # 執行評估流程
246
+ breed_type = determine_breed_type()
247
+ pattern = breed_exercise_patterns[breed_type]
248
+
249
+ # 計算基礎分數
250
+ time_score = calculate_time_match(pattern)
251
+ type_score = pattern['ideal_exercise'].get(exercise_type, 0.5)
252
+
253
+ # 應用特殊調整
254
+ time_score, type_score = apply_special_adjustments(time_score, type_score, breed_type, pattern)
255
+
256
+ # 根據品種類型決定最終權重
257
+ if breed_type == 'sprint_type':
258
+ if exercise_time > pattern['time_ranges']['penalty_start']:
259
+ # 超時時更重視運動類型的匹配度
260
+ return (time_score * 0.3) + (type_score * 0.7)
261
+ else:
262
+ return (time_score * 0.5) + (type_score * 0.5)
263
+ elif breed_type == 'endurance_type':
264
+ if exercise_time < pattern['time_ranges']['penalty_start']:
265
+ # 時間不足時更重視時間因素
266
+ return (time_score * 0.7) + (type_score * 0.3)
267
+ else:
268
+ return (time_score * 0.6) + (type_score * 0.4)
269
+ else:
270
+ return (time_score * 0.5) + (type_score * 0.5)
271
+
272
+ # 第二部分:專業技能需求評估
273
+ def evaluate_expertise_requirements():
274
+ care_level = breed_info.get('Care Level', 'MODERATE').upper()
275
+ temperament = breed_info.get('Temperament', '').lower()
276
+
277
+ # 定義專業技能要求
278
+ expertise_requirements = {
279
+ 'training_complexity': {
280
+ 'VERY HIGH': {'beginner': 0.2, 'intermediate': 0.5, 'advanced': 0.9},
281
+ 'HIGH': {'beginner': 0.3, 'intermediate': 0.7, 'advanced': 1.0},
282
+ 'MODERATE': {'beginner': 0.6, 'intermediate': 0.9, 'advanced': 1.0},
283
+ 'LOW': {'beginner': 0.9, 'intermediate': 0.95, 'advanced': 0.9}
284
+ },
285
+ 'special_traits': {
286
+ 'working': 0.2, # 工作犬需要額外技能
287
+ 'herding': 0.2, # 牧羊犬需要特殊訓練
288
+ 'intelligent': 0.15,# 高智商犬種需要心智刺激
289
+ 'independent': 0.15,# 獨立性強的需要特殊處理
290
+ 'protective': 0.1 # 護衛犬需要適當訓練
291
+ }
292
+ }
293
+
294
+ # 基礎分數
295
+ base_score = expertise_requirements['training_complexity'][care_level][user_prefs.experience_level]
296
+
297
+ # 特殊特徵評估
298
+ trait_penalty = 0
299
+ for trait, penalty in expertise_requirements['special_traits'].items():
300
+ if trait in temperament:
301
+ if user_prefs.experience_level == 'beginner':
302
+ trait_penalty += penalty
303
+ elif user_prefs.experience_level == 'advanced':
304
+ trait_penalty -= penalty * 0.5 # 專家反而因應對特殊特徵而加分
305
+
306
+ return max(0.2, min(1.0, base_score - trait_penalty))
307
+
308
+ def evaluate_living_conditions() -> float:
309
+ """
310
+ 評估生活環境適配性,特別加強:
311
+ 1. 降低對大型犬的過度懲罰
312
+ 2. 增加品種特性評估
313
+ 3. 提升對適應性的重視度
314
+ """
315
+ size = breed_info['Size']
316
+ exercise_needs = breed_info.get('Exercise Needs', 'MODERATE').upper()
317
+ temperament = breed_info.get('Temperament', '').lower()
318
+ description = breed_info.get('Description', '').lower()
319
+
320
+ # 重新定義空間需求矩陣,降低對大型犬的懲罰
321
+ space_requirements = {
322
+ 'apartment': {
323
+ 'Small': 1.0,
324
+ 'Medium': 0.8,
325
+ 'Large': 0.7,
326
+ 'Giant': 0.6
327
+ },
328
+ 'house_small': {
329
+ 'Small': 0.9,
330
+ 'Medium': 1.0,
331
+ 'Large': 0.8,
332
+ 'Giant': 0.7
333
+ },
334
+ 'house_large': {
335
+ 'Small': 0.8,
336
+ 'Medium': 0.9,
337
+ 'Large': 1.0,
338
+ 'Giant': 1.0
339
+ }
340
+ }
341
+
342
+ # 基礎空間分數
343
+ space_score = space_requirements.get(
344
+ user_prefs.living_space,
345
+ space_requirements['house_small']
346
+ )[size]
347
+
348
+ # 品種適應性評估
349
+ adaptability_bonus = 0
350
+ adaptable_traits = ['adaptable', 'calm', 'quiet', 'gentle', 'laid-back']
351
+ challenging_traits = ['hyperactive', 'restless', 'requires space']
352
+
353
+ # 計算適應性加分
354
+ if user_prefs.living_space == 'apartment':
355
+ for trait in adaptable_traits:
356
+ if trait in temperament or trait in description:
357
+ adaptability_bonus += 0.1
358
+
359
+ # 特別處理大型犬的適應性
360
+ if size in ['Large', 'Giant']:
361
+ apartment_friendly_traits = ['calm', 'gentle', 'quiet']
362
+ matched_traits = sum(1 for trait in apartment_friendly_traits
363
+ if trait in temperament or trait in description)
364
+ if matched_traits > 0:
365
+ adaptability_bonus += 0.15 * matched_traits
366
+
367
+ # 活動空間需求調整,更寬容的評估
368
+ if exercise_needs in ['HIGH', 'VERY HIGH']:
369
+ if user_prefs.living_space != 'house_large':
370
+ space_score *= 0.9 # 從0.8提升到0.9,降低懲罰
371
+
372
+ # 院子可用性評估,提供更合理的獎勵
373
+ yard_scores = {
374
+ 'no_yard': 0.85, # 從0.7提升到0.85
375
+ 'shared_yard': 0.92, # 從0.85提升到0.92
376
+ 'private_yard': 1.0
377
+ }
378
+ yard_multiplier = yard_scores.get(user_prefs.yard_access, 0.85)
379
+
380
+ # 根據體型調整院子重要性
381
+ if size in ['Large', 'Giant']:
382
+ yard_importance = 1.2
383
+ elif size == 'Medium':
384
+ yard_importance = 1.1
385
+ else:
386
+ yard_importance = 1.0
387
+
388
+ # 計算最終分數
389
+ final_score = space_score * (1 + adaptability_bonus)
390
+
391
+ # 應用院子影響
392
+ if user_prefs.yard_access != 'no_yard':
393
+ yard_bonus = (yard_multiplier - 1) * yard_importance
394
+ final_score = min(1.0, final_score + yard_bonus)
395
+
396
+ # 確保分數在合理範圍內,但提供更高的基礎分數
397
+ return max(0.4, min(1.0, final_score))
398
+
399
+ # 第四部分:品種特性評估
400
+ def evaluate_breed_traits():
401
+ temperament = breed_info.get('Temperament', '').lower()
402
+ description = breed_info.get('Description', '').lower()
403
+
404
+ trait_scores = []
405
+
406
+ # 評估性格特徵
407
+ if user_prefs.has_children:
408
+ if 'good with children' in description:
409
+ trait_scores.append(1.0)
410
+ elif 'patient' in temperament or 'gentle' in temperament:
411
+ trait_scores.append(0.8)
412
+ else:
413
+ trait_scores.append(0.5)
414
+
415
+ # 評估適應性
416
+ adaptability_keywords = ['adaptable', 'versatile', 'flexible']
417
+ if any(keyword in temperament for keyword in adaptability_keywords):
418
+ trait_scores.append(1.0)
419
+ else:
420
+ trait_scores.append(0.7)
421
+
422
+ return sum(trait_scores) / len(trait_scores) if trait_scores else 0.7
423
+
424
+ # 計算各項匹配分數
425
+ perfect_matches['exercise_match'] = evaluate_exercise_compatibility()
426
+ perfect_matches['experience_match'] = evaluate_expertise_requirements()
427
+ perfect_matches['living_condition_match'] = evaluate_living_conditions()
428
+ perfect_matches['size_match'] = evaluate_living_conditions() # 共用生活環境評估
429
+ perfect_matches['breed_trait_match'] = evaluate_breed_traits()
430
+
431
+ return perfect_matches
432
+
433
+ def calculate_weights() -> dict:
434
+ """
435
+ 動態計算評分權重,特別關注:
436
+ 1. 極端情況的權重調整
437
+ 2. 使用者條件的協同效應
438
+ 3. 品種特性的影響
439
+
440
+ Returns:
441
+ dict: 包含各評分項目權重的字典
442
+ """
443
+ # 定義基礎權重 - 提供更合理的起始分配
444
+ base_weights = {
445
+ 'space': 0.25, # 提升空間權重,因為這是最基本的需求
446
+ 'exercise': 0.25, # 運動需求同樣重要
447
+ 'experience': 0.20, # 保持經驗的重要性
448
+ 'grooming': 0.10, # 稍微降低美容需求的權重
449
+ 'noise': 0.10, # 維持噪音評估的權重
450
+ 'health': 0.10 # 維持健康評估的權重
451
+ }
452
+
453
+ def analyze_condition_extremity() -> dict:
454
+ """
455
+ 評估使用者條件的極端程度,這影響權重的動態調整。
456
+ 根據條件的極端程度返回相應的調整建議。
457
+ """
458
+ extremities = {}
459
+
460
+ # 運動時間評估 - 更細緻的分級
461
+ if user_prefs.exercise_time <= 30:
462
+ extremities['exercise'] = ('extremely_low', 0.8)
463
+ elif user_prefs.exercise_time <= 60:
464
+ extremities['exercise'] = ('low', 0.6)
465
+ elif user_prefs.exercise_time >= 180:
466
+ extremities['exercise'] = ('extremely_high', 0.8)
467
+ elif user_prefs.exercise_time >= 120:
468
+ extremities['exercise'] = ('high', 0.6)
469
+ else:
470
+ extremities['exercise'] = ('moderate', 0.3)
471
+
472
+ # 空間限制評估 - 更合理的空間評估
473
+ space_extremity = {
474
+ 'apartment': ('restricted', 0.7), # 從0.9降低到0.7,減少限制
475
+ 'house_small': ('moderate', 0.5),
476
+ 'house_large': ('spacious', 0.3)
477
+ }
478
+ extremities['space'] = space_extremity.get(user_prefs.living_space, ('moderate', 0.5))
479
+
480
+ # 經驗水平評估 - 保持原有的評估邏輯
481
+ experience_extremity = {
482
+ 'beginner': ('low', 0.7),
483
+ 'intermediate': ('moderate', 0.4),
484
+ 'advanced': ('high', 0.6)
485
+ }
486
+ extremities['experience'] = experience_extremity.get(user_prefs.experience_level, ('moderate', 0.5))
487
+
488
+ return extremities
489
+
490
+ def calculate_weight_adjustments(extremities: dict) -> dict:
491
+ """
492
+ 根據極端程度計算權重調整,特別注意條件組合的影響。
493
+ """
494
+ adjustments = {}
495
+ temperament = breed_info.get('Temperament', '').lower()
496
+ is_working_dog = any(trait in temperament
497
+ for trait in ['herding', 'working', 'intelligent', 'tireless'])
498
+
499
+ # 空間權重調整
500
+ if extremities['space'][0] == 'restricted':
501
+ if extremities['exercise'][0] in ['high', 'extremely_high']:
502
+ adjustments['space'] = 1.3
503
+ adjustments['exercise'] = 2.3
504
+ else:
505
+ adjustments['space'] = 1.6
506
+ adjustments['noise'] = 1.5
507
+
508
+ # 運動需求權重調整
509
+ if extremities['exercise'][0] in ['extremely_high', 'extremely_low']:
510
+ base_adjustment = 2.0
511
+ if extremities['exercise'][0] == 'extremely_high':
512
+ if is_working_dog:
513
+ base_adjustment = 2.3
514
+ adjustments['exercise'] = base_adjustment
515
+
516
+ # 經驗需求權重調整
517
+ if extremities['experience'][0] == 'low':
518
+ adjustments['experience'] = 1.8
519
+ if breed_info.get('Care Level') == 'HIGH':
520
+ adjustments['experience'] = 2.0
521
+ elif extremities['experience'][0] == 'high':
522
+ if is_working_dog:
523
+ adjustments['experience'] = 1.8 # 從2.5降低到1.8
524
+
525
+ # 特殊組合的處理
526
+ def adjust_for_combinations():
527
+ if (extremities['space'][0] == 'restricted' and
528
+ extremities['exercise'][0] in ['high', 'extremely_high']):
529
+ # 適度降低極端組合的影響
530
+ adjustments['space'] = adjustments.get('space', 1.0) * 1.2
531
+ adjustments['exercise'] = adjustments.get('exercise', 1.0) * 1.2
532
+
533
+ # 理想組合的獎勵
534
+ if (extremities['experience'][0] == 'high' and
535
+ extremities['space'][0] == 'spacious' and
536
+ extremities['exercise'][0] in ['high', 'extremely_high'] and
537
+ is_working_dog):
538
+ adjustments['exercise'] = adjustments.get('exercise', 1.0) * 1.3
539
+ adjustments['experience'] = adjustments.get('experience', 1.0) * 1.3
540
+
541
+ adjust_for_combinations()
542
+ return adjustments
543
+
544
+ # 獲取條件極端度
545
+ extremities = analyze_condition_extremity()
546
+
547
+ # 計算權重調整
548
+ weight_adjustments = calculate_weight_adjustments(extremities)
549
+
550
+ # 應用權重調整,確保總和為1
551
+ final_weights = base_weights.copy()
552
+ for key, adjustment in weight_adjustments.items():
553
+ if key in final_weights:
554
+ final_weights[key] *= adjustment
555
+
556
+ # 正規化權重
557
+ total_weight = sum(final_weights.values())
558
+ normalized_weights = {k: v/total_weight for k, v in final_weights.items()}
559
+
560
+ return normalized_weights
561
+
562
+ def calculate_base_score(scores: dict, weights: dict) -> float:
563
+ """
564
+ 計算基礎評分分數,採用更靈活的評分機制。
565
+
566
+ 這個函數使用了改進後的評分邏輯,主要關注:
567
+ 1. 降低關鍵指標的最低門檻,使系統更包容
568
+ 2. 引入非線性評分曲線,讓分數分布更合理
569
+ 3. 優化多重條件失敗的處理方式
570
+ 4. 加強對品種特性的考慮
571
+
572
+ Parameters:
573
+ scores: 包含各項評分的字典
574
+ weights: 包含各項權重的字典
575
+
576
+ Returns:
577
+ float: 0.2到1.0之間的基礎分數
578
+ """
579
+ # 重新定義關鍵指標閾值,提供更寬容的評分標準
580
+ critical_thresholds = {
581
+ 'space': 0.35,
582
+ 'exercise': 0.35,
583
+ 'experience': 0.5,
584
+ 'noise': 0.5
585
+ }
586
+
587
+ # 評估關鍵指標失敗情況
588
+ def evaluate_critical_failures() -> list:
589
+ """
590
+ 評估關鍵指標的失敗情況,但採用更寬容的標準。
591
+ 根據品種特性動態調整失敗判定。
592
+ """
593
+ failures = []
594
+ temperament = breed_info.get('Temperament', '').lower()
595
+
596
+ for metric, threshold in critical_thresholds.items():
597
+ if scores[metric] < threshold:
598
+ # 特殊情況處理:適應性強的品種可以有更低的空間要求
599
+ if metric == 'space' and any(trait in temperament
600
+ for trait in ['adaptable', 'calm', 'apartment']):
601
+ if scores[metric] >= threshold - 0.1:
602
+ continue
603
+
604
+ # 運動需求的特殊處理
605
+ elif metric == 'exercise':
606
+ exercise_needs = breed_info.get('Exercise Needs', 'MODERATE').upper()
607
+ if exercise_needs == 'LOW' and scores[metric] >= threshold - 0.1:
608
+ continue
609
+
610
+ failures.append((metric, scores[metric]))
611
+
612
+ return failures
613
+
614
+ # 計算基礎分數
615
+ def calculate_weighted_score() -> float:
616
+ """
617
+ 計算加權分數,使用非線性函數使分數分布更合理。
618
+ """
619
+ weighted_scores = []
620
+ for key, score in scores.items():
621
+ if key in weights:
622
+ # 使用sigmoid函數使分數曲線更平滑
623
+ adjusted_score = 1 / (1 + math.exp(-10 * (score - 0.5)))
624
+ weighted_scores.append(adjusted_score * weights[key])
625
+
626
+ return sum(weighted_scores)
627
+
628
+ # 處理臨界失敗情況
629
+ critical_failures = evaluate_critical_failures()
630
+ base_score = calculate_weighted_score()
631
+
632
+ if critical_failures:
633
+ # 分離空間和運動相關的懲罰
634
+ space_exercise_penalty = 0
635
+ other_penalty = 0
636
+
637
+ for metric, score in critical_failures:
638
+ if metric in ['space', 'exercise']:
639
+ # 降低空間和運動失敗的懲罰程度
640
+ penalty = (critical_thresholds[metric] - score) * 0.08
641
+ space_exercise_penalty += penalty
642
+ else:
643
+ # 其他失敗的懲罰保持較高
644
+ penalty = (critical_thresholds[metric] - score) * 0.20
645
+ other_penalty += penalty
646
+
647
+ # 計算總懲罰,但使用更溫和的方式
648
+ total_penalty = (space_exercise_penalty + other_penalty) / 2
649
+ base_score *= (1 - total_penalty)
650
+
651
+ # 多重失敗的處理更寬容
652
+ if len(critical_failures) > 1:
653
+ # 從0.98提升到0.99,降低多重失敗的疊加懲罰
654
+ base_score *= (0.99 ** (len(critical_failures) - 1))
655
+
656
+ # 品種特性加分
657
+ def apply_breed_bonus() -> float:
658
+ """
659
+ 根據品種特性提供額外加分,
660
+ 特別是對於在特定環境下表現良好的品種。
661
+ """
662
+ bonus = 0
663
+ temperament = breed_info.get('Temperament', '').lower()
664
+ description = breed_info.get('Description', '').lower()
665
+
666
+ # 適應性加分
667
+ adaptability_traits = ['adaptable', 'versatile', 'easy-going']
668
+ if any(trait in temperament for trait in adaptability_traits):
669
+ bonus += 0.05
670
+
671
+ # 公寓適應性加分
672
+ if user_prefs.living_space == 'apartment':
673
+ apartment_traits = ['calm', 'quiet', 'good for apartments']
674
+ if any(trait in temperament or trait in description for trait in apartment_traits):
675
+ bonus += 0.05
676
+
677
+ return min(0.1, bonus) # 限制最大加分
678
+
679
+ # 應用品種特性加分
680
+ breed_bonus = apply_breed_bonus()
681
+ base_score = min(1.0, base_score * (1 + breed_bonus))
682
+
683
+ # 確保最終分數在合理範圍內
684
+ return max(0.2, min(1.0, base_score))
685
+
686
+ def evaluate_condition_interactions(scores: dict) -> float:
687
+ """評估不同條件間的相互影響,更寬容地處理極端組合"""
688
+ interaction_penalty = 1.0
689
+
690
+ # 只保留最基本的經驗相關評估
691
+ if user_prefs.experience_level == 'beginner':
692
+ if breed_info.get('Care Level') == 'HIGH':
693
+ interaction_penalty *= 0.95
694
+
695
+ # 運動時間與類型的基本互動也降低懲罰程度
696
+ exercise_needs = breed_info.get('Exercise Needs', 'MODERATE').upper()
697
+ if exercise_needs == 'VERY HIGH' and user_prefs.exercise_type == 'light_walks':
698
+ interaction_penalty *= 0.95
699
+
700
+ return interaction_penalty
701
+
702
+ def calculate_adjusted_perfect_bonus(perfect_conditions: dict) -> float:
703
+ """計算完美匹配獎勵,但更注重條件的整體表現"""
704
+ bonus = 1.0
705
+
706
+ # 降低單項獎勵的影響力
707
+ bonus += 0.06 * perfect_conditions['size_match']
708
+ bonus += 0.06 * perfect_conditions['exercise_match']
709
+ bonus += 0.06 * perfect_conditions['experience_match']
710
+ bonus += 0.03 * perfect_conditions['living_condition_match']
711
+
712
+ # 如果有任何條件表現不佳,降低整體獎勵
713
+ low_scores = [score for score in perfect_conditions.values() if score < 0.6]
714
+ if low_scores:
715
+ bonus *= (0.85 ** len(low_scores))
716
+
717
+ # 確保獎勵不會過高
718
+ return min(1.25, bonus)
719
+
720
+ def apply_breed_specific_adjustments(score: float) -> float:
721
+ """根據品種特性進行最終調整"""
722
+ # 檢查是否存在極端不匹配的情況
723
+ exercise_mismatch = False
724
+ size_mismatch = False
725
+ experience_mismatch = False
726
+
727
+ # 運動需求極端不匹配
728
+ if breed_info.get('Exercise Needs', 'MODERATE').upper() == 'VERY HIGH':
729
+ if user_prefs.exercise_time < 90 or user_prefs.exercise_type == 'light_walks':
730
+ exercise_mismatch = True
731
+
732
+ # 體型與空間極端不匹配
733
+ if user_prefs.living_space == 'apartment' and breed_info['Size'] in ['Large', 'Giant']:
734
+ size_mismatch = True
735
+
736
+ # 經驗需求極端不匹配
737
+ if user_prefs.experience_level == 'beginner' and breed_info.get('Care Level') == 'HIGH':
738
+ experience_mismatch = True
739
+
740
+ # 根據不匹配的數量進行懲罰
741
+ mismatch_count = sum([exercise_mismatch, size_mismatch, experience_mismatch])
742
+ if mismatch_count > 0:
743
+ score *= (0.8 ** mismatch_count)
744
+
745
+ return score
746
+
747
+ # 計算動態權重
748
+ weights = calculate_weights()
749
+
750
+ # ���規化權重
751
+ total_weight = sum(weights.values())
752
+ normalized_weights = {k: v/total_weight for k, v in weights.items()}
753
+
754
+ # 計算基礎分數
755
+ base_score = calculate_base_score(scores, normalized_weights)
756
+
757
+ # 評估條件互動
758
+ interaction_multiplier = evaluate_condition_interactions(scores)
759
+
760
+ # 計算完美匹配獎勵
761
+ perfect_conditions = evaluate_perfect_conditions()
762
+ perfect_bonus = calculate_adjusted_perfect_bonus(perfect_conditions)
763
+
764
+ # 計算初步分數
765
+ preliminary_score = base_score * interaction_multiplier * perfect_bonus
766
+
767
+ # 應用品種特定調整
768
+ final_score = apply_breed_specific_adjustments(preliminary_score)
769
+
770
+ # 確保分數在合理範圍內,並降低最高可能分數
771
+ max_possible_score = 0.96 # 降低最高可能分數
772
+ min_possible_score = 0.3
773
+
774
+ return min(max_possible_score, max(min_possible_score, final_score))
775
+
776
+ def calculate_environmental_fit(self, breed_info: dict, user_prefs: UserPreferences) -> float:
777
+ """
778
+ 計算品種與環境的適應性加成
779
+
780
+ Args:
781
+ breed_info: 品種資訊
782
+ user_prefs: 使用者偏好
783
+
784
+ Returns:
785
+ float: 環境適應性加成分數
786
+ """
787
+ adaptability_score = 0.0
788
+ description = breed_info.get('Description', '').lower()
789
+ temperament = breed_info.get('Temperament', '').lower()
790
+
791
+ # 環境適應性評估
792
+ if user_prefs.living_space == 'apartment':
793
+ if 'adaptable' in temperament or 'apartment' in description:
794
+ adaptability_score += 0.1
795
+ if breed_info.get('Size') == 'Small':
796
+ adaptability_score += 0.05
797
+ elif user_prefs.living_space == 'house_large':
798
+ if 'active' in temperament or 'energetic' in description:
799
+ adaptability_score += 0.1
800
+
801
+ # 氣候適應性
802
+ if user_prefs.climate in description or user_prefs.climate in temperament:
803
+ adaptability_score += 0.05
804
+
805
+ return min(0.2, adaptability_score)
scoring_calculation_system.py CHANGED
The diff for this file is too large to render. See raw diff
 
semantic_breed_recommender.py ADDED
The diff for this file is too large to render. See raw diff
 
styles.py CHANGED
@@ -1,5 +1,128 @@
 
1
  def get_css_styles():
2
  return """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  .dog-info-card {
4
  margin: 0 0 20px 0;
5
  padding: 0;
@@ -234,7 +357,7 @@ def get_css_styles():
234
  }
235
 
236
  .breed-name {
237
- font-size: 1.2em !important; # 從 1.5em 改為 1.2em
238
  font-weight: bold;
239
  color: #2c3e50;
240
  flex-grow: 1;
@@ -1070,52 +1193,24 @@ def get_css_styles():
1070
  }
1071
 
1072
  @media (max-width: 768px) {
1073
- .info-cards {
1074
- grid-template-columns: 1fr !important; /* 在手機上改為單列 */
1075
- gap: 12px !important;
1076
- padding: 10px !important;
1077
- width: 100% !important;
1078
- box-sizing: border-box !important;
1079
- min-height: auto !important; /* 在手機上移除最小高度限制 */
1080
- height: auto !important; /* 允許高度自適應 */
1081
- padding: 12px !important; /* 稍微減少填充 */
1082
- }
1083
-
1084
- .info-card {
1085
- width: 100% !important;
1086
- margin: 0 !important;
1087
- padding: 12px !important;
1088
- min-height: auto !important; /* 移除最小高度限制 */
1089
- height: auto !important; /* 允許高度自適應 */
1090
- overflow: visible !important; /* 確保內容不被切斷 */
1091
  }
1092
 
1093
- .info-card .tooltip {
1094
- flex-wrap: wrap !important; /* 在手機版允許換行 */
1095
- }
1096
- .info-card span {
1097
- display: block !important; /* 確保文字完整顯示 */
1098
- overflow: visible !important;
1099
- }
1100
-
1101
- .tooltip {
1102
- width: 100% !important;
1103
- display: flex !important;
1104
- align-items: center !important;
1105
- gap: 8px !important;
1106
  }
1107
-
1108
- .tooltip-text {
1109
- left: auto !important;
1110
- right: 0 !important;
1111
- width: 200px !important;
1112
  }
1113
-
1114
- /* 確保所有文字可見 */
1115
- .label, .value {
1116
- overflow: visible !important;
1117
- white-space: normal !important;
1118
- word-wrap: break-word !important;
1119
  }
1120
  }
1121
 
 
1
+
2
  def get_css_styles():
3
  return """
4
+ /* SBERT Natural Language Recommendation Styles */
5
+ button#find-match-btn {
6
+ background: linear-gradient(90deg, #ff5f6d 0%, #ffc371 100%) !important;
7
+ border: none !important;
8
+ border-radius: 30px !important;
9
+ padding: 12px 24px !important;
10
+ color: white !important;
11
+ font-weight: bold !important;
12
+ cursor: pointer !important;
13
+ transition: all 0.3s ease !important;
14
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1) !important;
15
+ width: 100% !important;
16
+ margin: 20px 0 !important;
17
+ font-size: 1.1em !important;
18
+ }
19
+ button#find-match-btn:hover {
20
+ background: linear-gradient(90deg, #ff4f5d 0%, #ffb361 100%) !important;
21
+ box-shadow: 0 6px 12px rgba(0, 0, 0, 0.2) !important;
22
+ transform: translateY(-2px) !important;
23
+ }
24
+ button#find-match-btn:active {
25
+ transform: translateY(1px) !important;
26
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2) !important;
27
+ }
28
+ #search-status {
29
+ text-align: center;
30
+ padding: 15px;
31
+ font-size: 1.1em;
32
+ color: #666;
33
+ margin: 10px 0;
34
+ border-radius: 8px;
35
+ background: rgba(200, 200, 200, 0.1);
36
+ transition: opacity 0.3s ease;
37
+ }
38
+
39
+ /* Natural Language Search Button Styles */
40
+ button#find-by-description-btn {
41
+ background: linear-gradient(90deg, #4299e1 0%, #48bb78 100%) !important;
42
+ border: none !important;
43
+ border-radius: 30px !important;
44
+ padding: 12px 24px !important;
45
+ color: white !important;
46
+ font-weight: bold !important;
47
+ cursor: pointer !important;
48
+ transition: all 0.3s ease !important;
49
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1) !important;
50
+ width: 100% !important;
51
+ margin: 20px 0 !important;
52
+ font-size: 1.1em !important;
53
+ }
54
+ button#find-by-description-btn:hover {
55
+ background: linear-gradient(90deg, #3182ce 0%, #38a169 100%) !important;
56
+ box-shadow: 0 6px 12px rgba(0, 0, 0, 0.2) !important;
57
+ transform: translateY(-2px) !important;
58
+ }
59
+ button#find-by-description-btn:active {
60
+ background: linear-gradient(90deg, #2c5aa0 0%, #2f7d32 100%) !important;
61
+ transform: translateY(0px) scale(0.98) !important;
62
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2) !important;
63
+ }
64
+
65
+ /* Description Input Styles */
66
+ .description-input textarea {
67
+ border-radius: 10px !important;
68
+ border: 2px solid #e2e8f0 !important;
69
+ transition: all 0.3s ease !important;
70
+ }
71
+ .description-input textarea:focus {
72
+ border-color: #4299e1 !important;
73
+ box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.1) !important;
74
+ }
75
+
76
+ /* Force override any other styles */
77
+ .gradio-button {
78
+ position: relative !important;
79
+ overflow: visible !important;
80
+ }
81
+
82
+ /* Progress bars for semantic recommendations */
83
+ .progress {
84
+ transition: all 0.3s ease-in-out;
85
+ border-radius: 4px;
86
+ height: 12px;
87
+ }
88
+ .progress-bar {
89
+ background-color: #f5f5f5;
90
+ border-radius: 4px;
91
+ overflow: hidden;
92
+ position: relative;
93
+ }
94
+ .score-item {
95
+ margin: 10px 0;
96
+ }
97
+ .percentage {
98
+ margin-left: 8px;
99
+ font-weight: 500;
100
+ }
101
+
102
+ /* History display with colored tags */
103
+ .history-tag-criteria {
104
+ background: rgba(72, 187, 120, 0.1);
105
+ color: #48bb78;
106
+ padding: 4px 8px;
107
+ border-radius: 12px;
108
+ font-size: 0.8em;
109
+ font-weight: 600;
110
+ display: inline-flex;
111
+ align-items: center;
112
+ gap: 4px;
113
+ }
114
+ .history-tag-description {
115
+ background: rgba(66, 153, 225, 0.1);
116
+ color: #4299e1;
117
+ padding: 4px 8px;
118
+ border-radius: 12px;
119
+ font-size: 0.8em;
120
+ font-weight: 600;
121
+ display: inline-flex;
122
+ align-items: center;
123
+ gap: 4px;
124
+ }
125
+
126
  .dog-info-card {
127
  margin: 0 0 20px 0;
128
  padding: 0;
 
357
  }
358
 
359
  .breed-name {
360
+ font-size: 1.2em !important;
361
  font-weight: bold;
362
  color: #2c3e50;
363
  flex-grow: 1;
 
1193
  }
1194
 
1195
  @media (max-width: 768px) {
1196
+ /* 在小螢幕上改為單列顯示 */
1197
+ .health-grid, .noise-grid {
1198
+ grid-template-columns: 1fr;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1199
  }
1200
 
1201
+ /* 減少內邊距 */
1202
+ .health-section, .noise-section {
1203
+ padding: 16px;
 
 
 
 
 
 
 
 
 
 
1204
  }
1205
+
1206
+ /* 調整字體大小 */
1207
+ .section-header {
1208
+ font-size: 1rem;
 
1209
  }
1210
+
1211
+ /* 調整項目內邊距 */
1212
+ .health-item, .noise-item {
1213
+ padding: 10px 14px;
 
 
1214
  }
1215
  }
1216