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)