DawnC commited on
Commit
1b3ab7b
·
verified ·
1 Parent(s): 3eaea6e

Upload 19 files

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