Spaces:
Running
Running
update optimization algorithm
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- chatbot/__pycache__/config.cpython-310.pyc +0 -0
- chatbot/__pycache__/main.cpython-310.pyc +0 -0
- chatbot/agents/graphs/__pycache__/chatbot_graph.cpython-310.pyc +0 -0
- chatbot/agents/graphs/__pycache__/food_similarity_graph.cpython-310.pyc +0 -0
- chatbot/agents/graphs/__pycache__/meal_suggestion_graph.cpython-310.pyc +0 -0
- chatbot/agents/graphs/chatbot_graph.py +32 -13
- chatbot/agents/graphs/food_similarity_graph.py +14 -16
- chatbot/agents/graphs/meal_similarity_graph.py +0 -23
- chatbot/agents/graphs/meal_suggestion_graph.py +11 -10
- chatbot/agents/graphs/meal_suggestion_json_graph.py +0 -23
- chatbot/agents/nodes/__pycache__/classify_topic.cpython-310.pyc +0 -0
- chatbot/agents/nodes/__pycache__/meal_identify.cpython-310.pyc +0 -0
- chatbot/agents/nodes/__pycache__/suggest_meal_node.cpython-310.pyc +0 -0
- chatbot/agents/nodes/app_functions/__init__.py +21 -0
- chatbot/agents/nodes/app_functions/find_candidates.py +68 -0
- chatbot/agents/nodes/app_functions/generate_candidates.py +261 -0
- chatbot/agents/nodes/app_functions/get_profile.py +22 -0
- chatbot/agents/nodes/app_functions/optimize_macros.py +204 -0
- chatbot/agents/nodes/app_functions/optimize_select.py +76 -0
- chatbot/agents/nodes/app_functions/select_meal.py +129 -0
- chatbot/agents/nodes/app_functions/select_menu.py +237 -0
- chatbot/agents/nodes/chatbot/__init__.py +25 -0
- chatbot/agents/nodes/{classify_topic.py → chatbot/classify_topic.py} +70 -59
- chatbot/agents/nodes/chatbot/food_query.py +39 -0
- chatbot/agents/nodes/{food_query.py → chatbot/food_suggestion.py} +34 -30
- chatbot/agents/nodes/{general_chat.py → chatbot/general_chat.py} +44 -39
- chatbot/agents/nodes/chatbot/generate_final_response.py +39 -0
- chatbot/agents/nodes/{meal_identify.py → chatbot/meal_identify.py} +55 -64
- chatbot/agents/nodes/chatbot/policy.py +46 -0
- chatbot/agents/nodes/chatbot/select_food.py +56 -0
- chatbot/agents/nodes/{select_food_plan.py → chatbot/select_food_plan.py} +54 -50
- chatbot/agents/nodes/{suggest_meal_node.py → chatbot/suggest_meal_node.py} +77 -72
- chatbot/agents/nodes/functions/__init__.py +0 -25
- chatbot/agents/nodes/functions/__pycache__/__init__.cpython-310.pyc +0 -0
- chatbot/agents/nodes/functions/__pycache__/enrich_food_with_nutrition.cpython-310.pyc +0 -0
- chatbot/agents/nodes/functions/__pycache__/enrich_meal_plan_with_nutrition.cpython-310.pyc +0 -0
- chatbot/agents/nodes/functions/__pycache__/enrich_meal_plan_with_nutrition_2.cpython-310.pyc +0 -0
- chatbot/agents/nodes/functions/__pycache__/generate_best_food_choice.cpython-310.pyc +0 -0
- chatbot/agents/nodes/functions/__pycache__/generate_food_plan.cpython-310.pyc +0 -0
- chatbot/agents/nodes/functions/__pycache__/generate_food_similarity.cpython-310.pyc +0 -0
- chatbot/agents/nodes/functions/__pycache__/generate_food_similarity_2.cpython-310.pyc +0 -0
- chatbot/agents/nodes/functions/__pycache__/generate_meal_plan.cpython-310.pyc +0 -0
- chatbot/agents/nodes/functions/__pycache__/generate_meal_plan_day_json.cpython-310.pyc +0 -0
- chatbot/agents/nodes/functions/__pycache__/generate_meal_plan_json_2.cpython-310.pyc +0 -0
- chatbot/agents/nodes/functions/__pycache__/get_user_profile.cpython-310.pyc +0 -0
- chatbot/agents/nodes/functions/enrich_food_with_nutrition.py +0 -54
- chatbot/agents/nodes/functions/enrich_meal_plan_with_nutrition.py +0 -72
- chatbot/agents/nodes/functions/enrich_meal_plan_with_nutrition_2.py +0 -72
- chatbot/agents/nodes/functions/generate_best_food_choice.py +0 -79
- 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.
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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("
|
|
|
|
| 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",
|
|
|
|
| 36 |
|
| 37 |
-
workflow_chatbot.add_edge("
|
| 38 |
workflow_chatbot.add_edge("select_food_plan", END)
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
workflow_chatbot.add_edge("general_chat", END)
|
| 41 |
|
| 42 |
-
|
| 43 |
-
|
|
|
|
|
|
| 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
|
| 3 |
|
| 4 |
-
|
| 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 |
-
|
| 9 |
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
-
|
| 18 |
-
|
| 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.
|
| 6 |
|
| 7 |
-
def
|
| 8 |
workflow = StateGraph(AgentState)
|
| 9 |
|
| 10 |
-
workflow.add_node("
|
| 11 |
-
workflow.add_node("
|
| 12 |
-
workflow.add_node("
|
|
|
|
| 13 |
|
| 14 |
-
workflow.set_entry_point("
|
| 15 |
-
|
| 16 |
-
workflow.add_edge("
|
| 17 |
-
workflow.add_edge("
|
| 18 |
-
workflow.add_edge("
|
| 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 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 là bộ phân loại chủ đề câu hỏi người dùng trong hệ thống chatbot dinh dưỡng.
|
| 27 |
+
|
| 28 |
+
Nhiệm vụ:
|
| 29 |
+
- Phân loại câu hỏi vào một trong các nhóm:
|
| 30 |
+
1. "meal_suggestion": khi người dùng yêu cầu gợi ý thực đơn cho cả một bữa ăn hoặc trong cả một ngày (chỉ cho bữa ăn, không cho món ăn đơn lẻ).
|
| 31 |
+
2. "food_suggestion": khi người dùng yêu cầu tìm kiếm hoặc gợi ý một món ăn duy nhất (có thể của một bữa nào đó).
|
| 32 |
+
3. "food_query": khi người dùng muốn tìm kiếm thông tin về một món ăn như tên, thành phần, dinh dưỡng, cách chế biến
|
| 33 |
+
4. "policy": khi người dùng muốn biết các thông tin liên quan đến app.
|
| 34 |
+
5. "general_chat": khi người dùng muốn hỏi đáp các câu hỏi chung liên quan đến sức khỏe, chất dinh dưỡng.
|
| 35 |
+
|
| 36 |
+
Câu hỏi người dùng: {question}
|
| 37 |
+
|
| 38 |
+
Hãy trả lời dưới dạng JSON phù hợp với schema sau:
|
| 39 |
+
{format_instructions}
|
| 40 |
+
"""
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
messages = state["messages"]
|
| 44 |
+
user_message = messages[-1].content if messages else state.question
|
| 45 |
+
|
| 46 |
+
format_instructions = json.dumps(llm_with_structure_op.output_schema.model_json_schema(), ensure_ascii=False, indent=2)
|
| 47 |
+
|
| 48 |
+
chain = prompt | llm_with_structure_op
|
| 49 |
+
|
| 50 |
+
topic_result = chain.invoke({
|
| 51 |
+
"question": user_message,
|
| 52 |
+
"format_instructions": format_instructions
|
| 53 |
+
})
|
| 54 |
+
|
| 55 |
+
logger.info(f"Topic: {topic_result.name}")
|
| 56 |
+
|
| 57 |
+
return {"topic": topic_result.name}
|
| 58 |
+
|
| 59 |
+
def route_by_topic(state: AgentState):
|
| 60 |
+
topic = state["topic"]
|
| 61 |
+
if topic == "meal_suggestion":
|
| 62 |
+
return "meal_identify"
|
| 63 |
+
elif topic == "food_suggestion":
|
| 64 |
+
return "food_suggestion"
|
| 65 |
+
elif topic == "food_query":
|
| 66 |
+
return "food_query"
|
| 67 |
+
elif topic == "policy":
|
| 68 |
+
return "policy"
|
| 69 |
+
else:
|
| 70 |
+
return "general_chat"
|
| 71 |
|
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
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
"""
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 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 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 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 là một chuyên gia dinh dưỡng và ẩm thực AI.
|
| 22 |
+
Hãy trả lời các câu hỏi về:
|
| 23 |
+
- món ăn, thành phần, dinh dưỡng, calo, protein, chất béo, carb,
|
| 24 |
+
- chế độ ăn (ăn chay, keto, giảm cân, tăng cơ...),
|
| 25 |
+
- sức khỏe, lối sống, chế độ tập luyện liên quan đến ăn uống.
|
| 26 |
+
Một số thông tin về người dùng 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 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 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 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
f"
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 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 là chuyên gia dinh dưỡng AI.
|
| 28 |
+
Bạn có thể sử dụng thông tin người dùng có hồ sơ dinh dưỡng sau nếu cần thiết cho câu hỏi của người dùng:
|
| 29 |
+
- Tổng năng lượng mục tiêu: {user_profile['targetcalories']} kcal/ngày
|
| 30 |
+
- Protein: {user_profile['protein']}g
|
| 31 |
+
- Chất béo (lipid): {user_profile['totalfat']}g
|
| 32 |
+
- Carbohydrate: {user_profile['carbohydrate']}g
|
| 33 |
+
- Chế độ ăn: {user_profile['diet']}
|
| 34 |
+
|
| 35 |
+
Câu hỏi của người dùng: "{user_message}"
|
| 36 |
+
|
| 37 |
+
Danh sách món ăn hiện có để chọn:
|
| 38 |
+
{suggested_meals_text}
|
| 39 |
+
|
| 40 |
+
Yêu cầu:
|
| 41 |
+
1. Chọn một món ăn phù hợp nhất với yêu cầu của người dùng, dựa trên dinh dưỡng và chế độ ăn.
|
| 42 |
+
2. Nếu không có món nào phù hợp, hãy trả về:
|
| 43 |
+
"Không tìm thấy món phù hợp trong danh sách hiện có."
|
| 44 |
+
3. Không tự tạo thêm món mới hoặc tên món không có trong danh sách.
|
| 45 |
+
4. Nếu có nhiều món gần giống nhau, hãy chọn món có năng lượng và thành phần dinh dưỡng gần nhất với mục tiêu người dùng.
|
| 46 |
+
"""
|
| 47 |
+
|
| 48 |
+
logger.info("Prompt:")
|
| 49 |
+
logger.info(prompt)
|
| 50 |
+
|
| 51 |
+
result = llm.invoke(prompt)
|
| 52 |
+
|
| 53 |
+
logger.info(result.content if hasattr(result, "content") else result)
|
| 54 |
+
|
| 55 |
return {"response": result.content}
|
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 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
#
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 là một chuyên gia gợi ý thực đơn AI.
|
| 22 |
+
Bạn không được tự trả lời hay đặt câu hỏi thêm.
|
| 23 |
+
Nếu người dùng yêu cầu gợi ý món ăn, bắt buộc gọi tool 'daily_meal_suggestion'.
|
| 24 |
+
với các tham số:
|
| 25 |
+
- user_id: ID người dùng hiện tại
|
| 26 |
+
- question: nội dung câu hỏi họ vừa hỏi
|
| 27 |
+
- meals_to_generate: danh sách các bữa cần sinh thực đơn (nếu có)
|
| 28 |
+
|
| 29 |
+
Nếu bạn không chắc bữa nào cần sinh, vẫn gọi tool này — phần xử lý sẽ lo chi tiết sau.
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
user_prompt = f"""
|
| 33 |
+
Người dùng có ID: {user_id}
|
| 34 |
+
Yêu cầu: "{question}"
|
| 35 |
+
Danh sách các bữa cần gợi ý: {meals_to_generate}
|
| 36 |
+
"""
|
| 37 |
+
|
| 38 |
+
# 🚀 Gọi LLM và Tools
|
| 39 |
+
tools = [daily_meal_suggestion]
|
| 40 |
+
llm_with_tools = llm.bind_tools(tools)
|
| 41 |
+
|
| 42 |
+
response = llm_with_tools.invoke(
|
| 43 |
+
[
|
| 44 |
+
SystemMessage(content=system_prompt),
|
| 45 |
+
HumanMessage(content=user_prompt)
|
| 46 |
+
]
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
logger.info("===== DEBUG =====")
|
| 50 |
+
logger.info(f"Response type: {type(response)}")
|
| 51 |
+
logger.info(f"Tool calls: {getattr(response, 'tool_calls', None)}")
|
| 52 |
+
logger.info(f"Message content: {response.content}")
|
| 53 |
+
logger.info("=================")
|
| 54 |
+
|
| 55 |
+
if isinstance(response, AIMessage) and response.tool_calls:
|
| 56 |
+
tool_call = response.tool_calls[0]
|
| 57 |
+
tool_name = tool_call["name"]
|
| 58 |
+
tool_args = tool_call["args"]
|
| 59 |
+
tool_call_id = tool_call["id"]
|
| 60 |
+
|
| 61 |
+
logger.info(f"👉 Executing tool: {tool_name} with args: {tool_args}")
|
| 62 |
+
|
| 63 |
+
# Bổ sung tham số nếu LLM quên
|
| 64 |
+
tool_args.setdefault("user_id", user_id)
|
| 65 |
+
tool_args.setdefault("question", question)
|
| 66 |
+
tool_args.setdefault("meals_to_generate", meals_to_generate)
|
| 67 |
+
|
| 68 |
+
if tool_name == "daily_meal_suggestion":
|
| 69 |
+
result = daily_meal_suggestion.invoke(tool_args)
|
| 70 |
+
elif tool_name == "fallback":
|
| 71 |
+
result = {"message": "Không có tool phù hợp.", "reason": tool_args.get("reason", "")}
|
| 72 |
+
else:
|
| 73 |
+
result = {"message": f"Tool '{tool_name}' chưa được định nghĩa."}
|
| 74 |
+
|
| 75 |
+
tool_message = ToolMessage(content=str(result), name=tool_name, tool_call_id=tool_call_id)
|
| 76 |
+
return {"messages": state["messages"] + [response, tool_message], "response": result}
|
| 77 |
+
return {"response": "Lỗi!!!"}
|
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}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|