mata01 commited on
Commit
265cc9d
·
1 Parent(s): 85a11ff

feat: implement conversational chatbot with routing and chat history support

Browse files
app/api/v1/recommendations.py CHANGED
@@ -12,9 +12,15 @@ from pydantic import BaseModel
12
 
13
  router = APIRouter()
14
 
 
 
 
 
15
  class OutfitRecommendRequest(BaseModel):
16
- weather: str = "28C"
17
- event: str = "workplace"
 
 
18
 
19
  @router.post("/outfit")
20
  def recommend_outfit(
@@ -45,9 +51,11 @@ def recommend_outfit(
45
  for item in wardrobe
46
  ]
47
 
48
- recommendation = llm_service.recommend_outfits(
49
  user_profile=body_data,
50
  wardrobe_items=wardrobe_list,
 
 
51
  weather=req.weather,
52
  event=req.event
53
  )
 
12
 
13
  router = APIRouter()
14
 
15
+ class ChatMessage(BaseModel):
16
+ role: str
17
+ content: str
18
+
19
  class OutfitRecommendRequest(BaseModel):
20
+ message: str
21
+ history: List[ChatMessage] = []
22
+ weather: Optional[str] = "25C"
23
+ event: Optional[str] = None
24
 
25
  @router.post("/outfit")
26
  def recommend_outfit(
 
51
  for item in wardrobe
52
  ]
53
 
54
+ recommendation = llm_service.chat_and_recommend(
55
  user_profile=body_data,
56
  wardrobe_items=wardrobe_list,
57
+ message=req.message,
58
+ history=[{"role": m.role, "content": m.content} for m in req.history],
59
  weather=req.weather,
60
  event=req.event
61
  )
app/services/llm_service.py CHANGED
@@ -20,12 +20,33 @@ class LLMService:
20
  wardrobe_items: List[Dict[str, Any]],
21
  weather: str,
22
  event: str
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  ) -> Dict[str, Any]:
24
  if not self.enabled:
25
- # Mock mode for testing/fallback
 
 
26
  return {
27
- "recommendation": "Mock Outfit: Áo sơ mi trắng phối quần tây đen nhã nhặn phù hợp cho môi trường văn phòng/phỏng vấn.",
28
- "selected_item_ids": [str(item["id"]) for item in wardrobe_items[:2]] if wardrobe_items else []
 
29
  }
30
 
31
  # Structure wardrobe context
@@ -37,9 +58,16 @@ class LLMService:
37
  )
38
  wardrobe_context = "\n".join(wardrobe_desc)
39
 
 
 
 
 
 
 
 
40
  prompt = f"""
41
- Bạn là một Trợ lý thời trang cá nhân AI chuyên nghiệp.
42
- Hãy phân tích dữ liệu người dùng đưa ra gợi ý phối đồ từ tủ quần áo ảo của họ.
43
 
44
  Thông tin người dùng:
45
  - Giới tính: {user_profile.get('gender')}
@@ -49,21 +77,35 @@ class LLMService:
49
  - Vòng eo: {user_profile.get('waist_cm') or 'Chưa cung cấp'} cm
50
  - Vòng mông: {user_profile.get('hips_cm') or 'Chưa cung cấp'} cm
51
 
52
- Ngữ cảnh hiện tại:
53
- - Thời tiết: {weather}
54
- - Sự kiện/Hoàn cảnh tham gia: {event}
55
 
56
  Danh sách tủ quần áo hiện có của người dùng:
57
  {wardrobe_context}
58
 
59
- Nhiệm vụ:
60
- 1. Đề xuất một bộ phối đồ hoàn hảo và hợp thời trang nhất cho người dùng trong hoàn cảnh trên.
61
- 2. Chọn chính xác các ID sản phẩm phù hợp từ danh sách tủ đồ của họ ở trên.
62
- 3. Giải thích ngắn gọn lý do phối đồ dựa trên vóc dáng và thời tiết.
63
-
64
- Hãy trả về kết quả dưới định dạng JSON duy nhất và hợp lệ sau:
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  {{
66
- "recommendation": "Chuỗi văn bản giải thích và mô tả bộ đồ phối gợi ý",
 
67
  "selected_item_ids": ["uuid-1", "uuid-2"]
68
  }}
69
  """
@@ -101,10 +143,13 @@ class LLMService:
101
  content_str = "\n".join(lines).strip()
102
 
103
  data = json.loads(content_str)
 
 
104
  return data
105
  except Exception as e:
106
  logging.error(f"Error calling Nvidia API: {e}")
107
  return {
 
108
  "recommendation": "Hệ thống gặp lỗi khi liên kết với AI gợi ý. Hãy thử lại sau.",
109
  "selected_item_ids": []
110
  }
 
20
  wardrobe_items: List[Dict[str, Any]],
21
  weather: str,
22
  event: str
23
+ ) -> Dict[str, Any]:
24
+ return self.chat_and_recommend(
25
+ user_profile=user_profile,
26
+ wardrobe_items=wardrobe_items,
27
+ message=event,
28
+ history=[],
29
+ weather=weather,
30
+ event=event
31
+ )
32
+
33
+ def chat_and_recommend(
34
+ self,
35
+ user_profile: Dict[str, Any],
36
+ wardrobe_items: List[Dict[str, Any]],
37
+ message: str,
38
+ history: List[Dict[str, str]],
39
+ weather: Optional[str] = "25C",
40
+ event: Optional[str] = None
41
  ) -> Dict[str, Any]:
42
  if not self.enabled:
43
+ # Simple keyword-based classifier in mock mode
44
+ keywords = ["recommend", "gợi ý", "phối đồ", "mặc gì", "chọn đồ", "outfit", "wear"]
45
+ is_rec = any(kw in message.lower() for kw in keywords) or not history
46
  return {
47
+ "is_recommendation": is_rec,
48
+ "recommendation": "Mock Outfit: Áo mi trắng phối quần tây đen nhã nhặn phù hợp cho môi trường văn phòng/phỏng vấn." if is_rec else f"Tôi hiểu bạn đang nói về: '{message}'. Trong vai trò trợ lý thời trang, tôi khuyên bạn nên tự tin thể hiện phong cách của mình!",
49
+ "selected_item_ids": [str(item["id"]) for item in wardrobe_items[:2]] if (wardrobe_items and is_rec) else []
50
  }
51
 
52
  # Structure wardrobe context
 
58
  )
59
  wardrobe_context = "\n".join(wardrobe_desc)
60
 
61
+ # Structure chat history context
62
+ history_desc = []
63
+ for msg in history:
64
+ role_label = "User" if msg["role"] == "user" else "Stylist"
65
+ history_desc.append(f"{role_label}: {msg['content']}")
66
+ history_context = "\n".join(history_desc) if history_desc else "Chưa có lịch sử trò chuyện."
67
+
68
  prompt = f"""
69
+ Bạn là một Trợ lý thời trang cá nhân AI chuyên nghiệp (tên là Aura).
70
+ Hãy phân tích dữ liệu người dùng, tủ đồ hiện có, lịch sử trò chuyện tin nhắn mới nhất để phản hồi phù hợp.
71
 
72
  Thông tin người dùng:
73
  - Giới tính: {user_profile.get('gender')}
 
77
  - Vòng eo: {user_profile.get('waist_cm') or 'Chưa cung cấp'} cm
78
  - Vòng mông: {user_profile.get('hips_cm') or 'Chưa cung cấp'} cm
79
 
80
+ Ngữ cảnh hiện tại (nếu có):
81
+ - Thời tiết: {weather or 'Không xác định'}
82
+ - Sự kiện/Hoàn cảnh: {event or 'Không xác định'}
83
 
84
  Danh sách tủ quần áo hiện có của người dùng:
85
  {wardrobe_context}
86
 
87
+ Lịch sử trò chuyện gần đây:
88
+ {history_context}
89
+
90
+ Tin nhắn mới từ người dùng:
91
+ "{message}"
92
+
93
+ Nhiệm vụ của bạn:
94
+ 1. Phân loại ý định của người dùng (User Intent Routing):
95
+ - Nếu người dùng muốn gợi ý phối đồ mới, phối đồ lại, thay thế/thay đổi trang phục nào đó trong set đồ hiện tại, hoặc tìm đồ phối hợp từ tủ quần áo, hãy thiết lập "is_recommendation" thành true.
96
+ - Nếu người dùng chỉ đang bàn luận, hỏi đáp chung về thời trang, hỏi lý do lựa chọn sản phẩm trước đó (ví dụ: "Tại sao bạn chọn áo thun này?", "Quần này hợp với giày gì?", "Có nên sơ vin không?"), hoặc tán gẫu thông thường mà không cần thay đổi hay tạo mới set đồ đề xuất, hãy thiết lập "is_recommendation" thành false.
97
+ 2. Nếu "is_recommendation" là true:
98
+ - Hãy gợi ý một set phối đồ hoàn hảo và hợp thời trang nhất từ tủ đồ.
99
+ - Chọn chính xác các ID sản phẩm phù hợp từ danh sách tủ đồ của họ.
100
+ - Điền danh sách ID này vào "selected_item_ids".
101
+ 3. Nếu "is_recommendation" là false:
102
+ - Trả lời người dùng một cách thân thiện, chi tiết và có kiến thức chuyên môn về thời trang dựa trên câu hỏi của họ.
103
+ - Đặt "selected_item_ids" là [] (mảng rỗng).
104
+
105
+ Hãy trả về kết quả dưới định dạng JSON duy nhất và hợp lệ sau (không chứa ký tự thừa bên ngoài):
106
  {{
107
+ "is_recommendation": true,
108
+ "recommendation": "Chuỗi văn bản phản hồi người dùng",
109
  "selected_item_ids": ["uuid-1", "uuid-2"]
110
  }}
111
  """
 
143
  content_str = "\n".join(lines).strip()
144
 
145
  data = json.loads(content_str)
146
+ if "is_recommendation" not in data:
147
+ data["is_recommendation"] = len(data.get("selected_item_ids", [])) > 0
148
  return data
149
  except Exception as e:
150
  logging.error(f"Error calling Nvidia API: {e}")
151
  return {
152
+ "is_recommendation": False,
153
  "recommendation": "Hệ thống gặp lỗi khi liên kết với AI gợi ý. Hãy thử lại sau.",
154
  "selected_item_ids": []
155
  }
tests/test_llm.py CHANGED
@@ -10,6 +10,7 @@ def test_llm_service_mock_mode():
10
  service = LLMService()
11
  assert service.enabled is False
12
 
 
13
  res = service.recommend_outfits(
14
  user_profile={"gender": "male", "height_cm": 175, "weight_kg": 70},
15
  wardrobe_items=[{"id": "item1", "category": "shirt", "color_hex": "#ffffff"}],
@@ -20,6 +21,36 @@ def test_llm_service_mock_mode():
20
  assert "Mock Outfit" in res["recommendation"]
21
  assert "item1" in res["selected_item_ids"]
22
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  def test_llm_service_nvidia_api_success():
24
  with patch("app.services.llm_service.settings") as mock_settings:
25
  mock_settings.NVIDIA_API_KEY = "mock_key"
@@ -35,21 +66,23 @@ def test_llm_service_nvidia_api_success():
35
  "choices": [
36
  {
37
  "message": {
38
- "content": '{\n "recommendation": "Gợi ý: Áo thun trắng phối quần short năng động.",\n "selected_item_ids": ["item1"]\n}'
39
  }
40
  }
41
  ]
42
  }
43
 
44
  with patch("requests.post", return_value=mock_response) as mock_post:
45
- res = service.recommend_outfits(
46
  user_profile={"gender": "male", "height_cm": 175, "weight_kg": 70},
47
  wardrobe_items=[{"id": "item1", "category": "shirt", "color_hex": "#ffffff"}],
48
- weather="28C",
49
- event="workplace"
 
50
  )
51
 
52
  mock_post.assert_called_once()
 
53
  assert res["recommendation"] == "Gợi ý: Áo thun trắng phối quần short năng động."
54
  assert res["selected_item_ids"] == ["item1"]
55
 
@@ -62,13 +95,14 @@ def test_llm_service_nvidia_api_error_fallback():
62
  assert service.enabled is True
63
 
64
  with patch("requests.post", side_effect=Exception("API connection timeout")) as mock_post:
65
- res = service.recommend_outfits(
66
  user_profile={"gender": "male", "height_cm": 175, "weight_kg": 70},
67
  wardrobe_items=[{"id": "item1", "category": "shirt", "color_hex": "#ffffff"}],
68
- weather="28C",
69
- event="workplace"
70
  )
71
 
72
  mock_post.assert_called_once()
 
73
  assert "Hệ thống gặp lỗi" in res["recommendation"]
74
  assert res["selected_item_ids"] == []
 
10
  service = LLMService()
11
  assert service.enabled is False
12
 
13
+ # Test old recommend_outfits wrapper
14
  res = service.recommend_outfits(
15
  user_profile={"gender": "male", "height_cm": 175, "weight_kg": 70},
16
  wardrobe_items=[{"id": "item1", "category": "shirt", "color_hex": "#ffffff"}],
 
21
  assert "Mock Outfit" in res["recommendation"]
22
  assert "item1" in res["selected_item_ids"]
23
 
24
+ def test_chat_and_recommend_mock_routing():
25
+ with patch("app.services.llm_service.settings") as mock_settings:
26
+ mock_settings.NVIDIA_API_KEY = None
27
+
28
+ service = LLMService()
29
+ assert service.enabled is False
30
+
31
+ # Test recommendation intent (should be true)
32
+ res_rec = service.chat_and_recommend(
33
+ user_profile={"gender": "male", "height_cm": 175, "weight_kg": 70},
34
+ wardrobe_items=[{"id": "item1", "category": "shirt", "color_hex": "#ffffff"}],
35
+ message="Gợi ý phối đồ đi chơi",
36
+ history=[],
37
+ weather="28C"
38
+ )
39
+ assert res_rec["is_recommendation"] is True
40
+ assert "item1" in res_rec["selected_item_ids"]
41
+
42
+ # Test non-recommendation/chat intent (should be false if there is history)
43
+ res_chat = service.chat_and_recommend(
44
+ user_profile={"gender": "male", "height_cm": 175, "weight_kg": 70},
45
+ wardrobe_items=[{"id": "item1", "category": "shirt", "color_hex": "#ffffff"}],
46
+ message="Tại sao chiếc áo này lại hợp với tôi?",
47
+ history=[{"role": "user", "content": "Mặc gì đây"}, {"role": "assistant", "content": "Áo sơ mi"}],
48
+ weather="28C"
49
+ )
50
+ assert res_chat["is_recommendation"] is False
51
+ assert "item1" not in res_chat["selected_item_ids"]
52
+ assert "Tại sao chiếc áo này lại hợp với tôi?" in res_chat["recommendation"]
53
+
54
  def test_llm_service_nvidia_api_success():
55
  with patch("app.services.llm_service.settings") as mock_settings:
56
  mock_settings.NVIDIA_API_KEY = "mock_key"
 
66
  "choices": [
67
  {
68
  "message": {
69
+ "content": '{\n "is_recommendation": true,\n "recommendation": "Gợi ý: Áo thun trắng phối quần short năng động.",\n "selected_item_ids": ["item1"]\n}'
70
  }
71
  }
72
  ]
73
  }
74
 
75
  with patch("requests.post", return_value=mock_response) as mock_post:
76
+ res = service.chat_and_recommend(
77
  user_profile={"gender": "male", "height_cm": 175, "weight_kg": 70},
78
  wardrobe_items=[{"id": "item1", "category": "shirt", "color_hex": "#ffffff"}],
79
+ message="Mặc gì hôm nay?",
80
+ history=[],
81
+ weather="28C"
82
  )
83
 
84
  mock_post.assert_called_once()
85
+ assert res["is_recommendation"] is True
86
  assert res["recommendation"] == "Gợi ý: Áo thun trắng phối quần short năng động."
87
  assert res["selected_item_ids"] == ["item1"]
88
 
 
95
  assert service.enabled is True
96
 
97
  with patch("requests.post", side_effect=Exception("API connection timeout")) as mock_post:
98
+ res = service.chat_and_recommend(
99
  user_profile={"gender": "male", "height_cm": 175, "weight_kg": 70},
100
  wardrobe_items=[{"id": "item1", "category": "shirt", "color_hex": "#ffffff"}],
101
+ message="Mặc gì hôm nay?",
102
+ history=[]
103
  )
104
 
105
  mock_post.assert_called_once()
106
+ assert res["is_recommendation"] is False
107
  assert "Hệ thống gặp lỗi" in res["recommendation"]
108
  assert res["selected_item_ids"] == []