truglpk3 commited on
Commit
29b313e
·
1 Parent(s): b5961aa
Files changed (47) hide show
  1. .env +5 -1
  2. Dockerfile +14 -8
  3. README.md +3 -9
  4. chatbot/__pycache__/config.cpython-310.pyc +0 -0
  5. chatbot/__pycache__/main.cpython-310.pyc +0 -0
  6. chatbot/agents/graphs/__pycache__/chatbot_graph.cpython-310.pyc +0 -0
  7. chatbot/agents/graphs/chatbot_graph.py +76 -34
  8. chatbot/agents/nodes/app_functions/find_candidates.py +7 -9
  9. chatbot/agents/nodes/app_functions/generate_candidates.py +234 -71
  10. chatbot/agents/nodes/app_functions/get_profile.py +7 -6
  11. chatbot/agents/nodes/app_functions/optimize_macros.py +61 -54
  12. chatbot/agents/nodes/app_functions/optimize_select.py +44 -32
  13. chatbot/agents/nodes/app_functions/select_meal.py +10 -17
  14. chatbot/agents/nodes/app_functions/select_menu.py +113 -119
  15. chatbot/agents/nodes/chatbot/__init__.py +7 -2
  16. chatbot/agents/nodes/chatbot/ask_info.py +48 -0
  17. chatbot/agents/nodes/chatbot/classify_topic.py +45 -53
  18. chatbot/agents/nodes/chatbot/food_query.py +1 -1
  19. chatbot/agents/nodes/chatbot/food_suggestion.py +1 -2
  20. chatbot/agents/nodes/chatbot/general_chat.py +18 -25
  21. chatbot/agents/nodes/chatbot/generate_final_response.py +37 -30
  22. chatbot/agents/nodes/chatbot/load_context.py +125 -0
  23. chatbot/agents/nodes/chatbot/meal_identify.py +29 -28
  24. chatbot/agents/nodes/chatbot/policy.py +28 -22
  25. chatbot/agents/nodes/chatbot/select_food.py +18 -14
  26. chatbot/agents/nodes/chatbot/select_food_plan.py +42 -35
  27. chatbot/agents/nodes/chatbot/suggest_meal_node.py +29 -64
  28. chatbot/agents/nodes/chatbot/validator.py +44 -0
  29. chatbot/agents/states/__pycache__/state.cpython-310.pyc +0 -0
  30. chatbot/agents/states/state.py +15 -15
  31. chatbot/agents/tools/__pycache__/daily_meal_suggestion.cpython-310.pyc +0 -0
  32. chatbot/agents/tools/__pycache__/food_retriever.cpython-310.pyc +0 -0
  33. chatbot/agents/tools/daily_meal_suggestion.py +3 -6
  34. chatbot/agents/tools/food_retriever.py +172 -94
  35. chatbot/agents/tools/info_app_retriever.py +2 -2
  36. chatbot/config.py +3 -1
  37. chatbot/knowledge/field_requirement.py +17 -0
  38. chatbot/knowledge/vibe.py +61 -0
  39. chatbot/main.py +2 -0
  40. chatbot/routes/__pycache__/chat_router.cpython-310.pyc +0 -0
  41. chatbot/routes/__pycache__/food_replace_route.cpython-310.pyc +0 -0
  42. chatbot/routes/chat_router.py +28 -16
  43. chatbot/routes/food_replace_route.py +31 -17
  44. chatbot/routes/manage_food_route.py +71 -0
  45. chatbot/routes/meal_plan_route.py +32 -21
  46. chatbot/utils/chat_history.py +29 -0
  47. requirements.txt +2 -1
.env CHANGED
@@ -1,4 +1,8 @@
1
  DEEPSEEK_API_KEY=sk-0fde229ff9854ada814c2553c787d721
 
2
  ELASTIC_API_KEY=MW9QZm1aa0JkWjBPRkFiaUFwc0Q6T1cycFlqbVVXbzR6OWM0Tm1CeW1GQQ
3
  ELASTIC_CLOUD_URL=https://datnvdb-a564ef.es.australiaeast.azure.elastic.cloud:443
4
- PINECONE_API_KEY=pcsk_22nKGg_MA6xgCafiT145gfdVQV2dPka1vYEDZg8t3DaAcKhoWvV46PwnikPW71aY6nMAxh
 
 
 
 
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
+
8
+ API_BASE_URL=https://pmpsxfbs-5000.asse.devtunnels.ms
Dockerfile CHANGED
@@ -1,13 +1,19 @@
1
- FROM python:3.9
2
 
3
- RUN useradd -m -u 1000 user
4
- USER user
5
- ENV PATH="/home/user/.local/bin:$PATH"
6
 
7
  WORKDIR /app
8
 
9
- COPY --chown=user ./requirements.txt requirements.txt
10
- RUN pip install --no-cache-dir --upgrade -r requirements.txt
11
 
12
- COPY --chown=user . /app
13
- CMD ["uvicorn", "chatbot.main:app", "--host", "0.0.0.0", "--port", "7860"]
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
 
3
+ # Tắt cache HF để tránh lỗi nặng
4
+ ENV HF_HOME=/cache/huggingface
5
+ RUN mkdir -p /cache/huggingface
6
 
7
  WORKDIR /app
8
 
9
+ COPY requirements.txt .
 
10
 
11
+ RUN pip install --no-cache-dir -r requirements.txt
12
+
13
+ COPY . .
14
+
15
+ # Expose cổng 7860 (HF Spaces yêu cầu)
16
+ EXPOSE 7860
17
+
18
+ # Chạy FastAPI
19
+ CMD ["uvicorn", "start:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,9 +1,3 @@
1
- ---
2
- title: "datn-fastapi"
3
- emoji: 🚀
4
- colorFrom: blue
5
- colorTo: green
6
- sdk: "docker"
7
- app_file: "app.py"
8
- pinned: false
9
- ---
 
1
+ # Server_AI_DATN
2
+
3
+ # To run app: uvicorn chatbot.main:app --reload --host 0.0.0.0 --port 8000 --app-dir .
 
 
 
 
 
 
chatbot/__pycache__/config.cpython-310.pyc CHANGED
Binary files a/chatbot/__pycache__/config.cpython-310.pyc and b/chatbot/__pycache__/config.cpython-310.pyc differ
 
chatbot/__pycache__/main.cpython-310.pyc CHANGED
Binary files a/chatbot/__pycache__/main.cpython-310.pyc and b/chatbot/__pycache__/main.cpython-310.pyc differ
 
chatbot/agents/graphs/__pycache__/chatbot_graph.cpython-310.pyc CHANGED
Binary files a/chatbot/agents/graphs/__pycache__/chatbot_graph.cpython-310.pyc and b/chatbot/agents/graphs/__pycache__/chatbot_graph.cpython-310.pyc differ
 
chatbot/agents/graphs/chatbot_graph.py CHANGED
@@ -1,10 +1,9 @@
1
  from langgraph.graph import StateGraph, START, END
2
  from chatbot.agents.states.state import AgentState
3
-
4
- # Import các node
5
  from chatbot.agents.nodes.chatbot import (
6
  classify_topic,
7
- route_by_topic,
8
  meal_identify,
9
  suggest_meal_node,
10
  generate_final_response,
@@ -13,50 +12,93 @@ from chatbot.agents.nodes.chatbot import (
13
  food_query,
14
  select_food,
15
  general_chat,
16
- policy
 
 
 
17
  )
18
 
19
  def workflow_chatbot():
20
- workflow_chatbot = StateGraph(AgentState)
21
-
22
- workflow_chatbot.add_node("classify_topic", classify_topic)
23
- workflow_chatbot.add_node("meal_identify", meal_identify)
24
- workflow_chatbot.add_node("suggest_meal_node", suggest_meal_node)
25
- workflow_chatbot.add_node("generate_final_response", generate_final_response)
26
- workflow_chatbot.add_node("food_suggestion", food_suggestion)
27
- workflow_chatbot.add_node("select_food_plan", select_food_plan)
28
- workflow_chatbot.add_node("food_query", food_query)
29
- workflow_chatbot.add_node("select_food", select_food)
30
- workflow_chatbot.add_node("general_chat", general_chat)
31
- workflow_chatbot.add_node("policy", policy)
32
-
33
- workflow_chatbot.add_edge(START, "classify_topic")
34
-
35
- workflow_chatbot.add_conditional_edges(
 
 
 
 
 
 
 
 
36
  "classify_topic",
37
- route_by_topic,
38
  {
39
- "meal_identify": "meal_identify",
40
- "food_suggestion": "food_suggestion",
41
- "food_query": "food_query",
42
  "policy": "policy",
 
43
  "general_chat": "general_chat",
 
44
  }
45
  )
46
 
47
- workflow_chatbot.add_edge("meal_identify", "suggest_meal_node")
48
- workflow_chatbot.add_edge("suggest_meal_node", "generate_final_response")
49
- workflow_chatbot.add_edge("generate_final_response", END)
50
 
51
- workflow_chatbot.add_edge("food_suggestion", "select_food_plan")
52
- workflow_chatbot.add_edge("select_food_plan", END)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
 
54
- workflow_chatbot.add_edge("food_query", "select_food")
55
- workflow_chatbot.add_edge("select_food", END)
56
 
57
- workflow_chatbot.add_edge("policy", END)
58
- workflow_chatbot.add_edge("general_chat", END)
59
 
60
- app = workflow_chatbot.compile()
 
 
 
 
 
61
 
62
  return app
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from langgraph.graph import StateGraph, START, END
2
  from chatbot.agents.states.state import AgentState
3
+ from langgraph.checkpoint.memory import MemorySaver
4
+ from chatbot.knowledge.field_requirement import TOPIC_REQUIREMENTS
5
  from chatbot.agents.nodes.chatbot import (
6
  classify_topic,
 
7
  meal_identify,
8
  suggest_meal_node,
9
  generate_final_response,
 
12
  food_query,
13
  select_food,
14
  general_chat,
15
+ policy,
16
+ ask_missing_info,
17
+ load_context_strict,
18
+ universal_validator
19
  )
20
 
21
  def workflow_chatbot():
22
+ workflow = StateGraph(AgentState)
23
+
24
+ workflow.add_node("classify_topic", classify_topic)
25
+ workflow.add_node("validator", universal_validator)
26
+ workflow.add_node("load_context", load_context_strict)
27
+ workflow.add_node("ask_info", ask_missing_info)
28
+
29
+ workflow.add_node("meal_identify", meal_identify)
30
+ workflow.add_node("suggest_meal_node", suggest_meal_node)
31
+ workflow.add_node("generate_final_response", generate_final_response)
32
+
33
+ workflow.add_node("food_suggestion", food_suggestion)
34
+ workflow.add_node("select_food_plan", select_food_plan)
35
+
36
+ workflow.add_node("food_query", food_query)
37
+ workflow.add_node("select_food", select_food)
38
+
39
+ workflow.add_node("general_chat", general_chat)
40
+
41
+ workflow.add_node("policy", policy)
42
+
43
+ workflow.add_edge(START, "classify_topic")
44
+
45
+ workflow.add_conditional_edges(
46
  "classify_topic",
47
+ route_initial,
48
  {
 
 
 
49
  "policy": "policy",
50
+ "food_query": "food_query",
51
  "general_chat": "general_chat",
52
+ "load_context": "load_context"
53
  }
54
  )
55
 
56
+ workflow.add_edge("load_context", "validator")
 
 
57
 
58
+ workflow.add_conditional_edges(
59
+ "validator",
60
+ route_post_validation,
61
+ {
62
+ "ask_info": "ask_info",
63
+ "meal_suggestion": "meal_identify",
64
+ "food_suggestion": "food_suggestion",
65
+ # "food_query": "food_query",
66
+ # "general_chat": "general_chat",
67
+ # "policy": "policy",
68
+ }
69
+ )
70
+
71
+ workflow.add_edge("ask_info", END)
72
+
73
+ workflow.add_edge("meal_identify", "suggest_meal_node")
74
+ workflow.add_edge("suggest_meal_node", "generate_final_response")
75
+ workflow.add_edge("generate_final_response", END)
76
 
77
+ workflow.add_edge("food_suggestion", "select_food_plan")
78
+ workflow.add_edge("select_food_plan", END)
79
 
80
+ workflow.add_edge("food_query", "select_food")
81
+ workflow.add_edge("select_food", END)
82
 
83
+ workflow.add_edge("policy", END)
84
+
85
+ workflow.add_edge("general_chat", END)
86
+
87
+ memory = MemorySaver()
88
+ app = workflow.compile(checkpointer=memory)
89
 
90
  return app
91
+
92
+ def route_initial(state: AgentState):
93
+ topic = state.get("topic")
94
+ non_empty_keys = [key for key, value in TOPIC_REQUIREMENTS.items() if value]
95
+ if topic in non_empty_keys:
96
+ return "load_context"
97
+ return topic
98
+
99
+ def route_post_validation(state: AgentState):
100
+ if not state.get("is_valid"):
101
+ return "ask_info"
102
+
103
+ topic = state.get("topic")
104
+ return topic
chatbot/agents/nodes/app_functions/find_candidates.py CHANGED
@@ -9,15 +9,18 @@ logger = logging.getLogger(__name__)
9
 
10
  def find_replacement_candidates(state: SwapState):
11
  logger.info("---NODE: FIND REPLACEMENTS (SELF QUERY)---")
12
- food_old = state["food_old"]
13
- profile = state["user_profile"]
 
 
 
 
14
 
15
  diet_mode = profile.get('diet', '') # VD: Chế độ HighProtein
16
  restrictions = profile.get('limitFood', '') # VD: Dị ứng sữa, Thuần chay
17
  health_status = profile.get('healthStatus', '') # VD: Suy thận
18
 
19
  constraint_prompt = ""
20
-
21
  if restrictions:
22
  constraint_prompt += f"Yêu cầu bắt buộc: {restrictions}. "
23
  if health_status:
@@ -32,7 +35,6 @@ def find_replacement_candidates(state: SwapState):
32
  numerical_query = generate_numerical_constraints(profile, meal_type)
33
 
34
  # 2. Xây dựng Query tự nhiên để SelfQueryRetriever hiểu
35
- # Mẹo: Đưa thông tin phủ định "Không phải món X" vào
36
  query = (
37
  f"Tìm các món ăn đóng vai trò '{role}' phù hợp cho bữa '{meal_type}'. "
38
  f"Khác với món '{old_name}'. "
@@ -41,7 +43,6 @@ def find_replacement_candidates(state: SwapState):
41
 
42
  if numerical_query:
43
  query += f"Yêu cầu: {numerical_query}"
44
-
45
  logger.info(f"🔎 Query: {query}")
46
 
47
  # 3. Gọi Retriever
@@ -55,11 +56,8 @@ def find_replacement_candidates(state: SwapState):
55
  candidates = []
56
  for doc in docs:
57
  item = doc.metadata.copy()
58
-
59
- # Bỏ qua chính món cũ (Double check)
60
  if item.get("name") == old_name: continue
61
-
62
- # Gán context của món cũ sang để tính toán
63
  item["target_role"] = role
64
  item["target_meal"] = meal_type
65
  candidates.append(item)
 
9
 
10
  def find_replacement_candidates(state: SwapState):
11
  logger.info("---NODE: FIND REPLACEMENTS (SELF QUERY)---")
12
+ food_old = state.get("food_old")
13
+ profile = state.get("user_profile", {})
14
+
15
+ if not food_old:
16
+ logger.warning("⚠️ Không tìm thấy thông tin món cũ (food_old).")
17
+ return {"candidates": []}
18
 
19
  diet_mode = profile.get('diet', '') # VD: Chế độ HighProtein
20
  restrictions = profile.get('limitFood', '') # VD: Dị ứng sữa, Thuần chay
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:
 
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}'. "
 
43
 
44
  if numerical_query:
45
  query += f"Yêu cầu: {numerical_query}"
 
46
  logger.info(f"🔎 Query: {query}")
47
 
48
  # 3. Gọi Retriever
 
56
  candidates = []
57
  for doc in docs:
58
  item = doc.metadata.copy()
 
 
59
  if item.get("name") == old_name: continue
60
+
 
61
  item["target_role"] = role
62
  item["target_meal"] = meal_type
63
  candidates.append(item)
chatbot/agents/nodes/app_functions/generate_candidates.py CHANGED
@@ -1,7 +1,10 @@
1
  import random
2
  import logging
3
  from chatbot.agents.states.state import AgentState
4
- from chatbot.agents.tools.food_retriever import food_retriever_50
 
 
 
5
 
6
  # --- Cấu hình logging ---
7
  logging.basicConfig(level=logging.INFO)
@@ -13,7 +16,36 @@ def generate_food_candidates(state: AgentState):
13
  profile = state["user_profile"]
14
 
15
  candidates = []
 
 
 
 
 
 
 
16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  diet_mode = profile.get('diet', '') # VD: Chế độ HighProtein
18
  restrictions = profile.get('limitFood', '') # VD: Dị ứng sữa, Thuần chay
19
  health_status = profile.get('healthStatus', '') # VD: Suy thận
@@ -21,61 +53,63 @@ def generate_food_candidates(state: AgentState):
21
  constraint_prompt = ""
22
  if restrictions:
23
  constraint_prompt += f"Yêu cầu bắt buộc: {restrictions}. "
24
- if health_status:
25
  constraint_prompt += f"Phù hợp người bệnh: {health_status}. "
26
  if diet_mode:
27
  constraint_prompt += f"Chế độ: {diet_mode}."
28
 
29
- # ĐỊNH NGHĨA TEMPLATE PROMPT
30
  prompt_templates = {
31
- "sáng": (
32
- f"Món ăn sáng, điểm tâm. Ưu tiên món nước hoặc món khô dễ tiêu hóa. "
33
- f"{constraint_prompt}"
34
- ),
35
- "trưa": (
36
- f"Món ăn chính cho bữa trưa. "
37
- f"{constraint_prompt}"
38
- ),
39
- "tối": (
40
- f"Món ăn tối, nhẹ bụng. "
41
- f"{constraint_prompt}"
42
- ),
43
  }
44
 
45
- random_vibes = [
46
- "hương vị truyền thống", "phong cách hiện đại",
47
- "thanh đạm", "chế biến đơn giản", "phổ biến nhất"
48
- ]
49
-
50
  for meal_type in meals:
51
- logger.info(meal_type)
52
- base_prompt = prompt_templates.get(meal_type, f"Món ăn {meal_type}. {constraint_prompt}")
53
- vibe = random.choice(random_vibes)
54
- numerical_query = generate_numerical_constraints(profile, meal_type)
55
-
56
- final_query = f"{base_prompt} Phong cách: {vibe}.{' Ràng buộc: ' + numerical_query if numerical_query != '' else ''}"
57
- logger.info(f"🔎 Query ({meal_type}): {final_query}")
58
-
59
- docs = food_retriever_50.invoke(final_query)
60
- ranked_items = rank_candidates(docs, profile, meal_type)
61
-
62
- if len(ranked_items) > 0:
63
- ranked_items_shuffle = random.sample(ranked_items[:30], 30)
64
-
65
- k = 20 if len(meals) == 1 else 10
66
 
67
- selected_docs = ranked_items_shuffle[:k]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
- for doc in selected_docs:
70
- item = doc.copy()
71
- item["meal_type_tag"] = meal_type
72
- item["retrieval_vibe"] = vibe
73
- candidates.append(item)
 
 
 
 
 
 
 
 
 
 
 
 
74
 
75
- unique_candidates = {v['name']: v for v in candidates}.values()
76
  final_pool = list(unique_candidates)
77
-
78
  logger.info(f"📚 Candidate Pool Size: {len(final_pool)} món")
 
 
79
  return {"candidate_pool": final_pool, "meals_to_generate": meals}
80
 
81
  def generate_numerical_constraints(user_profile, meal_type):
@@ -87,17 +121,21 @@ def generate_numerical_constraints(user_profile, meal_type):
87
 
88
  critical_nutrients = {
89
  "Protein": ("protein", "protein", "g", "range"),
90
- "Saturated fat": ("saturatedfat", "saturated_fat", "g", "max"),
91
- "Natri": ("natri", "natri", "mg", "max"), # Quan trọng cho thận/tim
92
- "Kali": ("kali", "kali", "mg", "range"), # Quan trọng cho thận
93
- "Phốt pho": ("photpho", "photpho", "mg", "max"), # Quan trọng cho thận
94
- "Sugars": ("sugar", "sugar", "g", "max"), # Quan trọng cho tiểu đường
95
- "Carbohydrate": ("carbohydrate", "carbohydrate", "g", "range"),
96
  }
97
 
98
  constraints = []
99
 
100
  check_list = set(user_profile.get('Kiêng', []) + user_profile.get('Hạn chế', []))
 
 
 
 
101
  for item_name in check_list:
102
  if item_name not in critical_nutrients: continue
103
 
@@ -132,28 +170,28 @@ def rank_candidates(candidates, user_profile, meal_type):
132
  nutrient_config = {
133
  # --- Nhóm Đa lượng (Macro) ---
134
  "Protein": ("protein", "protein", "g", "range"),
135
- "Total Fat": ("totalfat", "lipid", "g", "max"),
136
- "Carbohydrate": ("carbohydrate", "carbohydrate", "g", "range"),
137
- "Saturated fat": ("saturatedfat", "saturated_fat", "g", "max"),
138
- "Monounsaturated fat": ("monounsaturatedfat", "monounsaturated_fat", "g", "max"),
139
- "Trans fat": ("transfat", "trans_fat", "g", "max"),
140
  "Sugars": ("sugar", "sugar", "g", "max"),
141
  "Chất xơ": ("fiber", "fiber", "g", "min"),
142
 
143
  # --- Nhóm Vi chất (Micro) ---
144
- "Vitamin A": ("vitamina", "vit_a", "mg", "min"),
145
- "Vitamin C": ("vitaminc", "vit_c", "mg", "min"),
146
- "Vitamin D": ("vitamind", "vit_d", "mg", "min"),
147
- "Vitamin E": ("vitamine", "vit_e", "mg", "min"),
148
- "Vitamin K": ("vitamink", "vit_k", "mg", "min"),
149
- "Vitamin B6": ("vitaminb6", "vit_b6", "mg", "min"),
150
- "Vitamin B12": ("vitaminb12", "vit_b12", "mg", "min"),
151
 
152
  # --- Khoáng chất ---
153
  "Canxi": ("canxi", "canxi", "mg", "min"),
154
- "Sắt": ("fe", "sat", "mg", "min"),
155
  "Magie": ("magie", "magie", "mg", "min"),
156
- "Kẽm": ("zn", "kem", "mg", "min"),
157
  "Kali": ("kali", "kali", "mg", "range"),
158
  "Natri": ("natri", "natri", "mg", "max"),
159
  "Phốt pho": ("photpho", "photpho", "mg", "max"),
@@ -197,7 +235,7 @@ def rank_candidates(candidates, user_profile, meal_type):
197
  score += 5
198
 
199
  # --- 2. CHẤM ĐIỂM NHÓM "HẠN CHẾ" & "KIÊNG" (PENALTY/REWARD) ---
200
- # Gộp chung vì logic giống nhau: Càng thấp càng tốt
201
  check_list = set(user_profile.get('Hạn chế', []) + user_profile.get('Kiêng', []))
202
 
203
  for nutrient in check_list:
@@ -234,13 +272,8 @@ def rank_candidates(candidates, user_profile, meal_type):
234
  # Thấp quá thì không trừ điểm nặng, chỉ không được cộng
235
 
236
  # --- 3. ĐIỂM THƯỞNG CHO SỰ PHÙ HỢP CƠ BẢN (BASE HEALTH) ---
237
- # Ít đường (< 5g) -> +2 điểm
238
  if float(item.get('sugar', 0)) < 5: score += 2
239
-
240
- # Ít saturated fat (< 3g) -> +2 điểm
241
  if float(item.get('saturated_fat', 0)) < 3: score += 2
242
-
243
- # Giàu xơ (> 3g) -> +3 điểm
244
  if float(item.get('fiber', 0)) > 3: score += 3
245
 
246
  # Lưu kết quả
@@ -250,7 +283,6 @@ def rank_candidates(candidates, user_profile, meal_type):
250
  scored_list.append(item_copy)
251
 
252
  # 4. SẮP XẾP & TRẢ VỀ
253
- # Sort giảm dần theo điểm (Điểm cao nhất lên đầu)
254
  scored_list.sort(key=lambda x: x["health_score"], reverse=True)
255
 
256
  # # Debug: In Top 3
@@ -258,4 +290,135 @@ def rank_candidates(candidates, user_profile, meal_type):
258
  # for i, m in enumerate(scored_list[:3]):
259
  # logger.info(f" {i+1}. {m['name']} (Score: {m['health_score']}) | {m.get('score_reason')}")
260
 
261
- return scored_list
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import random
2
  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
 
9
  # --- Cấu hình logging ---
10
  logging.basicConfig(level=logging.INFO)
 
16
  profile = state["user_profile"]
17
 
18
  candidates = []
19
+
20
+ # 1. NẠP KHO DỰ PHÒNG TỪ ELASTICSEARCH (BY ID)
21
+ try:
22
+ staples_data = fetch_staples_by_ids(docsearch, STAPLE_IDS)
23
+
24
+ if not staples_data:
25
+ staples_data = []
26
 
27
+ for staple in staples_data:
28
+ name_lower = staple.get("name", "").lower()
29
+
30
+ target_meals = []
31
+ if any(x in name_lower for x in ["cơm", "canh", "rau", "kho", "đậu"]):
32
+ target_meals = ["trưa", "tối"]
33
+ elif any(x in name_lower for x in ["bánh mì", "xôi", "trứng", "bún", "phở"]):
34
+ target_meals = ["sáng"]
35
+ else:
36
+ target_meals = ["sáng", "trưa", "tối"]
37
+
38
+ for meal in target_meals:
39
+ if meal in meals:
40
+ s_copy = staple.copy()
41
+ s_copy["meal_type_tag"] = meal
42
+ s_copy["retrieval_vibe"] = "Món ăn kèm cơ bản"
43
+ candidates.append(s_copy)
44
+
45
+ except Exception as e:
46
+ logger.warning(f"⚠️ Lỗi khi nạp Staples (Kho dự phòng): {e}")
47
+
48
+ # 2. XỬ LÝ DỮ LIỆU PROFILE NGƯỜI DÙNG
49
  diet_mode = profile.get('diet', '') # VD: Chế độ HighProtein
50
  restrictions = profile.get('limitFood', '') # VD: Dị ứng sữa, Thuần chay
51
  health_status = profile.get('healthStatus', '') # VD: Suy thận
 
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 = {
62
+ "sáng": f"Món ăn sáng, điểm tâm. Ưu tiên món nước hoặc món khô dễ tiêu hóa. {constraint_prompt}",
63
+ "trưa": f"Món ăn chính cho bữa trưa. {constraint_prompt}",
64
+ "tối": f"Món ăn tối, nhẹ bụng. {constraint_prompt}",
 
 
 
 
 
 
 
 
 
65
  }
66
 
 
 
 
 
 
67
  for meal_type in meals:
68
+ try:
69
+ logger.info(meal_type)
70
+ base_prompt = prompt_templates.get(meal_type, f"Món ăn {meal_type}. {constraint_prompt}")
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
+ try:
73
+ vibe = get_random_vibe(meal_type)
74
+ numerical_query = generate_numerical_constraints(profile, meal_type)
75
+ except Exception as sub_e:
76
+ logger.error(f"Lỗi logic phụ (vibe/numerical) cho bữa {meal_type}: {sub_e}")
77
+ vibe = "Hài hòa"
78
+ numerical_query = ""
79
+
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
87
+
88
+ ranked_items = rank_candidates(docs, profile, meal_type)
89
 
90
+ if ranked_items:
91
+ top_n_count = min(len(ranked_items), 30)
92
+ top_candidates = ranked_items[:top_n_count]
93
+ random.shuffle(top_candidates)
94
+
95
+ k = min(20, top_n_count) if len(meals) == 1 else min(10, top_n_count)
96
+ selected_docs = top_candidates[:k]
97
+
98
+ for item in selected_docs:
99
+ candidate = item.copy()
100
+ candidate["meal_type_tag"] = meal_type
101
+ candidate["retrieval_vibe"] = vibe
102
+ candidates.append(candidate)
103
+
104
+ except Exception as e:
105
+ logger.error(f"🔥 LỖI NGHIÊM TRỌNG khi retrieve bữa {meal_type}: {e}")
106
+ continue
107
 
108
+ unique_candidates = {v.get('name', 'Unknown'): v for v in candidates}.values()
109
  final_pool = list(unique_candidates)
 
110
  logger.info(f"📚 Candidate Pool Size: {len(final_pool)} món")
111
+ if len(final_pool) == 0:
112
+ logger.critical("❌ KHÔNG TÌM THẤY MÓN NÀO! Check lại DB connection.")
113
  return {"candidate_pool": final_pool, "meals_to_generate": meals}
114
 
115
  def generate_numerical_constraints(user_profile, meal_type):
 
121
 
122
  critical_nutrients = {
123
  "Protein": ("protein", "protein", "g", "range"),
124
+ "Saturated fat": ("saturatedfat", "saturatedfat", "g", "max"),
125
+ "Natri": ("natri", "natri", "mg", "max"),
126
+ "Kali": ("kali", "kali", "mg", "range"),
127
+ "Phốt pho": ("photpho", "photpho", "mg", "max"),
128
+ "Sugars": ("sugar", "sugar", "g", "max"),
129
+ "Carbohydrate": ("carbohydrate", "carbs", "g", "range"),
130
  }
131
 
132
  constraints = []
133
 
134
  check_list = set(user_profile.get('Kiêng', []) + user_profile.get('Hạn chế', []))
135
+
136
+ if "thận" in user_profile.get('healthStatus', '').lower():
137
+ check_list.update(["Protein", "Natri", "Kali", "Phốt pho"])
138
+
139
  for item_name in check_list:
140
  if item_name not in critical_nutrients: continue
141
 
 
170
  nutrient_config = {
171
  # --- Nhóm Đa lượng (Macro) ---
172
  "Protein": ("protein", "protein", "g", "range"),
173
+ "Total Fat": ("totalfat", "totalfat", "g", "max"),
174
+ "Carbohydrate": ("carbohydrate", "carbs", "g", "range"),
175
+ "Saturated fat": ("saturatedfat", "saturatedfat", "g", "max"),
176
+ "Monounsaturated fat": ("monounsaturatedfat", "monounsaturatedfat", "g", "max"),
177
+ "Trans fat": ("transfat", "transfat", "g", "max"),
178
  "Sugars": ("sugar", "sugar", "g", "max"),
179
  "Chất xơ": ("fiber", "fiber", "g", "min"),
180
 
181
  # --- Nhóm Vi chất (Micro) ---
182
+ "Vitamin A": ("vitamina", "vitamina", "mg", "min"),
183
+ "Vitamin C": ("vitaminc", "vitaminc", "mg", "min"),
184
+ "Vitamin D": ("vitamind", "vitamind", "mg", "min"),
185
+ "Vitamin E": ("vitamine", "vitamine", "mg", "min"),
186
+ "Vitamin K": ("vitamink", "vitamink", "mg", "min"),
187
+ "Vitamin B6": ("vitaminb6", "vitaminb6", "mg", "min"),
188
+ "Vitamin B12": ("vitaminb12", "vitaminb12", "mg", "min"),
189
 
190
  # --- Khoáng chất ---
191
  "Canxi": ("canxi", "canxi", "mg", "min"),
192
+ "Sắt": ("fe", "fe", "mg", "min"),
193
  "Magie": ("magie", "magie", "mg", "min"),
194
+ "Kẽm": ("zn", "zn", "mg", "min"),
195
  "Kali": ("kali", "kali", "mg", "range"),
196
  "Natri": ("natri", "natri", "mg", "max"),
197
  "Phốt pho": ("photpho", "photpho", "mg", "max"),
 
235
  score += 5
236
 
237
  # --- 2. CHẤM ĐIỂM NHÓM "HẠN CHẾ" & "KIÊNG" (PENALTY/REWARD) ---
238
+ # Gộp chung: Càng thấp càng tốt
239
  check_list = set(user_profile.get('Hạn chế', []) + user_profile.get('Kiêng', []))
240
 
241
  for nutrient in check_list:
 
272
  # Thấp quá thì không trừ điểm nặng, chỉ không được cộng
273
 
274
  # --- 3. ĐIỂM THƯỞNG CHO SỰ PHÙ HỢP CƠ BẢN (BASE HEALTH) ---
 
275
  if float(item.get('sugar', 0)) < 5: score += 2
 
 
276
  if float(item.get('saturated_fat', 0)) < 3: score += 2
 
 
277
  if float(item.get('fiber', 0)) > 3: score += 3
278
 
279
  # Lưu kết quả
 
283
  scored_list.append(item_copy)
284
 
285
  # 4. SẮP XẾP & TRẢ VỀ
 
286
  scored_list.sort(key=lambda x: x["health_score"], reverse=True)
287
 
288
  # # Debug: In Top 3
 
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
 
293
+ return scored_list
294
+
295
+ def get_random_vibe(meal_type):
296
+ """
297
+ Chọn vibe thông minh với xác suất cao ra món Thanh đạm/Canh cho bữa Trưa/Tối
298
+ """
299
+
300
+ # --- BỮA SÁNG ---
301
+ if meal_type == "sáng":
302
+ pool = [
303
+ "khởi đầu ngày mới năng lượng",
304
+ "món nước nóng hổi",
305
+ "chế biến nhanh gọn lẹ",
306
+ "điểm tâm nhẹ nhàng",
307
+ "hương vị thanh tao"
308
+ ] + vibes_flavor
309
+ return random.choice(pool)
310
+
311
+ # --- BỮA TRƯA / TỐI ---
312
+ else:
313
+ roll = random.random()
314
+
315
+ if roll < 0.3:
316
+ # 30%: Query tập trung vào Món Mặn Đậm Đà (Thịt/Cá kho, chiên...)
317
+ # "Kho tộ đậm đà mang hương vị đồng quê"
318
+ v_main = random.choice(vibes_cooking)
319
+ v_style = random.choice(vibes_style)
320
+ return f"{v_main} mang {v_style}"
321
+
322
+ elif roll < 0.6:
323
+ # 30%: Query tập trung hoàn toàn vào Món Thanh Đạm/Canh
324
+ # "Canh hầm thanh mát bổ dưỡng mang hương vị thanh đạm nhẹ nhàng"
325
+ v_soup = random.choice(vibes_soup_veg)
326
+ v_flavor = random.choice(vibes_healthy + vibes_flavor)
327
+ return f"{v_soup} mang {v_flavor}"
328
+
329
+ else:
330
+ # 40%: Query HỖN HỢP (Kỹ thuật "Combo Keyword")
331
+ # "Kho tộ đậm đà kết hợp với canh rau thanh mát"
332
+ v_main = random.choice(vibes_cooking)
333
+ v_soup = random.choice(vibes_soup_veg)
334
+ return f"{v_main} kết hợp với {v_soup}"
335
+
336
+ def fetch_staples_by_ids(vectorstore, doc_ids):
337
+ """
338
+ Lấy document từ ES theo ID và map về đúng định dạng candidate_pool.
339
+ """
340
+ if not doc_ids:
341
+ return []
342
+
343
+ try:
344
+ client = vectorstore.client
345
+
346
+ # 1. Gọi API mget để lấy dữ liệu thô cực nhanh
347
+ response = client.mget(index="food_v2_vdb", body={"ids": doc_ids})
348
+
349
+ fetched_items = []
350
+
351
+ for doc in response['docs']:
352
+ if doc['found']:
353
+ # Dữ liệu gốc trong ES
354
+ src = doc['_source']
355
+
356
+ meta = src.get('metadata', src)
357
+
358
+ # 2. Mapping chi tiết theo mẫu bạn cung cấp
359
+ item = {
360
+ # --- ĐỊNH DANH ---
361
+ 'meal_id': meta.get('meal_id', doc['_id']), # Fallback về doc_id nếu ko có meal_id
362
+ 'name': meta.get('name', 'Món không tên'),
363
+
364
+ # --- THÀNH PHẦN ---
365
+ 'ingredients': meta.get('ingredients', []),
366
+ 'ingredients_text': meta.get('ingredients_text', ''),
367
+ 'tags': meta.get('tags', []),
368
+
369
+ # --- CÁCH LÀM ---
370
+ 'preparation_steps': meta.get('preparation_steps', ''),
371
+ 'cooking_steps': meta.get('cooking_steps', ''),
372
+
373
+ # --- DINH DƯỠNG ---
374
+ 'kcal': float(meta.get('kcal', 0.0)),
375
+ 'carbs': float(meta.get('carbs', 0.0)),
376
+ 'protein': float(meta.get('protein', 0.0)),
377
+ 'totalfat': float(meta.get('totalfat', 0.0) or meta.get('lipid', 0.0)), # Handle alias
378
+
379
+ # --- VI CHẤT ---
380
+ 'sugar': float(meta.get('sugar', 0.0)),
381
+ 'fiber': float(meta.get('fiber', 0.0)),
382
+ 'saturatedfat': float(meta.get('saturatedfat', 0.0)),
383
+ 'monounsaturatedfat': float(meta.get('monounsaturatedfat', 0.0)),
384
+ 'polyunsaturatedfat': float(meta.get('polyunsaturatedfat', 0.0)),
385
+ 'transfat': float(meta.get('transfat', 0.0)),
386
+ 'cholesterol': float(meta.get('cholesterol', 0.0)),
387
+
388
+ # Vitamin & Khoáng (Map theo mẫu)
389
+ 'vitamina': float(meta.get('vitamina', 0.0)),
390
+ 'vitamind': float(meta.get('vitamind', 0.0)),
391
+ 'vitaminc': float(meta.get('vitaminc', 0.0)),
392
+ 'vitaminb6': float(meta.get('vitaminb6', 0.0)),
393
+ 'vitaminb12': float(meta.get('vitaminb12', 0.0)),
394
+ 'vitamine': float(meta.get('vitamine', 0.0)),
395
+ 'vitamink': float(meta.get('vitamink', 0.0)),
396
+ 'choline': float(meta.get('choline', 0.0)),
397
+ 'canxi': float(meta.get('canxi', 0.0)),
398
+ 'fe': float(meta.get('fe', 0.0)),
399
+ 'magie': float(meta.get('magie', 0.0)),
400
+ 'photpho': float(meta.get('photpho', 0.0)),
401
+ 'kali': float(meta.get('kali', 0.0)),
402
+ 'natri': float(meta.get('natri', 0.0)),
403
+ 'zn': float(meta.get('zn', 0.0)),
404
+ 'water': float(meta.get('water', 0.0)),
405
+ 'caffeine': float(meta.get('caffeine', 0.0)),
406
+ 'alcohol': float(meta.get('alcohol', 0.0)),
407
+
408
+ # --- AI LOGIC FIELDS ---
409
+ 'health_score': 5,
410
+ 'score_reason': 'Món ăn cơ bản (Staple Food)',
411
+ 'meal_type_tag': '', # Sẽ điền sau
412
+ 'retrieval_vibe': 'Món ăn kèm cơ bản',
413
+
414
+ # Cờ fallback
415
+ 'is_fallback': True
416
+ }
417
+
418
+ fetched_items.append(item)
419
+
420
+ return fetched_items
421
+
422
+ except Exception as e:
423
+ print(f"⚠️ Lỗi fetch staples từ ES: {e}")
424
+ return []
chatbot/agents/nodes/app_functions/get_profile.py CHANGED
@@ -11,12 +11,13 @@ logger = logging.getLogger(__name__)
11
  def get_user_profile(state: AgentState):
12
  logger.info("---NODE: GET USER PROFILE---")
13
  user_id = state.get("user_id", 1)
 
14
 
15
- raw_profile = get_user_by_id(user_id)
16
- restrictions = get_restrictions(raw_profile["healthStatus"])
17
-
18
- final_profile = {**raw_profile, **restrictions}
19
-
20
- logger.info(f"Tổng hợp user profile cho user_id={user_id} thành công!")
21
 
22
  return {"user_profile": final_profile}
 
11
  def get_user_profile(state: AgentState):
12
  logger.info("---NODE: GET USER PROFILE---")
13
  user_id = state.get("user_id", 1)
14
+ user_profile = state.get("user_profile", None)
15
 
16
+ if not user_profile:
17
+ raw_profile = get_user_by_id(user_id)
18
+ restrictions = get_restrictions(raw_profile["healthStatus"])
19
+ final_profile = {**raw_profile, **restrictions}
20
+ else:
21
+ final_profile = user_profile
22
 
23
  return {"user_profile": final_profile}
chatbot/agents/nodes/app_functions/optimize_macros.py CHANGED
@@ -11,13 +11,13 @@ 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
 
15
  if not menu:
16
  print("⚠️ Menu rỗng, bỏ qua tối ưu hóa.")
17
- return {"final_menu": []}
18
 
19
  # --- BƯỚC 1: XÁC ĐỊNH MỤC TIÊU TỐI ƯU HÓA (CRITICAL STEP) ---
20
- # Lấy Target Ngày gốc
21
  daily_targets = np.array([
22
  float(profile.get("targetcalories", 1314)),
23
  float(profile.get("protein", 98)),
@@ -25,10 +25,7 @@ def optimize_portions_scipy(state: AgentState):
25
  float(profile.get("carbohydrate", 131))
26
  ])
27
 
28
- # Tỷ lệ các bữa
29
  meal_ratios = {"sáng": 0.25, "trưa": 0.40, "tối": 0.35}
30
-
31
- # Xác định các bữa có trong menu hiện tại
32
  generated_meals = set(d.get("assigned_meal", "").lower() for d in menu)
33
 
34
  # Tính Target Thực Tế (Optimization Target)
@@ -62,8 +59,8 @@ def optimize_portions_scipy(state: AgentState):
62
  nutrients = [
63
  float(dish.get("kcal", 0)),
64
  float(dish.get("protein", 0)),
65
- float(dish.get("lipid", 0)),
66
- float(dish.get("carbohydrate", 0))
67
  ]
68
  matrix.append(nutrients)
69
 
@@ -88,50 +85,59 @@ def optimize_portions_scipy(state: AgentState):
88
  n_dishes = len(menu)
89
  initial_guess = np.ones(n_dishes)
90
 
91
- # --- BƯỚC 3: ADAPTIVE WEIGHTS (TRÁNH BẪY LIPID) ---
92
- # Kiểm tra tính khả thi: Liệu menu này có ĐỦ chất để đạt target không?
93
-
94
- # Tính dinh dưỡng tối đa có thể đạt được (nếu ăn x2.5 suất tất cả)
95
- max_possible = matrix.dot(np.full(n_dishes, 2.5))
96
-
97
- # Trọng số mặc định: [Kcal, P, L, C]
98
- adaptive_weights = np.array([3.0, 2.0, 1.0, 1.0])
99
- nutri_names = ["Kcal", "Protein", "Lipid", "Carb"]
100
-
101
- for i in range(1, 4): # Check P, L, C
102
- # Nếu Max khả thi vẫn < 70% Target -> Menu này quá thiếu chất đó
103
- # -> Giảm trọng số về gần 0 để Solver không cố gắng cứu nó
104
- if max_possible[i] < (active_target[i] * 0.7):
105
- logger.info(f" ⚠️ Thiếu hụt {nutri_names[i]} nghiêm trọng (Max {int(max_possible[i])} < Target {int(active_target[i])}). Bỏ qua tối ưu chỉ số này.")
106
- adaptive_weights[i] = 0.01
107
-
108
- # --- BƯỚC 4: LOSS FUNCTION ---
109
- def objective(portions):
110
- # A. Loss Macro (So với Active Target)
111
- current_macros = matrix.dot(portions)
112
-
113
- # Dùng adaptive_weights để tránh bẫy
114
- diff = (current_macros - active_target) / (active_target + 1e-5)
115
- loss_macro = np.sum(adaptive_weights * (diff ** 2))
116
-
117
- # B. Loss Phân bổ Bữa ăn (Chỉ cần thiết nếu sinh nhiều bữa)
118
- loss_dist = 0
119
- if active_ratios_sum > 0.5: # Chỉ tính nếu sinh > 1 bữa
120
- kcal_row = matrix[0]
121
- for m_type, indices in meal_indices.items():
122
- if not indices: continue
123
- current_meal_kcal = np.sum(kcal_row[indices] * portions[indices])
124
- target_meal = target_kcal_per_meal.get(m_type, 0)
125
- d = (current_meal_kcal - target_meal) / (target_meal + 1e-5)
126
- loss_dist += (d ** 2)
127
-
128
- return loss_macro + (1.5 * loss_dist)
129
-
130
- # 5. Run Optimization
131
- res = minimize(objective, initial_guess, method='SLSQP', bounds=bounds)
 
 
 
 
 
 
 
 
 
 
132
 
133
  # 6. Apply Results
134
- optimized_portions = res.x
135
  final_menu = []
136
  total_stats = np.zeros(4)
137
  achieved_meal_kcal = {"sáng": 0, "trưa": 0, "tối": 0}
@@ -143,15 +149,15 @@ def optimize_portions_scipy(state: AgentState):
143
  final_dish["portion_scale"] = float(round(ratio, 2))
144
  final_dish["final_kcal"] = int(dish.get("kcal", 0) * ratio)
145
  final_dish["final_protein"] = int(dish.get("protein", 0) * ratio)
146
- final_dish["final_lipid"] = int(dish.get("lipid", 0) * ratio)
147
- final_dish["final_carb"] = int(dish.get("carbohydrate", 0) * ratio)
148
 
149
- logger.info(f" - {dish['name']} ({dish['assigned_meal']}): x{final_dish['portion_scale']} suất -> {final_dish['final_kcal']}kcal, {final_dish['final_protein']}g Protein, {final_dish['final_lipid']}g Lipid, {final_dish['final_carb']}g Carbohydrate")
150
 
151
  final_menu.append(final_dish)
152
  total_stats += np.array([
153
  final_dish["final_kcal"], final_dish["final_protein"],
154
- final_dish["final_lipid"], final_dish["final_carb"]
155
  ])
156
 
157
  m_type = dish.get("assigned_meal", "").lower()
@@ -168,7 +174,7 @@ def optimize_portions_scipy(state: AgentState):
168
  logger.info(row_format.format(*headers))
169
  logger.info(" " + "-"*65)
170
 
171
- labels = ["Năng lượng", "Protein", "Lipid", "Carb"]
172
  units = ["Kcal", "g", "g", "g"]
173
 
174
  for i in range(4):
@@ -200,5 +206,6 @@ def optimize_portions_scipy(state: AgentState):
200
 
201
  return {
202
  "final_menu": final_menu,
 
203
  "user_profile": profile
204
  }
 
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.")
18
+ return {"final_menu": [], "user_profile": profile}
19
 
20
  # --- BƯỚC 1: XÁC ĐỊNH MỤC TIÊU TỐI ƯU HÓA (CRITICAL STEP) ---
 
21
  daily_targets = np.array([
22
  float(profile.get("targetcalories", 1314)),
23
  float(profile.get("protein", 98)),
 
25
  float(profile.get("carbohydrate", 131))
26
  ])
27
 
 
28
  meal_ratios = {"sáng": 0.25, "trưa": 0.40, "tối": 0.35}
 
 
29
  generated_meals = set(d.get("assigned_meal", "").lower() for d in menu)
30
 
31
  # Tính Target Thực Tế (Optimization Target)
 
59
  nutrients = [
60
  float(dish.get("kcal", 0)),
61
  float(dish.get("protein", 0)),
62
+ float(dish.get("totalfat", 0)),
63
+ float(dish.get("carbs", 0))
64
  ]
65
  matrix.append(nutrients)
66
 
 
85
  n_dishes = len(menu)
86
  initial_guess = np.ones(n_dishes)
87
 
88
+ # --- BƯỚC 3: ADAPTIVE WEIGHTS ---
89
+ optimized_portions = initial_guess
90
+ try:
91
+ # Tính dinh dưỡng tối đa có thể đạt được (nếu ăn x2.5 suất tất cả)
92
+ max_possible = matrix.dot(np.full(n_dishes, 2.5))
93
+
94
+ # Trọng số mặc định: [Kcal, P, L, C]
95
+ adaptive_weights = np.array([3.0, 2.0, 1.0, 1.0])
96
+ nutri_names = ["Kcal", "Protein", "Lipid", "Carb"]
97
+
98
+ for i in range(1, 4): # Check P, L, C
99
+ # Nếu Max khả thi vẫn < 70% Target -> Menu này quá thiếu chất đó
100
+ # -> Giảm trọng số về gần 0 để Solver không cố gắng cứu nó
101
+ if max_possible[i] < (active_target[i] * 0.7):
102
+ logger.info(f" ⚠️ Thiếu hụt {nutri_names[i]} nghiêm trọng (Max {int(max_possible[i])} < Target {int(active_target[i])}). Bỏ qua tối ưu chỉ số này.")
103
+ adaptive_weights[i] = 0.01
104
+
105
+ # --- BƯỚC 4: LOSS FUNCTION ---
106
+ def objective(portions):
107
+ # A. Loss Macro (So với Active Target)
108
+ current_macros = matrix.dot(portions)
109
+
110
+ # Dùng adaptive_weights để tránh bẫy
111
+ diff = (current_macros - active_target) / (active_target + 1e-5)
112
+ loss_macro = np.sum(adaptive_weights * (diff ** 2))
113
+
114
+ # B. Loss Phân bổ Bữa ăn (Chỉ cần thi���t nếu sinh nhiều bữa)
115
+ loss_dist = 0
116
+ if active_ratios_sum > 0.5: # Chỉ tính nếu sinh > 1 bữa
117
+ kcal_row = matrix[0]
118
+ for m_type, indices in meal_indices.items():
119
+ if not indices: continue
120
+ current_meal_kcal = np.sum(kcal_row[indices] * portions[indices])
121
+ target_meal = target_kcal_per_meal.get(m_type, 0)
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...")
129
+ res = minimize(objective, initial_guess, method='SLSQP', bounds=bounds)
130
+
131
+ if res.success:
132
+ optimized_portions = res.x
133
+ else:
134
+ logger.warning(f"⚠️ Solver không hội tụ: {res.message}. Dùng portions mặc định.")
135
+
136
+ except Exception as e:
137
+ logger.error(f"🔥 LỖI CRITICAL KHI CHẠY SOLVER: {e}")
138
+ optimized_portions = np.ones(n_dishes)
139
 
140
  # 6. Apply Results
 
141
  final_menu = []
142
  total_stats = np.zeros(4)
143
  achieved_meal_kcal = {"sáng": 0, "trưa": 0, "tối": 0}
 
149
  final_dish["portion_scale"] = float(round(ratio, 2))
150
  final_dish["final_kcal"] = int(dish.get("kcal", 0) * ratio)
151
  final_dish["final_protein"] = int(dish.get("protein", 0) * ratio)
152
+ final_dish["final_totalfat"] = int(dish.get("totalfat", 0) * ratio)
153
+ final_dish["final_carbs"] = int(dish.get("carbs", 0) * ratio)
154
 
155
+ logger.info(f" - {dish['name']} ({dish['assigned_meal']}): x{final_dish['portion_scale']} suất -> {final_dish['final_kcal']}kcal, {final_dish['final_protein']}g Protein, {final_dish['final_totalfat']}g Total Fat, {final_dish['final_carbs']}g Carbs")
156
 
157
  final_menu.append(final_dish)
158
  total_stats += np.array([
159
  final_dish["final_kcal"], final_dish["final_protein"],
160
+ final_dish["final_totalfat"], final_dish["final_carbs"]
161
  ])
162
 
163
  m_type = dish.get("assigned_meal", "").lower()
 
174
  logger.info(row_format.format(*headers))
175
  logger.info(" " + "-"*65)
176
 
177
+ labels = ["Năng lượng", "Protein", "TotalFat", "Carb"]
178
  units = ["Kcal", "g", "g", "g"]
179
 
180
  for i in range(4):
 
206
 
207
  return {
208
  "final_menu": final_menu,
209
+ "reason": reason,
210
  "user_profile": profile
211
  }
chatbot/agents/nodes/app_functions/optimize_select.py CHANGED
@@ -8,64 +8,76 @@ logger = logging.getLogger(__name__)
8
 
9
  def calculate_top_options(state: SwapState):
10
  logger.info("---NODE: SCIPY RANKING (MATH FILTER)---")
11
- candidates = state["candidates"]
12
  food_old = state["food_old"]
13
 
14
- if not candidates: return {"top_candidates": []}
 
 
15
 
16
  # 1. Xác định "KPI" từ món cũ
17
  old_scale = float(food_old.get("portion_scale", 1.0))
18
  target_vector = np.array([
19
  float(food_old.get("kcal", 0)) * old_scale,
20
  float(food_old.get("protein", 0)) * old_scale,
21
- float(food_old.get("lipid", 0)) * old_scale,
22
- float(food_old.get("carbohydrate", 0)) * old_scale
23
  ])
24
  weights = np.array([3.0, 2.0, 1.0, 1.0])
25
 
26
  # Bound của món cũ
27
  bounds = food_old.get("solver_bounds", (0.5, 2.0))
28
 
29
- # Hàm tính toán (giữ nguyên logic cũ)
30
  def calculate_score(candidate):
31
- base_vector = np.array([
32
- float(candidate.get("kcal", 0)),
33
- float(candidate.get("protein", 0)),
34
- float(candidate.get("lipid", 0)),
35
- float(candidate.get("carbohydrate", 0))
36
- ])
37
- if np.sum(base_vector) == 0: return float('inf'), 1.0
 
38
 
39
- def objective(x):
40
- current_vector = base_vector * x
41
- diff = (current_vector - target_vector) / (target_vector + 1e-5)
42
- loss = np.sum(weights * (diff ** 2))
43
- return loss
44
 
45
- res = minimize_scalar(objective, bounds=bounds, method='bounded')
46
- return res.fun, res.x
 
 
 
 
 
 
47
 
48
  # 3. Chấm điểm hàng loạt
49
  scored_candidates = []
50
  for item in candidates:
51
- loss, scale = calculate_score(item)
 
52
 
53
- # Chỉ lấy những món có sai số chấp nhận được (Loss < 5.0)
54
- if loss < 5.0:
55
- item_score = item.copy()
56
- item_score["optimization_loss"] = round(loss, 4)
57
- item_score["portion_scale"] = round(scale, 2)
58
 
59
- # Tính chỉ số hiển thị sau khi scale
60
- item_score["final_kcal"] = int(item["kcal"] * scale)
61
- item_score["final_protein"] = int(item["protein"] * scale)
62
- item_score["final_lipid"] = int(item["lipid"] * scale)
63
- item_score["final_carb"] = int(item["carbohydrate"] * scale)
64
 
65
- scored_candidates.append(item_score)
 
 
 
66
 
67
  # 4. Lấy Top 10 tốt nhất
68
- # Sắp xếp theo Loss (thấp nhất là giống dinh dưỡng nhất)
69
  scored_candidates.sort(key=lambda x: x["optimization_loss"])
70
  top_10 = scored_candidates[:10]
71
 
 
8
 
9
  def calculate_top_options(state: SwapState):
10
  logger.info("---NODE: SCIPY RANKING (MATH FILTER)---")
11
+ candidates = state.get("candidates", [])
12
  food_old = state["food_old"]
13
 
14
+ if not candidates or not food_old:
15
+ logger.warning("⚠️ Candidates hoặc Food_old rỗng, bỏ qua tính toán.")
16
+ return {"top_candidates": []}
17
 
18
  # 1. Xác định "KPI" từ món cũ
19
  old_scale = float(food_old.get("portion_scale", 1.0))
20
  target_vector = np.array([
21
  float(food_old.get("kcal", 0)) * old_scale,
22
  float(food_old.get("protein", 0)) * old_scale,
23
+ float(food_old.get("totalfat", 0)) * old_scale,
24
+ float(food_old.get("carbs", 0)) * old_scale
25
  ])
26
  weights = np.array([3.0, 2.0, 1.0, 1.0])
27
 
28
  # Bound của món cũ
29
  bounds = food_old.get("solver_bounds", (0.5, 2.0))
30
 
31
+ # Hàm tính toán
32
  def calculate_score(candidate):
33
+ try:
34
+ base_vector = np.array([
35
+ float(candidate.get("kcal", 0)),
36
+ float(candidate.get("protein", 0)),
37
+ float(candidate.get("totalfat", 0)),
38
+ float(candidate.get("carbs", 0))
39
+ ])
40
+ if np.sum(base_vector) == 0: return float('inf'), 1.0
41
 
42
+ def objective(x):
43
+ current_vector = base_vector * x
44
+ diff = (current_vector - target_vector) / (target_vector + 1e-5)
45
+ loss = np.sum(weights * (diff ** 2))
46
+ return loss
47
 
48
+ res = minimize_scalar(objective, bounds=bounds, method='bounded')
49
+ if res.success:
50
+ return res.fun, res.x
51
+ else:
52
+ return float('inf'), 1.0
53
+ except Exception as inner_e:
54
+ logger.debug(f"Bỏ qua món {candidate.get('name')} do lỗi toán học: {inner_e}")
55
+ return float('inf'), 1.0
56
 
57
  # 3. Chấm điểm hàng loạt
58
  scored_candidates = []
59
  for item in candidates:
60
+ try:
61
+ loss, scale = calculate_score(item)
62
 
63
+ # Chỉ lấy những món có sai số chấp nhận được
64
+ if loss < 10.0:
65
+ item_score = item.copy()
66
+ item_score["optimization_loss"] = round(loss, 4)
67
+ item_score["portion_scale"] = round(scale, 2)
68
 
69
+ # Tính chỉ số hiển thị sau khi scale
70
+ item_score["final_kcal"] = int(item["kcal"] * scale)
71
+ item_score["final_protein"] = int(item["protein"] * scale)
72
+ item_score["final_totalfat"] = int(item["totalfat"] * scale)
73
+ item_score["final_carbs"] = int(item["carbs"] * scale)
74
 
75
+ scored_candidates.append(item_score)
76
+ except Exception as e:
77
+ logger.warning(f"Lỗi khi xử lý món {item.get('name', 'N/A')}: {e}")
78
+ continue
79
 
80
  # 4. Lấy Top 10 tốt nhất
 
81
  scored_candidates.sort(key=lambda x: x["optimization_loss"])
82
  top_10 = scored_candidates[:10]
83
 
chatbot/agents/nodes/app_functions/select_meal.py CHANGED
@@ -21,12 +21,11 @@ def llm_finalize_choice(state: SwapState):
21
  # 1. Format danh sách hiển thị kèm Real ID
22
  options_text = ""
23
  for item in top_candidates:
24
- # Lấy meal_id thực tế từ dữ liệu
25
  real_id = item.get("meal_id")
26
 
27
  options_text += (
28
- f"ID [{real_id}] - {item['name']}\n" # <--- Hiển thị ID thật
29
- f" - Số liệu: {item['final_kcal']} Kcal | P:{item['final_protein']}g | L:{item['final_lipid']}g | C:{item['final_carb']}g\n"
30
  f" - Độ lệch (Loss): {item['optimization_loss']}\n"
31
  )
32
 
@@ -48,16 +47,15 @@ def llm_finalize_choice(state: SwapState):
48
  decision = llm_structured.invoke(system_prompt)
49
  target_id = decision.selected_meal_id
50
  except Exception as e:
51
- logger.info(f"⚠️ LLM Error: {e}. Fallback to first option.")
52
  # Fallback lấy ID của món đầu tiên
53
  target_id = top_candidates[0].get("meal_id")
54
  decision = ChefDecision(selected_meal_id=target_id, reason="Fallback do lỗi hệ thống.")
55
 
56
- # 4. Mapping lại bằng meal_id (Chính xác tuyệt đối)
57
  selected_full_candidate = None
58
 
59
  for item in top_candidates:
60
- # So sánh ID (lưu ý ép kiểu nếu cần thiết để tránh lỗi string vs int)
61
  if int(item.get("meal_id")) == int(target_id):
62
  selected_full_candidate = item
63
  break
@@ -70,26 +68,22 @@ def llm_finalize_choice(state: SwapState):
70
  # Bổ sung lý do
71
  selected_full_candidate["chef_reason"] = decision.reason
72
 
73
- # Bổ sung lý do
74
- selected_full_candidate["chef_reason"] = decision.reason
75
-
76
  #-------------------------------------------------------------------
77
  # --- PHẦN MỚI: IN BẢNG SO SÁNH (VISUAL COMPARISON) ---
78
- logger.info(f"\n✅ CHEF SELECTED: {selected_full_candidate['name']} (ID: {selected_full_candidate['meal_id']})")
79
  logger.info(f"📝 Lý do: {decision.reason}")
80
 
81
  # Lấy thông tin món cũ (đã scale ở menu gốc)
82
- # Lưu ý: food_old trong state là thông tin gốc hoặc đã tính toán ở daily menu
83
  old_kcal = float(food_old.get('final_kcal', food_old['kcal']))
84
  old_pro = float(food_old.get('final_protein', food_old['protein']))
85
- old_fat = float(food_old.get('final_lipid', food_old['lipid']))
86
- old_carb = float(food_old.get('final_carb', food_old['carbohydrate']))
87
 
88
  # Lấy thông tin món mới (đã re-scale bởi Scipy)
89
  new_kcal = selected_full_candidate['final_kcal']
90
  new_pro = selected_full_candidate['final_protein']
91
- new_fat = selected_full_candidate['final_lipid']
92
- new_carb = selected_full_candidate['final_carb']
93
  scale = selected_full_candidate['portion_scale']
94
 
95
  # In bảng
@@ -101,7 +95,6 @@ def llm_finalize_choice(state: SwapState):
101
  logger.info(row_fmt.format(*headers))
102
  logger.info(" " + "-"*68)
103
 
104
- # Helper in dòng
105
  def print_row(label, old_val, new_val, unit=""):
106
  diff = new_val - old_val
107
  diff_str = f"{diff:+.1f}"
@@ -120,7 +113,7 @@ def llm_finalize_choice(state: SwapState):
120
 
121
  print_row("Năng lượng", old_kcal, new_kcal, "Kcal")
122
  print_row("Protein", old_pro, new_pro, "g")
123
- print_row("Lipid", old_fat, new_fat, "g")
124
  print_row("Carb", old_carb, new_carb, "g")
125
  logger.info(" " + "-"*68)
126
 
 
21
  # 1. Format danh sách hiển thị kèm Real ID
22
  options_text = ""
23
  for item in top_candidates:
 
24
  real_id = item.get("meal_id")
25
 
26
  options_text += (
27
+ f"ID [{real_id}] - {item['name']}\n"
28
+ f" - Số liệu: {item['final_kcal']} Kcal | P:{item['final_protein']}g | L:{item['final_totalfat']}g | C:{item['final_carbs']}g\n"
29
  f" - Độ lệch (Loss): {item['optimization_loss']}\n"
30
  )
31
 
 
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.")
51
  # Fallback lấy ID của món đầu tiên
52
  target_id = top_candidates[0].get("meal_id")
53
  decision = ChefDecision(selected_meal_id=target_id, reason="Fallback do lỗi hệ thống.")
54
 
55
+ # 4. Mapping lại bằng meal_id
56
  selected_full_candidate = None
57
 
58
  for item in top_candidates:
 
59
  if int(item.get("meal_id")) == int(target_id):
60
  selected_full_candidate = item
61
  break
 
68
  # Bổ sung lý do
69
  selected_full_candidate["chef_reason"] = decision.reason
70
 
 
 
 
71
  #-------------------------------------------------------------------
72
  # --- PHẦN MỚI: IN BẢNG SO SÁNH (VISUAL COMPARISON) ---
73
+ logger.info(f"✅ CHEF SELECTED: {selected_full_candidate['name']} (ID: {selected_full_candidate['meal_id']})")
74
  logger.info(f"📝 Lý do: {decision.reason}")
75
 
76
  # Lấy thông tin món cũ (đã scale ở menu gốc)
 
77
  old_kcal = float(food_old.get('final_kcal', food_old['kcal']))
78
  old_pro = float(food_old.get('final_protein', food_old['protein']))
79
+ old_fat = float(food_old.get('final_totalfat', food_old['totalfat']))
80
+ old_carb = float(food_old.get('final_carbs', food_old['carbs']))
81
 
82
  # Lấy thông tin món mới (đã re-scale bởi Scipy)
83
  new_kcal = selected_full_candidate['final_kcal']
84
  new_pro = selected_full_candidate['final_protein']
85
+ new_fat = selected_full_candidate['final_totalfat']
86
+ new_carb = selected_full_candidate['final_carbs']
87
  scale = selected_full_candidate['portion_scale']
88
 
89
  # In bảng
 
95
  logger.info(row_fmt.format(*headers))
96
  logger.info(" " + "-"*68)
97
 
 
98
  def print_row(label, old_val, new_val, unit=""):
99
  diff = new_val - old_val
100
  diff_str = f"{diff:+.1f}"
 
113
 
114
  print_row("Năng lượng", old_kcal, new_kcal, "Kcal")
115
  print_row("Protein", old_pro, new_pro, "g")
116
+ print_row("TotalFat", old_fat, new_fat, "g")
117
  print_row("Carb", old_carb, new_carb, "g")
118
  logger.info(" " + "-"*68)
119
 
chatbot/agents/nodes/app_functions/select_menu.py CHANGED
@@ -16,129 +16,109 @@ class SelectedDish(BaseModel):
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)"
18
  )
19
- reason: str = Field(description="Lý do chọn (ngắn gọn)")
20
 
21
  class DailyMenuStructure(BaseModel):
22
  dishes: List[SelectedDish] = Field(description="Danh sách các món ăn được chọn")
 
23
 
24
- # --- NODE LOGIC ---
25
  def select_menu_structure(state: AgentState):
26
  logger.info("---NODE: AI SELECTOR (FULL MACRO AWARE)---")
27
- profile = state["user_profile"]
28
- candidates = state.get("candidate_pool", [])
29
- meals_req = state["meals_to_generate"]
30
 
31
- if len(candidates) == 0:
32
  logger.warning("⚠️ Danh sách ứng viên rỗng, không thể chọn món.")
33
  return {"selected_structure": []}
34
 
35
- # 1. TÍNH TOÁN MỤC TIÊU CHI TIẾT TỪNG BỮA (Budgeting)
36
  daily_targets = {
37
- "kcal": float(profile.get('targetcalories', 2000)),
38
- "protein": float(profile.get('protein', 150)),
39
- "lipid": float(profile.get('totalfat', 60)),
40
- "carbohydrate": float(profile.get('carbohydrate', 200))
41
  }
42
  ratios = {"sáng": 0.25, "trưa": 0.40, "tối": 0.35}
43
 
44
- # Tính target chi tiết cho từng bữa
45
- # Kết quả dạng: {'sáng': {'kcal': 500, 'protein': 37.5, ...}, 'trưa': ...}
46
  meal_targets = {}
47
  for meal, ratio in ratios.items():
48
  meal_targets[meal] = {
49
  k: int(v * ratio) for k, v in daily_targets.items()
50
  }
51
 
52
- # --- LOGIC TẠO HƯỚNG DẪN ĐỘNG ---
 
 
53
  health_condition = profile.get('healthStatus', 'Bình thường')
54
- safety_instruction = f"""
55
- - Tình trạng sức khỏe: {health_condition}.
56
- - Ưu tiên: Các món thanh đạm, chế biến đơn giản (Hấp/Luộc) nếu người dùng có nhiều bệnh nền.
57
- """
58
 
 
 
 
 
 
 
 
 
 
 
59
  # 2. TIỀN XỬ LÝ & PHÂN NHÓM CANDIDATES
60
- candidates_by_meal = {"sáng": [], "trưa": [], "tối": []}
 
61
 
62
- for m in candidates:
63
- if m.get('kcal', 0) > 1500: continue
64
- if m.get('kcal', 0) < 100: continue
65
-
66
- tag = m.get('meal_type_tag', '').lower()
67
- if "sáng" in tag: candidates_by_meal["sáng"].append(m)
68
- elif "trưa" in tag: candidates_by_meal["trưa"].append(m)
69
- elif "tối" in tag: candidates_by_meal["tối"].append(m)
70
-
71
- def format_list(items):
72
- if not items: return ""
73
- return "\n".join([
74
- f"- {m['name']}: {m.get('kcal')} kcal | P:{m.get('protein')}g | L:{m.get('lipid')}g | C:{m.get('carbohydrate')}g"
75
- for m in items
76
- ])
77
 
 
78
  def get_target_str(meal):
79
  t = meal_targets.get(meal, {})
80
- return f"{t.get('kcal')} Kcal (P: {t.get('protein')}g, L: {t.get('lipid')}g, C: {t.get('carbohydrate')}g)"
81
-
82
- # 3. XÂY DỰNG PROMPT (Kèm full chỉ số P/L/C)
83
- guidance_sang = ""
84
- if 'sáng' in meals_req:
85
- guidance_sang = f"""BỮA SÁNG (Mục tiêu ~{get_target_str('sáng')}):
86
- - Chọn 1 món chính có năng lượng ĐỦ LỚN (gần {get_target_str('sáng')}).
87
- - Có thể bổ sung 1 món phụ sao cho dinh dưỡng cân bằng.
88
- - Ưu tiên món nước (Phở/Bún) hoặc Bánh mì/Xôi, không nên ăn lẩu vào bữa sáng."""
89
-
90
- guidance_trua = ""
91
- if 'trưa' in meals_req:
92
- guidance_trua = f"""BỮA TRƯA (Mục tiêu ~{get_target_str('trưa')}):
93
- - Chọn tổ hợp gồm 3 món:
94
- 1. Main: Món cung cấp Protein chính.
95
- 2. Carb: Nguồn tinh bột thanh đạm như cơm trắng, cơm lứt, khoai, bún/phở (ít gia vị/dầu mỡ nếu Main đã đậm đà).
96
- 3. Side: Rau/Canh để bổ sung Xơ.
97
- - Hoặc chọn 1 món Hỗn hợp (VD: Cơm chiên/Mì xào) nhưng không chọn thêm món mặn.
98
- - Lưu ý: Món 'Main' và 'Side' phải tách biệt. Đừng chọn món rau xào thịt làm món Side (đó là Main)."""
99
-
100
- guidance_toi = ""
101
- if 'tối' in meals_req:
102
- guidance_toi = f"""BỮA TỐI (Mục tiêu ~{get_target_str('tối')}):
103
- - Tương tự như bữa trưa.
104
- - Ưu tiên các món nhẹ bụng, dễ tiêu hóa.
105
- - Giảm lượng tinh bột so với bữa trưa."""
106
-
107
- # 2. Ghép vào prompt chính
108
- system_prompt = f"""
109
- Bạn là Chuyên gia Dinh dưỡng AI.
110
- Nhiệm vụ: Chọn thực đơn cho các bữa: {', '.join(meals_req)} từ danh sách ứng viên đã được lọc sơ bộ. Mỗi bữa bao gồm từ 1 đến 3 món.
111
-
112
- TỔNG MỤC TIÊU NGÀY: {int(daily_targets['kcal'])} Kcal | Protein: {int(daily_targets['protein'])}g | Lipid: {int(daily_targets['lipid'])}g | Carbohydrate: {int(daily_targets['carbohydrate'])}g.
113
-
114
- NGUYÊN TẮC CỐT LÕI:
115
- 1. Nhìn vào số liệu: Hãy chọn món sao cho tổng dinh dưỡng xấp xỉ với Mục Tiêu Chi Tiết của từng bữa.
116
- 2. Cảm quan đầu bếp: Món ăn phải hợp vị (VD: Canh chua đi với Cá kho).
117
- 3. Ước lượng: Không cần tính chính xác tuyệt đối, nhưng đừng chọn món 5g Protein cho mục tiêu 60g Protein.
118
-
119
- NGUYÊN TẮC AN TOÀN:
120
- Mặc dù danh sách món đã được lọc, bạn vẫn là chốt chặn cuối cùng. Hãy tuân thủ:
121
- {safety_instruction}
122
 
123
- HƯỚNG DẪN TỪNG BỮA
124
- {guidance_sang}
125
- {guidance_trua}
126
- {guidance_toi}
127
-
128
- DANH SÁCH ỨNG VIÊN
129
- {format_list(candidates_by_meal['sáng'])}
130
- {format_list(candidates_by_meal['trưa'])}
131
- {format_list(candidates_by_meal['tối'])}
132
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
 
134
  logger.info("Prompt:")
135
  logger.info(system_prompt)
136
 
137
- # Gọi LLM
138
- llm_structured = llm.with_structured_output(DailyMenuStructure, strict=True)
139
- result = llm_structured.invoke(system_prompt)
140
-
141
- # In danh sách các món đã chọn lần lượt theo bữa
 
 
 
 
 
 
 
142
  def print_menu_by_meal(daily_menu):
143
  menu_by_meal = defaultdict(list)
144
  for dish in daily_menu.dishes:
@@ -148,16 +128,14 @@ def select_menu_structure(state: AgentState):
148
  if meal in menu_by_meal:
149
  logger.info(f"\n🍽 Bữa {meal.upper()}:")
150
  for d in menu_by_meal[meal]:
151
- logger.info(f" - {d.name} ({d.role}): {d.reason}")
152
 
153
  logger.info("\n--- MENU ĐÃ CHỌN ---")
154
  print_menu_by_meal(result)
155
 
156
  # 4. HẬU XỬ LÝ (Gán Bounds)
157
  selected_full_info = []
158
- all_clean_candidates = []
159
- for sublist in candidates_by_meal.values():
160
- all_clean_candidates.extend(sublist)
161
  candidate_map = {m['name']: m for m in all_clean_candidates}
162
 
163
  for choice in result.dishes:
@@ -165,33 +143,27 @@ def select_menu_structure(state: AgentState):
165
  dish_data = candidate_map[choice.name].copy()
166
  dish_data["assigned_meal"] = choice.meal_type
167
 
168
- # Lấy thông tin dinh dưỡng món hiện tại
169
  d_kcal = float(dish_data.get("kcal", 0))
170
  d_pro = float(dish_data.get("protein", 0))
171
 
172
- # Lấy target bữa hiện tại (VD: Trưa)
173
  t_target = meal_targets.get(choice.meal_type.lower(), {})
174
  t_kcal = t_target.get("kcal", 500)
175
  t_pro = t_target.get("protein", 30)
176
 
177
- # --- GIAI ĐOẠN 1: TỰ ĐỘNG SỬA SAI VAI TRÒ (ROLE CORRECTION) ---
178
- final_role = choice.role # Bắt đầu bằng role AI chọn
179
-
180
  # 1. Phát hiện "Carb trá hình" (Cơm chiên/Mì xào quá nhiều thịt)
181
  if final_role == "carb" and d_pro > 15:
182
- print(f" ⚠️ Phát hiện Carb giàu đạm ({choice.name}: {d_pro}g Pro). Đổi role sang 'main'.")
183
  final_role = "main"
184
-
185
  # 2. Phát hiện "Side giàu đạm" (Salad gà/bò, Canh sườn)
186
  elif final_role == "side" and d_pro > 10:
187
- print(f" ⚠️ Phát hiện Side giàu đạm ({choice.name}: {d_pro}g Pro). Đổi role sang 'main'.")
188
  final_role = "main"
189
-
190
- # Cập nhật lại role chuẩn vào dữ liệu
191
  dish_data["role"] = final_role
192
 
193
-
194
- # --- GIAI ĐOẠN 2: THIẾT LẬP BOUNDS CƠ BẢN (BASE BOUNDS) ---
195
  lower_bound = 0.5
196
  upper_bound = 1.5
197
 
@@ -208,25 +180,23 @@ def select_menu_structure(state: AgentState):
208
  lower_bound, upper_bound = 0.6, 1.8
209
 
210
 
211
- # --- GIAI ĐOẠN 3: KIỂM TRA AN TOÀN & GHI ĐÈ ---
212
-
213
- # Override A: Nếu món Main có Protein quá khủng so với Target
214
- # (VD: Món 52g Pro vs Target Bữa 30g Pro) -> Phải cho phép giảm sâu
215
  if final_role == "main" and d_pro > t_pro:
216
- print(f" ⚠️ Món {choice.name} thừa đạm ({d_pro}g > {t_pro}g). Mở rộng bound xuống thấp.")
217
- lower_bound = 0.3 # Cho phép giảm xuống 30% suất
218
- upper_bound = min(upper_bound, 1.2) # Không cho phép tăng quá nhiều
219
 
220
  # Override B: Nếu món quá nhiều Calo (Chiếm > 80% Kcal cả bữa)
221
  if d_kcal > (t_kcal * 0.8):
222
- print(f" ⚠️ Món {choice.name} quá đậm năng lượng ({d_kcal} kcal). Siết chặt bound.")
223
  lower_bound = 0.3
224
- upper_bound = min(upper_bound, 1.0) # Chặn không cho tăng
225
 
226
  # Override C: Nếu là món Side nhưng Protein vẫn hơi cao (5-10g)
227
- # Cho phép giảm để nhường quota Protein cho món Main
228
  if final_role == "side" and d_pro > 5:
229
- lower_bound = 0.2 # Cho phép ăn ít rau này lại
 
230
 
231
  # --- KẾT THÚC: GÁN VÀO DỮ LIỆU ---
232
  dish_data["solver_bounds"] = (lower_bound, upper_bound)
@@ -234,4 +204,28 @@ def select_menu_structure(state: AgentState):
234
 
235
  return {
236
  "selected_structure": selected_full_info,
237
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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)"
18
  )
 
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)---")
26
+ profile = state.get("user_profile", {})
27
+ full_pool = state.get("candidate_pool", [])
28
+ meals_req = state.get("meals_to_generate", [])
29
 
30
+ if len(full_pool) == 0:
31
  logger.warning("⚠️ Danh sách ứng viên rỗng, không thể chọn món.")
32
  return {"selected_structure": []}
33
 
34
+ # 1. TÍNH TOÁN MỤC TIÊU CHI TIẾT TỪNG BỮA
35
  daily_targets = {
36
+ "kcal": float(profile.get('targetcalories', 0)),
37
+ "protein": float(profile.get('protein', 0)),
38
+ "totalfat": float(profile.get('totalfat', 0)),
39
+ "carbs": float(profile.get('carbohydrate', 0))
40
  }
41
  ratios = {"sáng": 0.25, "trưa": 0.40, "tối": 0.35}
42
 
 
 
43
  meal_targets = {}
44
  for meal, ratio in ratios.items():
45
  meal_targets[meal] = {
46
  k: int(v * ratio) for k, v in daily_targets.items()
47
  }
48
 
49
+ # --- LOGIC TẠO HƯỚNG DẪN ĐỘNG CHO PROMPT ---
50
+ avoid_items = ", ".join(profile.get('Kiêng', []))
51
+ limit_items = ", ".join(profile.get('Hạn chế', []))
52
  health_condition = profile.get('healthStatus', 'Bình thường')
 
 
 
 
53
 
54
+ safety_instruction = ""
55
+ if health_condition and health_condition.strip() not in ["Bình thường", "Không có", "Khỏe mạnh"]:
56
+ safety_instruction += f"- Tình trạng sức khỏe: {health_condition}.\n"
57
+ if avoid_items:
58
+ safety_instruction += f"- TUYỆT ĐỐI TRÁNH: {avoid_items}. (Nếu thấy món chứa thành phần này trong danh sách, hãy BỎ QUA ngay lập tức).\n"
59
+ if limit_items:
60
+ safety_instruction += f"- HẠN CHẾ TỐI ĐA: {limit_items}.\n"
61
+ if safety_instruction:
62
+ safety_instruction = f"\nNGUYÊN TẮC AN TOÀN:\n{safety_instruction}\n"
63
+
64
  # 2. TIỀN XỬ LÝ & PHÂN NHÓM CANDIDATES
65
+ primary_pool = [m for m in full_pool if not m.get("is_fallback", False)]
66
+ backup_pool = [m for m in full_pool if m.get("is_fallback", False)]
67
 
68
+ primary_text = format_pool_detailed(primary_pool, "KHO MÓN ĂN NGON (Ưu tiên dùng)")
69
+ backup_text = format_pool_detailed(backup_pool, "KHO LƯƠNG THỰC CƠ BẢN")
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
+ # 3. XÂY DỰNG PROMPT
72
  def get_target_str(meal):
73
  t = meal_targets.get(meal, {})
74
+ return f"{t.get('kcal')} Kcal (P: {t.get('protein')}g, Fat: {t.get('totalfat')}g, Carb: {t.get('carbs')}g)"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
 
76
+ system_prompt = f"""
77
+ Vai trò: Đầu bếp trưởng kiêm Chuyên gia dinh dưỡng.
78
+ Nhiệm vụ: Ghép thực đơn cho: {', '.join(meals_req)}.
79
+
80
+ MỤC TIÊU CỤ THỂ TỪNG BỮA (Hãy nhẩm tính để chọn món sát với mục tiêu nhất):
81
+ {f"- SÁNG: ~{get_target_str('sáng')}" if 'sáng' in meals_req else ""}
82
+ {f"- TRƯA: ~{get_target_str('trưa')}" if 'trưa' in meals_req else ""}
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
+
90
+ NGUYÊN TẮC CHỌN MÓN (QUAN TRỌNG):
91
+ 1. Cấu trúc & Dinh dưỡng (Linh hoạt):
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:
99
+ - Luôn quét trong "KHO MÓN ĂN NGON" trước.
100
+ - Nếu chọn Cách B: Hãy tìm món canh/rau trong kho ngon trước. Chỉ khi kho ngon không có hoặc làm vỡ Target Kcal (quá cao), mới lấy Cơm/Rau từ "KHO LƯƠNG THỰC CƠ BẢN".
101
+
102
+ 3. Chiến thuật ghép món:
103
+ - Nếu Target bữa thấp (<500k): Ưu tiên 1 món hỗn hợp nhẹ hoặc bộ 3 món (Cá/Hấp + Cơm ít + Canh rau).
104
+ - Nếu Target bữa cao (>700k): Ưu tiên bộ 3 món đầy đủ hoặc món hỗn hợp đậm đà.
105
+ """
106
 
107
  logger.info("Prompt:")
108
  logger.info(system_prompt)
109
 
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.")
117
+
118
+ except Exception as e:
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:
 
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:
 
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))
147
  d_pro = float(dish_data.get("protein", 0))
148
 
 
149
  t_target = meal_targets.get(choice.meal_type.lower(), {})
150
  t_kcal = t_target.get("kcal", 500)
151
  t_pro = t_target.get("protein", 30)
152
 
153
+ # --- GIAI ĐOẠN 1: TỰ ĐỘNG SỬA SAI VAI TRÒ ---
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
165
 
166
+ # --- GIAI ĐOẠN 2: THIẾT LẬP BOUNDS CƠ BẢN ---
 
167
  lower_bound = 0.5
168
  upper_bound = 1.5
169
 
 
180
  lower_bound, upper_bound = 0.6, 1.8
181
 
182
 
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 đạ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 ---
202
  dish_data["solver_bounds"] = (lower_bound, upper_bound)
 
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
chatbot/agents/nodes/chatbot/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- from .classify_topic import classify_topic, route_by_topic
2
  from .meal_identify import meal_identify
3
  from .suggest_meal_node import suggest_meal_node
4
  from .food_query import food_query
@@ -8,7 +8,9 @@ from .generate_final_response import generate_final_response
8
  from .food_suggestion import food_suggestion
9
  from .policy import policy
10
  from .select_food import select_food
11
-
 
 
12
 
13
  __all__ = [
14
  "classify_topic",
@@ -22,4 +24,7 @@ __all__ = [
22
  "food_suggestion",
23
  "policy",
24
  "select_food",
 
 
 
25
  ]
 
1
+ from .classify_topic import classify_topic
2
  from .meal_identify import meal_identify
3
  from .suggest_meal_node import suggest_meal_node
4
  from .food_query import food_query
 
8
  from .food_suggestion import food_suggestion
9
  from .policy import policy
10
  from .select_food import select_food
11
+ from .ask_info import ask_missing_info
12
+ from .load_context import load_context_strict
13
+ from .validator import universal_validator
14
 
15
  __all__ = [
16
  "classify_topic",
 
24
  "food_suggestion",
25
  "policy",
26
  "select_food",
27
+ "ask_missing_info",
28
+ "load_context_strict",
29
+ "universal_validator",
30
  ]
chatbot/agents/nodes/chatbot/ask_info.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain_core.messages import AIMessage
2
+ from chatbot.agents.states.state import AgentState
3
+ from chatbot.knowledge.field_requirement import FIELD_NAMES_VN
4
+ import logging
5
+
6
+ # --- Cấu hình logging ---
7
+ logging.basicConfig(level=logging.INFO)
8
+ logger = logging.getLogger(__name__)
9
+
10
+ def ask_missing_info(state: AgentState):
11
+ logger.info("---NODE: ASK MISSING INFO---")
12
+
13
+ missing_fields = state.get("missing_fields", [])
14
+ topic = state.get("topic", "")
15
+
16
+ # 1. Chuyển tên trường kỹ thuật sang tiếng Việt
17
+ missing_vn = [FIELD_NAMES_VN.get(f, f) for f in missing_fields]
18
+ missing_str = ", ".join(missing_vn)
19
+
20
+ # 2. Tạo câu hỏi dựa trên ngữ cảnh
21
+ msg = ""
22
+
23
+ if topic == "meal_suggestion":
24
+ # Với gợi ý món, ưu tiên hỏi Calo hoặc Số đo
25
+ msg = (
26
+ f"🥗 Để thiết kế thực đơn chuẩn cho bạn, mình cần bổ sung: **{missing_str}**.\n\n"
27
+ "📌 Bạn có thể cung cấp theo 1 trong 2 cách:\n"
28
+ "1) **Thông tin cơ thể** → *mình sẽ tự tính dinh dưỡng cho bạn*:\n"
29
+ " - ⚖️ Cân nặng (kg)\n"
30
+ " - 📏 Chiều cao (cm hoặc m)\n"
31
+ " - 🎂 Tuổi\n"
32
+ " - 🚹 Giới tính (Nam/Nữ)\n"
33
+ " - 🏃 Mức độ vận động (Ít / Trung bình / Nhiều)\n\n"
34
+ "2) **Mục tiêu dinh dưỡng cụ thể** → *nếu bạn đã biết trước*:\n"
35
+ " - 🔥 Kcal\n"
36
+ " - 💪 Protein (g)\n"
37
+ " - 🍳 Lipid/Fat (g)\n"
38
+ " - 🍚 Carbohydrate (g)\n\n"
39
+ "💡 *Bạn có thể nhập nhanh ví dụ:*\n"
40
+ "• \"Mình 60kg, cao 170cm, 22 tuổi, nam, vận động nhẹ\"\n"
41
+ "• \"1500 kcal — Protein 100g, Fat 50g, Carb 140g\""
42
+ )
43
+
44
+ else:
45
+ # Fallback chung
46
+ msg = f"Mình cần thêm thông tin về **{missing_str}** để xử lý yêu cầu này."
47
+
48
+ return {"messages": [AIMessage(content=msg)]}
chatbot/agents/nodes/chatbot/classify_topic.py CHANGED
@@ -1,8 +1,9 @@
1
- from langchain.prompts import PromptTemplate
2
- import json
3
  from pydantic import BaseModel, Field
4
  from chatbot.agents.states.state import AgentState
5
  from chatbot.models.llm_setup import llm
 
 
6
  import logging
7
 
8
  # --- Cấu hình logging ---
@@ -18,54 +19,45 @@ class Topic(BaseModel):
18
  )
19
 
20
  def classify_topic(state: AgentState):
21
- logger.info("---CLASSIFY TOPIC---")
22
- llm_with_structure_op = llm.with_structured_output(Topic)
23
-
24
- prompt = PromptTemplate(
25
- template="""
26
- Bạn là bộ phân loại chủ đề câu hỏi người dùng trong hệ thống chatbot dinh dưỡng.
27
-
28
- Nhiệm vụ:
29
- - Phân loại câu hỏi vào một trong các nhóm:
30
- 1. "meal_suggestion": khi người dùng yêu cầu gợi ý thực đơn cho cả một bữa ăn hoặc trong cả một ngày (chỉ cho bữa ăn, không cho món ăn đơn lẻ).
31
- 2. "food_suggestion": khi người dùng yêu cầu tìm kiếm hoặc gợi ý một món ăn duy nhất (có thể của một bữa nào đó).
32
- 3. "food_query": khi người dùng muốn tìm kiếm thông tin về một món ăn như tên, thành phần, dinh dưỡng, cách chế biến
33
- 4. "policy": khi người dùng muốn biết các thông tin liên quan đến app.
34
- 5. "general_chat": khi người dùng muốn hỏi đáp các câu hỏi chung liên quan đến sức khỏe, chất dinh dưỡng.
35
-
36
- Câu hỏi người dùng: {question}
37
-
38
- Hãy trả lời dưới dạng JSON phù hợp với schema sau:
39
- {format_instructions}
40
- """
41
- )
42
-
43
- messages = state["messages"]
44
- user_message = messages[-1].content if messages else state.question
45
-
46
- format_instructions = json.dumps(llm_with_structure_op.output_schema.model_json_schema(), ensure_ascii=False, indent=2)
47
-
48
- chain = prompt | llm_with_structure_op
49
-
50
- topic_result = chain.invoke({
51
- "question": user_message,
52
- "format_instructions": format_instructions
53
- })
54
-
55
- logger.info(f"Topic: {topic_result.name}")
56
-
57
- return {"topic": topic_result.name}
58
-
59
- def route_by_topic(state: AgentState):
60
- topic = state["topic"]
61
- if topic == "meal_suggestion":
62
- return "meal_identify"
63
- elif topic == "food_suggestion":
64
- return "food_suggestion"
65
- elif topic == "food_query":
66
- return "food_query"
67
- elif topic == "policy":
68
- return "policy"
69
- else:
70
- return "general_chat"
71
-
 
1
+ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
 
2
  from pydantic import BaseModel, Field
3
  from chatbot.agents.states.state import AgentState
4
  from chatbot.models.llm_setup import llm
5
+ from chatbot.utils.chat_history import get_chat_history
6
+ from chatbot.knowledge.field_requirement import TOPIC_REQUIREMENTS
7
  import logging
8
 
9
  # --- Cấu hình logging ---
 
19
  )
20
 
21
  def classify_topic(state: AgentState):
22
+ print("---CLASSIFY TOPIC ---")
23
+
24
+ classifier_llm = llm.with_structured_output(Topic)
25
+
26
+ system_msg = """
27
+ Bạn là bộ điều hướng thông minh.
28
+ Nhiệm vụ: Phân loại câu hỏi của người dùng vào nhóm thích hợp.
29
+
30
+ CÁC NHÓM CHỦ ĐỀ:
31
+ 1. "meal_suggestion": Gợi ý thực đơn ăn uống.
32
+ 2. "food_suggestion": Tìm món ăn cụ thể.
33
+ 3. "food_query": Hỏi thông tin dinh dưỡng món ăn.
34
+ 4. "policy": Khi người dùng hỏi về quy định, chính sách, hướng dẫn sử dụng MỚI mà chưa có trong lịch sử.
35
+ 5. "general_chat":
36
+ - Chào hỏi xã giao.
37
+ - Các câu hỏi sức khỏe chung chung.
38
+ - QUAN TRỌNG: Các câu hỏi NỐI TIẾP (Follow-up) yêu cầu giải thích, làm rõ thông tin ĐÃ CÓ trong lịch sử hội thoại (Ví dụ: "Giải thích ý đó", "Tại sao lại thế", "Cụ thể hơn đi").
39
+
40
+ NGUYÊN TẮC ƯU TIÊN:
41
+ - Nếu câu hỏi mơ hồ (VD: "ý thứ 2 là gì", "nó hoạt động sao"), hãy kiểm tra lịch sử.
42
+ - Nếu câu trả lời cho câu hỏi đó ĐÃ NẰM trong tin nhắn trước của AI -> Chọn "general_chat" (để AI tự trả lời bằng trí nhớ).
43
+ - Chỉ chọn các topic chuyên biệt (policy/food...) khi cần tra cứu dữ liệu MỚI bên ngoài.
44
+ """
45
+
46
+ prompt = ChatPromptTemplate.from_messages([
47
+ ("system", system_msg),
48
+ MessagesPlaceholder(variable_name="history"),
49
+ ])
50
+
51
+ chain = prompt | classifier_llm
52
+
53
+ recent_messages = get_chat_history(state["messages"], max_tokens=500)
54
+
55
+ try:
56
+ topic_result = chain.invoke({"history": recent_messages})
57
+ topic_name = topic_result.name
58
+ except Exception as e:
59
+ print(f"⚠️ Lỗi phân loại: {e}")
60
+ topic_name = "general_chat"
61
+
62
+ print(f"Topic detected: {topic_name}")
63
+ return {"topic": topic_name}
 
 
 
 
 
 
 
 
 
chatbot/agents/nodes/chatbot/food_query.py CHANGED
@@ -26,7 +26,7 @@ def food_query(state: AgentState):
26
  query_constructor=query_constructor,
27
  vectorstore=docsearch,
28
  structured_query_translator=ElasticsearchTranslator(),
29
- search_kwargs={"k": 3},
30
  )
31
  logger.info(f"🔍 Dạng truy vấn: {food_retriever.structured_query_translator.visit_structured_query(structured_query=query_ans)}")
32
 
 
26
  query_constructor=query_constructor,
27
  vectorstore=docsearch,
28
  structured_query_translator=ElasticsearchTranslator(),
29
+ search_kwargs={"k": 2},
30
  )
31
  logger.info(f"🔍 Dạng truy vấn: {food_retriever.structured_query_translator.visit_structured_query(structured_query=query_ans)}")
32
 
chatbot/agents/nodes/chatbot/food_suggestion.py CHANGED
@@ -1,5 +1,4 @@
1
  from chatbot.agents.states.state import AgentState
2
- from chatbot.utils.user_profile import get_user_by_id
3
  from chatbot.agents.tools.food_retriever import query_constructor, food_retriever
4
  import logging
5
 
@@ -14,7 +13,7 @@ def food_suggestion(state: AgentState):
14
  messages = state["messages"]
15
  user_message = messages[-1].content if messages else state.question
16
 
17
- user_profile = get_user_by_id(user_id)
18
 
19
  suggested_meals = []
20
 
 
1
  from chatbot.agents.states.state import AgentState
 
2
  from chatbot.agents.tools.food_retriever import query_constructor, food_retriever
3
  import logging
4
 
 
13
  messages = state["messages"]
14
  user_message = messages[-1].content if messages else state.question
15
 
16
+ user_profile = state.get("user_profile", {})
17
 
18
  suggested_meals = []
19
 
chatbot/agents/nodes/chatbot/general_chat.py CHANGED
@@ -1,7 +1,7 @@
1
  from chatbot.agents.states.state import AgentState
2
  from chatbot.models.llm_setup import llm
3
  from langchain.schema.messages import SystemMessage, HumanMessage
4
- from chatbot.utils.user_profile import get_user_by_id
5
  import logging
6
 
7
  # --- Cấu hình logging ---
@@ -11,35 +11,28 @@ logger = logging.getLogger(__name__)
11
  def general_chat(state: AgentState):
12
  logger.info("---GENERAL CHAT---")
13
 
14
- user_id = state.get("user_id", {})
15
- messages = state["messages"]
16
- user_message = messages[-1].content if messages else state.question
17
 
18
- user_profile = get_user_by_id(user_id)
19
-
20
- system_prompt = f"""
21
  Bạn là một chuyên gia dinh dưỡng và ẩm thực AI.
22
  Hãy trả lời các câu hỏi về:
23
  - món ăn, thành phần, dinh dưỡng, calo, protein, chất béo, carb,
24
  - chế độ ăn (ăn chay, keto, giảm cân, tăng cơ...),
25
  - sức khỏe, lối sống, chế độ tập luyện liên quan đến ăn uống.
26
- Một số thông tin về người dùng thể dùng đến như sau:
27
- - Tổng năng lượng mục tiêu: {user_profile['targetcalories']} kcal/ngày
28
- - Protein: {user_profile['protein']}g
29
- - Chất béo (lipid): {user_profile['totalfat']}g
30
- - Carbohydrate: {user_profile['carbohydrate']}g
31
- - Chế độ ăn: {user_profile['diet']}
32
- Không trả lời các câu hỏi ngoài chủ đề này.
33
- Giải thích ngắn gọn, tự nhiên, rõ ràng.
34
- """
35
-
36
- messages = [
37
- SystemMessage(content=system_prompt),
38
- HumanMessage(content=user_message),
39
- ]
40
 
41
- response = llm.invoke(messages)
42
-
43
- logger.info(response.content if hasattr(response, "content") else response)
 
 
44
 
45
- return {"response": response.content}
 
 
 
 
 
 
 
 
 
1
  from chatbot.agents.states.state import AgentState
2
  from chatbot.models.llm_setup import llm
3
  from langchain.schema.messages import SystemMessage, HumanMessage
4
+ from chatbot.utils.chat_history import get_chat_history
5
  import logging
6
 
7
  # --- Cấu hình logging ---
 
11
  def general_chat(state: AgentState):
12
  logger.info("---GENERAL CHAT---")
13
 
14
+ history = get_chat_history(state["messages"], max_tokens=1000)
 
 
15
 
16
+ system_text = """
 
 
17
  Bạn là một chuyên gia dinh dưỡng và ẩm thực AI.
18
  Hãy trả lời các câu hỏi về:
19
  - món ăn, thành phần, dinh dưỡng, calo, protein, chất béo, carb,
20
  - chế độ ăn (ăn chay, keto, giảm cân, tăng cơ...),
21
  - sức khỏe, lối sống, chế độ tập luyện liên quan đến ăn uống.
22
+ - chức năng, điều khoản, chính sách của ứng dụng.
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
+ Quy tắc:
25
+ - Không trả lời các câu hỏi ngoài chủ đề này (hãy từ chối lịch sự).
26
+ - Giải thích ngắn gọn, tự nhiên, rõ ràng.
27
+ - Dựa vào lịch sử trò chuyện để trả lời mạch lạc nếu có câu hỏi nối tiếp.
28
+ """
29
 
30
+ messages_to_send = [SystemMessage(content=system_text)] + history
31
+
32
+ try:
33
+ response = llm.invoke(messages_to_send)
34
+ logger.info(f"🤖 AI Response: {response.content}")
35
+ return {"messages": [response]}
36
+ except Exception as e:
37
+ logger.info(f"⚠️ Lỗi General Chat: {e}")
38
+ return {"messages": []}
chatbot/agents/nodes/chatbot/generate_final_response.py CHANGED
@@ -1,5 +1,5 @@
 
1
  from chatbot.agents.states.state import AgentState
2
- from chatbot.models.llm_setup import llm
3
  import logging
4
 
5
  # --- Cấu hình logging ---
@@ -8,32 +8,39 @@ logger = logging.getLogger(__name__)
8
 
9
  def generate_final_response(state: AgentState):
10
  logger.info("---NODE: FINAL RESPONSE---")
11
- menu = state["response"]["final_menu"]
12
- profile = state["response"]["user_profile"]
13
-
14
- # Format text để LLM đọc
15
- menu_text = ""
16
- current_meal = ""
17
- for dish in sorted(menu, key=lambda x: x['assigned_meal']): # Sort theo bữa
18
- if dish['assigned_meal'] != current_meal:
19
- current_meal = dish['assigned_meal']
20
- menu_text += f"\n--- BỮA {current_meal.upper()} ---\n"
21
-
22
- menu_text += (
23
- f"- {dish['name']} (x{dish['portion_scale']} suất): "
24
- f"{dish['final_kcal']}kcal, {dish['final_protein']}g Protein, {dish['final_lipid']}g Lipid, {dish['final_carb']}g Carbohydrate\n"
25
- )
26
-
27
- prompt = f"""
28
- Người dùng có mục tiêu: {profile['targetcalories']} Kcal, {profile['protein']}g Protein, {profile['totalfat']}g Lipid, {profile['carbohydrate']}g Carbohydrate.
29
- Hệ thống đã tính toán thực đơn tối ưu sau:
30
-
31
- {menu_text}
32
-
33
- Nhiệm vụ:
34
- 1. Trình bày thực đơn này thật đẹp và ngon miệng cho người dùng.
35
- 2. Giải thích ngắn gọn tại sao khẩu phần lại như vậy (Ví dụ: "Mình đã tăng lượng ức gà lên 1.5 suất để đảm bảo đủ Protein cho bạn").
36
- """
37
-
38
- res = llm.invoke(prompt)
39
- return {"response": res.content}
 
 
 
 
 
 
 
 
1
+ from langchain_core.messages import AIMessage
2
  from chatbot.agents.states.state import AgentState
 
3
  import logging
4
 
5
  # --- Cấu hình logging ---
 
8
 
9
  def generate_final_response(state: AgentState):
10
  logger.info("---NODE: FINAL RESPONSE---")
11
+ menu = state.get("final_menu", [])
12
+ reason = state.get("reason", "")
13
+ profile = state.get("user_profile", {})
14
+
15
+ if not menu:
16
+ return {"messages": [AIMessage(content="Xin lỗi, tôi chưa thể tạo thực đơn lúc này.")]}
17
+
18
+ meal_priority = {"sáng": 1, "trưa": 2, "tối": 3}
19
+ sorted_menu = sorted(
20
+ menu,
21
+ key=lambda x: meal_priority.get(x.get('assigned_meal', '').lower(), 99)
22
+ )
23
+
24
+ output_text = "📋 **THỰC ĐƠN DINH DƯỠNG NHÂN HÓA**\n"
25
+ output_text += f"🎯 Mục tiêu: {int(profile.get('targetcalories', 0))} Kcal | {int(profile.get('protein', 0))}g Protein\n\n"
26
+
27
+ current_meal = None
28
+
29
+ for dish in sorted_menu:
30
+ meal_name = dish.get('assigned_meal', 'Khác').upper()
31
+
32
+ if meal_name != current_meal:
33
+ current_meal = meal_name
34
+ output_text += f"🍽️ **BỮA {current_meal}**:\n"
35
+
36
+ scale = dish.get('portion_scale', 1.0)
37
+ scale_info = f" (x{scale} suất)" if scale != 1.0 else ""
38
+
39
+ output_text += f" **{dish['name']}**{scale_info}\n"
40
+ output_text += f" └─ {dish['final_kcal']} Kcal | {dish['final_protein']}g Đạm | {dish['final_totalfat']}g Béo | {dish['final_carbs']}g Bột\n"
41
+
42
+ if reason:
43
+ output_text += f"\n💡 **Góc nhìn chuyên gia:**\n{reason}"
44
+
45
+ return {"messages": [AIMessage(content=output_text)]}
46
+
chatbot/agents/nodes/chatbot/load_context.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ from pydantic import BaseModel, Field
3
+ from chatbot.agents.states.state import AgentState
4
+ from chatbot.models.llm_setup import llm
5
+ from typing import Literal, List, Optional
6
+ from langchain_core.messages import SystemMessage
7
+ from chatbot.utils.chat_history import get_chat_history
8
+ from chatbot.utils.restriction import get_restrictions
9
+ from chatbot.utils.user_profile import get_user_by_id
10
+ import logging
11
+
12
+ # --- Cấu hình logging ---
13
+ logging.basicConfig(level=logging.INFO)
14
+ logger = logging.getLogger(__name__)
15
+
16
+ DiseaseType = Literal[
17
+ "Khỏe mạnh",
18
+ "Suy thận",
19
+ "Xơ gan, Viêm gan",
20
+ "Gout",
21
+ "Sỏi thận",
22
+ "Suy dinh dưỡng",
23
+ "Bỏng nặng",
24
+ "Thiếu máu thiếu sắt",
25
+ "Bệnh tim mạch",
26
+ "Tiểu đường",
27
+ "Loãng xương",
28
+ "Phụ nữ mang thai",
29
+ "Viêm loét, trào ngược dạ dày",
30
+ "Hội chứng ruột kích thích",
31
+ "Viêm khớp",
32
+ "Tăng huyết áp"
33
+ ]
34
+
35
+ class MacroGoals(BaseModel):
36
+ targetcalories: float = Field(..., description="Tổng calo mục tiêu (TDEE +/- goal)")
37
+ protein: float = Field(..., description="Protein (gram)")
38
+ totalfat: float = Field(..., description="Lipid/Fat (gram)")
39
+ carbohydrate: float = Field(..., description="Tinh bột (gram)")
40
+ heathStatus: DiseaseType = Field(..., description="Tình trạng sức khỏe")
41
+ diet: str = Field(..., description="Chế độ ăn")
42
+
43
+ class ContextDecision(BaseModel):
44
+ user_provided_info: bool = Field(description="True nếu user đề cập đến cân nặng, chiều cao, tuổi, hoặc mục tiêu ăn uống. False nếu user chỉ chào hỏi hoặc yêu cầu chung chung.")
45
+
46
+ # Nếu user_provided_info = True:
47
+ calculated_goals: Optional[MacroGoals] = Field(None, description="Kết quả tính toán NẾU đủ thông tin.")
48
+ missing_info: List[str] = Field(default=[], description="Danh sách các thông tin còn thiếu để tính TDEE (VD: ['height', 'age']). Nếu đủ thì để trống.")
49
+
50
+ reasoning: str = Field(description="Giải thích ngắn gọn tại sao đủ hoặc thiếu.")
51
+
52
+ def load_context_strict(state: AgentState):
53
+ logger.info("---NODE: STRICT CONTEXT & CALCULATOR---")
54
+
55
+ history = get_chat_history(state["messages"], max_tokens=1000)
56
+
57
+ user_id = state.get("user_id", 1)
58
+
59
+ system_prompt = """
60
+ Bạn là Chuyên gia Dinh dưỡng AI.
61
+ Nhiệm vụ: Phân tích hội thoại và xác định ngữ cảnh dữ liệu.
62
+
63
+ LOGIC XỬ LÝ:
64
+ 1. Kiểm tra xem người dùng có đang cung cấp thông tin cá nhân (Cân nặng, Chiều cao, Tuổi, Giới tính, Mục tiêu) hoặc yêu cầu Calo cụ thể không?
65
+
66
+ 2. TRƯỜNG HỢP A: Người dùng KHÔNG cung cấp thông tin gì mới liên quan đến chỉ số cơ thể (chỉ hỏi "Gợi ý món ăn mặn"), cung cấp thông tin dinh dưỡng món ăn cũng vào trường hợp này (ví dụ "Gợi ý món ăn 400kcal).
67
+ -> Trả về: user_provided_info = False. (Hệ thống sẽ tự dùng DB).
68
+
69
+ 3. TRƯỜNG HỢP B: Người dùng CÓ cung cấp thông tin (dù chỉ là 1 phần).
70
+ -> Trả về: user_provided_info = True.
71
+ -> Kiểm tra xem thông tin đã ĐỦ để tính TDEE chưa? (Cần đầy đủ (Weight, Height, Age, Gender, Activity) hoặc (Kcal, Protein, Lipid, Carbohydrate))
72
+ -> NẾU THIẾU: Liệt kê các trường thiếu vào 'missing_info'.
73
+ -> NẾU ĐỦ (hoặc user cho sẵn Target Kcal):
74
+ - Hãy TÍNH TOÁN ngay lập tức 4 chỉ số: Kcal, Protein, Lipid, Carbohydrate.
75
+ - Sử dụng công thức Mifflin-St Jeor cho BMR.
76
+ - Phân bổ Macro theo chế độ ăn user mong muốn (hoặc mặc định 30P/30F/40C).
77
+ - Trả về kết quả trong 'calculated_goals'.
78
+ """
79
+
80
+ try:
81
+ chain = llm.with_structured_output(ContextDecision)
82
+ input_messages = [SystemMessage(content=system_prompt)] + history
83
+ decision = chain.invoke(input_messages)
84
+
85
+ logger.info(f" 🤖 Decision: User Provided Info = {decision.user_provided_info}")
86
+ logger.info(f" 📝 Missing Info: {decision.missing_info}")
87
+ logger.info(f" 📝 Reasoning: {decision.reasoning}")
88
+
89
+ except Exception as e:
90
+ logger.info(f"⚠️ Lỗi LLM: {e}")
91
+ return {"missing_fields": ["system_error"]}
92
+
93
+ final_nutrition_goals = {}
94
+ missing_fields = []
95
+
96
+ if not decision.user_provided_info:
97
+ logger.info(" 💾 Dùng Profile Database.")
98
+ nutrition_goals = get_user_by_id(user_id)
99
+ restrictions = get_restrictions(nutrition_goals["healthStatus"])
100
+ final_nutrition_goals = {**nutrition_goals, **restrictions}
101
+
102
+ else:
103
+ logger.info(" 🚀 Dùng Profile Tạm thời (Session).")
104
+ if decision.missing_info:
105
+ logger.info(f" ⛔ Còn thiếu: {decision.missing_info}")
106
+ missing_fields = decision.missing_info
107
+ elif decision.calculated_goals:
108
+ goals = decision.calculated_goals
109
+ logger.info(f" ✅ Đã tính xong: {goals.targetcalories} Kcal")
110
+
111
+ nutrition_goals = {
112
+ "targetcalories": goals.targetcalories,
113
+ "protein": goals.protein,
114
+ "totalfat": goals.totalfat,
115
+ "carbohydrate": goals.carbohydrate,
116
+ "healthStatus": goals.heathStatus,
117
+ "diet": goals.diet
118
+ }
119
+ restrictions = get_restrictions(nutrition_goals["healthStatus"])
120
+ final_nutrition_goals = {**nutrition_goals, **restrictions}
121
+
122
+ return {
123
+ "user_profile": final_nutrition_goals,
124
+ "missing_fields": missing_fields
125
+ }
chatbot/agents/nodes/chatbot/meal_identify.py CHANGED
@@ -1,4 +1,4 @@
1
- from langchain.prompts import PromptTemplate
2
  import json
3
  from pydantic import BaseModel, Field
4
  from chatbot.agents.states.state import AgentState
@@ -17,39 +17,40 @@ class MealIntent(BaseModel):
17
 
18
  def meal_identify(state: AgentState):
19
  logger.info("---MEAL IDENTIFY---")
20
-
21
- llm_with_structure_op = llm.with_structured_output(MealIntent)
22
-
23
- # Lấy câu hỏi mới nhất từ lịch sử hội thoại
24
  messages = state["messages"]
25
  user_message = messages[-1].content if messages else state.question
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
- format_instructions = json.dumps(llm_with_structure_op.output_schema.model_json_schema(), ensure_ascii=False, indent=2)
28
-
29
- prompt = PromptTemplate(
30
- template="""
31
- Bạn là bộ phân tích yêu cầu gợi ý bữa ăn trong hệ thống chatbot dinh dưỡng.
32
-
33
- Dựa trên câu hỏi của người dùng, hãy xác định danh sách các bữa người dùng muốn gợi ý.
34
-
35
- - Các bữa người dùng có thể muốn gợi ý gồm: ["sáng", "trưa", "tối"].
36
-
37
- Câu hỏi người dùng: {question}
38
-
39
- Hãy xuất kết quả dưới dạng JSON theo schema sau:
40
- {format_instructions}
41
- """
42
- )
43
 
44
- chain = prompt | llm_with_structure_op
 
 
 
 
45
 
46
- result = chain.invoke({
47
- "question": user_message,
48
- "format_instructions": format_instructions
49
- })
50
 
51
- logger.info("Bữa cần gợi ý: " + ", ".join(result.meals_to_generate))
52
 
53
  return {
54
- "meals_to_generate": result.meals_to_generate,
55
  }
 
1
+ from langchain.prompts import ChatPromptTemplate
2
  import json
3
  from pydantic import BaseModel, Field
4
  from chatbot.agents.states.state import AgentState
 
17
 
18
  def meal_identify(state: AgentState):
19
  logger.info("---MEAL IDENTIFY---")
 
 
 
 
20
  messages = state["messages"]
21
  user_message = messages[-1].content if messages else state.question
22
+
23
+ structured_llm = llm.with_structured_output(MealIntent)
24
+
25
+ system = """
26
+ Bạn là chuyên gia phân tích yêu cầu dinh dưỡng.
27
+ Nhiệm vụ: Đọc câu hỏi người dùng và trích xuất danh sách các bữa ăn họ muốn gợi ý.
28
+ Chỉ được chọn trong các giá trị: "sáng", "trưa", "tối".
29
+ Nếu người dùng nói "cả ngày", hãy trả về ["sáng", "trưa", "tối"].
30
+ """
31
+
32
+ prompt = ChatPromptTemplate.from_messages([
33
+ ("system", system),
34
+ ("human", "{question}"),
35
+ ])
36
+
37
+ chain = prompt | structured_llm
38
 
39
+ try:
40
+ result = chain.invoke({"question": user_message})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
 
42
+ if not result:
43
+ logger.info("⚠️ Model không trả về định dạng đúng, dùng mặc định.")
44
+ meals = ["sáng", "trưa", "tối"]
45
+ else:
46
+ meals = result.meals_to_generate
47
 
48
+ except Exception as e:
49
+ logger.info(f"⚠️ Lỗi Parse JSON: {e}")
50
+ meals = ["sáng", "trưa", "tối"]
 
51
 
52
+ logger.info("Bữa cần gợi ý: " + ", ".join(meals))
53
 
54
  return {
55
+ "meals_to_generate": meals
56
  }
chatbot/agents/nodes/chatbot/policy.py CHANGED
@@ -1,6 +1,7 @@
1
  from chatbot.agents.states.state import AgentState
 
2
  from chatbot.models.llm_setup import llm
3
- from chatbot.agents.tools.info_app_retriever import policy_search
4
  import logging
5
 
6
  # --- Cấu hình logging ---
@@ -12,35 +13,40 @@ def policy(state: AgentState):
12
  messages = state["messages"]
13
  question = messages[-1].content if messages else state.question
14
 
15
- if not question:
16
- return {"response": "Chưa có câu hỏi."}
17
 
18
- # Tạo retriever, lấy 3 doc gần nhất
19
- policy_retriever = policy_search.as_retriever(search_kwargs={"k": 3})
20
 
21
- # Lấy các document liên quan
22
- docs = policy_retriever.invoke(question)
23
 
24
- if not docs:
25
- return {"response": "Không tìm thấy thông tin phù hợp."}
 
26
 
27
- # Gom nội dung các doc lại
28
- context_text = "\n\n".join([doc.page_content for doc in docs])
29
 
30
- # Tạo prompt cho LLM
31
- prompt_text = f"""
32
- Bạn là trợ lý AI chuyên về chính sách và thông tin app.
33
 
34
- Thông tin tham khảo từ hệ thống:
35
  {context_text}
36
 
37
- Câu hỏi của người dùng: {question}
 
 
 
 
38
 
39
- Hãy trả lời ngắn gọn, dễ hiểu, chính xác dựa trên thông tin có trong hệ thống.
40
- """
 
 
 
41
 
42
- # Gọi LLM
43
- result = llm.invoke(prompt_text)
44
- answer = result.content
45
 
46
- return {"response": answer}
 
 
1
  from chatbot.agents.states.state import AgentState
2
+ from langchain_core.messages import AIMessage, SystemMessage, HumanMessage
3
  from chatbot.models.llm_setup import llm
4
+ from chatbot.agents.tools.info_app_retriever import policy_retriever
5
  import logging
6
 
7
  # --- Cấu hình logging ---
 
13
  messages = state["messages"]
14
  question = messages[-1].content if messages else state.question
15
 
16
+ try:
17
+ docs = policy_retriever.invoke(question)
18
 
19
+ if not docs:
20
+ return {"messages": [AIMessage(content="Xin lỗi, tôi không tìm thấy thông tin chính sách liên quan đến câu hỏi của bạn trong hệ thống.")]}
21
 
22
+ context_text = "\n\n".join([d.page_content for d in docs])
 
23
 
24
+ except Exception as e:
25
+ logger.info(f"⚠️ Lỗi Policy Retriever: {e}")
26
+ return {"messages": [AIMessage(content="Hệ thống tra cứu chính sách đang gặp sự cố.")]}
27
 
28
+ system_prompt = f"""
29
+ Bạn Trợ AI hỗ trợ Chính sách & Quy định của Ứng dụng.
30
 
31
+ NHIỆM VỤ:
32
+ Trả lời câu hỏi người dùng CHỈ DỰA TRÊN thông tin được cung cấp dưới đây.
 
33
 
34
+ THÔNG TIN THAM KHẢO:
35
  {context_text}
36
 
37
+ QUY TẮC AN TOÀN:
38
+ 1. Nếu thông tin không có trong phần tham khảo, hãy trả lời: "Xin lỗi, hiện tại trong tài liệu chính sách không đề cập đến vấn đề này."
39
+ 2. Không được tự bịa ra chính sách hoặc đoán mò.
40
+ 3. Trả lời ngắn gọn, đi thẳng vào vấn đề.
41
+ """
42
 
43
+ try:
44
+ response = llm.invoke([
45
+ SystemMessage(content=system_prompt),
46
+ HumanMessage(content=question)
47
+ ])
48
 
49
+ return {"messages": [response]}
 
 
50
 
51
+ except Exception as e:
52
+ return {"messages": [AIMessage(content="Lỗi khi tạo câu trả lời.")]}
chatbot/agents/nodes/chatbot/select_food.py CHANGED
@@ -1,5 +1,6 @@
1
  from chatbot.agents.states.state import AgentState
2
  from chatbot.models.llm_setup import llm
 
3
  import logging
4
 
5
  # --- Cấu hình logging ---
@@ -14,27 +15,25 @@ def select_food(state: AgentState):
14
  messages = state.get("messages", [])
15
  user_message = messages[-1].content if messages else state.get("question", "")
16
 
17
- # 1. Format dữ liệu món ăn để đưa vào Prompt
18
  if not suggested_meals:
19
  return {"response": "Xin lỗi, tôi không tìm thấy món ăn nào phù hợp trong cơ sở dữ liệu."}
20
 
21
  meals_context = ""
22
  for i, doc in enumerate(suggested_meals):
23
  meta = doc.metadata
 
24
  meals_context += (
25
- f"Món {i+1}: {meta.get('name', 'Không tên')}\n"
26
- f" - Dinh dưỡng: {meta.get('kcal', '?')} kcal | "
27
- f"P: {meta.get('protein', '?')}g | L: {meta.get('lipid', '?')}g | C: {meta.get('carbohydrate', '?')}g\n"
28
- f" - tả/Thành phần: {doc.page_content}...\n"
 
29
  )
30
 
31
  # 2. Prompt Trả lời câu hỏi
32
- # Prompt này linh hoạt hơn: Không ép chọn 1 món nếu user hỏi dạng liệt kê ("Tìm các món gà...")
33
  system_prompt = f"""
34
  Bạn là Trợ lý Dinh dưỡng AI thông minh.
35
 
36
- CÂU HỎI: "{user_message}"
37
-
38
  DỮ LIỆU TÌM ĐƯỢC TỪ KHO MÓN ĂN:
39
  {meals_context}
40
 
@@ -46,11 +45,16 @@ def select_food(state: AgentState):
46
  Lưu ý: Chỉ sử dụng thông tin từ danh sách cung cấp, không bịa đặt số liệu.
47
  """
48
 
49
- # Gọi LLM
50
- response = llm.invoke(system_prompt)
51
- content = response.content if hasattr(response, "content") else response
 
 
 
 
52
 
53
- print("💬 AI Response:")
54
- print(content)
55
 
56
- return {"response": content}
 
 
 
1
  from chatbot.agents.states.state import AgentState
2
  from chatbot.models.llm_setup import llm
3
+ from langchain_core.messages import AIMessage, SystemMessage, HumanMessage
4
  import logging
5
 
6
  # --- Cấu hình logging ---
 
15
  messages = state.get("messages", [])
16
  user_message = messages[-1].content if messages else state.get("question", "")
17
 
 
18
  if not suggested_meals:
19
  return {"response": "Xin lỗi, tôi không tìm thấy món ăn nào phù hợp trong cơ sở dữ liệu."}
20
 
21
  meals_context = ""
22
  for i, doc in enumerate(suggested_meals):
23
  meta = doc.metadata
24
+ # Format kỹ hơn để LLM dễ đọc
25
  meals_context += (
26
+ f"--- Món {i+1} ---\n"
27
+ f"Tên: {meta.get('name', 'Không tên')}\n"
28
+ f"Dinh dưỡng (1 suất): {meta.get('kcal', '?')} kcal | "
29
+ f"Đạm: {meta.get('protein', '?')}g | Béo: {meta.get('totalfat', '?')}g | Carb: {meta.get('carbs', '?')}g\n"
30
+ f"Mô tả: {doc.page_content}\n\n"
31
  )
32
 
33
  # 2. Prompt Trả lời câu hỏi
 
34
  system_prompt = f"""
35
  Bạn là Trợ lý Dinh dưỡng AI thông minh.
36
 
 
 
37
  DỮ LIỆU TÌM ĐƯỢC TỪ KHO MÓN ĂN:
38
  {meals_context}
39
 
 
45
  Lưu ý: Chỉ sử dụng thông tin từ danh sách cung cấp, không bịa đặt số liệu.
46
  """
47
 
48
+ try:
49
+ response = llm.invoke([
50
+ SystemMessage(content=system_prompt),
51
+ HumanMessage(content=user_message)
52
+ ])
53
+
54
+ logger.info("💬 AI Response:", response.content)
55
 
56
+ return {"messages": [response]}
 
57
 
58
+ except Exception as e:
59
+ logger.info(f"⚠️ Lỗi sinh câu trả lời: {e}")
60
+ return {"messages": [AIMessage(content="Đã xảy ra lỗi khi phân tích dữ liệu món ăn.")]}
chatbot/agents/nodes/chatbot/select_food_plan.py CHANGED
@@ -1,4 +1,5 @@
1
  from chatbot.agents.states.state import AgentState
 
2
  from chatbot.models.llm_setup import llm
3
  import logging
4
 
@@ -9,47 +10,53 @@ logger = logging.getLogger(__name__)
9
  def select_food_plan(state: AgentState):
10
  logger.info("---SELECT FOOD PLAN---")
11
 
12
- user_profile = state["user_profile"]
13
- suggested_meals = state["suggested_meals"]
14
- messages = state["messages"]
15
- user_message = messages[-1].content if messages else state.question
 
 
 
 
 
 
16
 
17
  suggested_meals_text = "\n".join(
18
- f"{i+1}. {doc.metadata.get('name', 'Không rõ')} - "
19
- f"{doc.metadata.get('kcal', '?')} kcal, "
20
- f"Protein:{doc.metadata.get('protein', '?')}g, "
21
- f"Chất béo:{doc.metadata.get('lipid', '?')}g, "
22
- f"Carbohydrate:{doc.metadata.get('carbohydrate', '?')}g"
23
  for i, doc in enumerate(suggested_meals)
24
  )
25
 
26
- prompt = f"""
27
- Bạn là chuyên gia dinh dưỡng AI.
28
- Bạn có thể sử dụng thông tin người dùng có hồ sơ dinh dưỡng sau nếu cần thiết cho câu hỏi của người dùng:
29
- - Tổng năng lượng mục tiêu: {user_profile['targetcalories']} kcal/ngày
30
- - Protein: {user_profile['protein']}g
31
- - Chất béo (lipid): {user_profile['totalfat']}g
32
- - Carbohydrate: {user_profile['carbohydrate']}g
33
- - Chế độ ăn: {user_profile['diet']}
34
-
35
- Câu hỏi của người dùng: "{user_message}"
36
-
37
- Danh sách món ăn hiện có để chọn:
38
- {suggested_meals_text}
39
-
40
- Yêu cầu:
41
- 1. Chọn một món ăn phù hợp nhất với yêu cầu của người dùng, dựa trên dinh dưỡng và chế độ ăn.
42
- 2. Nếu không có món nào phù hợp, hãy trả về:
43
- "Không tìm thấy món phù hợp trong danh sách hiện có."
44
- 3. Không tự tạo thêm món mới hoặc tên món không có trong danh sách.
45
- 4. Nếu có nhiều món gần giống nhau, hãy chọn món có năng lượng và thành phần dinh dưỡng gần nhất với mục tiêu người dùng.
46
- """
47
 
48
- logger.info("Prompt:")
49
- logger.info(prompt)
 
 
50
 
51
- result = llm.invoke(prompt)
 
52
 
53
- logger.info(result.content if hasattr(result, "content") else result)
 
54
 
55
- return {"response": result.content}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from chatbot.agents.states.state import AgentState
2
+ from langchain_core.messages import AIMessage, SystemMessage, HumanMessage
3
  from chatbot.models.llm_setup import llm
4
  import logging
5
 
 
10
  def select_food_plan(state: AgentState):
11
  logger.info("---SELECT FOOD PLAN---")
12
 
13
+ user_profile = state.get("user_profile", {})
14
+ suggested_meals = state.get("suggested_meals", [])
15
+ messages = state.get("messages", [])
16
+ user_message = messages[-1].content if messages else state.get("question", "")
17
+
18
+ if not suggested_meals:
19
+ return {
20
+ "messages": [AIMessage(content="Xin lỗi, dựa trên tiêu chí của bạn, tôi không tìm thấy món ăn nào phù hợp trong dữ liệu.")]
21
+ }
22
+
23
 
24
  suggested_meals_text = "\n".join(
25
+ f"Món {i+1}: {doc.metadata.get('name', 'Không rõ')}\n"
26
+ f" - Dinh dưỡng: {doc.metadata.get('kcal', '?')} kcal | "
27
+ f"P: {doc.metadata.get('protein', '?')}g | L: {doc.metadata.get('totalfat', '?')}g | C: {doc.metadata.get('carbs', '?')}g\n"
 
 
28
  for i, doc in enumerate(suggested_meals)
29
  )
30
 
31
+ system_prompt = f"""
32
+ Bạn là chuyên gia dinh dưỡng AI.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
+ HỒ SƠ NGƯỜI DÙNG:
35
+ - Mục tiêu: {user_profile.get('targetcalories', 'N/A')} kcal/ngày
36
+ - Macro (P/F/C): {user_profile.get('protein', '?')}g / {user_profile.get('totalfat', '?')}g / {user_profile.get('carbohydrate', '?')}g
37
+ - Chế độ: {user_profile.get('diet', 'Cân bằng')}
38
 
39
+ CÂU HỎI:
40
+ {user_message}
41
 
42
+ DANH SÁCH ỨNG VIÊN TỪ DATABASE:
43
+ {suggested_meals_text}
44
 
45
+ NHIỆM VỤ:
46
+ 1. Dựa vào câu hỏi của người dùng, hãy chọn ra 2-3 món phù hợp nhất từ danh sách trên.
47
+ 2. Giải thích lý do chọn (dựa trên sự phù hợp về Calo/Macro hoặc khẩu vị).
48
+ 3. TUYỆT ĐỐI KHÔNG bịa ra món không có trong danh sách.
49
+ """
50
+
51
+ try:
52
+ response = llm.invoke([
53
+ SystemMessage(content=system_prompt),
54
+ HumanMessage(content=user_message)
55
+ ])
56
+
57
+ print("💬 AI Response:", response.content)
58
+ return {"messages": [response]}
59
+
60
+ except Exception as e:
61
+ print(f"⚠️ Lỗi sinh câu trả lời: {e}")
62
+ return {"messages": [AIMessage(content="Xin lỗi, đã có lỗi xảy ra khi xử lý thông tin món ăn.")]}
chatbot/agents/nodes/chatbot/suggest_meal_node.py CHANGED
@@ -1,6 +1,4 @@
1
- from langchain_core.messages import HumanMessage, SystemMessage, AIMessage, ToolMessage
2
  from chatbot.agents.states.state import AgentState
3
- from chatbot.models.llm_setup import llm
4
  from chatbot.agents.tools.daily_meal_suggestion import daily_meal_suggestion
5
  import logging
6
 
@@ -11,67 +9,34 @@ logger = logging.getLogger(__name__)
11
  def suggest_meal_node(state: AgentState):
12
  logger.info("---SUGGEST MEAL NODE---")
13
 
14
- # 🧠 Lấy dữ liệu từ state
15
- user_id = state.get("user_id", 0)
16
- question = state.get("messages")
17
  meals_to_generate = state.get("meals_to_generate", [])
18
-
19
- # 🧩 Chuẩn bị prompt mô tả yêu cầu
20
- system_prompt = """
21
- Bạn là một chuyên gia gợi ý thực đơn AI.
22
- Bạn không được tự trả lời hay đặt câu hỏi thêm.
23
- Nếu người dùng yêu cầu gợi ý món ăn, bắt buộc gọi tool 'daily_meal_suggestion'.
24
- với các tham số:
25
- - user_id: ID người dùng hiện tại
26
- - question: nội dung câu hỏi họ vừa hỏi
27
- - meals_to_generate: danh sách các bữa cần sinh thực đơn (nếu có)
28
-
29
- Nếu bạn không chắc bữa nào cần sinh, vẫn gọi tool này — phần xử lý sẽ lo chi tiết sau.
30
- """
31
-
32
- user_prompt = f"""
33
- Người dùng có ID: {user_id}
34
- Yêu cầu: "{question}"
35
- Danh sách các bữa cần gợi ý: {meals_to_generate}
36
- """
37
-
38
- # 🚀 Gọi LLM và Tools
39
- tools = [daily_meal_suggestion]
40
- llm_with_tools = llm.bind_tools(tools)
41
 
42
- response = llm_with_tools.invoke(
43
- [
44
- SystemMessage(content=system_prompt),
45
- HumanMessage(content=user_prompt)
46
- ]
47
- )
48
-
49
- logger.info("===== DEBUG =====")
50
- logger.info(f"Response type: {type(response)}")
51
- logger.info(f"Tool calls: {getattr(response, 'tool_calls', None)}")
52
- logger.info(f"Message content: {response.content}")
53
- logger.info("=================")
54
-
55
- if isinstance(response, AIMessage) and response.tool_calls:
56
- tool_call = response.tool_calls[0]
57
- tool_name = tool_call["name"]
58
- tool_args = tool_call["args"]
59
- tool_call_id = tool_call["id"]
60
-
61
- logger.info(f"👉 Executing tool: {tool_name} with args: {tool_args}")
62
-
63
- # Bổ sung tham số nếu LLM quên
64
- tool_args.setdefault("user_id", user_id)
65
- tool_args.setdefault("question", question)
66
- tool_args.setdefault("meals_to_generate", meals_to_generate)
67
-
68
- if tool_name == "daily_meal_suggestion":
69
- result = daily_meal_suggestion.invoke(tool_args)
70
- elif tool_name == "fallback":
71
- result = {"message": "Không có tool phù hợp.", "reason": tool_args.get("reason", "")}
72
- else:
73
- result = {"message": f"Tool '{tool_name}' chưa được định nghĩa."}
74
-
75
- tool_message = ToolMessage(content=str(result), name=tool_name, tool_call_id=tool_call_id)
76
- return {"messages": state["messages"] + [response, tool_message], "response": result}
77
- return {"response": "Lỗi!!!"}
 
 
1
  from chatbot.agents.states.state import AgentState
 
2
  from chatbot.agents.tools.daily_meal_suggestion import daily_meal_suggestion
3
  import logging
4
 
 
9
  def suggest_meal_node(state: AgentState):
10
  logger.info("---SUGGEST MEAL NODE---")
11
 
12
+ user_id = state.get("user_id", 1)
13
+ user_profile = state.get("user_profile", {})
 
14
  meals_to_generate = state.get("meals_to_generate", [])
15
+ messages = state.get("messages", [])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
+ if messages:
18
+ question = messages[-1].content
19
+ else:
20
+ question = "Gợi ý thực đơn tiêu chuẩn"
21
+
22
+ tool_input = {
23
+ "user_id": user_id,
24
+ "user_profile": user_profile,
25
+ "question": question,
26
+ "meals_to_generate": meals_to_generate
27
+ }
28
+
29
+ logger.info(f"👉 Gọi Tool: daily_meal_suggestion")
30
+
31
+ try:
32
+ result = daily_meal_suggestion.invoke(tool_input)
33
+ return {
34
+ "final_menu": result.get("final_menu"),
35
+ "reason": result.get("reason"),
36
+ }
37
+ except Exception as e:
38
+ print(f"❌ Lỗi khi chạy tool: {e}")
39
+ return {
40
+ "final_menu": [],
41
+ "error": str(e)
42
+ }
 
 
 
 
 
 
 
 
 
 
chatbot/agents/nodes/chatbot/validator.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ from chatbot.agents.states.state import AgentState
3
+ from chatbot.knowledge.field_requirement import TOPIC_REQUIREMENTS
4
+ import logging
5
+
6
+ # --- Cấu hình logging ---
7
+ logging.basicConfig(level=logging.INFO)
8
+ logger = logging.getLogger(__name__)
9
+
10
+ def universal_validator(state: AgentState):
11
+ print("---NODE: UNIVERSAL VALIDATOR---")
12
+
13
+ # 1. Lấy dữ liệu
14
+ topic = state.get("topic", "general_chat")
15
+ goals = state.get("user_profile", {})
16
+ missing_from_prev = state.get("missing_fields", [])
17
+
18
+ # 2. Xác định yêu cầu của Topic hiện tại
19
+ required_fields = TOPIC_REQUIREMENTS.get(topic, [])
20
+
21
+ # 3. Logic Kiểm Tra
22
+ final_missing = []
23
+ # Trường hợp A: Nếu bước trước LLM đã báo thiếu
24
+ if missing_from_prev:
25
+ final_missing.extend(missing_from_prev)
26
+ # Trường hợp B: Kiểm tra lại các trường bắt buộc của topic
27
+ for field in required_fields:
28
+ value = goals.get(field)
29
+
30
+ if value is None:
31
+ final_missing.append(field)
32
+ elif isinstance(value, (int, float)) and value <= 0:
33
+ final_missing.append(field)
34
+ elif isinstance(value, str) and not value.strip():
35
+ final_missing.append(field)
36
+
37
+ final_missing = list(set(final_missing))
38
+
39
+ if final_missing:
40
+ logger.info(f" ⛔ Topic '{topic}' thiếu: {final_missing}")
41
+ return {"is_valid": False, "missing_fields": final_missing}
42
+
43
+ logger.info(f" ✅ Topic '{topic}' đủ thông tin. Pass.")
44
+ return {"is_valid": True, "missing_fields": []}
chatbot/agents/states/__pycache__/state.cpython-310.pyc CHANGED
Binary files a/chatbot/agents/states/__pycache__/state.cpython-310.pyc and b/chatbot/agents/states/__pycache__/state.cpython-310.pyc differ
 
chatbot/agents/states/state.py CHANGED
@@ -1,30 +1,30 @@
1
  from typing import Annotated, Optional, Literal, Sequence, TypedDict, List, Dict, Any
2
  from langgraph.graph.message import add_messages
 
3
 
4
  class AgentState(TypedDict):
5
- # ========== Thông tin cơ bản ==========
6
- user_id: Optional[str]
7
- question: str
8
 
9
- # ========== Ngữ cảnh hội thoại ==========
10
- topic: Optional[str]
11
- user_profile: Optional[Dict[str, Any]]
12
 
13
- # ========== Gợi ý & lựa chọn món ăn ==========
14
- meals_to_generate: Optional[List[str]]
15
- suggested_meals: Optional[List[Dict[str, Any]]]
16
 
17
- # ========== Kết quả & phản hồi ==========
18
- response: Optional[str]
19
- messages: Annotated[list, add_messages]
20
 
21
- # ========== Mục tiêu & truy vấn ==========
22
  candidate_pool: List[dict]
23
  selected_structure: List[dict]
24
- reason: Optional[str]
25
  final_menu: List[dict]
26
 
27
- food_old: Optional[Dict[str, Any]]
 
 
 
28
 
29
  class SwapState(TypedDict):
30
  user_profile: Dict[str, Any]
 
1
  from typing import Annotated, Optional, Literal, Sequence, TypedDict, List, Dict, Any
2
  from langgraph.graph.message import add_messages
3
+ from langchain_core.messages import AnyMessage
4
 
5
  class AgentState(TypedDict):
6
+ user_id: Optional[str] = None
7
+ question: str = ""
 
8
 
9
+ topic: Optional[str] = None
10
+ user_profile: Optional[Dict[str, Any]] = None
 
11
 
12
+ missing_fields: List[str]
13
+ is_valid: bool
14
+ nutrition_goals: dict
15
 
16
+ meals_to_generate: Optional[List[str]] = None
17
+ suggested_meals: Optional[List[Dict[str, Any]]] = None
 
18
 
 
19
  candidate_pool: List[dict]
20
  selected_structure: List[dict]
21
+ reason: Optional[str] = None
22
  final_menu: List[dict]
23
 
24
+ response: Optional[str] = None
25
+ messages: Annotated[List[AnyMessage], add_messages]
26
+
27
+ food_old: Optional[Dict[str, Any]] = None
28
 
29
  class SwapState(TypedDict):
30
  user_profile: Dict[str, Any]
chatbot/agents/tools/__pycache__/daily_meal_suggestion.cpython-310.pyc CHANGED
Binary files a/chatbot/agents/tools/__pycache__/daily_meal_suggestion.cpython-310.pyc and b/chatbot/agents/tools/__pycache__/daily_meal_suggestion.cpython-310.pyc differ
 
chatbot/agents/tools/__pycache__/food_retriever.cpython-310.pyc CHANGED
Binary files a/chatbot/agents/tools/__pycache__/food_retriever.cpython-310.pyc and b/chatbot/agents/tools/__pycache__/food_retriever.cpython-310.pyc differ
 
chatbot/agents/tools/daily_meal_suggestion.py CHANGED
@@ -14,7 +14,7 @@ logging.basicConfig(level=logging.INFO)
14
  logger = logging.getLogger(__name__)
15
 
16
  @tool("daily_meal_suggestion", return_direct=True)
17
- def daily_meal_suggestion(user_id: str, question: str, meals_to_generate: list):
18
  """
19
  Sinh thực đơn hàng ngày hoặc cho các bữa cụ thể dựa trên hồ sơ dinh dưỡng người dùng,
20
  câu hỏi và danh sách các bữa cần gợi ý.
@@ -24,19 +24,16 @@ def daily_meal_suggestion(user_id: str, question: str, meals_to_generate: list):
24
  question (str): Câu hỏi hoặc mong muốn cụ thể của người dùng
25
  (VD: "Tôi muốn gợi ý bữa trưa và bữa tối").
26
  meals_to_generate (list): Danh sách các bữa cần sinh thực đơn, ví dụ ["trưa", "tối"].
 
27
  """
28
 
29
- logger.info(f"Tool daily_meal_suggestion invoked: user_id={user_id}, meals={meals_to_generate}")
30
- if not isinstance(meals_to_generate, list):
31
- logger.warning("meals_to_generate không phải list, ép về list")
32
- meals_to_generate = list(meals_to_generate)
33
-
34
  workflow = meal_plan_graph()
35
 
36
  result = workflow.invoke({
37
  "user_id": user_id,
38
  "question": question,
39
  "meals_to_generate": meals_to_generate,
 
40
  })
41
 
42
  return result
 
14
  logger = logging.getLogger(__name__)
15
 
16
  @tool("daily_meal_suggestion", return_direct=True)
17
+ def daily_meal_suggestion(user_id: str, question: str, meals_to_generate: list, user_profile: dict):
18
  """
19
  Sinh thực đơn hàng ngày hoặc cho các bữa cụ thể dựa trên hồ sơ dinh dưỡng người dùng,
20
  câu hỏi và danh sách các bữa cần gợi ý.
 
24
  question (str): Câu hỏi hoặc mong muốn cụ thể của người dùng
25
  (VD: "Tôi muốn gợi ý bữa trưa và bữa tối").
26
  meals_to_generate (list): Danh sách các bữa cần sinh thực đơn, ví dụ ["trưa", "tối"].
27
+ user_profile (dict): Thông tin về nhu cầu dinh dưỡng của người dùng.
28
  """
29
 
 
 
 
 
 
30
  workflow = meal_plan_graph()
31
 
32
  result = workflow.invoke({
33
  "user_id": user_id,
34
  "question": question,
35
  "meals_to_generate": meals_to_generate,
36
+ "user_profile": user_profile
37
  })
38
 
39
  return result
chatbot/agents/tools/food_retriever.py CHANGED
@@ -6,157 +6,145 @@ from langchain.chains.query_constructor.base import (
6
  from langchain_deepseek import ChatDeepSeek
7
  from langchain_elasticsearch import ElasticsearchStore
8
  from langchain.retrievers.self_query.elasticsearch import ElasticsearchTranslator
 
9
  from langchain.retrievers.self_query.base import SelfQueryRetriever
10
 
11
  from chatbot.models.embeddings import embeddings
12
  from chatbot.models.llm_setup import llm
13
- from chatbot.config import ELASTIC_CLOUD_URL, ELASTIC_API_KEY
14
 
15
 
16
  # ========================================
17
  # 1️⃣ Định nghĩa metadata field info
18
  # ========================================
19
  metadata_field_info = [
20
-
21
- # Thông tin chung về món ăn
22
  AttributeInfo(
23
  name="meal_id",
24
- description="ID duy nhất của món ăn",
25
  type="integer"
26
  ),
27
  AttributeInfo(
28
  name="name",
29
- description="Tên món ăn",
30
- type="string"
31
- ),
32
- AttributeInfo(
33
- name="servings",
34
- description="Số khẩu phần ăn",
35
- type="integer"
36
- ),
37
- AttributeInfo(
38
- name="difficulty",
39
- description="Độ khó chế biến",
40
  type="string"
41
  ),
42
  AttributeInfo(
43
- name="cooking_time_minutes",
44
- description="Thời gian nấu (phút)",
45
- type="integer"
 
 
 
 
 
 
 
46
  ),
47
 
48
- # Nguyên liệu
49
  AttributeInfo(
50
  name="ingredients",
51
- description="Danh sách nguyên liệu (list string), dụ: ['cà rốt', 'rong biển', 'trứng gà']",
52
- type="string"
53
  ),
54
  AttributeInfo(
55
  name="ingredients_text",
56
- description="Nguyên liệu dạng chuỗi nối, dụ: 'cà rốt, rong biển, trứng gà'",
57
  type="string"
58
  ),
59
 
60
- # Năng lượng & chất đa lượng
61
  AttributeInfo(
62
  name="kcal",
63
- description="Năng lượng của món ăn (kcal)",
64
  type="float"
65
  ),
66
  AttributeInfo(
67
  name="protein",
68
- description="Hàm lượng protein (g)",
69
  type="float"
70
  ),
71
  AttributeInfo(
72
- name="carbohydrate",
73
- description="Hàm lượng carbohydrate (g)",
74
  type="float"
75
  ),
76
  AttributeInfo(
77
  name="sugar",
78
- description="Hàm lượng đường tổng (g)",
79
  type="float"
80
  ),
81
  AttributeInfo(
82
  name="fiber",
83
- description="Hàm lượng chất xơ (g)",
84
  type="float"
85
  ),
 
86
  AttributeInfo(
87
- name="lipid",
88
- description="Tổng chất béo (g)",
89
  type="float"
90
  ),
91
  AttributeInfo(
92
- name="saturated_fat",
93
  description="Chất béo bão hòa (g)",
94
  type="float"
95
  ),
96
  AttributeInfo(
97
- name="monounsaturated_fat",
98
  description="Chất béo không bão hòa đơn (g)",
99
  type="float"
100
  ),
101
  AttributeInfo(
102
- name="polyunsaturated_fat",
103
  description="Chất béo không bão hòa đa (g)",
104
  type="float"
105
  ),
106
  AttributeInfo(
107
- name="trans_fat",
108
  description="Chất béo chuyển hóa (g)",
109
  type="float"
110
  ),
111
  AttributeInfo(
112
  name="cholesterol",
113
- description="Hàm lượng cholesterol (mg)",
114
  type="float"
115
  ),
116
 
117
- # Vitamin
118
  AttributeInfo(
119
- name="vit_a",
120
  description="Vitamin A (mg)",
121
  type="float"
122
  ),
123
  AttributeInfo(
124
- name="vit_d",
125
  description="Vitamin D (mg)",
126
  type="float"
127
  ),
128
  AttributeInfo(
129
- name="vit_c",
130
  description="Vitamin C (mg)",
131
  type="float"
132
  ),
133
  AttributeInfo(
134
- name="vit_b6",
135
  description="Vitamin B6 (mg)",
136
  type="float"
137
  ),
138
  AttributeInfo(
139
- name="vit_b12",
140
  description="Vitamin B12 (mg)",
141
  type="float"
142
  ),
143
  AttributeInfo(
144
- name="vit_b12_added",
145
- description="Vitamin B12 bổ sung (mg)",
146
- type="float"
147
- ),
148
- AttributeInfo(
149
- name="vit_e",
150
  description="Vitamin E (mg)",
151
  type="float"
152
  ),
153
  AttributeInfo(
154
- name="vit_e_added",
155
- description="Vitamin E bổ sung (mg)",
156
- type="float"
157
- ),
158
- AttributeInfo(
159
- name="vit_k",
160
  description="Vitamin K (mg)",
161
  type="float"
162
  ),
@@ -166,15 +154,15 @@ metadata_field_info = [
166
  type="float"
167
  ),
168
 
169
- # Khoáng chất
170
  AttributeInfo(
171
  name="canxi",
172
  description="Canxi (mg)",
173
  type="float"
174
  ),
175
  AttributeInfo(
176
- name="sat",
177
- description="Sắt (mg)",
178
  type="float"
179
  ),
180
  AttributeInfo(
@@ -198,15 +186,15 @@ metadata_field_info = [
198
  type="float"
199
  ),
200
  AttributeInfo(
201
- name="kem",
202
- description="Kẽm (mg)",
203
  type="float"
204
  ),
205
 
206
- # Thành phần khác
207
  AttributeInfo(
208
  name="water",
209
- description="Hàm lượng nước (g)",
210
  type="float"
211
  ),
212
  AttributeInfo(
@@ -216,87 +204,173 @@ metadata_field_info = [
216
  ),
217
  AttributeInfo(
218
  name="alcohol",
219
- description="Cồn (g)",
220
  type="float"
221
  ),
222
  ]
223
 
224
- document_content_description = "Mô tả ngắn gọn về món ăn"
225
-
 
 
 
 
 
226
 
227
  # ========================================
228
  # 2️⃣ Định nghĩa toán tử hỗ trợ và ví dụ
229
  # ========================================
230
  allowed_comparators = [
231
- "$eq",
232
- "$gt",
233
- "$gte",
234
- "$lt",
235
- "$lte",
236
- "$contain",
237
- "$like",
238
  ]
239
 
240
  examples = [
 
241
  (
242
  "Gợi ý các món ăn có trứng và ít hơn 500 kcal.",
243
  {
244
- "query": "món ăn trứng",
245
- "filter": 'and(lt("kcal", 500), contain("ingredients", "trứng"))',
 
246
  },
247
  ),
248
  (
249
  "Tìm món ăn không chứa trứng nhưng có nhiều protein hơn 30g.",
250
  {
251
- "query": "món ăn không có trứng",
252
- "filter": 'and(gt("protein", 30), not(contain("ingredients", "trứng")))',
 
253
  },
254
  ),
255
  (
256
- "Món ăn chay dễ nấu trong vòng 20 phút.",
257
  {
258
- "query": "món ăn chay",
259
- "filter": 'and(lte("cooking_time_minutes", 20), eq("difficulty", "easy"), not(contain("ingredients", "thịt")), not(contain("ingredients", "cá")))',
 
260
  },
261
  ),
 
 
262
  (
263
  "Món ăn giàu chất xơ, trên 10g, ít đường dưới 5g.",
264
  {
265
- "query": "món ăn giàu chất xơ",
266
  "filter": 'and(gt("fiber", 10), lt("sugar", 5))',
267
  },
268
  ),
269
  (
270
  "Món ăn có vitamin C trên 50mg và ít chất béo dưới 10g.",
271
  {
272
- "query": "món ăn nhiều vitamin C",
273
- "filter": 'and(gt("vit_c", 50), lt("lipid", 10))',
 
274
  },
275
  ),
276
  (
277
- "Gợi ý các món ăn keto với nhiều chất béo nhưng ít carb.",
278
  {
279
  "query": "món ăn keto",
280
- "filter": 'and(gt("lipid", 20), lt("carbohydrate", 5))',
 
281
  },
282
  ),
 
 
283
  (
284
- "Món ăn rốt, rong biểntrên 300 kcal.",
285
  {
286
- "query": "món ăn rốt rong biển",
287
- "filter": 'and(gt("kcal", 300), contain("ingredients", "cà rốt"), contain("ingredients", "rong biển"))',
 
288
  },
289
  ),
290
  (
291
- "Tìm món khoảng 500 kcal 20g protein",
292
  {
293
- "query": "món ăn năng lượng trung bình",
294
- "filter": 'and(gte("kcal", 450), lte("kcal", 450), gte("protein", 15), lte("protein", 25))',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
295
  },
296
  )
297
  ]
298
 
299
-
300
  # ========================================
301
  # 3️⃣ Tạo Query Constructor
302
  # ========================================
@@ -315,9 +389,13 @@ llm = ChatDeepSeek(
315
  max_retries=2,
316
  )
317
 
318
- output_parser = StructuredQueryOutputParser.from_components()
319
- query_constructor = prompt_query | llm | output_parser
320
-
 
 
 
 
321
 
322
  # ========================================
323
  # 4️⃣ Kết nối Elasticsearch
@@ -325,7 +403,7 @@ query_constructor = prompt_query | llm | output_parser
325
  docsearch = ElasticsearchStore(
326
  es_url=ELASTIC_CLOUD_URL,
327
  es_api_key=ELASTIC_API_KEY,
328
- index_name="food_vdb",
329
  embedding=embeddings,
330
  )
331
 
 
6
  from langchain_deepseek import ChatDeepSeek
7
  from langchain_elasticsearch import ElasticsearchStore
8
  from langchain.retrievers.self_query.elasticsearch import ElasticsearchTranslator
9
+ from langchain.chains.query_constructor.base import load_query_constructor_runnable
10
  from langchain.retrievers.self_query.base import SelfQueryRetriever
11
 
12
  from chatbot.models.embeddings import embeddings
13
  from chatbot.models.llm_setup import llm
14
+ from chatbot.config import ELASTIC_CLOUD_URL, ELASTIC_API_KEY, FOOD_DB_INDEX
15
 
16
 
17
  # ========================================
18
  # 1️⃣ Định nghĩa metadata field info
19
  # ========================================
20
  metadata_field_info = [
21
+ # --- THÔNG TIN ĐỊNH DANH & PHÂN LOẠI ---
 
22
  AttributeInfo(
23
  name="meal_id",
24
+ description="ID duy nhất của món ăn (số nguyên)",
25
  type="integer"
26
  ),
27
  AttributeInfo(
28
  name="name",
29
+ description="Tên của món ăn",
 
 
 
 
 
 
 
 
 
 
30
  type="string"
31
  ),
32
  AttributeInfo(
33
+ name="tags",
34
+ description=(
35
+ "Danh sách các thẻ phân loại đặc điểm món ăn. Bao gồm các nhóm chính: "
36
+ "1. Nhóm thực phẩm: #HảiSản, #Thịt, #RauXanh, #Lẩu, #ĐồĂnNhanh... "
37
+ "2. Dinh dưỡng (Macros): #HighProtein (Giàu đạm), #LowCarbs (Ít tinh bột), #LowCalories (Ít calo)... "
38
+ "3. Chất béo: #LowSaturatedFat (Ít béo bão hòa), #LowCholesterol... "
39
+ "4. Vitamin & Khoáng chất: #HighVitaminC, #HighFe (Giàu Sắt), #HighCanxi... "
40
+ "Lưu ý: Các tag thường bắt đầu bằng dấu # và viết liền (PascalCase)."
41
+ ),
42
+ type="list[string]"
43
  ),
44
 
45
+ # --- NGUYÊN LIỆU ---
46
  AttributeInfo(
47
  name="ingredients",
48
+ description="Danh sách các nguyên liệu trong món ăn (dạng list)",
49
+ type="list[string]"
50
  ),
51
  AttributeInfo(
52
  name="ingredients_text",
53
+ description="Chuỗi văn bản liệt toàn bộ nguyên liệu (dùng để tìm kiếm text)",
54
  type="string"
55
  ),
56
 
57
+ # --- NĂNG LƯỢNG & MACROS (CHẤT ĐA LƯỢNG) ---
58
  AttributeInfo(
59
  name="kcal",
60
+ description="Tổng năng lượng (kcal)",
61
  type="float"
62
  ),
63
  AttributeInfo(
64
  name="protein",
65
+ description="Hàm lượng Đạm/Protein (g)",
66
  type="float"
67
  ),
68
  AttributeInfo(
69
+ name="carbs",
70
+ description="Hàm lượng Bột đường/Carbohydrate (g)",
71
  type="float"
72
  ),
73
  AttributeInfo(
74
  name="sugar",
75
+ description="Hàm lượng Đường (g)",
76
  type="float"
77
  ),
78
  AttributeInfo(
79
  name="fiber",
80
+ description="Hàm lượng Chất xơ (g)",
81
  type="float"
82
  ),
83
+ # --- CẬP NHẬT MỚI: TOTAL FAT ---
84
  AttributeInfo(
85
+ name="totalfat",
86
+ description="Tổng lượng Chất béo (g)",
87
  type="float"
88
  ),
89
  AttributeInfo(
90
+ name="saturatedfat",
91
  description="Chất béo bão hòa (g)",
92
  type="float"
93
  ),
94
  AttributeInfo(
95
+ name="monounsaturatedfat",
96
  description="Chất béo không bão hòa đơn (g)",
97
  type="float"
98
  ),
99
  AttributeInfo(
100
+ name="polyunsaturatedfat",
101
  description="Chất béo không bão hòa đa (g)",
102
  type="float"
103
  ),
104
  AttributeInfo(
105
+ name="transfat",
106
  description="Chất béo chuyển hóa (g)",
107
  type="float"
108
  ),
109
  AttributeInfo(
110
  name="cholesterol",
111
+ description="Hàm lượng Cholesterol (mg)",
112
  type="float"
113
  ),
114
 
115
+ # --- VITAMINS ---
116
  AttributeInfo(
117
+ name="vitamina",
118
  description="Vitamin A (mg)",
119
  type="float"
120
  ),
121
  AttributeInfo(
122
+ name="vitamind",
123
  description="Vitamin D (mg)",
124
  type="float"
125
  ),
126
  AttributeInfo(
127
+ name="vitaminc",
128
  description="Vitamin C (mg)",
129
  type="float"
130
  ),
131
  AttributeInfo(
132
+ name="vitaminb6",
133
  description="Vitamin B6 (mg)",
134
  type="float"
135
  ),
136
  AttributeInfo(
137
+ name="vitaminb12",
138
  description="Vitamin B12 (mg)",
139
  type="float"
140
  ),
141
  AttributeInfo(
142
+ name="vitamine",
 
 
 
 
 
143
  description="Vitamin E (mg)",
144
  type="float"
145
  ),
146
  AttributeInfo(
147
+ name="vitamink",
 
 
 
 
 
148
  description="Vitamin K (mg)",
149
  type="float"
150
  ),
 
154
  type="float"
155
  ),
156
 
157
+ # --- KHOÁNG CHẤT ---
158
  AttributeInfo(
159
  name="canxi",
160
  description="Canxi (mg)",
161
  type="float"
162
  ),
163
  AttributeInfo(
164
+ name="fe",
165
+ description="Sắt/Fe (mg)",
166
  type="float"
167
  ),
168
  AttributeInfo(
 
186
  type="float"
187
  ),
188
  AttributeInfo(
189
+ name="zn",
190
+ description="Kẽm/Zn (mg)",
191
  type="float"
192
  ),
193
 
194
+ # --- THÀNH PHẦN KHÁC ---
195
  AttributeInfo(
196
  name="water",
197
+ description="Hàm lượng Nước (g)",
198
  type="float"
199
  ),
200
  AttributeInfo(
 
204
  ),
205
  AttributeInfo(
206
  name="alcohol",
207
+ description="Cồn/Alcohol (g)",
208
  type="float"
209
  ),
210
  ]
211
 
212
+ document_content_description = """
213
+ Thông tin chi tiết về các món ăn.
214
+ Quy tắc ánh xạ Tag (Tag Mapping Rules):
215
+ - Nếu người dùng tìm "Giàu/Nhiều X", hãy dùng tag "#HighX" (ví dụ: Giàu đạm -> #HighProtein, Giàu Sắt -> #HighFe).
216
+ - Nếu người dùng tìm "Ít/Thấp X", hãy dùng tag "#LowX" (ví dụ: Ít béo -> #LowSaturatedFat, Ít đường -> #LowSugar).
217
+ - Các thực phẩm cụ thể thường có tag tương ứng (Hải sản -> #HảiSản, Rau -> #RauXanh).
218
+ """
219
 
220
  # ========================================
221
  # 2️⃣ Định nghĩa toán tử hỗ trợ và ví dụ
222
  # ========================================
223
  allowed_comparators = [
224
+ "eq",
225
+ "gt",
226
+ "gte",
227
+ "lt",
228
+ "lte",
229
+ "contain",
230
+ "like"
231
  ]
232
 
233
  examples = [
234
+ # --- NHÓM 1: NGUYÊN LIỆU & SỐ LIỆU CỤ THỂ (Ưu tiên logic số học) ---
235
  (
236
  "Gợi ý các món ăn có trứng và ít hơn 500 kcal.",
237
  {
238
+ "query": "món ăn từ trứng",
239
+ # Dùng contain cho ingredients, lt cho kcal
240
+ "filter": 'and(lt("kcal", 500), contain("ingredients", "Trứng"))',
241
  },
242
  ),
243
  (
244
  "Tìm món ăn không chứa trứng nhưng có nhiều protein hơn 30g.",
245
  {
246
+ "query": "món ăn giàu đạm",
247
+ # Dùng not(contain(...))
248
+ "filter": 'and(gt("protein", 30), not(contain("ingredients", "Trứng")))',
249
  },
250
  ),
251
  (
252
+ "Món ăn rốt, rong biển trên 300 kcal.",
253
  {
254
+ "query": "món ăn nguyên liệu cụ thể",
255
+ # Contain riêng biệt cho từng nguyên liệu
256
+ "filter": 'and(gt("kcal", 300), contain("ingredients", "Cà Rốt"), contain("ingredients", "Rong Biển"))',
257
  },
258
  ),
259
+
260
+ # --- NHÓM 2: MACROS VỚI SỐ LIỆU (Chú ý tên trường: totalfat, carbs, vitaminc) ---
261
  (
262
  "Món ăn giàu chất xơ, trên 10g, ít đường dưới 5g.",
263
  {
264
+ "query": "món ăn healthy",
265
  "filter": 'and(gt("fiber", 10), lt("sugar", 5))',
266
  },
267
  ),
268
  (
269
  "Món ăn có vitamin C trên 50mg và ít chất béo dưới 10g.",
270
  {
271
+ "query": "món ăn giàu vitamin C",
272
+ # Sửa map: vit_c -> vitaminc, lipid -> totalfat
273
+ "filter": 'and(gt("vitaminc", 50), lt("totalfat", 10))',
274
  },
275
  ),
276
  (
277
+ "Gợi ý các món ăn keto với nhiều chất béo (trên 20g) nhưng ít carb (dưới 5g).",
278
  {
279
  "query": "món ăn keto",
280
+ # Sửa map: carbohydrate -> carbs
281
+ "filter": 'and(gt("totalfat", 20), lt("carbs", 5))',
282
  },
283
  ),
284
+
285
+ # --- NHÓM 3: MAPPING TAG TRỪU TƯỢNG (Khi không có số liệu) ---
286
  (
287
+ "Gợi ý các món giàu đạm (nhiều protein) ít tinh bột.",
288
  {
289
+ "query": "món ăn giàu đạm ít tinh bột",
290
+ # Không số -> Dùng Tags #HighProtein, #LowCarbs
291
+ "filter": 'and(contain("tags", "#HighProtein"), contain("tags", "#LowCarbs"))',
292
  },
293
  ),
294
  (
295
+ "Tìm món ăn ít calo để giảm cân.",
296
  {
297
+ "query": "món ăn giảm cân",
298
+ # Giảm cân -> #LowCalories
299
+ "filter": 'contain("tags", "#LowCalories")',
300
+ },
301
+ ),
302
+ (
303
+ "Tìm món thanh đạm, ít béo bão hòa.",
304
+ {
305
+ "query": "món ăn thanh đạm",
306
+ # Ít béo bão hòa -> #LowSaturatedFat
307
+ "filter": 'contain("tags", "#LowSaturatedFat")',
308
+ },
309
+ ),
310
+
311
+ # --- NHÓM 4: MAPPING VITAMIN & KHOÁNG CHẤT (Fe, Zn, Canxi...) ---
312
+ (
313
+ "Món ăn bổ máu (giàu sắt).",
314
+ {
315
+ "query": "món ăn bổ máu",
316
+ # Sắt -> #HighFe
317
+ "filter": 'contain("tags", "#HighFe")',
318
+ },
319
+ ),
320
+ (
321
+ "Món ăn giàu canxi cho xương chắc khỏe.",
322
+ {
323
+ "query": "món ăn giàu canxi",
324
+ # Canxi -> #HighCanxi
325
+ "filter": 'contain("tags", "#HighCanxi")',
326
+ },
327
+ ),
328
+ (
329
+ "Món ăn tốt cho mắt (giàu vitamin A).",
330
+ {
331
+ "query": "món ăn tốt cho mắt",
332
+ # Vitamin A -> #HighVitaminA
333
+ "filter": 'contain("tags", "#HighVitaminA")',
334
+ },
335
+ ),
336
+
337
+ # --- NHÓM 5: SỨC KHỎE TIM MẠCH & BỆNH LÝ ---
338
+ (
339
+ "Tìm món tốt cho tim mạch, ít cholesterol.",
340
+ {
341
+ "query": "món ăn tốt cho tim mạch",
342
+ # Cholesterol -> #LowCholesterol
343
+ "filter": 'contain("tags", "#LowCholesterol")',
344
+ },
345
+ ),
346
+ (
347
+ "Tìm món ăn nhạt muối cho người huyết áp cao.",
348
+ {
349
+ "query": "món ăn nhạt muối",
350
+ # Nhạt muối/Ít Natri -> #LowNatri
351
+ "filter": 'contain("tags", "#LowNatri")',
352
+ },
353
+ ),
354
+
355
+ # --- NHÓM 6: LOẠI MÓN ĂN & NHÓM THỰC PHẨM ---
356
+ (
357
+ "Tìm món lẩu hải sản",
358
+ {
359
+ "query": "lẩu hải sản",
360
+ # Tags loại món
361
+ "filter": 'and(contain("tags", "#Lẩu"), contain("tags", "#HảiSản"))',
362
+ },
363
+ ),
364
+ (
365
+ "Món chay có nhiều rau xanh.",
366
+ {
367
+ "query": "món chay rau xanh",
368
+ # Rau xanh -> #RauXanh
369
+ "filter": 'contain("tags", "#RauXanh")',
370
  },
371
  )
372
  ]
373
 
 
374
  # ========================================
375
  # 3️⃣ Tạo Query Constructor
376
  # ========================================
 
389
  max_retries=2,
390
  )
391
 
392
+ query_constructor = load_query_constructor_runnable(
393
+ llm=llm,
394
+ document_contents=document_content_description,
395
+ attribute_info=metadata_field_info,
396
+ examples=examples,
397
+ allowed_comparators=allowed_comparators
398
+ )
399
 
400
  # ========================================
401
  # 4️⃣ Kết nối Elasticsearch
 
403
  docsearch = ElasticsearchStore(
404
  es_url=ELASTIC_CLOUD_URL,
405
  es_api_key=ELASTIC_API_KEY,
406
+ index_name=FOOD_DB_INDEX,
407
  embedding=embeddings,
408
  )
409
 
chatbot/agents/tools/info_app_retriever.py CHANGED
@@ -1,12 +1,12 @@
1
  from langchain_elasticsearch import ElasticsearchStore
2
 
3
  from chatbot.models.embeddings import embeddings
4
- from chatbot.config import ELASTIC_CLOUD_URL, ELASTIC_API_KEY
5
 
6
  policy_search = ElasticsearchStore(
7
  es_url=ELASTIC_CLOUD_URL,
8
  es_api_key=ELASTIC_API_KEY,
9
- index_name="policy_vdb",
10
  embedding=embeddings,
11
  )
12
 
 
1
  from langchain_elasticsearch import ElasticsearchStore
2
 
3
  from chatbot.models.embeddings import embeddings
4
+ from chatbot.config import ELASTIC_CLOUD_URL, ELASTIC_API_KEY, POLICY_DB_INDEX
5
 
6
  policy_search = ElasticsearchStore(
7
  es_url=ELASTIC_CLOUD_URL,
8
  es_api_key=ELASTIC_API_KEY,
9
+ index_name=POLICY_DB_INDEX,
10
  embedding=embeddings,
11
  )
12
 
chatbot/config.py CHANGED
@@ -4,8 +4,10 @@ import os
4
  load_dotenv()
5
 
6
  DEEPSEEK_API_KEY = os.getenv('DEEPSEEK_API_KEY')
7
- PINECONE_API_KEY = os.getenv('PINECONE_API_KEY')
8
  ELASTIC_CLOUD_URL = os.getenv('ELASTIC_CLOUD_URL')
9
  ELASTIC_API_KEY = os.getenv('ELASTIC_API_KEY')
 
 
10
 
11
  API_BASE_URL=os.getenv('API_BASE_URL')
 
4
  load_dotenv()
5
 
6
  DEEPSEEK_API_KEY = os.getenv('DEEPSEEK_API_KEY')
7
+
8
  ELASTIC_CLOUD_URL = os.getenv('ELASTIC_CLOUD_URL')
9
  ELASTIC_API_KEY = os.getenv('ELASTIC_API_KEY')
10
+ FOOD_DB_INDEX = os.getenv('FOOD_DB_INDEX')
11
+ POLICY_DB_INDEX = os.getenv('POLICY_DB_INDEX')
12
 
13
  API_BASE_URL=os.getenv('API_BASE_URL')
chatbot/knowledge/field_requirement.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ TOPIC_REQUIREMENTS = {
2
+ "meal_suggestion": ["targetcalories", "protein", "totalfat", "carbohydrate"],
3
+ "food_query": [],
4
+ "food_suggestion": ["targetcalories", "protein", "totalfat", "carbohydrate"],
5
+ "general_chat": [],
6
+ "policy": []
7
+ }
8
+
9
+ FIELD_NAMES_VN = {
10
+ "targetcalories": "Mức Calo mục tiêu (hoặc Cân nặng & Chiều cao)",
11
+ "weight": "Cân nặng",
12
+ "height": "Chiều cao",
13
+ "age": "Tuổi",
14
+ "gender": "Giới tính",
15
+ "diet": "Chế độ ăn",
16
+ "health_status": "Tình trạng sức khỏe"
17
+ }
chatbot/knowledge/vibe.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ vibes_soup_veg = [
2
+ "canh hầm thanh mát bổ dưỡng",
3
+ "canh rau giải nhiệt ngày hè",
4
+ "món hấp thủy giữ trọn vị ngọt",
5
+ "rau củ luộc chấm kho quẹt",
6
+ "salad trộn dầu giấm tươi ngon",
7
+ "món xào rau củ thanh đạm",
8
+ "canh chua đậm đà hương vị việt",
9
+ "món nước thanh lọc cơ thể",
10
+ "rau xanh xào tỏi thơm lừng"
11
+ ]
12
+
13
+ vibes_flavor = [
14
+ "hương vị thanh đạm nhẹ nhàng",
15
+ "hương vị đậm đà đưa cơm",
16
+ "vị chua ngọt kích thích vị giác",
17
+ "hương thơm nồng nàn hấp dẫn",
18
+ "thanh mát giải nhiệt",
19
+ "ấm bụng ngày mưa",
20
+ "tươi ngon tự nhiên",
21
+ "ngọt thanh từ rau củ",
22
+ "vị ngon nguyên bản",
23
+ "hài hòa ngũ vị"
24
+ ]
25
+
26
+ vibes_style = [
27
+ "phong cách truyền thống việt nam",
28
+ "ẩm thực đồng quê dân dã",
29
+ "chuẩn vị cơm nhà mẹ nấu",
30
+ "đặc sản vùng miền",
31
+ "phong cách hiện đại mới lạ",
32
+ "ẩm thực cung đình huế",
33
+ "hương vị miền tây sông nước",
34
+ "ẩm thực hà nội xưa",
35
+ "phong cách đường phố (street food)",
36
+ "kết hợp á âu (fusion)"
37
+ ]
38
+
39
+ vibes_cooking = [
40
+ "chế biến đơn giản nhanh gọn",
41
+ "hầm mềm bổ dưỡng",
42
+ "nướng thơm lừng ít mỡ",
43
+ "hấp thủy giữ trọn chất",
44
+ "kho tộ đậm đà",
45
+ "xào nhanh tay lửa lớn",
46
+ "trộn gỏi giòn tan",
47
+ "canh hầm kỹ",
48
+ "chiên không dầu (air fryer)",
49
+ "áp chảo xém cạnh"
50
+ ]
51
+
52
+ vibes_healthy = [
53
+ "eat clean tốt cho sức khỏe",
54
+ "ít gia vị giữ vị tự nhiên",
55
+ "giàu chất xơ vitamin",
56
+ "năng lượng sạch (clean energy)",
57
+ "nhẹ bụng dễ tiêu hóa",
58
+ "thực dưỡng cân bằng",
59
+ "bổ sung đề kháng",
60
+ "ít dầu mỡ thanh lọc cơ thể"
61
+ ]
chatbot/main.py CHANGED
@@ -3,6 +3,7 @@ from fastapi.middleware.cors import CORSMiddleware
3
  from chatbot.routes.chat_router import router as chat_router
4
  from chatbot.routes.meal_plan_route import router as meal_plan_router
5
  from chatbot.routes.food_replace_route import router as food_replace_router
 
6
 
7
  app = FastAPI(
8
  title="AI Meal Chatbot API",
@@ -23,6 +24,7 @@ app.add_middleware(
23
  app.include_router(chat_router)
24
  app.include_router(meal_plan_router)
25
  app.include_router(food_replace_router)
 
26
 
27
  @app.get("/")
28
  def root():
 
3
  from chatbot.routes.chat_router import router as chat_router
4
  from chatbot.routes.meal_plan_route import router as meal_plan_router
5
  from chatbot.routes.food_replace_route import router as food_replace_router
6
+ from chatbot.routes.manage_food_route import router as manage_food_router
7
 
8
  app = FastAPI(
9
  title="AI Meal Chatbot API",
 
24
  app.include_router(chat_router)
25
  app.include_router(meal_plan_router)
26
  app.include_router(food_replace_router)
27
+ app.include_router(manage_food_router)
28
 
29
  @app.get("/")
30
  def root():
chatbot/routes/__pycache__/chat_router.cpython-310.pyc CHANGED
Binary files a/chatbot/routes/__pycache__/chat_router.cpython-310.pyc and b/chatbot/routes/__pycache__/chat_router.cpython-310.pyc differ
 
chatbot/routes/__pycache__/food_replace_route.cpython-310.pyc CHANGED
Binary files a/chatbot/routes/__pycache__/food_replace_route.cpython-310.pyc and b/chatbot/routes/__pycache__/food_replace_route.cpython-310.pyc differ
 
chatbot/routes/chat_router.py CHANGED
@@ -1,12 +1,17 @@
1
  from fastapi import APIRouter, HTTPException
2
  from pydantic import BaseModel
3
  from langchain_core.messages import HumanMessage
4
- from chatbot.agents.states.state import AgentState
5
  from chatbot.agents.graphs.chatbot_graph import workflow_chatbot
 
 
 
 
 
6
 
7
  # --- Định nghĩa request body ---
8
  class ChatRequest(BaseModel):
9
  user_id: str
 
10
  message: str
11
 
12
  # --- Tạo router ---
@@ -15,27 +20,34 @@ router = APIRouter(
15
  tags=["Chatbot"]
16
  )
17
 
 
 
 
 
 
 
18
  # --- Route xử lý chat ---
19
  @router.post("/")
20
  def chat(request: ChatRequest):
21
  try:
22
-
23
- print("Nhận được yêu cầu chat từ user:", request.user_id)
24
-
25
- # 1. Tạo state mới
26
- state = AgentState()
27
- state["user_id"] = request.user_id
28
- state["messages"] = [HumanMessage(content=request.message)]
29
 
30
- # 2. Lấy workflow chatbot
31
- graph = workflow_chatbot()
32
 
33
- # 3. Invoke workflow
34
- result = graph.invoke(state)
 
 
 
35
 
36
- # 4. Trả response
37
- response = result["response"] or "Không có kết quả"
38
- return {"response": response}
39
 
40
  except Exception as e:
41
- raise HTTPException(status_code=500, detail=f"Lỗi chatbot: {e}")
 
 
1
  from fastapi import APIRouter, HTTPException
2
  from pydantic import BaseModel
3
  from langchain_core.messages import HumanMessage
 
4
  from chatbot.agents.graphs.chatbot_graph import workflow_chatbot
5
+ import logging
6
+
7
+ # --- Cấu hình logging ---
8
+ logging.basicConfig(level=logging.INFO)
9
+ logger = logging.getLogger(__name__)
10
 
11
  # --- Định nghĩa request body ---
12
  class ChatRequest(BaseModel):
13
  user_id: str
14
+ thread_id: str
15
  message: str
16
 
17
  # --- Tạo router ---
 
20
  tags=["Chatbot"]
21
  )
22
 
23
+ try:
24
+ chatbot_app = workflow_chatbot()
25
+ except Exception as e:
26
+ logger.error(f"❌ Failed to compile Chatbot Graph: {e}")
27
+ raise e
28
+
29
  # --- Route xử lý chat ---
30
  @router.post("/")
31
  def chat(request: ChatRequest):
32
  try:
33
+ logger.info(f"Nhận được tin nhắn chat từ user: {request.user_id}")
34
+ config = {"configurable": {"thread_id": request.thread_id}}
35
+
36
+ initial_state = {
37
+ "user_id": request.user_id,
38
+ "messages": [HumanMessage(content=request.message)]
39
+ }
40
 
41
+ final_state = chatbot_app.invoke(initial_state, config=config)
 
42
 
43
+ messages = final_state.get("messages", [])
44
+ if messages and len(messages) > 0:
45
+ response_content = messages[-1].content
46
+ else:
47
+ response_content = "Không có kết quả trả về."
48
 
49
+ return {"response": response_content}
 
 
50
 
51
  except Exception as e:
52
+ logger.error(f"Lỗi chatbot: {e}", exc_info=True)
53
+ raise HTTPException(status_code=500, detail=f"Lỗi chatbot: {str(e)}")
chatbot/routes/food_replace_route.py CHANGED
@@ -1,8 +1,11 @@
1
  from fastapi import APIRouter, HTTPException
2
  from pydantic import BaseModel
3
- from langchain_core.messages import HumanMessage
4
- from chatbot.agents.states.state import AgentState
5
  from chatbot.agents.graphs.food_similarity_graph import food_similarity_graph
 
 
 
 
 
6
 
7
  # --- Định nghĩa request body ---
8
  class Request(BaseModel):
@@ -15,27 +18,38 @@ router = APIRouter(
15
  tags=["Food Replace"]
16
  )
17
 
 
 
 
 
 
 
18
  @router.post("/")
19
- def chat(request: Request):
20
  try:
 
21
 
22
- print("Nhận được yêu cầu chat từ user:", request.user_id)
 
23
 
24
- # 1. Tạo state mới
25
- state = AgentState()
26
- state["user_id"] = request.user_id
27
- state["food_old"] = request.food_old
28
- state["food_old"]["solver_bounds"] = tuple(state["food_old"].get("solver_bounds"))
29
 
30
- # 2. Lấy workflow
31
- graph = food_similarity_graph()
 
 
32
 
33
- # 3. Invoke workflow
34
- result = graph.invoke(state)
 
 
 
35
 
36
- # 4. Trả response
37
- response = result or "Không có kết quả"
38
- return {"response": response}
39
 
40
  except Exception as e:
41
- raise HTTPException(status_code=500, detail=f"Lỗi chatbot: {e}")
 
 
1
  from fastapi import APIRouter, HTTPException
2
  from pydantic import BaseModel
 
 
3
  from chatbot.agents.graphs.food_similarity_graph import food_similarity_graph
4
+ import logging
5
+
6
+ # --- Cấu hình logging ---
7
+ logging.basicConfig(level=logging.INFO)
8
+ logger = logging.getLogger(__name__)
9
 
10
  # --- Định nghĩa request body ---
11
  class Request(BaseModel):
 
18
  tags=["Food Replace"]
19
  )
20
 
21
+ try:
22
+ replace_app = food_similarity_graph()
23
+ except Exception as e:
24
+ logger.error(f"❌ Failed to compile Food Graph: {e}")
25
+ raise e
26
+
27
  @router.post("/")
28
+ def replace_food(request: Request):
29
  try:
30
+ logger.info(f"Nhận được yêu cầu thay thế món từ user: {request.user_id}")
31
 
32
+ food_data = request.food_old.copy()
33
+ bounds = food_data.get("solver_bounds")
34
 
35
+ if bounds and isinstance(bounds, list):
36
+ food_data["solver_bounds"] = tuple(bounds)
37
+ elif not bounds:
38
+ food_data["solver_bounds"] = (0.5, 2.0)
 
39
 
40
+ initial_state = {
41
+ "user_id": request.user_id,
42
+ "food_old": food_data,
43
+ }
44
 
45
+ final_state = replace_app.invoke(initial_state)
46
+ response = {"best_replacement": final_state["best_replacement"]}
47
+
48
+ if not response:
49
+ return {"status": "failed", "response": []}
50
 
51
+ return {"status": "success", "response": response}
 
 
52
 
53
  except Exception as e:
54
+ logger.error(f"Lỗi xử lý thay thế món: {e}", exc_info=True)
55
+ raise HTTPException(status_code=500, detail=f"Lỗi hệ thống: {str(e)}")
chatbot/routes/manage_food_route.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException
2
+ from pydantic import BaseModel
3
+ from langchain_core.documents import Document
4
+ from langchain_elasticsearch import ElasticsearchStore
5
+ from chatbot.config import ELASTIC_CLOUD_URL, ELASTIC_API_KEY, FOOD_DB_INDEX
6
+ from chatbot.models.embeddings import embeddings
7
+ import logging
8
+
9
+ # --- Cấu hình logging ---
10
+ logging.basicConfig(level=logging.INFO)
11
+ logger = logging.getLogger(__name__)
12
+
13
+ # --- Định nghĩa request body ---
14
+ class FoodItemPayload(BaseModel):
15
+ text_for_embedding: str
16
+ metadata: dict
17
+
18
+ # --- Tạo router ---
19
+ router = APIRouter(
20
+ prefix="/manage-food",
21
+ tags=["Food Management (CRUD)"]
22
+ )
23
+
24
+ try:
25
+ es_store = ElasticsearchStore(
26
+ es_url=ELASTIC_CLOUD_URL,
27
+ es_api_key=ELASTIC_API_KEY,
28
+ index_name=FOOD_DB_INDEX,
29
+ embedding=embeddings,
30
+ )
31
+ logger.info("✅ Connected to Elasticsearch for Management.")
32
+ except Exception as e:
33
+ logger.error(f"❌ Failed to connect to Elasticsearch: {e}")
34
+ es_store = None
35
+
36
+ @router.post("/save")
37
+ def save_food(item: FoodItemPayload):
38
+ if not es_store:
39
+ raise HTTPException(status_code=500, detail="Elasticsearch connection failed.")
40
+
41
+ try:
42
+ page_content = item.text_for_embedding
43
+ metadata = item.metadata
44
+ id = metadata["meal_id"]
45
+
46
+ doc = Document(
47
+ page_content=page_content,
48
+ metadata=metadata
49
+ )
50
+
51
+ es_store.add_documents(documents=[doc], ids=[id])
52
+
53
+ logger.info(f"Saved food: {id}")
54
+ return {"status": "success"}
55
+ except Exception as e:
56
+ logger.error(f"Error saving food: {e}")
57
+ raise HTTPException(status_code=500, detail=str(e))
58
+
59
+ @router.delete("/delete/{meal_id}")
60
+ def delete_food(meal_id: str):
61
+ if not es_store:
62
+ raise HTTPException(status_code=500, detail="Elasticsearch connection failed.")
63
+
64
+ try:
65
+ es_store.delete(ids=[meal_id])
66
+
67
+ logger.info(f"Deleted food: {meal_id}")
68
+ return {"status": "success"}
69
+ except Exception as e:
70
+ logger.error(f"Error deleting food: {e}")
71
+ raise HTTPException(status_code=500, detail=str(e))
chatbot/routes/meal_plan_route.py CHANGED
@@ -1,13 +1,16 @@
1
  from fastapi import APIRouter, HTTPException
2
  from pydantic import BaseModel
3
- from langchain_core.messages import HumanMessage
4
- from chatbot.agents.states.state import AgentState
5
  from chatbot.agents.graphs.meal_suggestion_graph import meal_plan_graph
 
 
 
 
 
6
 
7
  # --- Định nghĩa request body ---
8
  class Request(BaseModel):
9
  user_id: str
10
- meals_to_generate: list
11
 
12
  # --- Tạo router ---
13
  router = APIRouter(
@@ -15,27 +18,35 @@ router = APIRouter(
15
  tags=["Meal Plan"]
16
  )
17
 
18
- # --- Route xử lý chat ---
 
 
 
 
 
 
 
19
  @router.post("/")
20
- def chat(request: Request):
21
  try:
22
-
23
- print("Nhận được yêu cầu chat từ user:", request.user_id)
24
-
25
- # 1. Tạo state mới
26
- state = AgentState()
27
- state["user_id"] = request.user_id
28
- state["meals_to_generate"] = request.meals_to_generate
29
-
30
- # 2. Lấy workflow
31
- graph = meal_plan_graph()
32
 
33
- # 3. Invoke workflow
34
- result = graph.invoke(state)
 
 
 
 
 
 
35
 
36
- # 4. Trả response
37
- response = result or "Không có kết quả"
38
- return {"response": response}
39
 
40
  except Exception as e:
41
- raise HTTPException(status_code=500, detail=f"Lỗi chatbot: {e}")
 
 
1
  from fastapi import APIRouter, HTTPException
2
  from pydantic import BaseModel
 
 
3
  from chatbot.agents.graphs.meal_suggestion_graph import meal_plan_graph
4
+ import logging
5
+
6
+ # --- Cấu hình logging ---
7
+ logging.basicConfig(level=logging.INFO)
8
+ logger = logging.getLogger(__name__)
9
 
10
  # --- Định nghĩa request body ---
11
  class Request(BaseModel):
12
  user_id: str
13
+ meals_to_generate: list # VD: ["sáng", "trưa", "tối"]
14
 
15
  # --- Tạo router ---
16
  router = APIRouter(
 
18
  tags=["Meal Plan"]
19
  )
20
 
21
+ try:
22
+ meal_app = meal_plan_graph()
23
+ logger.info("✅ Meal Plan Graph compiled successfully!")
24
+ except Exception as e:
25
+ logger.error(f"❌ Failed to compile Meal Plan Graph: {e}")
26
+ raise e
27
+
28
+ # --- Route xử lý ---
29
  @router.post("/")
30
+ def generate_meal_plan(request: Request):
31
  try:
32
+ logger.info(f"Nhận yêu cầu lên thực đơn cho user: {request.user_id} - Bữa: {request.meals_to_generate}")
33
+
34
+ initial_state = {
35
+ "user_id": request.user_id,
36
+ "meals_to_generate": request.meals_to_generate,
37
+ }
 
 
 
 
38
 
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"]:
46
+ return {"status": "failed", "response": []}
47
 
48
+ return {"status": "success", "response": response}
 
 
49
 
50
  except Exception as e:
51
+ logger.error(f"Lỗi tạo thực đơn: {e}", exc_info=True)
52
+ raise HTTPException(status_code=500, detail=f"Lỗi hệ thống: {str(e)}")
chatbot/utils/chat_history.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain_core.messages import trim_messages, BaseMessage
2
+ import tiktoken
3
+
4
+ enc = tiktoken.get_encoding("cl100k_base")
5
+
6
+ def custom_token_counter(messages: list[BaseMessage]) -> int:
7
+ text_content = ""
8
+ for msg in messages:
9
+ if isinstance(msg.content, str):
10
+ text_content += msg.content
11
+ elif isinstance(msg.content, list):
12
+ for part in msg.content:
13
+ if isinstance(part, str):
14
+ text_content += part
15
+ elif isinstance(part, dict) and 'text' in part:
16
+ text_content += part['text']
17
+
18
+ return len(enc.encode(text_content))
19
+
20
+ def get_chat_history(messages, max_tokens=1000):
21
+ return trim_messages(
22
+ messages,
23
+ max_tokens=max_tokens,
24
+ strategy="last",
25
+ token_counter=custom_token_counter,
26
+ include_system=True,
27
+ start_on="human",
28
+ allow_partial=False
29
+ )
requirements.txt CHANGED
@@ -14,4 +14,5 @@ langchain-elasticsearch==0.3.0
14
  elasticsearch==8.19.1
15
  lark==1.3.1
16
  fastapi==0.121.0
17
- uvicorn[standard]==0.38.0
 
 
14
  elasticsearch==8.19.1
15
  lark==1.3.1
16
  fastapi==0.121.0
17
+ uvicorn[standard]==0.38.0
18
+ thefuzz==0.22.1