meal.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. import uuid
  2. import json
  3. import logging
  4. import re
  5. from fastapi import APIRouter
  6. from app.models.meal import (
  7. FoodRecognizeRequest,
  8. FoodRecognizeResponse,
  9. FoodsItem,
  10. MenuGenerateRequest,
  11. MenuGenerateResponse,
  12. )
  13. from app.config import settings
  14. logger = logging.getLogger(__name__)
  15. router = APIRouter(prefix="/api/v1", tags=["meal"])
  16. @router.post("/food/recognize", response_model=FoodRecognizeResponse)
  17. async def recognize_food(req: FoodRecognizeRequest):
  18. """食材识别: 上传图片 URL, 返回识别食材列表"""
  19. trace_id = str(uuid.uuid4())
  20. try:
  21. from langchain_openai import ChatOpenAI
  22. from langchain_core.messages import HumanMessage, SystemMessage
  23. llm = ChatOpenAI(
  24. model=settings.llm_model,
  25. api_key=settings.llm_api_key,
  26. base_url=settings.llm_base_url,
  27. temperature=0,
  28. )
  29. prompt = f"""请识别以下图片中的主要食材(3-8种),按置信度排序。
  30. 返回 JSON 数组格式,不要包含其他内容:
  31. [{{"name": "食材名称", "confidence": 0.95, "category": "蔬菜/水果/肉禽/水产/蛋奶/谷物/调味/其他"}}]
  32. 图片链接: {req.image_url}
  33. 要求: 只返回 JSON 数组,不要有任何其他文字。"""
  34. response = llm.invoke([
  35. SystemMessage(content="你是一个专业的食材识别助手。"),
  36. HumanMessage(content=prompt),
  37. ])
  38. text = response.content.strip()
  39. try:
  40. foods = json.loads(text)
  41. except json.JSONDecodeError:
  42. match = re.search(r'\[[\s\S]*\]', text)
  43. foods = json.loads(match.group()) if match else []
  44. foods_list = []
  45. for item in foods:
  46. if isinstance(item, dict):
  47. foods_list.append(FoodsItem(
  48. name=item.get("name", "未知"),
  49. confidence=float(item.get("confidence", 0.5)),
  50. category=item.get("category", "other"),
  51. ))
  52. return FoodRecognizeResponse(foods=foods_list, raw_response=text, trace_id=trace_id)
  53. except Exception as e:
  54. logger.error("食材识别失败: %s", e, exc_info=True)
  55. return FoodRecognizeResponse(foods=[], trace_id=trace_id)
  56. @router.post("/menu/generate", response_model=MenuGenerateResponse)
  57. async def generate_menu(req: MenuGenerateRequest):
  58. """菜单生成: 根据食材和用餐人数生成一日三餐菜单"""
  59. trace_id = str(uuid.uuid4())
  60. try:
  61. from langchain_openai import ChatOpenAI
  62. from langchain_core.messages import HumanMessage, SystemMessage
  63. try:
  64. selected_foods = json.loads(req.selected_foods) if req.selected_foods else []
  65. except json.JSONDecodeError:
  66. selected_foods = []
  67. try:
  68. participants = json.loads(req.participants) if req.participants else []
  69. except json.JSONDecodeError:
  70. participants = []
  71. participant_count = len(participants) if participants else 1
  72. foods_text = ", ".join([f.get("name", "") for f in selected_foods]) if selected_foods else "根据可用食材"
  73. constraints = []
  74. if req.allergies:
  75. constraints.append(f"禁忌: {req.allergies}")
  76. if req.health_goals:
  77. constraints.append(f"健康目标: {req.health_goals}")
  78. if req.cuisine_pref:
  79. constraints.append(f"菜系偏好: {req.cuisine_pref}")
  80. if req.spice_level is not None:
  81. constraints.append(f"辣度: {req.spice_level}/5")
  82. constraints_text = "\n".join(constraints) if constraints else "无特殊限制"
  83. prompt = f"""请为{participant_count}人生成{req.date}的一日三餐菜单。
  84. 可用食材: {foods_text}
  85. 用餐人数: {participant_count}人
  86. {constraints_text}
  87. 请返回 JSON 格式:
  88. {{"meals": [{{"type": "breakfast", "name": "早餐", "dishes": [{{"name": "菜品名", "ingredients": [{{"name": "食材", "grams": 100}}], "cooking_method": "烹饪方法", "nutrition": {{"calories": 200, "protein": 10, "carbs": 30, "fat": 5}}, "notes": "备注"}}]}}]}}
  89. 要求:
  90. 1. 早/午/晚各至少1-2道菜
  91. 2. 食材用量按{participant_count}人份计算
  92. 3. 营养均衡,考虑健康目标
  93. 4. 只用提供的食材
  94. 5. 只返回 JSON,不要其他文字"""
  95. llm = ChatOpenAI(
  96. model=settings.llm_model,
  97. api_key=settings.llm_api_key,
  98. base_url=settings.llm_base_url,
  99. temperature=0.7,
  100. )
  101. response = llm.invoke([
  102. SystemMessage(content="你是一个专业营养师和厨师,擅长根据食材和健康目标设计食谱。"),
  103. HumanMessage(content=prompt),
  104. ])
  105. text = response.content.strip()
  106. match = re.search(r'\{[\s\S]*\}', text)
  107. menu_json = match.group() if match else '{"meals": []}'
  108. return MenuGenerateResponse(menu_json=menu_json, trace_id=trace_id)
  109. except Exception as e:
  110. logger.error("菜单生成失败: %s", e, exc_info=True)
  111. return MenuGenerateResponse(menu_json='{"meals": []}', trace_id=trace_id)