|
@@ -0,0 +1,135 @@
|
|
|
|
|
+import uuid
|
|
|
|
|
+import json
|
|
|
|
|
+import logging
|
|
|
|
|
+import re
|
|
|
|
|
+from fastapi import APIRouter
|
|
|
|
|
+from app.models.meal import (
|
|
|
|
|
+ FoodRecognizeRequest,
|
|
|
|
|
+ FoodRecognizeResponse,
|
|
|
|
|
+ FoodsItem,
|
|
|
|
|
+ MenuGenerateRequest,
|
|
|
|
|
+ MenuGenerateResponse,
|
|
|
|
|
+)
|
|
|
|
|
+from app.config import settings
|
|
|
|
|
+
|
|
|
|
|
+logger = logging.getLogger(__name__)
|
|
|
|
|
+router = APIRouter(prefix="/api/v1", tags=["meal"])
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@router.post("/food/recognize", response_model=FoodRecognizeResponse)
|
|
|
|
|
+async def recognize_food(req: FoodRecognizeRequest):
|
|
|
|
|
+ """食材识别: 上传图片 URL, 返回识别食材列表"""
|
|
|
|
|
+ trace_id = str(uuid.uuid4())
|
|
|
|
|
+ try:
|
|
|
|
|
+ from langchain_openai import ChatOpenAI
|
|
|
|
|
+ from langchain_core.messages import HumanMessage, SystemMessage
|
|
|
|
|
+
|
|
|
|
|
+ llm = ChatOpenAI(
|
|
|
|
|
+ model=settings.llm_model,
|
|
|
|
|
+ api_key=settings.llm_api_key,
|
|
|
|
|
+ base_url=settings.llm_base_url,
|
|
|
|
|
+ temperature=0,
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ prompt = f"""请识别以下图片中的主要食材(3-8种),按置信度排序。
|
|
|
|
|
+返回 JSON 数组格式,不要包含其他内容:
|
|
|
|
|
+[{{"name": "食材名称", "confidence": 0.95, "category": "蔬菜/水果/肉禽/水产/蛋奶/谷物/调味/其他"}}]
|
|
|
|
|
+
|
|
|
|
|
+图片链接: {req.image_url}
|
|
|
|
|
+要求: 只返回 JSON 数组,不要有任何其他文字。"""
|
|
|
|
|
+
|
|
|
|
|
+ response = llm.invoke([
|
|
|
|
|
+ SystemMessage(content="你是一个专业的食材识别助手。"),
|
|
|
|
|
+ HumanMessage(content=prompt),
|
|
|
|
|
+ ])
|
|
|
|
|
+
|
|
|
|
|
+ text = response.content.strip()
|
|
|
|
|
+ try:
|
|
|
|
|
+ foods = json.loads(text)
|
|
|
|
|
+ except json.JSONDecodeError:
|
|
|
|
|
+ match = re.search(r'\[[\s\S]*\]', text)
|
|
|
|
|
+ foods = json.loads(match.group()) if match else []
|
|
|
|
|
+
|
|
|
|
|
+ foods_list = []
|
|
|
|
|
+ for item in foods:
|
|
|
|
|
+ if isinstance(item, dict):
|
|
|
|
|
+ foods_list.append(FoodsItem(
|
|
|
|
|
+ name=item.get("name", "未知"),
|
|
|
|
|
+ confidence=float(item.get("confidence", 0.5)),
|
|
|
|
|
+ category=item.get("category", "other"),
|
|
|
|
|
+ ))
|
|
|
|
|
+
|
|
|
|
|
+ return FoodRecognizeResponse(foods=foods_list, raw_response=text, trace_id=trace_id)
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ logger.error("食材识别失败: %s", e, exc_info=True)
|
|
|
|
|
+ return FoodRecognizeResponse(foods=[], trace_id=trace_id)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@router.post("/menu/generate", response_model=MenuGenerateResponse)
|
|
|
|
|
+async def generate_menu(req: MenuGenerateRequest):
|
|
|
|
|
+ """菜单生成: 根据食材和用餐人数生成一日三餐菜单"""
|
|
|
|
|
+ trace_id = str(uuid.uuid4())
|
|
|
|
|
+ try:
|
|
|
|
|
+ from langchain_openai import ChatOpenAI
|
|
|
|
|
+ from langchain_core.messages import HumanMessage, SystemMessage
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ selected_foods = json.loads(req.selected_foods) if req.selected_foods else []
|
|
|
|
|
+ except json.JSONDecodeError:
|
|
|
|
|
+ selected_foods = []
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ participants = json.loads(req.participants) if req.participants else []
|
|
|
|
|
+ except json.JSONDecodeError:
|
|
|
|
|
+ participants = []
|
|
|
|
|
+
|
|
|
|
|
+ participant_count = len(participants) if participants else 1
|
|
|
|
|
+ foods_text = ", ".join([f.get("name", "") for f in selected_foods]) if selected_foods else "根据可用食材"
|
|
|
|
|
+
|
|
|
|
|
+ constraints = []
|
|
|
|
|
+ if req.allergies:
|
|
|
|
|
+ constraints.append(f"禁忌: {req.allergies}")
|
|
|
|
|
+ if req.health_goals:
|
|
|
|
|
+ constraints.append(f"健康目标: {req.health_goals}")
|
|
|
|
|
+ if req.cuisine_pref:
|
|
|
|
|
+ constraints.append(f"菜系偏好: {req.cuisine_pref}")
|
|
|
|
|
+ if req.spice_level is not None:
|
|
|
|
|
+ constraints.append(f"辣度: {req.spice_level}/5")
|
|
|
|
|
+ constraints_text = "\n".join(constraints) if constraints else "无特殊限制"
|
|
|
|
|
+
|
|
|
|
|
+ prompt = f"""请为{participant_count}人生成{req.date}的一日三餐菜单。
|
|
|
|
|
+
|
|
|
|
|
+可用食材: {foods_text}
|
|
|
|
|
+用餐人数: {participant_count}人
|
|
|
|
|
+{constraints_text}
|
|
|
|
|
+
|
|
|
|
|
+请返回 JSON 格式:
|
|
|
|
|
+{{"meals": [{{"type": "breakfast", "name": "早餐", "dishes": [{{"name": "菜品名", "ingredients": [{{"name": "食材", "grams": 100}}], "cooking_method": "烹饪方法", "nutrition": {{"calories": 200, "protein": 10, "carbs": 30, "fat": 5}}, "notes": "备注"}}]}}]}}
|
|
|
|
|
+
|
|
|
|
|
+要求:
|
|
|
|
|
+1. 早/午/晚各至少1-2道菜
|
|
|
|
|
+2. 食材用量按{participant_count}人份计算
|
|
|
|
|
+3. 营养均衡,考虑健康目标
|
|
|
|
|
+4. 只用提供的食材
|
|
|
|
|
+5. 只返回 JSON,不要其他文字"""
|
|
|
|
|
+
|
|
|
|
|
+ llm = ChatOpenAI(
|
|
|
|
|
+ model=settings.llm_model,
|
|
|
|
|
+ api_key=settings.llm_api_key,
|
|
|
|
|
+ base_url=settings.llm_base_url,
|
|
|
|
|
+ temperature=0.7,
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ response = llm.invoke([
|
|
|
|
|
+ SystemMessage(content="你是一个专业营养师和厨师,擅长根据食材和健康目标设计食谱。"),
|
|
|
|
|
+ HumanMessage(content=prompt),
|
|
|
|
|
+ ])
|
|
|
|
|
+
|
|
|
|
|
+ text = response.content.strip()
|
|
|
|
|
+ match = re.search(r'\{[\s\S]*\}', text)
|
|
|
|
|
+ menu_json = match.group() if match else '{"meals": []}'
|
|
|
|
|
+
|
|
|
|
|
+ return MenuGenerateResponse(menu_json=menu_json, trace_id=trace_id)
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ logger.error("菜单生成失败: %s", e, exc_info=True)
|
|
|
|
|
+ return MenuGenerateResponse(menu_json='{"meals": []}', trace_id=trace_id)
|