truglpk3 commited on
Commit
b5961aa
·
1 Parent(s): 49ffc6f

update optimization algorithm

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. chatbot/__pycache__/config.cpython-310.pyc +0 -0
  2. chatbot/__pycache__/main.cpython-310.pyc +0 -0
  3. chatbot/agents/graphs/__pycache__/chatbot_graph.cpython-310.pyc +0 -0
  4. chatbot/agents/graphs/__pycache__/food_similarity_graph.cpython-310.pyc +0 -0
  5. chatbot/agents/graphs/__pycache__/meal_suggestion_graph.cpython-310.pyc +0 -0
  6. chatbot/agents/graphs/chatbot_graph.py +32 -13
  7. chatbot/agents/graphs/food_similarity_graph.py +14 -16
  8. chatbot/agents/graphs/meal_similarity_graph.py +0 -23
  9. chatbot/agents/graphs/meal_suggestion_graph.py +11 -10
  10. chatbot/agents/graphs/meal_suggestion_json_graph.py +0 -23
  11. chatbot/agents/nodes/__pycache__/classify_topic.cpython-310.pyc +0 -0
  12. chatbot/agents/nodes/__pycache__/meal_identify.cpython-310.pyc +0 -0
  13. chatbot/agents/nodes/__pycache__/suggest_meal_node.cpython-310.pyc +0 -0
  14. chatbot/agents/nodes/app_functions/__init__.py +21 -0
  15. chatbot/agents/nodes/app_functions/find_candidates.py +68 -0
  16. chatbot/agents/nodes/app_functions/generate_candidates.py +261 -0
  17. chatbot/agents/nodes/app_functions/get_profile.py +22 -0
  18. chatbot/agents/nodes/app_functions/optimize_macros.py +204 -0
  19. chatbot/agents/nodes/app_functions/optimize_select.py +76 -0
  20. chatbot/agents/nodes/app_functions/select_meal.py +129 -0
  21. chatbot/agents/nodes/app_functions/select_menu.py +237 -0
  22. chatbot/agents/nodes/chatbot/__init__.py +25 -0
  23. chatbot/agents/nodes/{classify_topic.py → chatbot/classify_topic.py} +70 -59
  24. chatbot/agents/nodes/chatbot/food_query.py +39 -0
  25. chatbot/agents/nodes/{food_query.py → chatbot/food_suggestion.py} +34 -30
  26. chatbot/agents/nodes/{general_chat.py → chatbot/general_chat.py} +44 -39
  27. chatbot/agents/nodes/chatbot/generate_final_response.py +39 -0
  28. chatbot/agents/nodes/{meal_identify.py → chatbot/meal_identify.py} +55 -64
  29. chatbot/agents/nodes/chatbot/policy.py +46 -0
  30. chatbot/agents/nodes/chatbot/select_food.py +56 -0
  31. chatbot/agents/nodes/{select_food_plan.py → chatbot/select_food_plan.py} +54 -50
  32. chatbot/agents/nodes/{suggest_meal_node.py → chatbot/suggest_meal_node.py} +77 -72
  33. chatbot/agents/nodes/functions/__init__.py +0 -25
  34. chatbot/agents/nodes/functions/__pycache__/__init__.cpython-310.pyc +0 -0
  35. chatbot/agents/nodes/functions/__pycache__/enrich_food_with_nutrition.cpython-310.pyc +0 -0
  36. chatbot/agents/nodes/functions/__pycache__/enrich_meal_plan_with_nutrition.cpython-310.pyc +0 -0
  37. chatbot/agents/nodes/functions/__pycache__/enrich_meal_plan_with_nutrition_2.cpython-310.pyc +0 -0
  38. chatbot/agents/nodes/functions/__pycache__/generate_best_food_choice.cpython-310.pyc +0 -0
  39. chatbot/agents/nodes/functions/__pycache__/generate_food_plan.cpython-310.pyc +0 -0
  40. chatbot/agents/nodes/functions/__pycache__/generate_food_similarity.cpython-310.pyc +0 -0
  41. chatbot/agents/nodes/functions/__pycache__/generate_food_similarity_2.cpython-310.pyc +0 -0
  42. chatbot/agents/nodes/functions/__pycache__/generate_meal_plan.cpython-310.pyc +0 -0
  43. chatbot/agents/nodes/functions/__pycache__/generate_meal_plan_day_json.cpython-310.pyc +0 -0
  44. chatbot/agents/nodes/functions/__pycache__/generate_meal_plan_json_2.cpython-310.pyc +0 -0
  45. chatbot/agents/nodes/functions/__pycache__/get_user_profile.cpython-310.pyc +0 -0
  46. chatbot/agents/nodes/functions/enrich_food_with_nutrition.py +0 -54
  47. chatbot/agents/nodes/functions/enrich_meal_plan_with_nutrition.py +0 -72
  48. chatbot/agents/nodes/functions/enrich_meal_plan_with_nutrition_2.py +0 -72
  49. chatbot/agents/nodes/functions/generate_best_food_choice.py +0 -79
  50. chatbot/agents/nodes/functions/generate_food_plan.py +0 -42
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/__pycache__/food_similarity_graph.cpython-310.pyc CHANGED
Binary files a/chatbot/agents/graphs/__pycache__/food_similarity_graph.cpython-310.pyc and b/chatbot/agents/graphs/__pycache__/food_similarity_graph.cpython-310.pyc differ
 
chatbot/agents/graphs/__pycache__/meal_suggestion_graph.cpython-310.pyc CHANGED
Binary files a/chatbot/agents/graphs/__pycache__/meal_suggestion_graph.cpython-310.pyc and b/chatbot/agents/graphs/__pycache__/meal_suggestion_graph.cpython-310.pyc differ
 
chatbot/agents/graphs/chatbot_graph.py CHANGED
@@ -2,22 +2,33 @@ 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.classify_topic import classify_topic, route_by_topic
6
- from chatbot.agents.nodes.meal_identify import meal_identify
7
- from chatbot.agents.nodes.suggest_meal_node import suggest_meal_node
8
- from chatbot.agents.nodes.food_query import food_query
9
- from chatbot.agents.nodes.select_food_plan import select_food_plan
10
- from chatbot.agents.nodes.general_chat import general_chat
11
-
12
- def workflow_chatbot() -> StateGraph:
 
 
 
 
 
 
 
13
  workflow_chatbot = StateGraph(AgentState)
14
 
15
  workflow_chatbot.add_node("classify_topic", classify_topic)
16
  workflow_chatbot.add_node("meal_identify", meal_identify)
17
  workflow_chatbot.add_node("suggest_meal_node", suggest_meal_node)
18
- workflow_chatbot.add_node("food_query", food_query)
 
19
  workflow_chatbot.add_node("select_food_plan", select_food_plan)
 
 
20
  workflow_chatbot.add_node("general_chat", general_chat)
 
21
 
22
  workflow_chatbot.add_edge(START, "classify_topic")
23
 
@@ -26,18 +37,26 @@ def workflow_chatbot() -> StateGraph:
26
  route_by_topic,
27
  {
28
  "meal_identify": "meal_identify",
 
29
  "food_query": "food_query",
 
30
  "general_chat": "general_chat",
31
  }
32
  )
33
 
34
  workflow_chatbot.add_edge("meal_identify", "suggest_meal_node")
35
- workflow_chatbot.add_edge("suggest_meal_node", END)
 
36
 
37
- workflow_chatbot.add_edge("food_query", "select_food_plan")
38
  workflow_chatbot.add_edge("select_food_plan", END)
39
 
 
 
 
 
40
  workflow_chatbot.add_edge("general_chat", END)
41
 
42
- graph = workflow_chatbot.compile()
43
- return graph
 
 
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,
11
+ food_suggestion,
12
+ select_food_plan,
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
 
 
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
chatbot/agents/graphs/food_similarity_graph.py CHANGED
@@ -1,23 +1,21 @@
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.functions import get_user_profile, generate_food_similarity, generate_best_food_choice, enrich_food_with_nutrition
6
 
7
  def food_similarity_graph() -> StateGraph:
8
- workflow = StateGraph(AgentState)
9
 
10
- workflow.add_node("Get_User_Profile", get_user_profile)
11
- workflow.add_node("Generate_Food_Similarity", generate_food_similarity)
12
- workflow.add_node("Generate_Best_Food_Choice", generate_best_food_choice)
13
- workflow.add_node("Enrich_Food_With_Nutrition", enrich_food_with_nutrition)
14
 
15
- workflow.set_entry_point("Get_User_Profile")
 
 
 
 
16
 
17
- workflow.add_edge("Get_User_Profile", "Generate_Food_Similarity")
18
- workflow.add_edge("Generate_Food_Similarity", "Generate_Best_Food_Choice")
19
- workflow.add_edge("Generate_Best_Food_Choice", "Enrich_Food_With_Nutrition")
20
- workflow.add_edge("Enrich_Food_With_Nutrition", END)
21
-
22
- graph = workflow.compile()
23
- return graph
 
1
  from langgraph.graph import StateGraph, START, END
2
+ from chatbot.agents.states.state import SwapState
3
 
4
+ from chatbot.agents.nodes.app_functions import get_user_profile, find_replacement_candidates, calculate_top_options, llm_finalize_choice
 
5
 
6
  def food_similarity_graph() -> StateGraph:
7
+ swap_workflow = StateGraph(SwapState)
8
 
9
+ swap_workflow.add_node("get_profile", get_user_profile)
10
+ swap_workflow.add_node("find_candidates", find_replacement_candidates)
11
+ swap_workflow.add_node("optimize_select", calculate_top_options)
12
+ swap_workflow.add_node("select_meal", llm_finalize_choice)
13
 
14
+ swap_workflow.set_entry_point("get_profile")
15
+ swap_workflow.add_edge("get_profile", "find_candidates")
16
+ swap_workflow.add_edge("find_candidates", "optimize_select")
17
+ swap_workflow.add_edge("optimize_select", "select_meal")
18
+ swap_workflow.add_edge("select_meal", END)
19
 
20
+ app = swap_workflow.compile()
21
+ return app
 
 
 
 
 
chatbot/agents/graphs/meal_similarity_graph.py DELETED
@@ -1,23 +0,0 @@
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.functions import get_user_profile, generate_food_similarity_2, generate_meal_plan_json_2, enrich_meal_plan_with_nutrition_2
6
-
7
- def meal_similarity_graph() -> StateGraph:
8
- workflow = StateGraph(AgentState)
9
-
10
- workflow.add_node("Get_User_Profile", get_user_profile)
11
- workflow.add_node("Generate_Food_Similarity_2", generate_food_similarity_2)
12
- workflow.add_node("Generate_Meal_Plan_Json_2", generate_meal_plan_json_2)
13
- workflow.add_node("Enrich_Meal_Plan_With_Nutrition_2", enrich_meal_plan_with_nutrition_2)
14
-
15
- workflow.set_entry_point("Get_User_Profile")
16
-
17
- workflow.add_edge("Get_User_Profile", "Generate_Food_Similarity_2")
18
- workflow.add_edge("Generate_Food_Similarity_2", "Generate_Meal_Plan_Json_2")
19
- workflow.add_edge("Generate_Meal_Plan_Json_2", "Enrich_Meal_Plan_With_Nutrition_2")
20
- workflow.add_edge("Enrich_Meal_Plan_With_Nutrition_2", END)
21
-
22
- graph = workflow.compile()
23
- return graph
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
chatbot/agents/graphs/meal_suggestion_graph.py CHANGED
@@ -2,20 +2,21 @@ 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.functions import get_user_profile, generate_food_plan, generate_meal_plan
6
 
7
- def workflow_meal_suggestion() -> StateGraph:
8
  workflow = StateGraph(AgentState)
9
 
10
- workflow.add_node("Get_User_Profile", get_user_profile)
11
- workflow.add_node("Generate_Food_Plan", generate_food_plan)
12
- workflow.add_node("Generate_Meal_Plan", generate_meal_plan)
 
13
 
14
- workflow.set_entry_point("Get_User_Profile")
15
-
16
- workflow.add_edge("Get_User_Profile", "Generate_Food_Plan")
17
- workflow.add_edge("Generate_Food_Plan", "Generate_Meal_Plan")
18
- workflow.add_edge("Generate_Meal_Plan", END)
19
 
20
  graph = workflow.compile()
21
  return graph
 
2
  from chatbot.agents.states.state import AgentState
3
 
4
  # Import các node
5
+ from chatbot.agents.nodes.app_functions import get_user_profile, generate_food_candidates, select_menu_structure, optimize_portions_scipy
6
 
7
+ def meal_plan_graph():
8
  workflow = StateGraph(AgentState)
9
 
10
+ workflow.add_node("get_profile", get_user_profile)
11
+ workflow.add_node("generate_candidates", generate_food_candidates)
12
+ workflow.add_node("select_menu", select_menu_structure)
13
+ workflow.add_node("optimize_macros", optimize_portions_scipy)
14
 
15
+ workflow.set_entry_point("get_profile")
16
+ workflow.add_edge("get_profile", "generate_candidates")
17
+ workflow.add_edge("generate_candidates", "select_menu")
18
+ workflow.add_edge("select_menu", "optimize_macros")
19
+ workflow.add_edge("optimize_macros", END)
20
 
21
  graph = workflow.compile()
22
  return graph
chatbot/agents/graphs/meal_suggestion_json_graph.py DELETED
@@ -1,23 +0,0 @@
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.functions import get_user_profile, generate_food_plan, generate_meal_plan_day_json, enrich_meal_plan_with_nutrition
6
-
7
- def workflow_meal_suggestion_json() -> StateGraph:
8
- workflow = StateGraph(AgentState)
9
-
10
- workflow.add_node("Get_User_Profile", get_user_profile)
11
- workflow.add_node("Generate_Food_Plan", generate_food_plan)
12
- workflow.add_node("Generate_Meal_Plan_Day_Json", generate_meal_plan_day_json)
13
- workflow.add_node("Enrich_Meal_Plan_With_Nutrition", enrich_meal_plan_with_nutrition)
14
-
15
- workflow.set_entry_point("Get_User_Profile")
16
-
17
- workflow.add_edge("Get_User_Profile", "Generate_Food_Plan")
18
- workflow.add_edge("Generate_Food_Plan", "Generate_Meal_Plan_Day_Json")
19
- workflow.add_edge("Generate_Meal_Plan_Day_Json", "Enrich_Meal_Plan_With_Nutrition")
20
- workflow.add_edge("Enrich_Meal_Plan_With_Nutrition", END)
21
-
22
- graph = workflow.compile()
23
- return graph
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
chatbot/agents/nodes/__pycache__/classify_topic.cpython-310.pyc CHANGED
Binary files a/chatbot/agents/nodes/__pycache__/classify_topic.cpython-310.pyc and b/chatbot/agents/nodes/__pycache__/classify_topic.cpython-310.pyc differ
 
chatbot/agents/nodes/__pycache__/meal_identify.cpython-310.pyc CHANGED
Binary files a/chatbot/agents/nodes/__pycache__/meal_identify.cpython-310.pyc and b/chatbot/agents/nodes/__pycache__/meal_identify.cpython-310.pyc differ
 
chatbot/agents/nodes/__pycache__/suggest_meal_node.cpython-310.pyc CHANGED
Binary files a/chatbot/agents/nodes/__pycache__/suggest_meal_node.cpython-310.pyc and b/chatbot/agents/nodes/__pycache__/suggest_meal_node.cpython-310.pyc differ
 
chatbot/agents/nodes/app_functions/__init__.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .get_profile import get_user_profile
2
+
3
+ from .generate_candidates import generate_food_candidates
4
+ from .select_menu import select_menu_structure
5
+ from .optimize_macros import optimize_portions_scipy
6
+
7
+ from .find_candidates import find_replacement_candidates
8
+ from .optimize_select import calculate_top_options
9
+ from .select_meal import llm_finalize_choice
10
+
11
+ __all__ = [
12
+ "get_user_profile",
13
+
14
+ "generate_food_candidates",
15
+ "select_menu_structure",
16
+ "optimize_portions_scipy",
17
+
18
+ "find_replacement_candidates",
19
+ "calculate_top_options",
20
+ "llm_finalize_choice",
21
+ ]
chatbot/agents/nodes/app_functions/find_candidates.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from chatbot.agents.states.state import SwapState
2
+ from chatbot.agents.tools.food_retriever import food_retriever
3
+ from chatbot.agents.nodes.app_functions.generate_candidates import generate_numerical_constraints
4
+ import logging
5
+
6
+ # --- Cấu hình logging ---
7
+ logging.basicConfig(level=logging.INFO)
8
+ 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:
24
+ constraint_prompt += f"Phù hợp người bệnh: {health_status}. "
25
+ if diet_mode:
26
+ constraint_prompt += f"Chế độ: {diet_mode}."
27
+
28
+ # 1. Trích xuất ngữ cảnh từ món cũ
29
+ role = food_old.get("role", "main") # VD: main, side, carb
30
+ meal_type = food_old.get("assigned_meal", "trưa") # VD: trưa
31
+ old_name = food_old.get("name", "")
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}'. "
39
+ f"{constraint_prompt}"
40
+ )
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
48
+ try:
49
+ docs = food_retriever.invoke(query)
50
+ except Exception as e:
51
+ logger.info(f"⚠️ Lỗi Retriever: {e}")
52
+ return {"candidates": []}
53
+
54
+ # 4. Lọc sơ bộ (Python Filter)
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)
66
+
67
+ logger.info(f"📚 Tìm thấy {len(candidates)} ứng viên tiềm năng.")
68
+ return {"candidates": candidates}
chatbot/agents/nodes/app_functions/generate_candidates.py ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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)
8
+ logger = logging.getLogger(__name__)
9
+
10
+ def generate_food_candidates(state: AgentState):
11
+ logger.info("---NODE: RETRIEVAL CANDIDATES (ADVANCED PROFILE)---")
12
+ meals = state.get("meals_to_generate", [])
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
20
+
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):
82
+ """
83
+ Tạo chuỗi ràng buộc số liệu dinh dưỡng dựa trên cấu hình người dùng.
84
+ """
85
+ ratios = {"sáng": 0.25, "trưa": 0.40, "tối": 0.35}
86
+ meal_ratio = ratios.get(meal_type, 0.3)
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
+
104
+ config = critical_nutrients.get(item_name)
105
+ profile_key, db_key, unit, logic = config
106
+ daily_val = float(user_profile.get(profile_key, 0))
107
+ meal_target = daily_val * meal_ratio
108
+
109
+ if logic == 'max':
110
+ # Nới lỏng một chút ở bước tìm kiếm (120-130% target) để không bị lọc hết
111
+ threshold = round(meal_target * 1.3, 2)
112
+ constraints.append(f"{db_key} < {threshold}{unit}")
113
+
114
+ elif logic == 'range':
115
+ # Range rộng (50% - 150%) để bắt được nhiều món
116
+ min_val = round(meal_target * 0.5, 2)
117
+ max_val = round(meal_target * 1.5, 2)
118
+ constraints.append(f"{db_key} > {min_val}{unit} - {db_key} < {max_val}{unit}")
119
+
120
+ if not constraints: return ""
121
+ return ", ".join(constraints)
122
+
123
+ def rank_candidates(candidates, user_profile, meal_type):
124
+ """
125
+ Chấm điểm (Scoring) các món ăn dựa trên cấu hình dinh dưỡng chi ti��t.
126
+ """
127
+ print("---NODE: RANKING CANDIDATES (ADVANCED SCORING)---")
128
+
129
+ ratios = {"sáng": 0.25, "trưa": 0.40, "tối": 0.35}
130
+ meal_ratio = ratios.get(meal_type, 0.3)
131
+
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"),
160
+
161
+ # --- Khác ---
162
+ "Cholesterol": ("cholesterol", "cholesterol", "mg", "max"),
163
+ "Choline": ("choline", "choline", "mg", "min"),
164
+ "Caffeine": ("caffeine", "caffeine", "mg", "max"),
165
+ "Alcohol": ("alcohol", "alcohol", "g", "max"),
166
+ }
167
+
168
+ scored_list = []
169
+
170
+ for doc in candidates:
171
+ item = doc.metadata
172
+ score = 0
173
+ reasons = [] # Lưu lý do để debug hoặc giải thích cho user
174
+
175
+ # --- 1. CHẤM ĐIỂM NHÓM "BỔ SUNG" (BOOST) ---
176
+ # Logic: Càng nhiều càng tốt
177
+ for nutrient in user_profile.get('Bổ sung', []):
178
+ config = nutrient_config.get(nutrient)
179
+ if not config: continue
180
+
181
+ p_key, db_key, unit, logic = config
182
+
183
+ # Lấy giá trị thực tế trong món ăn và mục tiêu
184
+ val = float(item.get(db_key, 0))
185
+ daily_target = float(user_profile.get(p_key, 0))
186
+ meal_target = daily_target * meal_ratio
187
+
188
+ if meal_target == 0: continue
189
+
190
+ # Chấm điểm
191
+ # Nếu đạt > 50% target bữa -> +10 điểm
192
+ if val >= meal_target * 0.5:
193
+ score += 10
194
+ reasons.append(f"Giàu {nutrient}")
195
+ # Nếu đạt > 80% target -> +15 điểm (thưởng thêm)
196
+ if val >= meal_target * 0.8:
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:
204
+ config = nutrient_config.get(nutrient)
205
+ if not config: continue
206
+
207
+ p_key, db_key, unit, logic = config
208
+ val = float(item.get(db_key, 0))
209
+ daily_target = float(user_profile.get(p_key, 0))
210
+ meal_target = daily_target * meal_ratio
211
+
212
+ if meal_target == 0: continue
213
+
214
+ if logic == 'max':
215
+ # Nếu thấp hơn target -> +10 điểm (Tốt)
216
+ if val <= meal_target:
217
+ score += 10
218
+ # Nếu thấp hơn hẳn (chỉ bằng 50% target) -> +15 điểm (Rất an toàn)
219
+ if val <= meal_target * 0.5:
220
+ score += 5
221
+ # Nếu vượt quá target -> -10 điểm (Phạt)
222
+ if val > meal_target:
223
+ score -= 10
224
+
225
+ elif logic == 'range':
226
+ # Logic cho Kali/Protein: Tốt nhất là nằm trong khoảng, không thấp quá, không cao quá
227
+ min_safe = meal_target * 0.5
228
+ max_safe = meal_target * 1.5
229
+
230
+ if min_safe <= val <= max_safe:
231
+ score += 10 # Nằm trong vùng an toàn
232
+ elif val > max_safe:
233
+ score -= 10 # Cao quá (nguy hiểm cho thận)
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ả
247
+ item_copy = item.copy()
248
+ item_copy["health_score"] = score
249
+ item_copy["score_reason"] = ", ".join(reasons[:3]) # Chỉ lấy 3 lý do chính
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
257
+ # logger.info("🏆 Top 3 Món Tốt Nhất (Sau khi chấm điểm):")
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
chatbot/agents/nodes/app_functions/get_profile.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+
3
+ from chatbot.agents.states.state import AgentState
4
+ from chatbot.utils.user_profile import get_user_by_id
5
+ from chatbot.utils.restriction import get_restrictions
6
+
7
+ # --- Cấu hình logging ---
8
+ logging.basicConfig(level=logging.INFO)
9
+ logger = logging.getLogger(__name__)
10
+
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}
chatbot/agents/nodes/app_functions/optimize_macros.py ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from chatbot.agents.states.state import AgentState
3
+ import numpy as np
4
+ from scipy.optimize import minimize
5
+
6
+ # --- Cấu hình logging ---
7
+ logging.basicConfig(level=logging.INFO)
8
+ logger = logging.getLogger(__name__)
9
+
10
+ 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)),
24
+ float(profile.get("totalfat", 43)),
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)
35
+ # Ví dụ: Nếu chỉ có bữa Trưa -> Target = Daily * 0.4
36
+ # Nếu có Trưa + Tối -> Target = Daily * (0.4 + 0.35)
37
+ active_target = np.zeros(4)
38
+ active_ratios_sum = 0
39
+
40
+ for m in ["sáng", "trưa", "tối"]:
41
+ if m in generated_meals:
42
+ active_target += daily_targets * meal_ratios[m]
43
+ active_ratios_sum += meal_ratios[m]
44
+
45
+ # Fallback: Nếu không xác định được bữa nào, dùng Target Ngày
46
+ if np.sum(active_target) == 0:
47
+ active_target = daily_targets
48
+
49
+ logger.info(f" 🎯 Mục tiêu tối ưu hóa (Active Target): {active_target.astype(int)}")
50
+
51
+ # --- BƯỚC 2: THIẾT LẬP MA TRẬN & BOUNDS ---
52
+ matrix = []
53
+ bounds = []
54
+ meal_indices = {"sáng": [], "trưa": [], "tối": []}
55
+
56
+ # Tính target riêng từng bữa để dùng cho Distribution Loss
57
+ target_kcal_per_meal = {
58
+ k: daily_targets[0] * v for k, v in meal_ratios.items()
59
+ }
60
+
61
+ for i, dish in enumerate(menu):
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
+
70
+ # Logic Bounds Thông minh
71
+ current_kcal = nutrients[0]
72
+ t_meal_name = dish.get("assigned_meal", "").lower()
73
+ t_meal_target = target_kcal_per_meal.get(t_meal_name, 500)
74
+
75
+ # Nếu 1 món chiếm > 90% Kcal mục tiêu của bữa đó -> Phải cho giảm sâu
76
+ if current_kcal > (t_meal_target * 0.9):
77
+ bounds.append((0.3, 1.0))
78
+ elif "solver_bounds" in dish:
79
+ bounds.append(dish["solver_bounds"])
80
+ else:
81
+ bounds.append((0.5, 1.5))
82
+
83
+ if "sáng" in t_meal_name: meal_indices["sáng"].append(i)
84
+ elif "trưa" in t_meal_name: meal_indices["trưa"].append(i)
85
+ elif "tối" in t_meal_name: meal_indices["tối"].append(i)
86
+
87
+ matrix = np.array(matrix).T
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}
138
+
139
+ for i, dish in enumerate(menu):
140
+ ratio = optimized_portions[i]
141
+ final_dish = dish.copy()
142
+
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()
158
+ if "sáng" in m_type: achieved_meal_kcal["sáng"] += final_dish["final_kcal"]
159
+ elif "trưa" in m_type: achieved_meal_kcal["trưa"] += final_dish["final_kcal"]
160
+ elif "tối" in m_type: achieved_meal_kcal["tối"] += final_dish["final_kcal"]
161
+
162
+ # --- BƯỚC 7: BÁO CÁO KẾT QUẢ ---
163
+ logger.info("\n 📊 BÁO CÁO TỐI ƯU HÓA CHI TIẾT:")
164
+
165
+ headers = ["Chỉ số", "Mục tiêu (Bữa)", "Kết quả", "Độ lệch"]
166
+ row_format = " | {:<12} | {:<15} | {:<15} | {:<15} |"
167
+ logger.info(" " + "-"*65)
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):
175
+ t_val = int(active_target[i]) # So sánh với Active Target
176
+ r_val = int(total_stats[i])
177
+ diff = r_val - t_val
178
+ diff_str = f"{diff:+d} {units[i]}"
179
+
180
+ status = ""
181
+ percent_diff = abs(diff) / (t_val + 1e-5)
182
+ # Nếu weight bị giảm về 0.01 thì không cảnh báo lỗi nữa (vì đã chấp nhận bỏ qua)
183
+ if percent_diff > 0.15 and adaptive_weights[i] > 0.1: status = "⚠️"
184
+ else: status = "✅"
185
+
186
+ logger.info(row_format.format(
187
+ labels[i],
188
+ f"{t_val} {units[i]}",
189
+ f"{r_val} {units[i]}",
190
+ f"{diff_str} {status}"
191
+ ))
192
+ logger.info(" " + "-"*65)
193
+
194
+ logger.info("\n ⚖️ PHÂN BỔ TỪNG BỮA (Kcal):")
195
+ for meal in ["sáng", "trưa", "tối"]:
196
+ if meal in generated_meals:
197
+ t_meal = int(target_kcal_per_meal[meal])
198
+ r_meal = int(achieved_meal_kcal[meal])
199
+ logger.info(f" - {meal.capitalize():<5}: Đạt {r_meal} / {t_meal} Kcal")
200
+
201
+ return {
202
+ "final_menu": final_menu,
203
+ "user_profile": profile
204
+ }
chatbot/agents/nodes/app_functions/optimize_select.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from scipy.optimize import minimize_scalar
2
+ from chatbot.agents.states.state import SwapState
3
+ import numpy as np
4
+ import logging
5
+
6
+ logging.basicConfig(level=logging.INFO)
7
+ 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
+
72
+ logger.info(f"📊 Scipy đã lọc ra {len(top_10)} ứng viên tiềm năng.")
73
+ for item in top_10:
74
+ logger.info(f" - {item['name']} (Scale x{item['portion_scale']} | Loss: {item['optimization_loss']})")
75
+
76
+ return {"top_candidates": top_10}
chatbot/agents/nodes/app_functions/select_meal.py ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain_core.pydantic_v1 import BaseModel, Field
2
+ from chatbot.models.llm_setup import llm
3
+ from chatbot.agents.states.state import SwapState
4
+ import logging
5
+
6
+ logging.basicConfig(level=logging.INFO)
7
+ logger = logging.getLogger(__name__)
8
+
9
+ class ChefDecision(BaseModel):
10
+ # Thay đổi tên trường cho rõ nghĩa
11
+ selected_meal_id: int = Field(description="ID (meal_id) của món ăn được chọn từ danh sách")
12
+ reason: str = Field(description="Lý do ẩm thực ngắn gọn")
13
+
14
+ def llm_finalize_choice(state: SwapState):
15
+ logger.info("---NODE: LLM FINAL SELECTION (BY REAL MEAL_ID)---")
16
+ top_candidates = state["top_candidates"]
17
+ food_old = state["food_old"]
18
+
19
+ if not top_candidates: return {"best_replacement": None}
20
+
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
+
33
+ # 2. Prompt cập nhật
34
+ system_prompt = f"""
35
+ Bạn là Bếp trưởng. Người dùng muốn đổi món '{food_old.get('name')}'.
36
+ Dưới đây là các ứng viên thay thế:
37
+
38
+ {options_text}
39
+
40
+ NHIỆM VỤ:
41
+ 1. Chọn ra 1 món thay thế tốt nhất về mặt ẩm thực.
42
+ 2. Trả về chính xác ID (số trong ngoặc vuông []) của món đó.
43
+ """
44
+
45
+ # 3. Gọi LLM
46
+ try:
47
+ llm_structured = llm.with_structured_output(ChefDecision)
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
64
+
65
+ # Fallback an toàn
66
+ if not selected_full_candidate:
67
+ logger.info(f"⚠️ ID {target_id} không tồn tại trong list. Chọn món Top 1.")
68
+ selected_full_candidate = top_candidates[0]
69
+
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
96
+ logger.info("\n 📊 BẢNG SO SÁNH THAY THẾ:")
97
+ headers = ["Chỉ số", "Món Cũ (Gốc)", "Món Mới (Re-scale)", "Chênh lệch"]
98
+ row_fmt = " | {:<10} | {:<15} | {:<20} | {:<12} |"
99
+
100
+ logger.info(" " + "-"*68)
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}"
108
+
109
+ # Đánh dấu màu (Logic text)
110
+ status = "✅"
111
+ # Nếu lệch > 20% thì cảnh báo
112
+ if old_val > 0 and abs(diff)/old_val > 0.2: status = "⚠️"
113
+
114
+ logger.info(row_fmt.format(
115
+ label,
116
+ f"{old_val:.0f} {unit}",
117
+ f"{new_val:.0f} {unit} (x{scale} suất)",
118
+ f"{diff_str} {status}"
119
+ ))
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
+
127
+ logger.info(f"✅ Chef Selected: ID {selected_full_candidate['meal_id']} - {selected_full_candidate['name']}")
128
+
129
+ return {"best_replacement": selected_full_candidate}
chatbot/agents/nodes/app_functions/select_menu.py ADDED
@@ -0,0 +1,237 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain_core.pydantic_v1 import BaseModel, Field
2
+ from typing import Literal, List
3
+ from collections import defaultdict
4
+ import logging
5
+ from chatbot.agents.states.state import AgentState
6
+ from chatbot.models.llm_setup import llm
7
+
8
+ # --- Cấu hình logging ---
9
+ logging.basicConfig(level=logging.INFO)
10
+ logger = logging.getLogger(__name__)
11
+
12
+ # --- DATA MODELS ---
13
+ class SelectedDish(BaseModel):
14
+ name: str = Field(description="Tên món ăn chính xác trong danh sách")
15
+ meal_type: str = Field(description="Bữa ăn (sáng/trưa/tối)")
16
+ role: Literal["main", "carb", "side"] = Field(
17
+ description="Vai trò: 'main' (Món mặn/Đạm), 'carb' (Cơm/Tinh bột), 'side' (Rau/Canh)"
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:
145
+ menu_by_meal[dish.meal_type.lower()].append(dish)
146
+ meal_order = ["sáng", "trưa", "tối"]
147
+ for meal in meal_order:
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:
164
+ if choice.name in candidate_map:
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
+
198
+ if final_role == "carb":
199
+ # Cơm/Bún thuần: Cho phép co dãn cực mạnh để bù Kcal
200
+ lower_bound, upper_bound = 0.4, 3.0
201
+
202
+ elif final_role == "side":
203
+ # Rau/Canh: Co dãn rộng để bù thể tích ăn
204
+ lower_bound, upper_bound = 0.5, 2.0
205
+
206
+ elif final_role == "main":
207
+ # Món mặn: Co dãn vừa phải để giữ hương vị
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)
233
+ selected_full_info.append(dish_data)
234
+
235
+ return {
236
+ "selected_structure": selected_full_info,
237
+ }
chatbot/agents/nodes/chatbot/__init__.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
5
+ from .select_food_plan import select_food_plan
6
+ from .general_chat import general_chat
7
+ 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",
15
+ "route_by_topic",
16
+ "meal_identify",
17
+ "suggest_meal_node",
18
+ "food_query",
19
+ "select_food_plan",
20
+ "general_chat",
21
+ "generate_final_response",
22
+ "food_suggestion",
23
+ "policy",
24
+ "select_food",
25
+ ]
chatbot/agents/nodes/{classify_topic.py → chatbot/classify_topic.py} RENAMED
@@ -1,60 +1,71 @@
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
-
7
- class Topic(BaseModel):
8
- name: str = Field(
9
- description=(
10
- "Tên chủ đề mà người dùng đang hỏi. "
11
- "Các giá trị hợp lệ: 'nutrition_analysis', 'meal_suggestion', 'general_chat'."
12
- )
13
- )
14
-
15
- def classify_topic(state: AgentState):
16
- print("---CLASSIFY TOPIC---")
17
- llm_with_structure_op = llm.with_structured_output(Topic)
18
-
19
- prompt = PromptTemplate(
20
- template="""
21
- 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.
22
-
23
- Nhiệm vụ:
24
- - Phân loại câu hỏi vào một trong các nhóm:
25
- 1. "meal_suggestion": khi người dùng muốn gợi ý thực đơn cho một bữa ăn, khẩu phần, hoặc chế độ ăn (chỉ cho bữa ăn, không cho món ăn đơn lẻ).
26
- 2. "food_query": khi người dùng tìm kiếm, gợi ý một món ăn hoặc muốn biết thành phần dinh dưỡng của món ăn hoặc khẩu phần cụ thể.
27
- 3. "general_chat": khi câu hỏi không thuộc hai nhóm trên.
28
-
29
- Câu hỏi người dùng: {question}
30
-
31
- Hãy trả lời dưới dạng JSON phù hợp với schema sau:
32
- {format_instructions}
33
- """
34
- )
35
-
36
- messages = state["messages"]
37
- user_message = messages[-1].content if messages else state.question
38
-
39
- format_instructions = json.dumps(llm_with_structure_op.output_schema.model_json_schema(), ensure_ascii=False, indent=2)
40
-
41
- chain = prompt | llm_with_structure_op
42
-
43
- topic_result = chain.invoke({
44
- "question": user_message,
45
- "format_instructions": format_instructions
46
- })
47
-
48
- print("Topic:", topic_result.name)
49
-
50
- return {"topic": topic_result.name}
51
-
52
- def route_by_topic(state: AgentState):
53
- topic = state["topic"]
54
- if topic == "meal_suggestion":
55
- return "meal_identify"
56
- elif topic == "food_query":
57
- return "food_query"
58
- else:
59
- return "general_chat"
 
 
 
 
 
 
 
 
 
 
 
60
 
 
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 ---
9
+ logging.basicConfig(level=logging.INFO)
10
+ logger = logging.getLogger(__name__)
11
+
12
+ class Topic(BaseModel):
13
+ name: str = Field(
14
+ description=(
15
+ "Tên chủ đề mà người dùng đang hỏi. "
16
+ "Các giá trị hợp lệ: 'meal_suggestion', 'food_suggestion', food_query, 'policy', 'general_chat'."
17
+ )
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 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
 
chatbot/agents/nodes/chatbot/food_query.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from chatbot.agents.states.state import AgentState
2
+ from chatbot.agents.tools.food_retriever import query_constructor, docsearch
3
+ from langchain.retrievers.self_query.elasticsearch import ElasticsearchTranslator
4
+ from langchain.retrievers.self_query.base import SelfQueryRetriever
5
+ import logging
6
+
7
+ # --- Cấu hình logging ---
8
+ logging.basicConfig(level=logging.INFO)
9
+ logger = logging.getLogger(__name__)
10
+
11
+ def food_query(state: AgentState):
12
+ logger.info("---FOOD QUERY---")
13
+
14
+ messages = state["messages"]
15
+ user_message = messages[-1].content if messages else state.question
16
+
17
+ suggested_meals = []
18
+
19
+ prompt = f"""
20
+ Câu hỏi: "{user_message}".
21
+ Hãy tìm các món ăn phù hợp với yêu cầu này.
22
+ """
23
+
24
+ query_ans = query_constructor.invoke(prompt)
25
+ food_retriever = SelfQueryRetriever(
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
+
33
+ foods = food_retriever.invoke(prompt)
34
+ logger.info(f"🔍 Kết quả truy vấn: ")
35
+ for i, food in enumerate(foods):
36
+ logger.info(f"{i} - {food.metadata['name']}")
37
+ suggested_meals.append(food)
38
+
39
+ return {"suggested_meals": suggested_meals}
chatbot/agents/nodes/{food_query.py → chatbot/food_suggestion.py} RENAMED
@@ -1,31 +1,35 @@
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 food_retriever, food_retriever_top3, query_constructor
4
-
5
-
6
- def food_query(state: AgentState):
7
- print("---FOOD QUERY---")
8
-
9
- user_id = state.get("user_id", {})
10
- messages = state["messages"]
11
- user_message = messages[-1].content if messages else state.question
12
-
13
- user_profile = get_user_by_id(user_id)
14
-
15
- suggested_meals = []
16
-
17
- prompt = f"""
18
- Người dùng có khẩu phần: {user_profile["khẩu phần"]}.
19
- Câu hỏi: "{user_message}".
20
- Hãy tìm các món ăn phù hợp với khẩu phần và yêu cầu này, cho phép sai lệch không quá 20%.
21
- """
22
-
23
- query_ans = query_constructor.invoke(prompt)
24
- print(f"🔍 Dạng truy vấn: {food_retriever.structured_query_translator.visit_structured_query(structured_query=query_ans)}")
25
- foods = food_retriever_top3.invoke(prompt)
26
- print(f"🔍 Kết quả truy vấn: ")
27
- for i, food in enumerate(foods):
28
- print(f"{i} - {food.metadata['name']}")
29
- suggested_meals.append(food)
30
-
 
 
 
 
31
  return {"suggested_meals": suggested_meals, "user_profile": user_profile}
 
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
+
6
+ # --- Cấu hình logging ---
7
+ logging.basicConfig(level=logging.INFO)
8
+ logger = logging.getLogger(__name__)
9
+
10
+ def food_suggestion(state: AgentState):
11
+ logger.info("---FOOD QUERY SUGGESTION---")
12
+
13
+ user_id = state.get("user_id", {})
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
+
21
+ prompt = f"""
22
+ Người dùng có khẩu phần: {user_profile["diet"]}.
23
+ Câu hỏi: "{user_message}".
24
+ Hãy tìm các món ăn phù hợp với khẩu phần và yêu cầu này, cho phép sai lệch không quá 20%.
25
+ """
26
+
27
+ query_ans = query_constructor.invoke(prompt)
28
+ logger.info(f"🔍 Dạng truy vấn: {food_retriever.structured_query_translator.visit_structured_query(structured_query=query_ans)}")
29
+ foods = food_retriever.invoke(prompt)
30
+ logger.info(f"🔍 Kết quả truy vấn: ")
31
+ for i, food in enumerate(foods):
32
+ logger.info(f"{i} - {food.metadata['name']}")
33
+ suggested_meals.append(food)
34
+
35
  return {"suggested_meals": suggested_meals, "user_profile": user_profile}
chatbot/agents/nodes/{general_chat.py → chatbot/general_chat.py} RENAMED
@@ -1,40 +1,45 @@
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
-
6
- def general_chat(state: AgentState):
7
- print("---GENERAL CHAT---")
8
-
9
- user_id = state.get("user_id", {})
10
- messages = state["messages"]
11
- user_message = messages[-1].content if messages else state.question
12
-
13
- user_profile = get_user_by_id(user_id)
14
-
15
- system_prompt = f"""
16
- Bạn một chuyên gia dinh dưỡng và ẩm thực AI.
17
- Hãy trả lời các câu hỏi về:
18
- - món ăn, thành phần, dinh dưỡng, calo, protein, chất béo, carb,
19
- - chế độ ăn (ăn chay, keto, giảm cân, tăng cơ...),
20
- - sức khỏe, lối sống, chế độ tập luyện liên quan đến ăn uống.
21
- Một số thông tin về người dùng thể dùng đến như sau:
22
- - Tổng năng lượng mục tiêu: {user_profile['kcal']} kcal/ngày
23
- - Protein: {user_profile['protein']}g
24
- - Chất béo (lipid): {user_profile['lipid']}g
25
- - Carbohydrate: {user_profile['carbohydrate']}g
26
- - Chế độ ăn: {user_profile['khẩu phần']}
27
- Không trả lời các câu hỏi ngoài chủ đề này.
28
- Giải thích ngắn gọn, tự nhiên, rõ ràng.
29
- """
30
-
31
- messages = [
32
- SystemMessage(content=system_prompt),
33
- HumanMessage(content=user_message),
34
- ]
35
-
36
- response = llm.invoke(messages)
37
-
38
- print(response.content if hasattr(response, "content") else response)
39
-
 
 
 
 
 
40
  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.user_profile import get_user_by_id
5
+ import logging
6
+
7
+ # --- Cấu hình logging ---
8
+ logging.basicConfig(level=logging.INFO)
9
+ logger = logging.getLogger(__name__)
10
+
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 một chuyên gia dinh dưỡng ẩ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 có 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}
chatbot/agents/nodes/chatbot/generate_final_response.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ---
6
+ logging.basicConfig(level=logging.INFO)
7
+ 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}
chatbot/agents/nodes/{meal_identify.py → chatbot/meal_identify.py} RENAMED
@@ -1,64 +1,55 @@
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
- from typing import List
7
-
8
- class MealIntent(BaseModel):
9
- intent: str = Field(
10
- description=(
11
- "Loại yêu cầu của người dùng, có thể là:\n"
12
- "- 'full_day_meal': khi người dùng chưa ăn bữa nào và muốn gợi ý thực đơn cho cả ngày.\n"
13
- "- 'not_full_day_meal': khi người dùng đã ăn một vài bữa và muốn gợi ý một bữa cụ thể hoặc các bữa còn lại."
14
- )
15
- )
16
- meals_to_generate: List[str] = Field(
17
- description="Danh sách các bữa được người dùng muốn gợi ý: ['sáng', 'trưa', 'tối']."
18
- )
19
-
20
-
21
- def meal_identify(state: AgentState):
22
- print("---MEAL IDENTIFY---")
23
-
24
- llm_with_structure_op = llm.with_structured_output(MealIntent)
25
-
26
- # Lấy câu hỏi mới nhất từ lịch sử hội thoại
27
- messages = state["messages"]
28
- user_message = messages[-1].content if messages else state.question
29
-
30
- format_instructions = json.dumps(llm_with_structure_op.output_schema.model_json_schema(), ensure_ascii=False, indent=2)
31
-
32
- prompt = PromptTemplate(
33
- template="""
34
- 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.
35
-
36
- Dựa trên câu hỏi của người dùng, hãy xác định:
37
- 1. Người dùng muốn gợi ý cho **cả ngày**, **một hoặc một vài bữa cụ thể**.
38
- 2. Danh sách các bữa người dùng muốn gợi ý (nếu có).
39
-
40
- Quy tắc:
41
- - Nếu người dùng muốn gợi ý thực đơn cho cả ngày → intent = "full_day_meal".
42
- - Nếu họ nói đã ăn một bữa nào đó, muốn gợi ý một hoặc các bữa còn lại → intent = "not_full_day_meal".
43
- - Các bữa người dùng có thể muốn gợi ý: ["sáng", "trưa", "tối"].
44
-
45
- Câu hỏi người dùng: {question}
46
-
47
- Hãy xuất kết quả dưới dạng JSON theo schema sau:
48
- {format_instructions}
49
- """
50
- )
51
-
52
- chain = prompt | llm_with_structure_op
53
-
54
- result = chain.invoke({
55
- "question": user_message,
56
- "format_instructions": format_instructions
57
- })
58
-
59
- print("Bữa cần gợi ý: " + ", ".join(result.meals_to_generate))
60
-
61
- return {
62
- "meal_intent": result.intent,
63
- "meals_to_generate": result.meals_to_generate,
64
- }
 
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
+ from typing import List
7
+ import logging
8
+
9
+ # --- Cấu hình logging ---
10
+ logging.basicConfig(level=logging.INFO)
11
+ logger = logging.getLogger(__name__)
12
+
13
+ class MealIntent(BaseModel):
14
+ meals_to_generate: List[str] = Field(
15
+ description="Danh sách các bữa được người dùng muốn gợi ý: ['sáng', 'trưa', 'tối']."
16
+ )
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
+ }
 
 
 
 
 
 
 
 
 
chatbot/agents/nodes/chatbot/policy.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ---
7
+ logging.basicConfig(level=logging.INFO)
8
+ logger = logging.getLogger(__name__)
9
+
10
+ def policy(state: AgentState):
11
+ logger.info("---POLICY---")
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}
chatbot/agents/nodes/chatbot/select_food.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ---
6
+ logging.basicConfig(level=logging.INFO)
7
+ logger = logging.getLogger(__name__)
8
+
9
+ def select_food(state: AgentState):
10
+ print("---NODE: ANALYZE & ANSWER---")
11
+
12
+ suggested_meals = state["suggested_meals"]
13
+
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" - Mô 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
+
41
+ YÊU CẦU TRẢ LỜI:
42
+ 1. Dựa vào "Dữ liệu tìm được", hãy trả lời câu hỏi của người dùng một cách trực tiếp.
43
+ 2. Nếu người dùng hỏi thông tin (VD: "Phở bò bao nhiêu calo?"), hãy lấy số liệu chính xác từ dữ liệu trên để trả lời.
44
+ 3. Nếu không có dữ liệu phù hợp trong danh sách, hãy thành thật nói "Tôi không tìm thấy thông tin chính xác về món này trong hệ thống".
45
+
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}
chatbot/agents/nodes/{select_food_plan.py → chatbot/select_food_plan.py} RENAMED
@@ -1,51 +1,55 @@
1
- from chatbot.agents.states.state import AgentState
2
- from chatbot.models.llm_setup import llm
3
-
4
- def select_food_plan(state: AgentState):
5
- print("---SELECT FOOD PLAN---")
6
-
7
- user_profile = state["user_profile"]
8
- suggested_meals = state["suggested_meals"]
9
- messages = state["messages"]
10
- user_message = messages[-1].content if messages else state.question
11
-
12
- suggested_meals_text = "\n".join(
13
- f"{i+1}. {doc.metadata.get('name', 'Không rõ')} - "
14
- f"{doc.metadata.get('kcal', '?')} kcal, "
15
- f"Protein:{doc.metadata.get('protein', '?')}g, "
16
- f"Chất béo:{doc.metadata.get('lipid', '?')}g, "
17
- f"Carbohydrate:{doc.metadata.get('carbohydrate', '?')}g\n"
18
- f" tả: {doc.page_content}"
19
- for i, doc in enumerate(suggested_meals)
20
- )
21
-
22
- prompt = f"""
23
- Bạn chuyên gia dinh dưỡng AI.
24
- 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:
25
- - Tổng năng lượng mục tiêu: {user_profile['kcal']} kcal/ngày
26
- - Protein: {user_profile['protein']}g
27
- - Chất béo (lipid): {user_profile['lipid']}g
28
- - Carbohydrate: {user_profile['carbohydrate']}g
29
- - Chế độ ăn: {user_profile['khẩu phần']}
30
-
31
- Câu hỏi của người dùng: "{user_message}"
32
-
33
- Danh sách món ăn hiện có để chọn:
34
- {suggested_meals_text}
35
-
36
- Yêu cầu:
37
- 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.
38
- 2. Nếu không có món nào phù hợp, hãy trả về:
39
- "Không tìm thấy món phù hợp trong danh sách hiện có."
40
- 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.
41
- 4. Nếu nhiều món gần giống nhau, hãy chọn món năng lượng thành phần dinh dưỡng gần nhất với mục tiêu người dùng.
42
- """
43
-
44
- print("Prompt:")
45
- print(prompt)
46
-
47
- result = llm.invoke(prompt)
48
-
49
- print(result.content if hasattr(result, "content") else result)
50
-
 
 
 
 
51
  return {"response": result.content}
 
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 ---
6
+ logging.basicConfig(level=logging.INFO)
7
+ logger = logging.getLogger(__name__)
8
+
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 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 để 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 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}
chatbot/agents/nodes/{suggest_meal_node.py → chatbot/suggest_meal_node.py} RENAMED
@@ -1,72 +1,77 @@
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
-
6
- def suggest_meal_node(state: AgentState):
7
- print("---SUGGEST MEAL NODE---")
8
-
9
- # 🧠 Lấy dữ liệu từ state
10
- user_id = state.get("user_id", 0)
11
- question = state.get("messages")
12
- meals_to_generate = state.get("meals_to_generate", [])
13
-
14
- # 🧩 Chuẩn bị prompt tả yêu cầu
15
- system_prompt = """
16
- Bạn một chuyên gia gợi ý thực đơn AI.
17
- Bạn không được tự trả lời hay đặt câu hỏi thêm.
18
- 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'.
19
- với các tham số:
20
- - user_id: ID người dùng hiện tại
21
- - question: nội dung câu hỏi họ vừa hỏi
22
- - meals_to_generate: danh sách các bữa cần sinh thực đơn (nếu có)
23
-
24
- 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.
25
- """
26
-
27
- user_prompt = f"""
28
- Người dùng có ID: {user_id}
29
- Yêu cầu: "{question}"
30
- Danh sách các bữa cần gợi ý: {meals_to_generate}
31
- """
32
-
33
- # 🚀 Gọi LLM và Tools
34
- tools = [daily_meal_suggestion]
35
- llm_with_tools = llm.bind_tools(tools)
36
-
37
- response = llm_with_tools.invoke(
38
- [
39
- SystemMessage(content=system_prompt),
40
- HumanMessage(content=user_prompt)
41
- ]
42
- )
43
-
44
- print("===== DEBUG =====")
45
- print("Response type:", type(response))
46
- print("Tool calls:", getattr(response, "tool_calls", None))
47
- print("Message content:", response.content)
48
- print("=================")
49
-
50
- if isinstance(response, AIMessage) and response.tool_calls:
51
- tool_call = response.tool_calls[0]
52
- tool_name = tool_call["name"]
53
- tool_args = tool_call["args"]
54
- tool_call_id = tool_call["id"]
55
-
56
- print(f"👉 Executing tool: {tool_name} with args: {tool_args}")
57
-
58
- # Bổ sung tham số nếu LLM quên
59
- tool_args.setdefault("user_id", user_id)
60
- tool_args.setdefault("question", question)
61
- tool_args.setdefault("meals_to_generate", meals_to_generate)
62
-
63
- if tool_name == "daily_meal_suggestion":
64
- result = daily_meal_suggestion.invoke(tool_args)
65
- elif tool_name == "fallback":
66
- result = {"message": "Không có tool phù hợp.", "reason": tool_args.get("reason", "")}
67
- else:
68
- result = {"message": f"Tool '{tool_name}' chưa được định nghĩa."}
69
-
70
- tool_message = ToolMessage(content=str(result), name=tool_name, tool_call_id=tool_call_id)
71
- return {"messages": state["messages"] + [response, tool_message], "response": result}
72
- return {"response": "Lỗi!!!"}
 
 
 
 
 
 
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
+
7
+ # --- Cấu hình logging ---
8
+ logging.basicConfig(level=logging.INFO)
9
+ logger = logging.getLogger(__name__)
10
+
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 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 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 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!!!"}
chatbot/agents/nodes/functions/__init__.py DELETED
@@ -1,25 +0,0 @@
1
- from .enrich_food_with_nutrition import enrich_food_with_nutrition
2
- from .enrich_meal_plan_with_nutrition import enrich_meal_plan_with_nutrition
3
- from .enrich_meal_plan_with_nutrition_2 import enrich_meal_plan_with_nutrition_2
4
- from .generate_best_food_choice import generate_best_food_choice
5
- from .generate_food_plan import generate_food_plan
6
- from .generate_food_similarity import generate_food_similarity
7
- from .generate_food_similarity_2 import generate_food_similarity_2
8
- from .generate_meal_plan_day_json import generate_meal_plan_day_json
9
- from .generate_meal_plan_json_2 import generate_meal_plan_json_2
10
- from .generate_meal_plan import generate_meal_plan
11
- from .get_user_profile import get_user_profile
12
-
13
- __all__ = [
14
- "enrich_food_with_nutrition",
15
- "enrich_meal_plan_with_nutrition",
16
- "enrich_meal_plan_with_nutrition_2",
17
- "generate_best_food_choice",
18
- "generate_food_plan",
19
- "generate_food_similarity",
20
- "generate_food_similarity_2",
21
- "generate_meal_plan_day_json",
22
- "generate_meal_plan_json_2",
23
- "generate_meal_plan",
24
- "get_user_profile",
25
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
chatbot/agents/nodes/functions/__pycache__/__init__.cpython-310.pyc DELETED
Binary file (806 Bytes)
 
chatbot/agents/nodes/functions/__pycache__/enrich_food_with_nutrition.cpython-310.pyc DELETED
Binary file (1.53 kB)
 
chatbot/agents/nodes/functions/__pycache__/enrich_meal_plan_with_nutrition.cpython-310.pyc DELETED
Binary file (1.78 kB)
 
chatbot/agents/nodes/functions/__pycache__/enrich_meal_plan_with_nutrition_2.cpython-310.pyc DELETED
Binary file (1.79 kB)
 
chatbot/agents/nodes/functions/__pycache__/generate_best_food_choice.cpython-310.pyc DELETED
Binary file (2.97 kB)
 
chatbot/agents/nodes/functions/__pycache__/generate_food_plan.cpython-310.pyc DELETED
Binary file (1.93 kB)
 
chatbot/agents/nodes/functions/__pycache__/generate_food_similarity.cpython-310.pyc DELETED
Binary file (1.7 kB)
 
chatbot/agents/nodes/functions/__pycache__/generate_food_similarity_2.cpython-310.pyc DELETED
Binary file (1.91 kB)
 
chatbot/agents/nodes/functions/__pycache__/generate_meal_plan.cpython-310.pyc DELETED
Binary file (3.31 kB)
 
chatbot/agents/nodes/functions/__pycache__/generate_meal_plan_day_json.cpython-310.pyc DELETED
Binary file (3.81 kB)
 
chatbot/agents/nodes/functions/__pycache__/generate_meal_plan_json_2.cpython-310.pyc DELETED
Binary file (3.74 kB)
 
chatbot/agents/nodes/functions/__pycache__/get_user_profile.cpython-310.pyc DELETED
Binary file (1.69 kB)
 
chatbot/agents/nodes/functions/enrich_food_with_nutrition.py DELETED
@@ -1,54 +0,0 @@
1
- import logging
2
- from typing import List, Dict, Any
3
-
4
- from chatbot.agents.states.state import AgentState
5
- from chatbot.agents.tools.food_retriever import query_constructor, food_retriever
6
- from langgraph.graph import END, StateGraph
7
- from chatbot.models.llm_setup import llm
8
- from langchain.tools import tool
9
- from chatbot.utils.user_profile import get_user_by_id
10
-
11
- def enrich_food_with_nutrition(state: AgentState):
12
- food_new_raw = state["food_new_raw"]
13
- suggested_meals = state.get("suggested_meals", [])
14
-
15
- # Map ID → món gốc
16
- meal_map = {str(m["meal_id"]): m for m in suggested_meals}
17
-
18
- # Những field KHÔNG nhân portion
19
- skip_scale_fields = {
20
- "meal_id", "name", "ingredients", "ingredients_text",
21
- "difficulty", "servings", "cooking_time_minutes"
22
- }
23
-
24
-
25
- meal_id = str(food_new_raw["id"])
26
- portion = float(food_new_raw["portion"])
27
-
28
- base = meal_map.get(meal_id)
29
-
30
- enriched_food = {}
31
-
32
- if base:
33
- enriched_food = {
34
- "id": meal_id,
35
- "name": food_new_raw["name"],
36
- "portion": portion
37
- }
38
-
39
- for key, value in base.items():
40
- # Nếu không nhân portion
41
- if key in skip_scale_fields:
42
- enriched_food[key] = value
43
- continue
44
-
45
- # Nếu là số → nhân portion
46
- if isinstance(value, (int, float)):
47
- enriched_food[key] = round(value * portion, 4)
48
- else:
49
- # Các field text, list thì giữ nguyên
50
- enriched_food[key] = value
51
-
52
- return {"food_new": enriched_food}
53
- else:
54
- return {"food_new": food_new_raw}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
chatbot/agents/nodes/functions/enrich_meal_plan_with_nutrition.py DELETED
@@ -1,72 +0,0 @@
1
- import logging
2
- from typing import List, Dict, Any
3
-
4
- from chatbot.agents.states.state import AgentState
5
- from chatbot.agents.tools.food_retriever import query_constructor, food_retriever
6
- from langgraph.graph import END, StateGraph
7
- from chatbot.models.llm_setup import llm
8
- from langchain.tools import tool
9
- from chatbot.utils.user_profile import get_user_by_id
10
-
11
- def enrich_meal_plan_with_nutrition(state: AgentState):
12
- meal_plan = state["meal_plan"]
13
- suggested_meals = state.get("suggested_meals", [])
14
-
15
- # Map ID → món gốc
16
- meal_map = {str(m["meal_id"]): m for m in suggested_meals}
17
-
18
- # Những field KHÔNG nhân portion
19
- skip_scale_fields = {
20
- "meal_id", "name", "ingredients", "ingredients_text",
21
- "difficulty", "servings", "cooking_time_minutes"
22
- }
23
-
24
- enriched_meals = []
25
-
26
- for meal in meal_plan.get("meals", []):
27
- enriched_items = []
28
-
29
- for item in meal.get("items", []):
30
- meal_id = str(item["id"])
31
- portion = float(item["portion"])
32
-
33
- base = meal_map.get(meal_id)
34
-
35
- if base:
36
- enriched_item = {
37
- "id": meal_id,
38
- "name": item["name"],
39
- "portion": portion
40
- }
41
-
42
- for key, value in base.items():
43
- # Nếu không nhân portion
44
- if key in skip_scale_fields:
45
- enriched_item[key] = value
46
- continue
47
-
48
- # Nếu là số → nhân portion
49
- if isinstance(value, (int, float)):
50
- enriched_item[key] = round(value * portion, 4)
51
- else:
52
- # Các field text, list thì giữ nguyên
53
- enriched_item[key] = value
54
-
55
- enriched_items.append(enriched_item)
56
-
57
- else:
58
- enriched_items.append(item)
59
-
60
- enriched_meals.append({
61
- "meal_name": meal["meal_name"],
62
- "items": enriched_items
63
- })
64
-
65
- meal_plan_day = {
66
- "meals": enriched_meals,
67
- "reason": meal_plan.get("reason", "")
68
- }
69
-
70
- return {
71
- "meal_plan_day": meal_plan_day
72
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
chatbot/agents/nodes/functions/enrich_meal_plan_with_nutrition_2.py DELETED
@@ -1,72 +0,0 @@
1
- import logging
2
- from typing import List, Dict, Any
3
-
4
- from chatbot.agents.states.state import AgentState
5
- from chatbot.agents.tools.food_retriever import query_constructor, food_retriever
6
- from langgraph.graph import END, StateGraph
7
- from chatbot.models.llm_setup import llm
8
- from langchain.tools import tool
9
- from chatbot.utils.user_profile import get_user_by_id
10
-
11
- def enrich_meal_plan_with_nutrition_2(state: AgentState):
12
- meal_new_raw = state["meal_new_raw"]
13
- suggested_meals = state.get("suggested_meals", [])
14
-
15
- # Map ID → món gốc
16
- meal_map = {str(m["meal_id"]): m for m in suggested_meals}
17
-
18
- # Những field KHÔNG nhân portion
19
- skip_scale_fields = {
20
- "meal_id", "name", "ingredients", "ingredients_text",
21
- "difficulty", "servings", "cooking_time_minutes"
22
- }
23
-
24
- enriched_meals = []
25
-
26
- for meal in meal_new_raw.get("meals", []):
27
- enriched_items = []
28
-
29
- for item in meal.get("items", []):
30
- meal_id = str(item["id"])
31
- portion = float(item["portion"])
32
-
33
- base = meal_map.get(meal_id)
34
-
35
- if base:
36
- enriched_item = {
37
- "id": meal_id,
38
- "name": item["name"],
39
- "portion": portion
40
- }
41
-
42
- for key, value in base.items():
43
- # Nếu không nhân portion
44
- if key in skip_scale_fields:
45
- enriched_item[key] = value
46
- continue
47
-
48
- # Nếu là số → nhân portion
49
- if isinstance(value, (int, float)):
50
- enriched_item[key] = round(value * portion, 4)
51
- else:
52
- # Các field text, list thì giữ nguyên
53
- enriched_item[key] = value
54
-
55
- enriched_items.append(enriched_item)
56
-
57
- else:
58
- enriched_items.append(item)
59
-
60
- enriched_meals.append({
61
- "meal_name": meal["meal_name"],
62
- "items": enriched_items
63
- })
64
-
65
- meal_new = {
66
- "meals": enriched_meals,
67
- "reason": meal_new_raw.get("reason", "")
68
- }
69
-
70
- return {
71
- "meal_new": meal_new
72
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
chatbot/agents/nodes/functions/generate_best_food_choice.py DELETED
@@ -1,79 +0,0 @@
1
- import logging
2
- from typing import List, Dict, Any
3
-
4
- from chatbot.agents.states.state import AgentState
5
- from chatbot.agents.tools.food_retriever import query_constructor, food_retriever
6
- from langgraph.graph import END, StateGraph
7
- from chatbot.models.llm_setup import llm
8
- from langchain.tools import tool
9
- from chatbot.utils.user_profile import get_user_by_id
10
-
11
- import json
12
-
13
- def generate_best_food_choice(state: AgentState):
14
- print("---GENERATE BEST FOOD CHOICE---")
15
-
16
- user_profile = state["user_profile"]
17
- food_old = state["food_old"]
18
- suggested_meals = state["suggested_meals"]
19
-
20
- if not suggested_meals:
21
- return {"error": "Không có món để chọn"}
22
-
23
- # Chuẩn bị danh sách món đã retriever được
24
- candidate_text = "\n".join([
25
- f"- ID: {m['meal_id']}, {m['name']} | "
26
- f"{m.get('kcal',0)} kcal, {m.get('protein',0)} protein, "
27
- f"{m.get('lipid',0)} lipid, {m.get('carbohydrate',0)} carbohydrate | "
28
- f"tags: {', '.join(m.get('tags', []))}"
29
- for m in suggested_meals
30
- ])
31
-
32
- # Prompt chọn món tốt nhất
33
- prompt = f"""
34
- Bạn là AI chuyên gia dinh dưỡng.
35
-
36
- Nhiệm vụ: Trong danh sách các món sau đây, hãy CHỌN RA 1 MÓN TƯƠNG TỰ NHẤT
37
- để thay thế món: {food_old['name']}.
38
-
39
- Giá trị dinh dưỡng món cũ:
40
- - kcal: {food_old['kcal']}
41
- - protein: {food_old['protein']}
42
- - lipid: {food_old['lipid']}
43
- - carbohydrate: {food_old['carbohydrate']}
44
- - tags: {', '.join(food_old['tags'])}
45
-
46
- Danh sách món gợi ý:
47
- {candidate_text}
48
-
49
- --- QUY TẮC ---
50
- 1. Chọn món có dinh dưỡng gần nhất với món cũ (±20%).
51
- 2. Ưu tiên món có nhiều tag trùng với món cũ.
52
- 3. Đề xuất khẩu phần (portion) phù hợp để tổng năng lượng món gần với món cũ (ví dụ: 0.5, 1, 1.2).
53
- 4. Chỉ trả JSON duy nhất, không viết gì thêm.
54
-
55
- --- ĐỊNH DẠNG JSON TRẢ VỀ ---
56
- {{
57
- "id": <ID>,
58
- "name": "<Tên món>",
59
- "portion": số_lượng_khẩu_phần (float)
60
- }}
61
-
62
- KHÔNG VIẾT GÌ NGOÀI JSON.
63
- """
64
-
65
-
66
- print("---Prompt---")
67
- print(prompt)
68
-
69
- result = llm.invoke(prompt)
70
- output = result.content
71
-
72
- # Parse JSON an toàn
73
- try:
74
- food_new_raw = json.loads(output)
75
- except Exception as e:
76
- print("❌ JSON parse error:", e)
77
- return {"response": "LLM trả về JSON không hợp lệ", "raw_output": output}
78
-
79
- return {"food_new_raw": food_new_raw}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
chatbot/agents/nodes/functions/generate_food_plan.py DELETED
@@ -1,42 +0,0 @@
1
- import logging
2
- from typing import List, Dict, Any
3
-
4
- from chatbot.agents.states.state import AgentState
5
- from chatbot.agents.tools.food_retriever import query_constructor, food_retriever
6
- from langgraph.graph import END, StateGraph
7
- from chatbot.models.llm_setup import llm
8
- from langchain.tools import tool
9
- from chatbot.utils.user_profile import get_user_by_id
10
-
11
- # --- Cấu hình logging ---
12
- logging.basicConfig(level=logging.INFO)
13
- logger = logging.getLogger(__name__)
14
-
15
- # --- Generate food plan ---
16
- def generate_food_plan(state: AgentState) -> Dict[str, Any]:
17
- logger.info("--- GENERATE_FOOD_PLAN ---")
18
- meals_to_generate: List[str] = state.get("meals_to_generate", [])
19
- user_profile: Dict[str, Any] = state.get("user_profile", {})
20
-
21
- if not meals_to_generate:
22
- logger.warning("meals_to_generate rỗng, sử dụng mặc định ['sáng', 'trưa', 'tối']")
23
- meals_to_generate = ["sáng", "trưa", "tối"]
24
-
25
- meals_text = ", ".join(meals_to_generate)
26
-
27
- query_text = (
28
- f"Tìm các món ăn phù hợp với người dùng có chế độ ăn: {user_profile.get('khẩu phần', 'ăn chay')}. "
29
- f"Ưu tiên món phổ biến, cân bằng dinh dưỡng, cho bữa {meals_text}."
30
- )
31
- logger.info(f"Query: {query_text}")
32
-
33
- try:
34
- foods = food_retriever.invoke(query_text)
35
- except Exception as e:
36
- logger.error(f"Lỗi khi truy vấn món ăn: {e}")
37
- foods = []
38
-
39
- suggested_meals = [food.metadata for food in foods] if foods else []
40
- logger.info(f"Số món được gợi ý: {len(suggested_meals)}")
41
-
42
- return {"suggested_meals": suggested_meals}