Spaces:
Running
on
Zero
Running
on
Zero
Upload 6 files
Browse filesrefactored description recommendation function and improved UI
- breed_recommendation_enhanced.py +14 -10
- matching_score_calculator.py +974 -0
- recommendation_formatter.py +5 -1
- semantic_breed_recommender.py +0 -0
- semantic_vector_manager.py +385 -0
- user_query_analyzer.py +511 -0
breed_recommendation_enhanced.py
CHANGED
|
@@ -30,6 +30,8 @@ def create_description_examples():
|
|
| 30 |
gap: 15px;
|
| 31 |
margin-top: 10px;
|
| 32 |
'>
|
|
|
|
|
|
|
| 33 |
<div style='
|
| 34 |
background: white;
|
| 35 |
padding: 12px;
|
|
@@ -37,12 +39,13 @@ def create_description_examples():
|
|
| 37 |
border: 1px solid #e2e8f0;
|
| 38 |
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
| 39 |
'>
|
| 40 |
-
<strong style='color: #4299e1;'
|
| 41 |
<span style='color: #4a5568; font-size: 0.9em;'>
|
| 42 |
-
"I live in
|
| 43 |
</span>
|
| 44 |
</div>
|
| 45 |
|
|
|
|
| 46 |
<div style='
|
| 47 |
background: white;
|
| 48 |
padding: 12px;
|
|
@@ -50,12 +53,13 @@ def create_description_examples():
|
|
| 50 |
border: 1px solid #e2e8f0;
|
| 51 |
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
| 52 |
'>
|
| 53 |
-
<strong style='color: #
|
| 54 |
<span style='color: #4a5568; font-size: 0.9em;'>
|
| 55 |
"I want an active medium to large dog for hiking and outdoor activities"
|
| 56 |
</span>
|
| 57 |
</div>
|
| 58 |
|
|
|
|
| 59 |
<div style='
|
| 60 |
background: white;
|
| 61 |
padding: 12px;
|
|
@@ -63,12 +67,13 @@ def create_description_examples():
|
|
| 63 |
border: 1px solid #e2e8f0;
|
| 64 |
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
| 65 |
'>
|
| 66 |
-
<strong style='color: #
|
| 67 |
<span style='color: #4a5568; font-size: 0.9em;'>
|
| 68 |
-
"I
|
| 69 |
</span>
|
| 70 |
</div>
|
| 71 |
|
|
|
|
| 72 |
<div style='
|
| 73 |
background: white;
|
| 74 |
padding: 12px;
|
|
@@ -76,7 +81,7 @@ def create_description_examples():
|
|
| 76 |
border: 1px solid #e2e8f0;
|
| 77 |
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
| 78 |
'>
|
| 79 |
-
<strong style='color: #
|
| 80 |
<span style='color: #4a5568; font-size: 0.9em;'>
|
| 81 |
"Looking for a calm, low-maintenance companion dog for elderly person"
|
| 82 |
</span>
|
|
@@ -98,7 +103,6 @@ def create_description_examples():
|
|
| 98 |
</div>
|
| 99 |
"""
|
| 100 |
|
| 101 |
-
|
| 102 |
def create_recommendation_tab(
|
| 103 |
UserPreferences,
|
| 104 |
get_breed_recommendations,
|
|
@@ -110,7 +114,7 @@ def create_recommendation_tab(
|
|
| 110 |
with gr.TabItem("Breed Recommendation"):
|
| 111 |
with gr.Tabs():
|
| 112 |
# --------------------------
|
| 113 |
-
#
|
| 114 |
# --------------------------
|
| 115 |
with gr.Tab("Find by Criteria"):
|
| 116 |
gr.HTML("""
|
|
@@ -334,7 +338,7 @@ def create_recommendation_tab(
|
|
| 334 |
)
|
| 335 |
|
| 336 |
# --------------------------
|
| 337 |
-
#
|
| 338 |
# --------------------------
|
| 339 |
with gr.Tab("Find by Description") as description_tab:
|
| 340 |
gr.HTML("""
|
|
@@ -639,4 +643,4 @@ def create_recommendation_tab(
|
|
| 639 |
'criteria_results': locals().get('criteria_results'),
|
| 640 |
'description_results': locals().get('description_results'),
|
| 641 |
'description_input': locals().get('description_input')
|
| 642 |
-
}
|
|
|
|
| 30 |
gap: 15px;
|
| 31 |
margin-top: 10px;
|
| 32 |
'>
|
| 33 |
+
|
| 34 |
+
<!-- 左上:冷色(藍) -->
|
| 35 |
<div style='
|
| 36 |
background: white;
|
| 37 |
padding: 12px;
|
|
|
|
| 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;
|
| 51 |
padding: 12px;
|
|
|
|
| 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;
|
| 65 |
padding: 12px;
|
|
|
|
| 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;
|
| 79 |
padding: 12px;
|
|
|
|
| 81 |
border: 1px solid #e2e8f0;
|
| 82 |
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
| 83 |
'>
|
| 84 |
+
<strong style='color: #276749;'>👥 Family Situation:</strong><br>
|
| 85 |
<span style='color: #4a5568; font-size: 0.9em;'>
|
| 86 |
"Looking for a calm, low-maintenance companion dog for elderly person"
|
| 87 |
</span>
|
|
|
|
| 103 |
</div>
|
| 104 |
"""
|
| 105 |
|
|
|
|
| 106 |
def create_recommendation_tab(
|
| 107 |
UserPreferences,
|
| 108 |
get_breed_recommendations,
|
|
|
|
| 114 |
with gr.TabItem("Breed Recommendation"):
|
| 115 |
with gr.Tabs():
|
| 116 |
# --------------------------
|
| 117 |
+
# Find by Criteria
|
| 118 |
# --------------------------
|
| 119 |
with gr.Tab("Find by Criteria"):
|
| 120 |
gr.HTML("""
|
|
|
|
| 338 |
)
|
| 339 |
|
| 340 |
# --------------------------
|
| 341 |
+
# Find by Description
|
| 342 |
# --------------------------
|
| 343 |
with gr.Tab("Find by Description") as description_tab:
|
| 344 |
gr.HTML("""
|
|
|
|
| 643 |
'criteria_results': locals().get('criteria_results'),
|
| 644 |
'description_results': locals().get('description_results'),
|
| 645 |
'description_input': locals().get('description_input')
|
| 646 |
+
}
|
matching_score_calculator.py
ADDED
|
@@ -0,0 +1,974 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import random
|
| 2 |
+
import hashlib
|
| 3 |
+
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
|
| 11 |
+
from sklearn.metrics.pairwise import cosine_similarity
|
| 12 |
+
from dog_database import get_dog_description
|
| 13 |
+
from breed_health_info import breed_health_info
|
| 14 |
+
from breed_noise_info import breed_noise_info
|
| 15 |
+
from scoring_calculation_system import UserPreferences, calculate_compatibility_score, UnifiedScoringSystem, calculate_unified_breed_scores
|
| 16 |
+
from query_understanding import QueryUnderstandingEngine, analyze_user_query
|
| 17 |
+
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 MatchingScoreCalculator:
|
| 23 |
+
"""
|
| 24 |
+
匹配評分計算器
|
| 25 |
+
處理多維度匹配計算、約束條件過濾和評分校準
|
| 26 |
+
"""
|
| 27 |
+
|
| 28 |
+
def __init__(self, breed_list: List[str]):
|
| 29 |
+
"""初始化匹配評分計算器"""
|
| 30 |
+
self.breed_list = breed_list
|
| 31 |
+
|
| 32 |
+
def apply_size_distribution_correction(self, recommendations: List[Dict]) -> List[Dict]:
|
| 33 |
+
"""應用尺寸分佈修正以防止大型品種偏差"""
|
| 34 |
+
if len(recommendations) < 10:
|
| 35 |
+
return recommendations
|
| 36 |
+
|
| 37 |
+
# 分析尺寸分佈
|
| 38 |
+
size_counts = {'toy': 0, 'small': 0, 'medium': 0, 'large': 0, 'giant': 0}
|
| 39 |
+
|
| 40 |
+
for rec in recommendations:
|
| 41 |
+
breed_info = get_dog_description(rec['breed'])
|
| 42 |
+
if breed_info:
|
| 43 |
+
size = self._normalize_breed_size(breed_info.get('Size', 'Medium'))
|
| 44 |
+
size_counts[size] += 1
|
| 45 |
+
|
| 46 |
+
total_recs = len(recommendations)
|
| 47 |
+
large_giant_ratio = (size_counts['large'] + size_counts['giant']) / total_recs
|
| 48 |
+
|
| 49 |
+
# 如果超過 70% 是大型/巨型品種,應用修正
|
| 50 |
+
if large_giant_ratio > 0.7:
|
| 51 |
+
corrected_recommendations = []
|
| 52 |
+
size_quotas = {'toy': 2, 'small': 4, 'medium': 6, 'large': 2, 'giant': 1}
|
| 53 |
+
current_counts = {'toy': 0, 'small': 0, 'medium': 0, 'large': 0, 'giant': 0}
|
| 54 |
+
|
| 55 |
+
# 第一輪:在配額內添加品種
|
| 56 |
+
for rec in recommendations:
|
| 57 |
+
breed_info = get_dog_description(rec['breed'])
|
| 58 |
+
if breed_info:
|
| 59 |
+
size = self._normalize_breed_size(breed_info.get('Size', 'Medium'))
|
| 60 |
+
if current_counts[size] < size_quotas[size]:
|
| 61 |
+
corrected_recommendations.append(rec)
|
| 62 |
+
current_counts[size] += 1
|
| 63 |
+
|
| 64 |
+
# 第二輪:用最佳剩餘候選品種填滿剩餘位置
|
| 65 |
+
remaining_slots = 15 - len(corrected_recommendations)
|
| 66 |
+
remaining_breeds = [rec for rec in recommendations if rec not in corrected_recommendations]
|
| 67 |
+
|
| 68 |
+
corrected_recommendations.extend(remaining_breeds[:remaining_slots])
|
| 69 |
+
return corrected_recommendations
|
| 70 |
+
|
| 71 |
+
return recommendations
|
| 72 |
+
|
| 73 |
+
def _normalize_breed_size(self, size: str) -> str:
|
| 74 |
+
"""標準化品種尺寸到標準分類"""
|
| 75 |
+
if not isinstance(size, str):
|
| 76 |
+
return 'medium'
|
| 77 |
+
|
| 78 |
+
size_lower = size.lower()
|
| 79 |
+
if any(term in size_lower for term in ['toy', 'tiny']):
|
| 80 |
+
return 'toy'
|
| 81 |
+
elif 'small' in size_lower:
|
| 82 |
+
return 'small'
|
| 83 |
+
elif 'medium' in size_lower:
|
| 84 |
+
return 'medium'
|
| 85 |
+
elif 'large' in size_lower:
|
| 86 |
+
return 'large'
|
| 87 |
+
elif any(term in size_lower for term in ['giant', 'extra large']):
|
| 88 |
+
return 'giant'
|
| 89 |
+
else:
|
| 90 |
+
return 'medium'
|
| 91 |
+
|
| 92 |
+
def apply_hard_constraints(self, breed: str, user_input: str, breed_characteristics: Dict[str, Any]) -> float:
|
| 93 |
+
"""增強硬約束,具有更嚴格的懲罰"""
|
| 94 |
+
penalty = 0.0
|
| 95 |
+
user_text_lower = user_input.lower()
|
| 96 |
+
|
| 97 |
+
# 獲取品種信息
|
| 98 |
+
breed_info = get_dog_description(breed)
|
| 99 |
+
if not breed_info:
|
| 100 |
+
return 0.0
|
| 101 |
+
|
| 102 |
+
breed_size = breed_info.get('Size', '').lower()
|
| 103 |
+
exercise_needs = breed_info.get('Exercise Needs', '').lower()
|
| 104 |
+
|
| 105 |
+
# 公寓居住約束 - 更嚴格
|
| 106 |
+
if any(term in user_text_lower for term in ['apartment', 'flat', 'studio', 'small space']):
|
| 107 |
+
if 'giant' in breed_size:
|
| 108 |
+
return -2.0 # 完全淘汰
|
| 109 |
+
elif 'large' in breed_size:
|
| 110 |
+
if any(term in exercise_needs for term in ['high', 'very high']):
|
| 111 |
+
return -2.0 # 完全淘汰
|
| 112 |
+
else:
|
| 113 |
+
penalty -= 0.5 # 仍有顯著懲罰
|
| 114 |
+
elif 'medium' in breed_size and 'very high' in exercise_needs:
|
| 115 |
+
penalty -= 0.6
|
| 116 |
+
|
| 117 |
+
# 運動不匹配約束
|
| 118 |
+
if "don't exercise much" in user_text_lower or "low exercise" in user_text_lower:
|
| 119 |
+
if any(term in exercise_needs for term in ['very high', 'extreme', 'intense']):
|
| 120 |
+
return -2.0 # 完全淘汰
|
| 121 |
+
elif 'high' in exercise_needs:
|
| 122 |
+
penalty -= 0.8
|
| 123 |
+
|
| 124 |
+
# 中等生活方式檢測
|
| 125 |
+
if any(term in user_text_lower for term in ['moderate', 'balanced', '30 minutes', 'half hour']):
|
| 126 |
+
# 懲罰極端情況
|
| 127 |
+
if 'giant' in breed_size:
|
| 128 |
+
penalty -= 0.7 # 對巨型犬的強懲罰
|
| 129 |
+
elif 'very high' in exercise_needs:
|
| 130 |
+
penalty -= 0.5
|
| 131 |
+
|
| 132 |
+
# 兒童安全(現有邏輯保持但增強)
|
| 133 |
+
if any(term in user_text_lower for term in ['child', 'kids', 'family', 'baby']):
|
| 134 |
+
good_with_children = breed_info.get('Good with Children', '').lower()
|
| 135 |
+
if good_with_children == 'no':
|
| 136 |
+
return -2.0 # 為了安全完全淘汰
|
| 137 |
+
|
| 138 |
+
return penalty
|
| 139 |
+
|
| 140 |
+
def calculate_lifestyle_bonus(self, breed_characteristics: Dict[str, Any],
|
| 141 |
+
lifestyle_keywords: Dict[str, List[str]]) -> float:
|
| 142 |
+
"""增強生活方式匹配獎勵計算"""
|
| 143 |
+
bonus = 0.0
|
| 144 |
+
penalties = 0.0
|
| 145 |
+
|
| 146 |
+
# 增強尺寸匹配
|
| 147 |
+
breed_size = breed_characteristics.get('size', '').lower()
|
| 148 |
+
size_prefs = lifestyle_keywords.get('size_preference', [])
|
| 149 |
+
for pref in size_prefs:
|
| 150 |
+
if pref in breed_size:
|
| 151 |
+
bonus += 0.25 # 尺寸匹配的強獎勵
|
| 152 |
+
elif (pref == 'small' and 'large' in breed_size) or \
|
| 153 |
+
(pref == 'large' and 'small' in breed_size):
|
| 154 |
+
penalties += 0.15 # 尺寸不匹配的懲罰
|
| 155 |
+
|
| 156 |
+
# 增強活動水平匹配
|
| 157 |
+
breed_exercise = breed_characteristics.get('exercise_needs', '').lower()
|
| 158 |
+
activity_prefs = lifestyle_keywords.get('activity_level', [])
|
| 159 |
+
|
| 160 |
+
if 'high' in activity_prefs:
|
| 161 |
+
if 'high' in breed_exercise or 'very high' in breed_exercise:
|
| 162 |
+
bonus += 0.2
|
| 163 |
+
elif 'low' in breed_exercise:
|
| 164 |
+
penalties += 0.2
|
| 165 |
+
elif 'low' in activity_prefs:
|
| 166 |
+
if 'low' in breed_exercise:
|
| 167 |
+
bonus += 0.2
|
| 168 |
+
elif 'high' in breed_exercise or 'very high' in breed_exercise:
|
| 169 |
+
penalties += 0.25
|
| 170 |
+
elif 'moderate' in activity_prefs:
|
| 171 |
+
if 'moderate' in breed_exercise:
|
| 172 |
+
bonus += 0.15
|
| 173 |
+
|
| 174 |
+
# 增強家庭情況匹配
|
| 175 |
+
good_with_children = breed_characteristics.get('good_with_children', 'Yes')
|
| 176 |
+
family_prefs = lifestyle_keywords.get('family_situation', [])
|
| 177 |
+
|
| 178 |
+
if 'children' in family_prefs:
|
| 179 |
+
if good_with_children == 'Yes':
|
| 180 |
+
bonus += 0.15
|
| 181 |
+
else:
|
| 182 |
+
penalties += 0.3 # 對非兒童友好品種的強懲罰
|
| 183 |
+
|
| 184 |
+
# 增強居住空間匹配
|
| 185 |
+
living_prefs = lifestyle_keywords.get('living_space', [])
|
| 186 |
+
if 'apartment' in living_prefs:
|
| 187 |
+
if 'small' in breed_size:
|
| 188 |
+
bonus += 0.2
|
| 189 |
+
elif 'medium' in breed_size and 'low' in breed_exercise:
|
| 190 |
+
bonus += 0.1
|
| 191 |
+
elif 'large' in breed_size or 'giant' in breed_size:
|
| 192 |
+
penalties += 0.2 # 公寓中大型犬的懲罰
|
| 193 |
+
|
| 194 |
+
# 噪音偏好匹配
|
| 195 |
+
noise_prefs = lifestyle_keywords.get('noise_preference', [])
|
| 196 |
+
temperament = breed_characteristics.get('temperament', '').lower()
|
| 197 |
+
|
| 198 |
+
if 'low' in noise_prefs:
|
| 199 |
+
# 獎勵安靜品種
|
| 200 |
+
if any(term in temperament for term in ['gentle', 'calm', 'quiet']):
|
| 201 |
+
bonus += 0.1
|
| 202 |
+
|
| 203 |
+
# 照護水平匹配
|
| 204 |
+
grooming_needs = breed_characteristics.get('grooming_needs', '').lower()
|
| 205 |
+
care_prefs = lifestyle_keywords.get('care_level', [])
|
| 206 |
+
|
| 207 |
+
if 'low' in care_prefs and 'low' in grooming_needs:
|
| 208 |
+
bonus += 0.1
|
| 209 |
+
elif 'high' in care_prefs and 'high' in grooming_needs:
|
| 210 |
+
bonus += 0.1
|
| 211 |
+
elif 'low' in care_prefs and 'high' in grooming_needs:
|
| 212 |
+
penalties += 0.15
|
| 213 |
+
|
| 214 |
+
# 特殊需求匹配
|
| 215 |
+
special_needs = lifestyle_keywords.get('special_needs', [])
|
| 216 |
+
|
| 217 |
+
if 'guard' in special_needs:
|
| 218 |
+
if any(term in temperament for term in ['protective', 'alert', 'watchful']):
|
| 219 |
+
bonus += 0.1
|
| 220 |
+
elif 'companion' in special_needs:
|
| 221 |
+
if any(term in temperament for term in ['affectionate', 'gentle', 'loyal']):
|
| 222 |
+
bonus += 0.1
|
| 223 |
+
|
| 224 |
+
# 計算包含懲罰的最終獎勵
|
| 225 |
+
final_bonus = bonus - penalties
|
| 226 |
+
return max(-0.3, min(0.5, final_bonus)) # 允許負獎勵但限制範圍
|
| 227 |
+
|
| 228 |
+
def apply_intelligent_trait_matching(self, recommendations: List[Dict], user_input: str) -> List[Dict]:
|
| 229 |
+
"""基於增強關鍵字提取和數據庫挖掘應用智能特徵匹配"""
|
| 230 |
+
try:
|
| 231 |
+
# 從用戶輸入提取增強關鍵字
|
| 232 |
+
extracted_keywords = self._extract_enhanced_lifestyle_keywords(user_input)
|
| 233 |
+
|
| 234 |
+
# 對每個推薦應用智能特徵匹配
|
| 235 |
+
enhanced_recommendations = []
|
| 236 |
+
|
| 237 |
+
for rec in recommendations:
|
| 238 |
+
breed_name = rec['breed'].replace(' ', '_')
|
| 239 |
+
|
| 240 |
+
# 獲取品種數據庫信息
|
| 241 |
+
breed_info = get_dog_description(breed_name) or {}
|
| 242 |
+
|
| 243 |
+
# 計算智能特徵獎勵
|
| 244 |
+
intelligence_bonus = 0.0
|
| 245 |
+
trait_match_details = {}
|
| 246 |
+
|
| 247 |
+
# 1. 智力匹配
|
| 248 |
+
if extracted_keywords.get('intelligence_preference'):
|
| 249 |
+
intelligence_pref = extracted_keywords['intelligence_preference'][0]
|
| 250 |
+
breed_desc = breed_info.get('Description', '').lower()
|
| 251 |
+
|
| 252 |
+
if intelligence_pref == 'high':
|
| 253 |
+
if any(word in breed_desc for word in ['intelligent', 'smart', 'clever', 'quick to learn', 'trainable']):
|
| 254 |
+
intelligence_bonus += 0.05
|
| 255 |
+
trait_match_details['intelligence_match'] = 'High intelligence match detected'
|
| 256 |
+
elif any(word in breed_desc for word in ['stubborn', 'independent', 'difficult']):
|
| 257 |
+
intelligence_bonus -= 0.02
|
| 258 |
+
trait_match_details['intelligence_warning'] = 'May be challenging to train'
|
| 259 |
+
|
| 260 |
+
elif intelligence_pref == 'independent':
|
| 261 |
+
if any(word in breed_desc for word in ['independent', 'stubborn', 'strong-willed']):
|
| 262 |
+
intelligence_bonus += 0.03
|
| 263 |
+
trait_match_details['independence_match'] = 'Independent nature match'
|
| 264 |
+
|
| 265 |
+
# 2. 美容偏好匹配
|
| 266 |
+
if extracted_keywords.get('grooming_preference'):
|
| 267 |
+
grooming_pref = extracted_keywords['grooming_preference'][0]
|
| 268 |
+
breed_grooming = breed_info.get('Grooming Needs', '').lower()
|
| 269 |
+
|
| 270 |
+
if grooming_pref == 'low' and 'low' in breed_grooming:
|
| 271 |
+
intelligence_bonus += 0.03
|
| 272 |
+
trait_match_details['grooming_match'] = 'Low maintenance grooming match'
|
| 273 |
+
elif grooming_pref == 'high' and 'high' in breed_grooming:
|
| 274 |
+
intelligence_bonus += 0.03
|
| 275 |
+
trait_match_details['grooming_match'] = 'High maintenance grooming match'
|
| 276 |
+
elif grooming_pref == 'low' and 'high' in breed_grooming:
|
| 277 |
+
intelligence_bonus -= 0.04
|
| 278 |
+
trait_match_details['grooming_mismatch'] = 'High grooming needs may not suit preferences'
|
| 279 |
+
|
| 280 |
+
# 3. 氣質偏好匹配
|
| 281 |
+
if extracted_keywords.get('temperament_preference'):
|
| 282 |
+
temp_prefs = extracted_keywords['temperament_preference']
|
| 283 |
+
breed_temperament = breed_info.get('Temperament', '').lower()
|
| 284 |
+
breed_desc = breed_info.get('Description', '').lower()
|
| 285 |
+
|
| 286 |
+
temp_text = (breed_temperament + ' ' + breed_desc).lower()
|
| 287 |
+
|
| 288 |
+
for temp_pref in temp_prefs:
|
| 289 |
+
if temp_pref == 'gentle' and any(word in temp_text for word in ['gentle', 'calm', 'peaceful', 'mild']):
|
| 290 |
+
intelligence_bonus += 0.04
|
| 291 |
+
trait_match_details['temperament_match'] = f'Gentle temperament match: {temp_pref}'
|
| 292 |
+
elif temp_pref == 'playful' and any(word in temp_text for word in ['playful', 'energetic', 'lively', 'fun']):
|
| 293 |
+
intelligence_bonus += 0.04
|
| 294 |
+
trait_match_details['temperament_match'] = f'Playful temperament match: {temp_pref}'
|
| 295 |
+
elif temp_pref == 'protective' and any(word in temp_text for word in ['protective', 'guard', 'alert', 'watchful']):
|
| 296 |
+
intelligence_bonus += 0.04
|
| 297 |
+
trait_match_details['temperament_match'] = f'Protective temperament match: {temp_pref}'
|
| 298 |
+
elif temp_pref == 'friendly' and any(word in temp_text for word in ['friendly', 'social', 'outgoing', 'people']):
|
| 299 |
+
intelligence_bonus += 0.04
|
| 300 |
+
trait_match_details['temperament_match'] = f'Friendly temperament match: {temp_pref}'
|
| 301 |
+
|
| 302 |
+
# 4. 經驗水平匹配
|
| 303 |
+
if extracted_keywords.get('experience_level'):
|
| 304 |
+
exp_level = extracted_keywords['experience_level'][0]
|
| 305 |
+
breed_desc = breed_info.get('Description', '').lower()
|
| 306 |
+
|
| 307 |
+
if exp_level == 'beginner':
|
| 308 |
+
# 為初學者偏愛易於處理的品種
|
| 309 |
+
if any(word in breed_desc for word in ['easy', 'gentle', 'good for beginners', 'family', 'calm']):
|
| 310 |
+
intelligence_bonus += 0.06
|
| 311 |
+
trait_match_details['beginner_friendly'] = 'Good choice for first-time owners'
|
| 312 |
+
elif any(word in breed_desc for word in ['challenging', 'dominant', 'requires experience', 'strong-willed']):
|
| 313 |
+
intelligence_bonus -= 0.08
|
| 314 |
+
trait_match_details['experience_warning'] = 'May be challenging for first-time owners'
|
| 315 |
+
|
| 316 |
+
elif exp_level == 'advanced':
|
| 317 |
+
# 高級用戶可以處理更具挑戰性的品種
|
| 318 |
+
if any(word in breed_desc for word in ['working', 'requires experience', 'intelligent', 'strong']):
|
| 319 |
+
intelligence_bonus += 0.03
|
| 320 |
+
trait_match_details['advanced_suitable'] = 'Good match for experienced owners'
|
| 321 |
+
|
| 322 |
+
# 5. 壽命偏好匹配
|
| 323 |
+
if extracted_keywords.get('lifespan_preference'):
|
| 324 |
+
lifespan_pref = extracted_keywords['lifespan_preference'][0]
|
| 325 |
+
breed_lifespan = breed_info.get('Lifespan', '10-12 years')
|
| 326 |
+
|
| 327 |
+
try:
|
| 328 |
+
import re
|
| 329 |
+
years = re.findall(r'\d+', breed_lifespan)
|
| 330 |
+
if years:
|
| 331 |
+
avg_years = sum(int(y) for y in years) / len(years)
|
| 332 |
+
if lifespan_pref == 'long' and avg_years >= 13:
|
| 333 |
+
intelligence_bonus += 0.02
|
| 334 |
+
trait_match_details['longevity_match'] = f'Long lifespan match: {breed_lifespan}'
|
| 335 |
+
elif lifespan_pref == 'healthy' and avg_years >= 12:
|
| 336 |
+
intelligence_bonus += 0.02
|
| 337 |
+
trait_match_details['health_match'] = f'Healthy lifespan: {breed_lifespan}'
|
| 338 |
+
except:
|
| 339 |
+
pass
|
| 340 |
+
|
| 341 |
+
# 將智力獎勵應用到總分
|
| 342 |
+
original_score = rec['overall_score']
|
| 343 |
+
enhanced_score = min(1.0, original_score + intelligence_bonus)
|
| 344 |
+
|
| 345 |
+
# 創建包含特徵匹配詳細信息的增強推薦
|
| 346 |
+
enhanced_rec = rec.copy()
|
| 347 |
+
enhanced_rec['overall_score'] = enhanced_score
|
| 348 |
+
enhanced_rec['intelligence_bonus'] = intelligence_bonus
|
| 349 |
+
enhanced_rec['trait_match_details'] = trait_match_details
|
| 350 |
+
|
| 351 |
+
# 如果發生顯著增強,添加詳細說明
|
| 352 |
+
if abs(intelligence_bonus) > 0.02:
|
| 353 |
+
enhancement_explanation = []
|
| 354 |
+
for detail_key, detail_value in trait_match_details.items():
|
| 355 |
+
enhancement_explanation.append(detail_value)
|
| 356 |
+
|
| 357 |
+
if enhancement_explanation:
|
| 358 |
+
current_explanation = enhanced_rec.get('explanation', '')
|
| 359 |
+
enhanced_explanation = current_explanation + f" Enhanced matching: {'; '.join(enhancement_explanation)}"
|
| 360 |
+
enhanced_rec['explanation'] = enhanced_explanation
|
| 361 |
+
|
| 362 |
+
enhanced_recommendations.append(enhanced_rec)
|
| 363 |
+
|
| 364 |
+
# 按增強總分重新排序
|
| 365 |
+
enhanced_recommendations.sort(key=lambda x: x['overall_score'], reverse=True)
|
| 366 |
+
|
| 367 |
+
# 更新排名
|
| 368 |
+
for i, rec in enumerate(enhanced_recommendations):
|
| 369 |
+
rec['rank'] = i + 1
|
| 370 |
+
|
| 371 |
+
print(f"Applied intelligent trait matching with average bonus: {sum(r['intelligence_bonus'] for r in enhanced_recommendations) / len(enhanced_recommendations):.3f}")
|
| 372 |
+
|
| 373 |
+
return enhanced_recommendations
|
| 374 |
+
|
| 375 |
+
except Exception as e:
|
| 376 |
+
print(f"Error in intelligent trait matching: {str(e)}")
|
| 377 |
+
# 如果特徵匹配失敗,返回原始推薦
|
| 378 |
+
return recommendations
|
| 379 |
+
|
| 380 |
+
def _extract_enhanced_lifestyle_keywords(self, user_input: str) -> Dict[str, List[str]]:
|
| 381 |
+
"""提取增強的生活方式關鍵字(用於智能特徵匹配)"""
|
| 382 |
+
keywords = {
|
| 383 |
+
'intelligence_preference': [],
|
| 384 |
+
'grooming_preference': [],
|
| 385 |
+
'temperament_preference': [],
|
| 386 |
+
'experience_level': [],
|
| 387 |
+
'lifespan_preference': []
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
text = user_input.lower()
|
| 391 |
+
|
| 392 |
+
# 智力偏好檢測
|
| 393 |
+
smart_terms = ['smart', 'intelligent', 'clever', 'bright', 'quick learner', 'easy to train', 'trainable', 'genius', 'brilliant']
|
| 394 |
+
independent_terms = ['independent', 'stubborn', 'strong-willed', 'less trainable', 'thinks for themselves']
|
| 395 |
+
|
| 396 |
+
if any(term in text for term in smart_terms):
|
| 397 |
+
keywords['intelligence_preference'].append('high')
|
| 398 |
+
if any(term in text for term in independent_terms):
|
| 399 |
+
keywords['intelligence_preference'].append('independent')
|
| 400 |
+
|
| 401 |
+
# 美容偏好檢測
|
| 402 |
+
low_grooming_terms = ['low grooming', 'minimal grooming', 'easy care', 'wash and wear', 'no grooming', 'simple coat']
|
| 403 |
+
high_grooming_terms = ['high grooming', 'professional grooming', 'lots of care', 'high maintenance coat', 'daily brushing', 'regular grooming']
|
| 404 |
+
|
| 405 |
+
if any(term in text for term in low_grooming_terms):
|
| 406 |
+
keywords['grooming_preference'].append('low')
|
| 407 |
+
if any(term in text for term in high_grooming_terms):
|
| 408 |
+
keywords['grooming_preference'].append('high')
|
| 409 |
+
|
| 410 |
+
# 氣質偏���檢測
|
| 411 |
+
gentle_terms = ['gentle', 'calm', 'peaceful', 'laid back', 'chill', 'mellow', 'docile']
|
| 412 |
+
playful_terms = ['playful', 'energetic', 'fun', 'active personality', 'lively', 'spirited', 'bouncy']
|
| 413 |
+
protective_terms = ['protective', 'guard', 'watchdog', 'alert', 'vigilant', 'defensive']
|
| 414 |
+
friendly_terms = ['friendly', 'social', 'outgoing', 'loves people', 'sociable', 'gregarious']
|
| 415 |
+
|
| 416 |
+
if any(term in text for term in gentle_terms):
|
| 417 |
+
keywords['temperament_preference'].append('gentle')
|
| 418 |
+
if any(term in text for term in playful_terms):
|
| 419 |
+
keywords['temperament_preference'].append('playful')
|
| 420 |
+
if any(term in text for term in protective_terms):
|
| 421 |
+
keywords['temperament_preference'].append('protective')
|
| 422 |
+
if any(term in text for term in friendly_terms):
|
| 423 |
+
keywords['temperament_preference'].append('friendly')
|
| 424 |
+
|
| 425 |
+
# 經驗水平檢測
|
| 426 |
+
beginner_terms = ['first time', 'beginner', 'new to dogs', 'never had', 'novice', 'inexperienced']
|
| 427 |
+
advanced_terms = ['experienced', 'advanced', 'dog expert', 'many dogs before', 'professional', 'seasoned']
|
| 428 |
+
|
| 429 |
+
if any(term in text for term in beginner_terms):
|
| 430 |
+
keywords['experience_level'].append('beginner')
|
| 431 |
+
if any(term in text for term in advanced_terms):
|
| 432 |
+
keywords['experience_level'].append('advanced')
|
| 433 |
+
|
| 434 |
+
# 壽命偏好檢測
|
| 435 |
+
long_lived_terms = ['long lived', 'long lifespan', 'live long', 'many years', '15+ years', 'longevity']
|
| 436 |
+
healthy_terms = ['healthy breed', 'few health issues', 'robust', 'hardy', 'strong constitution']
|
| 437 |
+
|
| 438 |
+
if any(term in text for term in long_lived_terms):
|
| 439 |
+
keywords['lifespan_preference'].append('long')
|
| 440 |
+
if any(term in text for term in healthy_terms):
|
| 441 |
+
keywords['lifespan_preference'].append('healthy')
|
| 442 |
+
|
| 443 |
+
return keywords
|
| 444 |
+
|
| 445 |
+
def calculate_enhanced_matching_score(self, breed: str, breed_info: dict, user_description: str, base_similarity: float) -> dict:
|
| 446 |
+
"""計算增強的匹配分數,基於用戶描述和品種特性"""
|
| 447 |
+
try:
|
| 448 |
+
user_desc = user_description.lower()
|
| 449 |
+
|
| 450 |
+
# 分析用戶需求
|
| 451 |
+
space_requirements = self._analyze_space_requirements(user_desc)
|
| 452 |
+
exercise_requirements = self._analyze_exercise_requirements(user_desc)
|
| 453 |
+
noise_requirements = self._analyze_noise_requirements(user_desc)
|
| 454 |
+
size_requirements = self._analyze_size_requirements(user_desc)
|
| 455 |
+
family_requirements = self._analyze_family_requirements(user_desc)
|
| 456 |
+
|
| 457 |
+
# 獲取品種特性
|
| 458 |
+
breed_size = breed_info.get('Size', '').lower()
|
| 459 |
+
breed_exercise = breed_info.get('Exercise Needs', '').lower()
|
| 460 |
+
breed_noise = breed_noise_info.get(breed, {}).get('noise_level', 'moderate').lower()
|
| 461 |
+
breed_temperament = breed_info.get('Temperament', '').lower()
|
| 462 |
+
breed_good_with_children = breed_info.get('Good with Children', '').lower()
|
| 463 |
+
|
| 464 |
+
# 計算各維度匹配分數
|
| 465 |
+
dimension_scores = {}
|
| 466 |
+
|
| 467 |
+
# 空間匹配 (30% 權重)
|
| 468 |
+
space_score = self._calculate_space_compatibility(space_requirements, breed_size, breed_exercise)
|
| 469 |
+
dimension_scores['space'] = space_score
|
| 470 |
+
|
| 471 |
+
# 運動需求匹配 (25% 權重)
|
| 472 |
+
exercise_score = self._calculate_exercise_compatibility(exercise_requirements, breed_exercise)
|
| 473 |
+
dimension_scores['exercise'] = exercise_score
|
| 474 |
+
|
| 475 |
+
# 噪音匹配 (20% 權重)
|
| 476 |
+
noise_score = self._calculate_noise_compatibility(noise_requirements, breed_noise)
|
| 477 |
+
dimension_scores['noise'] = noise_score
|
| 478 |
+
|
| 479 |
+
# 體型匹配 (15% 權重)
|
| 480 |
+
size_score = self._calculate_size_compatibility(size_requirements, breed_size)
|
| 481 |
+
dimension_scores['grooming'] = min(0.9, base_similarity + 0.1) # 美容需求基於語意相似度
|
| 482 |
+
|
| 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)
|
| 490 |
+
|
| 491 |
+
# 計算加權總分 - 精確化維度權重配置
|
| 492 |
+
# 根據指導建議重新平衡維度權重
|
| 493 |
+
weighted_score = (
|
| 494 |
+
space_score * 0.30 + # 空間相容性(降低5%)
|
| 495 |
+
exercise_score * 0.28 + # 運動需求匹配(降低2%)
|
| 496 |
+
noise_score * 0.18 + # 噪音控制(提升3%)
|
| 497 |
+
family_score * 0.12 + # 家庭相容性(提升2%)
|
| 498 |
+
size_score * 0.08 + # 體型匹配(降低2%)
|
| 499 |
+
min(0.9, base_similarity + 0.1) * 0.04 # 護理需求(新增獨立���重)
|
| 500 |
+
)
|
| 501 |
+
|
| 502 |
+
# 優化完美匹配獎勵機制 - 降低觸發門檻並增加層次
|
| 503 |
+
perfect_match_bonus = 0.0
|
| 504 |
+
if space_score >= 0.88 and exercise_score >= 0.88 and noise_score >= 0.85:
|
| 505 |
+
perfect_match_bonus = 0.08 # 卓越匹配獎勵
|
| 506 |
+
elif space_score >= 0.82 and exercise_score >= 0.82 and noise_score >= 0.75:
|
| 507 |
+
perfect_match_bonus = 0.04 # 優秀匹配獎勵
|
| 508 |
+
elif space_score >= 0.75 and exercise_score >= 0.75:
|
| 509 |
+
perfect_match_bonus = 0.02 # 良好匹配獎勵
|
| 510 |
+
|
| 511 |
+
# 結合語意相似度與維度匹配 - 調整為75%維度匹配 25%語義相似度
|
| 512 |
+
base_combined_score = (weighted_score * 0.75 + base_similarity * 0.25) + perfect_match_bonus
|
| 513 |
+
|
| 514 |
+
# 應用漸進式約束懲罰,但確保基礎分數保障
|
| 515 |
+
raw_final_score = base_combined_score + constraint_penalty
|
| 516 |
+
|
| 517 |
+
# 實施動態分數保障機制 - 提升至40-42%基礎分數
|
| 518 |
+
# 根據品種特性動態調整基礎分數
|
| 519 |
+
base_guaranteed_score = 0.42 # 提升基礎保障分數
|
| 520 |
+
|
| 521 |
+
# 特殊品種基礎分數調整
|
| 522 |
+
high_adaptability_breeds = ['French_Bulldog', 'Pug', 'Golden_Retriever', 'Labrador_Retriever']
|
| 523 |
+
if any(breed in breed for breed in high_adaptability_breeds):
|
| 524 |
+
base_guaranteed_score = 0.45 # 高適應性品種更高基礎分數
|
| 525 |
+
|
| 526 |
+
# 動態分數分佈優化
|
| 527 |
+
if raw_final_score >= base_guaranteed_score:
|
| 528 |
+
# 對於高分品種,實施適度壓縮避免過度集中
|
| 529 |
+
if raw_final_score > 0.85:
|
| 530 |
+
compression_factor = 0.92 # 輕度壓縮高分
|
| 531 |
+
final_score = 0.85 + (raw_final_score - 0.85) * compression_factor
|
| 532 |
+
else:
|
| 533 |
+
final_score = raw_final_score
|
| 534 |
+
final_score = min(0.93, final_score) # 降低最高分數限制
|
| 535 |
+
else:
|
| 536 |
+
# 對於低分品種,使用改進的保障機制
|
| 537 |
+
normalized_raw_score = max(0.15, raw_final_score)
|
| 538 |
+
# 基礎保障75% + 實際計算25%,保持一定區分度
|
| 539 |
+
final_score = base_guaranteed_score * 0.75 + normalized_raw_score * 0.25
|
| 540 |
+
final_score = max(base_guaranteed_score, min(0.93, final_score))
|
| 541 |
+
|
| 542 |
+
lifestyle_bonus = max(0.0, weighted_score - base_similarity)
|
| 543 |
+
|
| 544 |
+
return {
|
| 545 |
+
'final_score': final_score,
|
| 546 |
+
'weighted_score': weighted_score,
|
| 547 |
+
'lifestyle_bonus': lifestyle_bonus,
|
| 548 |
+
'dimension_scores': dimension_scores,
|
| 549 |
+
'constraint_penalty': constraint_penalty
|
| 550 |
+
}
|
| 551 |
+
|
| 552 |
+
except Exception as e:
|
| 553 |
+
print(f"Error in enhanced matching calculation for {breed}: {str(e)}")
|
| 554 |
+
return {
|
| 555 |
+
'final_score': base_similarity,
|
| 556 |
+
'weighted_score': base_similarity,
|
| 557 |
+
'lifestyle_bonus': 0.0,
|
| 558 |
+
'dimension_scores': {
|
| 559 |
+
'space': base_similarity * 0.9,
|
| 560 |
+
'exercise': base_similarity * 0.85,
|
| 561 |
+
'grooming': base_similarity * 0.8,
|
| 562 |
+
'experience': base_similarity * 0.75,
|
| 563 |
+
'noise': base_similarity * 0.7,
|
| 564 |
+
'family': base_similarity * 0.65
|
| 565 |
+
},
|
| 566 |
+
'constraint_penalty': 0.0
|
| 567 |
+
}
|
| 568 |
+
|
| 569 |
+
def _analyze_space_requirements(self, user_desc: str) -> dict:
|
| 570 |
+
"""分析空間需求 - 增強中等活動量識別"""
|
| 571 |
+
requirements = {'type': 'unknown', 'size': 'medium', 'importance': 0.5}
|
| 572 |
+
|
| 573 |
+
if any(word in user_desc for word in ['apartment', 'small apartment', 'small space', 'condo', 'flat']):
|
| 574 |
+
requirements['type'] = 'apartment'
|
| 575 |
+
requirements['size'] = 'small'
|
| 576 |
+
requirements['importance'] = 0.95 # 提高重要性
|
| 577 |
+
elif any(word in user_desc for word in ['medium-sized house', 'medium house', 'townhouse']):
|
| 578 |
+
requirements['type'] = 'medium_house'
|
| 579 |
+
requirements['size'] = 'medium'
|
| 580 |
+
requirements['importance'] = 0.8 # 中等活動量用戶的特殊標記
|
| 581 |
+
elif any(word in user_desc for word in ['large house', 'big house', 'yard', 'garden', 'large space', 'backyard']):
|
| 582 |
+
requirements['type'] = 'house'
|
| 583 |
+
requirements['size'] = 'large'
|
| 584 |
+
requirements['importance'] = 0.7
|
| 585 |
+
|
| 586 |
+
return requirements
|
| 587 |
+
|
| 588 |
+
def _analyze_exercise_requirements(self, user_desc: str) -> dict:
|
| 589 |
+
"""分析運動需求 - 增強中等活動量識別"""
|
| 590 |
+
requirements = {'level': 'moderate', 'importance': 0.5}
|
| 591 |
+
|
| 592 |
+
# 低運動量識別
|
| 593 |
+
if any(word in user_desc for word in ["don't exercise", "don't exercise much", "low exercise", "minimal", "lazy", "not active"]):
|
| 594 |
+
requirements['level'] = 'low'
|
| 595 |
+
requirements['importance'] = 0.95
|
| 596 |
+
# 中等運動量的精確識別
|
| 597 |
+
elif any(phrase in user_desc for phrase in ['30 minutes', 'half hour', 'moderate', 'balanced', 'walk about']):
|
| 598 |
+
if 'walk' in user_desc or 'daily' in user_desc:
|
| 599 |
+
requirements['level'] = 'moderate'
|
| 600 |
+
requirements['importance'] = 0.85 # 中等活動量的特殊標記
|
| 601 |
+
# 高運動量識別
|
| 602 |
+
elif any(word in user_desc for word in ['active', 'hiking', 'outdoor activities', 'running', 'outdoors', 'love hiking']):
|
| 603 |
+
requirements['level'] = 'high'
|
| 604 |
+
requirements['importance'] = 0.9
|
| 605 |
+
|
| 606 |
+
return requirements
|
| 607 |
+
|
| 608 |
+
def _analyze_noise_requirements(self, user_desc: str) -> dict:
|
| 609 |
+
"""分析噪音需求"""
|
| 610 |
+
requirements = {'tolerance': 'medium', 'importance': 0.5}
|
| 611 |
+
|
| 612 |
+
if any(word in user_desc for word in ['quiet', 'no bark', "won't bark", "doesn't bark", 'silent', 'peaceful']):
|
| 613 |
+
requirements['tolerance'] = 'low'
|
| 614 |
+
requirements['importance'] = 0.9
|
| 615 |
+
elif any(word in user_desc for word in ['loud', 'barking ok', 'noise ok']):
|
| 616 |
+
requirements['tolerance'] = 'high'
|
| 617 |
+
requirements['importance'] = 0.7
|
| 618 |
+
|
| 619 |
+
return requirements
|
| 620 |
+
|
| 621 |
+
def _analyze_size_requirements(self, user_desc: str) -> dict:
|
| 622 |
+
"""分析體型需求"""
|
| 623 |
+
requirements = {'preferred': 'any', 'importance': 0.5}
|
| 624 |
+
|
| 625 |
+
if any(word in user_desc for word in ['small', 'tiny', 'little', 'lap dog', 'compact']):
|
| 626 |
+
requirements['preferred'] = 'small'
|
| 627 |
+
requirements['importance'] = 0.8
|
| 628 |
+
elif any(word in user_desc for word in ['large', 'big', 'giant']):
|
| 629 |
+
requirements['preferred'] = 'large'
|
| 630 |
+
requirements['importance'] = 0.8
|
| 631 |
+
|
| 632 |
+
return requirements
|
| 633 |
+
|
| 634 |
+
def _analyze_family_requirements(self, user_desc: str) -> dict:
|
| 635 |
+
"""分析家庭需求"""
|
| 636 |
+
requirements = {'children': False, 'importance': 0.3}
|
| 637 |
+
|
| 638 |
+
if any(word in user_desc for word in ['children', 'kids', 'family', 'child']):
|
| 639 |
+
requirements['children'] = True
|
| 640 |
+
requirements['importance'] = 0.8
|
| 641 |
+
|
| 642 |
+
return requirements
|
| 643 |
+
|
| 644 |
+
def _calculate_space_compatibility(self, space_req: dict, breed_size: str, breed_exercise: str) -> float:
|
| 645 |
+
"""計算空間相容性分數 - 增強中等活動量處理"""
|
| 646 |
+
if space_req['type'] == 'apartment':
|
| 647 |
+
if 'small' in breed_size or 'toy' in breed_size:
|
| 648 |
+
base_score = 0.95
|
| 649 |
+
elif 'medium' in breed_size:
|
| 650 |
+
if 'low' in breed_exercise:
|
| 651 |
+
base_score = 0.75
|
| 652 |
+
else:
|
| 653 |
+
base_score = 0.45 # 降低中型犬在公寓的分數
|
| 654 |
+
elif 'large' in breed_size:
|
| 655 |
+
base_score = 0.05 # 大型犬極度不適合公寓
|
| 656 |
+
elif 'giant' in breed_size:
|
| 657 |
+
base_score = 0.01 # 超大型犬完全不適合公寓
|
| 658 |
+
else:
|
| 659 |
+
base_score = 0.7
|
| 660 |
+
elif space_req['type'] == 'medium_house':
|
| 661 |
+
# 中型房屋的特殊處理 - 適合中等活動量用戶
|
| 662 |
+
if 'small' in breed_size or 'toy' in breed_size:
|
| 663 |
+
base_score = 0.9
|
| 664 |
+
elif 'medium' in breed_size:
|
| 665 |
+
base_score = 0.95 # 中型犬在中型房屋很適合
|
| 666 |
+
elif 'large' in breed_size:
|
| 667 |
+
if 'moderate' in breed_exercise or 'low' in breed_exercise:
|
| 668 |
+
base_score = 0.8 # 低運動量大型犬還可以
|
| 669 |
+
else:
|
| 670 |
+
base_score = 0.6 # 高運動量大型犬不太適合
|
| 671 |
+
elif 'giant' in breed_size:
|
| 672 |
+
base_score = 0.3 # 超大型犬在中型房屋不太適合
|
| 673 |
+
else:
|
| 674 |
+
base_score = 0.85
|
| 675 |
+
else:
|
| 676 |
+
# 大型房屋的情況
|
| 677 |
+
if 'small' in breed_size or 'toy' in breed_size:
|
| 678 |
+
base_score = 0.85
|
| 679 |
+
elif 'medium' in breed_size:
|
| 680 |
+
base_score = 0.9
|
| 681 |
+
elif 'large' in breed_size or 'giant' in breed_size:
|
| 682 |
+
base_score = 0.95
|
| 683 |
+
else:
|
| 684 |
+
base_score = 0.8
|
| 685 |
+
|
| 686 |
+
return min(0.95, base_score)
|
| 687 |
+
|
| 688 |
+
def _calculate_exercise_compatibility(self, exercise_req: dict, breed_exercise: str) -> float:
|
| 689 |
+
"""計算運動需求相容性分數 - 增強中等活動量處理"""
|
| 690 |
+
if exercise_req['level'] == 'low':
|
| 691 |
+
if 'low' in breed_exercise or 'minimal' in breed_exercise:
|
| 692 |
+
return 0.95
|
| 693 |
+
elif 'moderate' in breed_exercise:
|
| 694 |
+
return 0.5 # 降低不匹配分數
|
| 695 |
+
elif 'high' in breed_exercise:
|
| 696 |
+
return 0.1 # 進一步降低高運動需求的匹配
|
| 697 |
+
else:
|
| 698 |
+
return 0.7
|
| 699 |
+
elif exercise_req['level'] == 'high':
|
| 700 |
+
if 'high' in breed_exercise:
|
| 701 |
+
return 0.95
|
| 702 |
+
elif 'moderate' in breed_exercise:
|
| 703 |
+
return 0.8
|
| 704 |
+
elif 'low' in breed_exercise:
|
| 705 |
+
return 0.6
|
| 706 |
+
else:
|
| 707 |
+
return 0.7
|
| 708 |
+
else: # moderate - 中等活動量的精確處理
|
| 709 |
+
if 'moderate' in breed_exercise:
|
| 710 |
+
return 0.95 # 完美匹配
|
| 711 |
+
elif 'low' in breed_exercise:
|
| 712 |
+
return 0.85 # 低運動需求的品種對中等活動量用戶也不錯
|
| 713 |
+
elif 'high' in breed_exercise:
|
| 714 |
+
return 0.5 # 中等活動量用戶不太適合高運動需求品種
|
| 715 |
+
else:
|
| 716 |
+
return 0.75
|
| 717 |
+
|
| 718 |
+
return 0.6
|
| 719 |
+
|
| 720 |
+
def _calculate_noise_compatibility(self, noise_req: dict, breed_noise: str) -> float:
|
| 721 |
+
"""計算噪音相容性分數,更好處理複合等級"""
|
| 722 |
+
breed_noise_lower = breed_noise.lower()
|
| 723 |
+
|
| 724 |
+
if noise_req['tolerance'] == 'low':
|
| 725 |
+
if 'low' in breed_noise_lower and 'moderate' not in breed_noise_lower:
|
| 726 |
+
return 0.95 # 純低噪音
|
| 727 |
+
elif 'low-moderate' in breed_noise_lower or 'low to moderate' in breed_noise_lower:
|
| 728 |
+
return 0.8 # 低到中等噪音,還可接受
|
| 729 |
+
elif breed_noise_lower in ['moderate']:
|
| 730 |
+
return 0.4 # 中等噪音有些問題
|
| 731 |
+
elif 'high' in breed_noise_lower:
|
| 732 |
+
return 0.1 # 高噪音不適合
|
| 733 |
+
else:
|
| 734 |
+
return 0.6 # 未知噪音水平,保守估計
|
| 735 |
+
elif noise_req['tolerance'] == 'high':
|
| 736 |
+
if 'high' in breed_noise_lower:
|
| 737 |
+
return 0.9
|
| 738 |
+
elif 'moderate' in breed_noise_lower:
|
| 739 |
+
return 0.85
|
| 740 |
+
elif 'low' in breed_noise_lower:
|
| 741 |
+
return 0.8 # 安靜犬對高容忍度的人也很好
|
| 742 |
+
else:
|
| 743 |
+
return 0.8
|
| 744 |
+
else: # moderate tolerance
|
| 745 |
+
if 'moderate' in breed_noise_lower:
|
| 746 |
+
return 0.9
|
| 747 |
+
elif 'low' in breed_noise_lower:
|
| 748 |
+
return 0.85
|
| 749 |
+
elif 'high' in breed_noise_lower:
|
| 750 |
+
return 0.6
|
| 751 |
+
else:
|
| 752 |
+
return 0.75
|
| 753 |
+
|
| 754 |
+
return 0.7
|
| 755 |
+
|
| 756 |
+
def _calculate_size_compatibility(self, size_req: dict, breed_size: str) -> float:
|
| 757 |
+
"""計算體型相容性分數"""
|
| 758 |
+
if size_req['preferred'] == 'small':
|
| 759 |
+
if any(word in breed_size for word in ['small', 'toy', 'tiny']):
|
| 760 |
+
return 0.9
|
| 761 |
+
elif 'medium' in breed_size:
|
| 762 |
+
return 0.6
|
| 763 |
+
else:
|
| 764 |
+
return 0.3
|
| 765 |
+
elif size_req['preferred'] == 'large':
|
| 766 |
+
if any(word in breed_size for word in ['large', 'giant']):
|
| 767 |
+
return 0.9
|
| 768 |
+
elif 'medium' in breed_size:
|
| 769 |
+
return 0.7
|
| 770 |
+
else:
|
| 771 |
+
return 0.4
|
| 772 |
+
|
| 773 |
+
return 0.7 # 無特別偏好
|
| 774 |
+
|
| 775 |
+
def _calculate_family_compatibility(self, family_req: dict, good_with_children: str, temperament: str) -> float:
|
| 776 |
+
"""計算家庭相容性分數"""
|
| 777 |
+
if family_req['children']:
|
| 778 |
+
if 'yes' in good_with_children.lower():
|
| 779 |
+
return 0.9
|
| 780 |
+
elif any(word in temperament for word in ['gentle', 'patient', 'friendly']):
|
| 781 |
+
return 0.8
|
| 782 |
+
elif 'no' in good_with_children.lower():
|
| 783 |
+
return 0.2
|
| 784 |
+
else:
|
| 785 |
+
return 0.6
|
| 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
|
| 792 |
+
|
| 793 |
+
# 建立懲罰衰減係數和補償機制
|
| 794 |
+
penalty_decay_factor = 0.7
|
| 795 |
+
breed_adaptability_bonus = 0.0
|
| 796 |
+
breed_size = breed_info.get('Size', '').lower()
|
| 797 |
+
breed_exercise = breed_info.get('Exercise Needs', '').lower()
|
| 798 |
+
breed_name = breed_info.get('Breed', '').replace(' ', '_')
|
| 799 |
+
|
| 800 |
+
# 公寓空間約束 - 品種特性感知懲罰機制
|
| 801 |
+
if 'apartment' in user_desc or 'small apartment' in user_desc:
|
| 802 |
+
if 'giant' in breed_size:
|
| 803 |
+
base_penalty = -0.35 # 減少基礎懲罰
|
| 804 |
+
# 特定品種適應性補償
|
| 805 |
+
adaptable_giants = ['Mastiff', 'Great Dane'] # 相對安靜的巨型犬
|
| 806 |
+
if any(adapt_breed in breed_name for adapt_breed in adaptable_giants):
|
| 807 |
+
breed_adaptability_bonus += 0.08
|
| 808 |
+
penalty += base_penalty * penalty_decay_factor
|
| 809 |
+
elif 'large' in breed_size:
|
| 810 |
+
base_penalty = -0.25 # 減少大型犬懲罰
|
| 811 |
+
# 適合公寓的大型犬補償
|
| 812 |
+
apartment_friendly_large = ['Greyhound', 'Great_Dane']
|
| 813 |
+
if any(apt_breed in breed_name for apt_breed in apartment_friendly_large):
|
| 814 |
+
breed_adaptability_bonus += 0.06
|
| 815 |
+
penalty += base_penalty * penalty_decay_factor
|
| 816 |
+
elif 'medium' in breed_size and 'high' in breed_exercise:
|
| 817 |
+
penalty += -0.15 * penalty_decay_factor # 進一步減少懲罰
|
| 818 |
+
|
| 819 |
+
# 運動需求不匹配 - 品種特性感知懲罰機制
|
| 820 |
+
if any(phrase in user_desc for phrase in ["don't exercise", "not active", "low exercise", "don't exercise much"]):
|
| 821 |
+
if 'high' in breed_exercise:
|
| 822 |
+
base_penalty = -0.28 # 減少基礎懲罰
|
| 823 |
+
# 低維護高運動犬種補償
|
| 824 |
+
adaptable_high_energy = ['Greyhound', 'Whippet'] # 運動爆發型,平時安靜
|
| 825 |
+
if any(adapt_breed in breed_name for adapt_breed in adaptable_high_energy):
|
| 826 |
+
breed_adaptability_bonus += 0.10
|
| 827 |
+
penalty += base_penalty * penalty_decay_factor
|
| 828 |
+
elif 'moderate' in breed_exercise:
|
| 829 |
+
penalty += -0.08 * penalty_decay_factor # 進一步減少懲罰
|
| 830 |
+
|
| 831 |
+
# 噪音控制需求不匹配 - 品種特性感知懲罰機制
|
| 832 |
+
if any(phrase in user_desc for phrase in ['quiet', "won't bark", "doesn't bark", "silent"]):
|
| 833 |
+
breed_noise = breed_noise_info.get(breed_name, {}).get('noise_level', 'moderate').lower()
|
| 834 |
+
if 'high' in breed_noise:
|
| 835 |
+
base_penalty = -0.18 # 減少基礎懲罰
|
| 836 |
+
# 訓練性良好的高噪音品種補償
|
| 837 |
+
trainable_vocal_breeds = ['German_Shepherd', 'Golden_Retriever']
|
| 838 |
+
if any(train_breed in breed_name for train_breed in trainable_vocal_breeds):
|
| 839 |
+
breed_adaptability_bonus += 0.05
|
| 840 |
+
penalty += base_penalty * penalty_decay_factor
|
| 841 |
+
elif 'moderate' in breed_noise and 'low' not in breed_noise:
|
| 842 |
+
penalty += -0.05 * penalty_decay_factor
|
| 843 |
+
|
| 844 |
+
# 體型偏好不匹配 - 漸進式懲罰
|
| 845 |
+
if any(phrase in user_desc for phrase in ['small', 'tiny', 'little']):
|
| 846 |
+
if 'giant' in breed_size:
|
| 847 |
+
penalty -= 0.35 # 超大型犬懲罰
|
| 848 |
+
elif 'large' in breed_size:
|
| 849 |
+
penalty -= 0.20 # 大型犬懲罰
|
| 850 |
+
|
| 851 |
+
# 中等活動量用戶的特殊約束處理 - 漸進式懲罰
|
| 852 |
+
moderate_activity_terms = ['30 minutes', 'half hour', 'moderate', 'balanced', 'medium-sized house']
|
| 853 |
+
if any(term in user_desc for term in moderate_activity_terms):
|
| 854 |
+
# 超大型犬對中等活動量用戶的適度懲罰
|
| 855 |
+
giant_breeds = ['Saint Bernard', 'Tibetan Mastiff', 'Great Dane', 'Mastiff', 'Newfoundland']
|
| 856 |
+
if any(giant in breed_name for giant in giant_breeds) or 'giant' in breed_size:
|
| 857 |
+
penalty -= 0.35 # 適度懲罰,不完全排除
|
| 858 |
+
|
| 859 |
+
# 中型房屋 + 超大型犬的額外考量
|
| 860 |
+
if 'medium-sized house' in user_desc and any(giant in breed_name for giant in giant_breeds):
|
| 861 |
+
if not any(high_activity in user_desc for high_activity in ['hiking', 'running', 'active', 'outdoor activities']):
|
| 862 |
+
penalty -= 0.15 # 輕度額外懲罰
|
| 863 |
+
|
| 864 |
+
# 30分鐘散步對極高運動需求品種的懲罰
|
| 865 |
+
if any(term in user_desc for term in ['30 minutes', 'half hour']) and 'walk' in user_desc:
|
| 866 |
+
high_energy_breeds = ['Siberian Husky', 'Border Collie', 'Jack Russell Terrier', 'Weimaraner']
|
| 867 |
+
if any(he_breed in breed_name for he_breed in high_energy_breeds) and 'high' in breed_exercise:
|
| 868 |
+
penalty -= 0.25 # 適度懲罰極高運動需求品種
|
| 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:
|
| 881 |
+
breed_adaptability_bonus += boundary_adaptable_breeds[breed_name]
|
| 882 |
+
|
| 883 |
+
# 應用品種適應性補償並設置懲罰上限
|
| 884 |
+
final_penalty = penalty + breed_adaptability_bonus
|
| 885 |
+
# 限制最大懲罰,避免單一約束主導評分
|
| 886 |
+
final_penalty = max(-0.4, final_penalty)
|
| 887 |
+
|
| 888 |
+
return final_penalty
|
| 889 |
+
|
| 890 |
+
def get_breed_characteristics_enhanced(self, breed: str) -> Dict[str, Any]:
|
| 891 |
+
"""獲取品種特徵"""
|
| 892 |
+
breed_info = get_dog_description(breed)
|
| 893 |
+
if not breed_info:
|
| 894 |
+
return {}
|
| 895 |
+
|
| 896 |
+
characteristics = {
|
| 897 |
+
'size': breed_info.get('Size', 'Unknown'),
|
| 898 |
+
'temperament': breed_info.get('Temperament', ''),
|
| 899 |
+
'exercise_needs': breed_info.get('Exercise Needs', 'Moderate'),
|
| 900 |
+
'grooming_needs': breed_info.get('Grooming Needs', 'Moderate'),
|
| 901 |
+
'good_with_children': breed_info.get('Good with Children', 'Unknown'),
|
| 902 |
+
'lifespan': breed_info.get('Lifespan', '10-12 years'),
|
| 903 |
+
'description': breed_info.get('Description', '')
|
| 904 |
+
}
|
| 905 |
+
|
| 906 |
+
# 添加噪音資訊
|
| 907 |
+
noise_info = breed_noise_info.get(breed, {})
|
| 908 |
+
characteristics['noise_level'] = noise_info.get('noise_level', 'moderate')
|
| 909 |
+
|
| 910 |
+
return characteristics
|
| 911 |
+
|
| 912 |
+
def get_breed_info_from_standardized(self, standardized_info) -> Dict[str, Any]:
|
| 913 |
+
"""將標準化品種信息轉換為字典���式"""
|
| 914 |
+
try:
|
| 915 |
+
size_map = {1: 'Tiny', 2: 'Small', 3: 'Medium', 4: 'Large', 5: 'Giant'}
|
| 916 |
+
exercise_map = {1: 'Low', 2: 'Moderate', 3: 'High', 4: 'Very High'}
|
| 917 |
+
care_map = {1: 'Low', 2: 'Moderate', 3: 'High'}
|
| 918 |
+
|
| 919 |
+
return {
|
| 920 |
+
'Size': size_map.get(standardized_info.size_category, 'Medium'),
|
| 921 |
+
'Exercise Needs': exercise_map.get(standardized_info.exercise_level, 'Moderate'),
|
| 922 |
+
'Grooming Needs': care_map.get(standardized_info.care_complexity, 'Moderate'),
|
| 923 |
+
'Good with Children': 'Yes' if standardized_info.child_compatibility >= 0.8 else
|
| 924 |
+
'No' if standardized_info.child_compatibility <= 0.2 else 'Unknown',
|
| 925 |
+
'Temperament': 'Varies by individual',
|
| 926 |
+
'Lifespan': '10-12 years',
|
| 927 |
+
'Description': f'A {size_map.get(standardized_info.size_category, "medium")} sized breed'
|
| 928 |
+
}
|
| 929 |
+
except Exception as e:
|
| 930 |
+
print(f"Error converting standardized info: {str(e)}")
|
| 931 |
+
return {}
|
| 932 |
+
|
| 933 |
+
def get_fallback_recommendations(self, top_k: int = 15) -> List[Dict[str, Any]]:
|
| 934 |
+
"""當增強系統失敗時獲取備用推薦"""
|
| 935 |
+
try:
|
| 936 |
+
safe_breeds = [
|
| 937 |
+
('Labrador Retriever', 0.85),
|
| 938 |
+
('Golden Retriever', 0.82),
|
| 939 |
+
('Cavalier King Charles Spaniel', 0.80),
|
| 940 |
+
('French Bulldog', 0.78),
|
| 941 |
+
('Boston Terrier', 0.76),
|
| 942 |
+
('Bichon Frise', 0.74),
|
| 943 |
+
('Pug', 0.72),
|
| 944 |
+
('Cocker Spaniel', 0.70)
|
| 945 |
+
]
|
| 946 |
+
|
| 947 |
+
recommendations = []
|
| 948 |
+
for i, (breed, score) in enumerate(safe_breeds[:top_k]):
|
| 949 |
+
breed_info = get_dog_description(breed.replace(' ', '_')) or {}
|
| 950 |
+
|
| 951 |
+
recommendation = {
|
| 952 |
+
'breed': breed,
|
| 953 |
+
'rank': i + 1,
|
| 954 |
+
'overall_score': score,
|
| 955 |
+
'final_score': score,
|
| 956 |
+
'semantic_score': score * 0.8,
|
| 957 |
+
'comparative_bonus': 0.0,
|
| 958 |
+
'lifestyle_bonus': 0.0,
|
| 959 |
+
'size': breed_info.get('Size', 'Unknown'),
|
| 960 |
+
'temperament': breed_info.get('Temperament', ''),
|
| 961 |
+
'exercise_needs': breed_info.get('Exercise Needs', 'Moderate'),
|
| 962 |
+
'grooming_needs': breed_info.get('Grooming Needs', 'Moderate'),
|
| 963 |
+
'good_with_children': breed_info.get('Good with Children', 'Yes'),
|
| 964 |
+
'lifespan': breed_info.get('Lifespan', '10-12 years'),
|
| 965 |
+
'description': breed_info.get('Description', ''),
|
| 966 |
+
'search_type': 'fallback'
|
| 967 |
+
}
|
| 968 |
+
recommendations.append(recommendation)
|
| 969 |
+
|
| 970 |
+
return recommendations
|
| 971 |
+
|
| 972 |
+
except Exception as e:
|
| 973 |
+
print(f"Error generating fallback recommendations: {str(e)}")
|
| 974 |
+
return []
|
recommendation_formatter.py
CHANGED
|
@@ -28,6 +28,10 @@ def get_breed_recommendations(user_prefs: UserPreferences, top_n: int = 15) -> L
|
|
| 28 |
breed = breed_tuple[0]
|
| 29 |
base_breed = breed.split('(')[0].strip()
|
| 30 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
if base_breed in seen_breeds:
|
| 32 |
continue
|
| 33 |
seen_breeds.add(base_breed)
|
|
@@ -127,7 +131,7 @@ def get_breed_recommendations(user_prefs: UserPreferences, top_n: int = 15) -> L
|
|
| 127 |
|
| 128 |
print(f"Breeds after filtering: {len(recommendations)}")
|
| 129 |
|
| 130 |
-
#
|
| 131 |
recommendations.sort(key=lambda x: (round(-x['final_score'], 4), x['breed']))
|
| 132 |
|
| 133 |
# 修正後的推薦選擇邏輯,移除有問題的分數比較
|
|
|
|
| 28 |
breed = breed_tuple[0]
|
| 29 |
base_breed = breed.split('(')[0].strip()
|
| 30 |
|
| 31 |
+
# 過濾掉野生動物品種
|
| 32 |
+
if base_breed == 'Dhole':
|
| 33 |
+
continue
|
| 34 |
+
|
| 35 |
if base_breed in seen_breeds:
|
| 36 |
continue
|
| 37 |
seen_breeds.add(base_breed)
|
|
|
|
| 131 |
|
| 132 |
print(f"Breeds after filtering: {len(recommendations)}")
|
| 133 |
|
| 134 |
+
# 按照 final_score 排序
|
| 135 |
recommendations.sort(key=lambda x: (round(-x['final_score'], 4), x['breed']))
|
| 136 |
|
| 137 |
# 修正後的推薦選擇邏輯,移除有問題的分數比較
|
semantic_breed_recommender.py
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
semantic_vector_manager.py
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import random
|
| 2 |
+
import hashlib
|
| 3 |
+
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
|
| 11 |
+
from sklearn.metrics.pairwise import cosine_similarity
|
| 12 |
+
from dog_database import get_dog_description
|
| 13 |
+
from breed_health_info import breed_health_info
|
| 14 |
+
from breed_noise_info import breed_noise_info
|
| 15 |
+
|
| 16 |
+
@dataclass
|
| 17 |
+
class BreedDescriptionVector:
|
| 18 |
+
"""品種描述向量的資料結構"""
|
| 19 |
+
breed_name: str
|
| 20 |
+
description_text: str
|
| 21 |
+
embedding: np.ndarray
|
| 22 |
+
characteristics: Dict[str, Any]
|
| 23 |
+
|
| 24 |
+
class SemanticVectorManager:
|
| 25 |
+
"""
|
| 26 |
+
語義向量管理器
|
| 27 |
+
處理 SBERT 模型初始化、品種向量化建構和品種描述生成
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
def __init__(self):
|
| 31 |
+
"""初始化語義向量管理器"""
|
| 32 |
+
self.model_name = 'all-MiniLM-L6-v2'
|
| 33 |
+
self.sbert_model = None
|
| 34 |
+
self._sbert_loading_attempted = False
|
| 35 |
+
self.breed_vectors = {}
|
| 36 |
+
self.breed_list = self._get_breed_list()
|
| 37 |
+
# 延遲SBERT模型載入直到需要時才在GPU環境中進行
|
| 38 |
+
print("SemanticVectorManager initialized (SBERT loading deferred)")
|
| 39 |
+
|
| 40 |
+
def _get_breed_list(self) -> List[str]:
|
| 41 |
+
"""從資料庫獲取品種清單"""
|
| 42 |
+
try:
|
| 43 |
+
conn = sqlite3.connect('animal_detector.db')
|
| 44 |
+
cursor = conn.cursor()
|
| 45 |
+
cursor.execute("SELECT DISTINCT Breed FROM AnimalCatalog")
|
| 46 |
+
breeds = [row[0] for row in cursor.fetchall()]
|
| 47 |
+
cursor.close()
|
| 48 |
+
conn.close()
|
| 49 |
+
# 過濾掉野生動物品種
|
| 50 |
+
breeds = [breed for breed in breeds if breed != 'Dhole']
|
| 51 |
+
return breeds
|
| 52 |
+
except Exception as e:
|
| 53 |
+
print(f"Error getting breed list: {str(e)}")
|
| 54 |
+
return ['Labrador_Retriever', 'German_Shepherd', 'Golden_Retriever',
|
| 55 |
+
'Bulldog', 'Poodle', 'Beagle', 'Rottweiler', 'Yorkshire_Terrier']
|
| 56 |
+
|
| 57 |
+
def _initialize_model(self):
|
| 58 |
+
"""初始化 SBERT 模型,包含容錯機制 - 設計用於ZeroGPU相容性"""
|
| 59 |
+
if self.sbert_model is not None or self._sbert_loading_attempted:
|
| 60 |
+
return self.sbert_model
|
| 61 |
+
|
| 62 |
+
try:
|
| 63 |
+
print("Loading SBERT model in GPU context...")
|
| 64 |
+
# 如果主要模型失敗,嘗試不同的模型名稱
|
| 65 |
+
model_options = ['all-MiniLM-L6-v2', 'all-mpnet-base-v2', 'all-MiniLM-L12-v2']
|
| 66 |
+
|
| 67 |
+
for model_name in model_options:
|
| 68 |
+
try:
|
| 69 |
+
# 明確指定設備以處理ZeroGPU環境
|
| 70 |
+
import torch
|
| 71 |
+
device = 'cuda' if torch.cuda.is_available() else 'cpu'
|
| 72 |
+
self.sbert_model = SentenceTransformer(model_name, device=device)
|
| 73 |
+
self.model_name = model_name
|
| 74 |
+
print(f"SBERT model {model_name} loaded successfully on {device}")
|
| 75 |
+
return self.sbert_model
|
| 76 |
+
except Exception as model_e:
|
| 77 |
+
print(f"Failed to load {model_name}: {str(model_e)}")
|
| 78 |
+
continue
|
| 79 |
+
|
| 80 |
+
# 如果所有模型都失敗
|
| 81 |
+
print("All SBERT models failed to load. Using basic text matching fallback.")
|
| 82 |
+
self.sbert_model = None
|
| 83 |
+
return None
|
| 84 |
+
|
| 85 |
+
except Exception as e:
|
| 86 |
+
print(f"Failed to initialize any SBERT model: {str(e)}")
|
| 87 |
+
print(traceback.format_exc())
|
| 88 |
+
print("Will provide basic text-based recommendations without embeddings")
|
| 89 |
+
self.sbert_model = None
|
| 90 |
+
return None
|
| 91 |
+
finally:
|
| 92 |
+
self._sbert_loading_attempted = True
|
| 93 |
+
|
| 94 |
+
def _create_breed_description(self, breed: str) -> str:
|
| 95 |
+
"""為品種創建包含所有關鍵特徵的全面自然語言描述"""
|
| 96 |
+
try:
|
| 97 |
+
# 獲取所有信息來源
|
| 98 |
+
breed_info = get_dog_description(breed) or {}
|
| 99 |
+
health_info = breed_health_info.get(breed, {}) if breed_health_info else {}
|
| 100 |
+
noise_info = breed_noise_info.get(breed, {}) if breed_noise_info else {}
|
| 101 |
+
|
| 102 |
+
breed_display_name = breed.replace('_', ' ')
|
| 103 |
+
description_parts = []
|
| 104 |
+
|
| 105 |
+
# 1. 基本尺寸和身體特徵
|
| 106 |
+
size = breed_info.get('Size', 'medium').lower()
|
| 107 |
+
description_parts.append(f"{breed_display_name} is a {size} sized dog breed")
|
| 108 |
+
|
| 109 |
+
# 2. 氣質和個性(匹配的關鍵因素)
|
| 110 |
+
temperament = breed_info.get('Temperament', '')
|
| 111 |
+
if temperament:
|
| 112 |
+
description_parts.append(f"with a {temperament.lower()} temperament")
|
| 113 |
+
|
| 114 |
+
# 3. 運動和活動水平(公寓居住的關鍵因素)
|
| 115 |
+
exercise_needs = breed_info.get('Exercise Needs', 'moderate').lower()
|
| 116 |
+
if 'high' in exercise_needs or 'very high' in exercise_needs:
|
| 117 |
+
description_parts.append("requiring high daily exercise and mental stimulation")
|
| 118 |
+
elif 'low' in exercise_needs or 'minimal' in exercise_needs:
|
| 119 |
+
description_parts.append("with minimal exercise requirements, suitable for apartment living")
|
| 120 |
+
else:
|
| 121 |
+
description_parts.append("with moderate exercise needs")
|
| 122 |
+
|
| 123 |
+
# 4. 噪音特徵(安靜需求的關鍵因素)
|
| 124 |
+
noise_level = noise_info.get('noise_level', 'moderate').lower()
|
| 125 |
+
if 'low' in noise_level or 'quiet' in noise_level:
|
| 126 |
+
description_parts.append("known for being quiet and rarely barking")
|
| 127 |
+
elif 'high' in noise_level or 'loud' in noise_level:
|
| 128 |
+
description_parts.append("tends to be vocal and bark frequently")
|
| 129 |
+
else:
|
| 130 |
+
description_parts.append("with moderate barking tendencies")
|
| 131 |
+
|
| 132 |
+
# 5. 居住空間相容性
|
| 133 |
+
if size in ['small', 'tiny']:
|
| 134 |
+
description_parts.append("excellent for small apartments and limited spaces")
|
| 135 |
+
elif size in ['large', 'giant']:
|
| 136 |
+
description_parts.append("requiring large living spaces and preferably a yard")
|
| 137 |
+
else:
|
| 138 |
+
description_parts.append("adaptable to various living situations")
|
| 139 |
+
|
| 140 |
+
# 6. 美容和維護
|
| 141 |
+
grooming_needs = breed_info.get('Grooming Needs', 'moderate').lower()
|
| 142 |
+
if 'high' in grooming_needs:
|
| 143 |
+
description_parts.append("requiring regular professional grooming")
|
| 144 |
+
elif 'low' in grooming_needs:
|
| 145 |
+
description_parts.append("with minimal grooming requirements")
|
| 146 |
+
else:
|
| 147 |
+
description_parts.append("with moderate grooming needs")
|
| 148 |
+
|
| 149 |
+
# 7. 家庭相容性
|
| 150 |
+
good_with_children = breed_info.get('Good with Children', 'Yes')
|
| 151 |
+
if good_with_children == 'Yes':
|
| 152 |
+
description_parts.append("excellent with children and families")
|
| 153 |
+
else:
|
| 154 |
+
description_parts.append("better suited for adult households")
|
| 155 |
+
|
| 156 |
+
# 8. 智力和可訓練性(從資料庫描述中提取)
|
| 157 |
+
intelligence_keywords = []
|
| 158 |
+
description_text = breed_info.get('Description', '').lower()
|
| 159 |
+
|
| 160 |
+
if description_text:
|
| 161 |
+
# 從描述中提取智力指標
|
| 162 |
+
if any(word in description_text for word in ['intelligent', 'smart', 'clever', 'quick to learn']):
|
| 163 |
+
intelligence_keywords.extend(['highly intelligent', 'trainable', 'quick learner'])
|
| 164 |
+
elif any(word in description_text for word in ['stubborn', 'independent', 'difficult to train']):
|
| 165 |
+
intelligence_keywords.extend(['independent minded', 'requires patience', 'challenging to train'])
|
| 166 |
+
else:
|
| 167 |
+
intelligence_keywords.extend(['moderate intelligence', 'trainable with consistency'])
|
| 168 |
+
|
| 169 |
+
# 從描述中提取工作/用途特徵
|
| 170 |
+
if any(word in description_text for word in ['working', 'herding', 'guard', 'hunting']):
|
| 171 |
+
intelligence_keywords.extend(['working breed', 'purpose-driven', 'task-oriented'])
|
| 172 |
+
elif any(word in description_text for word in ['companion', 'lap', 'toy', 'decorative']):
|
| 173 |
+
intelligence_keywords.extend(['companion breed', 'affectionate', 'people-focused'])
|
| 174 |
+
|
| 175 |
+
# 添加智力背景到描述中
|
| 176 |
+
if intelligence_keywords:
|
| 177 |
+
description_parts.append(f"characterized as {', '.join(intelligence_keywords[:2])}")
|
| 178 |
+
|
| 179 |
+
# 9. 特殊特徵和用途(使用資料庫挖掘進行增強)
|
| 180 |
+
if breed_info.get('Description'):
|
| 181 |
+
desc = breed_info.get('Description', '')[:150] # 增加到 150 字元以提供更多背景
|
| 182 |
+
if desc:
|
| 183 |
+
# 從描述中提取關鍵特徵以便更好的語義匹配
|
| 184 |
+
desc_lower = desc.lower()
|
| 185 |
+
key_traits = []
|
| 186 |
+
|
| 187 |
+
# 從描述中提取關鍵行為特徵
|
| 188 |
+
if 'friendly' in desc_lower:
|
| 189 |
+
key_traits.append('friendly')
|
| 190 |
+
if 'gentle' in desc_lower:
|
| 191 |
+
key_traits.append('gentle')
|
| 192 |
+
if 'energetic' in desc_lower or 'active' in desc_lower:
|
| 193 |
+
key_traits.append('energetic')
|
| 194 |
+
if 'calm' in desc_lower or 'peaceful' in desc_lower:
|
| 195 |
+
key_traits.append('calm')
|
| 196 |
+
if 'protective' in desc_lower or 'guard' in desc_lower:
|
| 197 |
+
key_traits.append('protective')
|
| 198 |
+
|
| 199 |
+
trait_text = f" and {', '.join(key_traits)}" if key_traits else ""
|
| 200 |
+
description_parts.append(f"Known for: {desc.lower()}{trait_text}")
|
| 201 |
+
|
| 202 |
+
# 10. 照護水平需求
|
| 203 |
+
try:
|
| 204 |
+
care_level = breed_info.get('Care Level', 'moderate')
|
| 205 |
+
if isinstance(care_level, str):
|
| 206 |
+
description_parts.append(f"requiring {care_level.lower()} overall care level")
|
| 207 |
+
else:
|
| 208 |
+
description_parts.append("requiring moderate overall care level")
|
| 209 |
+
except Exception as e:
|
| 210 |
+
print(f"Error processing care level for {breed}: {str(e)}")
|
| 211 |
+
description_parts.append("requiring moderate overall care level")
|
| 212 |
+
|
| 213 |
+
# 11. 壽命資訊
|
| 214 |
+
try:
|
| 215 |
+
lifespan = breed_info.get('Lifespan', '10-12 years')
|
| 216 |
+
if lifespan and isinstance(lifespan, str) and lifespan.strip():
|
| 217 |
+
description_parts.append(f"with a typical lifespan of {lifespan}")
|
| 218 |
+
else:
|
| 219 |
+
description_parts.append("with a typical lifespan of 10-12 years")
|
| 220 |
+
except Exception as e:
|
| 221 |
+
print(f"Error processing lifespan for {breed}: {str(e)}")
|
| 222 |
+
description_parts.append("with a typical lifespan of 10-12 years")
|
| 223 |
+
|
| 224 |
+
# 創建全面的描述
|
| 225 |
+
full_description = '. '.join(description_parts) + '.'
|
| 226 |
+
|
| 227 |
+
# 添加全面的關鍵字以便更好的語義匹配
|
| 228 |
+
keywords = []
|
| 229 |
+
|
| 230 |
+
# 基本品種名稱關鍵字
|
| 231 |
+
keywords.extend([word.lower() for word in breed_display_name.split()])
|
| 232 |
+
|
| 233 |
+
# 氣質關鍵字
|
| 234 |
+
if temperament:
|
| 235 |
+
keywords.extend([word.lower().strip(',') for word in temperament.split()])
|
| 236 |
+
|
| 237 |
+
# 基於尺寸的關鍵字
|
| 238 |
+
if 'small' in size or 'tiny' in size:
|
| 239 |
+
keywords.extend(['small', 'tiny', 'compact', 'little', 'apartment', 'indoor', 'lap'])
|
| 240 |
+
elif 'large' in size or 'giant' in size:
|
| 241 |
+
keywords.extend(['large', 'big', 'giant', 'huge', 'yard', 'space', 'outdoor'])
|
| 242 |
+
else:
|
| 243 |
+
keywords.extend(['medium', 'moderate', 'average', 'balanced'])
|
| 244 |
+
|
| 245 |
+
# 活動水平關鍵字
|
| 246 |
+
exercise_needs = breed_info.get('Exercise Needs', 'moderate').lower()
|
| 247 |
+
if 'high' in exercise_needs:
|
| 248 |
+
keywords.extend(['active', 'energetic', 'exercise', 'outdoor', 'hiking', 'running', 'athletic'])
|
| 249 |
+
elif 'low' in exercise_needs:
|
| 250 |
+
keywords.extend(['calm', 'low-energy', 'indoor', 'relaxed', 'couch', 'sedentary'])
|
| 251 |
+
else:
|
| 252 |
+
keywords.extend(['moderate', 'balanced', 'walks', 'regular'])
|
| 253 |
+
|
| 254 |
+
# 噪音水平關鍵字
|
| 255 |
+
noise_level = noise_info.get('noise_level', 'moderate').lower()
|
| 256 |
+
if 'quiet' in noise_level or 'low' in noise_level:
|
| 257 |
+
keywords.extend(['quiet', 'silent', 'calm', 'peaceful', 'low-noise'])
|
| 258 |
+
elif 'high' in noise_level or 'loud' in noise_level:
|
| 259 |
+
keywords.extend(['vocal', 'barking', 'loud', 'alert', 'watchdog'])
|
| 260 |
+
|
| 261 |
+
# 居住情況關鍵字
|
| 262 |
+
if size in ['small', 'tiny'] and 'low' in exercise_needs:
|
| 263 |
+
keywords.extend(['apartment', 'city', 'urban', 'small-space'])
|
| 264 |
+
if size in ['large', 'giant'] or 'high' in exercise_needs:
|
| 265 |
+
keywords.extend(['house', 'yard', 'suburban', 'rural', 'space'])
|
| 266 |
+
|
| 267 |
+
# 家庭關鍵字
|
| 268 |
+
good_with_children = breed_info.get('Good with Children', 'Yes')
|
| 269 |
+
if good_with_children == 'Yes':
|
| 270 |
+
keywords.extend(['family', 'children', 'kids', 'friendly', 'gentle'])
|
| 271 |
+
|
| 272 |
+
# 智力和可訓練性關鍵字(從資料庫描述挖掘)
|
| 273 |
+
if intelligence_keywords:
|
| 274 |
+
keywords.extend([word.lower() for phrase in intelligence_keywords for word in phrase.split()])
|
| 275 |
+
|
| 276 |
+
# 美容相關關鍵字(增強)
|
| 277 |
+
grooming_needs = breed_info.get('Grooming Needs', 'moderate').lower()
|
| 278 |
+
if 'high' in grooming_needs:
|
| 279 |
+
keywords.extend(['high-maintenance', 'professional-grooming', 'daily-brushing', 'coat-care'])
|
| 280 |
+
elif 'low' in grooming_needs:
|
| 281 |
+
keywords.extend(['low-maintenance', 'minimal-grooming', 'easy-care', 'wash-and-go'])
|
| 282 |
+
else:
|
| 283 |
+
keywords.extend(['moderate-grooming', 'weekly-brushing', 'regular-care'])
|
| 284 |
+
|
| 285 |
+
# 基於壽命的關鍵字
|
| 286 |
+
lifespan = breed_info.get('Lifespan', '10-12 years')
|
| 287 |
+
if lifespan and isinstance(lifespan, str):
|
| 288 |
+
try:
|
| 289 |
+
# 從壽命字符串中提取年數(例如 "10-12 years" 或 "12-15 years")
|
| 290 |
+
import re
|
| 291 |
+
years = re.findall(r'\d+', lifespan)
|
| 292 |
+
if years:
|
| 293 |
+
avg_years = sum(int(y) for y in years) / len(years)
|
| 294 |
+
if avg_years >= 14:
|
| 295 |
+
keywords.extend(['long-lived', 'longevity', 'durable', 'healthy-lifespan'])
|
| 296 |
+
elif avg_years <= 8:
|
| 297 |
+
keywords.extend(['shorter-lifespan', 'health-considerations', 'special-care'])
|
| 298 |
+
else:
|
| 299 |
+
keywords.extend(['average-lifespan', 'moderate-longevity'])
|
| 300 |
+
except:
|
| 301 |
+
keywords.extend(['average-lifespan'])
|
| 302 |
+
|
| 303 |
+
# 將關鍵字添加到描述中���便更好的語義匹配
|
| 304 |
+
unique_keywords = list(set(keywords))
|
| 305 |
+
keyword_text = ' '.join(unique_keywords)
|
| 306 |
+
full_description += f" Additional context: {keyword_text}"
|
| 307 |
+
|
| 308 |
+
return full_description
|
| 309 |
+
|
| 310 |
+
except Exception as e:
|
| 311 |
+
print(f"Error creating description for {breed}: {str(e)}")
|
| 312 |
+
return f"{breed.replace('_', ' ')} is a dog breed with unique characteristics."
|
| 313 |
+
|
| 314 |
+
def _build_breed_vectors(self):
|
| 315 |
+
"""為所有品種建立向量表示 - 延遲調用當需要時"""
|
| 316 |
+
try:
|
| 317 |
+
print("Building breed vector database...")
|
| 318 |
+
|
| 319 |
+
# 初始化模型如果尚未完成
|
| 320 |
+
if self.sbert_model is None:
|
| 321 |
+
self._initialize_model()
|
| 322 |
+
|
| 323 |
+
# 如果模型不可用則跳過
|
| 324 |
+
if self.sbert_model is None:
|
| 325 |
+
print("SBERT model not available, skipping vector building")
|
| 326 |
+
return
|
| 327 |
+
|
| 328 |
+
for breed in self.breed_list:
|
| 329 |
+
description = self._create_breed_description(breed)
|
| 330 |
+
|
| 331 |
+
# 生成嵌入向量
|
| 332 |
+
embedding = self.sbert_model.encode(description, convert_to_tensor=False)
|
| 333 |
+
|
| 334 |
+
# 獲取品種特徵
|
| 335 |
+
breed_info = get_dog_description(breed)
|
| 336 |
+
characteristics = {
|
| 337 |
+
'size': breed_info.get('Size', 'Medium') if breed_info else 'Medium',
|
| 338 |
+
'exercise_needs': breed_info.get('Exercise Needs', 'Moderate') if breed_info else 'Moderate',
|
| 339 |
+
'grooming_needs': breed_info.get('Grooming Needs', 'Moderate') if breed_info else 'Moderate',
|
| 340 |
+
'good_with_children': breed_info.get('Good with Children', 'Yes') if breed_info else 'Yes',
|
| 341 |
+
'temperament': breed_info.get('Temperament', '') if breed_info else ''
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
self.breed_vectors[breed] = BreedDescriptionVector(
|
| 345 |
+
breed_name=breed,
|
| 346 |
+
description_text=description,
|
| 347 |
+
embedding=embedding,
|
| 348 |
+
characteristics=characteristics
|
| 349 |
+
)
|
| 350 |
+
|
| 351 |
+
print(f"Successfully built {len(self.breed_vectors)} breed vectors")
|
| 352 |
+
|
| 353 |
+
except Exception as e:
|
| 354 |
+
print(f"Error building breed vectors: {str(e)}")
|
| 355 |
+
print(traceback.format_exc())
|
| 356 |
+
raise
|
| 357 |
+
|
| 358 |
+
def get_breed_vectors(self) -> Dict[str, BreedDescriptionVector]:
|
| 359 |
+
"""獲取所有品種向量"""
|
| 360 |
+
# 確保向量已建構
|
| 361 |
+
if not self.breed_vectors:
|
| 362 |
+
self._build_breed_vectors()
|
| 363 |
+
return self.breed_vectors
|
| 364 |
+
|
| 365 |
+
def get_sbert_model(self) -> Optional[SentenceTransformer]:
|
| 366 |
+
"""獲取 SBERT 模型"""
|
| 367 |
+
return self.sbert_model
|
| 368 |
+
|
| 369 |
+
def get_breed_list(self) -> List[str]:
|
| 370 |
+
"""獲取品種清單"""
|
| 371 |
+
return self.breed_list
|
| 372 |
+
|
| 373 |
+
def is_model_available(self) -> bool:
|
| 374 |
+
"""檢查 SBERT 模型是否可用"""
|
| 375 |
+
return self.sbert_model is not None
|
| 376 |
+
|
| 377 |
+
def encode_text(self, text: str) -> np.ndarray:
|
| 378 |
+
"""使用 SBERT 模型編碼文本"""
|
| 379 |
+
# 初始化模型如果尚未完成
|
| 380 |
+
if self.sbert_model is None:
|
| 381 |
+
self._initialize_model()
|
| 382 |
+
|
| 383 |
+
if self.sbert_model is None:
|
| 384 |
+
raise RuntimeError("SBERT model not available")
|
| 385 |
+
return self.sbert_model.encode(text, convert_to_tensor=False)
|
user_query_analyzer.py
ADDED
|
@@ -0,0 +1,511 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import random
|
| 2 |
+
import hashlib
|
| 3 |
+
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
|
| 11 |
+
from sklearn.metrics.pairwise import cosine_similarity
|
| 12 |
+
from dog_database import get_dog_description
|
| 13 |
+
from breed_health_info import breed_health_info
|
| 14 |
+
from breed_noise_info import breed_noise_info
|
| 15 |
+
from scoring_calculation_system import UserPreferences, calculate_compatibility_score, UnifiedScoringSystem, calculate_unified_breed_scores
|
| 16 |
+
from query_understanding import QueryUnderstandingEngine, analyze_user_query
|
| 17 |
+
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 |
+
"""
|
| 24 |
+
用戶查詢分析器
|
| 25 |
+
專門處理用戶輸入分析、生活方式關鍵字提取和偏好解析
|
| 26 |
+
"""
|
| 27 |
+
|
| 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,
|
| 34 |
+
'third': 0.5, 'least': 0.3, 'dislike': 0.2
|
| 35 |
+
}
|
| 36 |
+
self.stop_words = {
|
| 37 |
+
'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by',
|
| 38 |
+
'from', 'up', 'about', 'into', 'through', 'during', 'before', 'after', 'above', 'below',
|
| 39 |
+
'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 'do', 'does', 'did',
|
| 40 |
+
'will', 'would', 'could', 'should', 'may', 'might', 'must', 'can', 'i', 'me', 'my', 'myself',
|
| 41 |
+
'we', 'our', 'ours', 'ourselves', 'you', 'your', 'yours', 'yourself', 'yourselves', 'he',
|
| 42 |
+
'him', 'his', 'himself', 'she', 'her', 'hers', 'herself', 'it', 'its', 'itself', 'they',
|
| 43 |
+
'them', 'their', 'theirs', 'themselves'
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
def parse_comparative_preferences(self, user_input: str) -> Dict[str, float]:
|
| 47 |
+
"""解析比較性偏好表達"""
|
| 48 |
+
breed_scores = {}
|
| 49 |
+
|
| 50 |
+
# 標準化輸入
|
| 51 |
+
text = user_input.lower()
|
| 52 |
+
|
| 53 |
+
# 找到品種名稱和偏好關鍵字
|
| 54 |
+
for breed in self.breed_list:
|
| 55 |
+
breed_display = breed.replace('_', ' ').lower()
|
| 56 |
+
breed_words = breed_display.split()
|
| 57 |
+
|
| 58 |
+
# 檢查是否提到此品種
|
| 59 |
+
breed_mentioned = False
|
| 60 |
+
for word in breed_words:
|
| 61 |
+
if word in text:
|
| 62 |
+
breed_mentioned = True
|
| 63 |
+
break
|
| 64 |
+
|
| 65 |
+
if breed_mentioned:
|
| 66 |
+
# 在附近找到偏好關鍵字
|
| 67 |
+
breed_score = 0.5 # 預設分數
|
| 68 |
+
|
| 69 |
+
# 在品種名稱 50 字符內尋找關鍵字
|
| 70 |
+
breed_pos = text.find(breed_words[0])
|
| 71 |
+
if breed_pos != -1:
|
| 72 |
+
# 檢查背景中的關鍵字
|
| 73 |
+
context_start = max(0, breed_pos - 50)
|
| 74 |
+
context_end = min(len(text), breed_pos + 50)
|
| 75 |
+
context = text[context_start:context_end]
|
| 76 |
+
|
| 77 |
+
for keyword, score in self.comparative_keywords.items():
|
| 78 |
+
if keyword in context:
|
| 79 |
+
breed_score = max(breed_score, score)
|
| 80 |
+
|
| 81 |
+
breed_scores[breed] = breed_score
|
| 82 |
+
|
| 83 |
+
return breed_scores
|
| 84 |
+
|
| 85 |
+
def extract_lifestyle_keywords(self, user_input: str) -> Dict[str, List[str]]:
|
| 86 |
+
"""增強的生活方式關鍵字提取,具有更好的模式匹配"""
|
| 87 |
+
keywords = {
|
| 88 |
+
'living_space': [],
|
| 89 |
+
'activity_level': [],
|
| 90 |
+
'family_situation': [],
|
| 91 |
+
'noise_preference': [],
|
| 92 |
+
'size_preference': [],
|
| 93 |
+
'care_level': [],
|
| 94 |
+
'special_needs': [],
|
| 95 |
+
'intelligence_preference': [],
|
| 96 |
+
'grooming_preference': [],
|
| 97 |
+
'lifespan_preference': [],
|
| 98 |
+
'temperament_preference': [],
|
| 99 |
+
'experience_level': []
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
text = user_input.lower()
|
| 103 |
+
|
| 104 |
+
# 增強居住空間檢測
|
| 105 |
+
apartment_terms = ['apartment', 'flat', 'condo', 'small space', 'city living', 'urban', 'no yard', 'indoor']
|
| 106 |
+
house_terms = ['house', 'yard', 'garden', 'backyard', 'large space', 'suburban', 'rural', 'farm']
|
| 107 |
+
|
| 108 |
+
if any(term in text for term in apartment_terms):
|
| 109 |
+
keywords['living_space'].append('apartment')
|
| 110 |
+
if any(term in text for term in house_terms):
|
| 111 |
+
keywords['living_space'].append('house')
|
| 112 |
+
|
| 113 |
+
# 增強活動水平檢測
|
| 114 |
+
high_activity = ['active', 'energetic', 'exercise', 'hiking', 'running', 'outdoor', 'sports', 'jogging',
|
| 115 |
+
'athletic', 'adventure', 'vigorous', 'high energy', 'workout']
|
| 116 |
+
low_activity = ['calm', 'lazy', 'indoor', 'low energy', 'couch', 'sedentary', 'relaxed',
|
| 117 |
+
'peaceful', 'quiet lifestyle', 'minimal exercise']
|
| 118 |
+
moderate_activity = ['moderate', 'walk', 'daily walks', 'light exercise']
|
| 119 |
+
|
| 120 |
+
if any(term in text for term in high_activity):
|
| 121 |
+
keywords['activity_level'].append('high')
|
| 122 |
+
if any(term in text for term in low_activity):
|
| 123 |
+
keywords['activity_level'].append('low')
|
| 124 |
+
if any(term in text for term in moderate_activity):
|
| 125 |
+
keywords['activity_level'].append('moderate')
|
| 126 |
+
|
| 127 |
+
# 增強家庭情況檢測
|
| 128 |
+
children_terms = ['children', 'kids', 'family', 'child', 'toddler', 'baby', 'teenage', 'school age']
|
| 129 |
+
elderly_terms = ['elderly', 'senior', 'old', 'retirement', 'aged', 'mature']
|
| 130 |
+
single_terms = ['single', 'alone', 'individual', 'solo', 'myself']
|
| 131 |
+
|
| 132 |
+
if any(term in text for term in children_terms):
|
| 133 |
+
keywords['family_situation'].append('children')
|
| 134 |
+
if any(term in text for term in elderly_terms):
|
| 135 |
+
keywords['family_situation'].append('elderly')
|
| 136 |
+
if any(term in text for term in single_terms):
|
| 137 |
+
keywords['family_situation'].append('single')
|
| 138 |
+
|
| 139 |
+
# 增強噪音偏好檢測
|
| 140 |
+
quiet_terms = ['quiet', 'silent', 'noise-sensitive', 'peaceful', 'no barking', 'minimal noise',
|
| 141 |
+
'soft-spoken', 'calm', 'tranquil']
|
| 142 |
+
noise_ok_terms = ['loud', 'barking ok', 'noise tolerant', 'vocal', 'doesn\'t matter']
|
| 143 |
+
|
| 144 |
+
if any(term in text for term in quiet_terms):
|
| 145 |
+
keywords['noise_preference'].append('low')
|
| 146 |
+
if any(term in text for term in noise_ok_terms):
|
| 147 |
+
keywords['noise_preference'].append('high')
|
| 148 |
+
|
| 149 |
+
# 增強體型偏好檢測
|
| 150 |
+
small_terms = ['small', 'tiny', 'little', 'compact', 'miniature', 'toy', 'lap dog']
|
| 151 |
+
large_terms = ['large', 'big', 'giant', 'huge', 'massive', 'great']
|
| 152 |
+
medium_terms = ['medium', 'moderate size', 'average', 'mid-sized']
|
| 153 |
+
|
| 154 |
+
if any(term in text for term in small_terms):
|
| 155 |
+
keywords['size_preference'].append('small')
|
| 156 |
+
if any(term in text for term in large_terms):
|
| 157 |
+
keywords['size_preference'].append('large')
|
| 158 |
+
if any(term in text for term in medium_terms):
|
| 159 |
+
keywords['size_preference'].append('medium')
|
| 160 |
+
|
| 161 |
+
# 增強照護水平檢測
|
| 162 |
+
low_care = ['low maintenance', 'easy care', 'simple', 'minimal grooming', 'wash and go']
|
| 163 |
+
high_care = ['high maintenance', 'grooming', 'care intensive', 'professional grooming', 'daily brushing']
|
| 164 |
+
|
| 165 |
+
if any(term in text for term in low_care):
|
| 166 |
+
keywords['care_level'].append('low')
|
| 167 |
+
if any(term in text for term in high_care):
|
| 168 |
+
keywords['care_level'].append('high')
|
| 169 |
+
|
| 170 |
+
# 智力偏好檢測(新增)
|
| 171 |
+
smart_terms = ['smart', 'intelligent', 'clever', 'bright', 'quick learner', 'easy to train', 'trainable', 'genius', 'brilliant']
|
| 172 |
+
independent_terms = ['independent', 'stubborn', 'strong-willed', 'less trainable', 'thinks for themselves']
|
| 173 |
+
|
| 174 |
+
if any(term in text for term in smart_terms):
|
| 175 |
+
keywords['intelligence_preference'].append('high')
|
| 176 |
+
if any(term in text for term in independent_terms):
|
| 177 |
+
keywords['intelligence_preference'].append('independent')
|
| 178 |
+
|
| 179 |
+
# 美容偏好檢測(新增)
|
| 180 |
+
low_grooming_terms = ['low grooming', 'minimal grooming', 'easy care', 'wash and wear', 'no grooming', 'simple coat']
|
| 181 |
+
high_grooming_terms = ['high grooming', 'professional grooming', 'lots of care', 'high maintenance coat', 'daily brushing', 'regular grooming']
|
| 182 |
+
|
| 183 |
+
if any(term in text for term in low_grooming_terms):
|
| 184 |
+
keywords['grooming_preference'].append('low')
|
| 185 |
+
if any(term in text for term in high_grooming_terms):
|
| 186 |
+
keywords['grooming_preference'].append('high')
|
| 187 |
+
|
| 188 |
+
# 壽命偏好檢測(新增)
|
| 189 |
+
long_lived_terms = ['long lived', 'long lifespan', 'live long', 'many years', '15+ years', 'longevity']
|
| 190 |
+
healthy_terms = ['healthy breed', 'few health issues', 'robust', 'hardy', 'strong constitution']
|
| 191 |
+
|
| 192 |
+
if any(term in text for term in long_lived_terms):
|
| 193 |
+
keywords['lifespan_preference'].append('long')
|
| 194 |
+
if any(term in text for term in healthy_terms):
|
| 195 |
+
keywords['lifespan_preference'].append('healthy')
|
| 196 |
+
|
| 197 |
+
# 氣質偏好檢測(新增)
|
| 198 |
+
gentle_terms = ['gentle', 'calm', 'peaceful', 'laid back', 'chill', 'mellow', 'docile']
|
| 199 |
+
playful_terms = ['playful', 'energetic', 'fun', 'active personality', 'lively', 'spirited', 'bouncy']
|
| 200 |
+
protective_terms = ['protective', 'guard', 'watchdog', 'alert', 'vigilant', 'defensive']
|
| 201 |
+
friendly_terms = ['friendly', 'social', 'outgoing', 'loves people', 'sociable', 'gregarious']
|
| 202 |
+
|
| 203 |
+
if any(term in text for term in gentle_terms):
|
| 204 |
+
keywords['temperament_preference'].append('gentle')
|
| 205 |
+
if any(term in text for term in playful_terms):
|
| 206 |
+
keywords['temperament_preference'].append('playful')
|
| 207 |
+
if any(term in text for term in protective_terms):
|
| 208 |
+
keywords['temperament_preference'].append('protective')
|
| 209 |
+
if any(term in text for term in friendly_terms):
|
| 210 |
+
keywords['temperament_preference'].append('friendly')
|
| 211 |
+
|
| 212 |
+
# 經驗水平檢測(新增)
|
| 213 |
+
beginner_terms = ['first time', 'beginner', 'new to dogs', 'never had', 'novice', 'inexperienced']
|
| 214 |
+
advanced_terms = ['experienced', 'advanced', 'dog expert', 'many dogs before', 'professional', 'seasoned']
|
| 215 |
+
|
| 216 |
+
if any(term in text for term in beginner_terms):
|
| 217 |
+
keywords['experience_level'].append('beginner')
|
| 218 |
+
if any(term in text for term in advanced_terms):
|
| 219 |
+
keywords['experience_level'].append('advanced')
|
| 220 |
+
|
| 221 |
+
# 增強特殊需求檢測
|
| 222 |
+
guard_terms = ['guard', 'protection', 'security', 'watchdog', 'protective', 'defender']
|
| 223 |
+
companion_terms = ['therapy', 'emotional support', 'companion', 'comfort', 'lap dog', 'cuddly']
|
| 224 |
+
hypoallergenic_terms = ['hypoallergenic', 'allergies', 'non-shedding', 'allergy-friendly', 'no shed']
|
| 225 |
+
multi_pet_terms = ['good with cats', 'cat friendly', 'multi-pet', 'other animals']
|
| 226 |
+
|
| 227 |
+
if any(term in text for term in guard_terms):
|
| 228 |
+
keywords['special_needs'].append('guard')
|
| 229 |
+
if any(term in text for term in companion_terms):
|
| 230 |
+
keywords['special_needs'].append('companion')
|
| 231 |
+
if any(term in text for term in hypoallergenic_terms):
|
| 232 |
+
keywords['special_needs'].append('hypoallergenic')
|
| 233 |
+
if any(term in text for term in multi_pet_terms):
|
| 234 |
+
keywords['special_needs'].append('multi_pet')
|
| 235 |
+
|
| 236 |
+
return keywords
|
| 237 |
+
|
| 238 |
+
def preprocess_text(self, text: str) -> str:
|
| 239 |
+
"""預處理文本"""
|
| 240 |
+
# 轉換為小寫
|
| 241 |
+
text = text.lower()
|
| 242 |
+
|
| 243 |
+
# 移除特殊字符,保留字母、數字和基本標點
|
| 244 |
+
text = re.sub(r'[^\w\s\-\']', ' ', text)
|
| 245 |
+
|
| 246 |
+
# 標準化空格
|
| 247 |
+
text = ' '.join(text.split())
|
| 248 |
+
|
| 249 |
+
return text
|
| 250 |
+
|
| 251 |
+
def generate_search_keywords(self, text: str) -> List[str]:
|
| 252 |
+
"""
|
| 253 |
+
為語義搜索生成關鍵字
|
| 254 |
+
|
| 255 |
+
Args:
|
| 256 |
+
text: 輸入文本
|
| 257 |
+
|
| 258 |
+
Returns:
|
| 259 |
+
關鍵字列表
|
| 260 |
+
"""
|
| 261 |
+
text = self.preprocess_text(text)
|
| 262 |
+
keywords = []
|
| 263 |
+
|
| 264 |
+
try:
|
| 265 |
+
# 分詞並過濾停用詞
|
| 266 |
+
words = text.split()
|
| 267 |
+
for word in words:
|
| 268 |
+
if len(word) > 2 and word not in self.stop_words:
|
| 269 |
+
keywords.append(word)
|
| 270 |
+
|
| 271 |
+
# 提取重要短語
|
| 272 |
+
phrases = self._extract_phrases(text)
|
| 273 |
+
keywords.extend(phrases)
|
| 274 |
+
|
| 275 |
+
# 移除重複項
|
| 276 |
+
keywords = list(set(keywords))
|
| 277 |
+
|
| 278 |
+
return keywords
|
| 279 |
+
|
| 280 |
+
except Exception as e:
|
| 281 |
+
print(f"Error generating search keywords: {str(e)}")
|
| 282 |
+
return []
|
| 283 |
+
|
| 284 |
+
def _extract_phrases(self, text: str) -> List[str]:
|
| 285 |
+
"""
|
| 286 |
+
提取重要短語
|
| 287 |
+
|
| 288 |
+
Args:
|
| 289 |
+
text: 輸入文本
|
| 290 |
+
|
| 291 |
+
Returns:
|
| 292 |
+
短語列表
|
| 293 |
+
"""
|
| 294 |
+
phrases = []
|
| 295 |
+
|
| 296 |
+
# 定義重要短語模式
|
| 297 |
+
phrase_patterns = [
|
| 298 |
+
r'good with \w+',
|
| 299 |
+
r'apartment \w+',
|
| 300 |
+
r'family \w+',
|
| 301 |
+
r'exercise \w+',
|
| 302 |
+
r'grooming \w+',
|
| 303 |
+
r'noise \w+',
|
| 304 |
+
r'training \w+',
|
| 305 |
+
r'health \w+',
|
| 306 |
+
r'\w+ friendly',
|
| 307 |
+
r'\w+ tolerant',
|
| 308 |
+
r'\w+ maintenance',
|
| 309 |
+
r'\w+ energy',
|
| 310 |
+
r'\w+ barking',
|
| 311 |
+
r'\w+ shedding'
|
| 312 |
+
]
|
| 313 |
+
|
| 314 |
+
for pattern in phrase_patterns:
|
| 315 |
+
matches = re.findall(pattern, text)
|
| 316 |
+
phrases.extend(matches)
|
| 317 |
+
|
| 318 |
+
return phrases
|
| 319 |
+
|
| 320 |
+
def analyze_sentiment(self, text: str) -> Dict[str, float]:
|
| 321 |
+
"""
|
| 322 |
+
分析文本情感
|
| 323 |
+
|
| 324 |
+
Args:
|
| 325 |
+
text: 輸入文本
|
| 326 |
+
|
| 327 |
+
Returns:
|
| 328 |
+
情感分析結果
|
| 329 |
+
"""
|
| 330 |
+
# 簡化的情感分析實現
|
| 331 |
+
positive_words = [
|
| 332 |
+
'love', 'like', 'prefer', 'enjoy', 'want', 'need', 'looking for',
|
| 333 |
+
'good', 'great', 'excellent', 'perfect', 'wonderful', 'amazing'
|
| 334 |
+
]
|
| 335 |
+
|
| 336 |
+
negative_words = [
|
| 337 |
+
'hate', 'dislike', 'avoid', 'don\'t want', 'no', 'not',
|
| 338 |
+
'bad', 'terrible', 'awful', 'horrible', 'worst', 'never'
|
| 339 |
+
]
|
| 340 |
+
|
| 341 |
+
words = text.lower().split()
|
| 342 |
+
positive_count = sum(1 for word in words if word in positive_words)
|
| 343 |
+
negative_count = sum(1 for word in words if word in negative_words)
|
| 344 |
+
total_words = len(words)
|
| 345 |
+
|
| 346 |
+
if total_words == 0:
|
| 347 |
+
return {'positive': 0.5, 'negative': 0.5, 'neutral': 0.0}
|
| 348 |
+
|
| 349 |
+
positive_score = positive_count / total_words
|
| 350 |
+
negative_score = negative_count / total_words
|
| 351 |
+
neutral_score = max(0, 1 - positive_score - negative_score)
|
| 352 |
+
|
| 353 |
+
return {
|
| 354 |
+
'positive': positive_score,
|
| 355 |
+
'negative': negative_score,
|
| 356 |
+
'neutral': neutral_score
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
def parse_user_requirements(self, user_input: str) -> Dict[str, Any]:
|
| 360 |
+
"""更準確地解析用戶需求"""
|
| 361 |
+
requirements = {
|
| 362 |
+
'living_space': None,
|
| 363 |
+
'exercise_level': None,
|
| 364 |
+
'preferred_size': None,
|
| 365 |
+
'noise_tolerance': None
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
input_lower = user_input.lower()
|
| 369 |
+
|
| 370 |
+
# 居住空間檢測
|
| 371 |
+
if 'apartment' in input_lower or 'small' in input_lower:
|
| 372 |
+
requirements['living_space'] = 'apartment'
|
| 373 |
+
elif 'large house' in input_lower or 'big' in input_lower:
|
| 374 |
+
requirements['living_space'] = 'large_house'
|
| 375 |
+
elif 'medium' in input_lower:
|
| 376 |
+
requirements['living_space'] = 'medium_house'
|
| 377 |
+
|
| 378 |
+
# 運動水平檢測
|
| 379 |
+
if "don't exercise" in input_lower or 'low exercise' in input_lower:
|
| 380 |
+
requirements['exercise_level'] = 'low'
|
| 381 |
+
elif any(term in input_lower for term in ['hiking', 'running', 'active']):
|
| 382 |
+
requirements['exercise_level'] = 'high'
|
| 383 |
+
elif '30 minutes' in input_lower or 'moderate' in input_lower:
|
| 384 |
+
requirements['exercise_level'] = 'moderate'
|
| 385 |
+
|
| 386 |
+
# 體型偏好檢測
|
| 387 |
+
if any(term in input_lower for term in ['small dog', 'tiny', 'toy']):
|
| 388 |
+
requirements['preferred_size'] = 'small'
|
| 389 |
+
elif any(term in input_lower for term in ['large dog', 'big dog']):
|
| 390 |
+
requirements['preferred_size'] = 'large'
|
| 391 |
+
elif 'medium' in input_lower:
|
| 392 |
+
requirements['preferred_size'] = 'medium'
|
| 393 |
+
|
| 394 |
+
return requirements
|
| 395 |
+
|
| 396 |
+
def analyze_user_description_enhanced(self, user_description: str) -> Dict[str, Any]:
|
| 397 |
+
"""增強用戶描述分析"""
|
| 398 |
+
text = user_description.lower()
|
| 399 |
+
analysis = {
|
| 400 |
+
'mentioned_breeds': [],
|
| 401 |
+
'lifestyle_keywords': {},
|
| 402 |
+
'preference_strength': {},
|
| 403 |
+
'constraint_requirements': [],
|
| 404 |
+
'user_context': {}
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
# 提取提及的品種
|
| 408 |
+
for breed in self.breed_list:
|
| 409 |
+
breed_display = breed.replace('_', ' ').lower()
|
| 410 |
+
if breed_display in text or any(word in text for word in breed_display.split()):
|
| 411 |
+
analysis['mentioned_breeds'].append(breed)
|
| 412 |
+
# 簡單偏好強度分析
|
| 413 |
+
if any(word in text for word in ['love', 'prefer', 'like', '喜歡', '最愛']):
|
| 414 |
+
analysis['preference_strength'][breed] = 0.8
|
| 415 |
+
else:
|
| 416 |
+
analysis['preference_strength'][breed] = 0.5
|
| 417 |
+
|
| 418 |
+
# 提取約束要求
|
| 419 |
+
if any(word in text for word in ['quiet', 'silent', 'no barking', '安靜']):
|
| 420 |
+
analysis['constraint_requirements'].append('low_noise')
|
| 421 |
+
if any(word in text for word in ['apartment', 'small space', '公寓']):
|
| 422 |
+
analysis['constraint_requirements'].append('apartment_suitable')
|
| 423 |
+
if any(word in text for word in ['children', 'kids', 'family', '小孩']):
|
| 424 |
+
analysis['constraint_requirements'].append('child_friendly')
|
| 425 |
+
|
| 426 |
+
# 提取用戶背景
|
| 427 |
+
analysis['user_context'] = {
|
| 428 |
+
'has_children': any(word in text for word in ['children', 'kids', '小孩']),
|
| 429 |
+
'living_space': 'apartment' if any(word in text for word in ['apartment', '公寓']) else 'house',
|
| 430 |
+
'activity_level': 'high' if any(word in text for word in ['active', 'energetic', '活躍']) else 'moderate',
|
| 431 |
+
'noise_sensitive': any(word in text for word in ['quiet', 'silent', '安靜']),
|
| 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':
|
| 438 |
+
"""從分析結果創建用戶偏好物件"""
|
| 439 |
+
context = analysis['user_context']
|
| 440 |
+
|
| 441 |
+
# 推斷居住空間類型
|
| 442 |
+
living_space = 'apartment' if context.get('living_space') == 'apartment' else 'house_small'
|
| 443 |
+
|
| 444 |
+
# 推斷院子權限
|
| 445 |
+
yard_access = 'no_yard' if living_space == 'apartment' else 'shared_yard'
|
| 446 |
+
|
| 447 |
+
# 推斷運動時間
|
| 448 |
+
activity_level = context.get('activity_level', 'moderate')
|
| 449 |
+
exercise_time_map = {'high': 120, 'moderate': 60, 'low': 30}
|
| 450 |
+
exercise_time = exercise_time_map.get(activity_level, 60)
|
| 451 |
+
|
| 452 |
+
# 推斷運動類型
|
| 453 |
+
exercise_type_map = {'high': 'active_training', 'moderate': 'moderate_activity', 'low': 'light_walks'}
|
| 454 |
+
exercise_type = exercise_type_map.get(activity_level, 'moderate_activity')
|
| 455 |
+
|
| 456 |
+
# 推斷噪音容忍度
|
| 457 |
+
noise_tolerance = 'low' if context.get('noise_sensitive', False) else 'medium'
|
| 458 |
+
|
| 459 |
+
return UserPreferences(
|
| 460 |
+
living_space=living_space,
|
| 461 |
+
yard_access=yard_access,
|
| 462 |
+
exercise_time=exercise_time,
|
| 463 |
+
exercise_type=exercise_type,
|
| 464 |
+
grooming_commitment='medium',
|
| 465 |
+
experience_level=context.get('experience_level', 'intermediate'),
|
| 466 |
+
time_availability='moderate',
|
| 467 |
+
has_children=context.get('has_children', False),
|
| 468 |
+
children_age='school_age' if context.get('has_children', False) else None,
|
| 469 |
+
noise_tolerance=noise_tolerance,
|
| 470 |
+
space_for_play=(living_space != 'apartment'),
|
| 471 |
+
other_pets=False,
|
| 472 |
+
climate='moderate',
|
| 473 |
+
health_sensitivity='medium',
|
| 474 |
+
barking_acceptance=noise_tolerance,
|
| 475 |
+
size_preference='no_preference'
|
| 476 |
+
)
|
| 477 |
+
|
| 478 |
+
def get_candidate_breeds_enhanced(self, analysis: Dict[str, Any]) -> List[str]:
|
| 479 |
+
"""獲取候選品種列表"""
|
| 480 |
+
candidate_breeds = set()
|
| 481 |
+
|
| 482 |
+
# 如果提及特定品種,優先包含
|
| 483 |
+
if analysis['mentioned_breeds']:
|
| 484 |
+
candidate_breeds.update(analysis['mentioned_breeds'])
|
| 485 |
+
|
| 486 |
+
# 根據約束要求過濾品種
|
| 487 |
+
if 'apartment_suitable' in analysis['constraint_requirements']:
|
| 488 |
+
apartment_suitable = [
|
| 489 |
+
'French_Bulldog', 'Cavalier_King_Charles_Spaniel', 'Boston_Terrier',
|
| 490 |
+
'Pug', 'Bichon_Frise', 'Cocker_Spaniel', 'Yorkshire_Terrier', 'Shih_Tzu'
|
| 491 |
+
]
|
| 492 |
+
candidate_breeds.update(breed for breed in apartment_suitable if breed in self.breed_list)
|
| 493 |
+
|
| 494 |
+
if 'child_friendly' in analysis['constraint_requirements']:
|
| 495 |
+
child_friendly = [
|
| 496 |
+
'Labrador_Retriever', 'Golden_Retriever', 'Beagle', 'Cavalier_King_Charles_Spaniel',
|
| 497 |
+
'Bichon_Frise', 'Poodle', 'Cocker_Spaniel'
|
| 498 |
+
]
|
| 499 |
+
candidate_breeds.update(breed for breed in child_friendly if breed in self.breed_list)
|
| 500 |
+
|
| 501 |
+
# 如果候選品種不足,添加更多通用品種
|
| 502 |
+
if len(candidate_breeds) < 20:
|
| 503 |
+
general_breeds = [
|
| 504 |
+
'Labrador_Retriever', 'German_Shepherd', 'Golden_Retriever', 'French_Bulldog',
|
| 505 |
+
'Bulldog', 'Poodle', 'Beagle', 'Rottweiler', 'Yorkshire_Terrier', 'Boston_Terrier',
|
| 506 |
+
'Border_Collie', 'Siberian_Husky', 'Cavalier_King_Charles_Spaniel', 'Boxer',
|
| 507 |
+
'Bichon_Frise', 'Cocker_Spaniel', 'Shih_Tzu', 'Pug', 'Chihuahua'
|
| 508 |
+
]
|
| 509 |
+
candidate_breeds.update(breed for breed in general_breeds if breed in self.breed_list)
|
| 510 |
+
|
| 511 |
+
return list(candidate_breeds)[:30] # 限制候選數量以提高效率
|