Spaces:
Running
Running
| from langchain_core.pydantic_v1 import BaseModel, Field | |
| from chatbot.models.llm_setup import llm | |
| from chatbot.agents.states.state import SwapState | |
| import logging | |
| import time | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger(__name__) | |
| class ChefDecision(BaseModel): | |
| # Thay đổi tên trường cho rõ nghĩa | |
| selected_meal_id: int = Field(description="ID (meal_id) của món ăn được chọn từ danh sách") | |
| reason: str = Field(description="Lý do ẩm thực ngắn gọn") | |
| def llm_finalize_choice(state: SwapState): | |
| logger.info("---NODE: LLM FINAL SELECTION (BY REAL MEAL_ID)---") | |
| top_candidates = state["top_candidates"] | |
| food_old = state["food_old"] | |
| if not top_candidates: return {"best_replacement": None} | |
| # 1. Format danh sách hiển thị kèm Real ID | |
| options_text = "" | |
| for item in top_candidates: | |
| real_id = item.get("meal_id") | |
| options_text += ( | |
| f"ID [{real_id}] - {item['name']}\n" | |
| f" - Số liệu: {item['final_kcal']} Kcal | P:{item['final_protein']}g | L:{item['final_totalfat']}g | C:{item['final_carbs']}g\n" | |
| f" - Độ lệch (Loss): {item['optimization_loss']}\n" | |
| ) | |
| # 2. Prompt cập nhật | |
| system_prompt = f""" | |
| Bạn là Bếp trưởng. Người dùng muốn đổi món '{food_old.get('name')}'. | |
| Dưới đây là các ứng viên thay thế: | |
| {options_text} | |
| NHIỆM VỤ: | |
| 1. Chọn ra 1 món thay thế tốt nhất về mặt ẩm thực. | |
| 2. Trả về chính xác ID (số trong ngoặc vuông []) của món đó. | |
| """ | |
| # 3. Gọi LLM | |
| try: | |
| llm_structured = llm.with_structured_output(ChefDecision) | |
| time_start = time.time() | |
| decision = llm_structured.invoke(system_prompt) | |
| time_end = time.time() | |
| logger.info(f"⏱️ Thời gian LLM: {time_end - time_start:.2f} giây") | |
| target_id = decision.selected_meal_id | |
| except Exception as e: | |
| logger.info(f"⚠️ Lỗi LLM: {e}. Fallback về option đầu tiên.") | |
| # Fallback lấy ID của món đầu tiên | |
| target_id = top_candidates[0].get("meal_id") | |
| decision = ChefDecision(selected_meal_id=target_id, reason="Fallback do lỗi hệ thống.") | |
| # 4. Mapping lại bằng meal_id | |
| selected_full_candidate = None | |
| for item in top_candidates: | |
| if int(item.get("meal_id")) == int(target_id): | |
| selected_full_candidate = item | |
| break | |
| # Fallback an toàn | |
| if not selected_full_candidate: | |
| logger.info(f"⚠️ ID {target_id} không tồn tại trong list. Chọn món Top 1.") | |
| selected_full_candidate = top_candidates[0] | |
| # Bổ sung lý do | |
| selected_full_candidate["chef_reason"] = decision.reason | |
| #------------------------------------------------------------------- | |
| # --- PHẦN MỚI: IN BẢNG SO SÁNH (VISUAL COMPARISON) --- | |
| logger.info(f"✅ CHEF SELECTED: {selected_full_candidate['name']} (ID: {selected_full_candidate['meal_id']})") | |
| logger.info(f"📝 Lý do: {decision.reason}") | |
| # Lấy thông tin món cũ (đã scale ở menu gốc) | |
| old_kcal = float(food_old.get('final_kcal', food_old['kcal'])) | |
| old_pro = float(food_old.get('final_protein', food_old['protein'])) | |
| old_fat = float(food_old.get('final_totalfat', food_old['totalfat'])) | |
| old_carb = float(food_old.get('final_carbs', food_old['carbs'])) | |
| # Lấy thông tin món mới (đã re-scale bởi Scipy) | |
| new_kcal = selected_full_candidate['final_kcal'] | |
| new_pro = selected_full_candidate['final_protein'] | |
| new_fat = selected_full_candidate['final_totalfat'] | |
| new_carb = selected_full_candidate['final_carbs'] | |
| scale = selected_full_candidate['portion_scale'] | |
| # In bảng | |
| logger.info("\n 📊 BẢNG SO SÁNH THAY THẾ:") | |
| headers = ["Chỉ số", "Món Cũ (Gốc)", "Món Mới (Re-scale)", "Chênh lệch"] | |
| row_fmt = " | {:<10} | {:<15} | {:<20} | {:<12} |" | |
| logger.info(" " + "-"*68) | |
| logger.info(row_fmt.format(*headers)) | |
| logger.info(" " + "-"*68) | |
| def print_row(label, old_val, new_val, unit=""): | |
| diff = new_val - old_val | |
| diff_str = f"{diff:+.1f}" | |
| # Đánh dấu màu (Logic text) | |
| status = "✅" | |
| # Nếu lệch > 20% thì cảnh báo | |
| if old_val > 0 and abs(diff)/old_val > 0.2: status = "⚠️" | |
| logger.info(row_fmt.format( | |
| label, | |
| f"{old_val:.0f} {unit}", | |
| f"{new_val:.0f} {unit} (x{scale} suất)", | |
| f"{diff_str} {status}" | |
| )) | |
| print_row("Năng lượng", old_kcal, new_kcal, "Kcal") | |
| print_row("Protein", old_pro, new_pro, "g") | |
| print_row("TotalFat", old_fat, new_fat, "g") | |
| print_row("Carb", old_carb, new_carb, "g") | |
| logger.info(" " + "-"*68) | |
| logger.info(f"✅ Chef Selected: ID {selected_full_candidate['meal_id']} - {selected_full_candidate['name']}") | |
| return {"best_replacement": selected_full_candidate} |