diff --git a/chatbot/__pycache__/config.cpython-310.pyc b/chatbot/__pycache__/config.cpython-310.pyc index e97046dcf87653ac9e3e5a87724a1eea7e8f5e95..4c579e4a42197a4ef389f2b22664fa23269bd412 100644 Binary files a/chatbot/__pycache__/config.cpython-310.pyc and b/chatbot/__pycache__/config.cpython-310.pyc differ diff --git a/chatbot/__pycache__/main.cpython-310.pyc b/chatbot/__pycache__/main.cpython-310.pyc index d09dfe17b65f69709eb0f16be9346f878543b41a..c099022a7eb34d05212cb1510a84703fb5b41f12 100644 Binary files a/chatbot/__pycache__/main.cpython-310.pyc and b/chatbot/__pycache__/main.cpython-310.pyc differ diff --git a/chatbot/agents/graphs/__pycache__/chatbot_graph.cpython-310.pyc b/chatbot/agents/graphs/__pycache__/chatbot_graph.cpython-310.pyc index 87c7a8b11a74fa660d30de1f3fb90be5e1f5c90f..8125d01a5a542d1d3a4ffc3ea8ae17ee30123e11 100644 Binary files a/chatbot/agents/graphs/__pycache__/chatbot_graph.cpython-310.pyc and b/chatbot/agents/graphs/__pycache__/chatbot_graph.cpython-310.pyc differ diff --git a/chatbot/agents/graphs/__pycache__/food_similarity_graph.cpython-310.pyc b/chatbot/agents/graphs/__pycache__/food_similarity_graph.cpython-310.pyc index 5bfb10c9b64765a14f4f4a862128cafa15e1126f..89383771db8a2fbb5511645ca8bf2c723450ccc1 100644 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 diff --git a/chatbot/agents/graphs/__pycache__/meal_suggestion_graph.cpython-310.pyc b/chatbot/agents/graphs/__pycache__/meal_suggestion_graph.cpython-310.pyc index b05aa5b35e9593d4bf1bbb6cc55d90d546bedf21..7d73dcce2908cb83ea97207835d0c75628ce06d6 100644 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 diff --git a/chatbot/agents/graphs/chatbot_graph.py b/chatbot/agents/graphs/chatbot_graph.py index 11553d73766076cc03886af90518e8d2cd47810e..a22f922e8c6eb328710ae540d6c949dd5307361d 100644 --- a/chatbot/agents/graphs/chatbot_graph.py +++ b/chatbot/agents/graphs/chatbot_graph.py @@ -2,22 +2,33 @@ from langgraph.graph import StateGraph, START, END from chatbot.agents.states.state import AgentState # Import các node -from chatbot.agents.nodes.classify_topic import classify_topic, route_by_topic -from chatbot.agents.nodes.meal_identify import meal_identify -from chatbot.agents.nodes.suggest_meal_node import suggest_meal_node -from chatbot.agents.nodes.food_query import food_query -from chatbot.agents.nodes.select_food_plan import select_food_plan -from chatbot.agents.nodes.general_chat import general_chat - -def workflow_chatbot() -> StateGraph: +from chatbot.agents.nodes.chatbot import ( + classify_topic, + route_by_topic, + meal_identify, + suggest_meal_node, + generate_final_response, + food_suggestion, + select_food_plan, + food_query, + select_food, + general_chat, + policy +) + +def workflow_chatbot(): workflow_chatbot = StateGraph(AgentState) workflow_chatbot.add_node("classify_topic", classify_topic) workflow_chatbot.add_node("meal_identify", meal_identify) workflow_chatbot.add_node("suggest_meal_node", suggest_meal_node) - workflow_chatbot.add_node("food_query", food_query) + workflow_chatbot.add_node("generate_final_response", generate_final_response) + workflow_chatbot.add_node("food_suggestion", food_suggestion) workflow_chatbot.add_node("select_food_plan", select_food_plan) + workflow_chatbot.add_node("food_query", food_query) + workflow_chatbot.add_node("select_food", select_food) workflow_chatbot.add_node("general_chat", general_chat) + workflow_chatbot.add_node("policy", policy) workflow_chatbot.add_edge(START, "classify_topic") @@ -26,18 +37,26 @@ def workflow_chatbot() -> StateGraph: route_by_topic, { "meal_identify": "meal_identify", + "food_suggestion": "food_suggestion", "food_query": "food_query", + "policy": "policy", "general_chat": "general_chat", } ) workflow_chatbot.add_edge("meal_identify", "suggest_meal_node") - workflow_chatbot.add_edge("suggest_meal_node", END) + workflow_chatbot.add_edge("suggest_meal_node", "generate_final_response") + workflow_chatbot.add_edge("generate_final_response", END) - workflow_chatbot.add_edge("food_query", "select_food_plan") + workflow_chatbot.add_edge("food_suggestion", "select_food_plan") workflow_chatbot.add_edge("select_food_plan", END) + workflow_chatbot.add_edge("food_query", "select_food") + workflow_chatbot.add_edge("select_food", END) + + workflow_chatbot.add_edge("policy", END) workflow_chatbot.add_edge("general_chat", END) - graph = workflow_chatbot.compile() - return graph + app = workflow_chatbot.compile() + + return app diff --git a/chatbot/agents/graphs/food_similarity_graph.py b/chatbot/agents/graphs/food_similarity_graph.py index a9b66472ad833056efb8155347d1df289d2268ba..9bf01cbdd3c6e0a7943e0727b7aaebdbee94bf59 100644 --- a/chatbot/agents/graphs/food_similarity_graph.py +++ b/chatbot/agents/graphs/food_similarity_graph.py @@ -1,23 +1,21 @@ from langgraph.graph import StateGraph, START, END -from chatbot.agents.states.state import AgentState +from chatbot.agents.states.state import SwapState -# Import các node -from chatbot.agents.nodes.functions import get_user_profile, generate_food_similarity, generate_best_food_choice, enrich_food_with_nutrition +from chatbot.agents.nodes.app_functions import get_user_profile, find_replacement_candidates, calculate_top_options, llm_finalize_choice def food_similarity_graph() -> StateGraph: - workflow = StateGraph(AgentState) + swap_workflow = StateGraph(SwapState) - workflow.add_node("Get_User_Profile", get_user_profile) - workflow.add_node("Generate_Food_Similarity", generate_food_similarity) - workflow.add_node("Generate_Best_Food_Choice", generate_best_food_choice) - workflow.add_node("Enrich_Food_With_Nutrition", enrich_food_with_nutrition) + swap_workflow.add_node("get_profile", get_user_profile) + swap_workflow.add_node("find_candidates", find_replacement_candidates) + swap_workflow.add_node("optimize_select", calculate_top_options) + swap_workflow.add_node("select_meal", llm_finalize_choice) - workflow.set_entry_point("Get_User_Profile") + swap_workflow.set_entry_point("get_profile") + swap_workflow.add_edge("get_profile", "find_candidates") + swap_workflow.add_edge("find_candidates", "optimize_select") + swap_workflow.add_edge("optimize_select", "select_meal") + swap_workflow.add_edge("select_meal", END) - workflow.add_edge("Get_User_Profile", "Generate_Food_Similarity") - workflow.add_edge("Generate_Food_Similarity", "Generate_Best_Food_Choice") - workflow.add_edge("Generate_Best_Food_Choice", "Enrich_Food_With_Nutrition") - workflow.add_edge("Enrich_Food_With_Nutrition", END) - - graph = workflow.compile() - return graph + app = swap_workflow.compile() + return app diff --git a/chatbot/agents/graphs/meal_similarity_graph.py b/chatbot/agents/graphs/meal_similarity_graph.py deleted file mode 100644 index c2475ce07153051b5b592b10510ea60874df5770..0000000000000000000000000000000000000000 --- a/chatbot/agents/graphs/meal_similarity_graph.py +++ /dev/null @@ -1,23 +0,0 @@ -from langgraph.graph import StateGraph, START, END -from chatbot.agents.states.state import AgentState - -# Import các node -from chatbot.agents.nodes.functions import get_user_profile, generate_food_similarity_2, generate_meal_plan_json_2, enrich_meal_plan_with_nutrition_2 - -def meal_similarity_graph() -> StateGraph: - workflow = StateGraph(AgentState) - - workflow.add_node("Get_User_Profile", get_user_profile) - workflow.add_node("Generate_Food_Similarity_2", generate_food_similarity_2) - workflow.add_node("Generate_Meal_Plan_Json_2", generate_meal_plan_json_2) - workflow.add_node("Enrich_Meal_Plan_With_Nutrition_2", enrich_meal_plan_with_nutrition_2) - - workflow.set_entry_point("Get_User_Profile") - - workflow.add_edge("Get_User_Profile", "Generate_Food_Similarity_2") - workflow.add_edge("Generate_Food_Similarity_2", "Generate_Meal_Plan_Json_2") - workflow.add_edge("Generate_Meal_Plan_Json_2", "Enrich_Meal_Plan_With_Nutrition_2") - workflow.add_edge("Enrich_Meal_Plan_With_Nutrition_2", END) - - graph = workflow.compile() - return graph diff --git a/chatbot/agents/graphs/meal_suggestion_graph.py b/chatbot/agents/graphs/meal_suggestion_graph.py index 6852851ac1dcbc541ef3f19ea03b7ca94c400e8b..380aa4ea989d57f55e72873337099cb0a7607f1d 100644 --- a/chatbot/agents/graphs/meal_suggestion_graph.py +++ b/chatbot/agents/graphs/meal_suggestion_graph.py @@ -2,20 +2,21 @@ from langgraph.graph import StateGraph, START, END from chatbot.agents.states.state import AgentState # Import các node -from chatbot.agents.nodes.functions import get_user_profile, generate_food_plan, generate_meal_plan +from chatbot.agents.nodes.app_functions import get_user_profile, generate_food_candidates, select_menu_structure, optimize_portions_scipy -def workflow_meal_suggestion() -> StateGraph: +def meal_plan_graph(): workflow = StateGraph(AgentState) - workflow.add_node("Get_User_Profile", get_user_profile) - workflow.add_node("Generate_Food_Plan", generate_food_plan) - workflow.add_node("Generate_Meal_Plan", generate_meal_plan) + workflow.add_node("get_profile", get_user_profile) + workflow.add_node("generate_candidates", generate_food_candidates) + workflow.add_node("select_menu", select_menu_structure) + workflow.add_node("optimize_macros", optimize_portions_scipy) - workflow.set_entry_point("Get_User_Profile") - - workflow.add_edge("Get_User_Profile", "Generate_Food_Plan") - workflow.add_edge("Generate_Food_Plan", "Generate_Meal_Plan") - workflow.add_edge("Generate_Meal_Plan", END) + workflow.set_entry_point("get_profile") + workflow.add_edge("get_profile", "generate_candidates") + workflow.add_edge("generate_candidates", "select_menu") + workflow.add_edge("select_menu", "optimize_macros") + workflow.add_edge("optimize_macros", END) graph = workflow.compile() return graph diff --git a/chatbot/agents/graphs/meal_suggestion_json_graph.py b/chatbot/agents/graphs/meal_suggestion_json_graph.py deleted file mode 100644 index 09c3b208e80dab7383b9c7354796c6c09a5c0265..0000000000000000000000000000000000000000 --- a/chatbot/agents/graphs/meal_suggestion_json_graph.py +++ /dev/null @@ -1,23 +0,0 @@ -from langgraph.graph import StateGraph, START, END -from chatbot.agents.states.state import AgentState - -# Import các node -from chatbot.agents.nodes.functions import get_user_profile, generate_food_plan, generate_meal_plan_day_json, enrich_meal_plan_with_nutrition - -def workflow_meal_suggestion_json() -> StateGraph: - workflow = StateGraph(AgentState) - - workflow.add_node("Get_User_Profile", get_user_profile) - workflow.add_node("Generate_Food_Plan", generate_food_plan) - workflow.add_node("Generate_Meal_Plan_Day_Json", generate_meal_plan_day_json) - workflow.add_node("Enrich_Meal_Plan_With_Nutrition", enrich_meal_plan_with_nutrition) - - workflow.set_entry_point("Get_User_Profile") - - workflow.add_edge("Get_User_Profile", "Generate_Food_Plan") - workflow.add_edge("Generate_Food_Plan", "Generate_Meal_Plan_Day_Json") - workflow.add_edge("Generate_Meal_Plan_Day_Json", "Enrich_Meal_Plan_With_Nutrition") - workflow.add_edge("Enrich_Meal_Plan_With_Nutrition", END) - - graph = workflow.compile() - return graph diff --git a/chatbot/agents/nodes/__pycache__/classify_topic.cpython-310.pyc b/chatbot/agents/nodes/__pycache__/classify_topic.cpython-310.pyc index 03658d963a7afa764cc396ad4118c634d48f890f..0661d0d35a06a893d01222ae9b04a597a55703b8 100644 Binary files a/chatbot/agents/nodes/__pycache__/classify_topic.cpython-310.pyc and b/chatbot/agents/nodes/__pycache__/classify_topic.cpython-310.pyc differ diff --git a/chatbot/agents/nodes/__pycache__/meal_identify.cpython-310.pyc b/chatbot/agents/nodes/__pycache__/meal_identify.cpython-310.pyc index 099eca7a65422f184c81a7795b7e959d851ea724..888ca201791d07a915b0c82fda3c9f46d10269c2 100644 Binary files a/chatbot/agents/nodes/__pycache__/meal_identify.cpython-310.pyc and b/chatbot/agents/nodes/__pycache__/meal_identify.cpython-310.pyc differ diff --git a/chatbot/agents/nodes/__pycache__/suggest_meal_node.cpython-310.pyc b/chatbot/agents/nodes/__pycache__/suggest_meal_node.cpython-310.pyc index 82780b04701ec274a20df339306a0acec8d3cd75..4e4831790a1035bbe7f7f96765dfdf57250c5add 100644 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 diff --git a/chatbot/agents/nodes/app_functions/__init__.py b/chatbot/agents/nodes/app_functions/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..9a6dc8e229befc3447330381dd7a58cff8e49a8e --- /dev/null +++ b/chatbot/agents/nodes/app_functions/__init__.py @@ -0,0 +1,21 @@ +from .get_profile import get_user_profile + +from .generate_candidates import generate_food_candidates +from .select_menu import select_menu_structure +from .optimize_macros import optimize_portions_scipy + +from .find_candidates import find_replacement_candidates +from .optimize_select import calculate_top_options +from .select_meal import llm_finalize_choice + +__all__ = [ + "get_user_profile", + + "generate_food_candidates", + "select_menu_structure", + "optimize_portions_scipy", + + "find_replacement_candidates", + "calculate_top_options", + "llm_finalize_choice", +] \ No newline at end of file diff --git a/chatbot/agents/nodes/app_functions/find_candidates.py b/chatbot/agents/nodes/app_functions/find_candidates.py new file mode 100644 index 0000000000000000000000000000000000000000..85454229f7b64aa27447bb6a4fe6395152c0db09 --- /dev/null +++ b/chatbot/agents/nodes/app_functions/find_candidates.py @@ -0,0 +1,68 @@ +from chatbot.agents.states.state import SwapState +from chatbot.agents.tools.food_retriever import food_retriever +from chatbot.agents.nodes.app_functions.generate_candidates import generate_numerical_constraints +import logging + +# --- Cấu hình logging --- +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +def find_replacement_candidates(state: SwapState): + logger.info("---NODE: FIND REPLACEMENTS (SELF QUERY)---") + food_old = state["food_old"] + profile = state["user_profile"] + + diet_mode = profile.get('diet', '') # VD: Chế độ HighProtein + restrictions = profile.get('limitFood', '') # VD: Dị ứng sữa, Thuần chay + health_status = profile.get('healthStatus', '') # VD: Suy thận + + constraint_prompt = "" + + if restrictions: + constraint_prompt += f"Yêu cầu bắt buộc: {restrictions}. " + if health_status: + constraint_prompt += f"Phù hợp người bệnh: {health_status}. " + if diet_mode: + constraint_prompt += f"Chế độ: {diet_mode}." + + # 1. Trích xuất ngữ cảnh từ món cũ + role = food_old.get("role", "main") # VD: main, side, carb + meal_type = food_old.get("assigned_meal", "trưa") # VD: trưa + old_name = food_old.get("name", "") + numerical_query = generate_numerical_constraints(profile, meal_type) + + # 2. Xây dựng Query tự nhiên để SelfQueryRetriever hiểu + # Mẹo: Đưa thông tin phủ định "Không phải món X" vào + query = ( + f"Tìm các món ăn đóng vai trò '{role}' phù hợp cho bữa '{meal_type}'. " + f"Khác với món '{old_name}'. " + f"{constraint_prompt}" + ) + + if numerical_query: + query += f"Yêu cầu: {numerical_query}" + + logger.info(f"🔎 Query: {query}") + + # 3. Gọi Retriever + try: + docs = food_retriever.invoke(query) + except Exception as e: + logger.info(f"⚠️ Lỗi Retriever: {e}") + return {"candidates": []} + + # 4. Lọc sơ bộ (Python Filter) + candidates = [] + for doc in docs: + item = doc.metadata.copy() + + # Bỏ qua chính món cũ (Double check) + if item.get("name") == old_name: continue + + # Gán context của món cũ sang để tính toán + item["target_role"] = role + item["target_meal"] = meal_type + candidates.append(item) + + logger.info(f"📚 Tìm thấy {len(candidates)} ứng viên tiềm năng.") + return {"candidates": candidates} \ No newline at end of file diff --git a/chatbot/agents/nodes/app_functions/generate_candidates.py b/chatbot/agents/nodes/app_functions/generate_candidates.py new file mode 100644 index 0000000000000000000000000000000000000000..3764dfeeb512c20ce92d82d9f684989ff1275a3b --- /dev/null +++ b/chatbot/agents/nodes/app_functions/generate_candidates.py @@ -0,0 +1,261 @@ +import random +import logging +from chatbot.agents.states.state import AgentState +from chatbot.agents.tools.food_retriever import food_retriever_50 + +# --- Cấu hình logging --- +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +def generate_food_candidates(state: AgentState): + logger.info("---NODE: RETRIEVAL CANDIDATES (ADVANCED PROFILE)---") + meals = state.get("meals_to_generate", []) + profile = state["user_profile"] + + candidates = [] + + diet_mode = profile.get('diet', '') # VD: Chế độ HighProtein + restrictions = profile.get('limitFood', '') # VD: Dị ứng sữa, Thuần chay + health_status = profile.get('healthStatus', '') # VD: Suy thận + + constraint_prompt = "" + if restrictions: + constraint_prompt += f"Yêu cầu bắt buộc: {restrictions}. " + if health_status: + constraint_prompt += f"Phù hợp người bệnh: {health_status}. " + if diet_mode: + constraint_prompt += f"Chế độ: {diet_mode}." + + # ĐỊNH NGHĨA TEMPLATE PROMPT + prompt_templates = { + "sáng": ( + f"Món ăn sáng, điểm tâm. Ưu tiên món nước hoặc món khô dễ tiêu hóa. " + f"{constraint_prompt}" + ), + "trưa": ( + f"Món ăn chính cho bữa trưa. " + f"{constraint_prompt}" + ), + "tối": ( + f"Món ăn tối, nhẹ bụng. " + f"{constraint_prompt}" + ), + } + + random_vibes = [ + "hương vị truyền thống", "phong cách hiện đại", + "thanh đạm", "chế biến đơn giản", "phổ biến nhất" + ] + + for meal_type in meals: + logger.info(meal_type) + base_prompt = prompt_templates.get(meal_type, f"Món ăn {meal_type}. {constraint_prompt}") + vibe = random.choice(random_vibes) + numerical_query = generate_numerical_constraints(profile, meal_type) + + final_query = f"{base_prompt} Phong cách: {vibe}.{' Ràng buộc: ' + numerical_query if numerical_query != '' else ''}" + logger.info(f"🔎 Query ({meal_type}): {final_query}") + + docs = food_retriever_50.invoke(final_query) + ranked_items = rank_candidates(docs, profile, meal_type) + + if len(ranked_items) > 0: + ranked_items_shuffle = random.sample(ranked_items[:30], 30) + + k = 20 if len(meals) == 1 else 10 + + selected_docs = ranked_items_shuffle[:k] + + for doc in selected_docs: + item = doc.copy() + item["meal_type_tag"] = meal_type + item["retrieval_vibe"] = vibe + candidates.append(item) + + unique_candidates = {v['name']: v for v in candidates}.values() + final_pool = list(unique_candidates) + + logger.info(f"📚 Candidate Pool Size: {len(final_pool)} món") + return {"candidate_pool": final_pool, "meals_to_generate": meals} + +def generate_numerical_constraints(user_profile, meal_type): + """ + 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. + """ + ratios = {"sáng": 0.25, "trưa": 0.40, "tối": 0.35} + meal_ratio = ratios.get(meal_type, 0.3) + + critical_nutrients = { + "Protein": ("protein", "protein", "g", "range"), + "Saturated fat": ("saturatedfat", "saturated_fat", "g", "max"), + "Natri": ("natri", "natri", "mg", "max"), # Quan trọng cho thận/tim + "Kali": ("kali", "kali", "mg", "range"), # Quan trọng cho thận + "Phốt pho": ("photpho", "photpho", "mg", "max"), # Quan trọng cho thận + "Sugars": ("sugar", "sugar", "g", "max"), # Quan trọng cho tiểu đường + "Carbohydrate": ("carbohydrate", "carbohydrate", "g", "range"), + } + + constraints = [] + + check_list = set(user_profile.get('Kiêng', []) + user_profile.get('Hạn chế', [])) + for item_name in check_list: + if item_name not in critical_nutrients: continue + + config = critical_nutrients.get(item_name) + profile_key, db_key, unit, logic = config + daily_val = float(user_profile.get(profile_key, 0)) + meal_target = daily_val * meal_ratio + + if logic == 'max': + # Nới lỏng một chút ở bước tìm kiếm (120-130% target) để không bị lọc hết + threshold = round(meal_target * 1.3, 2) + constraints.append(f"{db_key} < {threshold}{unit}") + + elif logic == 'range': + # Range rộng (50% - 150%) để bắt được nhiều món + min_val = round(meal_target * 0.5, 2) + max_val = round(meal_target * 1.5, 2) + constraints.append(f"{db_key} > {min_val}{unit} - {db_key} < {max_val}{unit}") + + if not constraints: return "" + return ", ".join(constraints) + +def rank_candidates(candidates, user_profile, meal_type): + """ + Chấm điểm (Scoring) các món ăn dựa trên cấu hình dinh dưỡng chi tiết. + """ + print("---NODE: RANKING CANDIDATES (ADVANCED SCORING)---") + + ratios = {"sáng": 0.25, "trưa": 0.40, "tối": 0.35} + meal_ratio = ratios.get(meal_type, 0.3) + + nutrient_config = { + # --- Nhóm Đa lượng (Macro) --- + "Protein": ("protein", "protein", "g", "range"), + "Total Fat": ("totalfat", "lipid", "g", "max"), + "Carbohydrate": ("carbohydrate", "carbohydrate", "g", "range"), + "Saturated fat": ("saturatedfat", "saturated_fat", "g", "max"), + "Monounsaturated fat": ("monounsaturatedfat", "monounsaturated_fat", "g", "max"), + "Trans fat": ("transfat", "trans_fat", "g", "max"), + "Sugars": ("sugar", "sugar", "g", "max"), + "Chất xơ": ("fiber", "fiber", "g", "min"), + + # --- Nhóm Vi chất (Micro) --- + "Vitamin A": ("vitamina", "vit_a", "mg", "min"), + "Vitamin C": ("vitaminc", "vit_c", "mg", "min"), + "Vitamin D": ("vitamind", "vit_d", "mg", "min"), + "Vitamin E": ("vitamine", "vit_e", "mg", "min"), + "Vitamin K": ("vitamink", "vit_k", "mg", "min"), + "Vitamin B6": ("vitaminb6", "vit_b6", "mg", "min"), + "Vitamin B12": ("vitaminb12", "vit_b12", "mg", "min"), + + # --- Khoáng chất --- + "Canxi": ("canxi", "canxi", "mg", "min"), + "Sắt": ("fe", "sat", "mg", "min"), + "Magie": ("magie", "magie", "mg", "min"), + "Kẽm": ("zn", "kem", "mg", "min"), + "Kali": ("kali", "kali", "mg", "range"), + "Natri": ("natri", "natri", "mg", "max"), + "Phốt pho": ("photpho", "photpho", "mg", "max"), + + # --- Khác --- + "Cholesterol": ("cholesterol", "cholesterol", "mg", "max"), + "Choline": ("choline", "choline", "mg", "min"), + "Caffeine": ("caffeine", "caffeine", "mg", "max"), + "Alcohol": ("alcohol", "alcohol", "g", "max"), + } + + scored_list = [] + + for doc in candidates: + item = doc.metadata + score = 0 + reasons = [] # Lưu lý do để debug hoặc giải thích cho user + + # --- 1. CHẤM ĐIỂM NHÓM "BỔ SUNG" (BOOST) --- + # Logic: Càng nhiều càng tốt + for nutrient in user_profile.get('Bổ sung', []): + config = nutrient_config.get(nutrient) + if not config: continue + + p_key, db_key, unit, logic = config + + # Lấy giá trị thực tế trong món ăn và mục tiêu + val = float(item.get(db_key, 0)) + daily_target = float(user_profile.get(p_key, 0)) + meal_target = daily_target * meal_ratio + + if meal_target == 0: continue + + # Chấm điểm + # Nếu đạt > 50% target bữa -> +10 điểm + if val >= meal_target * 0.5: + score += 10 + reasons.append(f"Giàu {nutrient}") + # Nếu đạt > 80% target -> +15 điểm (thưởng thêm) + if val >= meal_target * 0.8: + score += 5 + + # --- 2. CHẤM ĐIỂM NHÓM "HẠN CHẾ" & "KIÊNG" (PENALTY/REWARD) --- + # Gộp chung vì logic giống nhau: Càng thấp càng tốt + check_list = set(user_profile.get('Hạn chế', []) + user_profile.get('Kiêng', [])) + + for nutrient in check_list: + config = nutrient_config.get(nutrient) + if not config: continue + + p_key, db_key, unit, logic = config + val = float(item.get(db_key, 0)) + daily_target = float(user_profile.get(p_key, 0)) + meal_target = daily_target * meal_ratio + + if meal_target == 0: continue + + if logic == 'max': + # Nếu thấp hơn target -> +10 điểm (Tốt) + if val <= meal_target: + score += 10 + # Nếu thấp hơn hẳn (chỉ bằng 50% target) -> +15 điểm (Rất an toàn) + if val <= meal_target * 0.5: + score += 5 + # Nếu vượt quá target -> -10 điểm (Phạt) + if val > meal_target: + score -= 10 + + elif logic == 'range': + # Logic cho Kali/Protein: Tốt nhất là nằm trong khoảng, không thấp quá, không cao quá + min_safe = meal_target * 0.5 + max_safe = meal_target * 1.5 + + if min_safe <= val <= max_safe: + score += 10 # Nằm trong vùng an toàn + elif val > max_safe: + score -= 10 # Cao quá (nguy hiểm cho thận) + # Thấp quá thì không trừ điểm nặng, chỉ không được cộng + + # --- 3. ĐIỂM THƯỞNG CHO SỰ PHÙ HỢP CƠ BẢN (BASE HEALTH) --- + # Ít đường (< 5g) -> +2 điểm + if float(item.get('sugar', 0)) < 5: score += 2 + + # Ít saturated fat (< 3g) -> +2 điểm + if float(item.get('saturated_fat', 0)) < 3: score += 2 + + # Giàu xơ (> 3g) -> +3 điểm + if float(item.get('fiber', 0)) > 3: score += 3 + + # Lưu kết quả + item_copy = item.copy() + item_copy["health_score"] = score + item_copy["score_reason"] = ", ".join(reasons[:3]) # Chỉ lấy 3 lý do chính + scored_list.append(item_copy) + + # 4. SẮP XẾP & TRẢ VỀ + # Sort giảm dần theo điểm (Điểm cao nhất lên đầu) + scored_list.sort(key=lambda x: x["health_score"], reverse=True) + + # # Debug: In Top 3 + # logger.info("🏆 Top 3 Món Tốt Nhất (Sau khi chấm điểm):") + # for i, m in enumerate(scored_list[:3]): + # logger.info(f" {i+1}. {m['name']} (Score: {m['health_score']}) | {m.get('score_reason')}") + + return scored_list \ No newline at end of file diff --git a/chatbot/agents/nodes/app_functions/get_profile.py b/chatbot/agents/nodes/app_functions/get_profile.py new file mode 100644 index 0000000000000000000000000000000000000000..bcac0570eb9852d49ef20873cfa8f3a61fde783a --- /dev/null +++ b/chatbot/agents/nodes/app_functions/get_profile.py @@ -0,0 +1,22 @@ +import logging + +from chatbot.agents.states.state import AgentState +from chatbot.utils.user_profile import get_user_by_id +from chatbot.utils.restriction import get_restrictions + +# --- Cấu hình logging --- +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +def get_user_profile(state: AgentState): + logger.info("---NODE: GET USER PROFILE---") + user_id = state.get("user_id", 1) + + raw_profile = get_user_by_id(user_id) + restrictions = get_restrictions(raw_profile["healthStatus"]) + + final_profile = {**raw_profile, **restrictions} + + logger.info(f"Tổng hợp user profile cho user_id={user_id} thành công!") + + return {"user_profile": final_profile} diff --git a/chatbot/agents/nodes/app_functions/optimize_macros.py b/chatbot/agents/nodes/app_functions/optimize_macros.py new file mode 100644 index 0000000000000000000000000000000000000000..f6df8eb728a364aa253d24e623dce17b6751f04b --- /dev/null +++ b/chatbot/agents/nodes/app_functions/optimize_macros.py @@ -0,0 +1,204 @@ +import logging +from chatbot.agents.states.state import AgentState +import numpy as np +from scipy.optimize import minimize + +# --- Cấu hình logging --- +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +def optimize_portions_scipy(state: AgentState): + logger.info("---NODE: SCIPY OPTIMIZER (FINAL VERSION)---") + profile = state.get("user_profile", {}) + menu = state.get("selected_structure", []) + + if not menu: + print("⚠️ Menu rỗng, bỏ qua tối ưu hóa.") + return {"final_menu": []} + + # --- BƯỚC 1: XÁC ĐỊNH MỤC TIÊU TỐI ƯU HÓA (CRITICAL STEP) --- + # Lấy Target Ngày gốc + daily_targets = np.array([ + float(profile.get("targetcalories", 1314)), + float(profile.get("protein", 98)), + float(profile.get("totalfat", 43)), + float(profile.get("carbohydrate", 131)) + ]) + + # Tỷ lệ các bữa + meal_ratios = {"sáng": 0.25, "trưa": 0.40, "tối": 0.35} + + # Xác định các bữa có trong menu hiện tại + generated_meals = set(d.get("assigned_meal", "").lower() for d in menu) + + # Tính Target Thực Tế (Optimization Target) + # Ví dụ: Nếu chỉ có bữa Trưa -> Target = Daily * 0.4 + # Nếu có Trưa + Tối -> Target = Daily * (0.4 + 0.35) + active_target = np.zeros(4) + active_ratios_sum = 0 + + for m in ["sáng", "trưa", "tối"]: + if m in generated_meals: + active_target += daily_targets * meal_ratios[m] + active_ratios_sum += meal_ratios[m] + + # Fallback: Nếu không xác định được bữa nào, dùng Target Ngày + if np.sum(active_target) == 0: + active_target = daily_targets + + logger.info(f" 🎯 Mục tiêu tối ưu hóa (Active Target): {active_target.astype(int)}") + + # --- BƯỚC 2: THIẾT LẬP MA TRẬN & BOUNDS --- + matrix = [] + bounds = [] + meal_indices = {"sáng": [], "trưa": [], "tối": []} + + # Tính target riêng từng bữa để dùng cho Distribution Loss + target_kcal_per_meal = { + k: daily_targets[0] * v for k, v in meal_ratios.items() + } + + for i, dish in enumerate(menu): + nutrients = [ + float(dish.get("kcal", 0)), + float(dish.get("protein", 0)), + float(dish.get("lipid", 0)), + float(dish.get("carbohydrate", 0)) + ] + matrix.append(nutrients) + + # Logic Bounds Thông minh + current_kcal = nutrients[0] + t_meal_name = dish.get("assigned_meal", "").lower() + t_meal_target = target_kcal_per_meal.get(t_meal_name, 500) + + # Nếu 1 món chiếm > 90% Kcal mục tiêu của bữa đó -> Phải cho giảm sâu + if current_kcal > (t_meal_target * 0.9): + bounds.append((0.3, 1.0)) + elif "solver_bounds" in dish: + bounds.append(dish["solver_bounds"]) + else: + bounds.append((0.5, 1.5)) + + if "sáng" in t_meal_name: meal_indices["sáng"].append(i) + elif "trưa" in t_meal_name: meal_indices["trưa"].append(i) + elif "tối" in t_meal_name: meal_indices["tối"].append(i) + + matrix = np.array(matrix).T + n_dishes = len(menu) + initial_guess = np.ones(n_dishes) + + # --- BƯỚC 3: ADAPTIVE WEIGHTS (TRÁNH BẪY LIPID) --- + # Kiểm tra tính khả thi: Liệu menu này có ĐỦ chất để đạt target không? + + # Tính dinh dưỡng tối đa có thể đạt được (nếu ăn x2.5 suất tất cả) + max_possible = matrix.dot(np.full(n_dishes, 2.5)) + + # Trọng số mặc định: [Kcal, P, L, C] + adaptive_weights = np.array([3.0, 2.0, 1.0, 1.0]) + nutri_names = ["Kcal", "Protein", "Lipid", "Carb"] + + for i in range(1, 4): # Check P, L, C + # Nếu Max khả thi vẫn < 70% Target -> Menu này quá thiếu chất đó + # -> Giảm trọng số về gần 0 để Solver không cố gắng cứu nó + if max_possible[i] < (active_target[i] * 0.7): + 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.") + adaptive_weights[i] = 0.01 + + # --- BƯỚC 4: LOSS FUNCTION --- + def objective(portions): + # A. Loss Macro (So với Active Target) + current_macros = matrix.dot(portions) + + # Dùng adaptive_weights để tránh bẫy + diff = (current_macros - active_target) / (active_target + 1e-5) + loss_macro = np.sum(adaptive_weights * (diff ** 2)) + + # B. Loss Phân bổ Bữa ăn (Chỉ cần thiết nếu sinh nhiều bữa) + loss_dist = 0 + if active_ratios_sum > 0.5: # Chỉ tính nếu sinh > 1 bữa + kcal_row = matrix[0] + for m_type, indices in meal_indices.items(): + if not indices: continue + current_meal_kcal = np.sum(kcal_row[indices] * portions[indices]) + target_meal = target_kcal_per_meal.get(m_type, 0) + d = (current_meal_kcal - target_meal) / (target_meal + 1e-5) + loss_dist += (d ** 2) + + return loss_macro + (1.5 * loss_dist) + + # 5. Run Optimization + res = minimize(objective, initial_guess, method='SLSQP', bounds=bounds) + + # 6. Apply Results + optimized_portions = res.x + final_menu = [] + total_stats = np.zeros(4) + achieved_meal_kcal = {"sáng": 0, "trưa": 0, "tối": 0} + + for i, dish in enumerate(menu): + ratio = optimized_portions[i] + final_dish = dish.copy() + + final_dish["portion_scale"] = float(round(ratio, 2)) + final_dish["final_kcal"] = int(dish.get("kcal", 0) * ratio) + final_dish["final_protein"] = int(dish.get("protein", 0) * ratio) + final_dish["final_lipid"] = int(dish.get("lipid", 0) * ratio) + final_dish["final_carb"] = int(dish.get("carbohydrate", 0) * ratio) + + 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") + + final_menu.append(final_dish) + total_stats += np.array([ + final_dish["final_kcal"], final_dish["final_protein"], + final_dish["final_lipid"], final_dish["final_carb"] + ]) + + m_type = dish.get("assigned_meal", "").lower() + if "sáng" in m_type: achieved_meal_kcal["sáng"] += final_dish["final_kcal"] + elif "trưa" in m_type: achieved_meal_kcal["trưa"] += final_dish["final_kcal"] + elif "tối" in m_type: achieved_meal_kcal["tối"] += final_dish["final_kcal"] + + # --- BƯỚC 7: BÁO CÁO KẾT QUẢ --- + logger.info("\n 📊 BÁO CÁO TỐI ƯU HÓA CHI TIẾT:") + + headers = ["Chỉ số", "Mục tiêu (Bữa)", "Kết quả", "Độ lệch"] + row_format = " | {:<12} | {:<15} | {:<15} | {:<15} |" + logger.info(" " + "-"*65) + logger.info(row_format.format(*headers)) + logger.info(" " + "-"*65) + + labels = ["Năng lượng", "Protein", "Lipid", "Carb"] + units = ["Kcal", "g", "g", "g"] + + for i in range(4): + t_val = int(active_target[i]) # So sánh với Active Target + r_val = int(total_stats[i]) + diff = r_val - t_val + diff_str = f"{diff:+d} {units[i]}" + + status = "" + percent_diff = abs(diff) / (t_val + 1e-5) + # 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) + if percent_diff > 0.15 and adaptive_weights[i] > 0.1: status = "⚠️" + else: status = "✅" + + logger.info(row_format.format( + labels[i], + f"{t_val} {units[i]}", + f"{r_val} {units[i]}", + f"{diff_str} {status}" + )) + logger.info(" " + "-"*65) + + logger.info("\n ⚖️ PHÂN BỔ TỪNG BỮA (Kcal):") + for meal in ["sáng", "trưa", "tối"]: + if meal in generated_meals: + t_meal = int(target_kcal_per_meal[meal]) + r_meal = int(achieved_meal_kcal[meal]) + logger.info(f" - {meal.capitalize():<5}: Đạt {r_meal} / {t_meal} Kcal") + + return { + "final_menu": final_menu, + "user_profile": profile + } \ No newline at end of file diff --git a/chatbot/agents/nodes/app_functions/optimize_select.py b/chatbot/agents/nodes/app_functions/optimize_select.py new file mode 100644 index 0000000000000000000000000000000000000000..3da1dd1144570dde63d4852deb57a1bfad1f1234 --- /dev/null +++ b/chatbot/agents/nodes/app_functions/optimize_select.py @@ -0,0 +1,76 @@ +from scipy.optimize import minimize_scalar +from chatbot.agents.states.state import SwapState +import numpy as np +import logging + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +def calculate_top_options(state: SwapState): + logger.info("---NODE: SCIPY RANKING (MATH FILTER)---") + candidates = state["candidates"] + food_old = state["food_old"] + + if not candidates: return {"top_candidates": []} + + # 1. Xác định "KPI" từ món cũ + old_scale = float(food_old.get("portion_scale", 1.0)) + target_vector = np.array([ + float(food_old.get("kcal", 0)) * old_scale, + float(food_old.get("protein", 0)) * old_scale, + float(food_old.get("lipid", 0)) * old_scale, + float(food_old.get("carbohydrate", 0)) * old_scale + ]) + weights = np.array([3.0, 2.0, 1.0, 1.0]) + + # Bound của món cũ + bounds = food_old.get("solver_bounds", (0.5, 2.0)) + + # Hàm tính toán (giữ nguyên logic cũ) + def calculate_score(candidate): + base_vector = np.array([ + float(candidate.get("kcal", 0)), + float(candidate.get("protein", 0)), + float(candidate.get("lipid", 0)), + float(candidate.get("carbohydrate", 0)) + ]) + if np.sum(base_vector) == 0: return float('inf'), 1.0 + + def objective(x): + current_vector = base_vector * x + diff = (current_vector - target_vector) / (target_vector + 1e-5) + loss = np.sum(weights * (diff ** 2)) + return loss + + res = minimize_scalar(objective, bounds=bounds, method='bounded') + return res.fun, res.x + + # 3. Chấm điểm hàng loạt + scored_candidates = [] + for item in candidates: + loss, scale = calculate_score(item) + + # Chỉ lấy những món có sai số chấp nhận được (Loss < 5.0) + if loss < 5.0: + item_score = item.copy() + item_score["optimization_loss"] = round(loss, 4) + item_score["portion_scale"] = round(scale, 2) + + # Tính chỉ số hiển thị sau khi scale + item_score["final_kcal"] = int(item["kcal"] * scale) + item_score["final_protein"] = int(item["protein"] * scale) + item_score["final_lipid"] = int(item["lipid"] * scale) + item_score["final_carb"] = int(item["carbohydrate"] * scale) + + scored_candidates.append(item_score) + + # 4. Lấy Top 10 tốt nhất + # Sắp xếp theo Loss (thấp nhất là giống dinh dưỡng nhất) + scored_candidates.sort(key=lambda x: x["optimization_loss"]) + top_10 = scored_candidates[:10] + + logger.info(f"📊 Scipy đã lọc ra {len(top_10)} ứng viên tiềm năng.") + for item in top_10: + logger.info(f" - {item['name']} (Scale x{item['portion_scale']} | Loss: {item['optimization_loss']})") + + return {"top_candidates": top_10} \ No newline at end of file diff --git a/chatbot/agents/nodes/app_functions/select_meal.py b/chatbot/agents/nodes/app_functions/select_meal.py new file mode 100644 index 0000000000000000000000000000000000000000..37fb8b79d49fabedd79d164309e939ead1138a2d --- /dev/null +++ b/chatbot/agents/nodes/app_functions/select_meal.py @@ -0,0 +1,129 @@ +from langchain_core.pydantic_v1 import BaseModel, Field +from chatbot.models.llm_setup import llm +from chatbot.agents.states.state import SwapState +import logging + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +class ChefDecision(BaseModel): + # Thay đổi tên trường cho rõ nghĩa + selected_meal_id: int = Field(description="ID (meal_id) của món ăn được chọn từ danh sách") + reason: str = Field(description="Lý do ẩm thực ngắn gọn") + +def llm_finalize_choice(state: SwapState): + logger.info("---NODE: LLM FINAL SELECTION (BY REAL MEAL_ID)---") + top_candidates = state["top_candidates"] + food_old = state["food_old"] + + if not top_candidates: return {"best_replacement": None} + + # 1. Format danh sách hiển thị kèm Real ID + options_text = "" + for item in top_candidates: + # Lấy meal_id thực tế từ dữ liệu + real_id = item.get("meal_id") + + options_text += ( + f"ID [{real_id}] - {item['name']}\n" # <--- Hiển thị ID thật + f" - Số liệu: {item['final_kcal']} Kcal | P:{item['final_protein']}g | L:{item['final_lipid']}g | C:{item['final_carb']}g\n" + f" - Độ lệch (Loss): {item['optimization_loss']}\n" + ) + + # 2. Prompt cập nhật + system_prompt = f""" + Bạn là Bếp trưởng. Người dùng muốn đổi món '{food_old.get('name')}'. + Dưới đây là các ứng viên thay thế: + + {options_text} + + NHIỆM VỤ: + 1. Chọn ra 1 món thay thế tốt nhất về mặt ẩm thực. + 2. Trả về chính xác ID (số trong ngoặc vuông []) của món đó. + """ + + # 3. Gọi LLM + try: + llm_structured = llm.with_structured_output(ChefDecision) + decision = llm_structured.invoke(system_prompt) + target_id = decision.selected_meal_id + except Exception as e: + logger.info(f"⚠️ LLM Error: {e}. Fallback to first option.") + # Fallback lấy ID của món đầu tiên + target_id = top_candidates[0].get("meal_id") + decision = ChefDecision(selected_meal_id=target_id, reason="Fallback do lỗi hệ thống.") + + # 4. Mapping lại bằng meal_id (Chính xác tuyệt đối) + selected_full_candidate = None + + for item in top_candidates: + # So sánh ID (lưu ý ép kiểu nếu cần thiết để tránh lỗi string vs int) + if int(item.get("meal_id")) == int(target_id): + selected_full_candidate = item + break + + # Fallback an toàn + if not selected_full_candidate: + logger.info(f"⚠️ ID {target_id} không tồn tại trong list. Chọn món Top 1.") + selected_full_candidate = top_candidates[0] + + # Bổ sung lý do + selected_full_candidate["chef_reason"] = decision.reason + + # Bổ sung lý do + selected_full_candidate["chef_reason"] = decision.reason + + #------------------------------------------------------------------- + # --- PHẦN MỚI: IN BẢNG SO SÁNH (VISUAL COMPARISON) --- + logger.info(f"\n✅ CHEF SELECTED: {selected_full_candidate['name']} (ID: {selected_full_candidate['meal_id']})") + logger.info(f"📝 Lý do: {decision.reason}") + + # Lấy thông tin món cũ (đã scale ở menu gốc) + # Lưu ý: food_old trong state là thông tin gốc hoặc đã tính toán ở daily menu + old_kcal = float(food_old.get('final_kcal', food_old['kcal'])) + old_pro = float(food_old.get('final_protein', food_old['protein'])) + old_fat = float(food_old.get('final_lipid', food_old['lipid'])) + old_carb = float(food_old.get('final_carb', food_old['carbohydrate'])) + + # Lấy thông tin món mới (đã re-scale bởi Scipy) + new_kcal = selected_full_candidate['final_kcal'] + new_pro = selected_full_candidate['final_protein'] + new_fat = selected_full_candidate['final_lipid'] + new_carb = selected_full_candidate['final_carb'] + scale = selected_full_candidate['portion_scale'] + + # In bảng + logger.info("\n 📊 BẢNG SO SÁNH THAY THẾ:") + headers = ["Chỉ số", "Món Cũ (Gốc)", "Món Mới (Re-scale)", "Chênh lệch"] + row_fmt = " | {:<10} | {:<15} | {:<20} | {:<12} |" + + logger.info(" " + "-"*68) + logger.info(row_fmt.format(*headers)) + logger.info(" " + "-"*68) + + # Helper in dòng + def print_row(label, old_val, new_val, unit=""): + diff = new_val - old_val + diff_str = f"{diff:+.1f}" + + # Đánh dấu màu (Logic text) + status = "✅" + # Nếu lệch > 20% thì cảnh báo + if old_val > 0 and abs(diff)/old_val > 0.2: status = "⚠️" + + logger.info(row_fmt.format( + label, + f"{old_val:.0f} {unit}", + f"{new_val:.0f} {unit} (x{scale} suất)", + f"{diff_str} {status}" + )) + + print_row("Năng lượng", old_kcal, new_kcal, "Kcal") + print_row("Protein", old_pro, new_pro, "g") + print_row("Lipid", old_fat, new_fat, "g") + print_row("Carb", old_carb, new_carb, "g") + logger.info(" " + "-"*68) + + logger.info(f"✅ Chef Selected: ID {selected_full_candidate['meal_id']} - {selected_full_candidate['name']}") + + return {"best_replacement": selected_full_candidate} \ No newline at end of file diff --git a/chatbot/agents/nodes/app_functions/select_menu.py b/chatbot/agents/nodes/app_functions/select_menu.py new file mode 100644 index 0000000000000000000000000000000000000000..0d282bd83c6348d5a2b9739a16504908e0c4bc8a --- /dev/null +++ b/chatbot/agents/nodes/app_functions/select_menu.py @@ -0,0 +1,237 @@ +from langchain_core.pydantic_v1 import BaseModel, Field +from typing import Literal, List +from collections import defaultdict +import logging +from chatbot.agents.states.state import AgentState +from chatbot.models.llm_setup import llm + +# --- Cấu hình logging --- +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# --- DATA MODELS --- +class SelectedDish(BaseModel): + name: str = Field(description="Tên món ăn chính xác trong danh sách") + meal_type: str = Field(description="Bữa ăn (sáng/trưa/tối)") + role: Literal["main", "carb", "side"] = Field( + description="Vai trò: 'main' (Món mặn/Đạm), 'carb' (Cơm/Tinh bột), 'side' (Rau/Canh)" + ) + reason: str = Field(description="Lý do chọn (ngắn gọn)") + +class DailyMenuStructure(BaseModel): + dishes: List[SelectedDish] = Field(description="Danh sách các món ăn được chọn") + +# --- NODE LOGIC --- +def select_menu_structure(state: AgentState): + logger.info("---NODE: AI SELECTOR (FULL MACRO AWARE)---") + profile = state["user_profile"] + candidates = state.get("candidate_pool", []) + meals_req = state["meals_to_generate"] + + if len(candidates) == 0: + logger.warning("⚠️ Danh sách ứng viên rỗng, không thể chọn món.") + return {"selected_structure": []} + + # 1. TÍNH TOÁN MỤC TIÊU CHI TIẾT TỪNG BỮA (Budgeting) + daily_targets = { + "kcal": float(profile.get('targetcalories', 2000)), + "protein": float(profile.get('protein', 150)), + "lipid": float(profile.get('totalfat', 60)), + "carbohydrate": float(profile.get('carbohydrate', 200)) + } + ratios = {"sáng": 0.25, "trưa": 0.40, "tối": 0.35} + + # Tính target chi tiết cho từng bữa + # Kết quả dạng: {'sáng': {'kcal': 500, 'protein': 37.5, ...}, 'trưa': ...} + meal_targets = {} + for meal, ratio in ratios.items(): + meal_targets[meal] = { + k: int(v * ratio) for k, v in daily_targets.items() + } + + # --- LOGIC TẠO HƯỚNG DẪN ĐỘNG --- + health_condition = profile.get('healthStatus', 'Bình thường') + safety_instruction = f""" + - Tình trạng sức khỏe: {health_condition}. + - Ư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. + """ + + # 2. TIỀN XỬ LÝ & PHÂN NHÓM CANDIDATES + candidates_by_meal = {"sáng": [], "trưa": [], "tối": []} + + for m in candidates: + if m.get('kcal', 0) > 1500: continue + if m.get('kcal', 0) < 100: continue + + tag = m.get('meal_type_tag', '').lower() + if "sáng" in tag: candidates_by_meal["sáng"].append(m) + elif "trưa" in tag: candidates_by_meal["trưa"].append(m) + elif "tối" in tag: candidates_by_meal["tối"].append(m) + + def format_list(items): + if not items: return "" + return "\n".join([ + f"- {m['name']}: {m.get('kcal')} kcal | P:{m.get('protein')}g | L:{m.get('lipid')}g | C:{m.get('carbohydrate')}g" + for m in items + ]) + + def get_target_str(meal): + t = meal_targets.get(meal, {}) + return f"{t.get('kcal')} Kcal (P: {t.get('protein')}g, L: {t.get('lipid')}g, C: {t.get('carbohydrate')}g)" + + # 3. XÂY DỰNG PROMPT (Kèm full chỉ số P/L/C) + guidance_sang = "" + if 'sáng' in meals_req: + guidance_sang = f"""BỮA SÁNG (Mục tiêu ~{get_target_str('sáng')}): + - Chọn 1 món chính có năng lượng ĐỦ LỚN (gần {get_target_str('sáng')}). + - Có thể bổ sung 1 món phụ sao cho dinh dưỡng cân bằng. + - Ư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.""" + + guidance_trua = "" + if 'trưa' in meals_req: + guidance_trua = f"""BỮA TRƯA (Mục tiêu ~{get_target_str('trưa')}): + - Chọn tổ hợp gồm 3 món: + 1. Main: Món cung cấp Protein chính. + 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 đà). + 3. Side: Rau/Canh để bổ sung Xơ. + - 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. + - 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).""" + + guidance_toi = "" + if 'tối' in meals_req: + guidance_toi = f"""BỮA TỐI (Mục tiêu ~{get_target_str('tối')}): + - Tương tự như bữa trưa. + - Ưu tiên các món nhẹ bụng, dễ tiêu hóa. + - Giảm lượng tinh bột so với bữa trưa.""" + + # 2. Ghép vào prompt chính + system_prompt = f""" + Bạn là Chuyên gia Dinh dưỡng AI. + 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. + + 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. + + NGUYÊN TẮC CỐT LÕI: + 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. + 2. Cảm quan đầu bếp: Món ăn phải hợp vị (VD: Canh chua đi với Cá kho). + 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. + + NGUYÊN TẮC AN TOÀN: + 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ủ: + {safety_instruction} + + HƯỚNG DẪN TỪNG BỮA + {guidance_sang} + {guidance_trua} + {guidance_toi} + + DANH SÁCH ỨNG VIÊN + {format_list(candidates_by_meal['sáng'])} + {format_list(candidates_by_meal['trưa'])} + {format_list(candidates_by_meal['tối'])} + """ + + logger.info("Prompt:") + logger.info(system_prompt) + + # Gọi LLM + llm_structured = llm.with_structured_output(DailyMenuStructure, strict=True) + result = llm_structured.invoke(system_prompt) + + # In danh sách các món đã chọn lần lượt theo bữa + def print_menu_by_meal(daily_menu): + menu_by_meal = defaultdict(list) + for dish in daily_menu.dishes: + menu_by_meal[dish.meal_type.lower()].append(dish) + meal_order = ["sáng", "trưa", "tối"] + for meal in meal_order: + if meal in menu_by_meal: + logger.info(f"\n🍽 Bữa {meal.upper()}:") + for d in menu_by_meal[meal]: + logger.info(f" - {d.name} ({d.role}): {d.reason}") + + logger.info("\n--- MENU ĐÃ CHỌN ---") + print_menu_by_meal(result) + + # 4. HẬU XỬ LÝ (Gán Bounds) + selected_full_info = [] + all_clean_candidates = [] + for sublist in candidates_by_meal.values(): + all_clean_candidates.extend(sublist) + candidate_map = {m['name']: m for m in all_clean_candidates} + + for choice in result.dishes: + if choice.name in candidate_map: + dish_data = candidate_map[choice.name].copy() + dish_data["assigned_meal"] = choice.meal_type + + # Lấy thông tin dinh dưỡng món hiện tại + d_kcal = float(dish_data.get("kcal", 0)) + d_pro = float(dish_data.get("protein", 0)) + + # Lấy target bữa hiện tại (VD: Trưa) + t_target = meal_targets.get(choice.meal_type.lower(), {}) + t_kcal = t_target.get("kcal", 500) + t_pro = t_target.get("protein", 30) + + # --- GIAI ĐOẠN 1: TỰ ĐỘNG SỬA SAI VAI TRÒ (ROLE CORRECTION) --- + final_role = choice.role # Bắt đầu bằng role AI chọn + + # 1. Phát hiện "Carb trá hình" (Cơm chiên/Mì xào quá nhiều thịt) + if final_role == "carb" and d_pro > 15: + print(f" ⚠️ Phát hiện Carb giàu đạm ({choice.name}: {d_pro}g Pro). Đổi role sang 'main'.") + final_role = "main" + + # 2. Phát hiện "Side giàu đạm" (Salad gà/bò, Canh sườn) + elif final_role == "side" and d_pro > 10: + print(f" ⚠️ Phát hiện Side giàu đạm ({choice.name}: {d_pro}g Pro). Đổi role sang 'main'.") + final_role = "main" + + # Cập nhật lại role chuẩn vào dữ liệu + dish_data["role"] = final_role + + + # --- GIAI ĐOẠN 2: THIẾT LẬP BOUNDS CƠ BẢN (BASE BOUNDS) --- + lower_bound = 0.5 + upper_bound = 1.5 + + if final_role == "carb": + # Cơm/Bún thuần: Cho phép co dãn cực mạnh để bù Kcal + lower_bound, upper_bound = 0.4, 3.0 + + elif final_role == "side": + # Rau/Canh: Co dãn rộng để bù thể tích ăn + lower_bound, upper_bound = 0.5, 2.0 + + elif final_role == "main": + # Món mặn: Co dãn vừa phải để giữ hương vị + lower_bound, upper_bound = 0.6, 1.8 + + + # --- GIAI ĐOẠN 3: KIỂM TRA AN TOÀN & GHI ĐÈ --- + + # Override A: Nếu món Main có Protein quá khủng so với Target + # (VD: Món 52g Pro vs Target Bữa 30g Pro) -> Phải cho phép giảm sâu + if final_role == "main" and d_pro > t_pro: + print(f" ⚠️ Món {choice.name} thừa đạm ({d_pro}g > {t_pro}g). Mở rộng bound xuống thấp.") + lower_bound = 0.3 # Cho phép giảm xuống 30% suất + upper_bound = min(upper_bound, 1.2) # Không cho phép tăng quá nhiều + + # Override B: Nếu món quá nhiều Calo (Chiếm > 80% Kcal cả bữa) + if d_kcal > (t_kcal * 0.8): + print(f" ⚠️ Món {choice.name} quá đậm năng lượng ({d_kcal} kcal). Siết chặt bound.") + lower_bound = 0.3 + upper_bound = min(upper_bound, 1.0) # Chặn không cho tăng + + # Override C: Nếu là món Side nhưng Protein vẫn hơi cao (5-10g) + # Cho phép giảm để nhường quota Protein cho món Main + if final_role == "side" and d_pro > 5: + lower_bound = 0.2 # Cho phép ăn ít rau này lại + + # --- KẾT THÚC: GÁN VÀO DỮ LIỆU --- + dish_data["solver_bounds"] = (lower_bound, upper_bound) + selected_full_info.append(dish_data) + + return { + "selected_structure": selected_full_info, + } \ No newline at end of file diff --git a/chatbot/agents/nodes/chatbot/__init__.py b/chatbot/agents/nodes/chatbot/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f31132e55444905f77a436785344124239e227a0 --- /dev/null +++ b/chatbot/agents/nodes/chatbot/__init__.py @@ -0,0 +1,25 @@ +from .classify_topic import classify_topic, route_by_topic +from .meal_identify import meal_identify +from .suggest_meal_node import suggest_meal_node +from .food_query import food_query +from .select_food_plan import select_food_plan +from .general_chat import general_chat +from .generate_final_response import generate_final_response +from .food_suggestion import food_suggestion +from .policy import policy +from .select_food import select_food + + +__all__ = [ + "classify_topic", + "route_by_topic", + "meal_identify", + "suggest_meal_node", + "food_query", + "select_food_plan", + "general_chat", + "generate_final_response", + "food_suggestion", + "policy", + "select_food", +] \ No newline at end of file diff --git a/chatbot/agents/nodes/classify_topic.py b/chatbot/agents/nodes/chatbot/classify_topic.py similarity index 56% rename from chatbot/agents/nodes/classify_topic.py rename to chatbot/agents/nodes/chatbot/classify_topic.py index 733ab0af42256b9db9976f308fd66f03650600af..84d0fb7ff4eaed9044140e51042440d72e9f0e4a 100644 --- a/chatbot/agents/nodes/classify_topic.py +++ b/chatbot/agents/nodes/chatbot/classify_topic.py @@ -1,60 +1,71 @@ -from langchain.prompts import PromptTemplate -import json -from pydantic import BaseModel, Field -from chatbot.agents.states.state import AgentState -from chatbot.models.llm_setup import llm - -class Topic(BaseModel): - name: str = Field( - description=( - "Tên chủ đề mà người dùng đang hỏi. " - "Các giá trị hợp lệ: 'nutrition_analysis', 'meal_suggestion', 'general_chat'." - ) - ) - -def classify_topic(state: AgentState): - print("---CLASSIFY TOPIC---") - llm_with_structure_op = llm.with_structured_output(Topic) - - prompt = PromptTemplate( - template=""" - 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. - - Nhiệm vụ: - - Phân loại câu hỏi vào một trong các nhóm: - 1. "meal_suggestion": khi người dùng muốn gợi ý thực đơn cho một bữa ăn, khẩu phần, hoặc chế độ ăn (chỉ cho bữa ăn, không cho món ăn đơn lẻ). - 2. "food_query": khi người dùng tìm kiếm, gợi ý một món ăn hoặc muốn biết thành phần dinh dưỡng của món ăn hoặc khẩu phần cụ thể. - 3. "general_chat": khi câu hỏi không thuộc hai nhóm trên. - - Câu hỏi người dùng: {question} - - Hãy trả lời dưới dạng JSON phù hợp với schema sau: - {format_instructions} - """ - ) - - messages = state["messages"] - user_message = messages[-1].content if messages else state.question - - format_instructions = json.dumps(llm_with_structure_op.output_schema.model_json_schema(), ensure_ascii=False, indent=2) - - chain = prompt | llm_with_structure_op - - topic_result = chain.invoke({ - "question": user_message, - "format_instructions": format_instructions - }) - - print("Topic:", topic_result.name) - - return {"topic": topic_result.name} - -def route_by_topic(state: AgentState): - topic = state["topic"] - if topic == "meal_suggestion": - return "meal_identify" - elif topic == "food_query": - return "food_query" - else: - return "general_chat" +from langchain.prompts import PromptTemplate +import json +from pydantic import BaseModel, Field +from chatbot.agents.states.state import AgentState +from chatbot.models.llm_setup import llm +import logging + +# --- Cấu hình logging --- +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +class Topic(BaseModel): + name: str = Field( + description=( + "Tên chủ đề mà người dùng đang hỏi. " + "Các giá trị hợp lệ: 'meal_suggestion', 'food_suggestion', food_query, 'policy', 'general_chat'." + ) + ) + +def classify_topic(state: AgentState): + logger.info("---CLASSIFY TOPIC---") + llm_with_structure_op = llm.with_structured_output(Topic) + + prompt = PromptTemplate( + template=""" + 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. + + Nhiệm vụ: + - Phân loại câu hỏi vào một trong các nhóm: + 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ẻ). + 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 đó). + 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 + 4. "policy": khi người dùng muốn biết các thông tin liên quan đến app. + 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. + + Câu hỏi người dùng: {question} + + Hãy trả lời dưới dạng JSON phù hợp với schema sau: + {format_instructions} + """ + ) + + messages = state["messages"] + user_message = messages[-1].content if messages else state.question + + format_instructions = json.dumps(llm_with_structure_op.output_schema.model_json_schema(), ensure_ascii=False, indent=2) + + chain = prompt | llm_with_structure_op + + topic_result = chain.invoke({ + "question": user_message, + "format_instructions": format_instructions + }) + + logger.info(f"Topic: {topic_result.name}") + + return {"topic": topic_result.name} + +def route_by_topic(state: AgentState): + topic = state["topic"] + if topic == "meal_suggestion": + return "meal_identify" + elif topic == "food_suggestion": + return "food_suggestion" + elif topic == "food_query": + return "food_query" + elif topic == "policy": + return "policy" + else: + return "general_chat" \ No newline at end of file diff --git a/chatbot/agents/nodes/chatbot/food_query.py b/chatbot/agents/nodes/chatbot/food_query.py new file mode 100644 index 0000000000000000000000000000000000000000..11bb3f4aa70de9decf44bc25826d08511f40bd9b --- /dev/null +++ b/chatbot/agents/nodes/chatbot/food_query.py @@ -0,0 +1,39 @@ +from chatbot.agents.states.state import AgentState +from chatbot.agents.tools.food_retriever import query_constructor, docsearch +from langchain.retrievers.self_query.elasticsearch import ElasticsearchTranslator +from langchain.retrievers.self_query.base import SelfQueryRetriever +import logging + +# --- Cấu hình logging --- +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +def food_query(state: AgentState): + logger.info("---FOOD QUERY---") + + messages = state["messages"] + user_message = messages[-1].content if messages else state.question + + suggested_meals = [] + + prompt = f""" + Câu hỏi: "{user_message}". + Hãy tìm các món ăn phù hợp với yêu cầu này. + """ + + query_ans = query_constructor.invoke(prompt) + food_retriever = SelfQueryRetriever( + query_constructor=query_constructor, + vectorstore=docsearch, + structured_query_translator=ElasticsearchTranslator(), + search_kwargs={"k": 3}, + ) + logger.info(f"🔍 Dạng truy vấn: {food_retriever.structured_query_translator.visit_structured_query(structured_query=query_ans)}") + + foods = food_retriever.invoke(prompt) + logger.info(f"🔍 Kết quả truy vấn: ") + for i, food in enumerate(foods): + logger.info(f"{i} - {food.metadata['name']}") + suggested_meals.append(food) + + return {"suggested_meals": suggested_meals} \ No newline at end of file diff --git a/chatbot/agents/nodes/food_query.py b/chatbot/agents/nodes/chatbot/food_suggestion.py similarity index 52% rename from chatbot/agents/nodes/food_query.py rename to chatbot/agents/nodes/chatbot/food_suggestion.py index b1fbfe71844e44dddb0fb906005d06323c86657d..92edc05a7be4d351e5658cbf2060dd757ec10698 100644 --- a/chatbot/agents/nodes/food_query.py +++ b/chatbot/agents/nodes/chatbot/food_suggestion.py @@ -1,31 +1,35 @@ -from chatbot.agents.states.state import AgentState -from chatbot.utils.user_profile import get_user_by_id -from chatbot.agents.tools.food_retriever import food_retriever, food_retriever_top3, query_constructor - - -def food_query(state: AgentState): - print("---FOOD QUERY---") - - user_id = state.get("user_id", {}) - messages = state["messages"] - user_message = messages[-1].content if messages else state.question - - user_profile = get_user_by_id(user_id) - - suggested_meals = [] - - prompt = f""" - Người dùng có khẩu phần: {user_profile["khẩu phần"]}. - Câu hỏi: "{user_message}". - 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%. - """ - - query_ans = query_constructor.invoke(prompt) - print(f"🔍 Dạng truy vấn: {food_retriever.structured_query_translator.visit_structured_query(structured_query=query_ans)}") - foods = food_retriever_top3.invoke(prompt) - print(f"🔍 Kết quả truy vấn: ") - for i, food in enumerate(foods): - print(f"{i} - {food.metadata['name']}") - suggested_meals.append(food) - +from chatbot.agents.states.state import AgentState +from chatbot.utils.user_profile import get_user_by_id +from chatbot.agents.tools.food_retriever import query_constructor, food_retriever +import logging + +# --- Cấu hình logging --- +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +def food_suggestion(state: AgentState): + logger.info("---FOOD QUERY SUGGESTION---") + + user_id = state.get("user_id", {}) + messages = state["messages"] + user_message = messages[-1].content if messages else state.question + + user_profile = get_user_by_id(user_id) + + suggested_meals = [] + + prompt = f""" + Người dùng có khẩu phần: {user_profile["diet"]}. + Câu hỏi: "{user_message}". + 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%. + """ + + query_ans = query_constructor.invoke(prompt) + logger.info(f"🔍 Dạng truy vấn: {food_retriever.structured_query_translator.visit_structured_query(structured_query=query_ans)}") + foods = food_retriever.invoke(prompt) + logger.info(f"🔍 Kết quả truy vấn: ") + for i, food in enumerate(foods): + logger.info(f"{i} - {food.metadata['name']}") + suggested_meals.append(food) + return {"suggested_meals": suggested_meals, "user_profile": user_profile} \ No newline at end of file diff --git a/chatbot/agents/nodes/general_chat.py b/chatbot/agents/nodes/chatbot/general_chat.py similarity index 75% rename from chatbot/agents/nodes/general_chat.py rename to chatbot/agents/nodes/chatbot/general_chat.py index 42fb4c9b06d718d1e59049317d42c296e2283c15..eb49089fda9db7d26ad6154575aeba3471f42413 100644 --- a/chatbot/agents/nodes/general_chat.py +++ b/chatbot/agents/nodes/chatbot/general_chat.py @@ -1,40 +1,45 @@ -from chatbot.agents.states.state import AgentState -from chatbot.models.llm_setup import llm -from langchain.schema.messages import SystemMessage, HumanMessage -from chatbot.utils.user_profile import get_user_by_id - -def general_chat(state: AgentState): - print("---GENERAL CHAT---") - - user_id = state.get("user_id", {}) - messages = state["messages"] - user_message = messages[-1].content if messages else state.question - - user_profile = get_user_by_id(user_id) - - system_prompt = f""" - Bạn là một chuyên gia dinh dưỡng và ẩm thực AI. - Hãy trả lời các câu hỏi về: - - món ăn, thành phần, dinh dưỡng, calo, protein, chất béo, carb, - - chế độ ăn (ăn chay, keto, giảm cân, tăng cơ...), - - sức khỏe, lối sống, chế độ tập luyện liên quan đến ăn uống. - Một số thông tin về người dùng có thể dùng đến như sau: - - Tổng năng lượng mục tiêu: {user_profile['kcal']} kcal/ngày - - Protein: {user_profile['protein']}g - - Chất béo (lipid): {user_profile['lipid']}g - - Carbohydrate: {user_profile['carbohydrate']}g - - Chế độ ăn: {user_profile['khẩu phần']} - Không trả lời các câu hỏi ngoài chủ đề này. - Giải thích ngắn gọn, tự nhiên, rõ ràng. - """ - - messages = [ - SystemMessage(content=system_prompt), - HumanMessage(content=user_message), - ] - - response = llm.invoke(messages) - - print(response.content if hasattr(response, "content") else response) - +from chatbot.agents.states.state import AgentState +from chatbot.models.llm_setup import llm +from langchain.schema.messages import SystemMessage, HumanMessage +from chatbot.utils.user_profile import get_user_by_id +import logging + +# --- Cấu hình logging --- +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +def general_chat(state: AgentState): + logger.info("---GENERAL CHAT---") + + user_id = state.get("user_id", {}) + messages = state["messages"] + user_message = messages[-1].content if messages else state.question + + user_profile = get_user_by_id(user_id) + + system_prompt = f""" + Bạn là một chuyên gia dinh dưỡng và ẩm thực AI. + Hãy trả lời các câu hỏi về: + - món ăn, thành phần, dinh dưỡng, calo, protein, chất béo, carb, + - chế độ ăn (ăn chay, keto, giảm cân, tăng cơ...), + - sức khỏe, lối sống, chế độ tập luyện liên quan đến ăn uống. + Một số thông tin về người dùng có thể dùng đến như sau: + - Tổng năng lượng mục tiêu: {user_profile['targetcalories']} kcal/ngày + - Protein: {user_profile['protein']}g + - Chất béo (lipid): {user_profile['totalfat']}g + - Carbohydrate: {user_profile['carbohydrate']}g + - Chế độ ăn: {user_profile['diet']} + Không trả lời các câu hỏi ngoài chủ đề này. + Giải thích ngắn gọn, tự nhiên, rõ ràng. + """ + + messages = [ + SystemMessage(content=system_prompt), + HumanMessage(content=user_message), + ] + + response = llm.invoke(messages) + + logger.info(response.content if hasattr(response, "content") else response) + return {"response": response.content} \ No newline at end of file diff --git a/chatbot/agents/nodes/chatbot/generate_final_response.py b/chatbot/agents/nodes/chatbot/generate_final_response.py new file mode 100644 index 0000000000000000000000000000000000000000..cb5102bc4775a47ff2682fa8f5592a48771a0f9c --- /dev/null +++ b/chatbot/agents/nodes/chatbot/generate_final_response.py @@ -0,0 +1,39 @@ +from chatbot.agents.states.state import AgentState +from chatbot.models.llm_setup import llm +import logging + +# --- Cấu hình logging --- +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +def generate_final_response(state: AgentState): + logger.info("---NODE: FINAL RESPONSE---") + menu = state["response"]["final_menu"] + profile = state["response"]["user_profile"] + + # Format text để LLM đọc + menu_text = "" + current_meal = "" + for dish in sorted(menu, key=lambda x: x['assigned_meal']): # Sort theo bữa + if dish['assigned_meal'] != current_meal: + current_meal = dish['assigned_meal'] + menu_text += f"\n--- BỮA {current_meal.upper()} ---\n" + + menu_text += ( + f"- {dish['name']} (x{dish['portion_scale']} suất): " + f"{dish['final_kcal']}kcal, {dish['final_protein']}g Protein, {dish['final_lipid']}g Lipid, {dish['final_carb']}g Carbohydrate\n" + ) + + prompt = f""" + Người dùng có mục tiêu: {profile['targetcalories']} Kcal, {profile['protein']}g Protein, {profile['totalfat']}g Lipid, {profile['carbohydrate']}g Carbohydrate. + Hệ thống đã tính toán thực đơn tối ưu sau: + + {menu_text} + + Nhiệm vụ: + 1. Trình bày thực đơn này thật đẹp và ngon miệng cho người dùng. + 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"). + """ + + res = llm.invoke(prompt) + return {"response": res.content} \ No newline at end of file diff --git a/chatbot/agents/nodes/meal_identify.py b/chatbot/agents/nodes/chatbot/meal_identify.py similarity index 55% rename from chatbot/agents/nodes/meal_identify.py rename to chatbot/agents/nodes/chatbot/meal_identify.py index 94b6f01bfd6f349f7f0afe05b984df8288e27a80..03a36d6ce9017eb5bd0439786af9199b35fff055 100644 --- a/chatbot/agents/nodes/meal_identify.py +++ b/chatbot/agents/nodes/chatbot/meal_identify.py @@ -1,64 +1,55 @@ -from langchain.prompts import PromptTemplate -import json -from pydantic import BaseModel, Field -from chatbot.agents.states.state import AgentState -from chatbot.models.llm_setup import llm -from typing import List - -class MealIntent(BaseModel): - intent: str = Field( - description=( - "Loại yêu cầu của người dùng, có thể là:\n" - "- 'full_day_meal': khi người dùng chưa ăn bữa nào và muốn gợi ý thực đơn cho cả ngày.\n" - "- 'not_full_day_meal': khi người dùng đã ăn một vài bữa và muốn gợi ý một bữa cụ thể hoặc các bữa còn lại." - ) - ) - meals_to_generate: List[str] = Field( - description="Danh sách các bữa được người dùng muốn gợi ý: ['sáng', 'trưa', 'tối']." - ) - - -def meal_identify(state: AgentState): - print("---MEAL IDENTIFY---") - - llm_with_structure_op = llm.with_structured_output(MealIntent) - - # Lấy câu hỏi mới nhất từ lịch sử hội thoại - messages = state["messages"] - user_message = messages[-1].content if messages else state.question - - format_instructions = json.dumps(llm_with_structure_op.output_schema.model_json_schema(), ensure_ascii=False, indent=2) - - prompt = PromptTemplate( - template=""" - 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. - - Dựa trên câu hỏi của người dùng, hãy xác định: - 1. Người dùng muốn gợi ý cho **cả ngày**, **một hoặc một vài bữa cụ thể**. - 2. Danh sách các bữa người dùng muốn gợi ý (nếu có). - - Quy tắc: - - Nếu người dùng muốn gợi ý thực đơn cho cả ngày → intent = "full_day_meal". - - Nếu họ nói đã ăn một bữa nào đó, muốn gợi ý một hoặc các bữa còn lại → intent = "not_full_day_meal". - - Các bữa người dùng có thể muốn gợi ý: ["sáng", "trưa", "tối"]. - - Câu hỏi người dùng: {question} - - Hãy xuất kết quả dưới dạng JSON theo schema sau: - {format_instructions} - """ - ) - - chain = prompt | llm_with_structure_op - - result = chain.invoke({ - "question": user_message, - "format_instructions": format_instructions - }) - - print("Bữa cần gợi ý: " + ", ".join(result.meals_to_generate)) - - return { - "meal_intent": result.intent, - "meals_to_generate": result.meals_to_generate, - } +from langchain.prompts import PromptTemplate +import json +from pydantic import BaseModel, Field +from chatbot.agents.states.state import AgentState +from chatbot.models.llm_setup import llm +from typing import List +import logging + +# --- Cấu hình logging --- +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +class MealIntent(BaseModel): + meals_to_generate: List[str] = Field( + description="Danh sách các bữa được người dùng muốn gợi ý: ['sáng', 'trưa', 'tối']." + ) + +def meal_identify(state: AgentState): + logger.info("---MEAL IDENTIFY---") + + llm_with_structure_op = llm.with_structured_output(MealIntent) + + # Lấy câu hỏi mới nhất từ lịch sử hội thoại + messages = state["messages"] + user_message = messages[-1].content if messages else state.question + + format_instructions = json.dumps(llm_with_structure_op.output_schema.model_json_schema(), ensure_ascii=False, indent=2) + + prompt = PromptTemplate( + template=""" + 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. + + 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 ý. + + - Các bữa người dùng có thể muốn gợi ý gồm: ["sáng", "trưa", "tối"]. + + Câu hỏi người dùng: {question} + + Hãy xuất kết quả dưới dạng JSON theo schema sau: + {format_instructions} + """ + ) + + chain = prompt | llm_with_structure_op + + result = chain.invoke({ + "question": user_message, + "format_instructions": format_instructions + }) + + logger.info("Bữa cần gợi ý: " + ", ".join(result.meals_to_generate)) + + return { + "meals_to_generate": result.meals_to_generate, + } diff --git a/chatbot/agents/nodes/chatbot/policy.py b/chatbot/agents/nodes/chatbot/policy.py new file mode 100644 index 0000000000000000000000000000000000000000..98aee3d324ffbeb58462a319a41f9ea1e5f94e00 --- /dev/null +++ b/chatbot/agents/nodes/chatbot/policy.py @@ -0,0 +1,46 @@ +from chatbot.agents.states.state import AgentState +from chatbot.models.llm_setup import llm +from chatbot.agents.tools.info_app_retriever import policy_search +import logging + +# --- Cấu hình logging --- +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +def policy(state: AgentState): + logger.info("---POLICY---") + messages = state["messages"] + question = messages[-1].content if messages else state.question + + if not question: + return {"response": "Chưa có câu hỏi."} + + # Tạo retriever, lấy 3 doc gần nhất + policy_retriever = policy_search.as_retriever(search_kwargs={"k": 3}) + + # Lấy các document liên quan + docs = policy_retriever.invoke(question) + + if not docs: + return {"response": "Không tìm thấy thông tin phù hợp."} + + # Gom nội dung các doc lại + context_text = "\n\n".join([doc.page_content for doc in docs]) + + # Tạo prompt cho LLM + prompt_text = f""" +Bạn là trợ lý AI chuyên về chính sách và thông tin app. + +Thông tin tham khảo từ hệ thống: +{context_text} + +Câu hỏi của người dùng: {question} + +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. +""" + + # Gọi LLM + result = llm.invoke(prompt_text) + answer = result.content + + return {"response": answer} diff --git a/chatbot/agents/nodes/chatbot/select_food.py b/chatbot/agents/nodes/chatbot/select_food.py new file mode 100644 index 0000000000000000000000000000000000000000..f9b221baa42920d7ec07551b30aa2db819a83341 --- /dev/null +++ b/chatbot/agents/nodes/chatbot/select_food.py @@ -0,0 +1,56 @@ +from chatbot.agents.states.state import AgentState +from chatbot.models.llm_setup import llm +import logging + +# --- Cấu hình logging --- +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +def select_food(state: AgentState): + print("---NODE: ANALYZE & ANSWER---") + + suggested_meals = state["suggested_meals"] + + messages = state.get("messages", []) + user_message = messages[-1].content if messages else state.get("question", "") + + # 1. Format dữ liệu món ăn để đưa vào Prompt + if not suggested_meals: + 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."} + + meals_context = "" + for i, doc in enumerate(suggested_meals): + meta = doc.metadata + meals_context += ( + f"Món {i+1}: {meta.get('name', 'Không tên')}\n" + f" - Dinh dưỡng: {meta.get('kcal', '?')} kcal | " + f"P: {meta.get('protein', '?')}g | L: {meta.get('lipid', '?')}g | C: {meta.get('carbohydrate', '?')}g\n" + f" - Mô tả/Thành phần: {doc.page_content}...\n" + ) + + # 2. Prompt Trả lời câu hỏi + # 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à...") + system_prompt = f""" + Bạn là Trợ lý Dinh dưỡng AI thông minh. + + CÂU HỎI: "{user_message}" + + DỮ LIỆU TÌM ĐƯỢC TỪ KHO MÓN ĂN: + {meals_context} + + YÊU CẦU TRẢ LỜI: + 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. + 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. + 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". + + Lưu ý: Chỉ sử dụng thông tin từ danh sách cung cấp, không bịa đặt số liệu. + """ + + # Gọi LLM + response = llm.invoke(system_prompt) + content = response.content if hasattr(response, "content") else response + + print("💬 AI Response:") + print(content) + + return {"response": content} \ No newline at end of file diff --git a/chatbot/agents/nodes/chatbot/select_food_plan.py b/chatbot/agents/nodes/chatbot/select_food_plan.py new file mode 100644 index 0000000000000000000000000000000000000000..0b76c1062f25dc2b967bcc2bf3162e2ad8049bab --- /dev/null +++ b/chatbot/agents/nodes/chatbot/select_food_plan.py @@ -0,0 +1,55 @@ +from chatbot.agents.states.state import AgentState +from chatbot.models.llm_setup import llm +import logging + +# --- Cấu hình logging --- +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +def select_food_plan(state: AgentState): + logger.info("---SELECT FOOD PLAN---") + + user_profile = state["user_profile"] + suggested_meals = state["suggested_meals"] + messages = state["messages"] + user_message = messages[-1].content if messages else state.question + + suggested_meals_text = "\n".join( + f"{i+1}. {doc.metadata.get('name', 'Không rõ')} - " + f"{doc.metadata.get('kcal', '?')} kcal, " + f"Protein:{doc.metadata.get('protein', '?')}g, " + f"Chất béo:{doc.metadata.get('lipid', '?')}g, " + f"Carbohydrate:{doc.metadata.get('carbohydrate', '?')}g" + for i, doc in enumerate(suggested_meals) + ) + + prompt = f""" +Bạn là chuyên gia dinh dưỡng AI. +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: +- Tổng năng lượng mục tiêu: {user_profile['targetcalories']} kcal/ngày +- Protein: {user_profile['protein']}g +- Chất béo (lipid): {user_profile['totalfat']}g +- Carbohydrate: {user_profile['carbohydrate']}g +- Chế độ ăn: {user_profile['diet']} + +Câu hỏi của người dùng: "{user_message}" + +Danh sách món ăn hiện có để chọn: +{suggested_meals_text} + +Yêu cầu: +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. +2. Nếu không có món nào phù hợp, hãy trả về: + "Không tìm thấy món phù hợp trong danh sách hiện có." +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. +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. + """ + + logger.info("Prompt:") + logger.info(prompt) + + result = llm.invoke(prompt) + + logger.info(result.content if hasattr(result, "content") else result) + + return {"response": result.content} \ No newline at end of file diff --git a/chatbot/agents/nodes/suggest_meal_node.py b/chatbot/agents/nodes/chatbot/suggest_meal_node.py similarity index 83% rename from chatbot/agents/nodes/suggest_meal_node.py rename to chatbot/agents/nodes/chatbot/suggest_meal_node.py index 225a94b3926f3f0bb774c9467b3a017acbe090d9..bc2156c0d5a1dff03b0693fee3c248d3353346e1 100644 --- a/chatbot/agents/nodes/suggest_meal_node.py +++ b/chatbot/agents/nodes/chatbot/suggest_meal_node.py @@ -1,72 +1,77 @@ -from langchain_core.messages import HumanMessage, SystemMessage, AIMessage, ToolMessage -from chatbot.agents.states.state import AgentState -from chatbot.models.llm_setup import llm -from chatbot.agents.tools.daily_meal_suggestion import daily_meal_suggestion - -def suggest_meal_node(state: AgentState): - print("---SUGGEST MEAL NODE---") - - # 🧠 Lấy dữ liệu từ state - user_id = state.get("user_id", 0) - question = state.get("messages") - meals_to_generate = state.get("meals_to_generate", []) - - # 🧩 Chuẩn bị prompt mô tả yêu cầu - system_prompt = """ - Bạn là một chuyên gia gợi ý thực đơn AI. - Bạn không được tự trả lời hay đặt câu hỏi thêm. - 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'. - với các tham số: - - user_id: ID người dùng hiện tại - - question: nội dung câu hỏi họ vừa hỏi - - meals_to_generate: danh sách các bữa cần sinh thực đơn (nếu có) - - 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. - """ - - user_prompt = f""" - Người dùng có ID: {user_id} - Yêu cầu: "{question}" - Danh sách các bữa cần gợi ý: {meals_to_generate} - """ - - # 🚀 Gọi LLM và Tools - tools = [daily_meal_suggestion] - llm_with_tools = llm.bind_tools(tools) - - response = llm_with_tools.invoke( - [ - SystemMessage(content=system_prompt), - HumanMessage(content=user_prompt) - ] - ) - - print("===== DEBUG =====") - print("Response type:", type(response)) - print("Tool calls:", getattr(response, "tool_calls", None)) - print("Message content:", response.content) - print("=================") - - if isinstance(response, AIMessage) and response.tool_calls: - tool_call = response.tool_calls[0] - tool_name = tool_call["name"] - tool_args = tool_call["args"] - tool_call_id = tool_call["id"] - - print(f"👉 Executing tool: {tool_name} with args: {tool_args}") - - # Bổ sung tham số nếu LLM quên - tool_args.setdefault("user_id", user_id) - tool_args.setdefault("question", question) - tool_args.setdefault("meals_to_generate", meals_to_generate) - - if tool_name == "daily_meal_suggestion": - result = daily_meal_suggestion.invoke(tool_args) - elif tool_name == "fallback": - result = {"message": "Không có tool phù hợp.", "reason": tool_args.get("reason", "")} - else: - result = {"message": f"Tool '{tool_name}' chưa được định nghĩa."} - - tool_message = ToolMessage(content=str(result), name=tool_name, tool_call_id=tool_call_id) - return {"messages": state["messages"] + [response, tool_message], "response": result} - return {"response": "Lỗi!!!"} +from langchain_core.messages import HumanMessage, SystemMessage, AIMessage, ToolMessage +from chatbot.agents.states.state import AgentState +from chatbot.models.llm_setup import llm +from chatbot.agents.tools.daily_meal_suggestion import daily_meal_suggestion +import logging + +# --- Cấu hình logging --- +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +def suggest_meal_node(state: AgentState): + logger.info("---SUGGEST MEAL NODE---") + + # 🧠 Lấy dữ liệu từ state + user_id = state.get("user_id", 0) + question = state.get("messages") + meals_to_generate = state.get("meals_to_generate", []) + + # 🧩 Chuẩn bị prompt mô tả yêu cầu + system_prompt = """ + Bạn là một chuyên gia gợi ý thực đơn AI. + Bạn không được tự trả lời hay đặt câu hỏi thêm. + 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'. + với các tham số: + - user_id: ID người dùng hiện tại + - question: nội dung câu hỏi họ vừa hỏi + - meals_to_generate: danh sách các bữa cần sinh thực đơn (nếu có) + + 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. + """ + + user_prompt = f""" + Người dùng có ID: {user_id} + Yêu cầu: "{question}" + Danh sách các bữa cần gợi ý: {meals_to_generate} + """ + + # 🚀 Gọi LLM và Tools + tools = [daily_meal_suggestion] + llm_with_tools = llm.bind_tools(tools) + + response = llm_with_tools.invoke( + [ + SystemMessage(content=system_prompt), + HumanMessage(content=user_prompt) + ] + ) + + logger.info("===== DEBUG =====") + logger.info(f"Response type: {type(response)}") + logger.info(f"Tool calls: {getattr(response, 'tool_calls', None)}") + logger.info(f"Message content: {response.content}") + logger.info("=================") + + if isinstance(response, AIMessage) and response.tool_calls: + tool_call = response.tool_calls[0] + tool_name = tool_call["name"] + tool_args = tool_call["args"] + tool_call_id = tool_call["id"] + + logger.info(f"👉 Executing tool: {tool_name} with args: {tool_args}") + + # Bổ sung tham số nếu LLM quên + tool_args.setdefault("user_id", user_id) + tool_args.setdefault("question", question) + tool_args.setdefault("meals_to_generate", meals_to_generate) + + if tool_name == "daily_meal_suggestion": + result = daily_meal_suggestion.invoke(tool_args) + elif tool_name == "fallback": + result = {"message": "Không có tool phù hợp.", "reason": tool_args.get("reason", "")} + else: + result = {"message": f"Tool '{tool_name}' chưa được định nghĩa."} + + tool_message = ToolMessage(content=str(result), name=tool_name, tool_call_id=tool_call_id) + return {"messages": state["messages"] + [response, tool_message], "response": result} + return {"response": "Lỗi!!!"} diff --git a/chatbot/agents/nodes/functions/__init__.py b/chatbot/agents/nodes/functions/__init__.py deleted file mode 100644 index f5ccf4f6b0692668b58c3d5ae104a028cb38480c..0000000000000000000000000000000000000000 --- a/chatbot/agents/nodes/functions/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -from .enrich_food_with_nutrition import enrich_food_with_nutrition -from .enrich_meal_plan_with_nutrition import enrich_meal_plan_with_nutrition -from .enrich_meal_plan_with_nutrition_2 import enrich_meal_plan_with_nutrition_2 -from .generate_best_food_choice import generate_best_food_choice -from .generate_food_plan import generate_food_plan -from .generate_food_similarity import generate_food_similarity -from .generate_food_similarity_2 import generate_food_similarity_2 -from .generate_meal_plan_day_json import generate_meal_plan_day_json -from .generate_meal_plan_json_2 import generate_meal_plan_json_2 -from .generate_meal_plan import generate_meal_plan -from .get_user_profile import get_user_profile - -__all__ = [ - "enrich_food_with_nutrition", - "enrich_meal_plan_with_nutrition", - "enrich_meal_plan_with_nutrition_2", - "generate_best_food_choice", - "generate_food_plan", - "generate_food_similarity", - "generate_food_similarity_2", - "generate_meal_plan_day_json", - "generate_meal_plan_json_2", - "generate_meal_plan", - "get_user_profile", -] \ No newline at end of file diff --git a/chatbot/agents/nodes/functions/__pycache__/__init__.cpython-310.pyc b/chatbot/agents/nodes/functions/__pycache__/__init__.cpython-310.pyc deleted file mode 100644 index 59d2faba8f3a79ab9e7745300c042ad90b4e3216..0000000000000000000000000000000000000000 Binary files a/chatbot/agents/nodes/functions/__pycache__/__init__.cpython-310.pyc and /dev/null differ diff --git a/chatbot/agents/nodes/functions/__pycache__/enrich_food_with_nutrition.cpython-310.pyc b/chatbot/agents/nodes/functions/__pycache__/enrich_food_with_nutrition.cpython-310.pyc deleted file mode 100644 index 3ec6cdb42fc6ef8872633b0da812ff5b6be32d29..0000000000000000000000000000000000000000 Binary files a/chatbot/agents/nodes/functions/__pycache__/enrich_food_with_nutrition.cpython-310.pyc and /dev/null differ diff --git a/chatbot/agents/nodes/functions/__pycache__/enrich_meal_plan_with_nutrition.cpython-310.pyc b/chatbot/agents/nodes/functions/__pycache__/enrich_meal_plan_with_nutrition.cpython-310.pyc deleted file mode 100644 index 4906b60d64635f170dec67123738d057cbc44bc0..0000000000000000000000000000000000000000 Binary files a/chatbot/agents/nodes/functions/__pycache__/enrich_meal_plan_with_nutrition.cpython-310.pyc and /dev/null differ diff --git a/chatbot/agents/nodes/functions/__pycache__/enrich_meal_plan_with_nutrition_2.cpython-310.pyc b/chatbot/agents/nodes/functions/__pycache__/enrich_meal_plan_with_nutrition_2.cpython-310.pyc deleted file mode 100644 index 0f7256f8c336925616f45e665b99485da2fe2715..0000000000000000000000000000000000000000 Binary files a/chatbot/agents/nodes/functions/__pycache__/enrich_meal_plan_with_nutrition_2.cpython-310.pyc and /dev/null differ diff --git a/chatbot/agents/nodes/functions/__pycache__/generate_best_food_choice.cpython-310.pyc b/chatbot/agents/nodes/functions/__pycache__/generate_best_food_choice.cpython-310.pyc deleted file mode 100644 index ac027a4d29cb3b544fc7c4527947bc3fad192c2a..0000000000000000000000000000000000000000 Binary files a/chatbot/agents/nodes/functions/__pycache__/generate_best_food_choice.cpython-310.pyc and /dev/null differ diff --git a/chatbot/agents/nodes/functions/__pycache__/generate_food_plan.cpython-310.pyc b/chatbot/agents/nodes/functions/__pycache__/generate_food_plan.cpython-310.pyc deleted file mode 100644 index 92872be38276c8c6eb607b0e19e3ed985178ffd2..0000000000000000000000000000000000000000 Binary files a/chatbot/agents/nodes/functions/__pycache__/generate_food_plan.cpython-310.pyc and /dev/null differ diff --git a/chatbot/agents/nodes/functions/__pycache__/generate_food_similarity.cpython-310.pyc b/chatbot/agents/nodes/functions/__pycache__/generate_food_similarity.cpython-310.pyc deleted file mode 100644 index 286c015beb3eeae8b6edb5238b6631649e1c2bb2..0000000000000000000000000000000000000000 Binary files a/chatbot/agents/nodes/functions/__pycache__/generate_food_similarity.cpython-310.pyc and /dev/null differ diff --git a/chatbot/agents/nodes/functions/__pycache__/generate_food_similarity_2.cpython-310.pyc b/chatbot/agents/nodes/functions/__pycache__/generate_food_similarity_2.cpython-310.pyc deleted file mode 100644 index 59dffb984fb9cb6afa36f8b2850b253c370eabcd..0000000000000000000000000000000000000000 Binary files a/chatbot/agents/nodes/functions/__pycache__/generate_food_similarity_2.cpython-310.pyc and /dev/null differ diff --git a/chatbot/agents/nodes/functions/__pycache__/generate_meal_plan.cpython-310.pyc b/chatbot/agents/nodes/functions/__pycache__/generate_meal_plan.cpython-310.pyc deleted file mode 100644 index 6692e38d964f651d5d71766b8a67146e74995e01..0000000000000000000000000000000000000000 Binary files a/chatbot/agents/nodes/functions/__pycache__/generate_meal_plan.cpython-310.pyc and /dev/null differ diff --git a/chatbot/agents/nodes/functions/__pycache__/generate_meal_plan_day_json.cpython-310.pyc b/chatbot/agents/nodes/functions/__pycache__/generate_meal_plan_day_json.cpython-310.pyc deleted file mode 100644 index 0313b54dbad66b064aa4b1260a1fe4c01ea6676a..0000000000000000000000000000000000000000 Binary files a/chatbot/agents/nodes/functions/__pycache__/generate_meal_plan_day_json.cpython-310.pyc and /dev/null differ diff --git a/chatbot/agents/nodes/functions/__pycache__/generate_meal_plan_json_2.cpython-310.pyc b/chatbot/agents/nodes/functions/__pycache__/generate_meal_plan_json_2.cpython-310.pyc deleted file mode 100644 index 945ad0457236941b75ecc435d1c88f2ce2498fd6..0000000000000000000000000000000000000000 Binary files a/chatbot/agents/nodes/functions/__pycache__/generate_meal_plan_json_2.cpython-310.pyc and /dev/null differ diff --git a/chatbot/agents/nodes/functions/__pycache__/get_user_profile.cpython-310.pyc b/chatbot/agents/nodes/functions/__pycache__/get_user_profile.cpython-310.pyc deleted file mode 100644 index 714fb1b03a1c1a26122020f702506c0dba431f1b..0000000000000000000000000000000000000000 Binary files a/chatbot/agents/nodes/functions/__pycache__/get_user_profile.cpython-310.pyc and /dev/null differ diff --git a/chatbot/agents/nodes/functions/enrich_food_with_nutrition.py b/chatbot/agents/nodes/functions/enrich_food_with_nutrition.py deleted file mode 100644 index ee28d78555251ce7c8ec4da3160db9dee75776c5..0000000000000000000000000000000000000000 --- a/chatbot/agents/nodes/functions/enrich_food_with_nutrition.py +++ /dev/null @@ -1,54 +0,0 @@ -import logging -from typing import List, Dict, Any - -from chatbot.agents.states.state import AgentState -from chatbot.agents.tools.food_retriever import query_constructor, food_retriever -from langgraph.graph import END, StateGraph -from chatbot.models.llm_setup import llm -from langchain.tools import tool -from chatbot.utils.user_profile import get_user_by_id - -def enrich_food_with_nutrition(state: AgentState): - food_new_raw = state["food_new_raw"] - suggested_meals = state.get("suggested_meals", []) - - # Map ID → món gốc - meal_map = {str(m["meal_id"]): m for m in suggested_meals} - - # Những field KHÔNG nhân portion - skip_scale_fields = { - "meal_id", "name", "ingredients", "ingredients_text", - "difficulty", "servings", "cooking_time_minutes" - } - - - meal_id = str(food_new_raw["id"]) - portion = float(food_new_raw["portion"]) - - base = meal_map.get(meal_id) - - enriched_food = {} - - if base: - enriched_food = { - "id": meal_id, - "name": food_new_raw["name"], - "portion": portion - } - - for key, value in base.items(): - # Nếu không nhân portion - if key in skip_scale_fields: - enriched_food[key] = value - continue - - # Nếu là số → nhân portion - if isinstance(value, (int, float)): - enriched_food[key] = round(value * portion, 4) - else: - # Các field text, list thì giữ nguyên - enriched_food[key] = value - - return {"food_new": enriched_food} - else: - return {"food_new": food_new_raw} \ No newline at end of file diff --git a/chatbot/agents/nodes/functions/enrich_meal_plan_with_nutrition.py b/chatbot/agents/nodes/functions/enrich_meal_plan_with_nutrition.py deleted file mode 100644 index 1bf13164f0df9fe97a412c45cb1c39809f3beca7..0000000000000000000000000000000000000000 --- a/chatbot/agents/nodes/functions/enrich_meal_plan_with_nutrition.py +++ /dev/null @@ -1,72 +0,0 @@ -import logging -from typing import List, Dict, Any - -from chatbot.agents.states.state import AgentState -from chatbot.agents.tools.food_retriever import query_constructor, food_retriever -from langgraph.graph import END, StateGraph -from chatbot.models.llm_setup import llm -from langchain.tools import tool -from chatbot.utils.user_profile import get_user_by_id - -def enrich_meal_plan_with_nutrition(state: AgentState): - meal_plan = state["meal_plan"] - suggested_meals = state.get("suggested_meals", []) - - # Map ID → món gốc - meal_map = {str(m["meal_id"]): m for m in suggested_meals} - - # Những field KHÔNG nhân portion - skip_scale_fields = { - "meal_id", "name", "ingredients", "ingredients_text", - "difficulty", "servings", "cooking_time_minutes" - } - - enriched_meals = [] - - for meal in meal_plan.get("meals", []): - enriched_items = [] - - for item in meal.get("items", []): - meal_id = str(item["id"]) - portion = float(item["portion"]) - - base = meal_map.get(meal_id) - - if base: - enriched_item = { - "id": meal_id, - "name": item["name"], - "portion": portion - } - - for key, value in base.items(): - # Nếu không nhân portion - if key in skip_scale_fields: - enriched_item[key] = value - continue - - # Nếu là số → nhân portion - if isinstance(value, (int, float)): - enriched_item[key] = round(value * portion, 4) - else: - # Các field text, list thì giữ nguyên - enriched_item[key] = value - - enriched_items.append(enriched_item) - - else: - enriched_items.append(item) - - enriched_meals.append({ - "meal_name": meal["meal_name"], - "items": enriched_items - }) - - meal_plan_day = { - "meals": enriched_meals, - "reason": meal_plan.get("reason", "") - } - - return { - "meal_plan_day": meal_plan_day - } \ No newline at end of file diff --git a/chatbot/agents/nodes/functions/enrich_meal_plan_with_nutrition_2.py b/chatbot/agents/nodes/functions/enrich_meal_plan_with_nutrition_2.py deleted file mode 100644 index 07e6a8240d49f47295e0c20e52506f2ccb23f7db..0000000000000000000000000000000000000000 --- a/chatbot/agents/nodes/functions/enrich_meal_plan_with_nutrition_2.py +++ /dev/null @@ -1,72 +0,0 @@ -import logging -from typing import List, Dict, Any - -from chatbot.agents.states.state import AgentState -from chatbot.agents.tools.food_retriever import query_constructor, food_retriever -from langgraph.graph import END, StateGraph -from chatbot.models.llm_setup import llm -from langchain.tools import tool -from chatbot.utils.user_profile import get_user_by_id - -def enrich_meal_plan_with_nutrition_2(state: AgentState): - meal_new_raw = state["meal_new_raw"] - suggested_meals = state.get("suggested_meals", []) - - # Map ID → món gốc - meal_map = {str(m["meal_id"]): m for m in suggested_meals} - - # Những field KHÔNG nhân portion - skip_scale_fields = { - "meal_id", "name", "ingredients", "ingredients_text", - "difficulty", "servings", "cooking_time_minutes" - } - - enriched_meals = [] - - for meal in meal_new_raw.get("meals", []): - enriched_items = [] - - for item in meal.get("items", []): - meal_id = str(item["id"]) - portion = float(item["portion"]) - - base = meal_map.get(meal_id) - - if base: - enriched_item = { - "id": meal_id, - "name": item["name"], - "portion": portion - } - - for key, value in base.items(): - # Nếu không nhân portion - if key in skip_scale_fields: - enriched_item[key] = value - continue - - # Nếu là số → nhân portion - if isinstance(value, (int, float)): - enriched_item[key] = round(value * portion, 4) - else: - # Các field text, list thì giữ nguyên - enriched_item[key] = value - - enriched_items.append(enriched_item) - - else: - enriched_items.append(item) - - enriched_meals.append({ - "meal_name": meal["meal_name"], - "items": enriched_items - }) - - meal_new = { - "meals": enriched_meals, - "reason": meal_new_raw.get("reason", "") - } - - return { - "meal_new": meal_new - } \ No newline at end of file diff --git a/chatbot/agents/nodes/functions/generate_best_food_choice.py b/chatbot/agents/nodes/functions/generate_best_food_choice.py deleted file mode 100644 index 85cab2ddfb7e46844016001d2e49c9b28492d6ed..0000000000000000000000000000000000000000 --- a/chatbot/agents/nodes/functions/generate_best_food_choice.py +++ /dev/null @@ -1,79 +0,0 @@ -import logging -from typing import List, Dict, Any - -from chatbot.agents.states.state import AgentState -from chatbot.agents.tools.food_retriever import query_constructor, food_retriever -from langgraph.graph import END, StateGraph -from chatbot.models.llm_setup import llm -from langchain.tools import tool -from chatbot.utils.user_profile import get_user_by_id - -import json - -def generate_best_food_choice(state: AgentState): - print("---GENERATE BEST FOOD CHOICE---") - - user_profile = state["user_profile"] - food_old = state["food_old"] - suggested_meals = state["suggested_meals"] - - if not suggested_meals: - return {"error": "Không có món để chọn"} - - # Chuẩn bị danh sách món đã retriever được - candidate_text = "\n".join([ - f"- ID: {m['meal_id']}, {m['name']} | " - f"{m.get('kcal',0)} kcal, {m.get('protein',0)} protein, " - f"{m.get('lipid',0)} lipid, {m.get('carbohydrate',0)} carbohydrate | " - f"tags: {', '.join(m.get('tags', []))}" - for m in suggested_meals - ]) - - # Prompt chọn món tốt nhất - prompt = f""" - Bạn là AI chuyên gia dinh dưỡng. - - 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 - để thay thế món: {food_old['name']}. - - Giá trị dinh dưỡng món cũ: - - kcal: {food_old['kcal']} - - protein: {food_old['protein']} - - lipid: {food_old['lipid']} - - carbohydrate: {food_old['carbohydrate']} - - tags: {', '.join(food_old['tags'])} - - Danh sách món gợi ý: - {candidate_text} - - --- QUY TẮC --- - 1. Chọn món có dinh dưỡng gần nhất với món cũ (±20%). - 2. Ưu tiên món có nhiều tag trùng với món cũ. - 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). - 4. Chỉ trả JSON duy nhất, không viết gì thêm. - - --- ĐỊNH DẠNG JSON TRẢ VỀ --- - {{ - "id": , - "name": "", - "portion": số_lượng_khẩu_phần (float) - }} - - KHÔNG VIẾT GÌ NGOÀI JSON. - """ - - - print("---Prompt---") - print(prompt) - - result = llm.invoke(prompt) - output = result.content - - # Parse JSON an toàn - try: - food_new_raw = json.loads(output) - except Exception as e: - print("❌ JSON parse error:", e) - return {"response": "LLM trả về JSON không hợp lệ", "raw_output": output} - - return {"food_new_raw": food_new_raw} \ No newline at end of file diff --git a/chatbot/agents/nodes/functions/generate_food_plan.py b/chatbot/agents/nodes/functions/generate_food_plan.py deleted file mode 100644 index eb1c9220955fa5938e3a927bc456dce9e3200efb..0000000000000000000000000000000000000000 --- a/chatbot/agents/nodes/functions/generate_food_plan.py +++ /dev/null @@ -1,42 +0,0 @@ -import logging -from typing import List, Dict, Any - -from chatbot.agents.states.state import AgentState -from chatbot.agents.tools.food_retriever import query_constructor, food_retriever -from langgraph.graph import END, StateGraph -from chatbot.models.llm_setup import llm -from langchain.tools import tool -from chatbot.utils.user_profile import get_user_by_id - -# --- Cấu hình logging --- -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - -# --- Generate food plan --- -def generate_food_plan(state: AgentState) -> Dict[str, Any]: - logger.info("--- GENERATE_FOOD_PLAN ---") - meals_to_generate: List[str] = state.get("meals_to_generate", []) - user_profile: Dict[str, Any] = state.get("user_profile", {}) - - if not meals_to_generate: - logger.warning("meals_to_generate rỗng, sử dụng mặc định ['sáng', 'trưa', 'tối']") - meals_to_generate = ["sáng", "trưa", "tối"] - - meals_text = ", ".join(meals_to_generate) - - query_text = ( - 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')}. " - f"Ưu tiên món phổ biến, cân bằng dinh dưỡng, cho bữa {meals_text}." - ) - logger.info(f"Query: {query_text}") - - try: - foods = food_retriever.invoke(query_text) - except Exception as e: - logger.error(f"Lỗi khi truy vấn món ăn: {e}") - foods = [] - - suggested_meals = [food.metadata for food in foods] if foods else [] - logger.info(f"Số món được gợi ý: {len(suggested_meals)}") - - return {"suggested_meals": suggested_meals} \ No newline at end of file diff --git a/chatbot/agents/nodes/functions/generate_food_similarity.py b/chatbot/agents/nodes/functions/generate_food_similarity.py deleted file mode 100644 index 374cf481be03d7143e1e148eaa8cb113c0614516..0000000000000000000000000000000000000000 --- a/chatbot/agents/nodes/functions/generate_food_similarity.py +++ /dev/null @@ -1,39 +0,0 @@ -import logging -from typing import List, Dict, Any - -from chatbot.agents.states.state import AgentState -from chatbot.agents.tools.food_retriever import query_constructor, food_retriever -from langgraph.graph import END, StateGraph -from chatbot.models.llm_setup import llm -from langchain.tools import tool -from chatbot.utils.user_profile import get_user_by_id - -def generate_food_similarity(state: AgentState): - print("---GENERATE FOOD SIMILARITY---") - meals_to_generate = state.get("meals_to_generate", ["sáng"]) - user_profile = state["user_profile"] - food_old = state["food_old"] - - suggested_meals = [] - meals_text = ", ".join(meals_to_generate) - - query = ( - f"Tìm món ăn tương tự món {food_old['name']} dựa trên: " - f"kcal ~{food_old['kcal']} (±20%), " - f"protein ~{food_old['protein']}g (±20%), " - f"lipid ~{food_old['lipid']}g (±20%), " - f"carbohydrate ~{food_old['carbohydrate']}g (±20%). " - f"Ưu tiên các món có tags: {', '.join(food_old['tags'])}. " - f"Phù hợp khẩu phần: {user_profile['khẩu phần']}, " - f"phục vụ cho bữa {meals_text}." - ) - - print("Query: " + query) - - foods = food_retriever.invoke(query) - print(f"🔍 Kết quả truy vấn: ") - for i, food in enumerate(foods): - print(f"{i} - {food.metadata['name']}") - suggested_meals.append(food.metadata) - - return {"suggested_meals": suggested_meals} \ No newline at end of file diff --git a/chatbot/agents/nodes/functions/generate_food_similarity_2.py b/chatbot/agents/nodes/functions/generate_food_similarity_2.py deleted file mode 100644 index 3db23712837b04867eaeb6238e811493f66b2600..0000000000000000000000000000000000000000 --- a/chatbot/agents/nodes/functions/generate_food_similarity_2.py +++ /dev/null @@ -1,45 +0,0 @@ -import logging -from typing import List, Dict, Any - -from chatbot.agents.states.state import AgentState -from chatbot.agents.tools.food_retriever import query_constructor, food_retriever -from langgraph.graph import END, StateGraph -from chatbot.models.llm_setup import llm -from langchain.tools import tool -from chatbot.utils.user_profile import get_user_by_id - -def generate_food_similarity_2(state: AgentState): - print("---GENERATE FOOD SIMILARITY---") - meals_to_generate = state.get("meals_to_generate", ["sáng"]) - user_profile = state["user_profile"] - meal_old = state["meal_old"] - - suggested_meals = [] - food_name_text = ", ".join([meal['name'] for meal in meal_old]) - - all_tags = [ - tag - for meal in meal_old - for tag in meal.get("tags", []) - if isinstance(tag, str) - ] - unique_tags = list(set(all_tags)) - food_tag_text = ", ".join(unique_tags) - - meals_text = ", ".join(meals_to_generate) - - query = ( - f"Tìm món ăn ưu tiên các món có tags: {food_tag_text}. " - f"Phù hợp khẩu phần: {user_profile['khẩu phần']}, " - f"phục vụ cho bữa {meals_text}." - ) - - print("Query: " + query) - - foods = food_retriever.invoke(query) - print(f"🔍 Kết quả truy vấn: ") - for i, food in enumerate(foods): - print(f"{i} - {food.metadata['name']}") - suggested_meals.append(food.metadata) - - return {"suggested_meals": suggested_meals} \ No newline at end of file diff --git a/chatbot/agents/nodes/functions/generate_meal_plan.py b/chatbot/agents/nodes/functions/generate_meal_plan.py deleted file mode 100644 index 45f61b611a52402a7f6c64c3afb524912b51aa72..0000000000000000000000000000000000000000 --- a/chatbot/agents/nodes/functions/generate_meal_plan.py +++ /dev/null @@ -1,66 +0,0 @@ -import logging -from typing import List, Dict, Any - -from chatbot.agents.states.state import AgentState -from chatbot.agents.tools.food_retriever import query_constructor, food_retriever -from langgraph.graph import END, StateGraph -from chatbot.models.llm_setup import llm -from langchain.tools import tool -from chatbot.utils.user_profile import get_user_by_id - -# --- Cấu hình logging --- -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - -def generate_meal_plan(state: AgentState): - logger.info("--- GENERATE_MEAL_PLAN ---") - user_profile = state.get("user_profile", {}) - suggested_meals = state.get("suggested_meals", []) - meals_to_generate = state.get("meals_to_generate", []) - question = state.get("question", "Hãy tạo thực đơn cho tôi.") - - meals_text = ", ".join(meals_to_generate) - - suggested_meals_text = "\n".join( - [f"- {meal['name']}: {meal.get('kcal', 0)} kcal, " - f"{meal.get('protein', 0)}g protein, " - f"{meal.get('lipid', 0)}g chất béo, " - f"{meal.get('carbohydrate', 0)}g carbohydrate" - for meal in suggested_meals] - ) if suggested_meals else "Chưa có món ăn gợi ý." - - prompt = f""" - 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: - - Tổng năng lượng mục tiêu: {user_profile['kcal']} kcal/ngày - - Protein: {user_profile['protein']}g - - Chất béo (lipid): {user_profile['lipid']}g - - Carbohydrate: {user_profile['carbohydrate']}g - - Chế độ ăn: {user_profile['khẩu phần']} - - Câu hỏi của người dùng: "{question}" - - Các bữa cần xây dựng: - {meals_text} - - Danh sách món ăn hiện có để chọn: - {suggested_meals_text} - - Yêu cầu: - 1. Hãy tổ hợp các món ăn trên để tạo thực đơn cho từng bữa (chỉ chọn trong danh sách có sẵn). - 2. Mỗi bữa gồm 1 đến 3 món, tổng năng lượng và dinh dưỡng xấp xỉ giá trị yêu cầu của bữa đó (±15%). - 3. Nếu cần, hãy ước tính khẩu phần mỗi món (ví dụ: 0.5 khẩu phần hoặc 1.2 khẩu phần) để đạt cân bằng chính xác. - 4. Đảm bảo tổng giá trị dinh dưỡng toàn ngày gần với hồ sơ người dùng. - 5. Chỉ chọn những món phù hợp với chế độ ăn: {user_profile['khẩu phần']}. - """ - - logger.info(prompt) - - try: - result = llm.invoke(prompt, timeout=60) - response_content = getattr(result, "content", str(result)) - except Exception as e: - logger.error(f"Lỗi khi gọi LLM: {e}") - response_content = "Không thể tạo thực đơn lúc này, vui lòng thử lại sau." - - logger.info("Meal plan suggestion generated.") - return {"response": response_content} \ No newline at end of file diff --git a/chatbot/agents/nodes/functions/generate_meal_plan_day_json.py b/chatbot/agents/nodes/functions/generate_meal_plan_day_json.py deleted file mode 100644 index 478b14d375e3bb58273bd5c030874ecf7f66f522..0000000000000000000000000000000000000000 --- a/chatbot/agents/nodes/functions/generate_meal_plan_day_json.py +++ /dev/null @@ -1,83 +0,0 @@ -import logging -from typing import List, Dict, Any - -from chatbot.agents.states.state import AgentState -from chatbot.agents.tools.food_retriever import query_constructor, food_retriever -from langgraph.graph import END, StateGraph -from chatbot.models.llm_setup import llm -from langchain.tools import tool -from chatbot.utils.user_profile import get_user_by_id - -import json - -def generate_meal_plan_day_json(state: AgentState): - # logger.info("--- GENERATE_MEAL_PLAN ---") - user_profile = state.get("user_profile", {}) - suggested_meals = state.get("suggested_meals", []) - - suggested_meals_text = "\n".join( - [f"- ID: {meal['meal_id']} {meal['name']}: {meal.get('kcal', 0)} kcal, " - f"{meal.get('protein', 0)}g protein, " - f"{meal.get('lipid', 0)}g chất béo, " - f"{meal.get('carbohydrate', 0)}g carbohydrate" - for meal in suggested_meals] - ) if suggested_meals else "Chưa có món ăn gợi ý." - - prompt = f""" - Bạn là AI chuyên gia dinh dưỡng. Nhiệm vụ: Tạo thực đơn cho một ngày theo dạng JSON. - - Hồ sơ dinh dưỡng người dùng: - - Năng lượng mục tiêu: {user_profile['kcal']} kcal/ngày - - Protein: {user_profile['protein']}g - - Lipid: {user_profile['lipid']}g - - Carbohydrate: {user_profile['carbohydrate']}g - - Chế độ ăn: {user_profile['khẩu phần']} - - Danh sách món ăn hiện có: - {suggested_meals_text} - - --- YÊU CẦU --- - Trả về duy nhất một JSON theo cấu trúc sau: - - {{ - "meals": [ - {{ - "meal_name": "Tên bữa", - "items": [ - {{ - "id": "ID món", - "name": "Tên món", - "portion": số_lượng_khẩu_phần (float) - }} - ] - }} - ], - "reason": "Lý do xây dựng thực đơn" - }} - - --- QUY TẮC --- - 1. Hãy tổ hợp các món ăn trên để tạo thực đơn cho từng bữa (chỉ chọn trong danh sách có sẵn). - 2. Mỗi bữa gồm 1 đến 3 món, tổng năng lượng và dinh dưỡng xấp xỉ giá trị yêu cầu của bữa đó (±15%). - 3. Mỗi món phải có cả "id", "name", "portion". - 4. Điều chỉnh "portion" có thể là 0.5, 1, 1.2... sao cho tổng năng lượng của một ngày đạt cân bằng chính xác. - 5. Đảm bảo tổng giá trị dinh dưỡng toàn ngày gần với hồ sơ người dùng. - 6. "reason" là lý do xây dựng thực đơn bao gồm dinh dưỡng của từng bữa sáng, trưa, tối và tổng lại cả ngày. - 7. Chỉ chọn món đúng chế độ ăn: {user_profile['khẩu phần']}. - 8. KHÔNG được trả lời thêm văn bản nào ngoài JSON. - """ - - print("---Prompt---") - print(prompt) - - result = llm.invoke(prompt) - output = result.content - - # Parse JSON an toàn - try: - meal_plan = json.loads(output) - logging.info("Lý do: " + meal_plan.get("reason", "Không có lý do")) - except Exception as e: - print("❌ JSON parse error:", e) - return {"response": "LLM trả về JSON không hợp lệ", "raw_output": output} - - return {"meal_plan": meal_plan} diff --git a/chatbot/agents/nodes/functions/generate_meal_plan_json_2.py b/chatbot/agents/nodes/functions/generate_meal_plan_json_2.py deleted file mode 100644 index a9b7a5d65eb8b544e3a1e64c3e56ce37b44914ab..0000000000000000000000000000000000000000 --- a/chatbot/agents/nodes/functions/generate_meal_plan_json_2.py +++ /dev/null @@ -1,94 +0,0 @@ -import logging -from typing import List, Dict, Any - -from chatbot.agents.states.state import AgentState -from chatbot.agents.tools.food_retriever import query_constructor, food_retriever -from langgraph.graph import END, StateGraph -from chatbot.models.llm_setup import llm -from langchain.tools import tool -from chatbot.utils.user_profile import get_user_by_id -import json - -def generate_meal_plan_json_2(state: AgentState): - # logger.info("--- GENERATE_MEAL_PLAN ---") - user_profile = state.get("user_profile", {}) - suggested_meals = state.get("suggested_meals", []) - meals_to_generate = state.get("meals_to_generate", ["sáng"]) - - meal_text = ", ".join(meals_to_generate) - - suggested_meals_text = "\n".join( - [f"- ID: {meal['meal_id']} {meal['name']}: {meal.get('kcal', 0)} kcal, " - f"{meal.get('protein', 0)}g protein, " - f"{meal.get('lipid', 0)}g chất béo, " - f"{meal.get('carbohydrate', 0)}g carbohydrate" - for meal in suggested_meals] - ) if suggested_meals else "Chưa có món ăn gợi ý." - - meal_old = state["meal_old"] - - meal_old_text = "\n".join([ - f"- {item['name']}: {item.get('kcal',0)} kcal, " - f"{item.get('protein',0)}g protein, " - f"{item.get('lipid',0)}g lipid, " - f"{item.get('carbohydrate',0)}g carbohydrate, " - f"tương đương: {item.get('portion', 1)} khẩu phần." - for item in meal_old - ]) - - prompt = f""" - Bạn là AI chuyên gia dinh dưỡng. - - Nhiệm vụ: Tạo lại thực đơn cho bữa {meal_text} bằng cách chọn MÓN MỚI - từ danh sách gợi ý bên dưới sao cho: - - Dinh dưỡng gần nhất với bữa cũ (±15%) - - Món phù hợp khẩu phần: {user_profile['khẩu phần']} - - Chỉ được chọn món có trong danh sách gợi ý - - --- THÔNG TIN BỮA CŨ --- - Bao gồm các món sau: - {meal_old_text} - - Danh sách món gợi ý: - {suggested_meals_text} - - --- QUY TẮC --- - 1. Chỉ chọn trong danh sách gợi ý. - 2. Mỗi bữa gồm 1–3 món. - 3. Mỗi món phải trả về đầy đủ: "id", "name", "portion". - 4. "portion" có thể là số thực: 0.5, 1.0, 1.2... - 5. Tổng dinh dưỡng bữa mới phải gần với tổng dinh dưỡng bữa cũ (±15%). - 6. Không được thêm mô tả ngoài JSON. - - --- JSON TRẢ VỀ --- - {{ - "meals": [ - {{ - "meal_name": "{meal_text}", - "items": [ - {{ - "id": "ID món", - "name": "Tên món", - "portion": số_lượng_khẩu_phần - }} - ] - }} - ], - "reason": "Giải thích vì sao chọn các món và khẩu phần" - }} - """ - - print("---Prompt---") - print(prompt) - - result = llm.invoke(prompt) - output = result.content - - # Parse JSON an toàn - try: - meal_new_raw = json.loads(output) - except Exception as e: - print("❌ JSON parse error:", e) - return {"response": "LLM trả về JSON không hợp lệ", "raw_output": output} - - return {"meal_new_raw": meal_new_raw} \ No newline at end of file diff --git a/chatbot/agents/nodes/functions/get_user_profile.py b/chatbot/agents/nodes/functions/get_user_profile.py deleted file mode 100644 index 1b2caa154fb9fa2b881d2a312e5ba1defad06666..0000000000000000000000000000000000000000 --- a/chatbot/agents/nodes/functions/get_user_profile.py +++ /dev/null @@ -1,35 +0,0 @@ -import logging -from typing import List, Dict, Any - -from chatbot.agents.states.state import AgentState -from chatbot.agents.tools.food_retriever import query_constructor, food_retriever -from langgraph.graph import END, StateGraph -from chatbot.models.llm_setup import llm -from langchain.tools import tool -from chatbot.utils.user_profile import get_user_by_id - -# --- Cấu hình logging --- -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - -def get_user_profile(state: AgentState) -> Dict[str, Any]: - """ - Node: Lấy profile người dùng và chuẩn hóa key. - """ - logger.info("--- GET_USER_PROFILE ---") - user_id = state.get("user_id", "") - user_profile = get_user_by_id(user_id) - - # Chuẩn hóa khóa - mapping = {"fat": "lipid", "carbs": "carbohydrate", "protein": "protein", - "kcal": "kcal", "lipid": "lipid", "carbohydrate": "carbohydrate"} - normalized_profile = {mapping.get(k.lower(), k.lower()): v for k, v in user_profile.items()} - - # Fallback default nếu thiếu - defaults = {"kcal": 1700, "protein": 120, "lipid": 56, "carbohydrate": 170, "khẩu phần": "ăn chay"} - for key, val in defaults.items(): - normalized_profile.setdefault(key, val) - - logger.info(f"User profile chuẩn hóa: {normalized_profile}") - - return {"user_profile": normalized_profile, "daily_goal": normalized_profile, "suggested_meals": []} diff --git a/chatbot/agents/nodes/select_food_plan.py b/chatbot/agents/nodes/select_food_plan.py deleted file mode 100644 index 36ef5d34d83783bd06b822e6cd23f077348ea2a0..0000000000000000000000000000000000000000 --- a/chatbot/agents/nodes/select_food_plan.py +++ /dev/null @@ -1,51 +0,0 @@ -from chatbot.agents.states.state import AgentState -from chatbot.models.llm_setup import llm - -def select_food_plan(state: AgentState): - print("---SELECT FOOD PLAN---") - - user_profile = state["user_profile"] - suggested_meals = state["suggested_meals"] - messages = state["messages"] - user_message = messages[-1].content if messages else state.question - - suggested_meals_text = "\n".join( - f"{i+1}. {doc.metadata.get('name', 'Không rõ')} - " - f"{doc.metadata.get('kcal', '?')} kcal, " - f"Protein:{doc.metadata.get('protein', '?')}g, " - f"Chất béo:{doc.metadata.get('lipid', '?')}g, " - f"Carbohydrate:{doc.metadata.get('carbohydrate', '?')}g\n" - f"Mô tả: {doc.page_content}" - for i, doc in enumerate(suggested_meals) - ) - - prompt = f""" - Bạn là chuyên gia dinh dưỡng AI. - 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: - - Tổng năng lượng mục tiêu: {user_profile['kcal']} kcal/ngày - - Protein: {user_profile['protein']}g - - Chất béo (lipid): {user_profile['lipid']}g - - Carbohydrate: {user_profile['carbohydrate']}g - - Chế độ ăn: {user_profile['khẩu phần']} - - Câu hỏi của người dùng: "{user_message}" - - Danh sách món ăn hiện có để chọn: - {suggested_meals_text} - - Yêu cầu: - 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. - 2. Nếu không có món nào phù hợp, hãy trả về: - "Không tìm thấy món phù hợp trong danh sách hiện có." - 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. - 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. - """ - - print("Prompt:") - print(prompt) - - result = llm.invoke(prompt) - - print(result.content if hasattr(result, "content") else result) - - return {"response": result.content} \ No newline at end of file diff --git a/chatbot/agents/states/__pycache__/state.cpython-310.pyc b/chatbot/agents/states/__pycache__/state.cpython-310.pyc index 896b0fc86c1ac27d4febf05e02f4760d9bee9877..166c40ca748e80775a541a5bdaee0d13ca4e6bc1 100644 Binary files a/chatbot/agents/states/__pycache__/state.cpython-310.pyc and b/chatbot/agents/states/__pycache__/state.cpython-310.pyc differ diff --git a/chatbot/agents/states/state.py b/chatbot/agents/states/state.py index 63aa6d12a15e47efc5df1db7628c4b054475824a..bce8ed684064267117fa7f636b5bdbcf9af352e8 100644 --- a/chatbot/agents/states/state.py +++ b/chatbot/agents/states/state.py @@ -2,41 +2,35 @@ from typing import Annotated, Optional, Literal, Sequence, TypedDict, List, Dict from langgraph.graph.message import add_messages class AgentState(TypedDict): - # ========== 1️⃣ Thông tin cơ bản ========== + # ========== Thông tin cơ bản ========== user_id: Optional[str] question: str - # ========== 2️⃣ Ngữ cảnh hội thoại ========== + # ========== Ngữ cảnh hội thoại ========== topic: Optional[str] user_profile: Optional[Dict[str, Any]] - # ========== 3️⃣ Gợi ý & lựa chọn món ăn ========== - meal_intent: Optional[str] + # ========== Gợi ý & lựa chọn món ăn ========== meals_to_generate: Optional[List[str]] suggested_meals: Optional[List[Dict[str, Any]]] - selected_meals: Optional[List[Dict[str, Any]]] - # ========== 4️⃣ Nhật ký & dinh dưỡng ========== - today_log: Optional[List[Dict[str, Any]]] - remaining: Optional[Dict[str, float]] - - # ========== 5️⃣ Kết quả & phản hồi ========== - analysis_result: Optional[Dict[str, Any]] + # ========== Kết quả & phản hồi ========== response: Optional[str] messages: Annotated[list, add_messages] - # ========== 6️⃣ Mục tiêu & truy vấn ========== - daily_goal: Optional[Dict[str, float]] - meal_plan: Optional[Dict[str, Any]] - meal_plan_day: Optional[Dict[str, Any]] + # ========== Mục tiêu & truy vấn ========== + candidate_pool: List[dict] + selected_structure: List[dict] + reason: Optional[str] + final_menu: List[dict] food_old: Optional[Dict[str, Any]] - food_new_raw: Optional[Dict[str, Any]] - food_new: Optional[Dict[str, Any]] - - meal_old: Optional[List[Dict[str, Any]]] - meal_new_raw: Optional[List[Dict[str, Any]]] - meal_new: Optional[List[Dict[str, Any]]] +class SwapState(TypedDict): + user_profile: Dict[str, Any] + food_old: Dict[str, Any] + candidates: List[Dict[str, Any]] + top_candidates: List[Dict[str, Any]] + best_replacement: Dict[str, Any] -__all__ = ["AgentState"] +__all__ = ["AgentState", "SwapState"] diff --git a/chatbot/agents/tools/__pycache__/daily_meal_suggestion.cpython-310.pyc b/chatbot/agents/tools/__pycache__/daily_meal_suggestion.cpython-310.pyc index 2becd2b6b5b48cff122a763bc8fea4d707f12516..6fbd87035015337551012cea2c5f33fb306623b5 100644 Binary files a/chatbot/agents/tools/__pycache__/daily_meal_suggestion.cpython-310.pyc and b/chatbot/agents/tools/__pycache__/daily_meal_suggestion.cpython-310.pyc differ diff --git a/chatbot/agents/tools/__pycache__/food_retriever.cpython-310.pyc b/chatbot/agents/tools/__pycache__/food_retriever.cpython-310.pyc index 42ccbf42e052827e75cfebcd636efd01674872ec..a82540bd04e60527fc0f713e87f07b411e1956d8 100644 Binary files a/chatbot/agents/tools/__pycache__/food_retriever.cpython-310.pyc and b/chatbot/agents/tools/__pycache__/food_retriever.cpython-310.pyc differ diff --git a/chatbot/agents/tools/daily_meal_functions.py b/chatbot/agents/tools/daily_meal_functions.py deleted file mode 100644 index c1c5c8d4bcdeed656171090fc6a59d93a917a266..0000000000000000000000000000000000000000 --- a/chatbot/agents/tools/daily_meal_functions.py +++ /dev/null @@ -1,117 +0,0 @@ -import logging -from typing import List, Dict, Any - -from chatbot.agents.states.state import AgentState -from chatbot.agents.tools.food_retriever import query_constructor, food_retriever -from langgraph.graph import END, StateGraph -from chatbot.models.llm_setup import llm -from langchain.tools import tool -from chatbot.utils.user_profile import get_user_by_id - -# --- Cấu hình logging --- -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - -def get_user_profile(state: AgentState) -> Dict[str, Any]: - """ - Node: Lấy profile người dùng và chuẩn hóa key. - """ - logger.info("--- GET_USER_PROFILE ---") - user_id = state.get("user_id", "") - user_profile = get_user_by_id(user_id) - - # Chuẩn hóa khóa - mapping = {"fat": "lipid", "carbs": "carbohydrate", "protein": "protein", - "kcal": "kcal", "lipid": "lipid", "carbohydrate": "carbohydrate"} - normalized_profile = {mapping.get(k.lower(), k.lower()): v for k, v in user_profile.items()} - - # Fallback default nếu thiếu - defaults = {"kcal": 1700, "protein": 120, "lipid": 56, "carbohydrate": 170, "khẩu phần": "ăn chay"} - for key, val in defaults.items(): - normalized_profile.setdefault(key, val) - - logger.info(f"User profile chuẩn hóa: {normalized_profile}") - - return {"user_profile": normalized_profile, "daily_goal": normalized_profile, "suggested_meals": []} - -# --- Generate food plan --- -def generate_food_plan(state: AgentState) -> Dict[str, Any]: - logger.info("--- GENERATE_FOOD_PLAN ---") - meals_to_generate: List[str] = state.get("meals_to_generate", []) - user_profile: Dict[str, Any] = state.get("user_profile", {}) - - if not meals_to_generate: - logger.warning("meals_to_generate rỗng, sử dụng mặc định ['sáng', 'trưa', 'tối']") - meals_to_generate = ["sáng", "trưa", "tối"] - - meals_text = ", ".join(meals_to_generate) - - query_text = ( - 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')}. " - f"Ưu tiên món phổ biến, cân bằng dinh dưỡng, cho bữa {meals_text}." - ) - logger.info(f"Query: {query_text}") - - try: - foods = food_retriever.invoke(query_text) - except Exception as e: - logger.error(f"Lỗi khi truy vấn món ăn: {e}") - foods = [] - - suggested_meals = [food.metadata for food in foods] if foods else [] - logger.info(f"Số món được gợi ý: {len(suggested_meals)}") - - return {"suggested_meals": suggested_meals} - -def generate_meal_plan(state: AgentState): - logger.info("--- GENERATE_MEAL_PLAN ---") - user_profile = state.get("user_profile", {}) - suggested_meals = state.get("suggested_meals", []) - meals_to_generate = state.get("meals_to_generate", []) - question = state.get("question", "Hãy tạo thực đơn cho tôi.") - - meals_text = ", ".join(meals_to_generate) - - suggested_meals_text = "\n".join( - [f"- {meal['name']}: {meal.get('kcal', 0)} kcal, " - f"{meal.get('protein', 0)}g protein, " - f"{meal.get('lipid', 0)}g chất béo, " - f"{meal.get('carbohydrate', 0)}g carbohydrate" - for meal in suggested_meals] - ) if suggested_meals else "Chưa có món ăn gợi ý." - - prompt = f""" - 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: - - Tổng năng lượng mục tiêu: {user_profile['kcal']} kcal/ngày - - Protein: {user_profile['protein']}g - - Chất béo (lipid): {user_profile['lipid']}g - - Carbohydrate: {user_profile['carbohydrate']}g - - Chế độ ăn: {user_profile['khẩu phần']} - - Câu hỏi của người dùng: "{question}" - - Các bữa cần xây dựng: - {meals_text} - - Danh sách món ăn hiện có để chọn: - {suggested_meals_text} - - Yêu cầu: - 1. Hãy tổ hợp các món ăn trên để tạo thực đơn cho từng bữa (chỉ chọn trong danh sách có sẵn). - 2. Mỗi bữa gồm 1 đến 3 món, tổng năng lượng và dinh dưỡng xấp xỉ giá trị yêu cầu của bữa đó (±15%). - 3. Nếu cần, hãy ước tính khẩu phần mỗi món (ví dụ: 0.5 khẩu phần hoặc 1.2 khẩu phần) để đạt cân bằng chính xác. - 4. Đảm bảo tổng giá trị dinh dưỡng toàn ngày gần với hồ sơ người dùng. - 5. Chỉ chọn những món phù hợp với chế độ ăn: {user_profile['khẩu phần']}. - """ - - logger.info(prompt) - - try: - result = llm.invoke(prompt, timeout=60) - response_content = getattr(result, "content", str(result)) - except Exception as e: - logger.error(f"Lỗi khi gọi LLM: {e}") - response_content = "Không thể tạo thực đơn lúc này, vui lòng thử lại sau." - - logger.info("Meal plan suggestion generated.") - return {"response": response_content} \ No newline at end of file diff --git a/chatbot/agents/tools/daily_meal_suggestion.py b/chatbot/agents/tools/daily_meal_suggestion.py index b4f9dba509e16eefae3189e5b6a2b6eed2e4e367..5b95a82551a9e615cccd92beb028e907da7e2626 100644 --- a/chatbot/agents/tools/daily_meal_suggestion.py +++ b/chatbot/agents/tools/daily_meal_suggestion.py @@ -7,7 +7,7 @@ from langgraph.graph import END, StateGraph from chatbot.models.llm_setup import llm from langchain.tools import tool from chatbot.utils.user_profile import get_user_by_id -from chatbot.agents.graphs.meal_suggestion_graph import workflow_meal_suggestion +from chatbot.agents.graphs.meal_suggestion_graph import meal_plan_graph # --- Cấu hình logging --- logging.basicConfig(level=logging.INFO) @@ -31,14 +31,13 @@ def daily_meal_suggestion(user_id: str, question: str, meals_to_generate: list): logger.warning("meals_to_generate không phải list, ép về list") meals_to_generate = list(meals_to_generate) - workflow = workflow_meal_suggestion() + workflow = meal_plan_graph() - # 🧩 Truyền vào graph để xử lý - result = workflow .invoke({ + result = workflow.invoke({ "user_id": user_id, "question": question, "meals_to_generate": meals_to_generate, }) - return result["response"] + return result diff --git a/chatbot/agents/tools/food_retriever.py b/chatbot/agents/tools/food_retriever.py index 08c70b217941182eaf2e603ecc72b374c132d61d..fb0b97ff9f5f1f52780f52b626bb170cc183b454 100644 --- a/chatbot/agents/tools/food_retriever.py +++ b/chatbot/agents/tools/food_retriever.py @@ -1,8 +1,3 @@ -# ======================================== -# 🥗 FOOD RETRIEVER - ElasticSearch Retriever -# ======================================== - -import os from langchain.chains.query_constructor.base import ( AttributeInfo, get_query_constructor_prompt, @@ -13,7 +8,7 @@ from langchain_elasticsearch import ElasticsearchStore from langchain.retrievers.self_query.elasticsearch import ElasticsearchTranslator from langchain.retrievers.self_query.base import SelfQueryRetriever -from chatbot.models.embeddings import embeddings # Import embeddings đã khởi tạo sẵn +from chatbot.models.embeddings import embeddings from chatbot.models.llm_setup import llm from chatbot.config import ELASTIC_CLOUD_URL, ELASTIC_API_KEY @@ -334,7 +329,6 @@ docsearch = ElasticsearchStore( embedding=embeddings, ) - # ======================================== # 5️⃣ Tạo retrievers (nhiều cấu hình) # ======================================== @@ -348,15 +342,21 @@ food_retriever = SelfQueryRetriever( ) # Truy vấn ngắn gọn hơn, trả về top-3 kết quả -food_retriever_top3 = SelfQueryRetriever( +food_retriever_3 = SelfQueryRetriever( query_constructor=query_constructor, vectorstore=docsearch, structured_query_translator=ElasticsearchTranslator(), search_kwargs={"k": 3}, ) +food_retriever_50 = SelfQueryRetriever( + query_constructor=query_constructor, + vectorstore=docsearch, + structured_query_translator=ElasticsearchTranslator(), + search_kwargs={"k": 50}, +) # ======================================== # 6️⃣ EXPORT # ======================================== -__all__ = ["food_retriever", "food_retriever_top3", "docsearch", "query_constructor"] +__all__ = ["food_retriever", "food_retriever_3", "food_retriever_50", "docsearch", "query_constructor"] diff --git a/chatbot/agents/tools/info_app_retriever.py b/chatbot/agents/tools/info_app_retriever.py new file mode 100644 index 0000000000000000000000000000000000000000..8fa8b218886f3a7009e156b0dc94efcef60896bf --- /dev/null +++ b/chatbot/agents/tools/info_app_retriever.py @@ -0,0 +1,17 @@ +from langchain_elasticsearch import ElasticsearchStore + +from chatbot.models.embeddings import embeddings +from chatbot.config import ELASTIC_CLOUD_URL, ELASTIC_API_KEY + +policy_search = ElasticsearchStore( + es_url=ELASTIC_CLOUD_URL, + es_api_key=ELASTIC_API_KEY, + index_name="policy_vdb", + embedding=embeddings, +) + +policy_retriever = policy_search.as_retriever( + search_kwargs={"k": 3} +) + +__all__ = ["policy_retriever", "policy_search"] \ No newline at end of file diff --git a/chatbot/config.py b/chatbot/config.py index fa48621883fc53e8ffe8a6d0f4e2f8c20388eaa9..4981242a871d2a2781e607c7bff882daf70d98a1 100644 --- a/chatbot/config.py +++ b/chatbot/config.py @@ -7,3 +7,5 @@ DEEPSEEK_API_KEY = os.getenv('DEEPSEEK_API_KEY') PINECONE_API_KEY = os.getenv('PINECONE_API_KEY') ELASTIC_CLOUD_URL = os.getenv('ELASTIC_CLOUD_URL') ELASTIC_API_KEY = os.getenv('ELASTIC_API_KEY') + +API_BASE_URL=os.getenv('API_BASE_URL') diff --git a/chatbot/knowledge/disease.py b/chatbot/knowledge/disease.py new file mode 100644 index 0000000000000000000000000000000000000000..85451a32f4257cea0c71a073a08a623e1617b1be --- /dev/null +++ b/chatbot/knowledge/disease.py @@ -0,0 +1,25 @@ +nutrients = [ + "Protein","Saturated fat","Monounsaturated fat","Omega-3","Omega-6", + "Trans fat","Sugars","Tinh bột","Chất xơ","Vitamin A","Vitamin C", + "Vitamin D","Vitamin E","Vitamin K","Vitamin B6","Vitamin B12", + "Choline","Canxi","Sắt","Magie","Kẽm","Kali","Natri", + "Phốt pho","Caffeine","Alcohol","Cholesterol" +] + +disease_data = { + "Suy thận": ["Kiêng","Hạn chế","Hạn chế","","","","","","","Bổ sung","","Bổ sung","","","Hạn chế","Hạn chế","","Kiêng","Hạn chế"], + "Xơ gan, Viêm gan": ["Hạn chế","Hạn chế","Hạn chế","Bổ sung","","","","Bổ sung","Bổ sung","Bổ sung","Bổ sung","Bổ sung","Bổ sung","Bổ sung","Bổ sung","","Bổ sung","Bổ sung","Hạn chế","Hạn chế","","Kiêng","Kiêng"], + "Gout": ["Kiêng","Hạn chế","Hạn chế"] + [""]*18 + ["Hạn chế","Kiêng","Kiêng"], + "Sỏi thận": [""]*21 + ["Hạn chế"] + [""]*5, + "Suy dinh dưỡng": ["Bổ sung"]*24 + ["Hạn chế","Hạn chế","Hạn chế"], + "Bỏng nặng": ["Bổ sung"] + [""]*7 + ["Bổ sung",""] + ["Bổ sung","Bổ sung"] + [""]*8 + ["Hạn chế","Hạn chế",""], + "Thiếu máu thiếu sắt": [""]*17 + ["Bổ sung"] + [""]*4 + ["Hạn chế","Hạn chế","Hạn chế"], + "Bệnh tim mạch": ["Bổ sung","Hạn chế","Hạn chế"] + [""]*10 + ["Hạn chế"] + [""]*5 + ["Hạn chế"] + [""]*2 + ["Kiêng","Kiêng","Kiêng"], + "Tiểu đường": ["Bổ sung"] + [""]*5 + ["Kiêng","Kiêng"] + [""]*14 + ["Kiêng","Kiêng","Hạn chế"], + "Loãng xương": ["Bổ sung"] + [""]*21 + ["Hạn chế","Hạn chế","Hạn chế"], + "Phụ nữ mang thai": ["Bổ sung","","Bổ sung","Bổ sung","Hạn chế","Hạn chế","Bổ sung","Bổ sung","Bổ sung","Bổ sung","Bổ sung","Bổ sung","Bổ sung","Bổ sung","Bổ sung","Bổ sung","Bổ sung","Bổ sung","Bổ sung"] + [""]*2 + ["Kiêng","Kiêng","Hạn chế"], + "Viêm loét, trào ngược dạ dày": [""]*24 + ["Kiêng","Kiêng",""], + "Hội chứng ruột kích thích": [""]*7 + ["Hạn chế"] + [""]*15 + ["Kiêng","Kiêng","Hạn chế"], + "Viêm khớp": ["Hạn chế","Hạn chế","Hạn chế","","","Hạn chế"] + [""]*16 + ["Kiêng","Kiêng","Hạn chế"], + "Tăng huyết áp": ["","Hạn chế","Hạn chế","","Kiêng","Hạn chế"] + [""]*15 + ["Hạn chế","","Kiêng","Kiêng","Hạn chế"] +} \ No newline at end of file diff --git a/chatbot/main.py b/chatbot/main.py index 1ddc84f75764f1fd1662b105294d03315d6a540f..e6add4239860cba7b6aa61e235062edcfb960436 100644 --- a/chatbot/main.py +++ b/chatbot/main.py @@ -1,9 +1,8 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from chatbot.routes.chat_router import router as chat_router -from chatbot.routes.daily_plan_route import router as daily_plan_router +from chatbot.routes.meal_plan_route import router as meal_plan_router from chatbot.routes.food_replace_route import router as food_replace_router -from chatbot.routes.meal_replace_route import router as meal_replace_router app = FastAPI( title="AI Meal Chatbot API", @@ -22,9 +21,8 @@ app.add_middleware( # Đăng ký route app.include_router(chat_router) -app.include_router(daily_plan_router) +app.include_router(meal_plan_router) app.include_router(food_replace_router) -app.include_router(meal_replace_router) @app.get("/") def root(): diff --git a/chatbot/models/__pycache__/embeddings.cpython-310.pyc b/chatbot/models/__pycache__/embeddings.cpython-310.pyc index 86afe479b6211cd2599c34b5e4c0102e062e3afb..e1a11b9556aa668f5996be10dcafef4410156e5a 100644 Binary files a/chatbot/models/__pycache__/embeddings.cpython-310.pyc and b/chatbot/models/__pycache__/embeddings.cpython-310.pyc differ diff --git a/chatbot/models/__pycache__/llm_setup.cpython-310.pyc b/chatbot/models/__pycache__/llm_setup.cpython-310.pyc index c5fda492a5ef4047f69e5c772be6e13ab3b1bbc5..0f4c087e77cb65331f1fbcb10fca8a493be799f3 100644 Binary files a/chatbot/models/__pycache__/llm_setup.cpython-310.pyc and b/chatbot/models/__pycache__/llm_setup.cpython-310.pyc differ diff --git a/chatbot/models/llm_setup.py b/chatbot/models/llm_setup.py index 8f509d1627728e0f52b7d02a525888fc619c6d3d..d40fbf5250eab9513529c687fdbc085cc5f1bd7d 100644 --- a/chatbot/models/llm_setup.py +++ b/chatbot/models/llm_setup.py @@ -1,21 +1,9 @@ -""" -models/llm_setup.py -------------------- -Khởi tạo LLM DeepSeek cho toàn bộ hệ thống. -""" - from langchain_deepseek import ChatDeepSeek from chatbot.config import DEEPSEEK_API_KEY -# ======================================== -# 1️⃣ KIỂM TRA CẤU HÌNH -# ======================================== if not DEEPSEEK_API_KEY: raise ValueError("❌ Thiếu biến môi trường: DEEPSEEK_API_KEY trong file .env") -# ======================================== -# 2️⃣ KHỞI TẠO LLM CHÍNH -# ======================================== llm = ChatDeepSeek( model="deepseek-chat", temperature=0.3, @@ -24,13 +12,7 @@ llm = ChatDeepSeek( max_retries=2, ) -# ======================================== -# 3️⃣ TIỆN ÍCH KHỞI TẠO LLM KHÁC -# ======================================== def get_llm(model_name: str = "deepseek-chat", temperature: float = 0.3): - """ - Khởi tạo model LLM khác (ví dụ deepseek-coder). - """ return ChatDeepSeek( model=model_name, temperature=temperature, @@ -39,7 +21,4 @@ def get_llm(model_name: str = "deepseek-chat", temperature: float = 0.3): max_retries=2, ) -# ======================================== -# 4️⃣ EXPORT -# ======================================== __all__ = ["llm", "get_llm"] diff --git a/chatbot/routes/__pycache__/chat_router.cpython-310.pyc b/chatbot/routes/__pycache__/chat_router.cpython-310.pyc index d828462888010ff072f34361f07f50c2825d4559..e206c64f830e9927f59065f161503f6cbdd3a485 100644 Binary files a/chatbot/routes/__pycache__/chat_router.cpython-310.pyc and b/chatbot/routes/__pycache__/chat_router.cpython-310.pyc differ diff --git a/chatbot/routes/__pycache__/food_replace_route.cpython-310.pyc b/chatbot/routes/__pycache__/food_replace_route.cpython-310.pyc index cd5512720a1ab7627c23a1250fd1f90a12422156..7a399448b5b2a21aba18916b6f5abc996450490f 100644 Binary files a/chatbot/routes/__pycache__/food_replace_route.cpython-310.pyc and b/chatbot/routes/__pycache__/food_replace_route.cpython-310.pyc differ diff --git a/chatbot/routes/chat_router.py b/chatbot/routes/chat_router.py index 1382dcaa569a24b703457c8f855acb3b5964fb93..52f05bf6229aad47f1ff872c066f9f5188934e79 100644 --- a/chatbot/routes/chat_router.py +++ b/chatbot/routes/chat_router.py @@ -25,7 +25,7 @@ def chat(request: ChatRequest): # 1. Tạo state mới state = AgentState() state["user_id"] = request.user_id - state["messages"] = request.message or [HumanMessage(content="Tập luyện sức khỏe như nào cho tốt?")] + state["messages"] = [HumanMessage(content=request.message)] # 2. Lấy workflow chatbot graph = workflow_chatbot() diff --git a/chatbot/routes/food_replace_route.py b/chatbot/routes/food_replace_route.py index 55edb1581a6817cd39c2fb266fd243b423d31b60..5fc2b1cc233a2bbcf7e39976caa9e1571c085895 100644 --- a/chatbot/routes/food_replace_route.py +++ b/chatbot/routes/food_replace_route.py @@ -25,6 +25,7 @@ def chat(request: Request): state = AgentState() state["user_id"] = request.user_id state["food_old"] = request.food_old + state["food_old"]["solver_bounds"] = tuple(state["food_old"].get("solver_bounds")) # 2. Lấy workflow graph = food_similarity_graph() @@ -33,7 +34,7 @@ def chat(request: Request): result = graph.invoke(state) # 4. Trả response - response = result["food_new"] or "Không có kết quả" + response = result or "Không có kết quả" return {"response": response} except Exception as e: diff --git a/chatbot/routes/daily_plan_route.py b/chatbot/routes/meal_plan_route.py similarity index 74% rename from chatbot/routes/daily_plan_route.py rename to chatbot/routes/meal_plan_route.py index edf45916149bdf68b9cad844f7eb6598bb311bec..a965e4629a0d140636b9766ced73743e5deac5c7 100644 --- a/chatbot/routes/daily_plan_route.py +++ b/chatbot/routes/meal_plan_route.py @@ -1,39 +1,41 @@ -from fastapi import APIRouter, HTTPException -from pydantic import BaseModel -from langchain_core.messages import HumanMessage -from chatbot.agents.states.state import AgentState -from chatbot.agents.graphs.meal_suggestion_json_graph import workflow_meal_suggestion_json - -# --- Định nghĩa request body --- -class Request(BaseModel): - user_id: str - -# --- Tạo router --- -router = APIRouter( - prefix="/daily-plan", - tags=["Daily Plan"] -) - -# --- Route xử lý chat --- -@router.post("/") -def chat(request: Request): - try: - - print("Nhận được yêu cầu chat từ user:", request.user_id) - - # 1. Tạo state mới - state = AgentState() - state["user_id"] = request.user_id - - # 2. Lấy workflow - graph = workflow_meal_suggestion_json() - - # 3. Invoke workflow - result = graph.invoke(state) - - # 4. Trả response - response = result["meal_plan_day"] or "Không có kết quả" - return {"response": response} - - except Exception as e: - raise HTTPException(status_code=500, detail=f"Lỗi chatbot: {e}") +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel +from langchain_core.messages import HumanMessage +from chatbot.agents.states.state import AgentState +from chatbot.agents.graphs.meal_suggestion_graph import meal_plan_graph + +# --- Định nghĩa request body --- +class Request(BaseModel): + user_id: str + meals_to_generate: list + +# --- Tạo router --- +router = APIRouter( + prefix="/meal-plan", + tags=["Meal Plan"] +) + +# --- Route xử lý chat --- +@router.post("/") +def chat(request: Request): + try: + + print("Nhận được yêu cầu chat từ user:", request.user_id) + + # 1. Tạo state mới + state = AgentState() + state["user_id"] = request.user_id + state["meals_to_generate"] = request.meals_to_generate + + # 2. Lấy workflow + graph = meal_plan_graph() + + # 3. Invoke workflow + result = graph.invoke(state) + + # 4. Trả response + response = result or "Không có kết quả" + return {"response": response} + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Lỗi chatbot: {e}") diff --git a/chatbot/routes/meal_replace_route.py b/chatbot/routes/meal_replace_route.py deleted file mode 100644 index 3677ead4a6a274de21d764a233a30c151217da0b..0000000000000000000000000000000000000000 --- a/chatbot/routes/meal_replace_route.py +++ /dev/null @@ -1,40 +0,0 @@ -from fastapi import APIRouter, HTTPException -from pydantic import BaseModel -from langchain_core.messages import HumanMessage -from chatbot.agents.states.state import AgentState -from chatbot.agents.graphs.meal_similarity_graph import meal_similarity_graph - -# --- Định nghĩa request body --- -class Request(BaseModel): - user_id: str - meal_old: list - -# --- Tạo router --- -router = APIRouter( - prefix="/meal-replace", - tags=["Meal Replace"] -) - -@router.post("/") -def chat(request: Request): - try: - - print("Nhận được yêu cầu chat từ user:", request.user_id) - - # 1. Tạo state mới - state = AgentState() - state["user_id"] = request.user_id - state["meal_old"] = request.meal_old - - # 2. Lấy workflow - graph = meal_similarity_graph() - - # 3. Invoke workflow - result = graph.invoke(state) - - # 4. Trả response - response = result["meal_new"] or "Không có kết quả" - return {"response": response} - - except Exception as e: - raise HTTPException(status_code=500, detail=f"Lỗi chatbot: {e}") diff --git a/chatbot/utils/__pycache__/user_profile.cpython-310.pyc b/chatbot/utils/__pycache__/user_profile.cpython-310.pyc index b9950eba3b823d8d71bcfe3868152c926547ad40..41e5ca2003a35c26d5b5077ba4e9e4e8f7fd6710 100644 Binary files a/chatbot/utils/__pycache__/user_profile.cpython-310.pyc and b/chatbot/utils/__pycache__/user_profile.cpython-310.pyc differ diff --git a/chatbot/utils/format.py b/chatbot/utils/format.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/chatbot/utils/restriction.py b/chatbot/utils/restriction.py new file mode 100644 index 0000000000000000000000000000000000000000..981c37c9b95d8aba52338f5843f95fe1cca0129c --- /dev/null +++ b/chatbot/utils/restriction.py @@ -0,0 +1,14 @@ +from chatbot.knowledge.disease import disease_data, nutrients + +def get_restrictions(disease: str): + result = {"Kiêng": [], "Hạn chế": [], "Bổ sung": []} + if disease not in disease_data: + return result + + values = disease_data[disease] + + for nutrient, action in zip(nutrients, values): + if action in result: + result[action].append(nutrient) + + return result diff --git a/chatbot/utils/user_profile.py b/chatbot/utils/user_profile.py index ada30f772ba19f8cfff709cd422f8278d3a39054..0d1d897e5e69d4d458492fae6cb0fb04d941948c 100644 --- a/chatbot/utils/user_profile.py +++ b/chatbot/utils/user_profile.py @@ -1,27 +1,39 @@ import logging -from typing import Dict, Any +import requests +from chatbot.config import API_BASE_URL # --- Cấu hình logging --- logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # --- User profile --- -def get_user_by_id(user_id: str) -> Dict[str, Any]: - """ - Lấy thông tin người dùng từ DB hoặc hệ thống. - Nếu không tìm thấy, trả về default. - """ - if not user_id: - logger.warning("User ID trống, sử dụng profile mặc định") - return {} +def get_user_by_id(user_id: int): + url = f"{API_BASE_URL}/get_all_info?id={user_id}" - # Thử lấy từ DB (ở đây tạm hardcode) - user_profile = { - "kcal": 1700, - "protein": 120, - "lipid": 56, - "carbohydrate": 170, - "khẩu phần": "ăn chay" + user_profile = {'id': 1, 'fullname': 'An', 'age': 22, 'height': 12, 'weight': 42, 'activityLevel': 'Ít vận động', 'limitFood': 'Dị ứng sữa, Thuần chay','healthStatus': 'Không có', 'diet': 'Chế độ HighProtein', 'bmr': 583.73, 'tdee': 700.476, 'gender': 'male', + 'userinfoid': 1, 'targetcalories': 1033.8093, 'water': 1260.0, 'protein': 90.45831, 'totalfat': 22.973541, 'saturatedfat': 8.040739, 'monounsaturatedfat': 10.338094, 'polyunsaturatedfat': 4.594708, 'transfat': 0.0, + 'carbohydrate': 116.30355, 'carbs': 90.71677, 'sugar': 8.141249, 'fiber': 17.445532, 'cholesterol': 300.0, 'vitamina': 3000.0, 'vitamind': 15.0, 'vitaminc': 90.0, 'vitaminb6': 1.3, 'vitaminb12': 2.4, 'vitamine': 15.0, + 'vitamink': 120.0, 'choline': 550.0, 'canxi': 1000.0, 'fe': 8.0, 'magie': 400.0, 'photpho': 700.0, 'kali': 4700.0, 'natri': 2300.0, 'zn': 11.0, 'caffeine': 126.0, 'alcohol': 20.0 } - logger.info(f"Lấy profile cho user_id={user_id}: {user_profile}") + + try: + response = requests.get(url, timeout=10) + response.raise_for_status() + + result = response.json() + userInfo = result['userInfo'] + requiredIndex = result['requiredIndex'] + user_profile = {**userInfo, **requiredIndex} + + logger.info(f"Lấy profile cho user_id={user_id} tên {user_profile['fullname']}") + + return user_profile + + except requests.HTTPError as http_err: + logger.warning(f"HTTP error occurred: {http_err}") + except Exception as e: + logger.warning(f"Other error: {e}") + + logger.info(f"Sử dụng profile mặc định cho user_id={user_id}") + return user_profile \ No newline at end of file