From cb168f6d8887fe079597a7da84948ffaee489da8 Mon Sep 17 00:00:00 2001 From: wsx676 <71129835+wsx676@users.noreply.github.com> Date: Tue, 11 Nov 2025 12:01:05 +0800 Subject: [PATCH] Add files via upload --- food_cal_README.md | 77 ++++++++++ food_calorie_agent.py | 350 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 427 insertions(+) create mode 100644 food_cal_README.md create mode 100644 food_calorie_agent.py diff --git a/food_cal_README.md b/food_cal_README.md new file mode 100644 index 00000000..fc0bb0a4 --- /dev/null +++ b/food_cal_README.md @@ -0,0 +1,77 @@ + 智能食物热量计算与营养分析助手 + + 1. 简介 + +“智能食物热量计算与营养分析助手”是一个高级多智能体(Multi-Agent System, MAS)应用,旨在帮助用户轻松追踪日常饮食的营养信息。用户只需通过自然语言输入所食用的食物及其份量,系统便能自动解析、计算并提供详细的营养报告和个性化的饮食建议。 + +该系统不仅能识别用户输入的食物,还能处理常见的重量单位和量词(如“一个”、“一碗”),并能通过网络搜索功能查询本地数据库中不存在的食物,极大地提升了实用性和准确性。 + + 2. 核心功能 + + 2.1 自然语言理解与食物解析 + +- 智能文本解析:系统能够理解并解析用户的自然语言输入,例如“我早餐吃了一个苹果和200克面包”,并将其准确转换为结构化的食物列表(`[{'name': '苹果', 'weight': 150}, {'name': '面包', 'weight': 200}]`)。 +- 智能重量估算:当用户使用“一个”、“一碗”等量词时,系统会根据内置的知识库估算一个合理的重量,使输入更加便捷。 + + 2.2 动态食物营养数据库 + +- 本地数据库:系统内置了一个包含常见食物营养信息的本地数据库(`FOOD_NUTRITION_DB`),涵盖了卡路里、蛋白质、脂肪、碳水化合物和膳食纤维等关键指标。 +- 实时网络查询:当用户输入的食物在本地数据库中不存在时,系统会自动调用网络搜索工具,从互联网上获取最新的营养信息,并将其动态添加到本地数据库中,以备后续使用。 + + 2.3 营养成分计算 + +- 精准计算:根据解析出的食物列表和对应的重量,系统会精确计算总热量以及各种营养成分(蛋白质、脂肪、碳水化合物、膳食纤维)的总量。 +- 结构化输出:计算结果以清晰的JSON格式呈现,包含了每种食物的详细营养信息以及汇总数据。 + + 2.4 个性化饮食建议 + +- 智能分析:系统会根据计算出的总营养数据,评估当前饮食的热量水平、蛋白质、脂肪、碳水化合物和膳食纤维的摄入情况。 +- 提供建议:基于分析结果,系统会生成个性化的饮食建议,例如: + - “这顿饭的热量较高,建议减少食用量或选择低热量的替代食品。” + - “蛋白质摄入偏低,建议增加瘦肉、鱼、蛋、豆类等富含蛋白质的食物。” + - “膳食纤维摄入不足,建议增加蔬菜、水果、全谷物的摄入。” + + 3. 技术实现 + +本系统基于`OxyGent`框架构建,采用先进的多智能体协作架构,主要包括以下几个核心组件: + +- 主智能体 (Master Agent):作为工作流的协调者,负责接收用户请求,并依次调用其他子智能体和工具来完成任务。它采用`WorkflowAgent`实现,通过`master_workflow`函数编排整个处理流程。 + +- 食物解析智能体 (Food Parsing Agent):一个专门用于解析用户输入的`ChatAgent`。它被配置为一个“JSON生成器”,专注于从文本中提取食物名称和重量,并以结构化的JSON格式输出。 + +- 食物营养搜索智能体 (Food Nutrition Search Agent):一个`ReActAgent`,当遇到未知食物时,它会使用`web_search`工具在网上搜索该食物的营养成分,并将结果返回给主智能体。 + +- 工具集 (Tools): + - `calorie_calculation_tools`:用于计算食物总热量和营养成分的函数工具。 + - `nutrition_advice_tools`:根据营养数据提供饮食建议的函数工具。 + - `web_search_tools`:用于在网络上搜索食物营养信息的工具。 + +- 工作流程: + 1. 接收输入:用户通过Web界面输入自然语言描述。 + 2. 食物解析:`master_agent`调用`food_parsing_agent`解析输入,提取食物列表。 + 3. 数据补充:如果存在未知食物,`master_agent`调用`food_nutrition_search_agent`进行网络搜索,并更新本地数据库。 + 4. 热量计算:`master_agent`调用`calculate_calories`工具计算营养成分。 + 5. 生成建议:`master_agent`调用`provide_nutrition_advice`工具生成饮食建议。 + 6. 返回结果:`master_agent`整合所有信息,以JSON格式返回最终的营养分析报告和饮食建议。 + + 4. 使用方法 + +1. 启动系统:运行`food_calorie_agent.py`脚本,系统将启动一个Web服务。 + ```bash + python food_calorie_agent.py + ``` +2. 访问Web界面:在浏览器中打开提供的URL(通常是`http://localhost:8000`)。 +3. 输入查询:在聊天界面中,输入您想要查询的食物和份量。例如: + - “一个苹果和200克的土豆炖牛肉和一盘番茄炒蛋” + - “我午餐吃了300克烤鸡胸肉和一碗西兰花” +4. 获取结果:系统将立即处理您的请求,并在界面上显示详细的营养分析报告和饮食建议。 + + 5. 未来展望 + +- 支持图片识别:增加通过上传食物图片自动识别菜品和估算份量的功能。 +- 集成更广泛的数据库:对接专业的第三方食物营养数据库API,提供更全面、更权威的数据。 +- 长期饮食追踪:增加用户账户系统,记录用户的历史饮食数据,并提供长期的健康分析和趋势报告。 +- 多语言支持:扩展自然语言处理能力,支持更多语言的输入。 + + + \ No newline at end of file diff --git a/food_calorie_agent.py b/food_calorie_agent.py new file mode 100644 index 00000000..00740638 --- /dev/null +++ b/food_calorie_agent.py @@ -0,0 +1,350 @@ +import asyncio +import os +import json +import base64 +from io import BytesIO +from typing import List, Dict, Any, Optional +from pydantic import Field, BaseModel +from PIL import Image +import logging +from oxygent import MAS, oxy, OxyRequest +from oxygent.utils.common_utils import image_to_base64 +import time +import re +from zai import ZhipuAiClient +from test_unstructured.staging.test_label_box import attachment + + + + +# 定义食物营养数据库 +FOOD_NUTRITION_DB = { + "苹果": {"calories_per_100g": 52, "protein": 0.3, "fat": 0.2, "carbs": 13.8, "fiber": 2.4}, + "香蕉": {"calories_per_100g": 89, "protein": 1.1, "fat": 0.3, "carbs": 22.8, "fiber": 2.6}, + "橙子": {"calories_per_100g": 47, "protein": 0.9, "fat": 0.1, "carbs": 11.8, "fiber": 2.4}, + "草莓": {"calories_per_100g": 32, "protein": 0.7, "fat": 0.3, "carbs": 7.7, "fiber": 2.0}, + "西瓜": {"calories_per_100g": 30, "protein": 0.6, "fat": 0.2, "carbs": 7.6, "fiber": 0.4}, + "面包": {"calories_per_100g": 265, "protein": 9.0, "fat": 3.2, "carbs": 49.0, "fiber": 2.7}, + "米饭": {"calories_per_100g": 130, "protein": 2.7, "fat": 0.3, "carbs": 28.0, "fiber": 0.4}, + "面条": {"calories_per_100g": 138, "protein": 5.0, "fat": 2.0, "carbs": 25.0, "fiber": 1.2}, + "牛肉": {"calories_per_100g": 250, "protein": 26.0, "fat": 17.0, "carbs": 0.0, "fiber": 0.0}, + "鸡肉": {"calories_per_100g": 165, "protein": 31.0, "fat": 3.6, "carbs": 0.0, "fiber": 0.0}, + "鱼": {"calories_per_100g": 206, "protein": 22.0, "fat": 12.0, "carbs": 0.0, "fiber": 0.0}, + "鸡蛋": {"calories_per_100g": 155, "protein": 12.6, "fat": 11.0, "carbs": 1.1, "fiber": 0.0}, + "牛奶": {"calories_per_100g": 42, "protein": 3.4, "fat": 1.0, "carbs": 5.0, "fiber": 0.0}, + "奶酪": {"calories_per_100g": 402, "protein": 25.0, "fat": 33.0, "carbs": 1.3, "fiber": 0.0}, + "酸奶": {"calories_per_100g": 59, "protein": 3.5, "fat": 3.3, "carbs": 4.7, "fiber": 0.0}, + "土豆": {"calories_per_100g": 77, "protein": 2.0, "fat": 0.1, "carbs": 17.0, "fiber": 2.2}, + "胡萝卜": {"calories_per_100g": 41, "protein": 0.9, "fat": 0.2, "carbs": 9.6, "fiber": 2.8}, + "西兰花": {"calories_per_100g": 34, "protein": 2.8, "fat": 0.4, "carbs": 6.6, "fiber": 2.6}, + "菠菜": {"calories_per_100g": 23, "protein": 2.9, "fat": 0.4, "carbs": 3.6, "fiber": 2.2}, + "番茄": {"calories_per_100g": 18, "protein": 0.9, "fat": 0.2, "carbs": 3.9, "fiber": 1.2}, +} + +web_search_tools = oxy.FunctionHub(name="web_search_tools") + +@web_search_tools.tool(description="网络搜索食物热量和营养成分") +async def web_search(food_name: str = Field(description="需要查询营养成分的食物名称")) -> str: + print(f"[Web Search MCP] Received request for: {food_name}") + + client = ZhipuAiClient(api_key=os.getenv("DEFAULT_LLM_API_KEY")) + + response = client.web_search.web_search( + search_engine="search_pro", + search_query="搜索" + food_name + "的营养,包括每百克热量,蛋白质、脂肪、碳水化合物和纤维素", + count=3, # 返回结果的条数,范围1-50,默认10 + search_recency_filter="noLimit", # 搜索指定日期范围内的内容 + content_size="medium" # 控制网页摘要的字数,默认medium + ) + + return response.search_result + +calorie_calculation_tools = oxy.FunctionHub(name="calorie_calculation_tools") +@calorie_calculation_tools.tool(description="计算食物热量和营养成分") +async def calculate_calories(food_items: List[Dict[str, Any]] = Field(description="食物列表,包含名称和重量")): + """计算食物的热量和营养成分""" + try: + total_calories = 0 + total_protein = 0 + total_fat = 0 + total_carbs = 0 + total_fiber = 0 + results = [] + + for food in food_items: + food_name = food["name"] + weight = food["weight"] + + if food_name in FOOD_NUTRITION_DB: + data = FOOD_NUTRITION_DB[food_name] + calories = (data["calories_per_100g"] * weight) / 100 + protein = (data["protein"] * weight) / 100 + fat = (data["fat"] * weight) / 100 + carbs = (data["carbs"] * weight) / 100 + fiber = (data["fiber"] * weight) / 100 if "fiber" in data else 0 + + total_calories += calories + total_protein += protein + total_fat += fat + total_carbs += carbs + total_fiber += fiber + + results.append({ + "name": food_name, + "weight": weight, + "calories": round(calories, 2), + "protein": round(protein, 2), + "fat": round(fat, 2), + "carbs": round(carbs, 2), + "fiber": round(fiber, 2) + }) + else: + # 如果食物不在数据库中,使用默认值或跳过 + results.append({ + "name": food_name, + "weight": weight, + "calories": "未知", + "protein": "未知", + "fat": "未知", + "carbs": "未知", + "fiber": "未知", + "note": "该食物在数据库中不存在" + }) + + return { + "food_items": results, + "total_calories": round(total_calories, 2), + "total_protein": round(total_protein, 2), + "total_fat": round(total_fat, 2), + "total_carbs": round(total_carbs, 2), + "total_fiber": round(total_fiber, 2), + "summary": f"总共检测到 {len(results)} 种食物,总热量为 {round(total_calories, 2)} 千卡" + } + except Exception as e: + return { + "status": "error", + "message": f"热量计算失败: {str(e)}" + } + + +class FoodItem(BaseModel): + """Represents a single food item with its name and weight.""" + name: str = Field(description="食物的名称,例如:'米饭', '土豆炖牛肉'。") + weight: float = Field(description="食物的重量,单位是克(g)。如果用户输入了'公斤'或'kg',请转换为克。如果用户使用了量词如'一个'、'一碗',请估算一个合理的重量。例如:一个苹果约150克,一碗米饭约200克。") + + +# 创建营养建议工具 +nutrition_advice_tools = oxy.FunctionHub(name="nutrition_advice_tools") + + +@nutrition_advice_tools.tool(description="根据食物热量和营养成分提供饮食建议") +async def provide_nutrition_advice(nutrition_data: Dict[str, Any] = Field(description="食物的营养数据")): + """根据食物热量和营养成分提供饮食建议""" + try: + total_calories = nutrition_data.get("total_calories", 0) + total_protein = nutrition_data.get("total_protein", 0) + total_fat = nutrition_data.get("total_fat", 0) + total_carbs = nutrition_data.get("total_carbs", 0) + total_fiber = nutrition_data.get("total_fiber", 0) + + # 根据营养成分提供建议 + advice = [] + + if total_calories > 800: + advice.append("这顿饭的热量较高,建议减少食用量或选择低热量的替代食品。") + elif total_calories < 300: + advice.append("这顿饭的热量较低,可能不足以满足身体需求,建议适当增加食物摄入。") + else: + advice.append("这顿饭的热量适中,符合一般成人单餐热量需求。") + + + if total_protein < 15: + advice.append("蛋白质摄入偏低,建议增加瘦肉、鱼、蛋、豆类等富含蛋白质的食物。") + else: + advice.append("蛋白质摄入充足,有助于维持肌肉健康。") + + + if total_fat > 30: + advice.append("脂肪摄入偏高,建议减少油脂类食物的摄入,选择低脂烹饪方式。") + + + if total_carbs > 100: + advice.append("碳水化合物摄入较多,建议控制主食量,增加蔬菜摄入。") + + + if total_fiber < 5: + advice.append("膳食纤维摄入不足,建议增加蔬菜、水果、全谷物的摄入。") + + return { + "advice": advice, + "summary": "\n".join(advice) + } + except Exception as e: + return { + "status": "error", + "message": f"提供营养建议失败: {str(e)}" + } + +async def master_workflow(oxy_request: OxyRequest): + query = oxy_request.get_query() + + # 调用 food_parsing_agent 解析食物 + food_parsing_response = await oxy_request.call( + callee='food_parsing_agent', + arguments={'query':"你是一个JSON生成器。你的唯一任务是从用户输入中提取食物信息并以JSON格式输出。" + "**绝对不要**输出任何JSON以外的文本、解释或注释。只返回JSON对象。" + "JSON对象必须包含一个 'food_items' 键,其值为一个食物对象列表。" + "每个食物对象都必须包含 'name' (字符串) 和 'weight' (数字, 单位为克) 两个字段。" + "如果用户使用了'一个'、'一碗'等量词,请估算一个合理的重量(例如:一个苹果约150克,一碗米饭约200克)。" + "如果用户没有提供明确的食物信息,请返回一个空的 'food_items' 列表。" + "示例输入: '我早餐吃了一个苹果和200克面包'" + "示例输出: {\"food_items\": [{\"name\": \"苹果\", \"weight\": 150}, {\"name\": \"面包\", \"weight\": 200}]}" + "示例输入: '今天天气怎么样'" + "示例输出: {\"food_items\": []}" + "现在,请处理以下用户输入:" + query} + ) + + # 从子智能体的响应中获取输出 + food_parsing_result_str = food_parsing_response.output + + try: + try: + food_parsing_result = json.loads(food_parsing_result_str) + except json.JSONDecodeError: + match = re.search(r'\{.*\}', food_parsing_result_str, re.DOTALL) + if not match: + return f"解析食物信息失败,无法从子智能体返回中找到有效的JSON内容。返回内容:{food_parsing_result_str}" + json_str = match.group(0) + food_parsing_result = json.loads(json_str) + + except (json.JSONDecodeError, TypeError): + return f"解析食物信息失败,无法解析子智能体返回的JSON字符串。返回内容:{food_parsing_result_str}" + + food_items = food_parsing_result.get('food_items', []) + + if not food_items: + return "无法从您的描述中解析出有效的食物信息,请提供更详细的描述。" + + unknown_foods = [item for item in food_items if item['name'] not in FOOD_NUTRITION_DB] + + if unknown_foods: + # 对未知食物进行网络搜索 + for food_item in unknown_foods: + food_name = food_item['name'] + # 调用新的搜索智能体 + search_response = await oxy_request.call( + callee='food_nutrition_search_agent', + arguments={'query':"你是一个JSON生成器。你的唯一任务是使用工具在网上查找食物的营养成分并将结果以JSON格式输出,并且无需输出其他内容。" + "你的任务是根据提供的食物名称,通过网络搜索工具返回的结果找到它每100克的营养成分," + "包括热量(calories_per_100g)、蛋白质(protein)、脂肪(fat)、碳水化合物(carbs)和纤维素(fiber)。" + "请以JSON格式返回结果。" + "例如,输入'牛油果',应返回类似 `{\"calories_per_100g\": 160, \"protein\": 2.0, \"fat\": 15.0, \"carbs\": 9.0, \"fiber\": 1.5}` 的JSON对象。" + "如果有营养成分缺失的情况,则以默认值0.0来代替。" + "现在,请返回以下食物的营养成分:" + food_name}, + + ) + + try: + # 解析返回的营养信息JSON + nutrition_data = json.loads(search_response.output) + # 验证数据并动态更新到FOOD_NUTRITION_DB + if all(k in nutrition_data for k in ['calories_per_100g', 'protein', 'fat', 'carbs']): + FOOD_NUTRITION_DB[food_name] = nutrition_data + else: + # 如果搜索结果不完整,可以记录日志或跳过 + print(f"Warning: Incomplete nutrition data for {food_name} from web search.") + except (json.JSONDecodeError, TypeError): + print(f"Warning: Failed to parse nutrition data for {food_name} from web search.") + + # 调用 calorie_calculation_agent 计算热量 + calories_response = await oxy_request.call( + callee='calculate_calories', + arguments={'food_items': food_items} + ) + nutrition_data = calories_response.output + + if not nutrition_data or 'total_calories' not in nutrition_data: + return f"计算食物热量失败。返回内容: {nutrition_data}" + + # 调用 provide_nutrition_advice 工具提供建议 + advice_response = await oxy_request.call( + callee='provide_nutrition_advice', + arguments={'nutrition_data': nutrition_data} + ) + advice_data = advice_response.output + + # 整合结果并返回 + final_response = { + "nutrition_analysis": nutrition_data, + "dietary_advice": advice_data + } + + return json.dumps(final_response, ensure_ascii=False, indent=2) + + +# Food Parsing Agent +food_parsing_agent = oxy.ChatAgent( + name="food_parsing_agent", + desc="一个JSON生成器。唯一任务是从用户输入中提取食物信息并以JSON格式输出", + llm_model="default_llm", + verbose=True +) + +# --- 注册网络搜索智能体 --- +food_nutrition_search_agent = oxy.ReActAgent( + name="food_nutrition_search_agent", + desc="一个专门负责通过网络搜索获取特定食物营养成分的智能体。", + llm_model="free_llm", + max_react_rounds=1, + tools=["web_search_tools"] +) + +#Master Agent (主智能体) +master_agent = oxy.WorkflowAgent( + name="master_agent", + is_master=True, + sub_agents=["food_parsing_agent", "food_nutrition_search_agent"], + tools=["calorie_calculation_tools", "nutrition_advice_tools"], + func_workflow=master_workflow, + llm_model="default_llm", + verbose=True +) + +oxy_space = [ + oxy.HttpLLM( + name="default_llm", + api_key=os.getenv("DEFAULT_LLM_API_KEY"), + base_url=os.getenv("DEFAULT_LLM_BASE_URL"), + model_name=os.getenv("DEFAULT_LLM_MODEL_NAME"), + headers=lambda _: {"Content-Type": "application/json"}, + ), + oxy.HttpLLM( + name="free_llm", + api_key=os.getenv("DEFAULT_LLM_API_KEY"), + base_url=os.getenv("DEFAULT_LLM_BASE_URL"), + model_name=os.getenv("DEFAULT_LLM_MODEL_NAME"), + headers=lambda _: {"Content-Type": "application/json"}, + ), + calorie_calculation_tools, + nutrition_advice_tools, + food_parsing_agent, + food_nutrition_search_agent, + master_agent, + web_search_tools +] + + +async def main(): + """主函数""" + async with MAS(oxy_space=oxy_space) as mas: + # 启动Web服务,让用户通过聊天界面输入 + await mas.start_web_service( + first_query="一个苹果和200克的土豆炖牛肉和一盘番茄炒蛋", + welcome_message="欢迎使用食物热量计算智能体!请输入食物名称和重量,我将为您计算热量。", + ) + + +if __name__ == "__main__": + asyncio.run(main()) + +