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