truglpk3 commited on
Commit
445252b
·
1 Parent(s): 6701c72
.env CHANGED
@@ -1,7 +1,7 @@
1
  DEEPSEEK_API_KEY=sk-0fde229ff9854ada814c2553c787d721
2
 
3
- ELASTIC_API_KEY=MW9QZm1aa0JkWjBPRkFiaUFwc0Q6T1cycFlqbVVXbzR6OWM0Tm1CeW1GQQ
4
- ELASTIC_CLOUD_URL=https://datnvdb-a564ef.es.australiaeast.azure.elastic.cloud:443
5
  FOOD_DB_INDEX=food_v2_vdb
6
  POLICY_DB_INDEX=policy_vdb
7
 
 
1
  DEEPSEEK_API_KEY=sk-0fde229ff9854ada814c2553c787d721
2
 
3
+ ELASTIC_API_KEY="U0RkcElwc0JwbGxnYkIxOVIyYVo6WlpJMXpoUXNxZVdkT295cnpZRWdLUQ=="
4
+ ELASTIC_CLOUD_URL=https://my-elasticsearch-project-a94f0e.es.ap-southeast-1.aws.elastic.cloud:443
5
  FOOD_DB_INDEX=food_v2_vdb
6
  POLICY_DB_INDEX=policy_vdb
7
 
chatbot/agents/nodes/app_functions/find_candidates.py CHANGED
@@ -21,22 +21,23 @@ def find_replacement_candidates(state: SwapState):
21
  health_status = profile.get('healthStatus', '') # VD: Suy thận
22
 
23
  constraint_prompt = ""
24
- if restrictions:
25
  constraint_prompt += f"Yêu cầu bắt buộc: {restrictions}. "
26
- if health_status:
27
  constraint_prompt += f"Phù hợp người bệnh: {health_status}. "
28
- if diet_mode:
29
  constraint_prompt += f"Chế độ: {diet_mode}."
30
 
31
  # 1. Trích xuất ngữ cảnh từ món cũ
32
  role = food_old.get("role", "main") # VD: main, side, carb
 
33
  meal_type = food_old.get("assigned_meal", "trưa") # VD: trưa
34
  old_name = food_old.get("name", "")
35
  numerical_query = generate_numerical_constraints(profile, meal_type)
36
 
37
  # 2. Xây dựng Query tự nhiên để SelfQueryRetriever hiểu
38
  query = (
39
- f"Tìm các món ăn đóng vai trò '{role}' phù hợp cho bữa '{meal_type}'. "
40
  f"Khác với món '{old_name}'. "
41
  f"{constraint_prompt}"
42
  )
 
21
  health_status = profile.get('healthStatus', '') # VD: Suy thận
22
 
23
  constraint_prompt = ""
24
+ if restrictions not in ["Không có"]:
25
  constraint_prompt += f"Yêu cầu bắt buộc: {restrictions}. "
26
+ if health_status not in ["Khỏe mạnh", "Không có", "Bình thường", None]:
27
  constraint_prompt += f"Phù hợp người bệnh: {health_status}. "
28
+ if diet_mode not in ["Bình thường"]:
29
  constraint_prompt += f"Chế độ: {diet_mode}."
30
 
31
  # 1. Trích xuất ngữ cảnh từ món cũ
32
  role = food_old.get("role", "main") # VD: main, side, carb
33
+ vibe = food_old.get("retrieval_vibe", "Món ăn kèm cơ bản") # VD: món nhẹ nhàng, món giàu đạm
34
  meal_type = food_old.get("assigned_meal", "trưa") # VD: trưa
35
  old_name = food_old.get("name", "")
36
  numerical_query = generate_numerical_constraints(profile, meal_type)
37
 
38
  # 2. Xây dựng Query tự nhiên để SelfQueryRetriever hiểu
39
  query = (
40
+ f"Tìm các món ăn đóng vai trò '{role}' phù hợp cho bữa '{meal_type}'. Phong cách: '{vibe}'. "
41
  f"Khác với món '{old_name}'. "
42
  f"{constraint_prompt}"
43
  )
chatbot/agents/nodes/app_functions/generate_candidates.py CHANGED
@@ -3,6 +3,7 @@ import logging
3
  from chatbot.agents.states.state import AgentState
4
  from chatbot.agents.tools.food_retriever import food_retriever_50, docsearch
5
  from chatbot.knowledge.vibe import vibes_cooking, vibes_flavor, vibes_healthy, vibes_soup_veg, vibes_style
 
6
 
7
  STAPLE_IDS = ["112", "1852", "2236", "2386", "2388"]
8
 
@@ -51,11 +52,11 @@ def generate_food_candidates(state: AgentState):
51
  health_status = profile.get('healthStatus', '') # VD: Suy thận
52
 
53
  constraint_prompt = ""
54
- if restrictions:
55
  constraint_prompt += f"Yêu cầu bắt buộc: {restrictions}. "
56
  if health_status not in ["Khỏe mạnh", "Không có", "Bình thường", None]:
57
  constraint_prompt += f"Phù hợp người bệnh: {health_status}. "
58
- if diet_mode:
59
  constraint_prompt += f"Chế độ: {diet_mode}."
60
 
61
  prompt_templates = {
@@ -80,7 +81,10 @@ def generate_food_candidates(state: AgentState):
80
  final_query = f"{base_prompt} Phong cách: {vibe}.{' Ràng buộc: ' + numerical_query if numerical_query else ''}"
81
  logger.info(f"🔎 Query ({meal_type}): {final_query}")
82
 
 
83
  docs = food_retriever_50.invoke(final_query)
 
 
84
  if not docs:
85
  logger.warning(f"⚠️ Retriever trả về rỗng cho bữa: {meal_type}")
86
  continue
@@ -286,7 +290,7 @@ def rank_candidates(candidates, user_profile, meal_type):
286
  scored_list.sort(key=lambda x: x["health_score"], reverse=True)
287
 
288
  # # Debug: In Top 3
289
- # logger.info("🏆 Top 3 Món Tốt Nhất (Sau khi chấm điểm):")
290
  # for i, m in enumerate(scored_list[:3]):
291
  # logger.info(f" {i+1}. {m['name']} (Score: {m['health_score']}) | {m.get('score_reason')}")
292
 
 
3
  from chatbot.agents.states.state import AgentState
4
  from chatbot.agents.tools.food_retriever import food_retriever_50, docsearch
5
  from chatbot.knowledge.vibe import vibes_cooking, vibes_flavor, vibes_healthy, vibes_soup_veg, vibes_style
6
+ import time
7
 
8
  STAPLE_IDS = ["112", "1852", "2236", "2386", "2388"]
9
 
 
52
  health_status = profile.get('healthStatus', '') # VD: Suy thận
53
 
54
  constraint_prompt = ""
55
+ if restrictions not in ["Không có"]:
56
  constraint_prompt += f"Yêu cầu bắt buộc: {restrictions}. "
57
  if health_status not in ["Khỏe mạnh", "Không có", "Bình thường", None]:
58
  constraint_prompt += f"Phù hợp người bệnh: {health_status}. "
59
+ if diet_mode not in ["Bình thường"]:
60
  constraint_prompt += f"Chế độ: {diet_mode}."
61
 
62
  prompt_templates = {
 
81
  final_query = f"{base_prompt} Phong cách: {vibe}.{' Ràng buộc: ' + numerical_query if numerical_query else ''}"
82
  logger.info(f"🔎 Query ({meal_type}): {final_query}")
83
 
84
+ time_start = time.time()
85
  docs = food_retriever_50.invoke(final_query)
86
+ time_end = time.time()
87
+ logger.info(f"Thời gian thực thi: {round(time_end - time_start, 2)}s")
88
  if not docs:
89
  logger.warning(f"⚠️ Retriever trả về rỗng cho bữa: {meal_type}")
90
  continue
 
290
  scored_list.sort(key=lambda x: x["health_score"], reverse=True)
291
 
292
  # # Debug: In Top 3
293
+ # logger.info("Top 3 Món Tốt Nhất (Sau khi chấm điểm):")
294
  # for i, m in enumerate(scored_list[:3]):
295
  # logger.info(f" {i+1}. {m['name']} (Score: {m['health_score']}) | {m.get('score_reason')}")
296
 
chatbot/agents/nodes/app_functions/optimize_macros.py CHANGED
@@ -11,7 +11,6 @@ def optimize_portions_scipy(state: AgentState):
11
  logger.info("---NODE: SCIPY OPTIMIZER (FINAL VERSION)---")
12
  profile = state.get("user_profile", {})
13
  menu = state.get("selected_structure", [])
14
- reason = state.get("reason", "")
15
 
16
  if not menu:
17
  print("⚠️ Menu rỗng, bỏ qua tối ưu hóa.")
@@ -122,7 +121,7 @@ def optimize_portions_scipy(state: AgentState):
122
  d = (current_meal_kcal - target_meal) / (target_meal + 1e-5)
123
  loss_dist += (d ** 2)
124
 
125
- return loss_macro + (1.5 * loss_dist)
126
 
127
  # 5. Run Optimization
128
  logger.info("Đang tối ưu hóa phần suất món ăn...")
@@ -206,6 +205,5 @@ def optimize_portions_scipy(state: AgentState):
206
 
207
  return {
208
  "final_menu": final_menu,
209
- "reason": reason,
210
  "user_profile": profile
211
  }
 
11
  logger.info("---NODE: SCIPY OPTIMIZER (FINAL VERSION)---")
12
  profile = state.get("user_profile", {})
13
  menu = state.get("selected_structure", [])
 
14
 
15
  if not menu:
16
  print("⚠️ Menu rỗng, bỏ qua tối ưu hóa.")
 
121
  d = (current_meal_kcal - target_meal) / (target_meal + 1e-5)
122
  loss_dist += (d ** 2)
123
 
124
+ return 3 * loss_macro + loss_dist
125
 
126
  # 5. Run Optimization
127
  logger.info("Đang tối ưu hóa phần suất món ăn...")
 
205
 
206
  return {
207
  "final_menu": final_menu,
 
208
  "user_profile": profile
209
  }
chatbot/agents/nodes/app_functions/select_meal.py CHANGED
@@ -2,6 +2,7 @@ from langchain_core.pydantic_v1 import BaseModel, Field
2
  from chatbot.models.llm_setup import llm
3
  from chatbot.agents.states.state import SwapState
4
  import logging
 
5
 
6
  logging.basicConfig(level=logging.INFO)
7
  logger = logging.getLogger(__name__)
@@ -44,7 +45,10 @@ def llm_finalize_choice(state: SwapState):
44
  # 3. Gọi LLM
45
  try:
46
  llm_structured = llm.with_structured_output(ChefDecision)
 
47
  decision = llm_structured.invoke(system_prompt)
 
 
48
  target_id = decision.selected_meal_id
49
  except Exception as e:
50
  logger.info(f"⚠️ Lỗi LLM: {e}. Fallback về option đầu tiên.")
 
2
  from chatbot.models.llm_setup import llm
3
  from chatbot.agents.states.state import SwapState
4
  import logging
5
+ import time
6
 
7
  logging.basicConfig(level=logging.INFO)
8
  logger = logging.getLogger(__name__)
 
45
  # 3. Gọi LLM
46
  try:
47
  llm_structured = llm.with_structured_output(ChefDecision)
48
+ time_start = time.time()
49
  decision = llm_structured.invoke(system_prompt)
50
+ time_end = time.time()
51
+ logger.info(f"⏱️ Thời gian LLM: {time_end - time_start:.2f} giây")
52
  target_id = decision.selected_meal_id
53
  except Exception as e:
54
  logger.info(f"⚠️ Lỗi LLM: {e}. Fallback về option đầu tiên.")
chatbot/agents/nodes/app_functions/select_menu.py CHANGED
@@ -4,6 +4,7 @@ from collections import defaultdict
4
  import logging
5
  from chatbot.agents.states.state import AgentState
6
  from chatbot.models.llm_setup import llm
 
7
 
8
  # --- Cấu hình logging ---
9
  logging.basicConfig(level=logging.INFO)
@@ -11,7 +12,7 @@ logger = logging.getLogger(__name__)
11
 
12
  # --- DATA MODELS ---
13
  class SelectedDish(BaseModel):
14
- name: str = Field(description="Tên món ăn chính xác trong danh sách")
15
  meal_type: str = Field(description="Bữa ăn (sáng/trưa/tối)")
16
  role: Literal["main", "carb", "side"] = Field(
17
  description="Vai trò: 'main' (Món mặn/Đạm), 'carb' (Cơm/Tinh bột), 'side' (Rau/Canh)"
@@ -19,7 +20,6 @@ class SelectedDish(BaseModel):
19
 
20
  class DailyMenuStructure(BaseModel):
21
  dishes: List[SelectedDish] = Field(description="Danh sách các món ăn được chọn")
22
- reason: str = Field(description="Lý do tổng quan cho thực đơn này và đánh giá thực đơn đã chọn")
23
 
24
  def select_menu_structure(state: AgentState):
25
  logger.info("---NODE: AI SELECTOR (FULL MACRO AWARE)---")
@@ -83,7 +83,7 @@ MỤC TIÊU CỤ THỂ TỪNG BỮA (Hãy nhẩm tính để chọn món sát v
83
  {f"- TỐI : ~{get_target_str('tối')}" if 'tối' in meals_req else ""}
84
  {safety_instruction}
85
 
86
- DỮ LIỆU ĐẦU VÀO (Tên món - Role - Dinh dưỡng):
87
  {primary_text}
88
  {backup_text}
89
 
@@ -92,7 +92,7 @@ NGUYÊN TẮC CHỌN MÓN (QUAN TRỌNG):
92
  - SÁNG: 1 Món chính (Ưu tiên món nước/bánh mì).
93
  - TRƯA & TỐI: Không bắt buộc phải đủ 3 món. Hãy chọn theo 1 trong 2 cách sau:
94
  + Cách A (Món hỗn hợp): Chọn 1-2 món nếu món đó là món hỗn hợp (VD: Bún, Mì, Nui, Cơm rang, Salad thịt...) và đã cung cấp đủ Kcal/Protein/Carb gần với Target.
95
- + Cách B (Cơm gia đình): Nếu chọn món mặn rời (ít Carb/Rau), hãy ghép thêm [Tinh Bột] + [Rau/Canh] để cân bằng.
96
  => MỤC TIÊU: Tổng Kcal của bữa ăn phải sát với Target (sai số cho phép ~10-15%).
97
 
98
  2. Quy tắc Ưu tiên & Dự phòng:
@@ -110,7 +110,11 @@ NGUYÊN TẮC CHỌN MÓN (QUAN TRỌNG):
110
  try:
111
  logger.info("Đang gọi LLM lựa chọn món...")
112
  llm_structured = llm.with_structured_output(DailyMenuStructure, strict=True)
 
 
113
  result = llm_structured.invoke(system_prompt)
 
 
114
 
115
  if not result or not hasattr(result, 'dishes'):
116
  raise ValueError("LLM trả về kết quả rỗng hoặc sai định dạng object.")
@@ -119,28 +123,39 @@ NGUYÊN TẮC CHỌN MÓN (QUAN TRỌNG):
119
  logger.error(f"🔥 LỖI GỌI LLM SELECTOR: {e}")
120
  return {"selected_structure": [], "reason": "Lỗi hệ thống khi chọn món."}
121
 
122
- def print_menu_by_meal(daily_menu):
 
 
 
 
123
  menu_by_meal = defaultdict(list)
 
124
  for dish in daily_menu.dishes:
125
  menu_by_meal[dish.meal_type.lower()].append(dish)
 
126
  meal_order = ["sáng", "trưa", "tối"]
 
127
  for meal in meal_order:
128
  if meal in menu_by_meal:
129
  logger.info(f"\n🍽 Bữa {meal.upper()}:")
130
  for d in menu_by_meal[meal]:
131
- logger.info(f" - {d.name} ({d.role})")
 
 
 
 
 
132
 
133
  logger.info("\n--- MENU ĐÃ CHỌN ---")
134
- print_menu_by_meal(result)
135
 
136
  # 4. HẬU XỬ LÝ (Gán Bounds)
137
  selected_full_info = []
138
- all_clean_candidates = primary_pool + backup_pool
139
- candidate_map = {m['name']: m for m in all_clean_candidates}
140
 
141
  for choice in result.dishes:
142
- if choice.name in candidate_map:
143
- dish_data = candidate_map[choice.name].copy()
 
144
  dish_data["assigned_meal"] = choice.meal_type
145
 
146
  d_kcal = float(dish_data.get("kcal", 0))
@@ -154,11 +169,11 @@ NGUYÊN TẮC CHỌN MÓN (QUAN TRỌNG):
154
  final_role = choice.role
155
  # 1. Phát hiện "Carb trá hình" (Cơm chiên/Mì xào quá nhiều thịt)
156
  if final_role == "carb" and d_pro > 15:
157
- logger.info(f" ⚠️ Phát hiện Carb giàu đạm ({choice.name}: {d_pro}g Pro). Đổi role sang 'main'.")
158
  final_role = "main"
159
  # 2. Phát hiện "Side giàu đạm" (Salad gà/bò, Canh sườn)
160
  elif final_role == "side" and d_pro > 10:
161
- logger.info(f" ⚠️ Phát hiện Side giàu đạm ({choice.name}: {d_pro}g Pro). Đổi role sang 'main'.")
162
  final_role = "main"
163
 
164
  dish_data["role"] = final_role
@@ -183,19 +198,19 @@ NGUYÊN TẮC CHỌN MÓN (QUAN TRỌNG):
183
  # --- GIAI ĐOẠN 3: KIỂM TRA AN TOÀN & GHI ĐÈ ---
184
  # Override A: Nếu món Main có Protein quá lớn so với Target
185
  if final_role == "main" and d_pro > t_pro:
186
- logger.info(f" ⚠️ Món {choice.name} thừa đạm ({d_pro}g > {t_pro}g). Mở rộng bound xuống thấp.")
187
  lower_bound = 0.3
188
  upper_bound = min(upper_bound, 1.2)
189
 
190
  # Override B: Nếu món quá nhiều Calo (Chiếm > 80% Kcal cả bữa)
191
  if d_kcal > (t_kcal * 0.8):
192
- logger.info(f" ⚠️ Món {choice.name} quá đậm năng lượng ({d_kcal} kcal). Siết chặt bound.")
193
  lower_bound = 0.3
194
  upper_bound = min(upper_bound, 1.0)
195
 
196
  # Override C: Nếu là món Side nhưng Protein vẫn hơi cao (5-10g)
197
  if final_role == "side" and d_pro > 5:
198
- logger.info(f" ⚠️ Món {choice.name} Side có đạm hơi cao ({d_pro}g). Hạ thấp bound.")
199
  lower_bound = 0.2
200
 
201
  # --- KẾT THÚC: GÁN VÀO DỮ LIỆU ---
@@ -204,28 +219,15 @@ NGUYÊN TẮC CHỌN MÓN (QUAN TRỌNG):
204
 
205
  return {
206
  "selected_structure": selected_full_info,
207
- "reason": result.reason
208
  }
209
 
210
  def format_pool_detailed(pool, title):
211
- if not pool: return ""
212
- text = f"--- {title} ---\n"
213
-
214
- for m in pool:
215
- name = m['name']
216
- name_lower = name.lower()
217
-
218
- role_hint = ""
219
- if any(w in name_lower for w in ["rau", "cải", "canh", "salad", "nộm", "gỏi", "bầu", "bí", "su su"]):
220
- role_hint = "[Role: Rau/Canh]"
221
- elif any(w in name_lower for w in ["cơm", "bún", "phở", "mì", "miến", "xôi", "cháo", "bánh mì"]):
222
- role_hint = "[Role: Tinh Bột]"
223
- else:
224
- role_hint = "[Role: Món Mặn]"
225
-
226
- stats = f"({int(m.get('kcal',0))}k, P:{int(m.get('protein',0))}, F:{int(m.get('totalfat',0))}, C:{int(m.get('carbs',0))})"
227
-
228
- # Kết hợp: "Món A [Role] (500k, P30...)"
229
- text += f"- {name} {role_hint} {stats}\n"
230
-
231
- return text
 
4
  import logging
5
  from chatbot.agents.states.state import AgentState
6
  from chatbot.models.llm_setup import llm
7
+ import time
8
 
9
  # --- Cấu hình logging ---
10
  logging.basicConfig(level=logging.INFO)
 
12
 
13
  # --- DATA MODELS ---
14
  class SelectedDish(BaseModel):
15
+ dish_id: str = Field(description="ID duy nhất của món ăn (được ghi trong dấu ngoặc vuông [ID: ...])")
16
  meal_type: str = Field(description="Bữa ăn (sáng/trưa/tối)")
17
  role: Literal["main", "carb", "side"] = Field(
18
  description="Vai trò: 'main' (Món mặn/Đạm), 'carb' (Cơm/Tinh bột), 'side' (Rau/Canh)"
 
20
 
21
  class DailyMenuStructure(BaseModel):
22
  dishes: List[SelectedDish] = Field(description="Danh sách các món ăn được chọn")
 
23
 
24
  def select_menu_structure(state: AgentState):
25
  logger.info("---NODE: AI SELECTOR (FULL MACRO AWARE)---")
 
83
  {f"- TỐI : ~{get_target_str('tối')}" if 'tối' in meals_req else ""}
84
  {safety_instruction}
85
 
86
+ DỮ LIỆU ĐẦU VÀO (Định dạng: [ID] Tên món - Dinh dưỡng):
87
  {primary_text}
88
  {backup_text}
89
 
 
92
  - SÁNG: 1 Món chính (Ưu tiên món nước/bánh mì).
93
  - TRƯA & TỐI: Không bắt buộc phải đủ 3 món. Hãy chọn theo 1 trong 2 cách sau:
94
  + Cách A (Món hỗn hợp): Chọn 1-2 món nếu món đó là món hỗn hợp (VD: Bún, Mì, Nui, Cơm rang, Salad thịt...) và đã cung cấp đủ Kcal/Protein/Carb gần với Target.
95
+ + Cách B (Cơm gia đình): Nếu chọn món mặn rời ít Carb hãy ghép thêm [Tinh Bột], nếu ít Rau hãy thêm [Rau/Canh] để cân bằng.
96
  => MỤC TIÊU: Tổng Kcal của bữa ăn phải sát với Target (sai số cho phép ~10-15%).
97
 
98
  2. Quy tắc Ưu tiên & Dự phòng:
 
110
  try:
111
  logger.info("Đang gọi LLM lựa chọn món...")
112
  llm_structured = llm.with_structured_output(DailyMenuStructure, strict=True)
113
+
114
+ time_start = time.time()
115
  result = llm_structured.invoke(system_prompt)
116
+ time_end = time.time()
117
+ logger.info(f"LLM chọn món thành công trong {int((time_end - time_start)*1000)} ms.")
118
 
119
  if not result or not hasattr(result, 'dishes'):
120
  raise ValueError("LLM trả về kết quả rỗng hoặc sai định dạng object.")
 
123
  logger.error(f"🔥 LỖI GỌI LLM SELECTOR: {e}")
124
  return {"selected_structure": [], "reason": "Lỗi hệ thống khi chọn món."}
125
 
126
+
127
+ all_clean_candidates = primary_pool + backup_pool
128
+ candidate_map = {str(m.get('id') or m.get('meal_id')): m for m in all_clean_candidates}
129
+
130
+ def print_menu_by_meal(daily_menu, lookup_map):
131
  menu_by_meal = defaultdict(list)
132
+
133
  for dish in daily_menu.dishes:
134
  menu_by_meal[dish.meal_type.lower()].append(dish)
135
+
136
  meal_order = ["sáng", "trưa", "tối"]
137
+
138
  for meal in meal_order:
139
  if meal in menu_by_meal:
140
  logger.info(f"\n🍽 Bữa {meal.upper()}:")
141
  for d in menu_by_meal[meal]:
142
+ d_id = str(d.dish_id)
143
+ if d_id in lookup_map:
144
+ d_name = lookup_map[d_id]['name']
145
+ logger.info(f" - [ID:{d_id}] {d_name} ({d.role})")
146
+ else:
147
+ logger.info(f" - [ID:{d_id}] ??? (Không tìm thấy trong kho) ({d.role})")
148
 
149
  logger.info("\n--- MENU ĐÃ CHỌN ---")
150
+ print_menu_by_meal(result, candidate_map)
151
 
152
  # 4. HẬU XỬ LÝ (Gán Bounds)
153
  selected_full_info = []
 
 
154
 
155
  for choice in result.dishes:
156
+ chosen_id = str(choice.dish_id)
157
+ if chosen_id in candidate_map:
158
+ dish_data = candidate_map[chosen_id].copy()
159
  dish_data["assigned_meal"] = choice.meal_type
160
 
161
  d_kcal = float(dish_data.get("kcal", 0))
 
169
  final_role = choice.role
170
  # 1. Phát hiện "Carb trá hình" (Cơm chiên/Mì xào quá nhiều thịt)
171
  if final_role == "carb" and d_pro > 15:
172
+ logger.info(f" ⚠️ Phát hiện Carb giàu đạm ({dish_data['name']}: {d_pro}g Pro). Đổi role sang 'main'.")
173
  final_role = "main"
174
  # 2. Phát hiện "Side giàu đạm" (Salad gà/bò, Canh sườn)
175
  elif final_role == "side" and d_pro > 10:
176
+ logger.info(f" ⚠️ Phát hiện Side giàu đạm ({dish_data['name']}: {d_pro}g Pro). Đổi role sang 'main'.")
177
  final_role = "main"
178
 
179
  dish_data["role"] = final_role
 
198
  # --- GIAI ĐOẠN 3: KIỂM TRA AN TOÀN & GHI ĐÈ ---
199
  # Override A: Nếu món Main có Protein quá lớn so với Target
200
  if final_role == "main" and d_pro > t_pro:
201
+ logger.info(f" ⚠️ Món {dish_data['name']} thừa đạm ({d_pro}g > {t_pro}g). Mở rộng bound xuống thấp.")
202
  lower_bound = 0.3
203
  upper_bound = min(upper_bound, 1.2)
204
 
205
  # Override B: Nếu món quá nhiều Calo (Chiếm > 80% Kcal cả bữa)
206
  if d_kcal > (t_kcal * 0.8):
207
+ logger.info(f" ⚠️ Món {dish_data['name']} quá đậm năng lượng ({d_kcal} kcal). Siết chặt bound.")
208
  lower_bound = 0.3
209
  upper_bound = min(upper_bound, 1.0)
210
 
211
  # Override C: Nếu là món Side nhưng Protein vẫn hơi cao (5-10g)
212
  if final_role == "side" and d_pro > 5:
213
+ logger.info(f" ⚠️ Món {dish_data['name']} Side có đạm hơi cao ({d_pro}g). Hạ thấp bound.")
214
  lower_bound = 0.2
215
 
216
  # --- KẾT THÚC: GÁN VÀO DỮ LIỆU ---
 
219
 
220
  return {
221
  "selected_structure": selected_full_info,
 
222
  }
223
 
224
  def format_pool_detailed(pool, title):
225
+ if not pool: return ""
226
+ text = f"--- {title} ---\n"
227
+ for m in pool:
228
+ d_id = m.get('id') or m.get('meal_id')
229
+ name = m['name']
230
+ stats = f"({int(m.get('kcal',0))}k, P:{int(m.get('protein',0))}, F:{int(m.get('totalfat',0))}, C:{int(m.get('carbs',0))})"
231
+ text += f"- [ID: {d_id}] {name} {stats}\n"
232
+
233
+ return text
 
 
 
 
 
 
 
 
 
 
 
 
chatbot/routes/meal_plan_route.py CHANGED
@@ -39,7 +39,6 @@ def generate_meal_plan(request: Request):
39
  final_state = meal_app.invoke(initial_state)
40
  response = {
41
  "final_menu": final_state["final_menu"],
42
- "reason": final_state["reason"]
43
  }
44
 
45
  if not response["final_menu"]:
 
39
  final_state = meal_app.invoke(initial_state)
40
  response = {
41
  "final_menu": final_state["final_menu"],
 
42
  }
43
 
44
  if not response["final_menu"]: